diff --git a/pkg/config/resource.go b/pkg/config/resource.go index 793ecddc..bc1b2cea 100644 --- a/pkg/config/resource.go +++ b/pkg/config/resource.go @@ -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 diff --git a/pkg/resource/sensitive.go b/pkg/resource/sensitive.go index e10b73b8..bde568c4 100644 --- a/pkg/resource/sensitive.go +++ b/pkg/resource/sensitive.go @@ -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 { diff --git a/pkg/types/builder.go b/pkg/types/builder.go index 18c1076d..037ce10b 100644 --- a/pkg/types/builder.go +++ b/pkg/types/builder.go @@ -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 { @@ -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) @@ -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 @@ -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. diff --git a/pkg/types/field.go b/pkg/types/field.go index b492a309..983b0d31 100644 --- a/pkg/types/field.go +++ b/pkg/types/field.go @@ -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 @@ -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: