From ca434e90fcf6e73b19323a474f54fe4dcc1a7171 Mon Sep 17 00:00:00 2001 From: Ekaterina Kazakova Date: Thu, 8 Aug 2024 19:21:17 +0400 Subject: [PATCH] Validate Deployment objects Closes #141 --- internal/utils/util.go | 38 ++++++++ internal/webhook/deployment_webhook.go | 122 +++++++++++++++++++++---- 2 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 internal/utils/util.go diff --git a/internal/utils/util.go b/internal/utils/util.go new file mode 100644 index 000000000..35c60ea36 --- /dev/null +++ b/internal/utils/util.go @@ -0,0 +1,38 @@ +// 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 utils + +// SliceToMapKeys converts a given slice to a map with slice's values +// as the map's keys zeroing value for each. +func SliceToMapKeys[S ~[]K, M ~map[K]V, K comparable, V any](s S) M { + m := make(M) + for i := range s { + m[s[i]] = *new(V) + } + return m +} + +// DiffSliceSubset finds missing items of a given slice in a given map. +// If the slice is a subset of the map, returns empty slice. +// Boolean return argument indicates whether the slice is a subset. +func DiffSliceSubset[S ~[]K, M ~map[K]V, K comparable, V any](s S, m M) (diff S, isSubset bool) { + for _, v := range s { + if _, ok := m[v]; !ok { + diff = append(diff, v) + } + } + + return diff, len(diff) == 0 +} diff --git a/internal/webhook/deployment_webhook.go b/internal/webhook/deployment_webhook.go index 80e1df9e3..2c1334f06 100644 --- a/internal/webhook/deployment_webhook.go +++ b/internal/webhook/deployment_webhook.go @@ -16,9 +16,11 @@ package webhook // nolint:dupl import ( "context" + "errors" "fmt" "github.com/Mirantis/hmc/api/v1alpha1" + "github.com/Mirantis/hmc/internal/utils" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -33,12 +35,16 @@ type DeploymentValidator struct { client.Client } -func (in *DeploymentValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { - in.Client = mgr.GetClient() +var ( + InvalidDeploymentErr = errors.New("the deployment is invalid") +) + +func (v *DeploymentValidator) SetupWebhookWithManager(mgr ctrl.Manager) error { + v.Client = mgr.GetClient() return ctrl.NewWebhookManagedBy(mgr). For(&v1alpha1.Deployment{}). - WithValidator(in). - WithDefaulter(in). + WithValidator(v). + WithDefaulter(v). Complete() } @@ -48,12 +54,35 @@ var ( ) // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. -func (*DeploymentValidator) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) { +func (v *DeploymentValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + deployment, ok := obj.(*v1alpha1.Deployment) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected Deployment but got a %T", obj)) + } + template, err := v.getDeploymentTemplate(ctx, deployment.Spec.Template) + if err != nil { + return nil, fmt.Errorf("%s: %v", InvalidDeploymentErr, err) + } + err = v.isTemplateValid(ctx, template) + if err != nil { + return nil, fmt.Errorf("%s: %v", InvalidDeploymentErr, err) + } return nil, nil } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. -func (*DeploymentValidator) ValidateUpdate(_ context.Context, _ runtime.Object, _ runtime.Object) (admission.Warnings, error) { +func (v *DeploymentValidator) ValidateUpdate(_ context.Context, oldObj runtime.Object, newObj runtime.Object) (admission.Warnings, error) { + oldDeployment, ok := oldObj.(*v1alpha1.Deployment) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected Deployment but got a %T", oldObj)) + } + newDeployment, ok := newObj.(*v1alpha1.Deployment) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected Deployment but got a %T", newObj)) + } + if oldDeployment.Spec.Template != newDeployment.Spec.Template { + return nil, apierrors.NewBadRequest("spec.template field is immutable") + } return nil, nil } @@ -63,25 +92,86 @@ func (*DeploymentValidator) ValidateDelete(_ context.Context, _ runtime.Object) } // Default implements webhook.Defaulter so a webhook will be registered for the type. -func (in *DeploymentValidator) Default(ctx context.Context, obj runtime.Object) error { +func (v *DeploymentValidator) Default(ctx context.Context, obj runtime.Object) error { deployment, ok := obj.(*v1alpha1.Deployment) if !ok { return apierrors.NewBadRequest(fmt.Sprintf("expected Deployment but got a %T", obj)) } + + // Only apply defaults when there's no configuration provided + if deployment.Spec.Config != nil { + return nil + } + template, err := v.getDeploymentTemplate(ctx, deployment.Spec.Template) + if err != nil { + return fmt.Errorf("could not get template for the deployment: %s", err) + } + err = v.isTemplateValid(ctx, template) + if err != nil { + return fmt.Errorf("template is invalid: %s", err) + } + if template.Status.Config == nil { + return nil + } + deployment.Spec.DryRun = true + deployment.Spec.Config = &apiextensionsv1.JSON{Raw: template.Status.Config.Raw} + return nil +} + +func (v *DeploymentValidator) getDeploymentTemplate(ctx context.Context, templateName string) (*v1alpha1.Template, error) { template := &v1alpha1.Template{} - templateRef := types.NamespacedName{Name: deployment.Spec.Template, Namespace: v1alpha1.TemplatesNamespace} - if err := in.Get(ctx, templateRef, template); err != nil { + templateRef := types.NamespacedName{Name: templateName, Namespace: v1alpha1.TemplatesNamespace} + if err := v.Get(ctx, templateRef, template); err != nil { + return nil, err + } + return template, nil +} + +func (v *DeploymentValidator) isTemplateValid(ctx context.Context, template *v1alpha1.Template) error { + if template.Status.Type != v1alpha1.TemplateTypeDeployment { + return fmt.Errorf("the template should be of the deployment type. Current: %s", template.Status.Type) + } + if !template.Status.Valid { + return fmt.Errorf("the template is not valid: %s", template.Status.ValidationError) + } + err := v.verifyProviders(ctx, template) + if err != nil { + return fmt.Errorf("providers verification failed: %v", err) + } + return nil +} + +func (v *DeploymentValidator) verifyProviders(ctx context.Context, template *v1alpha1.Template) error { + requiredProviders := template.Status.Providers + management := &v1alpha1.Management{} + managementRef := types.NamespacedName{Name: v1alpha1.ManagementName, Namespace: v1alpha1.ManagementNamespace} + if err := v.Get(ctx, managementRef, management); err != nil { + return err + } + + exposedProviders := management.Status.AvailableProviders + missingProviders := make(map[string][]string) + missingProviders["bootstrap"] = getMissingProviders(exposedProviders.BootstrapProviders, requiredProviders.BootstrapProviders) + missingProviders["control plane"] = getMissingProviders(exposedProviders.ControlPlaneProviders, requiredProviders.ControlPlaneProviders) + missingProviders["infrastructure"] = getMissingProviders(exposedProviders.InfrastructureProviders, requiredProviders.InfrastructureProviders) + + var err error + for providerType, missing := range missingProviders { + if len(missing) > 0 { + err = errors.Join(err, fmt.Errorf("one or more required %s providers are not deployed yet: %v", providerType, missing)) + } + } + if err != nil { return err } - applyDefaultDeploymentConfiguration(deployment, template) return nil } -func applyDefaultDeploymentConfiguration(deployment *v1alpha1.Deployment, template *v1alpha1.Template) { - if deployment.Spec.Config != nil || template.Status.Config == nil { - // Only apply defaults when there's no configuration provided - return +func getMissingProviders(exposedProviders []string, requiredProviders []string) []string { + exposedBootstrapProviders := utils.SliceToMapKeys[[]string, map[string]struct{}](exposedProviders) + diff, isSubset := utils.DiffSliceSubset[[]string, map[string]struct{}](requiredProviders, exposedBootstrapProviders) + if !isSubset { + return diff } - deployment.Spec.DryRun = true - deployment.Spec.Config = &apiextensionsv1.JSON{Raw: template.Status.Config.Raw} + return []string{} }