Skip to content

Commit 04bd6d6

Browse files
committed
Added validation of duplicate names
- AI (Cursor) assisted in webhook - AI (Cursor) authored kuttl test
1 parent ce3da51 commit 04bd6d6

9 files changed

Lines changed: 533 additions & 41 deletions

File tree

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

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package v1alpha1
1818

1919
import (
20+
"context"
2021
"fmt"
2122

2223
apps "k8s.io/api/apps/v1"
@@ -25,61 +26,92 @@ import (
2526
"k8s.io/apimachinery/pkg/runtime/schema"
2627
"k8s.io/apimachinery/pkg/util/validation/field"
2728
ctrl "sigs.k8s.io/controller-runtime"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
2830
logf "sigs.k8s.io/controller-runtime/pkg/log"
2931
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3032
)
3133

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

37+
// clowdAppValidator is a webhook that validates ClowdApp resources
38+
type clowdAppValidator struct {
39+
client.Client
40+
}
41+
3542
func (r *ClowdApp) SetupWebhookWithManager(mgr ctrl.Manager) error {
43+
// Add index for spec.envName field for webhook queries
44+
if err := mgr.GetFieldIndexer().IndexField(
45+
context.TODO(), &ClowdApp{}, "spec.envName", func(o client.Object) []string {
46+
return []string{o.(*ClowdApp).Spec.EnvName}
47+
}); err != nil {
48+
return err
49+
}
50+
3651
return ctrl.NewWebhookManagedBy(mgr).
3752
For(r).
53+
WithValidator(&clowdAppValidator{Client: mgr.GetClient()}).
3854
Complete()
3955
}
4056

4157
//+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}
4258
//+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}
4359

60+
// Define default validations that should always run
61+
var defaultValidations = []appValidationFunc{
62+
validateDatabase,
63+
validateSidecars,
64+
validateInit,
65+
validateDeploymentStrategy,
66+
}
67+
4468
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
45-
func (r *ClowdApp) ValidateCreate() (admission.Warnings, error) {
46-
clowdapplog.Info("validate create", "name", r.Name)
47-
48-
return []string{}, r.processValidations(r,
49-
validateDatabase,
50-
validateSidecars,
51-
validateInit,
52-
validateDeploymentStrategy,
53-
)
69+
func (v *clowdAppValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
70+
clowdapp := obj.(*ClowdApp)
71+
clowdapplog.Info("validate create", "name", clowdapp.Name)
72+
73+
// Create validations list with default validations plus duplicate name check
74+
validations := append([]appValidationFunc{v.validateDuplicateName}, defaultValidations...)
75+
76+
return []string{}, v.processValidations(ctx, clowdapp, validations...)
5477
}
5578

5679
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
57-
func (r *ClowdApp) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) {
58-
clowdapplog.Info("validate update", "name", r.Name)
59-
60-
return []string{}, r.processValidations(r,
61-
validateDatabase,
62-
validateSidecars,
63-
validateInit,
64-
validateDeploymentStrategy,
65-
)
80+
func (v *clowdAppValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
81+
clowdapp := newObj.(*ClowdApp)
82+
oldClowdApp := oldObj.(*ClowdApp)
83+
clowdapplog.Info("validate update", "name", clowdapp.Name)
84+
85+
// Start with default validations
86+
validations := make([]appValidationFunc, len(defaultValidations))
87+
copy(validations, defaultValidations)
88+
89+
// Append duplicate name validation if names differ
90+
if oldClowdApp.Name != clowdapp.Name {
91+
validations = append(validations, v.validateDuplicateName)
92+
}
93+
94+
return []string{}, v.processValidations(ctx, clowdapp, validations...)
6695
}
6796

6897
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
69-
func (r *ClowdApp) ValidateDelete() (admission.Warnings, error) {
70-
clowdapplog.Info("validate delete", "name", r.Name)
98+
func (v *clowdAppValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
99+
clowdapp := obj.(*ClowdApp)
100+
clowdapplog.Info("validate delete", "name", clowdapp.Name)
71101
return []string{}, nil
72102
}
73103

74-
type appValidationFunc func(*ClowdApp) field.ErrorList
104+
type appValidationFunc func(context.Context, client.Client, *ClowdApp) field.ErrorList
75105

76-
func (r *ClowdApp) processValidations(o *ClowdApp, vfns ...appValidationFunc) error {
106+
func (v *clowdAppValidator) processValidations(ctx context.Context, o *ClowdApp, vfns ...appValidationFunc) error {
77107
var allErrs field.ErrorList
78108

79109
for _, validation := range vfns {
80-
fieldList := validation(o)
81-
if fieldList != nil {
82-
allErrs = append(allErrs, fieldList...)
110+
if validation != nil {
111+
fieldList := validation(ctx, v.Client, o)
112+
if fieldList != nil {
113+
allErrs = append(allErrs, fieldList...)
114+
}
83115
}
84116
}
85117

@@ -89,11 +121,11 @@ func (r *ClowdApp) processValidations(o *ClowdApp, vfns ...appValidationFunc) er
89121

90122
return apierrors.NewInvalid(
91123
schema.GroupKind{Group: "cloud.redhat.com", Kind: "ClowdApp"},
92-
r.Name, allErrs,
124+
o.Name, allErrs,
93125
)
94126
}
95127

96-
func validateDatabase(r *ClowdApp) field.ErrorList {
128+
func validateDatabase(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
97129
allErrs := field.ErrorList{}
98130

99131
if r.Spec.Database.Name != "" && r.Spec.Database.SharedDBAppName != "" {
@@ -111,7 +143,7 @@ func validateDatabase(r *ClowdApp) field.ErrorList {
111143
return allErrs
112144
}
113145

114-
func validateInit(r *ClowdApp) field.ErrorList {
146+
func validateInit(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
115147
allErrs := field.ErrorList{}
116148

117149
for depIdx, deployment := range r.Spec.Deployments {
@@ -131,7 +163,7 @@ func validateInit(r *ClowdApp) field.ErrorList {
131163
return allErrs
132164
}
133165

134-
func validateSidecars(r *ClowdApp) field.ErrorList {
166+
func validateSidecars(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
135167
allErrs := field.ErrorList{}
136168
for depIndx, deployment := range r.Spec.Deployments {
137169
for carIndx, sidecar := range deployment.PodSpec.Sidecars {
@@ -165,7 +197,7 @@ func validateSidecars(r *ClowdApp) field.ErrorList {
165197
return allErrs
166198
}
167199

168-
func validateDeploymentStrategy(r *ClowdApp) field.ErrorList {
200+
func validateDeploymentStrategy(_ context.Context, _ client.Client, r *ClowdApp) field.ErrorList {
169201
allErrs := field.ErrorList{}
170202
for depIndex, deployment := range r.Spec.Deployments {
171203
if deployment.DeploymentStrategy != nil && deployment.WebServices.Public.Enabled && deployment.DeploymentStrategy.PrivateStrategy == apps.RecreateDeploymentStrategyType {
@@ -180,3 +212,33 @@ func validateDeploymentStrategy(r *ClowdApp) field.ErrorList {
180212
}
181213
return allErrs
182214
}
215+
216+
func (v *clowdAppValidator) validateDuplicateName(ctx context.Context, c client.Client, r *ClowdApp) field.ErrorList {
217+
allErrs := field.ErrorList{}
218+
219+
// Check if another ClowdApp with the same name already exists in the same ClowdEnvironment
220+
existingClowdApps := &ClowdAppList{}
221+
err := c.List(ctx, existingClowdApps, client.MatchingFields{
222+
"spec.envName": r.Spec.EnvName,
223+
})
224+
225+
if err != nil {
226+
// If we got an error, log it but don't fail validation
227+
// This allows the webhook to continue functioning even if there are temporary
228+
// API server issues
229+
clowdapplog.Error(err, "Error checking for duplicate ClowdApp name", "name", r.Name)
230+
return allErrs
231+
}
232+
233+
// Iterate through existing ClowdApps to check for duplicates with same name in different namespaces
234+
for _, existingApp := range existingClowdApps.Items {
235+
if existingApp.Name == r.Name && existingApp.Namespace != r.Namespace {
236+
allErrs = append(allErrs, field.Duplicate(
237+
field.NewPath("metadata").Child("name"),
238+
fmt.Sprintf("ClowdApp with name '%s' already exists in ClowdEnvironment '%s' in namespace '%s'", r.Name, r.Spec.EnvName, existingApp.Namespace)),
239+
)
240+
}
241+
}
242+
243+
return allErrs
244+
}

0 commit comments

Comments
 (0)