diff --git a/examples/resources/cdp_dw_aws_cluster/resource.tf b/examples/resources/cdp_dw_aws_cluster/resource.tf new file mode 100644 index 00000000..c9f9bb38 --- /dev/null +++ b/examples/resources/cdp_dw_aws_cluster/resource.tf @@ -0,0 +1,49 @@ +## Copyright 2024 Cloudera. All Rights Reserved. +# +# This file is 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. +# +# This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, either express or implied. Refer to the License for the specific +# permissions and limitations governing your use of the file. + +resource "cdp_dw_aws_cluster" "example" { + crn = cdp_datalake_aws_datalake.example.crn + name = "" + cluster_id = "" + node_role_cdw_managed_policy_arn = "" + database_backup_retention_days = 7 + custom_registry_options = { + registry_type = "ECR" + repository_url = "" + } + custom_subdomain = "" + network_settings = { + worker_subnet_ids = ["", "", ""] + load_balancer_subnet_ids = ["", "", ""] + use_overlay_network = false + whitelist_k8s_cluster_access_ip_cidrs = ["0.0.0.0/0"] + whitelist_workload_access_ip_cidrs = ["0.0.0.0/0"] + use_private_load_balancer = true + use_public_worker_node = false + } + instance_settings = { + custom_ami_id = "" + enable_spot_instances = false + compute_instance_types = [""] + additional_instance_types = [""] + } +} + +output "crn" { + value = cdp_dw_aws_cluster.example.crn +} + +output "cluster_id" { + value = cdp_dw_aws_cluster.example.cluster_id +} + +output "name" { + value = cdp_dw_aws_cluster.example.name +} diff --git a/provider/provider.go b/provider/provider.go index 2e385dbb..5a0151d9 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -17,13 +17,6 @@ import ( "runtime" "strconv" - "github.com/cloudera/terraform-provider-cdp/resources/datahub" - "github.com/cloudera/terraform-provider-cdp/resources/de" - "github.com/cloudera/terraform-provider-cdp/resources/dw" - "github.com/cloudera/terraform-provider-cdp/resources/iam" - "github.com/cloudera/terraform-provider-cdp/resources/ml" - "github.com/cloudera/terraform-provider-cdp/resources/opdb" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -33,8 +26,15 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + "github.com/cloudera/terraform-provider-cdp/resources/datahub" "github.com/cloudera/terraform-provider-cdp/resources/datalake" + "github.com/cloudera/terraform-provider-cdp/resources/de" + "github.com/cloudera/terraform-provider-cdp/resources/dw" + dwaws "github.com/cloudera/terraform-provider-cdp/resources/dw/cluster/aws" "github.com/cloudera/terraform-provider-cdp/resources/environments" + "github.com/cloudera/terraform-provider-cdp/resources/iam" + "github.com/cloudera/terraform-provider-cdp/resources/ml" + "github.com/cloudera/terraform-provider-cdp/resources/opdb" ) // Ensure the implementation satisfies the expected interfaces. @@ -249,6 +249,7 @@ func (p *CdpProvider) Resources(_ context.Context) []func() resource.Resource { ml.NewWorkspaceResource, de.NewServiceResource, dw.NewHiveResource, + dwaws.NewDwClusterResource, } } diff --git a/provider/provider_test.go b/provider/provider_test.go index 7459af9f..0d58d15e 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -21,16 +21,16 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/datasource" + fwprovider "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" - - fwprovider "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/cloudera/terraform-provider-cdp/resources/datahub" "github.com/cloudera/terraform-provider-cdp/resources/datalake" "github.com/cloudera/terraform-provider-cdp/resources/de" "github.com/cloudera/terraform-provider-cdp/resources/dw" + dwaws "github.com/cloudera/terraform-provider-cdp/resources/dw/cluster/aws" "github.com/cloudera/terraform-provider-cdp/resources/environments" "github.com/cloudera/terraform-provider-cdp/resources/iam" "github.com/cloudera/terraform-provider-cdp/resources/ml" @@ -635,6 +635,7 @@ func TestCdpProvider_Resources(t *testing.T) { ml.NewWorkspaceResource, de.NewServiceResource, dw.NewHiveResource, + dwaws.NewDwClusterResource, } provider := CdpProvider{testVersion} diff --git a/resources/dw/cluster/aws/model_cluster.go b/resources/dw/cluster/aws/model_cluster.go new file mode 100644 index 00000000..18e27bb5 --- /dev/null +++ b/resources/dw/cluster/aws/model_cluster.go @@ -0,0 +1,114 @@ +// Copyright 2024 Cloudera. All Rights Reserved. +// +// This file is 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. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package aws + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/dw/models" + "github.com/cloudera/terraform-provider-cdp/utils" +) + +type networkResourceModel struct { + WorkerSubnetIDs types.List `tfsdk:"worker_subnet_ids"` + LoadBalancerSubnetIDs types.List `tfsdk:"load_balancer_subnet_ids"` + UseOverlayNetwork types.Bool `tfsdk:"use_overlay_network"` + WhitelistK8sClusterAccessIPCIDRs types.List `tfsdk:"whitelist_k8s_cluster_access_ip_cidrs"` + WhitelistWorkloadAccessIPCIDRs types.List `tfsdk:"whitelist_workload_access_ip_cidrs"` + UsePrivateLoadBalancer types.Bool `tfsdk:"use_private_load_balancer"` + UsePublicWorkerNode types.Bool `tfsdk:"use_public_worker_node"` +} + +type customRegistryOptions struct { + RegistryType types.String `tfsdk:"registry_type"` + RepositoryURL types.String `tfsdk:"repository_url"` +} + +type instanceResourceModel struct { + CustomAmiID types.String `tfsdk:"custom_ami_id"` + EnableSpotInstances types.Bool `tfsdk:"enable_spot_instances"` + ComputeInstanceTypes types.List `tfsdk:"compute_instance_types"` + AdditionalInstanceTypes types.List `tfsdk:"additional_instance_types"` +} + +type resourceModel struct { + ID types.String `tfsdk:"id"` + Crn types.String `tfsdk:"crn"` + Name types.String `tfsdk:"name"` + ClusterID types.String `tfsdk:"cluster_id"` + LastUpdated types.String `tfsdk:"last_updated"` + NodeRoleCDWManagedPolicyArn types.String `tfsdk:"node_role_cdw_managed_policy_arn"` + DatabaseBackupRetentionDays types.Int64 `tfsdk:"database_backup_retention_days"` + CustomRegistryOptions *customRegistryOptions `tfsdk:"custom_registry_options"` + CustomSubdomain types.String `tfsdk:"custom_subdomain"` + NetworkSettings *networkResourceModel `tfsdk:"network_settings"` + InstanceSettings *instanceResourceModel `tfsdk:"instance_settings"` +} + +func (p *resourceModel) convertToCreateAwsClusterRequest() *models.CreateAwsClusterRequest { + return &models.CreateAwsClusterRequest{ + EnvironmentCrn: p.Crn.ValueStringPointer(), + UseOverlayNetwork: p.NetworkSettings.UseOverlayNetwork.ValueBool(), + WhitelistK8sClusterAccessIPCIDRs: utils.FromListValueToStringList(p.NetworkSettings.WhitelistK8sClusterAccessIPCIDRs), + WhitelistWorkloadAccessIPCIDRs: utils.FromListValueToStringList(p.NetworkSettings.WhitelistWorkloadAccessIPCIDRs), + UsePrivateLoadBalancer: p.NetworkSettings.UsePrivateLoadBalancer.ValueBool(), + UsePublicWorkerNode: p.NetworkSettings.UsePublicWorkerNode.ValueBool(), + WorkerSubnetIds: utils.FromListValueToStringList(p.NetworkSettings.WorkerSubnetIDs), + LbSubnetIds: utils.FromListValueToStringList(p.NetworkSettings.LoadBalancerSubnetIDs), + NodeRoleCDWManagedPolicyArn: p.NodeRoleCDWManagedPolicyArn.ValueString(), + DatabaseBackupRetentionPeriod: utils.Int64To32Pointer(p.DatabaseBackupRetentionDays), + CustomSubdomain: p.CustomSubdomain.ValueString(), + CustomRegistryOptions: p.getCustomRegistryOptions(), + EnableSpotInstances: p.getEnableSpotInstances(), + CustomAmiID: p.getCustomAmiID(), + ComputeInstanceTypes: p.getComputeInstanceTypes(), + AdditionalInstanceTypes: p.getAdditionalInstanceTypes(), + } +} + +func (p *resourceModel) getEnableSpotInstances() *bool { + if i := p.InstanceSettings; i != nil { + return i.EnableSpotInstances.ValueBoolPointer() + } + return nil +} + +func (p *resourceModel) getCustomAmiID() string { + if i := p.InstanceSettings; i != nil { + return p.InstanceSettings.CustomAmiID.ValueString() + } + return "" +} + +func (p *resourceModel) getComputeInstanceTypes() []string { + if i := p.InstanceSettings; i != nil { + return utils.FromListValueToStringList(p.InstanceSettings.ComputeInstanceTypes) + } + return nil +} + +func (p *resourceModel) getAdditionalInstanceTypes() []string { + if i := p.InstanceSettings; i != nil { + return utils.FromListValueToStringList(p.InstanceSettings.AdditionalInstanceTypes) + } + return nil +} + +func (p *resourceModel) getCustomRegistryOptions() *models.CustomRegistryOptions { + if cro := p.CustomRegistryOptions; cro != nil { + return &models.CustomRegistryOptions{ + DisableImageVerification: true, // option will be deprecated, we should disallow image verification + RegistryType: p.CustomRegistryOptions.RegistryType.ValueString(), + RepositoryURL: p.CustomRegistryOptions.RepositoryURL.ValueString(), + } + } + return nil +} diff --git a/resources/dw/cluster/aws/resource_cluster.go b/resources/dw/cluster/aws/resource_cluster.go new file mode 100644 index 00000000..03349ec5 --- /dev/null +++ b/resources/dw/cluster/aws/resource_cluster.go @@ -0,0 +1,136 @@ +// Copyright 2024 Cloudera. All Rights Reserved. +// +// This file is 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. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package aws + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/dw/client/operations" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/dw/models" + "github.com/cloudera/terraform-provider-cdp/utils" +) + +type dwClusterResource struct { + client *cdp.Client +} + +var ( + _ resource.Resource = (*dwClusterResource)(nil) + _ resource.ResourceWithConfigure = (*dwClusterResource)(nil) +) + +func NewDwClusterResource() resource.Resource { + return &dwClusterResource{} +} + +func (r *dwClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = utils.GetCdpClientForResource(req, resp) +} + +func (r *dwClusterResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dw_aws_cluster" +} + +func (r *dwClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = dwClusterSchema +} + +func (r *dwClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Retrieve values from plan + var plan resourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from plan + clusterParams := operations.NewCreateAwsClusterParamsWithContext(ctx). + WithInput(plan.convertToCreateAwsClusterRequest()) + + // Create new aws cluster + response, err := r.client.Dw.Operations.CreateAwsCluster(clusterParams) + if err != nil { + resp.Diagnostics.AddError( + "Error creating data warehouse aws cluster", + "Could not create cluster, unexpected error: "+err.Error(), + ) + return + } + + payload := response.GetPayload() + desc := operations.NewDescribeClusterParamsWithContext(ctx). + WithInput(&models.DescribeClusterRequest{ClusterID: &payload.ClusterID}) + describe, err := r.client.Dw.Operations.DescribeCluster(desc) + if err != nil { + resp.Diagnostics.AddError( + "Error creating data warehouse aws cluster", + "Could not describe cluster, unexpected error: "+err.Error(), + ) + return + } + + cluster := describe.GetPayload() + + // Map response body to schema and populate Computed attribute values + plan.ID = types.StringValue(cluster.Cluster.EnvironmentCrn) + plan.Crn = types.StringValue(cluster.Cluster.EnvironmentCrn) + plan.Name = types.StringValue(cluster.Cluster.Name) + plan.ClusterID = types.StringValue(cluster.Cluster.ID) + plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) + + // Set state to fully populated data + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +func (r *dwClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Warn(ctx, "Read operation is not implemented yet.") +} + +func (r *dwClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Warn(ctx, "Update operation is not implemented yet.") +} + +func (r *dwClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state resourceModel + + // Read Terraform prior state into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + op := operations.NewDeleteClusterParamsWithContext(ctx). + WithInput(&models.DeleteClusterRequest{ + ClusterID: state.ClusterID.ValueStringPointer(), + // Force: true, + }) + + if _, err := r.client.Dw.Operations.DeleteCluster(op); err != nil { + resp.Diagnostics.AddError( + "Error deleting data warehouse aws cluster", + "Could not delete cluster, unexpected error: "+err.Error(), + ) + return + } +} diff --git a/resources/dw/cluster/aws/resource_cluster_test.go b/resources/dw/cluster/aws/resource_cluster_test.go new file mode 100644 index 00000000..3268b673 --- /dev/null +++ b/resources/dw/cluster/aws/resource_cluster_test.go @@ -0,0 +1,459 @@ +// Copyright 2024 Cloudera. All Rights Reserved. +// +// This file is 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. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package aws + +import ( + "context" + "fmt" + "testing" + + "github.com/go-openapi/runtime" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/cdp" + dwclient "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/dw/client" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/dw/client/operations" + "github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/dw/models" + mocks "github.com/cloudera/terraform-provider-cdp/mocks/github.com/cloudera/terraform-provider-cdp/cdp-sdk-go/gen/dw/client/operations" +) + +var testDwClusterSchema = schema.Schema{ + MarkdownDescription: "Creates an AWS Data Warehouse cluster.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "crn": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The cloudera resource name of the environment that the cluster will read from.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the cluster matches the environment name.", + }, + "cluster_id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The id of the cluster.", + }, + "last_updated": schema.StringAttribute{ + Description: "Timestamp of the last Terraform update of the order.", + Computed: true, + }, + "node_role_cdw_managed_policy_arn": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The managed policy ARN to be attached to the created node instance role.", + }, + "database_backup_retention_days": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The number of days to retain database backups.", + }, + "custom_registry_options": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "registry_type": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Registry type, supported values are ECR or ACR.", + }, + "repository_url": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The URL of the registry.", + }, + }, + }, + "custom_subdomain": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The custom subdomain to keep compatibility with old URL format.", + }, + "network_settings": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "worker_subnet_ids": schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + MarkdownDescription: "The list of subnet IDs for worker nodes.", + }, + "load_balancer_subnet_ids": schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + MarkdownDescription: "The list of subnet IDs for the load balancer.", + }, + "use_overlay_network": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Whether to use overlay network.", + }, + "whitelist_k8s_cluster_access_ip_cidrs": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The list of IP CIDRs to allow access for kubernetes cluster API endpoint.", + }, + "whitelist_workload_access_ip_cidrs": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The list of IP CIDRs to allow access for workload endpoints.", + }, + "use_private_load_balancer": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Whether to use private IP addresses for the load balancer. Determines workload endpoint access.", + }, + "use_public_worker_node": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Whether to use public IP addresses for worker nodes.", + }, + }, + }, + "instance_settings": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "custom_ami_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The custom AMI ID to use for worker nodes.", + }, + "enable_spot_instances": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Whether to use spot instances for worker nodes.", + }, + "compute_instance_types": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The compute instance types that the environment is restricted to use. This affects the creation of virtual warehouses where this restriction will apply. Select an instance type that meets your computing, memory, networking, or storage needs. As of now, only a single instance type can be listed.", + }, + "additional_instance_types": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The additional instance types that the environment is allowed to use, listed in their priority order. They will be used instead of the primary compute instance type in case it is unavailable. You cannot include any instance type that was already indicated in computeInstanceTypes.", + }, + }, + }, + }, +} + +type MockTransport struct { + runtime.ClientTransport +} + +func NewDwApi(client *mocks.MockDwClientService) *dwClusterResource { + return &dwClusterResource{ + client: &cdp.Client{ + Dw: &dwclient.Dw{ + Operations: client, + Transport: MockTransport{}, + }}} +} + +func createRawClusterResource() tftypes.Value { + return tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "crn": tftypes.String, + "name": tftypes.String, + "cluster_id": tftypes.String, + "last_updated": tftypes.String, + "node_role_cdw_managed_policy_arn": tftypes.String, + "database_backup_retention_days": tftypes.Number, + "custom_registry_options": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "registry_type": tftypes.String, + "repository_url": tftypes.String, + }, + }, + "custom_subdomain": tftypes.String, + "network_settings": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "worker_subnet_ids": tftypes.List{ElementType: tftypes.String}, + "load_balancer_subnet_ids": tftypes.List{ElementType: tftypes.String}, + "use_overlay_network": tftypes.Bool, + "whitelist_k8s_cluster_access_ip_cidrs": tftypes.List{ElementType: tftypes.String}, + "whitelist_workload_access_ip_cidrs": tftypes.List{ElementType: tftypes.String}, + "use_private_load_balancer": tftypes.Bool, + "use_public_worker_node": tftypes.Bool, + }, + }, + "instance_settings": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "custom_ami_id": tftypes.String, + "enable_spot_instances": tftypes.Bool, + "compute_instance_types": tftypes.List{ElementType: tftypes.String}, + "additional_instance_types": tftypes.List{ElementType: tftypes.String}, + }, + }, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, ""), + "crn": tftypes.NewValue(tftypes.String, "crn"), + "name": tftypes.NewValue(tftypes.String, ""), + "cluster_id": tftypes.NewValue(tftypes.String, ""), + "last_updated": tftypes.NewValue(tftypes.String, ""), + "node_role_cdw_managed_policy_arn": tftypes.NewValue(tftypes.String, ""), + "database_backup_retention_days": tftypes.NewValue(tftypes.Number, 0), + "custom_registry_options": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "registry_type": tftypes.String, + "repository_url": tftypes.String, + }, + }, map[string]tftypes.Value{ + "registry_type": tftypes.NewValue(tftypes.String, ""), + "repository_url": tftypes.NewValue(tftypes.String, ""), + }), + "custom_subdomain": tftypes.NewValue(tftypes.String, ""), + "network_settings": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "worker_subnet_ids": tftypes.List{ElementType: tftypes.String}, + "load_balancer_subnet_ids": tftypes.List{ElementType: tftypes.String}, + "use_overlay_network": tftypes.Bool, + "whitelist_k8s_cluster_access_ip_cidrs": tftypes.List{ElementType: tftypes.String}, + "whitelist_workload_access_ip_cidrs": tftypes.List{ElementType: tftypes.String}, + "use_private_load_balancer": tftypes.Bool, + "use_public_worker_node": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "worker_subnet_ids": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "subnet-1"), + tftypes.NewValue(tftypes.String, "subnet-2"), + tftypes.NewValue(tftypes.String, "subnet-3"), + }), + "load_balancer_subnet_ids": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "subnet-4"), + tftypes.NewValue(tftypes.String, "subnet-5"), + tftypes.NewValue(tftypes.String, "subnet-6"), + }), + "use_overlay_network": tftypes.NewValue(tftypes.Bool, true), + "whitelist_k8s_cluster_access_ip_cidrs": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "cidr-1"), + tftypes.NewValue(tftypes.String, "cidr-2"), + tftypes.NewValue(tftypes.String, "cidr-3"), + }), + "whitelist_workload_access_ip_cidrs": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "cidr-4"), + tftypes.NewValue(tftypes.String, "cidr-2"), + tftypes.NewValue(tftypes.String, "cidr-3"), + }), + "use_private_load_balancer": tftypes.NewValue(tftypes.Bool, true), + "use_public_worker_node": tftypes.NewValue(tftypes.Bool, false), + }, + ), + "instance_settings": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "custom_ami_id": tftypes.String, + "enable_spot_instances": tftypes.Bool, + "compute_instance_types": tftypes.List{ElementType: tftypes.String}, + "additional_instance_types": tftypes.List{ElementType: tftypes.String}, + }}, map[string]tftypes.Value{ + "custom_ami_id": tftypes.NewValue(tftypes.String, ""), + "enable_spot_instances": tftypes.NewValue(tftypes.Bool, false), + "compute_instance_types": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{}), + "additional_instance_types": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{}), + }, + ), + }) +} + +type DwClusterTestSuite struct { + suite.Suite + expectedCreateResponse *operations.CreateAwsClusterOK +} + +func TestDwAwsClusterTestSuite(t *testing.T) { + suite.Run(t, new(DwClusterTestSuite)) +} + +func (suite *DwClusterTestSuite) SetupTest() { + suite.expectedCreateResponse = &operations.CreateAwsClusterOK{ + Payload: &models.CreateAwsClusterResponse{ + ClusterID: "cluster-id"}} +} + +func (suite *DwClusterTestSuite) TestDwAwsClusterMetadata() { + dwApi := NewDwApi(new(mocks.MockDwClientService)) + resp := resource.MetadataResponse{} + + // Function under test + dwApi.Metadata( + context.TODO(), + resource.MetadataRequest{ProviderTypeName: "cdp"}, + &resp, + ) + suite.Equal("cdp_dw_aws_cluster", resp.TypeName) +} + +func (suite *DwClusterTestSuite) TestDwAwsClusterSchema() { + dwApi := NewDwApi(new(mocks.MockDwClientService)) + resp := resource.SchemaResponse{} + + // Function under test + dwApi.Schema( + context.TODO(), + resource.SchemaRequest{}, + &resp, + ) + suite.Equal(testDwClusterSchema, resp.Schema) +} + +func (suite *DwClusterTestSuite) TestDwAwsClusterCreate_Success() { + ctx := context.TODO() + expectedDescribeResponse := &operations.DescribeClusterOK{ + Payload: &models.DescribeClusterResponse{ + Cluster: &models.ClusterSummaryResponse{ + EnvironmentCrn: "crn", + ID: "cluster-id", + Name: "test-name", + }}} + + client := new(mocks.MockDwClientService) + client.On("CreateAwsCluster", mock.Anything).Return(suite.expectedCreateResponse, nil) + client.On("DescribeCluster", mock.Anything).Return(expectedDescribeResponse, nil) + dwApi := NewDwApi(client) + + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawClusterResource(), + Schema: testDwClusterSchema, + }, + } + + resp := &resource.CreateResponse{ + State: tfsdk.State{ + Schema: testDwClusterSchema, + }, + } + + // Function under test + dwApi.Create(ctx, req, resp) + var result resourceModel + resp.State.Get(ctx, &result) + suite.False(resp.Diagnostics.HasError()) + suite.Equal("crn", result.ID.ValueString()) + suite.Equal("crn", result.Crn.ValueString()) + suite.Equal("cluster-id", result.ClusterID.ValueString()) + suite.Equal("test-name", result.Name.ValueString()) +} + +func (suite *DwClusterTestSuite) TestDwAwsClusterCreate_CreationError() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + client.On("CreateAwsCluster", mock.Anything).Return(&operations.CreateAwsClusterOK{}, fmt.Errorf("create failed")) + client.On("DescribeCluster", mock.Anything).Return(&operations.DescribeClusterOK{}, nil) + dwApi := NewDwApi(client) + + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawClusterResource(), + Schema: testDwClusterSchema, + }, + } + + resp := &resource.CreateResponse{ + State: tfsdk.State{ + Schema: testDwClusterSchema, + }, + } + + // Function under test + dwApi.Create(ctx, req, resp) + var result resourceModel + resp.State.Get(ctx, &result) + suite.True(resp.Diagnostics.HasError()) + suite.Contains(resp.Diagnostics.Errors()[0].Summary(), "Error creating data warehouse aws cluster") + suite.Contains(resp.Diagnostics.Errors()[0].Detail(), "Could not create cluster") +} + +func (suite *DwClusterTestSuite) TestDwAwsClusterCreate_DescribeError() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + client.On("CreateAwsCluster", mock.Anything).Return(suite.expectedCreateResponse, nil) + client.On("DescribeCluster", mock.Anything).Return(&operations.DescribeClusterOK{}, fmt.Errorf("describe failed")) + dwApi := NewDwApi(client) + + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawClusterResource(), + Schema: testDwClusterSchema, + }, + } + + resp := &resource.CreateResponse{ + State: tfsdk.State{ + Schema: testDwClusterSchema, + }, + } + + // Function under test + dwApi.Create(ctx, req, resp) + var result resourceModel + resp.State.Get(ctx, &result) + suite.True(resp.Diagnostics.HasError()) + suite.Contains(resp.Diagnostics.Errors()[0].Summary(), "Error creating data warehouse aws cluster") + suite.Contains(resp.Diagnostics.Errors()[0].Detail(), "Could not describe cluster") +} + +func (suite *DwClusterTestSuite) TestDwAwsClusterDeletion_Success() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + client.On("DeleteCluster", mock.Anything).Return(&operations.DeleteClusterOK{}, nil) + dwApi := NewDwApi(client) + + req := resource.DeleteRequest{ + State: tfsdk.State{ + Schema: testDwClusterSchema, + Raw: createRawClusterResource(), + }, + } + + resp := &resource.DeleteResponse{} + + // Function under test + dwApi.Delete(ctx, req, resp) + suite.False(resp.Diagnostics.HasError()) +} + +func (suite *DwClusterTestSuite) TestDwAwsClusterDeletion_ReturnsError() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + client.On("DeleteCluster", mock.Anything).Return(&operations.DeleteClusterOK{}, fmt.Errorf("delete failed")) + dwApi := NewDwApi(client) + + req := resource.DeleteRequest{ + State: tfsdk.State{ + Schema: testDwClusterSchema, + Raw: createRawClusterResource(), + }, + } + + resp := &resource.DeleteResponse{} + + // Function under test + dwApi.Delete(ctx, req, resp) + suite.True(resp.Diagnostics.HasError()) + suite.Contains(resp.Diagnostics.Errors()[0].Summary(), "Error deleting data warehouse aws cluster") + suite.Contains(resp.Diagnostics.Errors()[0].Detail(), "Could not delete cluster") +} diff --git a/resources/dw/cluster/aws/schema_cluster.go b/resources/dw/cluster/aws/schema_cluster.go new file mode 100644 index 00000000..82519d38 --- /dev/null +++ b/resources/dw/cluster/aws/schema_cluster.go @@ -0,0 +1,141 @@ +// Copyright 2024 Cloudera. All Rights Reserved. +// +// This file is 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. +// +// This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, either express or implied. Refer to the License for the specific +// permissions and limitations governing your use of the file. + +package aws + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var dwClusterSchema = schema.Schema{ + MarkdownDescription: "Creates an AWS Data Warehouse cluster.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "crn": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The cloudera resource name of the environment that the cluster will read from.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the cluster matches the environment name.", + }, + "cluster_id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The id of the cluster.", + }, + "last_updated": schema.StringAttribute{ + Description: "Timestamp of the last Terraform update of the order.", + Computed: true, + }, + "node_role_cdw_managed_policy_arn": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The managed policy ARN to be attached to the created node instance role.", + }, + "database_backup_retention_days": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The number of days to retain database backups.", + }, + "custom_registry_options": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "registry_type": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Registry type, supported values are ECR or ACR.", + }, + "repository_url": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The URL of the registry.", + }, + }, + }, + "custom_subdomain": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The custom subdomain to keep compatibility with old URL format.", + }, + "network_settings": schema.SingleNestedAttribute{ + Required: true, + Attributes: networkSettings, + }, + "instance_settings": schema.SingleNestedAttribute{ + Optional: true, + Attributes: instanceSettings, + }, + }, +} + +var networkSettings = map[string]schema.Attribute{ + "worker_subnet_ids": schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + MarkdownDescription: "The list of subnet IDs for worker nodes.", + }, + "load_balancer_subnet_ids": schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + MarkdownDescription: "The list of subnet IDs for the load balancer.", + }, + "use_overlay_network": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Whether to use overlay network.", + }, + "whitelist_k8s_cluster_access_ip_cidrs": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The list of IP CIDRs to allow access for kubernetes cluster API endpoint.", + }, + "whitelist_workload_access_ip_cidrs": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The list of IP CIDRs to allow access for workload endpoints.", + }, + "use_private_load_balancer": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Whether to use private IP addresses for the load balancer. Determines workload endpoint access.", + }, + "use_public_worker_node": schema.BoolAttribute{ + Required: true, + MarkdownDescription: "Whether to use public IP addresses for worker nodes.", + }, +} + +var instanceSettings = map[string]schema.Attribute{ + "custom_ami_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The custom AMI ID to use for worker nodes.", + }, + "enable_spot_instances": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Whether to use spot instances for worker nodes.", + }, + "compute_instance_types": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The compute instance types that the environment is restricted to use. This affects the creation of virtual warehouses where this restriction will apply. Select an instance type that meets your computing, memory, networking, or storage needs. As of now, only a single instance type can be listed.", + }, + "additional_instance_types": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + MarkdownDescription: "The additional instance types that the environment is allowed to use, listed in their priority order. They will be used instead of the primary compute instance type in case it is unavailable. You cannot include any instance type that was already indicated in computeInstanceTypes.", + }, +}