From 7fb6b65b2ade6655aa0dd9c0fafcb397fdef7a1c Mon Sep 17 00:00:00 2001 From: Michael Kanchuker Date: Thu, 16 Jul 2026 21:38:37 +0300 Subject: [PATCH 1/2] feat: support name-only secret references Allow generated APIs to reference Secrets whose data keys are controlled by the controller. --- pkg/config/field.go | 4 ++++ pkg/model/types.go | 5 +++++ pkg/model/types_test.go | 15 +++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/pkg/config/field.go b/pkg/config/field.go index 408438579..045dc63ef 100644 --- a/pkg/config/field.go +++ b/pkg/config/field.go @@ -422,6 +422,10 @@ type FieldConfig struct { // IsSecret instructs the code generator that this field should be a // SecretKeyReference. IsSecret bool `json:"is_secret"` + // IsSecretReference instructs the code generator that this field should be + // a name/namespace-only SecretReference. Use this when the controller owns + // the Secret data key convention instead of accepting a user-selected key. + IsSecretReference bool `json:"is_secret_reference"` // IsImmutable indicates that the field is enforced as immutable at the // admission layer. The code generator will add kubebuilder:validation:XValidation // lines to the CRD, preventing changes to this field after it’s set. diff --git a/pkg/model/types.go b/pkg/model/types.go index edbc8a666..c660bfdd9 100644 --- a/pkg/model/types.go +++ b/pkg/model/types.go @@ -82,6 +82,11 @@ func CleanGoType( gtwp = "*metav1.Time" gte = "metav1.Time" gt = "*metav1.Time" + } else if fieldCfg != nil && fieldCfg.IsSecretReference { + gt = "*ackv1alpha1.SecretReference" + gte = "SecretReference" + gtwp = "*ackv1alpha1.SecretReference" + return gte, gt, gtwp } else if fieldCfg != nil && fieldCfg.IsSecret { gt = "*ackv1alpha1.SecretKeyReference" gte = "SecretKeyReference" diff --git a/pkg/model/types_test.go b/pkg/model/types_test.go index 9c7bb6d8e..e2f16fb8a 100644 --- a/pkg/model/types_test.go +++ b/pkg/model/types_test.go @@ -3,6 +3,8 @@ package model_test import ( "testing" + "github.com/aws-controllers-k8s/code-generator/pkg/api" + "github.com/aws-controllers-k8s/code-generator/pkg/config" "github.com/aws-controllers-k8s/code-generator/pkg/model" "github.com/stretchr/testify/assert" ) @@ -77,3 +79,16 @@ func TestReplacePkgName(t *testing.T) { assert.Equal(tc.want, result) } } + +func TestCleanGoTypeSecretReference(t *testing.T) { + gte, gt, gtwp := model.CleanGoType( + nil, + nil, + &api.Shape{Type: "blob"}, + &config.FieldConfig{IsSecretReference: true}, + ) + + assert.Equal(t, "SecretReference", gte) + assert.Equal(t, "*ackv1alpha1.SecretReference", gt) + assert.Equal(t, "*ackv1alpha1.SecretReference", gtwp) +} From 0585f7b217d2dd32d60f5d66c870d2271d80e238 Mon Sep 17 00:00:00 2001 From: Michael Kanchuker Date: Thu, 16 Jul 2026 21:54:50 +0300 Subject: [PATCH 2/2] feat: support CRD spec CEL validations support creation of x-kubernetes-validations fields in crd definitions --- pkg/config/resource.go | 11 ++++++++ pkg/config/validate.go | 22 ++++++++++++++++ pkg/config/validate_test.go | 50 +++++++++++++++++++++++++++++++++++++ pkg/model/crd.go | 10 ++++++++ pkg/model/crd_test.go | 19 ++++++++++++++ templates/apis/crd.go.tpl | 3 +++ 6 files changed, 115 insertions(+) diff --git a/pkg/config/resource.go b/pkg/config/resource.go index b71929bbf..91d369899 100644 --- a/pkg/config/resource.go +++ b/pkg/config/resource.go @@ -135,6 +135,17 @@ type ResourceConfig struct { // SDK implementation details that are auto-filled by the SDK middleware // when nil and should not be exposed in the CRD. IgnoreIdempotencyToken bool `json:"ignore_idempotency_token,omitempty"` + // SpecValidations contains CEL validation rules applied to the generated + // resource Spec as kubebuilder XValidation markers. + SpecValidations []XValidationConfig `json:"spec_validations,omitempty"` +} + +// XValidationConfig describes a kubebuilder CEL validation rule. +type XValidationConfig struct { + // Rule is the CEL expression evaluated against the generated Spec. + Rule string `json:"rule"` + // Message is returned when Rule evaluates to false. + Message string `json:"message"` } // TagConfig instructs the code generator on how to generate functions that diff --git a/pkg/config/validate.go b/pkg/config/validate.go index 285ec13c5..9c5d12187 100644 --- a/pkg/config/validate.go +++ b/pkg/config/validate.go @@ -44,10 +44,32 @@ func ValidateConfig( errs = append(errs, validateRenameOperations(cfg, sdkOperations)...) errs = append(errs, validateIgnoredOperations(cfg, sdkOperations)...) + errs = append(errs, validateSpecValidations(cfg)...) return errs } +func validateSpecValidations(cfg *Config) []error { + var errs []error + for resourceName, resourceConfig := range cfg.Resources { + for index, validation := range resourceConfig.SpecValidations { + if strings.TrimSpace(validation.Rule) == "" { + errs = append(errs, fmt.Errorf( + "resources.%s.spec_validations[%d].rule: must not be empty", + resourceName, index, + )) + } + if strings.TrimSpace(validation.Message) == "" { + errs = append(errs, fmt.Errorf( + "resources.%s.spec_validations[%d].message: must not be empty", + resourceName, index, + )) + } + } + } + return errs +} + // validateRenameOperations checks that operation names referenced in // resources[R].renames.operations[OpName] exist in the SDK. func validateRenameOperations( diff --git a/pkg/config/validate_test.go b/pkg/config/validate_test.go index 686862b2e..23a8878d2 100644 --- a/pkg/config/validate_test.go +++ b/pkg/config/validate_test.go @@ -103,6 +103,56 @@ func TestValidateRenameOperations(t *testing.T) { } } +func TestValidateSpecValidations(t *testing.T) { + tests := []struct { + name string + validations []XValidationConfig + wantErrCount int + }{ + { + name: "valid", + validations: []XValidationConfig{{ + Rule: "!has(self.importFrom) || !has(self.certificate)", + Message: "importFrom and certificate are mutually exclusive", + }}, + }, + { + name: "missing rule", + validations: []XValidationConfig{{ + Message: "message", + }}, + wantErrCount: 1, + }, + { + name: "missing message", + validations: []XValidationConfig{{ + Rule: "true", + }}, + wantErrCount: 1, + }, + { + name: "missing rule and message", + validations: []XValidationConfig{{ + Rule: " ", + Message: " ", + }}, + wantErrCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &Config{Resources: map[string]ResourceConfig{ + "Certificate": {SpecValidations: tt.validations}, + }} + errs := validateSpecValidations(cfg) + if len(errs) != tt.wantErrCount { + t.Fatalf("got %d errors, want %d: %v", len(errs), tt.wantErrCount, errs) + } + }) + } +} + func TestValidateIgnoredOperations(t *testing.T) { sdkOps := map[string]struct{}{ "CreateBucket": {}, diff --git a/pkg/model/crd.go b/pkg/model/crd.go index 5eb5e0729..43a0cd4b1 100644 --- a/pkg/model/crd.go +++ b/pkg/model/crd.go @@ -98,6 +98,16 @@ func (r *CRD) Config() *ackgenconfig.Config { return r.cfg } +// SpecValidations returns the CEL validation rules configured for the +// generated resource Spec. +func (r *CRD) SpecValidations() []ackgenconfig.XValidationConfig { + resourceConfig := r.cfg.GetResourceConfig(r.Names.Original) + if resourceConfig == nil { + return nil + } + return resourceConfig.SpecValidations +} + // GetStorageVersion returns the configured storage API version for the CRD, or // the specified default version. func (r *CRD) GetStorageVersion(defaultVersion string) (string, error) { diff --git a/pkg/model/crd_test.go b/pkg/model/crd_test.go index 8e3a5b197..e5573f370 100644 --- a/pkg/model/crd_test.go +++ b/pkg/model/crd_test.go @@ -17,6 +17,8 @@ import ( "testing" awssdkmodel "github.com/aws-controllers-k8s/code-generator/pkg/api" + ackgenconfig "github.com/aws-controllers-k8s/code-generator/pkg/config" + "github.com/aws-controllers-k8s/pkg/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -44,6 +46,23 @@ func TestAddMemberShapRef_Structure(t *testing.T) { assert.Equal(memberShapeRef, structShape.MemberRefs["NewField"]) } +func TestCRDSpecValidations(t *testing.T) { + validation := ackgenconfig.XValidationConfig{ + Rule: "!has(self.importFrom) || !has(self.certificate)", + Message: "fields are mutually exclusive", + } + crd := &CRD{ + cfg: &ackgenconfig.Config{ + Resources: map[string]ackgenconfig.ResourceConfig{ + "Certificate": {SpecValidations: []ackgenconfig.XValidationConfig{validation}}, + }, + }, + Names: names.New("Certificate"), + } + + assert.Equal(t, []ackgenconfig.XValidationConfig{validation}, crd.SpecValidations()) +} + func TestAddMemberShapRef_List(t *testing.T) { assert := assert.New(t) require := require.New(t) diff --git a/templates/apis/crd.go.tpl b/templates/apis/crd.go.tpl index 67343b423..5ca956951 100644 --- a/templates/apis/crd.go.tpl +++ b/templates/apis/crd.go.tpl @@ -15,6 +15,9 @@ import ( ) {{ .CRD.Documentation }} +{{- range $validation := .CRD.SpecValidations }} +// +kubebuilder:validation:XValidation:rule="{{ $validation.Rule }}",message="{{ $validation.Message }}" +{{- end }} type {{ .CRD.Kind }}Spec struct { {{ range $fieldName := .CRD.SpecFieldNames }} {{- $field := (index $.CRD.SpecFields $fieldName) }}