Skip to content

Commit

Permalink
Merge pull request Mirantis#357 from eromanova/validate-chains
Browse files Browse the repository at this point in the history
Validate TemplateChains
  • Loading branch information
Kshatrix authored Sep 23, 2024
2 parents 5192d13 + 297d502 commit 9da3c31
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 0 deletions.
2 changes: 2 additions & 0 deletions api/v1alpha1/templatechain_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ClusterTemplateChain struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Spec is immutable"
Spec TemplateChainSpec `json:"spec,omitempty"`
}

Expand All @@ -46,6 +47,7 @@ type ServiceTemplateChain struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Spec is immutable"
Spec TemplateChainSpec `json:"spec,omitempty"`
}

Expand Down
7 changes: 7 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ func main() {
SystemNamespace: currentNamespace,
}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "TemplateManagement")
}
if err := (&hmcwebhook.ClusterTemplateChainValidator{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ClusterTemplateChain")
os.Exit(1)
}
if err := (&hmcwebhook.ServiceTemplateChainValidator{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "ServiceTemplateChain")
os.Exit(1)
}
if err := (&hmcwebhook.ClusterTemplateValidator{}).SetupWebhookWithManager(mgr); err != nil {
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ var _ = BeforeSuite(func() {
err = (&hmcwebhook.TemplateManagementValidator{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&hmcwebhook.ClusterTemplateChainValidator{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&hmcwebhook.ServiceTemplateChainValidator{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&hmcwebhook.ClusterTemplateValidator{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

Expand Down
145 changes: 145 additions & 0 deletions internal/webhook/templatechain_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright 2024
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package webhook // nolint:dupl

import (
"context"
"errors"
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/Mirantis/hmc/api/v1alpha1"
)

var (
ErrInvalidTemplateChainSpec = errors.New("the template chain spec is invalid")
)

type ClusterTemplateChainValidator struct {
client.Client
}

func (in *ClusterTemplateChainValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
in.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ClusterTemplateChain{}).
WithValidator(in).
WithDefaulter(in).
Complete()
}

var (
_ webhook.CustomValidator = &ClusterTemplateChainValidator{}
_ webhook.CustomDefaulter = &ClusterTemplateChainValidator{}
)

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (*ClusterTemplateChainValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
chain, ok := obj.(*v1alpha1.ClusterTemplateChain)
if !ok {
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ClusterTemplateChain but got a %T", obj))
}

warnings := isTemplateChainValid(chain.Spec)
if len(warnings) > 0 {
return warnings, ErrInvalidTemplateChainSpec
}
return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (*ClusterTemplateChainValidator) ValidateUpdate(_ context.Context, _ runtime.Object, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (*ClusterTemplateChainValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (*ClusterTemplateChainValidator) Default(_ context.Context, _ runtime.Object) error {
return nil
}

type ServiceTemplateChainValidator struct {
client.Client
}

func (in *ServiceTemplateChainValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
in.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.ServiceTemplateChain{}).
WithValidator(in).
WithDefaulter(in).
Complete()
}

var (
_ webhook.CustomValidator = &ServiceTemplateChainValidator{}
_ webhook.CustomDefaulter = &ServiceTemplateChainValidator{}
)

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (*ServiceTemplateChainValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
chain, ok := obj.(*v1alpha1.ServiceTemplateChain)
if !ok {
return admission.Warnings{"Wrong object"}, apierrors.NewBadRequest(fmt.Sprintf("expected ServiceTemplateChain but got a %T", obj))
}
warnings := isTemplateChainValid(chain.Spec)
if len(warnings) > 0 {
return warnings, ErrInvalidTemplateChainSpec
}
return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (*ServiceTemplateChainValidator) ValidateUpdate(_ context.Context, _ runtime.Object, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (*ServiceTemplateChainValidator) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (*ServiceTemplateChainValidator) Default(_ context.Context, _ runtime.Object) error {
return nil
}

func isTemplateChainValid(spec v1alpha1.TemplateChainSpec) admission.Warnings {
supportedTemplates := make(map[string]bool)
availableForUpgrade := make(map[string]bool)
for _, supportedTemplate := range spec.SupportedTemplates {
supportedTemplates[supportedTemplate.Name] = true
for _, template := range supportedTemplate.AvailableUpgrades {
availableForUpgrade[template.Name] = true
}
}
warnings := admission.Warnings{}
for template := range availableForUpgrade {
if !supportedTemplates[template] {
warnings = append(warnings, fmt.Sprintf("template %s is allowed for upgrade but is not present in the list of spec.SupportedTemplates", template))
}
}
return warnings
}
90 changes: 90 additions & 0 deletions internal/webhook/templatechain_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2024
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package webhook

import (
"context"
"testing"

. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/Mirantis/hmc/api/v1alpha1"
tc "github.com/Mirantis/hmc/test/objects/templatechain"
"github.com/Mirantis/hmc/test/scheme"
)

func TestClusterTemplateChainValidateCreate(t *testing.T) {
g := NewWithT(t)

ctx := context.Background()

upgradeFromTemplateName := "template-1-0-1"
upgradeToTemplateName := "template-1-0-2"
supportedTemplates := []v1alpha1.SupportedTemplate{
{
Name: upgradeFromTemplateName,
AvailableUpgrades: []v1alpha1.AvailableUpgrade{
{
Name: upgradeToTemplateName,
},
},
},
}

tests := []struct {
name string
chain *v1alpha1.ClusterTemplateChain
existingObjects []runtime.Object
err string
warnings admission.Warnings
}{
{
name: "should fail if spec is invalid: incorrect supported templates",
chain: tc.NewClusterTemplateChain(tc.WithName("test"), tc.WithSupportedTemplates(supportedTemplates)),
warnings: admission.Warnings{
"template template-1-0-2 is allowed for upgrade but is not present in the list of spec.SupportedTemplates",
},
err: "the template chain spec is invalid",
},
{
name: "should succeed",
chain: tc.NewClusterTemplateChain(tc.WithName("test"), tc.WithSupportedTemplates(append(supportedTemplates, v1alpha1.SupportedTemplate{Name: upgradeToTemplateName}))),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithRuntimeObjects(tt.existingObjects...).Build()
validator := &ClusterTemplateChainValidator{Client: c}
warn, err := validator.ValidateCreate(ctx, tt.chain)
if tt.err != "" {
g.Expect(err).To(HaveOccurred())
if err.Error() != tt.err {
t.Fatalf("expected error '%s', got error: %s", tt.err, err.Error())
}
} else {
g.Expect(err).To(Succeed())
}
if len(tt.warnings) > 0 {
g.Expect(warn).To(Equal(tt.warnings))
} else {
g.Expect(warn).To(BeEmpty())
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: Spec is immutable
rule: self == oldSelf
type: object
served: true
storage: true
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ spec:
type: object
type: array
type: object
x-kubernetes-validations:
- message: Spec is immutable
rule: self == oldSelf
type: object
served: true
storage: true
42 changes: 42 additions & 0 deletions templates/provider/hmc/templates/webhooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,46 @@ webhooks:
resources:
- templatemanagements
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: {{ include "hmc.webhook.serviceName" . }}
namespace: {{ include "hmc.webhook.serviceNamespace" . }}
path: /validate-hmc-mirantis-com-v1alpha1-clustertemplatechain
failurePolicy: Fail
matchPolicy: Equivalent
name: validation.clustertemplatechain.hmc.mirantis.com
rules:
- apiGroups:
- hmc.mirantis.com
apiVersions:
- v1alpha1
operations:
- CREATE
resources:
- clustertemplatechains
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: {{ include "hmc.webhook.serviceName" . }}
namespace: {{ include "hmc.webhook.serviceNamespace" . }}
path: /validate-hmc-mirantis-com-v1alpha1-servicetemplatechain
failurePolicy: Fail
matchPolicy: Equivalent
name: validation.servicetemplatechain.hmc.mirantis.com
rules:
- apiGroups:
- hmc.mirantis.com
apiVersions:
- v1alpha1
operations:
- CREATE
resources:
- servicetemplatechains
sideEffects: None
{{- end }}

0 comments on commit 9da3c31

Please sign in to comment.