Skip to content

Commit

Permalink
Validate Deployment objects
Browse files Browse the repository at this point in the history
Closes #141
  • Loading branch information
eromanova committed Aug 12, 2024
1 parent 2129cea commit ca434e9
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 16 deletions.
38 changes: 38 additions & 0 deletions internal/utils/util.go
Original file line number Diff line number Diff line change
@@ -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
}
122 changes: 106 additions & 16 deletions internal/webhook/deployment_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
}

Expand All @@ -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
}

Expand All @@ -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{}
}

0 comments on commit ca434e9

Please sign in to comment.