Skip to content

Commit

Permalink
Validation for MultiClusterService
Browse files Browse the repository at this point in the history
  • Loading branch information
wahabmk committed Nov 5, 2024
1 parent 04311ce commit 59131c2
Show file tree
Hide file tree
Showing 8 changed files with 429 additions and 12 deletions.
32 changes: 30 additions & 2 deletions api/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ func SetupIndexers(ctx context.Context, mgr ctrl.Manager) error {
return err
}

if err := SetupMultiClusterServiceServicesIndexer(ctx, mgr); err != nil {
return err
}

if err := SetupClusterTemplateChainIndexer(ctx, mgr); err != nil {
return err
}
Expand Down Expand Up @@ -131,10 +135,10 @@ func ExtractReleaseTemplates(rawObj client.Object) []string {
const ServicesTemplateKey = ".spec.services[].Template"

func SetupManagedClusterServicesIndexer(ctx context.Context, mgr ctrl.Manager) error {
return mgr.GetFieldIndexer().IndexField(ctx, &ManagedCluster{}, ServicesTemplateKey, ExtractServiceTemplateName)
return mgr.GetFieldIndexer().IndexField(ctx, &ManagedCluster{}, ServicesTemplateKey, ExtractServiceTemplateFromManagedCluster)
}

func ExtractServiceTemplateName(rawObj client.Object) []string {
func ExtractServiceTemplateFromManagedCluster(rawObj client.Object) []string {
cluster, ok := rawObj.(*ManagedCluster)
if !ok {
return nil
Expand All @@ -148,6 +152,30 @@ func ExtractServiceTemplateName(rawObj client.Object) []string {
return templates
}

// const ServicesTemplateKey2 = ".spec.services[].Template2"

func SetupMultiClusterServiceServicesIndexer(ctx context.Context, mgr ctrl.Manager) error {
return mgr.GetFieldIndexer().IndexField(ctx, &MultiClusterService{}, ServicesTemplateKey, ExtractServiceTemplateFromMultiClusterService)
}

func ExtractServiceTemplateFromMultiClusterService(rawObj client.Object) []string {
cluster, ok := rawObj.(*MultiClusterService)
if !ok {
return nil
}

templates := []string{}
for _, s := range cluster.Spec.Services {
templates = append(templates, s.Template)
}

return templates
}

// func SetupMultiClusterSerivceServicesIndexer(ctx context.Context, mgr ctrl.Manager) error {
// return mgr.GetFieldIndexer().IndexField(ctx, &MultiClusterService{}, ServicesTemplateKey, ExtractServiceTemplateName)
// }

const SupportedTemplateKey = ".spec.supportedTemplates[].Name"

func SetupClusterTemplateChainIndexer(ctx context.Context, mgr ctrl.Manager) error {
Expand Down
36 changes: 30 additions & 6 deletions internal/webhook/managedcluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

if err := isTemplateValid(template); err != nil {
if err := isTemplateValid(template.GetCommonStatus()); err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

Expand All @@ -79,6 +79,18 @@ func (v *ManagedClusterValidator) ValidateCreate(ctx context.Context, obj runtim
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

// validate services
for _, svc := range managedCluster.Spec.Services {
tpl, err := getServiceTemplate(ctx, v.Client, managedCluster.Namespace, svc.Template)
if err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

if err := isTemplateValid(tpl.GetCommonStatus()); err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}
}

return nil, nil
}

Expand Down Expand Up @@ -106,7 +118,7 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, oldObj, ne
return admission.Warnings{msg}, errClusterUpgradeForbidden
}

if err := isTemplateValid(template); err != nil {
if err := isTemplateValid(template.GetCommonStatus()); err != nil {
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

Expand All @@ -119,6 +131,18 @@ func (v *ManagedClusterValidator) ValidateUpdate(ctx context.Context, oldObj, ne
return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
}

// // validate services
// for _, svc := range newManagedCluster.Spec.Services {
// tpl, err := getServiceTemplate(ctx, v.Client, newManagedCluster.Namespace, svc.Template)
// if err != nil {
// return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
// }

// if err := isTemplateValid(tpl.GetCommonStatus()); err != nil {
// return nil, fmt.Errorf("%s: %v", invalidManagedClusterMsg, err)
// }
// }

return nil, nil
}

Expand Down Expand Up @@ -185,7 +209,7 @@ func (v *ManagedClusterValidator) Default(ctx context.Context, obj runtime.Objec
return fmt.Errorf("could not get template for the managedcluster: %v", err)
}

if err := isTemplateValid(template); err != nil {
if err := isTemplateValid(template.GetCommonStatus()); err != nil {
return fmt.Errorf("template is invalid: %v", err)
}

Expand Down Expand Up @@ -216,9 +240,9 @@ func (v *ManagedClusterValidator) getManagedClusterCredential(ctx context.Contex
return cred, nil
}

func isTemplateValid(template *hmcv1alpha1.ClusterTemplate) error {
if !template.Status.Valid {
return fmt.Errorf("the template is not valid: %s", template.Status.ValidationError)
func isTemplateValid(status *hmcv1alpha1.TemplateStatusCommon) error {
if !status.Valid {
return fmt.Errorf("the template is not valid: %s", status.ValidationError)
}

return nil
Expand Down
68 changes: 65 additions & 3 deletions internal/webhook/managedcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ import (
)

var (
testTemplateName = "template-test"
testCredentialName = "cred-test"
newTemplateName = "new-template-name"
testTemplateName = "template-test"
testSvcTemplateName = "svc-template-test"
testCredentialName = "cred-test"
newTemplateName = "new-template-name"

testNamespace = "test"

Expand Down Expand Up @@ -97,6 +98,32 @@ func TestManagedClusterValidateCreate(t *testing.T) {
},
err: fmt.Sprintf("the ManagedCluster is invalid: clustertemplates.hmc.mirantis.com \"%s\" not found", testTemplateName),
},
{
name: "should fail if the ServiceTemplates are not found in same namespace",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
managedcluster.WithServiceTemplate(testSvcTemplateName),
),
existingObjects: []runtime.Object{
mgmt,
cred,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithProvidersStatus(v1alpha1.Providers{
"infrastructure-aws",
"control-plane-k0smotron",
"bootstrap-k0smotron",
}),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
template.NewServiceTemplate(
template.WithName(testSvcTemplateName),
template.WithNamespace("othernamespace"),
),
},
err: fmt.Sprintf("the ManagedCluster is invalid: servicetemplates.hmc.mirantis.com \"%s\" not found", testSvcTemplateName),
},
{
name: "should fail if the cluster template was found but is invalid (some validation error)",
managedCluster: managedcluster.NewManagedCluster(
Expand All @@ -116,11 +143,41 @@ func TestManagedClusterValidateCreate(t *testing.T) {
},
err: "the ManagedCluster is invalid: the template is not valid: validation error example",
},
{
name: "should fail if the service templates were found but are invalid (some validation error)",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
managedcluster.WithServiceTemplate(testSvcTemplateName),
),
existingObjects: []runtime.Object{
mgmt,
cred,
template.NewClusterTemplate(
template.WithName(testTemplateName),
template.WithProvidersStatus(v1alpha1.Providers{
"infrastructure-aws",
"control-plane-k0smotron",
"bootstrap-k0smotron",
}),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
template.NewServiceTemplate(
template.WithName(testSvcTemplateName),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{
Valid: false,
ValidationError: "validation error example",
}),
),
},
err: "the ManagedCluster is invalid: the template is not valid: validation error example",
},
{
name: "should succeed",
managedCluster: managedcluster.NewManagedCluster(
managedcluster.WithClusterTemplate(testTemplateName),
managedcluster.WithCredential(testCredentialName),
managedcluster.WithServiceTemplate(testSvcTemplateName),
),
existingObjects: []runtime.Object{
mgmt,
Expand All @@ -134,6 +191,11 @@ func TestManagedClusterValidateCreate(t *testing.T) {
}),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
template.NewServiceTemplate(
template.WithName(testSvcTemplateName),
// template.WithNamespace(testNamespace),
template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}),
),
},
},
{
Expand Down
111 changes: 111 additions & 0 deletions internal/webhook/multiclusterservice_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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"
"fmt"

"github.com/Mirantis/hmc/api/v1alpha1"
"github.com/Mirantis/hmc/internal/utils"
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"
)

type MultiClusterServiceValidator struct {
client.Client
}

const invalidMultiClusterServiceMsg = "the MultiClusterService is invalid"

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (v *MultiClusterServiceValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
v.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(&v1alpha1.MultiClusterService{}).
WithValidator(v).
WithDefaulter(v).
Complete()
}

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

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

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (v *MultiClusterServiceValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
mcs, ok := obj.(*v1alpha1.MultiClusterService)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected MultiClusterService but got a %T", obj))
}

for _, svc := range mcs.Spec.Services {
tpl, err := getServiceTemplate(ctx, v.Client, utils.DefaultSystemNamespace, svc.Template)
if err != nil {
return nil, fmt.Errorf("%s: %v", invalidMultiClusterServiceMsg, err)
}

if err := isTemplateValid(tpl.GetCommonStatus()); err != nil {
return nil, fmt.Errorf("%s: %v", invalidMultiClusterServiceMsg, err)
}
}

return nil, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (v *MultiClusterServiceValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
_, ok := oldObj.(*v1alpha1.MultiClusterService)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected MultiClusterService but got a %T", oldObj))
}
mcs, ok := newObj.(*v1alpha1.MultiClusterService)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected MultiClusterService but got a %T", newObj))
}

for _, svc := range mcs.Spec.Services {
tpl, err := getServiceTemplate(ctx, v.Client, utils.DefaultSystemNamespace, svc.Template)
if err != nil {
return nil, fmt.Errorf("%s: %v", invalidMultiClusterServiceMsg, err)
}

if err := isTemplateValid(tpl.GetCommonStatus()); err != nil {
return nil, fmt.Errorf("%s: %v", invalidMultiClusterServiceMsg, err)
}
}

return nil, nil
}

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

func getServiceTemplate(ctx context.Context, c client.Client, templateNamespace, templateName string) (tpl *v1alpha1.ServiceTemplate, err error) {
tpl = new(v1alpha1.ServiceTemplate)
return tpl, c.Get(ctx, client.ObjectKey{Namespace: templateNamespace, Name: templateName}, tpl)
}
Loading

0 comments on commit 59131c2

Please sign in to comment.