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
4 changes: 4 additions & 0 deletions pkg/config/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Comment on lines +425 to +428

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure if the difference between Secret and SecretReference is clear just from the names.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it can be something like isTLSSecret?

@mkl262 mkl262 Jul 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of it as a generlized catch all for secrets that have a predefined structure, such as docker config, TLS, or ssh type secrets.

// 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.
Expand Down
11 changes: 11 additions & 0 deletions pkg/config/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions pkg/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
50 changes: 50 additions & 0 deletions pkg/config/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down
10 changes: 10 additions & 0 deletions pkg/model/crd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions pkg/model/crd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions pkg/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions pkg/model/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions templates/apis/crd.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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) }}
Expand Down