diff --git a/build.gradle b/build.gradle index 032292769..0c595d60c 100644 --- a/build.gradle +++ b/build.gradle @@ -83,7 +83,7 @@ dependencies { implementation 'com.google.guava:guava:23.0' implementation 'com.psddev:dari-util:3.3.607-xe0f27a' implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' - implementation enforcedPlatform('software.amazon.awssdk:bom:2.25.5') + implementation enforcedPlatform('software.amazon.awssdk:bom:2.29.30') implementation 'software.amazon.awssdk:apache-client' implementation 'software.amazon.awssdk:apigatewayv2' implementation 'software.amazon.awssdk:autoscaling' @@ -125,7 +125,8 @@ dependencies { implementation 'software.amazon.awssdk:kendra' implementation 'software.amazon.awssdk:wafv2' implementation 'software.amazon.awssdk:eventbridge' - implementation 'org.json:json:20180813' + implementation 'software.amazon.awssdk:opensearch' + implementation 'org.json:json:20231013' implementation 'com.fasterxml.jackson.core:jackson-core:2.13.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' diff --git a/examples/opensearch/access-policy.json b/examples/opensearch/access-policy.json new file mode 100644 index 000000000..e5304507a --- /dev/null +++ b/examples/opensearch/access-policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "*" + }, + "Action": "es:*", + "Resource": "arn:aws:es:us-east-2:242040583208:domain/testdomain/*" + } + ] +} diff --git a/examples/opensearch/domain.gyro b/examples/opensearch/domain.gyro new file mode 100644 index 000000000..bb00963d5 --- /dev/null +++ b/examples/opensearch/domain.gyro @@ -0,0 +1,109 @@ +aws::vpc vpc-example + cidr-block: "10.0.0.0/16" + provide-ipv6-cidr-block: true + + tags: { + Name: "opensearch-domain-example" + } +end + +aws::subnet example-subnet-1 + vpc: $(aws::vpc vpc-example) + availability-zone: us-east-1a + cidr-block: "10.0.0.0/24" + + tags: { + Name: "opensearch-domain-example-1" + } +end + +aws::subnet example-subnet-2 + vpc: $(aws::vpc vpc-example) + availability-zone: us-east-1b + cidr-block: "10.0.1.0/24" + + tags: { + Name: "opensearch-domain-example-2" + } +end + +aws::subnet example-subnet-3 + vpc: $(aws::vpc vpc-example) + availability-zone: us-east-1c + cidr-block: "10.0.2.0/24" + + tags: { + Name: "opensearch-domain-example-3" + } +end + +aws::security-group example-security-group + name: "opensearch-domain-example" + vpc: $(aws::vpc vpc-example) + description: "security group example for es" +end + +aws::opensearch-domain opensearch-domain-example + domain-name: "testdomain" + open-search-version: "OpenSearch_1.0" + + ebs-options + enable-ebs: true + volume-type: standard + volume-count: 10 + end + + node-to-node-encryption-options + enable-node-to-node-encryption: true + end + + encryption-at-rest-options + enable-encryption-at-rest: true + end + + cluster-configuration + enable-zone-awareness: true + instance-count: 4 + + zone-awareness-configuration + availability-zone-count: 2 + end + end + + domain-endpoint-options + enforce-https: true + end + + advanced-security-options + enable-advanced-security-options: true + enable-internal-user-database: true + + master-user-options + master-username: "masteruser" + master-password: "MasterUser1!" + end + end + + access-policies: "access-policy.json" + + advanced-options: { + "indices.query.bool.max_clause_count": "1026", + "rest.action.multi.allow_explicit_index": "true", + "override_main_response_version": "false" + } + + tags: { + "description": "Test Domain" + } + + vpc-options + subnets: [ + $(aws::subnet example-subnet-1), + $(aws::subnet example-subnet-3) + ] + + security-groups: [ + $(aws::security-group example-security-group) + ] + end +end diff --git a/examples/opensearch/vpc-endpoint.gyro b/examples/opensearch/vpc-endpoint.gyro new file mode 100644 index 000000000..abd375ba0 --- /dev/null +++ b/examples/opensearch/vpc-endpoint.gyro @@ -0,0 +1,58 @@ +aws::vpc vpc-example + cidr-block: "10.0.0.0/16" + provide-ipv6-cidr-block: true + + tags: { + Name: "opensearch-domain-vpc-endpoint-example" + } +end + +aws::subnet example-subnet-1 + vpc: $(aws::vpc vpc-example) + availability-zone: us-east-1a + cidr-block: "10.0.0.0/24" + + tags: { + Name: "opensearch-domain-vpc-endpoint-example-1" + } +end + +aws::subnet example-subnet-2 + vpc: $(aws::vpc vpc-example) + availability-zone: us-east-1b + cidr-block: "10.0.1.0/24" + + tags: { + Name: "opensearch-domain-vpc-endpoint-example-2" + } +end + +aws::subnet example-subnet-3 + vpc: $(aws::vpc vpc-example) + availability-zone: us-east-1c + cidr-block: "10.0.2.0/24" + + tags: { + Name: "opensearch-domain-vpc-endpoint-example-3" + } +end + +aws::security-group example-security-group + name: "opensearch-domain-vpc-endpoint-example" + vpc: $(aws::vpc vpc-example) + description: "security group example for es" +end + +aws::opensearch-vpc-endpoint vpc-endpoint + domain: "arn:aws:es:us-east-1:242040583208:domain/dj-test" + vpc-options + subnets: [ + $(aws::subnet example-subnet-1), + $(aws::subnet example-subnet-3) + ] + + security-groups: [ + $(aws::security-group example-security-group) + ] + end +end diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchAdvancedSecurityOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchAdvancedSecurityOptions.java new file mode 100644 index 000000000..6f6f335b2 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchAdvancedSecurityOptions.java @@ -0,0 +1,127 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import gyro.core.validation.ValidationError; +import software.amazon.awssdk.services.opensearch.model.AdvancedSecurityOptions; +import software.amazon.awssdk.services.opensearch.model.AdvancedSecurityOptionsInput; + +public class OpenSearchAdvancedSecurityOptions extends Diffable implements Copyable { + + private Boolean enableAdvancedSecurityOptions; + private Boolean enableInternalUserDatabase; + private OpenSearchMasterUserOptions masterUserOptions; + + /** + * Enable advanced security for the domain. + */ + @Updatable + @Required + public Boolean getEnableAdvancedSecurityOptions() { + return enableAdvancedSecurityOptions; + } + + public void setEnableAdvancedSecurityOptions(Boolean enableAdvancedSecurityOptions) { + this.enableAdvancedSecurityOptions = enableAdvancedSecurityOptions; + } + + /** + * Enable the Internal User Database. + */ + @Updatable + public Boolean getEnableInternalUserDatabase() { + return enableInternalUserDatabase; + } + + public void setEnableInternalUserDatabase(Boolean enableInternalUserDatabase) { + this.enableInternalUserDatabase = enableInternalUserDatabase; + } + + /** + * The master user options configuration. Can only be set if ``enable-advanced-security-options`` is set to ``true``. + * + * @subresource gyro.aws.opensearch.OpenSearchMasterUserOptions + */ + @Updatable + public OpenSearchMasterUserOptions getMasterUserOptions() { + return masterUserOptions; + } + + public void setMasterUserOptions(OpenSearchMasterUserOptions masterUserOptions) { + this.masterUserOptions = masterUserOptions; + } + + @Override + public void copyFrom(AdvancedSecurityOptions model) { + setEnableAdvancedSecurityOptions(model.enabled()); + setEnableInternalUserDatabase(model.internalUserDatabaseEnabled()); + + // Not resetting the masterUserOptions since the api doesn't return the credentials for the master user. + } + + @Override + public String primaryKey() { + return ""; + } + + AdvancedSecurityOptionsInput toAdvancedSecurityOptionsInput() { + AdvancedSecurityOptionsInput.Builder builder = AdvancedSecurityOptionsInput.builder() + .enabled(getEnableAdvancedSecurityOptions()); + + if (getEnableInternalUserDatabase() != null) { + builder.internalUserDatabaseEnabled(getEnableInternalUserDatabase()); + } + + if (getMasterUserOptions() != null) { + builder.masterUserOptions(getMasterUserOptions().toMasterUserOptions()); + } + + return builder.build(); + } + + @Override + public List validate(Set configuredFields) { + List errors = new ArrayList<>(); + + if (getEnableAdvancedSecurityOptions().equals(Boolean.FALSE) && ( + configuredFields.contains("enable-internal-user-database") || configuredFields.contains( + "master-user-options"))) { + errors.add(new ValidationError( + this, + null, + "The 'enable-internal-user-database' or 'master-user-options' can only be set if 'enable-advanced-security-options' is set to 'true'.")); + } + + if (getEnableAdvancedSecurityOptions().equals(Boolean.TRUE) + && !configuredFields.contains("master-user-options")) { + errors.add(new ValidationError( + this, + "master-user-options", + "The 'master-user-options' is required if 'enable-advanced-security-options' is set to 'true'.")); + } + + return errors; + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchAutoTuneMaintenanceSchedule.java b/src/main/java/gyro/aws/opensearch/OpenSearchAutoTuneMaintenanceSchedule.java new file mode 100644 index 000000000..3f11019d8 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchAutoTuneMaintenanceSchedule.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.Date; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.opensearch.model.AutoTuneMaintenanceSchedule; +import software.amazon.awssdk.services.opensearch.model.Duration; +import software.amazon.awssdk.services.opensearch.model.TimeUnit; + +public class OpenSearchAutoTuneMaintenanceSchedule extends Diffable implements Copyable { + + private Long duration; + private TimeUnit durationUnit; + private String cronExpressionForRecurrence; + private Date startAt; + + /** + * The duration of the maintenance schedule. + */ + @Required + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + /** + * The unit of the duration. + */ + @Required + public TimeUnit getDurationUnit() { + return durationUnit; + } + + public void setDurationUnit(TimeUnit durationUnit) { + this.durationUnit = durationUnit; + } + + /** + * The cron expression for the recurrence of the maintenance schedule. + */ + @Updatable + public String getCronExpressionForRecurrence() { + return cronExpressionForRecurrence; + } + + public void setCronExpressionForRecurrence(String cronExpressionForRecurrence) { + this.cronExpressionForRecurrence = cronExpressionForRecurrence; + } + + /** + * The start time of the maintenance schedule. + */ + @Required + public Date getStartAt() { + return startAt; + } + + public void setStartAt(Date startAt) { + this.startAt = startAt; + } + + @Override + public String primaryKey() { + return String.format( + "duration: %s, duration-unit: %s, start-at: %s", + getDuration(), + getDurationUnit(), + getStartAt()); + } + + @Override + public void copyFrom(AutoTuneMaintenanceSchedule model) { + setDuration(model.duration().value()); + setDurationUnit(model.duration().unit()); + setCronExpressionForRecurrence(model.cronExpressionForRecurrence()); + setStartAt(Date.from(model.startAt())); + } + + AutoTuneMaintenanceSchedule toAutoTuneMaintenanceSchedule() { + return AutoTuneMaintenanceSchedule.builder() + .duration(Duration.builder().value(getDuration()).unit(getDurationUnit()).build()) + .cronExpressionForRecurrence(getCronExpressionForRecurrence()) + .startAt(getStartAt().toInstant()) + .build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchAutoTuneOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchAutoTuneOptions.java new file mode 100644 index 000000000..aaa62bce4 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchAutoTuneOptions.java @@ -0,0 +1,148 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import gyro.core.validation.ValidStrings; +import software.amazon.awssdk.services.opensearch.model.AutoTuneDesiredState; +import software.amazon.awssdk.services.opensearch.model.AutoTuneOptions; +import software.amazon.awssdk.services.opensearch.model.AutoTuneOptionsInput; +import software.amazon.awssdk.services.opensearch.model.RollbackOnDisable; + +public class OpenSearchAutoTuneOptions extends Diffable implements Copyable { + + private AutoTuneDesiredState desiredState; + private RollbackOnDisable rollbackOnDisable; + private Set maintenanceSchedules; + private Boolean useOffPeakWindow; + + /** + * The desired state of the Auto-Tune options. + */ + @Required + @Updatable + @ValidStrings({ "ENABLED", "DISABLED" }) + public AutoTuneDesiredState getDesiredState() { + return desiredState; + } + + public void setDesiredState(AutoTuneDesiredState desiredState) { + this.desiredState = desiredState; + } + + /** + * The rollback behavior when Auto-Tune is disabled. + */ + @Updatable + @ValidStrings({ "DEFAULT_ROLLBACK", "NO_ROLLBACK" }) + public RollbackOnDisable getRollbackOnDisable() { + return rollbackOnDisable; + } + + public void setRollbackOnDisable(RollbackOnDisable rollbackOnDisable) { + this.rollbackOnDisable = rollbackOnDisable; + } + + /** + * The maintenance schedules for the Auto-Tune options. + * + * @subresource gyro.aws.opensearch.OpenSearchAutoTuneMaintenanceSchedule + */ + @Updatable + public Set getMaintenanceSchedules() { + if (maintenanceSchedules == null) { + maintenanceSchedules = new HashSet<>(); + } + + return maintenanceSchedules; + } + + public void setMaintenanceSchedules(Set maintenanceSchedules) { + this.maintenanceSchedules = maintenanceSchedules; + } + + /** + * Enable off-peak window for the domain. + */ + @Required + @Updatable + public Boolean getUseOffPeakWindow() { + return useOffPeakWindow; + } + + public void setUseOffPeakWindow(Boolean useOffPeakWindow) { + this.useOffPeakWindow = useOffPeakWindow; + } + + @Override + public void copyFrom(AutoTuneOptions model) { + setDesiredState(model.desiredState()); + setRollbackOnDisable(model.rollbackOnDisable()); + setUseOffPeakWindow(model.useOffPeakWindow()); + + getMaintenanceSchedules().clear(); + if (model.maintenanceSchedules() != null) { + model.maintenanceSchedules().forEach(maintenanceSchedule -> { + OpenSearchAutoTuneMaintenanceSchedule autoTuneMaintenanceSchedule = newSubresource( + OpenSearchAutoTuneMaintenanceSchedule.class); + autoTuneMaintenanceSchedule.copyFrom(maintenanceSchedule); + getMaintenanceSchedules().add(autoTuneMaintenanceSchedule); + }); + } + } + + @Override + public String primaryKey() { + return ""; + } + + AutoTuneOptions toAutoTuneOptions() { + AutoTuneOptions.Builder builder = AutoTuneOptions.builder() + .desiredState(getDesiredState()) + .rollbackOnDisable(getRollbackOnDisable()) + .useOffPeakWindow(getUseOffPeakWindow()); + + if (!getMaintenanceSchedules().isEmpty()) { + builder.maintenanceSchedules(getMaintenanceSchedules().stream() + .map(OpenSearchAutoTuneMaintenanceSchedule::toAutoTuneMaintenanceSchedule) + .collect(Collectors.toList())); + } + + return builder.build(); + } + + AutoTuneOptionsInput toAutoTuneOptionsInput() { + AutoTuneOptionsInput.Builder builder = AutoTuneOptionsInput.builder() + .desiredState(getDesiredState()) + .useOffPeakWindow(getUseOffPeakWindow()); + + if (!getMaintenanceSchedules().isEmpty()) { + builder.maintenanceSchedules(getMaintenanceSchedules().stream() + .map(OpenSearchAutoTuneMaintenanceSchedule::toAutoTuneMaintenanceSchedule) + .collect(Collectors.toList())); + } + + return builder.build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchClusterConfiguration.java b/src/main/java/gyro/aws/opensearch/OpenSearchClusterConfiguration.java new file mode 100644 index 000000000..d4b64cc6f --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchClusterConfiguration.java @@ -0,0 +1,253 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.DependsOn; +import gyro.core.validation.Min; +import gyro.core.validation.Range; +import gyro.core.validation.ValidStrings; +import gyro.core.validation.ValidationError; +import software.amazon.awssdk.services.opensearch.model.ClusterConfig; +import software.amazon.awssdk.services.opensearch.model.OpenSearchPartitionInstanceType; +import software.amazon.awssdk.services.opensearch.model.OpenSearchWarmPartitionInstanceType; + +public class OpenSearchClusterConfiguration extends Diffable implements Copyable { + + private Boolean enableZoneAwareness; + private OpenSearchZoneAwarenessConfiguration zoneAwarenessConfiguration; + private OpenSearchPartitionInstanceType instanceType; + private Integer instanceCount; + private Boolean dedicatedMasterEnabled; + private OpenSearchPartitionInstanceType dedicatedMasterType; + private Integer dedicatedMasterCount; + private Boolean enableWarm; + private Integer warmCount; + private OpenSearchWarmPartitionInstanceType warmType; + + /** + * Enable zone awareness for the domain. + */ + @Updatable + public Boolean getEnableZoneAwareness() { + return enableZoneAwareness; + } + + public void setEnableZoneAwareness(Boolean enableZoneAwareness) { + this.enableZoneAwareness = enableZoneAwareness; + } + + /** + * The zone awareness options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchZoneAwarenessConfiguration + */ + @Updatable + @DependsOn("enable-zone-awareness") + public OpenSearchZoneAwarenessConfiguration getZoneAwarenessConfiguration() { + return zoneAwarenessConfiguration; + } + + public void setZoneAwarenessConfiguration(OpenSearchZoneAwarenessConfiguration zoneAwarenessConfiguration) { + this.zoneAwarenessConfiguration = zoneAwarenessConfiguration; + } + + /** + * The instance type for the OpenSearch domain cluster. Defaults to ``m4.large_elasticsearch``. + */ + @Updatable + public OpenSearchPartitionInstanceType getInstanceType() { + return instanceType; + } + + public void setInstanceType(OpenSearchPartitionInstanceType instanceType) { + this.instanceType = instanceType; + } + + /** + * The number of nodes in the specified domain cluster. Defaults to ``1``. + */ + @Updatable + @Range(min = 0, max = 40) + public Integer getInstanceCount() { + return instanceCount; + } + + public void setInstanceCount(Integer instanceCount) { + this.instanceCount = instanceCount; + } + + /** + * Dedicate master nodes to the domain cluster. Defaults to ``false``. + */ + @Updatable + public Boolean getDedicatedMasterEnabled() { + return dedicatedMasterEnabled; + } + + public void setDedicatedMasterEnabled(Boolean dedicatedMasterEnabled) { + this.dedicatedMasterEnabled = dedicatedMasterEnabled; + } + + /** + * The instance type for the dedicated master nodes. Defaults to ``m4.large_elasticsearch``. + */ + @Updatable + @DependsOn("dedicated-master-enabled") + public OpenSearchPartitionInstanceType getDedicatedMasterType() { + return dedicatedMasterType; + } + + public void setDedicatedMasterType(OpenSearchPartitionInstanceType dedicatedMasterType) { + this.dedicatedMasterType = dedicatedMasterType; + } + + /** + * The number of dedicated master nodes for the cluster. Defaults to ``3``. + */ + @Updatable + @DependsOn("dedicated-master-enabled") + @Range(min = 2, max = 5) + public Integer getDedicatedMasterCount() { + return dedicatedMasterCount; + } + + public void setDedicatedMasterCount(Integer dedicatedMasterCount) { + this.dedicatedMasterCount = dedicatedMasterCount; + } + + /** + * Enable warm storage. Defaults to ``false``. + */ + @Updatable + public Boolean getEnableWarm() { + return enableWarm; + } + + public void setEnableWarm(Boolean enableWarm) { + this.enableWarm = enableWarm; + } + + /** + * The number of warm nodes in the cluster. Defaults to ``3``. + */ + @Min(3) + @Updatable + @DependsOn("enable-warm") + public Integer getWarmCount() { + return warmCount; + } + + public void setWarmCount(Integer warmCount) { + this.warmCount = warmCount; + } + + /** + * The instance type for warm nodes. Defaults to ``ultrawarm1.medium.elasticsearch``. + */ + @Updatable + @DependsOn("enable-warm") + @ValidStrings({ "ultrawarm1.medium.elasticsearch", "ultrawarm1.large.elasticsearch" }) + public OpenSearchWarmPartitionInstanceType getWarmType() { + return warmType; + } + + public void setWarmType(OpenSearchWarmPartitionInstanceType warmType) { + this.warmType = warmType; + } + + @Override + public void copyFrom(ClusterConfig model) { + setDedicatedMasterCount(model.dedicatedMasterCount()); + setDedicatedMasterEnabled(model.dedicatedMasterEnabled()); + setDedicatedMasterType(model.dedicatedMasterType()); + setEnableWarm(model.warmEnabled()); + setWarmCount(model.warmCount()); + setWarmType(model.warmType()); + setEnableZoneAwareness(model.zoneAwarenessEnabled()); + if (model.zoneAwarenessConfig() != null) { + OpenSearchZoneAwarenessConfiguration configuration = newSubresource( + OpenSearchZoneAwarenessConfiguration.class); + configuration.copyFrom(model.zoneAwarenessConfig()); + setZoneAwarenessConfiguration(configuration); + } + setInstanceCount(model.instanceCount()); + setInstanceType(model.instanceType()); + } + + @Override + public String primaryKey() { + return ""; + } + + ClusterConfig toOpenSearchClusterConfig() { + ClusterConfig.Builder builder = ClusterConfig.builder() + .zoneAwarenessEnabled(getEnableZoneAwareness()) + .instanceType(getInstanceType()) + .instanceCount(getInstanceCount()) + .dedicatedMasterEnabled(getDedicatedMasterEnabled()) + .dedicatedMasterType(getDedicatedMasterType()) + .dedicatedMasterCount(getDedicatedMasterCount()) + .warmEnabled(getEnableWarm()) + .warmType(getWarmType()) + .warmCount(getWarmCount()); + + if (getEnableZoneAwareness() != null) { + builder = builder.zoneAwarenessConfig(getZoneAwarenessConfiguration().toZoneAwarenessConfig()); + } + + return builder.build(); + } + + @Override + public List validate(Set configuredFields) { + List errors = new ArrayList<>(); + + if (configuredFields.contains("enable-zone-awareness") && getEnableZoneAwareness().equals(Boolean.FALSE) + && configuredFields.contains("zone-awareness-configuration")) { + errors.add(new ValidationError( + this, + null, + "The 'zone-awareness-configuration' can only be set if 'enable-zone-awareness' is set to 'true'.")); + } + + if (configuredFields.contains("dedicated-master-enabled") && getDedicatedMasterEnabled().equals(Boolean.FALSE) + && (configuredFields.contains("dedicated-master-type") + || configuredFields.contains("dedicated-master-count"))) { + errors.add(new ValidationError( + this, + null, + "The 'dedicated-master-count' and 'dedicated-master-type' can only be set if 'dedicated-master-enabled' is set to 'true'.")); + } + + if (configuredFields.contains("enable-warm") && getEnableWarm().equals(Boolean.FALSE) && ( + configuredFields.contains("warm-type") || configuredFields.contains("warm-count"))) { + errors.add(new ValidationError( + this, + null, + "The 'warm-count' and 'warm-type' can only be set if 'enable-warm' is set to 'true'.")); + } + + return errors; + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchDomainEndpointOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchDomainEndpointOptions.java new file mode 100644 index 000000000..20abd930c --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchDomainEndpointOptions.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import gyro.core.validation.ValidStrings; +import software.amazon.awssdk.services.opensearch.model.DomainEndpointOptions; +import software.amazon.awssdk.services.opensearch.model.TLSSecurityPolicy; + +public class OpenSearchDomainEndpointOptions extends Diffable implements Copyable { + + private Boolean enforceHttps; + private TLSSecurityPolicy tlsSecurityPolicy; + + /** + * Enforce HTTPS endpoints for the OpenSearch cluster. + */ + @Required + @Updatable + public Boolean getEnforceHttps() { + return enforceHttps; + } + + public void setEnforceHttps(Boolean enforceHttps) { + this.enforceHttps = enforceHttps; + } + + /** + * The TLS security policy that needs to be applied to the HTTPS endpoints of the OpenSearch domain. + */ + @Updatable + @ValidStrings({ "Policy-Min-TLS-1-0-2019-07", "Policy-Min-TLS-1-2-2019-07" }) + public TLSSecurityPolicy getTlsSecurityPolicy() { + return tlsSecurityPolicy; + } + + public void setTlsSecurityPolicy(TLSSecurityPolicy tlsSecurityPolicy) { + this.tlsSecurityPolicy = tlsSecurityPolicy; + } + + @Override + public void copyFrom(DomainEndpointOptions model) { + setEnforceHttps(model.enforceHTTPS()); + setTlsSecurityPolicy(model.tlsSecurityPolicy()); + } + + @Override + public String primaryKey() { + return ""; + } + + DomainEndpointOptions toDomainEndpointOptions() { + return DomainEndpointOptions.builder() + .enforceHTTPS(getEnforceHttps()) + .tlsSecurityPolicy(getTlsSecurityPolicy()) + .build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchDomainFinder.java b/src/main/java/gyro/aws/opensearch/OpenSearchDomainFinder.java new file mode 100644 index 000000000..e0ab75a46 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchDomainFinder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import gyro.aws.AwsFinder; +import gyro.core.Type; +import software.amazon.awssdk.services.opensearch.OpenSearchClient; +import software.amazon.awssdk.services.opensearch.model.DescribeDomainsRequest; +import software.amazon.awssdk.services.opensearch.model.DomainInfo; +import software.amazon.awssdk.services.opensearch.model.DomainStatus; +import software.amazon.awssdk.services.opensearch.model.ResourceNotFoundException; +import software.amazon.awssdk.utils.builder.SdkBuilder; + +/** + * Query elasticsearch domain + * + * Example + * ------- + * + * .. code-block::gyro + * + * opensearch-domain: $(external-query aws::opensearch-domain {domain-name: "testdomain"}) + */ +@Type("opensearch-domain") +public class OpenSearchDomainFinder + extends AwsFinder { + + private String domainName; + + /** + * The name of the OpenSearch Domain. + */ + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + @Override + protected List findAllAws(OpenSearchClient client) { + return client.describeDomains(DescribeDomainsRequest.builder() + .domainNames(client.listDomainNames(SdkBuilder::build) + .domainNames() + .stream() + .map(DomainInfo::domainName) + .collect( + Collectors.toList())) + .build()).domainStatusList(); + } + + @Override + protected List findAws( + OpenSearchClient client, Map filters) { + try { + return client.describeDomains(r -> r.domainNames(filters.get("domain-name"))).domainStatusList(); + } catch (ResourceNotFoundException ex) { + return new ArrayList<>(); + } + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchDomainResource.java b/src/main/java/gyro/aws/opensearch/OpenSearchDomainResource.java new file mode 100644 index 000000000..432a5f115 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchDomainResource.java @@ -0,0 +1,710 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import gyro.aws.AwsResource; +import gyro.aws.Copyable; +import gyro.aws.iam.PolicyResource; +import gyro.core.GyroException; +import gyro.core.GyroUI; +import gyro.core.TimeoutSettings; +import gyro.core.Type; +import gyro.core.Wait; +import gyro.core.resource.Id; +import gyro.core.resource.Output; +import gyro.core.resource.Resource; +import gyro.core.resource.Updatable; +import gyro.core.scope.State; +import gyro.core.validation.Regex; +import gyro.core.validation.Required; +import gyro.core.validation.ValidStrings; +import software.amazon.awssdk.services.opensearch.OpenSearchClient; +import software.amazon.awssdk.services.opensearch.model.CreateDomainRequest; +import software.amazon.awssdk.services.opensearch.model.CreateDomainResponse; +import software.amazon.awssdk.services.opensearch.model.DescribeDomainResponse; +import software.amazon.awssdk.services.opensearch.model.DomainStatus; +import software.amazon.awssdk.services.opensearch.model.IPAddressType; +import software.amazon.awssdk.services.opensearch.model.ResourceNotFoundException; +import software.amazon.awssdk.services.opensearch.model.Tag; +import software.amazon.awssdk.services.opensearch.model.UpdateDomainConfigRequest; +import software.amazon.awssdk.utils.IoUtils; + +/** + * Creates an opensearch domain. + * + * Example + * ------- + * .. code-block:: gyro + * + * aws::opensearch-domain opensearch-domain-example + * domain-name: "testdomain3" + * open-search-version: "OpenSearch_1.0" + * + * ebs-options + * enable-ebs: true + * volume-type: standard + * volume-count: 10 + * end + * + * node-to-node-encryption-options + * enable-node-to-node-encryption: true + * end + * + * encryption-at-rest-options + * enable-encryption-at-rest: true + * end + * + * cluster-configuration + * enable-zone-awareness: true + * instance-count: 4 + * + * zone-awareness-configuration + * availability-zone-count: 2 + * end + * end + * + * domain-endpoint-options + * enforce-https: true + * end + * + * advanced-security-options + * enable-advanced-security-options: true + * enable-internal-user-database: true + * + * master-user-options + * master-username: "masteruser" + * master-password: "MasterUser1!" + * end + * end + * + * access-policies: "access-policy.json" + * + * advanced-options: { + * "indices.query.bool.max_clause_count": "1026" + * } + * + * tags: { + * "description": "Test Domain" + * } + * + * vpc-options + * subnets: [ + * $(aws::subnet example-subnet-1), + * $(aws::subnet example-subnet-3) + * ] + * + * security-groups: [ + * $(aws::security-group example-security-group) + * ] + * end + * end + */ +@Type("opensearch-domain") +public class OpenSearchDomainResource extends AwsResource implements Copyable { + + private String openSearchVersion; + private String domainName; + private OpenSearchEbsOptions ebsOptions; + private OpenSearchClusterConfiguration clusterConfiguration; + private OpenSearchSnapshotOptions snapshotOptions; + private String accessPolicies; + private Map advancedOptions; + private OpenSearchNodeToNodeEncryptionOptions nodeToNodeEncryptionOptions; + private OpenSearchDomainEndpointOptions domainEndpointOptions; + private OpenSearchEncryptionAtRestOptions encryptionAtRestOptions; + private OpenSearchVpcOptions vpcOptions; + private OpenSearchAdvancedSecurityOptions advancedSecurityOptions; + private OpenSearchOffPeakWindowOptions offPeakWindowOptions; + private OpenSearchAutoTuneOptions autoTuneOptions; + private IPAddressType ipAddressType; + private Map tags; + + // Output + private String id; + private String arn; + + /** + * The version of OpenSearch. + */ + public String getOpenSearchVersion() { + return openSearchVersion; + } + + public void setOpenSearchVersion(String elasticSearchVersion) { + this.openSearchVersion = elasticSearchVersion; + } + + /** + * The name of the OpenSearch Domain. The name can be a combination of lowercase letters, numbers, or hyphens (``-``) and it must start with a lowercase letter. It can be between ``3`` to ``28`` characters in length. + */ + @Required + @Regex("^[a-z]([a-z]|[0-9]|-){2,27}$") + public String getDomainName() { + return domainName; + } + + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + /** + * The Elastic Block Storage options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchEbsOptions + */ + public OpenSearchEbsOptions getEbsOptions() { + return ebsOptions; + } + + @Updatable + public void setEbsOptions(OpenSearchEbsOptions ebsOptions) { + this.ebsOptions = ebsOptions; + } + + /** + * The OpenSearch Domain cluster configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchClusterConfiguration + */ + @Updatable + public OpenSearchClusterConfiguration getClusterConfiguration() { + return clusterConfiguration; + } + + public void setClusterConfiguration(OpenSearchClusterConfiguration clusterConfiguration) { + this.clusterConfiguration = clusterConfiguration; + } + + /** + * The automated snapshot time configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchSnapshotOptions + */ + @Updatable + public OpenSearchSnapshotOptions getSnapshotOptions() { + return snapshotOptions; + } + + public void setSnapshotOptions(OpenSearchSnapshotOptions snapshotOptions) { + this.snapshotOptions = snapshotOptions; + } + + /** + * The Json formatted IAM access policies. It can either be a JSON formatted string or the file path to a ``.json`` file. + */ + @Updatable + public String getAccessPolicies() { + if (accessPolicies != null && accessPolicies.contains(".json")) { + try (InputStream input = openInput(accessPolicies)) { + accessPolicies = PolicyResource.formatPolicy(IoUtils.toUtf8String(input)); + return accessPolicies; + } catch (IOException err) { + throw new GyroException(err.getMessage()); + } + } else { + return PolicyResource.formatPolicy(accessPolicies); + } + } + + public void setAccessPolicies(String accessPolicies) { + this.accessPolicies = accessPolicies; + } + + /** + * Configure advanced options for the cluster to allow references to indices in an HTTP request body. The valid options are + * ``rest.action.multi.allow_explicit_index``: ``true`` | ``false`` + * ``override_main_response_version``: ``true`` | ``false`` + * ``indices.fielddata.cache.size``: A number between ``1`` and ``100`` + * ``indices.query.bool.max_clause``: A number between ``1`` and ``2147483647``. + */ + @Updatable + public Map getAdvancedOptions() { + if (advancedOptions == null) { + advancedOptions = new HashMap<>(); + } + + return advancedOptions; + } + + public void setAdvancedOptions(Map advancedOptions) { + this.advancedOptions = advancedOptions; + } + + /** + * The node to node encryption options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchNodeToNodeEncryptionOptions + */ + public OpenSearchNodeToNodeEncryptionOptions getNodeToNodeEncryptionOptions() { + return nodeToNodeEncryptionOptions; + } + + public void setNodeToNodeEncryptionOptions(OpenSearchNodeToNodeEncryptionOptions nodeToNodeEncryptionOptions) { + this.nodeToNodeEncryptionOptions = nodeToNodeEncryptionOptions; + } + + /** + * The OpenSearch domain endpoint options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchDomainEndpointOptions + */ + @Updatable + public OpenSearchDomainEndpointOptions getDomainEndpointOptions() { + return domainEndpointOptions; + } + + public void setDomainEndpointOptions(OpenSearchDomainEndpointOptions domainEndpointOptions) { + this.domainEndpointOptions = domainEndpointOptions; + } + + /** + * The encryption at rest options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchEncryptionAtRestOptions + */ + public OpenSearchEncryptionAtRestOptions getEncryptionAtRestOptions() { + return encryptionAtRestOptions; + } + + public void setEncryptionAtRestOptions(OpenSearchEncryptionAtRestOptions encryptionAtRestOptions) { + this.encryptionAtRestOptions = encryptionAtRestOptions; + } + + /** + * The VPC options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchVpcOptions + */ + @Updatable + public OpenSearchVpcOptions getVpcOptions() { + return vpcOptions; + } + + public void setVpcOptions(OpenSearchVpcOptions vpcOptions) { + this.vpcOptions = vpcOptions; + } + + /** + * The advanced security options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchAdvancedSecurityOptions + */ + @Updatable + public OpenSearchAdvancedSecurityOptions getAdvancedSecurityOptions() { + return advancedSecurityOptions; + } + + public void setAdvancedSecurityOptions(OpenSearchAdvancedSecurityOptions advancedSecurityOptions) { + this.advancedSecurityOptions = advancedSecurityOptions; + } + + /** + * The off-peak window options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchOffPeakWindowOptions + */ + @Updatable + public OpenSearchOffPeakWindowOptions getOffPeakWindowOptions() { + return offPeakWindowOptions; + } + + public void setOffPeakWindowOptions(OpenSearchOffPeakWindowOptions offPeakWindowOptions) { + this.offPeakWindowOptions = offPeakWindowOptions; + } + + /** + * The IP address type for the OpenSearch domain. + */ + @Updatable + @ValidStrings({ "ipv4", "dualstack" }) + public IPAddressType getIpAddressType() { + return ipAddressType; + } + + public void setIpAddressType(IPAddressType ipAddressType) { + this.ipAddressType = ipAddressType; + } + + /** + * The auto-tune options configuration. + * + * @subresource gyro.aws.opensearch.OpenSearchAutoTuneOptions + */ + @Updatable + public OpenSearchAutoTuneOptions getAutoTuneOptions() { + return autoTuneOptions; + } + + public void setAutoTuneOptions(OpenSearchAutoTuneOptions autoTuneOptions) { + this.autoTuneOptions = autoTuneOptions; + } + + /** + * The list of tags. + */ + @Updatable + public Map getTags() { + if (tags == null) { + tags = new HashMap<>(); + } + + return tags; + } + + public void setTags(Map tags) { + this.tags = tags; + } + + /** + * The ID of the OpenSearch domain. + */ + @Output + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * The Amazon Resource Name of an OpenSearch domain. + */ + @Id + @Output + public String getArn() { + return arn; + } + + public void setArn(String arn) { + this.arn = arn; + } + + @Override + public void copyFrom(DomainStatus model) { + setId(model.domainId()); + setAccessPolicies(model.accessPolicies()); + setAdvancedOptions(model.advancedOptions()); + setDomainName(model.domainName()); + setOpenSearchVersion(model.engineVersion()); + setIpAddressType(model.ipAddressType()); + setArn(model.arn()); + + if (model.ebsOptions() != null) { + OpenSearchEbsOptions openSearchEbsOptions = newSubresource(OpenSearchEbsOptions.class); + openSearchEbsOptions.copyFrom(model.ebsOptions()); + setEbsOptions(openSearchEbsOptions); + } + + if (model.clusterConfig() != null) { + OpenSearchClusterConfiguration openSearchClusterConfiguration = newSubresource( + OpenSearchClusterConfiguration.class); + openSearchClusterConfiguration.copyFrom(model.clusterConfig()); + setClusterConfiguration(openSearchClusterConfiguration); + } + + if (model.snapshotOptions() != null) { + OpenSearchSnapshotOptions openSearchSnapshotOptions = newSubresource(OpenSearchSnapshotOptions.class); + openSearchSnapshotOptions.copyFrom(model.snapshotOptions()); + setSnapshotOptions(openSearchSnapshotOptions); + } + + if (model.nodeToNodeEncryptionOptions() != null) { + OpenSearchNodeToNodeEncryptionOptions openSearchNodeToNodeEncryptionOptions = newSubresource( + OpenSearchNodeToNodeEncryptionOptions.class); + openSearchNodeToNodeEncryptionOptions.copyFrom(model.nodeToNodeEncryptionOptions()); + setNodeToNodeEncryptionOptions(openSearchNodeToNodeEncryptionOptions); + } + + if (model.domainEndpointOptions() != null) { + OpenSearchDomainEndpointOptions openSearchDomainEndpointOptions = newSubresource( + OpenSearchDomainEndpointOptions.class); + openSearchDomainEndpointOptions.copyFrom(model.domainEndpointOptions()); + setDomainEndpointOptions(openSearchDomainEndpointOptions); + } + + if (model.encryptionAtRestOptions() != null) { + OpenSearchEncryptionAtRestOptions openSearchEncryptionAtRestOptions = newSubresource( + OpenSearchEncryptionAtRestOptions.class); + openSearchEncryptionAtRestOptions.copyFrom(model.encryptionAtRestOptions()); + setEncryptionAtRestOptions(openSearchEncryptionAtRestOptions); + } + + if (model.vpcOptions() != null) { + OpenSearchVpcOptions openSearchVpcOptions = newSubresource(OpenSearchVpcOptions.class); + openSearchVpcOptions.copyFrom(model.vpcOptions()); + setVpcOptions(openSearchVpcOptions); + } + + OpenSearchAdvancedSecurityOptions oldOptions = getAdvancedSecurityOptions(); + if (model.advancedSecurityOptions() != null) { + OpenSearchAdvancedSecurityOptions openSearchAdvancedSecurityOptions = newSubresource( + OpenSearchAdvancedSecurityOptions.class); + openSearchAdvancedSecurityOptions.copyFrom(model.advancedSecurityOptions()); + openSearchAdvancedSecurityOptions.setMasterUserOptions(oldOptions != null + ? oldOptions.getMasterUserOptions() + : null); + setAdvancedSecurityOptions(openSearchAdvancedSecurityOptions); + } + + if (model.offPeakWindowOptions() != null) { + OpenSearchOffPeakWindowOptions openSearchOffPeakWindowOptions = newSubresource( + OpenSearchOffPeakWindowOptions.class); + openSearchOffPeakWindowOptions.copyFrom(model.offPeakWindowOptions()); + setOffPeakWindowOptions(openSearchOffPeakWindowOptions); + } + + if (model.autoTuneOptions() != null) { + OpenSearchAutoTuneOptions openSearchAutoTuneOptions = newSubresource(OpenSearchAutoTuneOptions.class); + //openSearchAutoTuneOptions.copyFrom(model.autoTuneOptions()); + setAutoTuneOptions(openSearchAutoTuneOptions); + } + + if (model.advancedOptions() != null) { + setAdvancedOptions(model.advancedOptions()); + } + + getTags().clear(); + OpenSearchClient client = createClient(OpenSearchClient.class); + client.listTags(r -> r.arn(arn)).tagList() + .forEach(t -> getTags().put(t.key(), t.value())); + } + + @Override + public boolean refresh() { + OpenSearchClient client = createClient(OpenSearchClient.class); + DomainStatus domain = getOpenSearchDomain(client); + + if (domain == null) { + return false; + } + + copyFrom(domain); + return true; + } + + @Override + public void create(GyroUI ui, State state) throws Exception { + CreateDomainRequest.Builder builder = CreateDomainRequest.builder() + .domainName(getDomainName()) + .advancedOptions(getAdvancedOptions()) + .accessPolicies(getAccessPolicies()) + .engineVersion(getOpenSearchVersion()); + + if (getNodeToNodeEncryptionOptions() != null) { + builder = builder.nodeToNodeEncryptionOptions(getNodeToNodeEncryptionOptions().toNodeEncryptionOptions()); + } + + if (getEbsOptions() != null) { + builder = builder.ebsOptions(getEbsOptions().toEBSOptions()); + } + + if (getClusterConfiguration() != null) { + builder = builder.clusterConfig(getClusterConfiguration().toOpenSearchClusterConfig()); + } + + if (getSnapshotOptions() != null) { + builder = builder.snapshotOptions(getSnapshotOptions().toSnapshotOptions()); + } + + if (getEncryptionAtRestOptions() != null) { + builder = builder.encryptionAtRestOptions(getEncryptionAtRestOptions().toEncryptionAtRestOptions()); + } + + if (getAdvancedSecurityOptions() != null) { + builder = builder.advancedSecurityOptions(getAdvancedSecurityOptions().toAdvancedSecurityOptionsInput()); + } + + if (getVpcOptions() != null) { + builder = builder.vpcOptions(getVpcOptions().toVPCOptions()); + } + + if (getDomainEndpointOptions() != null) { + builder = builder.domainEndpointOptions(getDomainEndpointOptions().toDomainEndpointOptions()); + } + + if (getOffPeakWindowOptions() != null) { + builder = builder.offPeakWindowOptions(getOffPeakWindowOptions().toOffPeakWindowOptions()); + } + + if (getIpAddressType() != null) { + builder = builder.ipAddressType(getIpAddressType()); + } + + if (getAutoTuneOptions() != null) { + builder = builder.autoTuneOptions(getAutoTuneOptions().toAutoTuneOptionsInput()); + } + + OpenSearchClient client = createClient(OpenSearchClient.class); + CreateDomainResponse response = client.createDomain(builder.build()); + + setArn(response.domainStatus().arn()); + setId(response.domainStatus().domainId()); + + addTags(client); + + waitForAvailability(client, TimeoutSettings.Action.CREATE); + + OpenSearchAutoTuneOptions tuneOptions = getAutoTuneOptions(); + + OpenSearchMasterUserOptions masterUserOptions = Optional.ofNullable(getAdvancedSecurityOptions()) + .map(OpenSearchAdvancedSecurityOptions::getMasterUserOptions) + .orElse(null); + + copyFrom(getOpenSearchDomain(client)); + + if (masterUserOptions != null) { + getAdvancedSecurityOptions().setMasterUserOptions(masterUserOptions); + } + + setAutoTuneOptions(tuneOptions); + } + + @Override + public void update(GyroUI ui, State state, Resource current, Set changedFieldNames) throws Exception { + UpdateDomainConfigRequest.Builder builder = UpdateDomainConfigRequest.builder() + .domainName(getDomainName()); + + if (changedFieldNames.contains("ebs-options")) { + builder = builder.ebsOptions(getEbsOptions().toEBSOptions()); + } + + if (changedFieldNames.contains("access-policies")) { + builder = builder.accessPolicies(getAccessPolicies()); + } + + if (changedFieldNames.contains("advanced-options")) { + builder = builder.advancedOptions(getAdvancedOptions()); + } + + if (changedFieldNames.contains("cluster-configuration")) { + builder = builder.clusterConfig(getClusterConfiguration().toOpenSearchClusterConfig()); + } + + if (changedFieldNames.contains("snapshot-options")) { + builder = builder.snapshotOptions(getSnapshotOptions().toSnapshotOptions()); + } + + if (changedFieldNames.contains("advanced-security-options")) { + builder = builder.advancedSecurityOptions(getAdvancedSecurityOptions().toAdvancedSecurityOptionsInput()); + } + + if (changedFieldNames.contains("domain-endpoint-options")) { + builder = builder.domainEndpointOptions(getDomainEndpointOptions().toDomainEndpointOptions()); + } + + if (changedFieldNames.contains("vpc-options")) { + builder = builder.vpcOptions(getVpcOptions().toVPCOptions()); + } + + if (changedFieldNames.contains("off-peak-window-options")) { + builder = builder.offPeakWindowOptions(getOffPeakWindowOptions().toOffPeakWindowOptions()); + } + + if (changedFieldNames.contains("encryption-at-rest-options")) { + builder = builder.encryptionAtRestOptions(getEncryptionAtRestOptions().toEncryptionAtRestOptions()); + } + + if (changedFieldNames.contains("ip-address-type")) { + builder = builder.ipAddressType(getIpAddressType()); + } + + if (changedFieldNames.contains("auto-tune-options")) { + builder = builder.autoTuneOptions(getAutoTuneOptions().toAutoTuneOptions()); + } + + OpenSearchClient client = createClient(OpenSearchClient.class); + client.updateDomainConfig(builder.build()); + + OpenSearchDomainResource currentResource = (OpenSearchDomainResource) current; + + if (changedFieldNames.contains("tags")) { + if (!currentResource.getTags().isEmpty()) { + client.removeTags(r -> r.arn(currentResource.getArn()) + .tagKeys(new ArrayList<>(currentResource.getTags().keySet()))); + } + + addTags(client); + } + + waitForAvailability(client, TimeoutSettings.Action.UPDATE); + } + + @Override + public void delete(GyroUI ui, State state) throws Exception { + OpenSearchClient client = createClient(OpenSearchClient.class); + client.deleteDomain(r -> r.domainName(getDomainName())); + + Wait.atMost(20, TimeUnit.MINUTES) + .checkEvery(4, TimeUnit.MINUTES) + .resourceOverrides(this, TimeoutSettings.Action.DELETE) + .prompt(false) + .until(() -> getOpenSearchDomain(client) == null); + } + + private DomainStatus getOpenSearchDomain(OpenSearchClient client) { + DomainStatus domain = null; + + try { + DescribeDomainResponse response = client.describeDomain(r -> r.domainName(getDomainName())); + + if (response != null && response.domainStatus() != null && (response.domainStatus() + .deleted() + .equals(Boolean.FALSE) || (response.domainStatus() + .deleted() + .equals(Boolean.TRUE) && response.domainStatus().processing().equals(Boolean.TRUE))) + ) { + domain = response.domainStatus(); + } + } catch (ResourceNotFoundException ex) { + // Ignore + } + + return domain; + } + + private void addTags(OpenSearchClient client) { + if (!getTags().isEmpty()) { + client.addTags(r -> r.arn(getArn()).tagList(getTags().entrySet() + .stream() + .map(e -> Tag.builder().key(e.getKey()).value(e.getValue()).build()) + .collect(Collectors.toList()))); + } + } + + private void waitForAvailability(OpenSearchClient client, TimeoutSettings.Action action) { + Wait.atMost(20, TimeUnit.MINUTES) + .checkEvery(4, TimeUnit.MINUTES) + .resourceOverrides(this, action) + .prompt(false) + .until(() -> { + DomainStatus openSearchDomain = getOpenSearchDomain(client); + return openSearchDomain != null && openSearchDomain.processing().equals(Boolean.FALSE) + && openSearchDomain.created().equals(Boolean.TRUE); + }); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchEbsOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchEbsOptions.java new file mode 100644 index 000000000..994ca1739 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchEbsOptions.java @@ -0,0 +1,135 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Range; +import gyro.core.validation.Required; +import gyro.core.validation.ValidStrings; +import gyro.core.validation.ValidationError; +import software.amazon.awssdk.services.opensearch.model.EBSOptions; +import software.amazon.awssdk.services.opensearch.model.VolumeType; + +public class OpenSearchEbsOptions extends Diffable implements Copyable { + + private Boolean enableEbs; + private VolumeType volumeType; + private Integer volumeCount; + private Integer iops; + + /** + * Enable the EBS volume, a block level storage device. + */ + @Updatable + @Required + public Boolean getEnableEbs() { + return enableEbs; + } + + public void setEnableEbs(Boolean enableEbs) { + this.enableEbs = enableEbs; + } + + /** + * The volume type for the EBS-based storage. Can only be set if ``enable-ebs`` is set to ``true``. + */ + @Updatable + @ValidStrings({ "standard", "gp2", "gp3", "io1" }) + public VolumeType getVolumeType() { + return volumeType; + } + + public void setVolumeType(VolumeType volumeType) { + this.volumeType = volumeType; + } + + /** + * The size of the EBS volume. Can only be set if ``enable-ebs`` is set to ``true``. + */ + @Updatable + @Range(min = 10, max = 1024) + public Integer getVolumeCount() { + return volumeCount; + } + + public void setVolumeCount(Integer volumeCount) { + this.volumeCount = volumeCount; + } + + /** + * The baseline I/O performance for the EBS volume. Only used by Provisioned IOPS volumes. Can only be set if ``enable-ebs`` is set to ``true``. + */ + @Updatable + @Range(min = 1000, max = 16000) + public Integer getIops() { + return iops; + } + + public void setIops(Integer iops) { + this.iops = iops; + } + + @Override + public void copyFrom(EBSOptions model) { + setEnableEbs(model.ebsEnabled()); + setIops(model.iops()); + setVolumeCount(model.volumeSize()); + setVolumeType(model.volumeType()); + } + + @Override + public String primaryKey() { + return ""; + } + + EBSOptions toEBSOptions() { + return EBSOptions.builder().ebsEnabled(getEnableEbs()) + .volumeType(getVolumeType()) + .volumeSize(getVolumeCount()) + .iops(getIops()).build(); + } + + @Override + public List validate(Set configuredFields) { + List errors = new ArrayList<>(); + + if (getEnableEbs().equals(Boolean.FALSE) && (configuredFields.contains("volume-type") + || configuredFields.contains("volume-count") + || configuredFields.contains("iops"))) { + errors.add(new ValidationError( + this, + null, + "The 'volume-count', 'volume-type' and 'iops' can only be set if 'enable-ebs' is set to 'true'.")); + } + + if (getEnableEbs().equals(Boolean.TRUE) && !(configuredFields.contains("volume-type") + && configuredFields.contains("volume-count"))) { + errors.add(new ValidationError( + this, + null, + "The 'volume-count' and 'volume-type' are required if 'enable-ebs' is set to 'true'.")); + } + + return errors; + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchEncryptionAtRestOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchEncryptionAtRestOptions.java new file mode 100644 index 000000000..001c5ab51 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchEncryptionAtRestOptions.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import gyro.aws.Copyable; +import gyro.aws.kms.KmsKeyResource; +import gyro.core.resource.Diffable; +import gyro.core.validation.Required; +import gyro.core.validation.ValidationError; +import software.amazon.awssdk.services.opensearch.model.EncryptionAtRestOptions; + +public class OpenSearchEncryptionAtRestOptions extends Diffable implements Copyable { + + private Boolean enableEncryptionAtRest; + private KmsKeyResource kmsKeyResource; + + /** + * Enable encryption at rest to prevent unauthorized access to the data. + */ + @Required + public Boolean getEnableEncryptionAtRest() { + return enableEncryptionAtRest; + } + + public void setEnableEncryptionAtRest(Boolean enableEncryptionAtRest) { + this.enableEncryptionAtRest = enableEncryptionAtRest; + } + + /** + * The KMS key resource for encryption options. Can only be set if ``enable-encryption-at-rest`` is set to ``true``. + */ + public KmsKeyResource getKmsKeyResource() { + return kmsKeyResource; + } + + public void setKmsKeyResource(KmsKeyResource kmsKeyResource) { + this.kmsKeyResource = kmsKeyResource; + } + + @Override + public void copyFrom(EncryptionAtRestOptions model) { + setEnableEncryptionAtRest(model.enabled()); + setKmsKeyResource(findById(KmsKeyResource.class, model.kmsKeyId())); + } + + @Override + public String primaryKey() { + return ""; + } + + EncryptionAtRestOptions toEncryptionAtRestOptions() { + EncryptionAtRestOptions.Builder builder = EncryptionAtRestOptions.builder() + .enabled(getEnableEncryptionAtRest()); + + if (getKmsKeyResource() != null) { + builder.kmsKeyId(getKmsKeyResource().getId()); + } + + return builder.build(); + } + + @Override + public List validate(Set configuredFields) { + List errors = new ArrayList<>(); + + if (getEnableEncryptionAtRest().equals(Boolean.FALSE) && configuredFields.contains("kms-key-resource")) { + errors.add(new ValidationError( + this, + null, + "The 'kms-key-resource' can only be set if 'enable-encryption-at-rest' is set to 'true'.")); + } + + return errors; + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchMasterUserOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchMasterUserOptions.java new file mode 100644 index 000000000..8302afa3c --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchMasterUserOptions.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.ConflictsWith; +import software.amazon.awssdk.services.opensearch.model.MasterUserOptions; + +public class OpenSearchMasterUserOptions extends Diffable implements Copyable { + + private String masterUsername; + private String masterPassword; + private String masterUserArn; + + /** + * The master username stored in the domain's internal database. + */ + @Updatable + @ConflictsWith("master-user-arn") + public String getMasterUsername() { + return masterUsername; + } + + public void setMasterUsername(String masterUsername) { + this.masterUsername = masterUsername; + } + + /** + * The master password stored in the domain's internal database. + */ + @Updatable + @ConflictsWith("master-user-arn") + public String getMasterPassword() { + return masterPassword; + } + + public void setMasterPassword(String masterPassword) { + this.masterPassword = masterPassword; + } + + /** + * The master user's Amazon Resource Number. + */ + @Updatable + @ConflictsWith({ "master-username", "master-password" }) + public String getMasterUserArn() { + return masterUserArn; + } + + public void setMasterUserArn(String masterUserArn) { + this.masterUserArn = masterUserArn; + } + + @Override + public void copyFrom(MasterUserOptions model) { + setMasterUsername(model.masterUserName()); + setMasterPassword(model.masterUserPassword()); + setMasterUserArn(model.masterUserARN()); + } + + @Override + public String primaryKey() { + return ""; + } + + MasterUserOptions toMasterUserOptions() { + return MasterUserOptions.builder() + .masterUserName(getMasterUsername()) + .masterUserPassword(getMasterPassword()) + .masterUserARN(getMasterUserArn()) + .build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchNodeToNodeEncryptionOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchNodeToNodeEncryptionOptions.java new file mode 100644 index 000000000..b4ece3f3e --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchNodeToNodeEncryptionOptions.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.opensearch.model.NodeToNodeEncryptionOptions; + +public class OpenSearchNodeToNodeEncryptionOptions extends Diffable + implements Copyable { + + private Boolean enableNodeToNodeEncryption; + + /** + * Enable node to node encryption to prevent potential interception of traffic between OpenSearch nodes. + */ + @Required + public Boolean getEnableNodeToNodeEncryption() { + return enableNodeToNodeEncryption; + } + + public void setEnableNodeToNodeEncryption(Boolean enableNodeToNodeEncryption) { + this.enableNodeToNodeEncryption = enableNodeToNodeEncryption; + } + + @Override + public void copyFrom(NodeToNodeEncryptionOptions model) { + setEnableNodeToNodeEncryption(model.enabled()); + } + + @Override + public String primaryKey() { + return ""; + } + + NodeToNodeEncryptionOptions toNodeEncryptionOptions() { + return NodeToNodeEncryptionOptions.builder().enabled(getEnableNodeToNodeEncryption()).build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchOffPeakWindowOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchOffPeakWindowOptions.java new file mode 100644 index 000000000..8856eba4a --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchOffPeakWindowOptions.java @@ -0,0 +1,101 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.DependsOn; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.opensearch.model.OffPeakWindow; +import software.amazon.awssdk.services.opensearch.model.OffPeakWindowOptions; +import software.amazon.awssdk.services.opensearch.model.WindowStartTime; + +public class OpenSearchOffPeakWindowOptions extends Diffable implements Copyable { + + private Boolean offPeakEnabled; + private Long offPeakWindowHour; + private Long offPeakWindowMinutes; + + /** + * Enable off-peak window for the domain. + */ + @Required + @Updatable + public Boolean getOffPeakEnabled() { + return offPeakEnabled; + } + + public void setOffPeakEnabled(Boolean offPeakEnabled) { + this.offPeakEnabled = offPeakEnabled; + } + + /** + * The hour at which the off-peak window starts. + */ + @Updatable + @DependsOn("off-peak-window-minutes") + public Long getOffPeakWindowHour() { + return offPeakWindowHour; + } + + public void setOffPeakWindowHour(Long offPeakWindowHour) { + this.offPeakWindowHour = offPeakWindowHour; + } + + @Updatable + @DependsOn("off-peak-window-hour") + public Long getOffPeakWindowMinutes() { + return offPeakWindowMinutes; + } + + public void setOffPeakWindowMinutes(Long offPeakWindowMinutes) { + this.offPeakWindowMinutes = offPeakWindowMinutes; + } + + @Override + public void copyFrom(OffPeakWindowOptions model) { + setOffPeakEnabled(model.enabled()); + setOffPeakWindowHour(null); + setOffPeakWindowMinutes(null); + if (model.offPeakWindow() != null) { + setOffPeakWindowHour(model.offPeakWindow().windowStartTime().hours()); + setOffPeakWindowMinutes(model.offPeakWindow().windowStartTime().minutes()); + } + } + + @Override + public String primaryKey() { + return ""; + } + + OffPeakWindowOptions toOffPeakWindowOptions() { + OffPeakWindowOptions.Builder enabled = OffPeakWindowOptions.builder() + .enabled(getOffPeakEnabled()); + + if (getOffPeakEnabled()) { + enabled.offPeakWindow(OffPeakWindow.builder() + .windowStartTime(WindowStartTime.builder() + .hours(getOffPeakWindowHour()) + .minutes(getOffPeakWindowMinutes()) + .build()) + .build()); + } + + return enabled.build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchSnapshotOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchSnapshotOptions.java new file mode 100644 index 000000000..84bf56e9f --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchSnapshotOptions.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Range; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.opensearch.model.SnapshotOptions; + +public class OpenSearchSnapshotOptions extends Diffable implements Copyable { + + private Integer automatedSnapshotStartHour; + + /** + * The hour, in UTC format, when the service takes a daily automated snapshot of the specified OpenSearch domain. + */ + @Required + @Updatable + @Range(min = 0, max = 23) + public Integer getAutomatedSnapshotStartHour() { + return automatedSnapshotStartHour; + } + + public void setAutomatedSnapshotStartHour(Integer automatedSnapshotStartHour) { + this.automatedSnapshotStartHour = automatedSnapshotStartHour; + } + + @Override + public void copyFrom(SnapshotOptions model) { + setAutomatedSnapshotStartHour(model.automatedSnapshotStartHour()); + } + + @Override + public String primaryKey() { + return ""; + } + + SnapshotOptions toSnapshotOptions() { + return SnapshotOptions.builder().automatedSnapshotStartHour(getAutomatedSnapshotStartHour()).build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchVpcEndpointFinder.java b/src/main/java/gyro/aws/opensearch/OpenSearchVpcEndpointFinder.java new file mode 100644 index 000000000..44355dc56 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchVpcEndpointFinder.java @@ -0,0 +1,63 @@ +package gyro.aws.opensearch; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import gyro.aws.AwsFinder; +import gyro.core.Type; +import software.amazon.awssdk.services.opensearch.OpenSearchClient; +import software.amazon.awssdk.services.opensearch.model.DescribeVpcEndpointsResponse; +import software.amazon.awssdk.services.opensearch.model.ListVpcEndpointsResponse; +import software.amazon.awssdk.services.opensearch.model.ResourceNotFoundException; +import software.amazon.awssdk.services.opensearch.model.VpcEndpoint; +import software.amazon.awssdk.services.opensearch.model.VpcEndpointSummary; +import software.amazon.awssdk.utils.builder.SdkBuilder; + +/** + * Query OpenSearch VPC endpoint. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * vpc-endpoint: $(external-query aws::opensearch-vpc-endpoint { id: ''}) + */ +@Type("opensearch-vpc-endpoint") +public class OpenSearchVpcEndpointFinder + extends AwsFinder { + + @Override + protected List findAllAws(OpenSearchClient client) { + List vpcEndpoints = new ArrayList<>(); + ListVpcEndpointsResponse response = client.listVpcEndpoints(SdkBuilder::build); + if (response.hasVpcEndpointSummaryList() && !response.vpcEndpointSummaryList().isEmpty()) { + vpcEndpoints.addAll(client.describeVpcEndpoints(r -> r + .vpcEndpointIds(response.vpcEndpointSummaryList().stream().map(VpcEndpointSummary::vpcEndpointId) + .collect(Collectors.toList()))).vpcEndpoints()); + } + + return vpcEndpoints; + } + + @Override + protected List findAws(OpenSearchClient client, Map filters) { + List vpcEndpoints = new ArrayList<>(); + if (filters.containsKey("id")) { + try { + DescribeVpcEndpointsResponse response = client.describeVpcEndpoints(r -> r + .vpcEndpointIds(filters.get("id"))); + + if (response.hasVpcEndpoints() && !response.vpcEndpoints().isEmpty()) { + vpcEndpoints.addAll(response.vpcEndpoints()); + } + } catch (ResourceNotFoundException ex) { + // Ignore + } + } + + return vpcEndpoints; + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchVpcEndpointResource.java b/src/main/java/gyro/aws/opensearch/OpenSearchVpcEndpointResource.java new file mode 100644 index 000000000..ee0f1f497 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchVpcEndpointResource.java @@ -0,0 +1,190 @@ +package gyro.aws.opensearch; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import gyro.aws.AwsResource; +import gyro.aws.Copyable; +import gyro.core.GyroUI; +import gyro.core.TimeoutSettings; +import gyro.core.Type; +import gyro.core.Wait; +import gyro.core.resource.Id; +import gyro.core.resource.Output; +import gyro.core.resource.Resource; +import gyro.core.resource.Updatable; +import gyro.core.scope.State; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.opensearch.OpenSearchClient; +import software.amazon.awssdk.services.opensearch.model.CreateVpcEndpointResponse; +import software.amazon.awssdk.services.opensearch.model.DescribeVpcEndpointsResponse; +import software.amazon.awssdk.services.opensearch.model.ResourceNotFoundException; +import software.amazon.awssdk.services.opensearch.model.VpcEndpoint; +import software.amazon.awssdk.services.opensearch.model.VpcEndpointStatus; + +/** + * Create an OpenSearch VPC endpoint. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * aws::opensearch-vpc-endpoint open-search-vpc-endpoint-example + * domain: $(aws::opensearch-domain open-search-domain-example) + * vpc-options + * subnets: [ + * $(aws::subnet subnet-example-1), + * $(aws::subnet subnet-example-2) + * ] + * security-groups: [ + * $(aws::security-group security-group-example-1), + * $(aws::security-group security-group-example-2) + * ] + * end + * end + */ +@Type("opensearch-vpc-endpoint") +public class OpenSearchVpcEndpointResource extends AwsResource implements Copyable { + + private OpenSearchDomainResource domain; + private OpenSearchVpcOptions vpcOptions; + private String endpoint; + private String vpcEndpointId; + + /** + * The domain for the VPC endpoint. + */ + @Required + public OpenSearchDomainResource getDomain() { + return domain; + } + + public void setDomain(OpenSearchDomainResource domain) { + this.domain = domain; + } + + /** + * The VPC options for the VPC endpoint. + * + * @subresource gyro.aws.opensearch.OpenSearchVpcOptions + */ + @Required + @Updatable + public OpenSearchVpcOptions getVpcOptions() { + return vpcOptions; + } + + public void setVpcOptions(OpenSearchVpcOptions vpcOptions) { + this.vpcOptions = vpcOptions; + } + + /** + * The endpoint connection id for the VPC endpoint. + */ + @Output + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + /** + * The VPC endpoint id. + */ + @Id + @Output + public String getVpcEndpointId() { + return vpcEndpointId; + } + + public void setVpcEndpointId(String vpcEndpointId) { + this.vpcEndpointId = vpcEndpointId; + } + + @Override + public void copyFrom(VpcEndpoint model) { + setEndpoint(model.endpoint()); + setDomain(findById(OpenSearchDomainResource.class, model.domainArn())); + setVpcEndpointId(model.vpcEndpointId()); + + OpenSearchVpcOptions options = newSubresource(OpenSearchVpcOptions.class); + options.copyFrom(model.vpcOptions()); + setVpcOptions(options); + } + + @Override + public boolean refresh() { + OpenSearchClient client = createClient(OpenSearchClient.class); + VpcEndpoint vpcEndpoint = getVpcEndpoint(client); + if (vpcEndpoint == null) { + return false; + } + + copyFrom(vpcEndpoint); + return true; + } + + @Override + public void create(GyroUI ui, State state) throws Exception { + OpenSearchClient client = createClient(OpenSearchClient.class); + CreateVpcEndpointResponse response = client.createVpcEndpoint(r -> r + .domainArn(getDomain().getArn()) + .vpcOptions(getVpcOptions().toVPCOptions())); + + setVpcEndpointId(response.vpcEndpoint().vpcEndpointId()); + + Wait.atMost(10, TimeUnit.MINUTES) + .checkEvery(30, TimeUnit.SECONDS) + .resourceOverrides(this, TimeoutSettings.Action.CREATE) + .prompt(false) + .until(() -> { + VpcEndpoint vpcEndpoint = getVpcEndpoint(client); + return vpcEndpoint != null && vpcEndpoint.status().equals(VpcEndpointStatus.ACTIVE); + }); + } + + @Override + public void update(GyroUI ui, State state, Resource current, Set changedFieldNames) throws Exception { + OpenSearchClient client = createClient(OpenSearchClient.class); + client.updateVpcEndpoint(r -> r.vpcEndpointId(getVpcEndpointId()) + .vpcOptions(getVpcOptions().toVPCOptions())); + + Wait.atMost(10, TimeUnit.MINUTES) + .checkEvery(30, TimeUnit.SECONDS) + .resourceOverrides(this, TimeoutSettings.Action.UPDATE) + .prompt(false) + .until(() -> { + VpcEndpoint vpcEndpoint = getVpcEndpoint(client); + return vpcEndpoint != null && vpcEndpoint.status().equals(VpcEndpointStatus.ACTIVE); + }); + } + + @Override + public void delete(GyroUI ui, State state) throws Exception { + OpenSearchClient client = createClient(OpenSearchClient.class); + client.deleteVpcEndpoint(r -> r.vpcEndpointId(getVpcEndpointId())); + + Wait.atMost(10, TimeUnit.MINUTES) + .checkEvery(30, TimeUnit.SECONDS) + .resourceOverrides(this, TimeoutSettings.Action.DELETE) + .prompt(false) + .until(() -> getVpcEndpoint(client) == null); + } + + private VpcEndpoint getVpcEndpoint(OpenSearchClient client) { + VpcEndpoint vpcEndpoint = null; + try { + DescribeVpcEndpointsResponse response = client.describeVpcEndpoints(r -> r.vpcEndpointIds(getVpcEndpointId())); + if (response.hasVpcEndpoints() && !response.vpcEndpoints().isEmpty()) { + vpcEndpoint = response.vpcEndpoints().get(0); + } + } catch (ResourceNotFoundException ex) { + // ignore + } + + return vpcEndpoint; + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchVpcOptions.java b/src/main/java/gyro/aws/opensearch/OpenSearchVpcOptions.java new file mode 100644 index 000000000..3ea64c979 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchVpcOptions.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import gyro.aws.Copyable; +import gyro.aws.ec2.SecurityGroupResource; +import gyro.aws.ec2.SubnetResource; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.opensearch.model.VPCDerivedInfo; +import software.amazon.awssdk.services.opensearch.model.VPCOptions; + +public class OpenSearchVpcOptions extends Diffable implements Copyable { + + private Set subnets; + private Set securityGroups; + + /** + * The list of subnets in the same region for the VPC endpoint. One subnet per availability zone. + */ + @Required + @Updatable + public Set getSubnets() { + if (subnets == null) { + subnets = new HashSet<>(); + } + + return subnets; + } + + public void setSubnets(Set subnets) { + this.subnets = subnets; + } + + /** + * The list if security groups for the VPC endpoint that need to access the domain. + */ + @Required + @Updatable + public Set getSecurityGroups() { + if (securityGroups == null) { + securityGroups = new HashSet<>(); + } + + return securityGroups; + } + + public void setSecurityGroups(Set securityGroups) { + this.securityGroups = securityGroups; + } + + @Override + public void copyFrom(VPCDerivedInfo model) { + setSecurityGroups(model.securityGroupIds() + .stream() + .map(s -> findById(SecurityGroupResource.class, s)) + .collect( + Collectors.toSet())); + setSubnets(model.subnetIds() + .stream() + .map(s -> findById(SubnetResource.class, s)) + .collect(Collectors.toSet())); + } + + @Override + public String primaryKey() { + return ""; + } + + VPCOptions toVPCOptions() { + return VPCOptions.builder() + .subnetIds(getSubnets().stream().map(SubnetResource::getId).collect(Collectors.toSet())) + .securityGroupIds(getSecurityGroups().stream() + .map(SecurityGroupResource::getId) + .collect(Collectors.toSet())) + .build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/OpenSearchZoneAwarenessConfiguration.java b/src/main/java/gyro/aws/opensearch/OpenSearchZoneAwarenessConfiguration.java new file mode 100644 index 000000000..ae0fd19b6 --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/OpenSearchZoneAwarenessConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gyro.aws.opensearch; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Updatable; +import gyro.core.validation.Required; +import software.amazon.awssdk.services.opensearch.model.ZoneAwarenessConfig; + +public class OpenSearchZoneAwarenessConfiguration extends Diffable implements Copyable { + + private Integer availabilityZoneCount; + + /** + * The number of availability zones for a domain when zone awareness is enabled. + */ + @Required + @Updatable + public Integer getAvailabilityZoneCount() { + return availabilityZoneCount; + } + + public void setAvailabilityZoneCount(Integer availabilityZoneCount) { + this.availabilityZoneCount = availabilityZoneCount; + } + + @Override + public void copyFrom(ZoneAwarenessConfig model) { + setAvailabilityZoneCount(model.availabilityZoneCount()); + } + + @Override + public String primaryKey() { + return ""; + } + + ZoneAwarenessConfig toZoneAwarenessConfig() { + return ZoneAwarenessConfig.builder().availabilityZoneCount(getAvailabilityZoneCount()).build(); + } +} diff --git a/src/main/java/gyro/aws/opensearch/package-info.java b/src/main/java/gyro/aws/opensearch/package-info.java new file mode 100644 index 000000000..99808adef --- /dev/null +++ b/src/main/java/gyro/aws/opensearch/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2024, Brightspot. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@DocGroup("OpenSearch") +package gyro.aws.opensearch; + +import gyro.core.resource.DocGroup;