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
6 changes: 6 additions & 0 deletions pkg/config/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ type Sensitive struct {
// fieldPaths keeps the mapping of sensitive fields in Terraform schema with
// terraform field path as key and xp field path as value.
fieldPaths map[string]string

// AllowPlaintextValue allows sensitive fields to be passed as plaintext
// in addition to secret references. When true, sensitive fields will be
// generated as both a regular field and a secret reference field, giving
// users the option to provide values directly or via secrets.
AllowPlaintextValue bool
}

// LateInitializer represents configurations that control
Expand Down
8 changes: 8 additions & 0 deletions pkg/resource/sensitive.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ func GetSensitiveParameters(ctx context.Context, client SecretClient, from resou
prefixes = []string{""}
}

// Check if plaintext value already exists in Terraform state
// This happens when AllowPlaintextValue is enabled and user provided plaintext
existingValue, err := pavedTF.GetValue(tfPath)
if err == nil && existingValue != nil {
// Plaintext value exists, skip secret reference handling
continue
}

// spec.forProvider secret references override the spec.initProvider
// references.
for _, p := range prefixes {
Expand Down
37 changes: 33 additions & 4 deletions pkg/types/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ func (g *Builder) buildResource(res *schema.Resource, cfg *config.Resource, tfPa
if drop {
continue
}
// If AllowPlaintextValue is enabled, also generate the regular field
if cfg.Sensitive.AllowPlaintextValue && !IsObservation(res.Schema[snakeFieldName]) {
regularField, err := NewField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode)
if err != nil {
return nil, nil, nil, err
}
// Track that these fields are alternatives for validation
regularField.AlternateFieldName = f.TransformedName
regularField.AddToResource(g, r, typeNames, ptr.Deref(cfg.SchemaElementOptions[cPath], config.SchemaElementOption{}))
}
case reference != nil:
f, err = NewReferenceField(g, cfg, r, res.Schema[snakeFieldName], reference, snakeFieldName, tfPath, xpPath, names, asBlocksMode)
if err != nil {
Expand Down Expand Up @@ -204,7 +214,16 @@ func (g *Builder) AddToBuilder(typeNames *TypeNames, r *resource) (*types.Named,
for _, p := range r.topLevelRequiredParams {
g.validationRules += "\n"
sp := sanitizePath(p.path)
if p.includeInit {
if p.alternateFieldPath != "" {
// When there's an alternate field path (e.g., AllowPlaintextValue),
// create an OR condition requiring at least one of the two fields
asp := sanitizePath(p.alternateFieldPath)
if p.includeInit {
g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s) || has(self.forProvider.%s) || (has(self.initProvider) && (has(self.initProvider.%s) || has(self.initProvider.%s)))",message="spec.forProvider.%s or spec.forProvider.%s is a required parameter"`, sp, asp, sp, asp, p.path, p.alternateFieldPath)
} else {
g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s) || has(self.forProvider.%s)",message="spec.forProvider.%s or spec.forProvider.%s is a required parameter"`, sp, asp, p.path, p.alternateFieldPath)
}
} else if p.includeInit {
g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s) || (has(self.initProvider) && has(self.initProvider.%s))",message="spec.forProvider.%s is a required parameter"`, sp, sp, p.path)
} else {
g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="!('*' in self.managementPolicies || 'Create' in self.managementPolicies || 'Update' in self.managementPolicies) || has(self.forProvider.%s)",message="spec.forProvider.%s is a required parameter"`, sp, p.path)
Expand Down Expand Up @@ -374,14 +393,19 @@ type resource struct {
}

type topLevelRequiredParam struct {
path string
includeInit bool
path string
includeInit bool
alternateFieldPath string // Alternative field path (e.g., for AllowPlaintextValue)
}

func newTopLevelRequiredParam(path string, includeInit bool) *topLevelRequiredParam {
return &topLevelRequiredParam{path: path, includeInit: includeInit}
}

func newTopLevelRequiredParamWithAlternate(path string, includeInit bool, alternatePath string) *topLevelRequiredParam {
return &topLevelRequiredParam{path: path, includeInit: includeInit, alternateFieldPath: alternatePath}
}

func (r *resource) addParameterField(f *Field, field *types.Var) {
requiredBySchema := (!f.Schema.Optional && !f.Schema.Computed) || f.Required
// Note(turkenh): We are collecting the top level required parameters that
Expand All @@ -401,7 +425,12 @@ func (r *resource) addParameterField(f *Field, field *types.Var) {
requiredBySchema = false
// If the field is not a terraform field, we should not require it in init,
// as it is not an initProvider field.
r.topLevelRequiredParams = append(r.topLevelRequiredParams, newTopLevelRequiredParam(f.TransformedName, !f.TFTag.AlwaysOmitted()))
if f.AlternateFieldName != "" {
// This field has an alternate (e.g., AllowPlaintextValue scenario)
r.topLevelRequiredParams = append(r.topLevelRequiredParams, newTopLevelRequiredParamWithAlternate(f.TransformedName, !f.TFTag.AlwaysOmitted(), f.AlternateFieldName))
} else {
r.topLevelRequiredParams = append(r.topLevelRequiredParams, newTopLevelRequiredParam(f.TransformedName, !f.TFTag.AlwaysOmitted()))
}
}

// Note(lsviben): Only fields which are not also initProvider fields should have a required kubebuilder comment.
Expand Down
16 changes: 16 additions & 0 deletions pkg/types/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ type Field struct {
// Sensitive is set if this Field holds sensitive data and is thus
// generated as a secret reference.
Sensitive bool
// AlternateFieldName is set when this field has an alternative field
// (e.g., clientId and clientIdSecretRef when AllowPlaintextValue is true)
AlternateFieldName string
}

// getDocString tries to extract the documentation string for the specified
Expand Down Expand Up @@ -288,6 +291,19 @@ func NewSensitiveField(g *Builder, cfg *config.Resource, r *resource, sch *schem
// Data will be stored in connection details secret
return nil, true, nil
}

// When AllowPlaintextValue is true, the secret ref field is not required
// by itself since the plaintext field can be used instead.
// We need to mark the schema as optional to prevent it from being added
// to topLevelRequiredParams.
if cfg.Sensitive.AllowPlaintextValue {
f.Required = false
// Create a copy of the schema and mark it as optional
schemaCopy := *f.Schema
schemaCopy.Optional = true
f.Schema = &schemaCopy
}

sfx := "SecretRef"
switch f.FieldType.(type) {
case *types.Slice:
Expand Down