diff --git a/apis/rds/v1beta1/zz_cluster_types.go b/apis/rds/v1beta1/zz_cluster_types.go index 1a2ece5703..f9f87088f2 100755 --- a/apis/rds/v1beta1/zz_cluster_types.go +++ b/apis/rds/v1beta1/zz_cluster_types.go @@ -423,6 +423,11 @@ type ClusterParameters struct { // +kubebuilder:validation:Optional ApplyImmediately *bool `json:"applyImmediately,omitempty" tf:"apply_immediately,omitempty"` + // If true, the password will be auto-generated and stored in the Secret referenced by the masterPasswordSecretRef field. + // +upjet:crd:field:TFTag=- + // +kubebuilder:validation:Optional + AutoGeneratePassword *bool `json:"autoGeneratePassword,omitempty" tf:"-"` + // List of EC2 Availability Zones for the DB cluster storage where DB cluster instances can be created. // We recommend specifying 3 AZs or using the if necessary. // A maximum of 3 AZs can be configured. @@ -550,6 +555,7 @@ type ClusterParameters struct { ManageMasterUserPassword *bool `json:"manageMasterUserPassword,omitempty" tf:"manage_master_user_password,omitempty"` // Password for the master DB user. Note that this may show up in logs, and it will be stored in the state file. Please refer to the RDS Naming Constraints. Cannot be set if manage_master_user_password is set to true. + // Password for the master DB user. If you set autoGeneratePassword to true, the Secret referenced here will be created or updated with generated password if it does not already contain one. // +kubebuilder:validation:Optional MasterPasswordSecretRef *v1.SecretKeySelector `json:"masterPasswordSecretRef,omitempty" tf:"-"` diff --git a/apis/rds/v1beta1/zz_generated.deepcopy.go b/apis/rds/v1beta1/zz_generated.deepcopy.go index 77d271b7c7..b0de7e7c47 100644 --- a/apis/rds/v1beta1/zz_generated.deepcopy.go +++ b/apis/rds/v1beta1/zz_generated.deepcopy.go @@ -2677,6 +2677,11 @@ func (in *ClusterParameters) DeepCopyInto(out *ClusterParameters) { *out = new(bool) **out = **in } + if in.AutoGeneratePassword != nil { + in, out := &in.AutoGeneratePassword, &out.AutoGeneratePassword + *out = new(bool) + **out = **in + } if in.AvailabilityZones != nil { in, out := &in.AvailabilityZones, &out.AvailabilityZones *out = make([]*string, len(*in)) diff --git a/config/common/common_test.go b/config/common/common_test.go index a9cd392bbb..4f2f6f4bec 100644 --- a/config/common/common_test.go +++ b/config/common/common_test.go @@ -9,11 +9,11 @@ import ( "testing" "time" - "github.com/crossplane/crossplane-runtime/pkg/errors" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/crossplane-runtime/pkg/resource/fake" "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -85,6 +85,35 @@ func TestPasswordGenerator(t *testing.T) { }, }, }, + "ClusterSecretAlreadyFull": { + reason: "Should be no-op if the Secret already has password.", + args: args{ + kube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + s, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("needs to be secret") + } + s.Data = map[string][]byte{ + "password": []byte("foo"), + } + return nil + }, + }, + secretRefFieldPath: "parameterizable.parameters.masterPasswordSecretRef", + mg: &ujfake.Terraformed{ + Parameterizable: ujfake.Parameterizable{ + Parameters: map[string]any{ + "masterPasswordSecretRef": map[string]any{ + "name": "foo", + "namespace": "bar", + "key": "password", + }, + }, + }, + }, + }, + }, "NoSecretReference": { reason: "Should be no-op if the secret reference is not given.", args: args{ @@ -98,6 +127,19 @@ func TestPasswordGenerator(t *testing.T) { }, }, }, + "NoClusterSecretReference": { + reason: "Should be no-op if the secret reference is not given.", + args: args{ + secretRefFieldPath: "parameterizable.parameters.masterPasswordSecretRef", + mg: &ujfake.Terraformed{ + Parameterizable: ujfake.Parameterizable{ + Parameters: map[string]any{ + "another": "field", + }, + }, + }, + }, + }, "ToggleNotSet": { reason: "Should be no-op if the toggle is not set at all.", args: args{ @@ -119,6 +161,27 @@ func TestPasswordGenerator(t *testing.T) { }, }, }, + "ClusterToggleNotSet": { + reason: "Should be no-op if the toggle is not set at all.", + args: args{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + }, + secretRefFieldPath: "parameterizable.parameters.masterPasswordSecretRef", + toggleFieldPath: "parameterizable.parameters.autoGeneratePassword", + mg: &ujfake.Terraformed{ + Parameterizable: ujfake.Parameterizable{ + Parameters: map[string]any{ + "masterPasswordSecretRef": map[string]any{ + "name": "foo", + "namespace": "bar", + "key": "password", + }, + }, + }, + }, + }, + }, "ToggleFalse": { reason: "Should be no-op if the toggle is set to false.", args: args{ @@ -141,6 +204,28 @@ func TestPasswordGenerator(t *testing.T) { }, }, }, + "ClusterToggleFalse": { + reason: "Should be no-op if the toggle is set to false.", + args: args{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + }, + secretRefFieldPath: "parameterizable.parameters.masterPasswordSecretRef", + toggleFieldPath: "parameterizable.parameters.autoGeneratePassword", + mg: &ujfake.Terraformed{ + Parameterizable: ujfake.Parameterizable{ + Parameters: map[string]any{ + "masterPasswordSecretRef": map[string]any{ + "name": "foo", + "namespace": "bar", + "key": "password", + }, + "autoGeneratePassword": false, + }, + }, + }, + }, + }, "GenerateAndApply": { reason: "Should apply if we generate, set the content of an already existing secret.", args: args{ @@ -183,6 +268,48 @@ func TestPasswordGenerator(t *testing.T) { }, }, }, + "ClusterSecretGenerateAndApply": { + reason: "Should apply if we generate, set the content of an already existing secret.", + args: args{ + kube: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + s, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("needs to be secret") + } + s.CreationTimestamp = metav1.Time{Time: time.Now()} + return nil + }, + MockPatch: func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + s, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("needs to be secret") + } + if len(s.Data["password"]) == 0 { + return errors.New("password is not set") + } + if len(s.OwnerReferences) != 0 { + return errors.New("owner references should not be set if secret already exists") + } + return nil + }, + }, + secretRefFieldPath: "parameterizable.parameters.masterPasswordSecretRef", + toggleFieldPath: "parameterizable.parameters.autoGeneratePassword", + mg: &ujfake.Terraformed{ + Parameterizable: ujfake.Parameterizable{ + Parameters: map[string]any{ + "masterPasswordSecretRef": map[string]any{ + "name": "foo", + "namespace": "bar", + "key": "password", + }, + "autoGeneratePassword": true, + }, + }, + }, + }, + }, "GenerateAndCreate": { reason: "Should create if we generate, set the content and there is no secret in place.", args: args{ @@ -224,6 +351,47 @@ func TestPasswordGenerator(t *testing.T) { }, }, }, + "ClusterSecretGenerateAndCreate": { + reason: "Should create if we generate, set the content and there is no secret in place.", + args: args{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")), + MockCreate: func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + s, ok := obj.(*corev1.Secret) + if !ok { + return errors.New("needs to be secret") + } + if len(s.Data["password"]) == 0 { + return errors.New("password is not set") + } + if len(s.OwnerReferences) == 1 && + s.OwnerReferences[0].Name == "foo-mgd" { + return nil + } + return errors.New("owner references should be set if secret is created") + }, + }, + secretRefFieldPath: "parameterizable.parameters.masterPasswordSecretRef", + toggleFieldPath: "parameterizable.parameters.autoGeneratePassword", + mg: &ujfake.Terraformed{ + Managed: fake.Managed{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-mgd", + }, + }, + Parameterizable: ujfake.Parameterizable{ + Parameters: map[string]any{ + "masterPasswordSecretRef": map[string]any{ + "name": "foo", + "namespace": "bar", + "key": "password", + }, + "autoGeneratePassword": true, + }, + }, + }, + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { diff --git a/config/rds/config.go b/config/rds/config.go index 46f4d53e56..02b78953e3 100644 --- a/config/rds/config.go +++ b/config/rds/config.go @@ -57,6 +57,23 @@ func Configure(p *config.Provider) { "MasterUserSecretInitParameters": "ClusterMasterUserSecretInitParameters", "MasterUserSecretObservation": "ClusterMasterUserSecretObservation", } + desc, _ := comments.New("If true, the password will be auto-generated and"+ + " stored in the Secret referenced by the masterPasswordSecretRef field.", + comments.WithTFTag("-")) + r.TerraformResource.Schema["auto_generate_password"] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Description: desc.String(), + } + r.InitializerFns = append(r.InitializerFns, + common.PasswordGenerator( + "spec.forProvider.masterPasswordSecretRef", + "spec.forProvider.autoGeneratePassword", + )) + r.TerraformResource.Schema["master_password"].Description = "Password for the " + + "master DB user. If you set autoGeneratePassword to true, the Secret" + + " referenced here will be created or updated with generated password" + + " if it does not already contain one." }) p.AddResourceConfigurator("aws_rds_cluster_instance", func(r *config.Resource) { diff --git a/examples/rds/v1beta1/cluster.yaml b/examples/rds/v1beta1/cluster.yaml index 5a1442be9b..d9c896403e 100644 --- a/examples/rds/v1beta1/cluster.yaml +++ b/examples/rds/v1beta1/cluster.yaml @@ -9,6 +9,7 @@ spec: region: us-west-1 engine: aurora-postgresql masterUsername: cpadmin + autoGeneratePassword: true masterPasswordSecretRef: name: sample-cluster-password namespace: upbound-system @@ -17,12 +18,3 @@ spec: writeConnectionSecretToRef: name: sample-rds-cluster-secret namespace: upbound-system ---- -apiVersion: v1 -kind: Secret -metadata: - name: sample-cluster-password - namespace: upbound-system -type: Opaque -stringData: - password: TestPass0! diff --git a/package/crds/rds.aws.upbound.io_clusters.yaml b/package/crds/rds.aws.upbound.io_clusters.yaml index 3374a466a3..3712c64f5c 100644 --- a/package/crds/rds.aws.upbound.io_clusters.yaml +++ b/package/crds/rds.aws.upbound.io_clusters.yaml @@ -86,6 +86,11 @@ spec: immediately, or during the next maintenance window. Default is false. See Amazon RDS Documentation for more information. type: boolean + autoGeneratePassword: + description: If true, the password will be auto-generated and + stored in the Secret referenced by the masterPasswordSecretRef + field. + type: boolean availabilityZones: description: |- List of EC2 Availability Zones for the DB cluster storage where DB cluster instances can be created. @@ -372,10 +377,9 @@ spec: is provided. type: boolean masterPasswordSecretRef: - description: Password for the master DB user. Note that this may - show up in logs, and it will be stored in the state file. Please - refer to the RDS Naming Constraints. Cannot be set if manage_master_user_password - is set to true. + description: |- + Password for the master DB user. Note that this may show up in logs, and it will be stored in the state file. Please refer to the RDS Naming Constraints. Cannot be set if manage_master_user_password is set to true. + Password for the master DB user. If you set autoGeneratePassword to true, the Secret referenced here will be created or updated with generated password if it does not already contain one. properties: key: description: The key to select.