diff --git a/docs/data-sources/cluster_rosa_hcp.md b/docs/data-sources/cluster_rosa_hcp.md index fdbc795d..7571db21 100644 --- a/docs/data-sources/cluster_rosa_hcp.md +++ b/docs/data-sources/cluster_rosa_hcp.md @@ -37,9 +37,11 @@ data "rhcs_cluster_rosa_hcp" "cluster" { - `api_url` (String) URL of the API server. - `availability_zones` (List of String) Availability zones. This attribute specifically applies to the Worker Machine Pool and becomes irrelevant once the resource is created. Any modifications to the initial Machine Pool should be made through the Terraform imported Machine Pool resource. For more details, refer to [Worker Machine Pool in ROSA Cluster](../guides/worker-machine-pool.md) - `aws_account_id` (String) Identifier of the AWS account. After the creation of the resource, it is not possible to update the attribute value. +- `aws_additional_allowed_principals` (List of String) AWS additional allowed principals. - `aws_additional_compute_security_group_ids` (List of String) AWS additional compute security group ids. After the creation of the resource, it is not possible to update the attribute value. - `aws_billing_account_id` (String) Identifier of the AWS account for billing. After the creation of the resource, it is not possible to update the attribute value. - `aws_subnet_ids` (List of String) AWS subnet IDs. After the creation of the resource, it is not possible to update the attribute value. +- `base_dns_domain` (String) Base DNS domain name previously reserved, e.g. '1vo8.p3.openshiftapps.com'. After the creation of the resource, it is not possible to update the attribute value. - `channel_group` (String) This attribute is not supported for cluster data source. Therefore, it will not be displayed as an output of the datasource - `cloud_region` (String) Cloud region identifier, for example 'us-east-1'. - `compute_machine_type` (String) This attribute is not supported for cluster data source. Therefore, it will not be displayed as an output of the datasource @@ -63,6 +65,7 @@ data "rhcs_cluster_rosa_hcp" "cluster" { - `proxy` (Attributes) proxy (see [below for nested schema](#nestedatt--proxy)) - `replicas` (Number) This attribute is not supported for cluster data source. Therefore, it will not be displayed as an output of the datasource - `service_cidr` (String) Block of IP addresses for the cluster service network. After the creation of the resource, it is not possible to update the attribute value. +- `shared_vpc` (Attributes) Shared VPC configuration.After the creation of the resource, it is not possible to update the attribute value. (see [below for nested schema](#nestedatt--shared_vpc)) - `state` (String) State of the cluster. - `sts` (Attributes) STS configuration. (see [below for nested schema](#nestedatt--sts)) - `tags` (Map of String) Apply user defined tags to all cluster resources created in AWS. After the creation of the resource, it is not possible to update the attribute value. @@ -122,6 +125,17 @@ Read-Only: - `no_proxy` (String) No proxy. + +### Nested Schema for `shared_vpc` + +Read-Only: + +- `ingress_private_hosted_zone_id` (String) ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'. +- `internal_communication_private_hosted_zone_id` (String) ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'. +- `route53_role_arn` (String) AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage Route 53 DNS records in private Route 53 hosted zone associated with intended shared VPC. +- `vpce_role_arn` (String) AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage VPC Endpoints associated with intended shared VPC. + + ### Nested Schema for `sts` diff --git a/docs/resources/cluster_rosa_hcp.md b/docs/resources/cluster_rosa_hcp.md index 00e51318..119c327f 100644 --- a/docs/resources/cluster_rosa_hcp.md +++ b/docs/resources/cluster_rosa_hcp.md @@ -60,7 +60,9 @@ resource "rhcs_cluster_rosa_hcp" "rosa_sts_cluster" { ### Optional - `admin_credentials` (Attributes) Admin user credentials. After the creation of the resource, it is not possible to update the attribute value. (see [below for nested schema](#nestedatt--admin_credentials)) +- `aws_additional_allowed_principals` (List of String) AWS additional allowed principals. - `aws_additional_compute_security_group_ids` (List of String) AWS additional compute security group ids. +- `base_dns_domain` (String) Base DNS domain name previously reserved, e.g. '1vo8.p3.openshiftapps.com'. After the creation of the resource, it is not possible to update the attribute value. - `channel_group` (String) Name of the channel group where you select the OpenShift cluster version, for example 'stable'. For ROSA, only 'stable' is supported. After the creation of the resource, it is not possible to update the attribute value. - `compute_machine_type` (String) Identifies the machine type used by the initial worker nodes, for example `m5.xlarge`. Use the `rhcs_machine_types` data source to find the possible values. This attribute specifically applies to the Worker Machine Pool and becomes irrelevant once the resource is created. Any modifications to the initial Machine Pool should be made through the Terraform imported Machine Pool resource. For more details, refer to [Worker Machine Pool in ROSA Cluster](../guides/worker-machine-pool.md) - `create_admin_user` (Boolean) Indicates if create cluster admin user. Set it true to create cluster admin user with default username `cluster-admin` and generated password. It will be ignored if `admin_credentials` is set.After the creation of the resource, it is not possible to update the attribute value. @@ -80,6 +82,7 @@ resource "rhcs_cluster_rosa_hcp" "rosa_sts_cluster" { - `registry_config` (Attributes) Registry configuration for this cluster. (see [below for nested schema](#nestedatt--registry_config)) - `replicas` (Number) Number of worker/compute nodes to provision. Requires that the number supplied be a multiple of the number of private subnets. This attribute specifically applies to the Worker Machine Pool and becomes irrelevant once the resource is created. Any modifications to the initial Machine Pool should be made through the Terraform imported Machine Pool resource. For more details, refer to [Worker Machine Pool in ROSA Cluster](../guides/worker-machine-pool.md) - `service_cidr` (String) Block of IP addresses for the cluster service network. After the creation of the resource, it is not possible to update the attribute value. +- `shared_vpc` (Attributes) Shared VPC configuration.After the creation of the resource, it is not possible to update the attribute value. (see [below for nested schema](#nestedatt--shared_vpc)) - `tags` (Map of String) Apply user defined tags to all cluster resources created in AWS. After the creation of the resource, it is not possible to update the attribute value. - `upgrade_acknowledgements_for` (String) Indicates acknowledgement of agreements required to upgrade the cluster version between minor versions (e.g. a value of "4.12" indicates acknowledgement of any agreements required to upgrade to OpenShift 4.12.z from 4.11 or before). - `version` (String) Desired version of OpenShift for the cluster, for example '4.11.0'. If version is greater than the currently running version, an upgrade will be scheduled. @@ -173,3 +176,18 @@ Optional: - `allowed_registries` (List of String) allowed_registries: registries for which image pull and push actions are allowed. To specify all subdomains, add the asterisk (*) wildcard character as a prefix to the domain name. For example, *.example.com. You can specify an individual repository within a registry. For example: reg1.io/myrepo/myapp:latest. All other registries are blocked. Mutually exclusive with `BlockedRegistries` - `blocked_registries` (List of String) blocked_registries: registries for which image pull and push actions are denied. To specify all subdomains, add the asterisk (*) wildcard character as a prefix to the domain name. For example, *.example.com. You can specify an individual repository within a registry. For example: reg1.io/myrepo/myapp:latest. All other registries are allowed. Mutually exclusive with `AllowedRegistries` - `insecure_registries` (List of String) insecure_registries are registries which do not have a valid TLS certificate or only support HTTP connections. To specify all subdomains, add the asterisk (*) wildcard character as a prefix to the domain name. For example, *.example.com. You can specify an individual repository within a registry. For example: reg1.io/myrepo/myapp:latest. + + + + +### Nested Schema for `shared_vpc` + +Required: + +- `ingress_private_hosted_zone_id` (String) ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'. +- `route53_role_arn` (String) AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage Route 53 DNS records in private Route 53 hosted zone associated with intended shared VPC. +- `vpce_role_arn` (String) AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage VPC Endpoints associated with intended shared VPC. + +Optional: + +- `internal_communication_private_hosted_zone_id` (String) ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'. diff --git a/internal/ocm/resource/cluster.go b/internal/ocm/resource/cluster.go index fad5a6ca..a33bcaa4 100644 --- a/internal/ocm/resource/cluster.go +++ b/internal/ocm/resource/cluster.go @@ -13,7 +13,7 @@ import ( ) var privateHostedZoneRoleArnRE = regexp.MustCompile( - `^arn:aws:iam::\d{12}:role\/[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+$`, + `^arn:aws:iam::\d{12}:role(?:(?:\/?.+\/?)?)(?:\/[0-9A-Za-z\\+\\.@_,-]{1,64})$`, ) type Cluster struct { @@ -142,9 +142,11 @@ func (c *Cluster) CreateAWSBuilder(clusterTopology rosaTypes.ClusterTopology, isPrivateLink bool, awsAccountID *string, awsBillingAccountId *string, stsBuilder *cmv1.STSBuilder, awsSubnetIDs []string, privateHostedZoneID *string, privateHostedZoneRoleARN *string, + hcpInternalCommunicationPrivateHostedZoneId *string, vpceRoleArn *string, additionalComputeSecurityGroupIds []string, additionalInfraSecurityGroupIds []string, - additionalControlPlaneSecurityGroupIds []string) error { + additionalControlPlaneSecurityGroupIds []string, + additionalAllowedPrincipals []string) error { if clusterTopology == rosaTypes.Hcp && awsSubnetIDs == nil { return errors.New("Hosted Control Plane clusters must have a pre-configure VPC. Make sure to specify the subnet ids.") @@ -209,14 +211,30 @@ func (c *Cluster) CreateAWSBuilder(clusterTopology rosaTypes.ClusterTopology, } if privateHostedZoneID != nil && privateHostedZoneRoleARN != nil { - if !privateHostedZoneRoleArnRE.MatchString(*privateHostedZoneRoleARN) { - return errors.New(fmt.Sprintf("Expected a valid value for PrivateHostedZoneRoleARN matching %s. Got %s", privateHostedZoneRoleArnRE, *privateHostedZoneRoleARN)) - } if awsSubnetIDs == nil || stsBuilder == nil { - return errors.New("PrivateHostedZone parameters require STS and SubnetIDs configurations.") + return errors.New("Shared VPC parameters require STS and SubnetIDs configurations.") + } + privateRoleArnField := "PrivateHostedZoneRoleARN" + if clusterTopology == rosaTypes.Hcp { + privateRoleArnField = "Route53RoleArn" + } + if !privateHostedZoneRoleArnRE.MatchString(*privateHostedZoneRoleARN) { + return errors.New(fmt.Sprintf("Expected a valid value for %s matching %s. Got %s", + privateRoleArnField, privateHostedZoneRoleArnRE, *privateHostedZoneRoleARN)) } awsBuilder.PrivateHostedZoneID(*privateHostedZoneID) awsBuilder.PrivateHostedZoneRoleARN(*privateHostedZoneRoleARN) + if clusterTopology == rosaTypes.Hcp && hcpInternalCommunicationPrivateHostedZoneId != nil && vpceRoleArn != nil { + if !privateHostedZoneRoleArnRE.MatchString(*vpceRoleArn) { + return errors.New(fmt.Sprintf("Expected a valid value for VpcEndpointRoleArn matching %s. Got %s", privateHostedZoneRoleArnRE, *vpceRoleArn)) + } + awsBuilder.HcpInternalCommunicationHostedZoneId(*hcpInternalCommunicationPrivateHostedZoneId) + awsBuilder.VpcEndpointRoleArn(*vpceRoleArn) + } + } + + if additionalAllowedPrincipals != nil { + awsBuilder.AdditionalAllowedPrincipals(additionalAllowedPrincipals...) } c.clusterBuilder.AWS(awsBuilder) diff --git a/internal/ocm/resource/cluster_test.go b/internal/ocm/resource/cluster_test.go index b8189dca..fb310f6e 100644 --- a/internal/ocm/resource/cluster_test.go +++ b/internal/ocm/resource/cluster_test.go @@ -203,17 +203,17 @@ var _ = Describe("Cluster", func() { }) Context("CreateAWSBuilder validation", func() { It("PrivateLink true subnets IDs empty - failure", func() { - err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, nil, nil, nil, true, nil, nil, nil, nil, nil, nil, nil, nil, nil) + err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, nil, nil, nil, true, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("Clusters with PrivateLink must have a pre-configured VPC. Make sure to specify the subnet ids.")) }) It("PrivateLink false invalid kmsKeyARN - failure", func() { - err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, nil, pointer("test"), nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil) + err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, nil, pointer("test"), nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal(fmt.Sprintf("expected the kms-key-arn: %s to match %s", "test", kmsArnRegexpValidator.KmsArnRE))) }) It("PrivateLink false empty kmsKeyARN - success", func() { - err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, nil, nil, nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil) + err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, nil, nil, nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) Expect(err).NotTo(HaveOccurred()) ocmCluster, err := cluster.Build() Expect(err).NotTo(HaveOccurred()) @@ -228,7 +228,7 @@ var _ = Describe("Cluster", func() { }) It("PrivateLink false invalid Ec2MetadataHttpTokens - success", func() { // TODO Need to add validation for Ec2MetadataHttpTokens - err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, pointer("test"), nil, nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil) + err := cluster.CreateAWSBuilder(rosaTypes.Classic, nil, pointer("test"), nil, nil, false, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) Expect(err).NotTo(HaveOccurred()) ocmCluster, err := cluster.Build() Expect(err).NotTo(HaveOccurred()) @@ -257,7 +257,7 @@ var _ = Describe("Cluster", func() { err := cluster.CreateAWSBuilder(rosaTypes.Classic, map[string]string{"key1": "val1"}, pointer(string(cmv1.Ec2MetadataHttpTokensRequired)), pointer(validKmsKey), nil, true, pointer(accountID), nil, - sts, subnets, nil, nil, nil, nil, nil) + sts, subnets, nil, nil, nil, nil, nil, nil, nil, nil) Expect(err).NotTo(HaveOccurred()) ocmCluster, err := cluster.Build() Expect(err).NotTo(HaveOccurred()) @@ -299,7 +299,7 @@ var _ = Describe("Cluster", func() { err := cluster.CreateAWSBuilder(rosaTypes.Classic, map[string]string{"key1": "val1"}, pointer(string(cmv1.Ec2MetadataHttpTokensRequired)), pointer(validKmsKey), nil, true, pointer(accountID), nil, - sts, subnets, &privateHZId, &privateHZRoleArn, nil, nil, nil) + sts, subnets, &privateHZId, &privateHZRoleArn, nil, nil, nil, nil, nil, nil) Expect(err).NotTo(HaveOccurred()) ocmCluster, err := cluster.Build() Expect(err).NotTo(HaveOccurred()) @@ -324,7 +324,7 @@ var _ = Describe("Cluster", func() { err := cluster.CreateAWSBuilder(rosaTypes.Classic, map[string]string{"key1": "val1"}, pointer(string(cmv1.Ec2MetadataHttpTokensRequired)), pointer(validKmsKey), nil, true, pointer(accountID), nil, - sts, subnets, &privateHZId, &privateHZRoleArn, nil, nil, nil) + sts, subnets, &privateHZId, &privateHZRoleArn, nil, nil, nil, nil, nil, nil) Expect(err).To(HaveOccurred()) }) It("PrivateHostedZone set missing STS - fail", func() { @@ -336,7 +336,7 @@ var _ = Describe("Cluster", func() { err := cluster.CreateAWSBuilder(rosaTypes.Classic, map[string]string{"key1": "val1"}, pointer(string(cmv1.Ec2MetadataHttpTokensRequired)), pointer(validKmsKey), nil, true, pointer(accountID), nil, - nil, subnets, &privateHZId, &privateHZRoleArn, nil, nil, nil) + nil, subnets, &privateHZId, &privateHZRoleArn, nil, nil, nil, nil, nil, nil) Expect(err).To(HaveOccurred()) }) It("PrivateHostedZone set missing subnet ids - fail", func() { @@ -355,7 +355,7 @@ var _ = Describe("Cluster", func() { err := cluster.CreateAWSBuilder(rosaTypes.Classic, map[string]string{"key1": "val1"}, pointer(string(cmv1.Ec2MetadataHttpTokensRequired)), pointer(validKmsKey), nil, true, pointer(accountID), nil, - sts, nil, &privateHZId, &privateHZRoleArn, nil, nil, nil) + sts, nil, &privateHZId, &privateHZRoleArn, nil, nil, nil, nil, nil, nil) Expect(err).To(HaveOccurred()) }) }) diff --git a/provider/clusterrosa/classic/cluster_rosa_classic_resource.go b/provider/clusterrosa/classic/cluster_rosa_classic_resource.go index a07cc3df..27213a39 100644 --- a/provider/clusterrosa/classic/cluster_rosa_classic_resource.go +++ b/provider/clusterrosa/classic/cluster_rosa_classic_resource.go @@ -653,9 +653,10 @@ func createClassicClusterObject(ctx context.Context, } if err := ocmClusterResource.CreateAWSBuilder(rosaTypes.Classic, awsTags, ec2MetadataHttpTokens, kmsKeyARN, nil, - isPrivateLink, awsAccountID, nil, stsBuilder, awsSubnetIDs, privateHostedZoneID, privateHostedZoneRoleARN, + isPrivateLink, awsAccountID, nil, stsBuilder, awsSubnetIDs, + privateHostedZoneID, privateHostedZoneRoleARN, nil, nil, awsAdditionalComputeSecurityGroupIds, awsAdditionalInfraSecurityGroupIds, - awsAdditionalControlPlaneSecurityGroupIds); err != nil { + awsAdditionalControlPlaneSecurityGroupIds, nil); err != nil { return nil, err } @@ -997,7 +998,8 @@ func validateNoImmutableAttChange(state, plan *ClusterRosaClassicState) diag.Dia common.ValidateStateAndPlanEquals(state.AWSAdditionalComputeSecurityGroupIds, plan.AWSAdditionalComputeSecurityGroupIds, "aws_additional_compute_security_group_ids", &diags) if !reflect.DeepEqual(state.PrivateHostedZone, plan.PrivateHostedZone) { - diags.AddError(common.AssertionErrorSummaryMessage, fmt.Sprintf(common.AssertionErrorDetailsMessage, "private_hosted_zone", *state.PrivateHostedZone, *plan.PrivateHostedZone)) + diags.AddError(common.AssertionErrorSummaryMessage, fmt.Sprintf(common.AssertionErrorDetailsMessage, "private_hosted_zone", + common.GetJsonStringOrNullString(state.PrivateHostedZone), common.GetJsonStringOrNullString(plan.PrivateHostedZone))) } // default machine pool's attributes diff --git a/provider/clusterrosa/common/validators.go b/provider/clusterrosa/common/validators.go index 6c5d2ca4..8a6c68b9 100644 --- a/provider/clusterrosa/common/validators.go +++ b/provider/clusterrosa/common/validators.go @@ -30,7 +30,7 @@ var AvailabilityZoneValidator = attrvalidators.NewStringValidator("AZ should be } }) -var PrivateHZValidator = attrvalidators.NewObjectValidator("proxy map should not include an hard coded OCM proxy", +var PrivateHZValidator = attrvalidators.NewObjectValidator("Private Hosted Zone attribute must include all attributes", func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return diff --git a/provider/clusterrosa/hcp/datasource.go b/provider/clusterrosa/hcp/datasource.go index b0960388..f183f21e 100644 --- a/provider/clusterrosa/hcp/datasource.go +++ b/provider/clusterrosa/hcp/datasource.go @@ -30,6 +30,7 @@ import ( cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" rosa "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/common" rosaTypes "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/common/types" + sharedvpc "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/hcp/shared_vpc" "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/sts" "github.com/terraform-redhat/terraform-provider-rhcs/provider/proxy" "github.com/terraform-redhat/terraform-provider-rhcs/provider/registry_config" @@ -119,6 +120,11 @@ func (r *ClusterRosaHcpDatasource) Schema(ctx context.Context, req datasource.Sc Description: "DNS domain of cluster.", Computed: true, }, + "base_dns_domain": schema.StringAttribute{ + //nolint:lll + Description: "Base DNS domain name previously reserved, e.g. '1vo8.p3.openshiftapps.com'. " + common.ValueCannotBeChangedStringDescription, + Computed: true, + }, "replicas": schema.Int64Attribute{ Description: deprecatedMessage, Computed: true, @@ -265,6 +271,16 @@ func (r *ClusterRosaHcpDatasource) Schema(ctx context.Context, req datasource.Sc ElementType: types.StringType, Computed: true, }, + "shared_vpc": schema.SingleNestedAttribute{ + Description: "Shared VPC configuration." + common.ValueCannotBeChangedStringDescription, + Attributes: sharedvpc.HcpStsDatasource(), + Computed: true, + }, + "aws_additional_allowed_principals": schema.ListAttribute{ + Description: "AWS additional allowed principals.", + ElementType: types.StringType, + Computed: true, + }, }, } } @@ -322,7 +338,7 @@ func (r *ClusterRosaHcpDatasource) Read(ctx context.Context, request datasource. object := get.Body() // Save the state: - err = populateRosaHcpClusterState(ctx, object, state, common.DefaultHttpClient{}) + err = populateRosaHcpClusterState(ctx, object, state) if err != nil { response.Diagnostics.AddError( "Can't populate cluster state", diff --git a/provider/clusterrosa/hcp/resource.go b/provider/clusterrosa/hcp/resource.go index 77f04f5f..554f4804 100644 --- a/provider/clusterrosa/hcp/resource.go +++ b/provider/clusterrosa/hcp/resource.go @@ -32,6 +32,7 @@ import ( "github.com/terraform-redhat/terraform-provider-rhcs/provider/registry_config" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -64,6 +65,7 @@ import ( ocmr "github.com/terraform-redhat/terraform-provider-rhcs/internal/ocm/resource" rosa "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/common" rosaTypes "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/common/types" + sharedvpc "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/hcp/shared_vpc" "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/hcp/upgrade" "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/sts" ) @@ -177,6 +179,15 @@ func (r *ClusterRosaHcpResource) Schema(ctx context.Context, req resource.Schema Description: "DNS domain of cluster.", Computed: true, }, + "base_dns_domain": schema.StringAttribute{ + //nolint:lll + Description: "Base DNS domain name previously reserved, e.g. '1vo8.p3.openshiftapps.com'. " + common.ValueCannotBeChangedStringDescription, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, "replicas": schema.Int64Attribute{ Description: "Number of worker/compute nodes to provision. " + "Requires that the number supplied be a multiple of the number of private subnets. " + @@ -379,6 +390,20 @@ func (r *ClusterRosaHcpResource) Schema(ctx context.Context, req resource.Schema ElementType: types.StringType, Optional: true, }, + "shared_vpc": schema.SingleNestedAttribute{ + Description: "Shared VPC configuration." + common.ValueCannotBeChangedStringDescription, + Attributes: sharedvpc.SharedVpcResource(), + Optional: true, + Validators: []validator.Object{ + objectvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName("base_dns_domain")), + sharedvpc.HcpSharedVpcValidator, + }, + }, + "aws_additional_allowed_principals": schema.ListAttribute{ + Description: "AWS additional allowed principals.", + ElementType: types.StringType, + Optional: true, + }, }, } } @@ -539,13 +564,38 @@ func createHcpClusterObject(ctx context.Context, if err != nil { return nil, err } + + var ingressHostedZoneId, route53RoleArn, vpceRoleArn, internalCommunicationHostedZoneId *string + if state.SharedVpc != nil && + !common.IsStringAttributeUnknownOrEmpty(state.SharedVpc.IngressPrivateHostedZoneId) && + !common.IsStringAttributeUnknownOrEmpty(state.SharedVpc.Route53RoleArn) && + !common.IsStringAttributeUnknownOrEmpty(state.SharedVpc.InternalCommunicationPrivateHostedZoneId) && + !common.IsStringAttributeUnknownOrEmpty(state.SharedVpc.VpceRoleArn) { + route53RoleArn = state.SharedVpc.Route53RoleArn.ValueStringPointer() + ingressHostedZoneId = state.SharedVpc.IngressPrivateHostedZoneId.ValueStringPointer() + vpceRoleArn = state.SharedVpc.VpceRoleArn.ValueStringPointer() + internalCommunicationHostedZoneId = state.SharedVpc.InternalCommunicationPrivateHostedZoneId.ValueStringPointer() + } + + awsAdditionalAllowedPrincipals, err := common.StringListToArray(ctx, state.AWSAdditionalAllowedPrincipals) + if err != nil { + return nil, err + } + if err := ocmClusterResource.CreateAWSBuilder(rosaTypes.Hcp, awsTags, ec2MetadataHttpTokens, kmsKeyARN, etcdKmsKeyArn, - isPrivate, awsAccountID, awsBillingAccountId, stsBuilder, awsSubnetIDs, nil, nil, - awsAdditionalComputeSecurityGroupIds, nil, nil); err != nil { + isPrivate, awsAccountID, awsBillingAccountId, stsBuilder, awsSubnetIDs, + ingressHostedZoneId, route53RoleArn, internalCommunicationHostedZoneId, vpceRoleArn, + awsAdditionalComputeSecurityGroupIds, nil, nil, awsAdditionalAllowedPrincipals); err != nil { return nil, err } + if !common.IsStringAttributeUnknownOrEmpty(state.BaseDNSDomain) { + dnsBuilder := cmv1.NewDNS() + dnsBuilder.BaseDomain(state.BaseDNSDomain.ValueString()) + builder.DNS(dnsBuilder) + } + if err := ocmClusterResource.SetAPIPrivacy(isPrivate, isPrivate, stsBuilder != nil); err != nil { return nil, err } @@ -751,7 +801,7 @@ func (r *ClusterRosaHcpResource) Create(ctx context.Context, request resource.Cr object = add.Body() // Save initial state: - err = populateRosaHcpClusterState(ctx, object, state, common.DefaultHttpClient{}) + err = populateRosaHcpClusterState(ctx, object, state) if err != nil { response.Diagnostics.AddError( "Can't populate cluster state", @@ -794,7 +844,7 @@ func (r *ClusterRosaHcpResource) Create(ctx context.Context, request resource.Cr } // Save the state post wait completion: - err = populateRosaHcpClusterState(ctx, object, state, common.DefaultHttpClient{}) + err = populateRosaHcpClusterState(ctx, object, state) if err != nil { response.Diagnostics.AddError( "Can't populate cluster state", @@ -843,7 +893,7 @@ func (r *ClusterRosaHcpResource) Read(ctx context.Context, request resource.Read object := get.Body() // Save the state: - err = populateRosaHcpClusterState(ctx, object, state, common.DefaultHttpClient{}) + err = populateRosaHcpClusterState(ctx, object, state) if err != nil { response.Diagnostics.AddError( "Can't populate cluster state", @@ -899,6 +949,12 @@ func validateNoImmutableAttChange(state, plan *ClusterRosaHcpState) diag.Diagnos diags.AddError(common.AssertionErrorSummaryMessage, fmt.Sprintf(common.AssertionErrorDetailsMessage, "admin_credentials", state.AdminCredentials, plan.AdminCredentials)) } + common.ValidateStateAndPlanEquals(state.BaseDNSDomain, plan.BaseDNSDomain, "base_dns_domain", &diags) + if !reflect.DeepEqual(state.SharedVpc, plan.SharedVpc) { + diags.AddError(common.AssertionErrorSummaryMessage, fmt.Sprintf(common.AssertionErrorDetailsMessage, "shared_vpc", + common.GetJsonStringOrNullString(state.SharedVpc), common.GetJsonStringOrNullString(plan.SharedVpc))) + } + return diags } @@ -969,10 +1025,6 @@ func (r *ClusterRosaHcpResource) Update(ctx context.Context, request resource.Up return } - if newBillingAcc, ok := common.ShouldPatchString(state.AWSBillingAccountID, plan.AWSBillingAccountID); ok { - clusterBuilder.AWS(cmv1.NewAWS().BillingAccountID(newBillingAcc)) - } - patchProperties := shouldPatchProperties(state, plan) if patchProperties { propertiesElements, err := rosa.ValidatePatchProperties(ctx, state.Properties, plan.Properties) @@ -990,10 +1042,6 @@ func (r *ClusterRosaHcpResource) Update(ctx context.Context, request resource.Up } } - if toPatch, shouldPatch := common.ShouldPatchString(state.AWSBillingAccountID, plan.AWSBillingAccountID); shouldPatch { - clusterBuilder.AWS(cmv1.NewAWS().BillingAccountID(toPatch)) - } - registryConfigBuilder, err := registry_config.UpdateRegistryConfigBuilder(ctx, state.RegistryConfig, plan.RegistryConfig) if err != nil { @@ -1007,6 +1055,31 @@ func (r *ClusterRosaHcpResource) Update(ctx context.Context, request resource.Up clusterBuilder.RegistryConfig(registryConfigBuilder) } + awsBuilder := cmv1.NewAWS() + changesToAws := false + + if toPatch, shouldPatch := common.ShouldPatchList(state.AWSAdditionalAllowedPrincipals, plan.AWSAdditionalAllowedPrincipals); shouldPatch { + additionalAllowedPrincipalsPatch, err := common.StringListToArray(ctx, toPatch) + if err != nil { + response.Diagnostics.AddError( + "Can't patch cluster", + fmt.Sprintf("Can't patch additional allowed principals for cluster with identifier: '%s', %v", state.ID.ValueString(), err), + ) + return + } + awsBuilder.AdditionalAllowedPrincipals(additionalAllowedPrincipalsPatch...) + changesToAws = shouldPatch + } + + if newBillingAcc, shouldPatch := common.ShouldPatchString(state.AWSBillingAccountID, plan.AWSBillingAccountID); shouldPatch { + awsBuilder.BillingAccountID(newBillingAcc) + changesToAws = shouldPatch + } + + if changesToAws { + clusterBuilder.AWS(awsBuilder) + } + clusterSpec, err := clusterBuilder.Build() if err != nil { response.Diagnostics.AddError( @@ -1036,7 +1109,7 @@ func (r *ClusterRosaHcpResource) Update(ctx context.Context, request resource.Up object := update.Body() // Update the state: - err = populateRosaHcpClusterState(ctx, object, plan, common.DefaultHttpClient{}) + err = populateRosaHcpClusterState(ctx, object, plan) if err != nil { response.Diagnostics.AddError( "Can't populate cluster state", @@ -1300,7 +1373,7 @@ func (r *ClusterRosaHcpResource) ImportState(ctx context.Context, request resour } // populateRosaHcpClusterState copies the data from the API object to the Terraform state. -func populateRosaHcpClusterState(ctx context.Context, object *cmv1.Cluster, state *ClusterRosaHcpState, httpClient common.HttpClient) error { +func populateRosaHcpClusterState(ctx context.Context, object *cmv1.Cluster, state *ClusterRosaHcpState) error { state.ID = types.StringValue(object.ID()) state.ExternalID = types.StringValue(object.ExternalID()) object.API() @@ -1333,6 +1406,7 @@ func populateRosaHcpClusterState(ctx context.Context, object *cmv1.Cluster, stat state.APIURL = types.StringValue(object.API().URL()) state.ConsoleURL = types.StringValue(object.Console().URL()) state.Domain = types.StringValue(fmt.Sprintf("%s.%s", object.DomainPrefix(), object.DNS().BaseDomain())) + state.BaseDNSDomain = types.StringValue(object.DNS().BaseDomain()) if azs, ok := object.Nodes().GetAvailabilityZones(); ok { listValue, err := common.StringArrayToList(azs) @@ -1534,6 +1608,30 @@ func populateRosaHcpClusterState(ctx context.Context, object *cmv1.Cluster, stat return err } + if awsObj, ok := object.GetAWS(); ok { + ingressHostedZoneId := awsObj.PrivateHostedZoneID() + route53RoleArn := awsObj.PrivateHostedZoneRoleARN() + internalCommunicationHostedZoneId := awsObj.HcpInternalCommunicationHostedZoneId() + vpceRoleArn := awsObj.VpcEndpointRoleArn() + if len(ingressHostedZoneId) > 0 && len(route53RoleArn) > 0 && + len(internalCommunicationHostedZoneId) > 0 && len(vpceRoleArn) > 0 { + state.SharedVpc = &sharedvpc.SharedVpc{ + IngressPrivateHostedZoneId: types.StringValue(ingressHostedZoneId), + InternalCommunicationPrivateHostedZoneId: types.StringValue(internalCommunicationHostedZoneId), + Route53RoleArn: types.StringValue(route53RoleArn), + VpceRoleArn: types.StringValue(vpceRoleArn), + } + } + + if additionalAllowedPrincipals, ok := awsObj.GetAdditionalAllowedPrincipals(); ok { + awsAdditionalAllowedPrincipals, err := common.StringArrayToList(additionalAllowedPrincipals) + if err != nil { + return err + } + state.AWSAdditionalAllowedPrincipals = awsAdditionalAllowedPrincipals + } + } + return nil } diff --git a/provider/clusterrosa/hcp/resource_test.go b/provider/clusterrosa/hcp/resource_test.go index ba623760..43862a64 100644 --- a/provider/clusterrosa/hcp/resource_test.go +++ b/provider/clusterrosa/hcp/resource_test.go @@ -248,7 +248,7 @@ var _ = Describe("Rosa HCP Sts cluster", func() { clusterObject, err := cmv1.UnmarshalCluster(clusterJsonString) Expect(err).ToNot(HaveOccurred()) - Expect(populateRosaHcpClusterState(context.Background(), clusterObject, clusterState, mockHttpClient)).To(Succeed()) + Expect(populateRosaHcpClusterState(context.Background(), clusterObject, clusterState)).To(Succeed()) Expect(clusterState.ID.ValueString()).To(Equal(clusterId)) Expect(clusterState.CloudRegion.ValueString()).To(Equal(regionId)) @@ -292,7 +292,7 @@ var _ = Describe("Rosa HCP Sts cluster", func() { clusterObject, err := cmv1.UnmarshalCluster(clusterJsonString) Expect(err).ToNot(HaveOccurred()) - err = populateRosaHcpClusterState(context.Background(), clusterObject, clusterState, mockHttpClient) + err = populateRosaHcpClusterState(context.Background(), clusterObject, clusterState) Expect(err).ToNot(HaveOccurred()) Expect(clusterState.Sts.OIDCEndpointURL.ValueString()).To(Equal("nonce.com")) }) @@ -307,7 +307,7 @@ var _ = Describe("Rosa HCP Sts cluster", func() { clusterObject, err := cmv1.UnmarshalCluster(clusterJsonString) Expect(err).ToNot(HaveOccurred()) - err = populateRosaHcpClusterState(context.Background(), clusterObject, clusterState, mockHttpClient) + err = populateRosaHcpClusterState(context.Background(), clusterObject, clusterState) Expect(err).ToNot(HaveOccurred()) Expect(clusterState.Sts.Thumbprint.ValueString()).To(Equal("")) }) diff --git a/provider/clusterrosa/hcp/shared_vpc/shared_vpc.go b/provider/clusterrosa/hcp/shared_vpc/shared_vpc.go new file mode 100644 index 00000000..51e1ca7d --- /dev/null +++ b/provider/clusterrosa/hcp/shared_vpc/shared_vpc.go @@ -0,0 +1,99 @@ +package sharedvpc + +import ( + "context" + + dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/terraform-redhat/terraform-provider-rhcs/provider/common" + "github.com/terraform-redhat/terraform-provider-rhcs/provider/common/attrvalidators" +) + +type SharedVpc struct { + IngressPrivateHostedZoneId types.String `tfsdk:"ingress_private_hosted_zone_id"` + InternalCommunicationPrivateHostedZoneId types.String `tfsdk:"internal_communication_private_hosted_zone_id"` + Route53RoleArn types.String `tfsdk:"route53_role_arn"` + VpceRoleArn types.String `tfsdk:"vpce_role_arn"` +} + +func SharedVpcResource() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "ingress_private_hosted_zone_id": schema.StringAttribute{ + //nolint:lll + Description: "ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'.", + Required: true, + }, + "internal_communication_private_hosted_zone_id": schema.StringAttribute{ + //noling:lll + Description: "ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'.", + Optional: true, + }, + "route53_role_arn": schema.StringAttribute{ + //nolint:lll + Description: "AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage Route 53 DNS records in private Route 53 hosted zone associated with intended shared VPC.", + Required: true, + }, + "vpce_role_arn": schema.StringAttribute{ + //nolint:lll + Description: "AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage VPC Endpoints associated with intended shared VPC.", + Required: true, + }, + } +} + +func HcpStsDatasource() map[string]dsschema.Attribute { + return map[string]dsschema.Attribute{ + "ingress_private_hosted_zone_id": schema.StringAttribute{ + //nolint:lll + Description: "ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'.", + Computed: true, + }, + "internal_communication_private_hosted_zone_id": schema.StringAttribute{ + //noling:lll + Description: "ID assigned by AWS to private Route 53 hosted zone associated with intended shared VPC, e.g. 'Z05646003S02O1ENCDCSN'.", + Computed: true, + }, + "route53_role_arn": schema.StringAttribute{ + //nolint:lll + Description: "AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage Route 53 DNS records in private Route 53 hosted zone associated with intended shared VPC.", + Computed: true, + }, + "vpce_role_arn": schema.StringAttribute{ + //nolint:lll + Description: "AWS IAM role ARN with a policy attached, granting permissions necessary to create and manage VPC Endpoints associated with intended shared VPC.", + Computed: true, + }, + } +} + +var HcpSharedVpcValidator = attrvalidators.NewObjectValidator("Shared VPC attribute must include all attributes", + func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + sharedVpc := SharedVpc{} + d := req.ConfigValue.As(ctx, &sharedVpc, basetypes.ObjectAsOptions{}) + if d.HasError() { + // No attribute to validate + return + } + errSum := "Invalid shared_vpc attribute assignment" + + // validate ID and ARN are not empty + valuesToCheck := []basetypes.StringValue{ + sharedVpc.IngressPrivateHostedZoneId, + sharedVpc.InternalCommunicationPrivateHostedZoneId, + sharedVpc.Route53RoleArn, + sharedVpc.VpceRoleArn, + } + for _, value := range valuesToCheck { + if common.IsStringAttributeKnownAndEmpty(value) { + resp.Diagnostics.AddError(errSum, "Invalid configuration, all attributes are required") + return + } + } + }) diff --git a/provider/clusterrosa/hcp/state.go b/provider/clusterrosa/hcp/state.go index 6ab385c1..c4d7a3a2 100644 --- a/provider/clusterrosa/hcp/state.go +++ b/provider/clusterrosa/hcp/state.go @@ -2,6 +2,7 @@ package hcp import ( "github.com/hashicorp/terraform-plugin-framework/types" + sharedvpc "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/hcp/shared_vpc" "github.com/terraform-redhat/terraform-provider-rhcs/provider/clusterrosa/sts" "github.com/terraform-redhat/terraform-provider-rhcs/provider/proxy" "github.com/terraform-redhat/terraform-provider-rhcs/provider/registry_config" @@ -31,6 +32,7 @@ type ClusterRosaHcpState struct { EtcdKmsKeyArn types.String `tfsdk:"etcd_kms_key_arn"` Tags types.Map `tfsdk:"tags"` AWSAdditionalComputeSecurityGroupIds types.List `tfsdk:"aws_additional_compute_security_group_ids"` + AWSAdditionalAllowedPrincipals types.List `tfsdk:"aws_additional_allowed_principals"` // Network fields Domain types.String `tfsdk:"domain"` @@ -64,4 +66,10 @@ type ClusterRosaHcpState struct { // Registry config fields RegistryConfig *registry_config.RegistryConfig `tfsdk:"registry_config"` + + // DNS reservation fields + BaseDNSDomain types.String `tfsdk:"base_dns_domain"` + + // Shared VPC fields + SharedVpc *sharedvpc.SharedVpc `tfsdk:"shared_vpc"` } diff --git a/provider/common/helpers.go b/provider/common/helpers.go index adb90441..f047ffec 100644 --- a/provider/common/helpers.go +++ b/provider/common/helpers.go @@ -158,3 +158,10 @@ func ValidateStateAndPlanEquals(stateAttr attr.Value, planAttr attr.Value, attrN diags.AddError(AssertionErrorSummaryMessage, fmt.Sprintf(AssertionErrorDetailsMessage, attrName, stateAttr, planAttr)) } } + +func GetJsonStringOrNullString[T any](value *T) string { + if value == nil { + return "null" + } + return fmt.Sprintf("%+v", *value) +} diff --git a/subsystem/hcp/cluster_resource_test.go b/subsystem/hcp/cluster_resource_test.go index 2eec4c86..424d31a0 100644 --- a/subsystem/hcp/cluster_resource_test.go +++ b/subsystem/hcp/cluster_resource_test.go @@ -1513,6 +1513,181 @@ var _ = Describe("HCP Cluster", func() { Expect(resource).To(MatchJQ(`.attributes.aws_billing_account_id`, "123456799012")) }) + It("Creates basic cluster and update additional allowed principals", func() { + // Prepare the server: + TestServer.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/versions"), + RespondWithJSON(http.StatusOK, versionListPage), + ), + CombineHandlers( + VerifyRequest(http.MethodPost, "/api/clusters_mgmt/v1/clusters"), + VerifyJQ(`.name`, "my-cluster"), + VerifyJQ(`.cloud_provider.id`, "aws"), + VerifyJQ(`.region.id`, "us-west-1"), + VerifyJQ(`.product.id`, "rosa"), + VerifyJQ(`.aws.billing_account_id`, "123456789012"), + RespondWithPatchedJSON(http.StatusCreated, template, fmt.Sprintf(`[ + { + "op": "add", + "path": "/aws", + "value": { + "sts": { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + }, + "additional_allowed_principals": ["arn:aws:iam::123456789012:role/dummy"] + } + }, + { + "op": "add", + "path": "/properties", + "value": { + "rosa_creator_arn": "arn:aws:iam::123456789012:user/dummy", + "rosa_tf_commit":"%s", + "rosa_tf_version":"%s" + } + }]`, build.Commit, build.Version)), + ), + ) + + // Run the apply command: + Terraform.Source(` + resource "rhcs_cluster_rosa_hcp" "my_cluster" { + name = "my-cluster" + cloud_region = "us-west-1" + aws_account_id = "123456789012" + aws_billing_account_id = "123456789012" + properties = { + rosa_creator_arn = "arn:aws:iam::123456789012:user/dummy", + } + aws_additional_allowed_principals = ["arn:aws:iam::123456789012:role/dummy"] + sts = { + operator_role_prefix = "test" + role_arn = "", + support_role_arn = "", + instance_iam_roles = { + worker_role_arn = "", + } + } + aws_subnet_ids = [ + "id1", "id2", "id3" + ] + availability_zones = [ + "us-west-1a", + "us-west-1b", + "us-west-1c", + ] + }`) + runOutput := Terraform.Apply() + Expect(runOutput.ExitCode).To(BeZero()) + resource := Terraform.Resource("rhcs_cluster_rosa_hcp", "my_cluster") + Expect(resource).To(MatchJQ(`.attributes.aws_additional_allowed_principals[0]`, "arn:aws:iam::123456789012:role/dummy")) + + // Prepare server for update + TestServer.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, cluster123Route), + RespondWithPatchedJSON(http.StatusOK, template, fmt.Sprintf(`[ + { + "op": "add", + "path": "/aws", + "value": { + "sts" : { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + }, + "additional_allowed_principals": ["arn:aws:iam::123456789012:role/dummy"] + } + }, + { + "op": "add", + "path": "/properties", + "value": { + "rosa_creator_arn": "arn:aws:iam::123456789012:user/dummy", + "rosa_tf_commit":"%s", + "rosa_tf_version":"%s" + } + }]`, build.Commit, build.Version)), + ), + CombineHandlers( + VerifyRequest(http.MethodPatch, cluster123Route), + RespondWithPatchedJSON(http.StatusOK, template, fmt.Sprintf(`[ + { + "op": "add", + "path": "/aws", + "value": { + "sts" : { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + }, + "additional_allowed_principals": ["arn:aws:iam::123456789012:role/dummy2"] + } + }, + { + "op": "add", + "path": "/properties", + "value": { + "rosa_creator_arn": "arn:aws:iam::123456789012:user/dummy", + "rosa_tf_commit":"%s", + "rosa_tf_version":"%s" + } + }]`, build.Commit, build.Version)), + ), + ) + + // Run the apply command: + Terraform.Source(` + resource "rhcs_cluster_rosa_hcp" "my_cluster" { + name = "my-cluster" + cloud_region = "us-west-1" + aws_account_id = "123456789012" + aws_billing_account_id = "123456789012" + properties = { + rosa_creator_arn = "arn:aws:iam::123456789012:user/dummy", + } + aws_additional_allowed_principals = ["arn:aws:iam::123456789012:role/dummy2"] + sts = { + operator_role_prefix = "test" + role_arn = "", + support_role_arn = "", + instance_iam_roles = { + worker_role_arn = "", + } + } + aws_subnet_ids = [ + "id1", "id2", "id3" + ] + availability_zones = [ + "us-west-1a", + "us-west-1b", + "us-west-1c", + ] + }`) + runOutput = Terraform.Apply() + Expect(runOutput.ExitCode).To(BeZero()) + resource = Terraform.Resource("rhcs_cluster_rosa_hcp", "my_cluster") + Expect(resource).To(MatchJQ(`.attributes.aws_additional_allowed_principals[0]`, "arn:aws:iam::123456789012:role/dummy2")) + }) + It("Creates basic cluster with properties and update them", func() { // Prepare the server: TestServer.AppendHandlers( @@ -1878,6 +2053,354 @@ var _ = Describe("HCP Cluster", func() { Expect(resource).To(MatchJQ(`.attributes.ocm_properties | keys | length`, 3)) }) + It("Creates cluster with dns base domain and tries to update it", func() { + // Prepare the server: + TestServer.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/versions"), + RespondWithJSON(http.StatusOK, versionListPage), + ), + CombineHandlers( + VerifyRequest(http.MethodPost, "/api/clusters_mgmt/v1/clusters"), + VerifyJQ(`.name`, "my-cluster"), + VerifyJQ(`.cloud_provider.id`, "aws"), + VerifyJQ(`.region.id`, "us-west-1"), + VerifyJQ(`.product.id`, "rosa"), + VerifyJQ(`.aws.billing_account_id`, "123456789012"), + RespondWithPatchedJSON(http.StatusCreated, template, `[ + { + "op": "add", + "path": "/aws", + "value": { + "sts": { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + } + } + }, + { + "op": "add", + "path": "/dns", + "value": { + "base_domain": "test.org" + } + }]`), + ), + ) + + // Run the apply command: + Terraform.Source(` + resource "rhcs_cluster_rosa_hcp" "my_cluster" { + name = "my-cluster" + cloud_region = "us-west-1" + aws_account_id = "123456789012" + aws_billing_account_id = "123456789012" + sts = { + operator_role_prefix = "test" + role_arn = "", + support_role_arn = "", + instance_iam_roles = { + worker_role_arn = "", + } + } + aws_subnet_ids = [ + "id1", "id2", "id3" + ] + availability_zones = [ + "us-west-1a", + "us-west-1b", + "us-west-1c", + ] + base_dns_domain = "test.org" + }`) + runOutput := Terraform.Apply() + Expect(runOutput.ExitCode).To(BeZero()) + resource := Terraform.Resource("rhcs_cluster_rosa_hcp", "my_cluster") + Expect(resource).To(MatchJQ(`.attributes.base_dns_domain`, "test.org")) + + // Prepare server for update + TestServer.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, cluster123Route), + RespondWithPatchedJSON(http.StatusOK, template, `[ + { + "op": "add", + "path": "/aws", + "value": { + "sts" : { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + } + } + }]`), + ), + ) + + // Run the apply command: + Terraform.Source(` + resource "rhcs_cluster_rosa_hcp" "my_cluster" { + name = "my-cluster" + cloud_region = "us-west-1" + aws_account_id = "123456789012" + aws_billing_account_id = "123456789012" + sts = { + operator_role_prefix = "test" + role_arn = "", + support_role_arn = "", + instance_iam_roles = { + worker_role_arn = "", + } + } + aws_subnet_ids = [ + "id1", "id2", "id3" + ] + availability_zones = [ + "us-west-1a", + "us-west-1b", + "us-west-1c", + ] + base_dns_domain = "newtest.org" + }`) + runOutput = Terraform.Apply() + Expect(runOutput.ExitCode).NotTo(BeZero()) + runOutput.VerifyErrorContainsSubstring("Attribute base_dns_domain, cannot be changed from") + }) + + It("Creates cluster with shared vpc and without base domain", func() { + // Prepare the server: + TestServer.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/versions"), + RespondWithJSON(http.StatusOK, versionListPage), + ), + CombineHandlers( + VerifyRequest(http.MethodPost, "/api/clusters_mgmt/v1/clusters"), + VerifyJQ(`.name`, "my-cluster"), + VerifyJQ(`.cloud_provider.id`, "aws"), + VerifyJQ(`.region.id`, "us-west-1"), + VerifyJQ(`.product.id`, "rosa"), + VerifyJQ(`.aws.billing_account_id`, "123456789012"), + RespondWithPatchedJSON(http.StatusCreated, template, `[ + { + "op": "add", + "path": "/aws", + "value": { + "sts": { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + }, + "private_hosted_zone_id": "1", + "private_hosted_zone_role_arn": "arn:aws:iam::123456789012:role/r53", + "vpc_endpoint_role_arn": "arn:aws:iam::123456789012:role/vpce", + "hcp_internal_communication_hosted_zone_id": "2" + } + }]`), + ), + ) + + // Run the apply command: + Terraform.Source(` + resource "rhcs_cluster_rosa_hcp" "my_cluster" { + name = "my-cluster" + cloud_region = "us-west-1" + aws_account_id = "123456789012" + aws_billing_account_id = "123456789012" + sts = { + operator_role_prefix = "test" + role_arn = "", + support_role_arn = "", + instance_iam_roles = { + worker_role_arn = "", + } + } + aws_subnet_ids = [ + "id1", "id2", "id3" + ] + availability_zones = [ + "us-west-1a", + "us-west-1b", + "us-west-1c", + ] + shared_vpc = { + ingress_private_hosted_zone_id = "1" + internal_communication_private_hosted_zone_id = "2" + route53_role_arn = "arn:aws:iam::123456789012:role/r53" + vpce_role_arn = "arn:aws:iam::123456789012:role/vpce" + } + }`) + runOutput := Terraform.Apply() + Expect(runOutput.ExitCode).ToNot(BeZero()) + runOutput.VerifyErrorContainsSubstring("Attribute \"base_dns_domain\" must be specified when \"shared_vpc\"") + }) + + It("Creates cluster with shared vpc and tries to update it", func() { + // Prepare the server: + TestServer.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/versions"), + RespondWithJSON(http.StatusOK, versionListPage), + ), + CombineHandlers( + VerifyRequest(http.MethodPost, "/api/clusters_mgmt/v1/clusters"), + VerifyJQ(`.name`, "my-cluster"), + VerifyJQ(`.cloud_provider.id`, "aws"), + VerifyJQ(`.region.id`, "us-west-1"), + VerifyJQ(`.product.id`, "rosa"), + VerifyJQ(`.aws.billing_account_id`, "123456789012"), + RespondWithPatchedJSON(http.StatusCreated, template, `[ + { + "op": "add", + "path": "/aws", + "value": { + "sts": { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + }, + "private_hosted_zone_id": "1", + "private_hosted_zone_role_arn": "arn:aws:iam::123456789012:role/route53", + "vpc_endpoint_role_arn": "arn:aws:iam::123456789012:role/vpce", + "hcp_internal_communication_hosted_zone_id": "2" + } + }, + { + "op": "add", + "path": "/dns", + "value": { + "base_domain": "test.org" + } + }]`), + ), + ) + + // Run the apply command: + Terraform.Source(` + resource "rhcs_cluster_rosa_hcp" "my_cluster" { + name = "my-cluster" + cloud_region = "us-west-1" + aws_account_id = "123456789012" + aws_billing_account_id = "123456789012" + sts = { + operator_role_prefix = "test" + role_arn = "", + support_role_arn = "", + instance_iam_roles = { + worker_role_arn = "", + } + } + aws_subnet_ids = [ + "id1", "id2", "id3" + ] + availability_zones = [ + "us-west-1a", + "us-west-1b", + "us-west-1c", + ] + shared_vpc = { + ingress_private_hosted_zone_id = "1" + internal_communication_private_hosted_zone_id = "2" + route53_role_arn = "arn:aws:iam::123456789012:role/route53" + vpce_role_arn = "arn:aws:iam::123456789012:role/vpce" + } + base_dns_domain = "test.org" + }`) + runOutput := Terraform.Apply() + Expect(runOutput.ExitCode).To(BeZero()) + resource := Terraform.Resource("rhcs_cluster_rosa_hcp", "my_cluster") + Expect(resource).To(MatchJQ(`.attributes.base_dns_domain`, "test.org")) + Expect(resource).To(MatchJQ(`.attributes.shared_vpc.ingress_private_hosted_zone_id`, "1")) + Expect(resource).To(MatchJQ(`.attributes.shared_vpc.internal_communication_private_hosted_zone_id`, "2")) + Expect(resource).To(MatchJQ(`.attributes.shared_vpc.route53_role_arn`, "arn:aws:iam::123456789012:role/route53")) + Expect(resource).To(MatchJQ(`.attributes.shared_vpc.vpce_role_arn`, "arn:aws:iam::123456789012:role/vpce")) + + // Prepare server for update + TestServer.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, cluster123Route), + RespondWithPatchedJSON(http.StatusOK, template, `[ + { + "op": "add", + "path": "/aws", + "value": { + "sts" : { + "oidc_endpoint_url": "https://127.0.0.1", + "thumbprint": "111111", + "role_arn": "", + "support_role_arn": "", + "instance_iam_roles" : { + "worker_role_arn" : "" + }, + "operator_role_prefix" : "test" + }, + "private_hosted_zone_id": "1", + "private_hosted_zone_role_arn": "arn:aws:iam::123456789012:role/route53", + "vpc_endpoint_role_arn": "arn:aws:iam::123456789012:role/vpce", + "hcp_internal_communication_hosted_zone_id": "2" + } + }, + { + "op": "add", + "path": "/dns", + "value": { + "base_domain": "test.org" + } + }]`), + ), + ) + + // Run the apply command: + Terraform.Source(` + resource "rhcs_cluster_rosa_hcp" "my_cluster" { + name = "my-cluster" + cloud_region = "us-west-1" + aws_account_id = "123456789012" + aws_billing_account_id = "123456789012" + sts = { + operator_role_prefix = "test" + role_arn = "", + support_role_arn = "", + instance_iam_roles = { + worker_role_arn = "", + } + } + aws_subnet_ids = [ + "id1", "id2", "id3" + ] + availability_zones = [ + "us-west-1a", + "us-west-1b", + "us-west-1c", + ] + base_dns_domain = "test.org" + }`) + runOutput = Terraform.Apply() + Expect(runOutput.ExitCode).ToNot(BeZero()) + runOutput.VerifyErrorContainsSubstring("Attribute shared_vpc, cannot be changed") + }) + It("Creates basic cluster with custom worker disk size", func() { // Prepare the server: TestServer.AppendHandlers(