From 7f89cc2acad1c6247de099b529503a3fed139d72 Mon Sep 17 00:00:00 2001 From: Michele Baldessari Date: Mon, 29 Jun 2026 20:00:23 +0200 Subject: [PATCH 1/3] Add variant CR support It is just the same as clusterGroupName field but variant is a lot clearer for a user. Only one of variant and clustergroupname can be used at the same time --- api/v1alpha1/pattern_types.go | 6 +- api/v1alpha1/pattern_webhook.go | 19 +++ api/v1alpha1/pattern_webhook_test.go | 119 ++++++++++++++++++ ...ops.hybrid-cloud-patterns.io_patterns.yaml | 13 +- ...tterns-operator.clusterserviceversion.yaml | 4 + ...ops.hybrid-cloud-patterns.io_patterns.yaml | 5 +- ...tterns-operator.clusterserviceversion.yaml | 3 + config/samples/gitops_v1alpha1_pattern.yaml | 1 + console/src/components/InstallPatternPage.tsx | 2 + console/src/types.ts | 1 + internal/controller/pattern_controller.go | 7 +- .../controller/pattern_controller_test.go | 16 ++- 12 files changed, 182 insertions(+), 14 deletions(-) diff --git a/api/v1alpha1/pattern_types.go b/api/v1alpha1/pattern_types.go index 59d773fc6..9bdfb4df0 100644 --- a/api/v1alpha1/pattern_types.go +++ b/api/v1alpha1/pattern_types.go @@ -56,7 +56,11 @@ type PatternSpec struct { // Important: Run "make generate" to regenerate code after modifying this file // +operator-sdk:csv:customresourcedefinitions:type=spec,order=3 - ClusterGroupName string `json:"clusterGroupName"` + ClusterGroupName string `json:"clusterGroupName,omitempty"` + + // Variant is an alias for ClusterGroupName. Only one of the two may be set. + // +operator-sdk:csv:customresourcedefinitions:type=spec,order=3 + Variant string `json:"variant,omitempty"` // +operator-sdk:csv:customresourcedefinitions:type=spec,order=4 GitConfig GitConfig `json:"gitSpec"` diff --git a/api/v1alpha1/pattern_webhook.go b/api/v1alpha1/pattern_webhook.go index dcb501957..0cf90aa0c 100644 --- a/api/v1alpha1/pattern_webhook.go +++ b/api/v1alpha1/pattern_webhook.go @@ -61,6 +61,10 @@ func (r *PatternValidator) ValidateCreate(ctx context.Context, obj runtime.Objec } patternlog.Info("validate create", "name", p.Name) + if err := validateVariantAlias(p); err != nil { + return nil, err + } + var patterns PatternList if err = r.Client.List(ctx, &patterns); err != nil { return nil, fmt.Errorf("failed to list Pattern resources: %v", err) @@ -79,6 +83,11 @@ func (r *PatternValidator) ValidateUpdate(_ context.Context, _, newObj runtime.O return nil, err } patternlog.Info("validate update", "name", p.Name) + + if err := validateVariantAlias(p); err != nil { + return nil, err + } + return nil, nil } @@ -105,3 +114,13 @@ func convertToPattern(obj runtime.Object) (*Pattern, error) { } return p, nil } + +func validateVariantAlias(p *Pattern) error { + if p.Spec.ClusterGroupName != "" && p.Spec.Variant != "" { + return fmt.Errorf("spec.variant and spec.clusterGroupName are mutually exclusive, set only one") + } + if p.Spec.ClusterGroupName == "" && p.Spec.Variant == "" { + return fmt.Errorf("one of spec.variant or spec.clusterGroupName must be set") + } + return nil +} diff --git a/api/v1alpha1/pattern_webhook_test.go b/api/v1alpha1/pattern_webhook_test.go index 63c065f85..c9d2f4ea4 100644 --- a/api/v1alpha1/pattern_webhook_test.go +++ b/api/v1alpha1/pattern_webhook_test.go @@ -100,6 +100,122 @@ func TestValidateCreate_DeniesSecondPattern(t *testing.T) { } } +func TestValidateCreate_AllowsVariantOnly(t *testing.T) { + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("failed to add scheme: %v", err) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + validator := &PatternValidator{Client: fakeClient} + + p := &Pattern{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pattern", + Namespace: "default", + }, + Spec: PatternSpec{ + Variant: "hub", + GitConfig: GitConfig{ + TargetRepo: "https://github.com/example/repo", + TargetRevision: "main", + }, + }, + } + + warnings, err := validator.ValidateCreate(context.Background(), p) + if err != nil { + t.Errorf("expected no error when only variant is set, got: %v", err) + } + if warnings != nil { + t.Errorf("expected no warnings, got: %v", warnings) + } +} + +func TestValidateCreate_RejectsBothSet(t *testing.T) { + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("failed to add scheme: %v", err) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + validator := &PatternValidator{Client: fakeClient} + + p := &Pattern{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pattern", + Namespace: "default", + }, + Spec: PatternSpec{ + ClusterGroupName: "hub", + Variant: "hub", + GitConfig: GitConfig{ + TargetRepo: "https://github.com/example/repo", + TargetRevision: "main", + }, + }, + } + + _, err := validator.ValidateCreate(context.Background(), p) + if err == nil { + t.Error("expected error when both variant and clusterGroupName are set, got nil") + } +} + +func TestValidateCreate_RejectsBothEmpty(t *testing.T) { + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("failed to add scheme: %v", err) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + validator := &PatternValidator{Client: fakeClient} + + p := &Pattern{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pattern", + Namespace: "default", + }, + Spec: PatternSpec{ + GitConfig: GitConfig{ + TargetRepo: "https://github.com/example/repo", + TargetRevision: "main", + }, + }, + } + + _, err := validator.ValidateCreate(context.Background(), p) + if err == nil { + t.Error("expected error when neither variant nor clusterGroupName is set, got nil") + } +} + +func TestValidateUpdate_RejectsBothSet(t *testing.T) { + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("failed to add scheme: %v", err) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() + validator := &PatternValidator{Client: fakeClient} + + p := &Pattern{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pattern", + Namespace: "default", + }, + Spec: PatternSpec{ + ClusterGroupName: "hub", + Variant: "factory", + }, + } + + _, err := validator.ValidateUpdate(context.Background(), p, p) + if err == nil { + t.Error("expected error when both variant and clusterGroupName are set on update, got nil") + } +} + func TestValidateCreate_RejectsNonPatternObject(t *testing.T) { scheme := runtime.NewScheme() if err := AddToScheme(scheme); err != nil { @@ -131,6 +247,9 @@ func TestValidateUpdate_Allows(t *testing.T) { Name: "test-pattern", Namespace: "default", }, + Spec: PatternSpec{ + Variant: "hub", + }, } warnings, err := validator.ValidateUpdate(context.Background(), p, p) diff --git a/bundle/manifests/gitops.hybrid-cloud-patterns.io_patterns.yaml b/bundle/manifests/gitops.hybrid-cloud-patterns.io_patterns.yaml index 50bbc22dd..6ea4248bf 100644 --- a/bundle/manifests/gitops.hybrid-cloud-patterns.io_patterns.yaml +++ b/bundle/manifests/gitops.hybrid-cloud-patterns.io_patterns.yaml @@ -1,9 +1,9 @@ +--- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.4 - creationTimestamp: null name: patterns.gitops.hybrid-cloud-patterns.io spec: group: gitops.hybrid-cloud-patterns.io @@ -152,8 +152,11 @@ spec: in order to deploy the pattern. Defaults to https://charts.validatedpatterns.io/ type: string type: object + variant: + description: Variant is an alias for ClusterGroupName. If both are + set, they must match. + type: string required: - - clusterGroupName - gitSpec type: object status: @@ -247,9 +250,3 @@ spec: storage: true subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: null - storedVersions: null diff --git a/bundle/manifests/patterns-operator.clusterserviceversion.yaml b/bundle/manifests/patterns-operator.clusterserviceversion.yaml index 32dbeed1f..1355f7a15 100644 --- a/bundle/manifests/patterns-operator.clusterserviceversion.yaml +++ b/bundle/manifests/patterns-operator.clusterserviceversion.yaml @@ -12,6 +12,7 @@ metadata: }, "spec": { "clusterGroupName": "hub", + "variant": "hub", "gitSpec": { "targetRepo": "https://github.com/validatedpatterns/multicloud-gitops", "targetRevision": "main" @@ -67,6 +68,9 @@ spec: path: extraParameters[0].value - displayName: Cluster Group Name path: clusterGroupName + - description: Alias for Cluster Group Name + displayName: Variant + path: variant - displayName: Git Config path: gitSpec - displayName: Multi Source Config diff --git a/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml b/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml index 7bfe26bf6..0bd1a6cc1 100644 --- a/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml +++ b/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml @@ -152,8 +152,11 @@ spec: in order to deploy the pattern. Defaults to https://charts.validatedpatterns.io/ type: string type: object + variant: + description: Variant is an alias for ClusterGroupName. Only one of + the two may be set. + type: string required: - - clusterGroupName - gitSpec type: object status: diff --git a/config/manifests/bases/patterns-operator.clusterserviceversion.yaml b/config/manifests/bases/patterns-operator.clusterserviceversion.yaml index db9336a32..68a468937 100644 --- a/config/manifests/bases/patterns-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/patterns-operator.clusterserviceversion.yaml @@ -45,6 +45,9 @@ spec: path: extraParameters[0].value - displayName: Cluster Group Name path: clusterGroupName + - description: Alias for Cluster Group Name + displayName: Variant + path: variant - displayName: Git Config path: gitSpec - displayName: Multi Source Config diff --git a/config/samples/gitops_v1alpha1_pattern.yaml b/config/samples/gitops_v1alpha1_pattern.yaml index 1dfb173c8..94b506b8e 100644 --- a/config/samples/gitops_v1alpha1_pattern.yaml +++ b/config/samples/gitops_v1alpha1_pattern.yaml @@ -4,6 +4,7 @@ metadata: name: pattern-sample spec: clusterGroupName: hub + variant: hub gitSpec: targetRepo: "https://github.com/validatedpatterns/multicloud-gitops" targetRevision: "main" diff --git a/console/src/components/InstallPatternPage.tsx b/console/src/components/InstallPatternPage.tsx index 7c3508482..f5e69d672 100644 --- a/console/src/components/InstallPatternPage.tsx +++ b/console/src/components/InstallPatternPage.tsx @@ -305,6 +305,7 @@ export default function InstallPatternPage() { metadata: { name: string; namespace: string }; spec: { clusterGroupName: string; + variant: string; gitSpec: { targetRepo: string; targetRevision: string }; secretsConfig?: { template: string; values: string }; }; @@ -317,6 +318,7 @@ export default function InstallPatternPage() { }, spec: { clusterGroupName: clusterGroupName, + variant: clusterGroupName, gitSpec: { targetRepo, targetRevision, diff --git a/console/src/types.ts b/console/src/types.ts index 82854c780..3f601e0ea 100644 --- a/console/src/types.ts +++ b/console/src/types.ts @@ -48,6 +48,7 @@ export interface Pattern { owners: string[]; org: string; clustergroupname: string; + variant?: string; description?: string; docs_repo_url?: string; spoke?: unknown; diff --git a/internal/controller/pattern_controller.go b/internal/controller/pattern_controller.go index 8b954e03e..f5f779a36 100644 --- a/internal/controller/pattern_controller.go +++ b/internal/controller/pattern_controller.go @@ -646,8 +646,11 @@ func (r *PatternReconciler) applyDefaults(input *api.Pattern) (*api.Pattern, err multiSourceBool := true output.Spec.MultiSourceConfig.Enabled = &multiSourceBool } - if output.Spec.ClusterGroupName == "" { - output.Spec.ClusterGroupName = "default" //nolint:goconst + switch { + case output.Spec.ClusterGroupName == "" && output.Spec.Variant != "": + output.Spec.ClusterGroupName = output.Spec.Variant + case output.Spec.ClusterGroupName != "" && output.Spec.Variant == "": + output.Spec.Variant = output.Spec.ClusterGroupName } if output.Spec.MultiSourceConfig.HelmRepoUrl == "" { output.Spec.MultiSourceConfig.HelmRepoUrl = GiteaHelmRepoUrl diff --git a/internal/controller/pattern_controller_test.go b/internal/controller/pattern_controller_test.go index 7deaa54d5..2da47033a 100644 --- a/internal/controller/pattern_controller_test.go +++ b/internal/controller/pattern_controller_test.go @@ -306,12 +306,24 @@ var _ = Describe("pattern controller - applyDefaults", func() { Expect(*output.Spec.MultiSourceConfig.Enabled).To(BeTrue()) }) - It("should default ClusterGroupName to 'default' when empty", func() { + It("should sync Variant from ClusterGroupName when Variant is empty", func() { + p := buildPatternManifest() + p.Spec.ClusterGroupName = "hub" + p.Spec.Variant = "" + output, err := reconciler.applyDefaults(p) + Expect(err).ToNot(HaveOccurred()) + Expect(output.Spec.ClusterGroupName).To(Equal("hub")) + Expect(output.Spec.Variant).To(Equal("hub")) + }) + + It("should sync ClusterGroupName from Variant when ClusterGroupName is empty", func() { p := buildPatternManifest() p.Spec.ClusterGroupName = "" + p.Spec.Variant = "factory" output, err := reconciler.applyDefaults(p) Expect(err).ToNot(HaveOccurred()) - Expect(output.Spec.ClusterGroupName).To(Equal("default")) + Expect(output.Spec.ClusterGroupName).To(Equal("factory")) + Expect(output.Spec.Variant).To(Equal("factory")) }) It("should default HelmRepoUrl when empty", func() { From 456a17e1ba400bee29a74b99b460abcf8c297592 Mon Sep 17 00:00:00 2001 From: Michele Baldessari Date: Thu, 2 Jul 2026 20:17:27 +0200 Subject: [PATCH 2/3] Fix UI installation and logging --- api/v1alpha1/pattern_webhook.go | 2 ++ console/src/components/InstallPatternPage.tsx | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/pattern_webhook.go b/api/v1alpha1/pattern_webhook.go index 0cf90aa0c..fa339ffaa 100644 --- a/api/v1alpha1/pattern_webhook.go +++ b/api/v1alpha1/pattern_webhook.go @@ -62,6 +62,7 @@ func (r *PatternValidator) ValidateCreate(ctx context.Context, obj runtime.Objec patternlog.Info("validate create", "name", p.Name) if err := validateVariantAlias(p); err != nil { + patternlog.Error(err, "validate create failed", "name", p.Name) return nil, err } @@ -85,6 +86,7 @@ func (r *PatternValidator) ValidateUpdate(_ context.Context, _, newObj runtime.O patternlog.Info("validate update", "name", p.Name) if err := validateVariantAlias(p); err != nil { + patternlog.Error(err, "validate update failed", "name", p.Name) return nil, err } diff --git a/console/src/components/InstallPatternPage.tsx b/console/src/components/InstallPatternPage.tsx index f5e69d672..36ca5ebcd 100644 --- a/console/src/components/InstallPatternPage.tsx +++ b/console/src/components/InstallPatternPage.tsx @@ -304,7 +304,7 @@ export default function InstallPatternPage() { kind: string; metadata: { name: string; namespace: string }; spec: { - clusterGroupName: string; + clusterGroupName?: string; variant: string; gitSpec: { targetRepo: string; targetRevision: string }; secretsConfig?: { template: string; values: string }; @@ -317,7 +317,6 @@ export default function InstallPatternPage() { namespace: PATTERN_OPERATOR_NS, }, spec: { - clusterGroupName: clusterGroupName, variant: clusterGroupName, gitSpec: { targetRepo, From 74552daaf1d4f6bf1e241341be3b768fd13adbd4 Mon Sep 17 00:00:00 2001 From: Michele Baldessari Date: Fri, 3 Jul 2026 07:15:57 +0200 Subject: [PATCH 3/3] Make sure we do validation also on update of pattern CR --- api/v1alpha1/pattern_webhook.go | 2 +- bundle/manifests/patterns-operator.clusterserviceversion.yaml | 1 + config/samples/gitops_v1alpha1_pattern.yaml | 1 - config/webhook/manifests.yaml | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/pattern_webhook.go b/api/v1alpha1/pattern_webhook.go index fa339ffaa..1c325e4be 100644 --- a/api/v1alpha1/pattern_webhook.go +++ b/api/v1alpha1/pattern_webhook.go @@ -40,7 +40,7 @@ type PatternValidator struct { } //nolint:lll -// +kubebuilder:webhook:verbs=create;delete,path=/validate-gitops-hybrid-cloud-patterns-io-v1alpha1-pattern,mutating=false,failurePolicy=fail,groups=gitops.hybrid-cloud-patterns.io,resources=patterns,versions=v1alpha1,name=vpattern.gitops.hybrid-cloud-patterns.io,admissionReviewVersions=v1,sideEffects=none +// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-gitops-hybrid-cloud-patterns-io-v1alpha1-pattern,mutating=false,failurePolicy=fail,groups=gitops.hybrid-cloud-patterns.io,resources=patterns,versions=v1alpha1,name=vpattern.gitops.hybrid-cloud-patterns.io,admissionReviewVersions=v1,sideEffects=none var _ webhook.CustomValidator = &PatternValidator{} diff --git a/bundle/manifests/patterns-operator.clusterserviceversion.yaml b/bundle/manifests/patterns-operator.clusterserviceversion.yaml index 1355f7a15..5a07a4ac9 100644 --- a/bundle/manifests/patterns-operator.clusterserviceversion.yaml +++ b/bundle/manifests/patterns-operator.clusterserviceversion.yaml @@ -631,6 +631,7 @@ spec: - v1alpha1 operations: - CREATE + - UPDATE - DELETE resources: - patterns diff --git a/config/samples/gitops_v1alpha1_pattern.yaml b/config/samples/gitops_v1alpha1_pattern.yaml index 94b506b8e..1dfb173c8 100644 --- a/config/samples/gitops_v1alpha1_pattern.yaml +++ b/config/samples/gitops_v1alpha1_pattern.yaml @@ -4,7 +4,6 @@ metadata: name: pattern-sample spec: clusterGroupName: hub - variant: hub gitSpec: targetRepo: "https://github.com/validatedpatterns/multicloud-gitops" targetRevision: "main" diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index dbc91d5eb..3ec9636d5 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -20,6 +20,7 @@ webhooks: - v1alpha1 operations: - CREATE + - UPDATE - DELETE resources: - patterns