Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 95 additions & 33 deletions apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package v1alpha1

import (
"context"
"fmt"

apps "k8s.io/api/apps/v1"
Expand All @@ -25,62 +26,93 @@
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// log is for logging in this package.
var clowdapplog = logf.Log.WithName("clowdapp-resource")

// SetupWebhookWithManager configures the webhook for this ClowdApp resource
func (i *ClowdApp) SetupWebhookWithManager(mgr ctrl.Manager) error {
// clowdAppValidator is a webhook that validates ClowdApp resources
type clowdAppValidator struct {
client.Client
}

func (r *ClowdApp) SetupWebhookWithManager(mgr ctrl.Manager) error {
// Add index for spec.envName field for webhook queries
if err := mgr.GetFieldIndexer().IndexField(
context.TODO(), &ClowdApp{}, "spec.envName", func(o client.Object) []string {
return []string{o.(*ClowdApp).Spec.EnvName}
}); err != nil {
return err
}

return ctrl.NewWebhookManagedBy(mgr).
For(i).
For(r).
WithValidator(&clowdAppValidator{Client: mgr.GetClient()}).
Complete()
}

//+kubebuilder:webhook:path=/validate-cloud-redhat-com-v1alpha1-clowdapp,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloud.redhat.com,resources=clowdapps,verbs=create;update,versions=v1alpha1,name=vclowdapp.kb.io,admissionReviewVersions={v1}
//+kubebuilder:webhook:path=/mutate-pod,mutating=true,failurePolicy=ignore,sideEffects=None,groups="",resources=pods,verbs=create;update,versions=v1,name=vclowdmutatepod.kb.io,admissionReviewVersions={v1}

// Define default validations that should always run
var defaultValidations = []appValidationFunc{
validateDatabase,
validateSidecars,
validateInit,
validateDeploymentStrategy,
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (i *ClowdApp) ValidateCreate() (admission.Warnings, error) {
clowdapplog.Info("validate create", "name", i.Name)

return []string{}, i.processValidations(i,
validateDatabase,
validateSidecars,
validateInit,
validateDeploymentStrategy,
)
func (v *clowdAppValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
clowdapp := obj.(*ClowdApp)
clowdapplog.Info("validate create", "name", clowdapp.Name)

// Create validations list with default validations plus duplicate name check
validations := append([]appValidationFunc{v.validateDuplicateName}, defaultValidations...)

return []string{}, v.processValidations(ctx, clowdapp, validations...)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (i *ClowdApp) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) {
clowdapplog.Info("validate update", "name", i.Name)

return []string{}, i.processValidations(i,
validateDatabase,
validateSidecars,
validateInit,
validateDeploymentStrategy,
)
func (v *clowdAppValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
clowdapp := newObj.(*ClowdApp)
oldClowdApp := oldObj.(*ClowdApp)
clowdapplog.Info("validate update", "name", clowdapp.Name)

// Start with default validations
validations := make([]appValidationFunc, len(defaultValidations))
copy(validations, defaultValidations)

// Append duplicate name validation if names differ
if oldClowdApp.Name != clowdapp.Name {
validations = append(validations, v.validateDuplicateName)
}

return []string{}, v.processValidations(ctx, clowdapp, validations...)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (i *ClowdApp) ValidateDelete() (admission.Warnings, error) {
clowdapplog.Info("validate delete", "name", i.Name)
func (v *clowdAppValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
clowdapp := obj.(*ClowdApp)
clowdapplog.Info("validate delete", "name", clowdapp.Name)

return []string{}, nil
}

type appValidationFunc func(*ClowdApp) field.ErrorList
type appValidationFunc func(context.Context, client.Client, *ClowdApp) field.ErrorList

func (i *ClowdApp) processValidations(o *ClowdApp, vfns ...appValidationFunc) error {
func (v *clowdAppValidator) processValidations(ctx context.Context, o *ClowdApp, vfns ...appValidationFunc) error {
var allErrs field.ErrorList

for _, validation := range vfns {
fieldList := validation(o)
if fieldList != nil {
allErrs = append(allErrs, fieldList...)
if validation != nil {
fieldList := validation(ctx, v.Client, o)
if fieldList != nil {
allErrs = append(allErrs, fieldList...)
}
}
}

Expand All @@ -90,20 +122,20 @@

return apierrors.NewInvalid(
schema.GroupKind{Group: "cloud.redhat.com", Kind: "ClowdApp"},
i.Name, allErrs,
o.Name, allErrs,
)
}

func validateDatabase(i *ClowdApp) field.ErrorList {
func validateDatabase(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
allErrs := field.ErrorList{}

if i.Spec.Database.Name != "" && i.Spec.Database.SharedDBAppName != "" {

Check failure on line 132 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: i

Check failure on line 132 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / clowder-on-pull-request

apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go#L132

undefined: i
allErrs = append(allErrs, field.Forbidden(
field.NewPath("spec.Database.Name", "spec.Database.SharedDBAppName"), "cannot set db name and sharedDbApp Name together"),
)
}

if i.Spec.Database.SharedDBAppName != "" && i.Spec.Cyndi.Enabled {

Check failure on line 138 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: i

Check failure on line 138 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / clowder-on-pull-request

apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go#L138

undefined: i
allErrs = append(allErrs, field.Forbidden(
field.NewPath("spec.Database.SharedDBAppName", "spec.Cyndi.Enabled"), "cannot use cyndi with a shared database"),
)
Expand All @@ -112,10 +144,10 @@
return allErrs
}

func validateInit(i *ClowdApp) field.ErrorList {
func validateInit(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
allErrs := field.ErrorList{}

for depIdx, deployment := range i.Spec.Deployments {

Check failure on line 150 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: i

Check failure on line 150 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / clowder-on-pull-request

apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go#L150

undefined: i
if len(deployment.PodSpec.InitContainers) > 1 {
for icIdx, ic := range deployment.PodSpec.InitContainers {
if ic.Name == "" {
Expand All @@ -132,9 +164,9 @@
return allErrs
}

func validateSidecars(i *ClowdApp) field.ErrorList {
func validateSidecars(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
allErrs := field.ErrorList{}
for depIndx, deployment := range i.Spec.Deployments {

Check failure on line 169 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: i

Check failure on line 169 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / clowder-on-pull-request

apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go#L169

undefined: i
for carIndx, sidecar := range deployment.PodSpec.Sidecars {
if sidecar.Name != "token-refresher" && sidecar.Name != "otel-collector" {
allErrs = append(
Expand All @@ -147,7 +179,7 @@
}
}
}
for jobIndx, job := range i.Spec.Jobs {

Check failure on line 182 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: i

Check failure on line 182 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / clowder-on-pull-request

apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go#L182

undefined: i
if job.Schedule == "" {
continue
}
Expand All @@ -166,9 +198,9 @@
return allErrs
}

func validateDeploymentStrategy(i *ClowdApp) field.ErrorList {
func validateDeploymentStrategy(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
allErrs := field.ErrorList{}
for depIndex, deployment := range i.Spec.Deployments {

Check failure on line 203 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: i (typecheck)

Check failure on line 203 in apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / clowder-on-pull-request

apis/cloud.redhat.com/v1alpha1/clowdapp_webhook.go#L203

undefined: i
if deployment.DeploymentStrategy != nil && deployment.WebServices.Public.Enabled && deployment.DeploymentStrategy.PrivateStrategy == apps.RecreateDeploymentStrategyType {
allErrs = append(
allErrs,
Expand All @@ -181,3 +213,33 @@
}
return allErrs
}

func (v *clowdAppValidator) validateDuplicateName(ctx context.Context, c client.Client, r *ClowdApp) field.ErrorList {
allErrs := field.ErrorList{}

// Check if another ClowdApp with the same name already exists in the same ClowdEnvironment
existingClowdApps := &ClowdAppList{}
err := c.List(ctx, existingClowdApps, client.MatchingFields{
"spec.envName": r.Spec.EnvName,
})

if err != nil {
// If we got an error, log it but don't fail validation
// This allows the webhook to continue functioning even if there are temporary
// API server issues
clowdapplog.Error(err, "Error checking for duplicate ClowdApp name", "name", r.Name)
return allErrs
}

// Iterate through existing ClowdApps to check for duplicates with same name in different namespaces
for _, existingApp := range existingClowdApps.Items {
if existingApp.Name == r.Name && existingApp.Namespace != r.Namespace {
allErrs = append(allErrs, field.Duplicate(
field.NewPath("metadata").Child("name"),
fmt.Sprintf("ClowdApp with name '%s' already exists in ClowdEnvironment '%s' in namespace '%s'", r.Name, r.Spec.EnvName, existingApp.Namespace)),
)
}
}

return allErrs
}
Loading
Loading