diff --git a/examples/rds/db-cluster-clone.gyro b/examples/rds/db-cluster-clone.gyro new file mode 100644 index 000000000..1af689b8b --- /dev/null +++ b/examples/rds/db-cluster-clone.gyro @@ -0,0 +1,180 @@ +aws::vpc vpc + cidr-block: "10.0.0.0/16" + + tags: { + Name: "vpc-example" + } +end + +aws::subnet subnet-us-east-2a + vpc: $(aws::vpc vpc) + availability-zone: "us-east-2a" + cidr-block: "10.0.0.0/24" + tags: { + Name: "subnet-us-east-2a-example" + } +end + +aws::subnet subnet-us-east-2b + vpc: $(aws::vpc vpc) + availability-zone: "us-east-2b" + cidr-block: "10.0.1.0/24" + tags: { + Name: "subnet-us-east-2b-example" + } +end + +aws::subnet subnet-us-east-2c + vpc: $(aws::vpc vpc) + availability-zone: "us-east-2c" + cidr-block: "10.0.2.0/24" + tags: { + Name: "subnet-us-east-2c-example" + } +end + +aws::db-subnet-group db-subnet-group + name: "db-subnet-group-example" + description: "db subnet group description" + subnets: [ + $(aws::subnet subnet-us-east-2a), + $(aws::subnet subnet-us-east-2b), + $(aws::subnet subnet-us-east-2c) + ] + + tags: { + Name: "db-subnet-group-example" + } +end + +aws::security-group security-group-example + name: "example-db-cluster-security-group" + vpc: $(aws::vpc vpc) + description: "Allow web traffic only" +end + +aws::security-group-rules security-group-example-rules + security-group: $(aws::security-group security-group-example) + keep-default-egress-rules: true + + @for port, type -in [80, 'http', 443, 'https', 999, 'foo'] + ingress + description: "allow inbound $(type) traffic, ipv4 only" + cidr-block: "0.0.0.0/0" + protocol: "TCP" + from-port: $(port) + to-port: $(port) + end + @end + + egress + description: "allow outbound http traffic, ipv6 only" + ipv6-cidr-block: "::/0" + protocol: "TCP" + from-port: 80 + to-port: 80 + end +end + +aws::kms-key aurora-master-password-encryption + description: "KMS key used to encrypt aurora database master password" + + aliases: ["alias/example/aurora-master-password-encryption"] + enabled: "true" +end + +aws::db-cluster db-cluster-example + identifier: "aurora-mysql-cluster" + engine: "aurora-mysql" + engine-version: "8.0.mysql_aurora.3.06.0" + availability-zones: ["us-east-2a", "us-east-2b", "us-east-2c"] + db-name: "clusterexample" + + master-username: "root" + manage-master-user-password: true + master-user-secret-kms-key: $(aws::kms-key aurora-master-password-encryption) + + storage-type: "aurora" + + engine-mode: "provisioned" + serverless-v2-scaling-configuration + min-capacity: 1 + max-capacity: 3 + end + + db-subnet-group: $(aws::db-subnet-group db-subnet-group) + vpc-security-groups: [ + $(aws::security-group security-group-example) + ] + backup-retention-period: 5 + preferred-backup-window: "07:00-09:00" + preferred-maintenance-window: "tue:15:00-tue:17:00" + skip-final-snapshot: true + enable-local-write-forwarding: false + + tags: { + Name: "aurora-mysql-cluster" + } + copy-tags-to-snapshot: true + storage-encrypted: true + kms-key: $(aws::kms-key aurora-master-password-encryption) + back-track-window: 0 + auto-minor-version-upgrade: true + deletion-protection: false +end + +aws::db-instance db-instance-example + identifier: "aurora-mysql-cluster-instance" + db-instance-class: "db.serverless" + db-cluster: $(aws::db-cluster db-cluster-example) + engine: "aurora-mysql" + apply-immediately: true + tags: { + Name: "aurora-mysql-cluster-instance" + } +end + +aws::db-cluster db-cluster-example-copy + identifier: "aurora-mysql-cluster-copy" + engine: "aurora-mysql" + engine-version: "8.0.mysql_aurora.3.06.0" + availability-zones: ["us-east-2a", "us-east-2b", "us-east-2c"] + db-name: "clusterexample" + source-db-cluster: $(aws::db-cluster db-cluster-example) + #restore-to-time: "Tue Oct 29 10:16:07 EDT 2024" + restore-type: "copy-on-write" + use-latest-restorable-time: true + + master-username: "root" + manage-master-user-password: true + master-user-secret-kms-key: $(aws::kms-key aurora-master-password-encryption) + + storage-type: "aurora" + + engine-mode: "provisioned" + serverless-v2-scaling-configuration + min-capacity: 1 + max-capacity: 3 + end + + db-subnet-group: $(aws::db-subnet-group db-subnet-group) + vpc-security-groups: [ + $(aws::security-group security-group-example) + ] + backup-retention-period: 5 + preferred-backup-window: "07:00-09:00" + preferred-maintenance-window: "tue:15:00-tue:17:00" + skip-final-snapshot: true + enable-local-write-forwarding: false + + tags: { + Name: "aurora-mysql-cluster-copy" + } + + copy-tags-to-snapshot: true + storage-encrypted: true + kms-key: $(aws::kms-key aurora-master-password-encryption) + back-track-window: 0 + auto-minor-version-upgrade: true + deletion-protection: false +end diff --git a/src/main/java/gyro/aws/rds/DbClusterResource.java b/src/main/java/gyro/aws/rds/DbClusterResource.java index a533b7db2..94d035265 100644 --- a/src/main/java/gyro/aws/rds/DbClusterResource.java +++ b/src/main/java/gyro/aws/rds/DbClusterResource.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Set; @@ -39,6 +40,7 @@ import gyro.core.resource.Updatable; import gyro.core.scope.State; import gyro.core.validation.ConflictsWith; +import gyro.core.validation.DependsOn; import gyro.core.validation.Required; import gyro.core.validation.ValidStrings; import gyro.core.validation.ValidationError; @@ -53,6 +55,7 @@ import software.amazon.awssdk.services.rds.model.ModifyDbClusterRequest; import software.amazon.awssdk.services.rds.model.RestoreDbClusterFromS3Response; import software.amazon.awssdk.services.rds.model.RestoreDbClusterFromSnapshotResponse; +import software.amazon.awssdk.services.rds.model.RestoreDbClusterToPointInTimeResponse; /** * Create a Aurora cluster. @@ -151,10 +154,16 @@ public class DbClusterResource extends RdsTaggableResource implements Copyable(cluster.availabilityZones())); @@ -789,6 +873,11 @@ public void copyFrom(DBCluster cluster) { LocalWriteForwardingStatus.ENABLING.equals(cluster.localWriteForwardingStatus()) || LocalWriteForwardingStatus.ENABLED.equals(cluster.localWriteForwardingStatus()) ); + + setEarliestRestorableTime( + cluster.earliestRestorableTime() == null ? null : Date.from(cluster.earliestRestorableTime())); + setLatestRestorableTime( + cluster.latestRestorableTime() == null ? null : Date.from(cluster.latestRestorableTime())); } @Override @@ -826,7 +915,48 @@ protected void doCreate(GyroUI ui, State state) { .build() : null; - if (getSnapshotIdentifier() != null) { + ModifyDbClusterRequest.Builder modifyRequest = + ModifyDbClusterRequest.builder().applyImmediately(true).dbClusterIdentifier(getIdentifier()); + + boolean modify = false; + + if (getSourceDbCluster() != null) { + RestoreDbClusterToPointInTimeResponse response = + client.restoreDBClusterToPointInTime( + r -> r.backtrackWindow(getBackTrackWindow()) + .copyTagsToSnapshot(getCopyTagsToSnapshot()) + .dbClusterIdentifier(getIdentifier()) + .dbClusterInstanceClass(getDbClusterInstanceClass()) + .dbClusterParameterGroupName( + getDbClusterParameterGroup() != null ? getDbClusterParameterGroup().getName() : null) + .dbSubnetGroupName(getDbSubnetGroup() != null ? getDbSubnetGroup().getName() : null) + .deletionProtection(getDeletionProtection()) + .enableCloudwatchLogsExports(getEnableCloudwatchLogsExports()) + .enableIAMDatabaseAuthentication(getEnableIamDatabaseAuthentication()) + .engineMode(getEngineMode()) + .iops(getIops()) + .kmsKeyId(getKmsKey() != null ? getKmsKey().getArn() : null) + .optionGroupName(getOptionGroup() != null ? getOptionGroup().getName() : null) + .port(getPort()) + .restoreToTime(getRestoreToTime() == null ? null : getRestoreToTime().toInstant()) + .restoreType(getRestoreType()) + .scalingConfiguration(scalingConfiguration) + .serverlessV2ScalingConfiguration(getServerlessV2ScalingConfiguration() != null ? + getServerlessV2ScalingConfiguration().toServerlessV2ScalingConfiguration() : null) + .sourceDBClusterIdentifier(getSourceDbCluster().getIdentifier()) + .storageType(getStorageType()) + .useLatestRestorableTime(getUseLatestRestorableTime()) + .vpcSecurityGroupIds(getVpcSecurityGroups() != null ? getVpcSecurityGroups() + .stream() + .map(SecurityGroupResource::getId) + .collect(Collectors.toList()) : null) + ); + + setArn(response.dbCluster().dbClusterArn()); + state.save(); + waitForActiveStatus(client, TimeoutSettings.Action.CREATE); + + } else if (getSnapshotIdentifier() != null) { RestoreDbClusterFromSnapshotResponse response = client.restoreDBClusterFromSnapshot( r -> r.availabilityZones(getAvailabilityZones()) .snapshotIdentifier(getSnapshotIdentifier()) @@ -862,17 +992,6 @@ protected void doCreate(GyroUI ui, State state) { state.save(); waitForActiveStatus(client, TimeoutSettings.Action.CREATE); - if (getBackupRetentionPeriod() != null || getPreferredBackupWindow() != null - || getPreferredMaintenanceWindow() != null) { - client.modifyDBCluster(r -> r.applyImmediately(true) - .dbClusterIdentifier(getIdentifier()) - .backupRetentionPeriod(getBackupRetentionPeriod()) - .preferredBackupWindow(getPreferredBackupWindow()) - .preferredMaintenanceWindow(getPreferredMaintenanceWindow())); - - waitForActiveStatus(client, TimeoutSettings.Action.CREATE); - } - } else if (getS3Import() != null) { RestoreDbClusterFromS3Response response = client.restoreDBClusterFromS3(r -> r.availabilityZones(getAvailabilityZones()) @@ -919,13 +1038,9 @@ protected void doCreate(GyroUI ui, State state) { state.save(); waitForActiveStatus(client, TimeoutSettings.Action.CREATE); - if (getBackupRetentionPeriod() != null || getPreferredBackupWindow() != null - || getPreferredMaintenanceWindow() != null) { - client.modifyDBCluster(r -> r.applyImmediately(true) - .dbClusterIdentifier(getIdentifier()) - .scalingConfiguration(scalingConfiguration)); - - waitForActiveStatus(client, TimeoutSettings.Action.CREATE); + if (getScalingConfiguration() != null) { + modifyRequest = modifyRequest.scalingConfiguration(scalingConfiguration); + modify = true; } } else { @@ -986,6 +1101,43 @@ protected void doCreate(GyroUI ui, State state) { waitForActiveStatus(client, TimeoutSettings.Action.CREATE); } + if (getSourceDbCluster() != null || getSnapshotIdentifier() != null || getS3Import() == null) { + if (getBackupRetentionPeriod() != null) { + modifyRequest = modifyRequest.backupRetentionPeriod(getBackupRetentionPeriod()); + modify = true; + } + + if (getPreferredBackupWindow() != null) { + modifyRequest = modifyRequest.preferredBackupWindow(getPreferredBackupWindow()); + modify = true; + } + + if (getPreferredMaintenanceWindow() != null) { + modifyRequest = modifyRequest.preferredMaintenanceWindow(getPreferredMaintenanceWindow()); + modify = true; + } + + if (getManageMasterUserPassword() != null) { + modifyRequest = modifyRequest.manageMasterUserPassword(getManageMasterUserPassword()); + modify = true; + } + + if (getMasterUserSecretKmsKey() != null) { + modifyRequest = modifyRequest.masterUserSecretKmsKeyId(getMasterUserSecretKmsKey().getId()); + modify = true; + } + + if (getAutoMinorVersionUpgrade() != null) { + modifyRequest = modifyRequest.autoMinorVersionUpgrade(getAutoMinorVersionUpgrade()); + modify = true; + } + + if (modify) { + client.modifyDBCluster(modifyRequest.build()); + waitForActiveStatus(client, TimeoutSettings.Action.CREATE); + } + } + DescribeDbClustersResponse describeResponse = client.describeDBClusters( r -> r.dbClusterIdentifier(getIdentifier()) ); @@ -1145,6 +1297,26 @@ public List validate(Set configuredFields) { )); } + if (getSourceDbCluster() != null) { + if (getRestoreToTime() == null && + (getUseLatestRestorableTime() == null || Boolean.FALSE.equals(getUseLatestRestorableTime()))) { + errors.add(new ValidationError( + this, + "restore-to-time", + "Either 'restore-to-time' or 'use-latest-restorable-time' is required when restoring from a 'source-db-cluster'." + )); + } + + if (getRestoreType() != null && getRestoreType().equals("copy-on-write") && + (getRestoreToTime() != null || !Boolean.TRUE.equals(getUseLatestRestorableTime()))) { + errors.add(new ValidationError( + this, + "restore-to-time", + "'restore-to-time' cannot be set when 'restore-type' is set to 'copy-on-write'. Use 'use-latest-restorable-time' instead and set it to `true` instead." + )); + } + } + return errors; } }