diff --git a/provider/provider.go b/provider/provider.go index 5a0151d9..6c0b78ff 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -31,6 +31,7 @@ import ( "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" + dwdatabasecatalog "github.com/cloudera/terraform-provider-cdp/resources/dw/databasecatalog" "github.com/cloudera/terraform-provider-cdp/resources/environments" "github.com/cloudera/terraform-provider-cdp/resources/iam" "github.com/cloudera/terraform-provider-cdp/resources/ml" @@ -250,6 +251,7 @@ func (p *CdpProvider) Resources(_ context.Context) []func() resource.Resource { de.NewServiceResource, dw.NewHiveResource, dwaws.NewDwClusterResource, + dwdatabasecatalog.NewDwDatabaseCatalogResource, } } diff --git a/provider/provider_test.go b/provider/provider_test.go index 0d58d15e..9b1ae792 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -31,6 +31,7 @@ import ( "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" + dwdatabasecatalog "github.com/cloudera/terraform-provider-cdp/resources/dw/databasecatalog" "github.com/cloudera/terraform-provider-cdp/resources/environments" "github.com/cloudera/terraform-provider-cdp/resources/iam" "github.com/cloudera/terraform-provider-cdp/resources/ml" @@ -636,6 +637,7 @@ func TestCdpProvider_Resources(t *testing.T) { de.NewServiceResource, dw.NewHiveResource, dwaws.NewDwClusterResource, + dwdatabasecatalog.NewDwDatabaseCatalogResource, } provider := CdpProvider{testVersion} diff --git a/resources/dw/databasecatalog/model_catalog.go b/resources/dw/databasecatalog/model_catalog.go new file mode 100644 index 00000000..59a40540 --- /dev/null +++ b/resources/dw/databasecatalog/model_catalog.go @@ -0,0 +1,42 @@ +// 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 databasecatalog + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/cloudera/terraform-provider-cdp/utils" +) + +type resourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ClusterID types.String `tfsdk:"cluster_id"` + LastUpdated types.String `tfsdk:"last_updated"` + Status types.String `tfsdk:"status"` + PollingOptions *utils.PollingOptions `tfsdk:"polling_options"` +} + +func (p *resourceModel) getPollingTimeout() time.Duration { + if p.PollingOptions != nil { + return time.Duration(p.PollingOptions.PollingTimeout.ValueInt64()) * time.Minute + } + return 40 * time.Minute +} + +func (p *resourceModel) getCallFailureThreshold() int { + if p.PollingOptions != nil { + return int(p.PollingOptions.CallFailureThreshold.ValueInt64()) + } + return 3 +} diff --git a/resources/dw/databasecatalog/model_catalog_test.go b/resources/dw/databasecatalog/model_catalog_test.go new file mode 100644 index 00000000..03dffbc7 --- /dev/null +++ b/resources/dw/databasecatalog/model_catalog_test.go @@ -0,0 +1,51 @@ +// 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 databasecatalog + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/stretchr/testify/suite" + "testing" + "time" +) + +type DwDatabaseCatalogModelTestSuite struct { + suite.Suite + rm *resourceModel +} + +func TestDwModelDatabaseCatalogTestSuite(t *testing.T) { + suite.Run(t, new(DwDatabaseCatalogModelTestSuite)) +} + +func (s *DwDatabaseCatalogModelTestSuite) SetupSuite() { + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawCatalogResource(), + Schema: testDatabaseCatalogSchema, + }, + } + rm := &resourceModel{} + req.Plan.Get(context.Background(), &rm) + s.rm = rm +} + +func (s *DwDatabaseCatalogModelTestSuite) TestGetPollingTimeout() { + timeout := s.rm.getPollingTimeout() + s.Equal(90*time.Minute, timeout) +} + +func (s *DwDatabaseCatalogModelTestSuite) TestGetCallFailureThreshold() { + out := s.rm.getCallFailureThreshold() + s.Equal(3, out) +} diff --git a/resources/dw/databasecatalog/resource_catalog.go b/resources/dw/databasecatalog/resource_catalog.go new file mode 100644 index 00000000..5a28cbcd --- /dev/null +++ b/resources/dw/databasecatalog/resource_catalog.go @@ -0,0 +1,170 @@ +// 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 databasecatalog + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + + "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 dwDatabaseCatalogResource struct { + client *cdp.Client +} + +var ( + _ resource.Resource = (*dwDatabaseCatalogResource)(nil) + _ resource.ResourceWithConfigure = (*dwDatabaseCatalogResource)(nil) +) + +func NewDwDatabaseCatalogResource() resource.Resource { + return &dwDatabaseCatalogResource{} +} + +func (r *dwDatabaseCatalogResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = utils.GetCdpClientForResource(req, resp) +} + +func (r *dwDatabaseCatalogResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dw_database_catalog" +} + +func (r *dwDatabaseCatalogResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = dwDefaultDatabaseCatalogSchema +} + +func (r *dwDatabaseCatalogResource) 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 + } + + clusterID := plan.ClusterID.ValueStringPointer() + + if opts := plan.PollingOptions; !(opts != nil && opts.Async.ValueBool()) { + callFailedCount := 0 + stateConf := &retry.StateChangeConf{ + Pending: []string{"Accepted", "Creating", "Created", "Loading", "Starting"}, + Target: []string{"Running"}, + Delay: 30 * time.Second, + Timeout: plan.getPollingTimeout(), + PollInterval: 30 * time.Second, + Refresh: r.stateRefresh(ctx, clusterID, &callFailedCount, plan.getCallFailureThreshold()), + } + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + resp.Diagnostics.AddError( + "Error waiting for Data Warehouse database catalog", + fmt.Sprintf("Could not create database catalog, unexpected error: %v", err), + ) + return + } + } + + catalog, err := r.getDatabaseCatalog(ctx, clusterID) + if err != nil { + resp.Diagnostics.AddError( + "Error finding Data Warehouse database catalog", fmt.Sprintf("unexpected error: %v", err), + ) + return + } + plan.ID = types.StringValue(catalog.ID) + plan.Name = types.StringValue(catalog.Name) + plan.Status = types.StringValue(catalog.Status) + 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...) +} + +func (r *dwDatabaseCatalogResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Warn(ctx, "Read operation is not implemented yet.") +} + +func (r *dwDatabaseCatalogResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Warn(ctx, "Update operation is not implemented yet.") +} + +func (r *dwDatabaseCatalogResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state resourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + // Catalog is deleted as part of environment deletion, no need to delete it explicitly + state.Status = types.StringValue("Deleted") + resp.State.Set(ctx, state) +} + +func (r *dwDatabaseCatalogResource) stateRefresh(ctx context.Context, clusterID *string, callFailedCount *int, callFailureThreshold int) func() (any, string, error) { + return func() (any, string, error) { + tflog.Debug(ctx, "About to get DBCs") + catalogParams := operations.NewListDbcsParamsWithContext(ctx).WithInput(&models.ListDbcsRequest{ClusterID: clusterID}) + // List existing catalogs + response, err := r.client.Dw.Operations.ListDbcs(catalogParams) + if err != nil { + tflog.Error(ctx, + fmt.Sprintf("could not list database catalogs, unexpected error: %s", err.Error()), + ) + return nil, "", err + } + resp := response.GetPayload() + if len(resp.Dbcs) == 0 { + *callFailedCount++ + if *callFailedCount <= callFailureThreshold { + tflog.Warn(ctx, fmt.Sprintf("could not find Data Warehouse database catalog "+ + "due to [%s] but threshold limit is not reached yet (%d out of %d).", err.Error(), callFailedCount, callFailureThreshold)) + return nil, "", nil + } + tflog.Error(ctx, fmt.Sprintf("error describing Data Warehouse database catalog due to [%s] "+ + "failure threshold limit exceeded.", err.Error())) + return nil, "", err + } + if len(resp.Dbcs) > 1 { + err = fmt.Errorf("found more than one Data Warehouse database catalog for cluster %s", *clusterID) + tflog.Error(ctx, fmt.Sprintf("error describing Data Warehouse database catalog due to [%s] ", err.Error())) + return nil, "", err + } + *callFailedCount = 0 + + tflog.Debug(ctx, fmt.Sprintf("Found database catalog %s with status %s", resp.Dbcs[0].ID, resp.Dbcs[0].Status)) + return resp.Dbcs[0], resp.Dbcs[0].Status, nil + } +} + +func (r *dwDatabaseCatalogResource) getDatabaseCatalog(ctx context.Context, clusterID *string) (*models.DbcSummary, error) { + catalogParams := operations.NewListDbcsParamsWithContext(ctx).WithInput(&models.ListDbcsRequest{ClusterID: clusterID}) + // List existing catalogs + response, err := r.client.Dw.Operations.ListDbcs(catalogParams) + if err != nil { + err = fmt.Errorf("could not list database catalogs, unexpected error: %s", err.Error()) + return nil, err + } + resp := response.GetPayload() + if len(resp.Dbcs) != 1 { + err = fmt.Errorf("exactly one Data Warehouse database catalog should be deployed for cluster %s", *clusterID) + return nil, err + } + return resp.Dbcs[0], nil +} diff --git a/resources/dw/databasecatalog/resource_catalog_test.go b/resources/dw/databasecatalog/resource_catalog_test.go new file mode 100644 index 00000000..39b311b2 --- /dev/null +++ b/resources/dw/databasecatalog/resource_catalog_test.go @@ -0,0 +1,379 @@ +// 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 databasecatalog + +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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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-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 testDatabaseCatalogSchema = schema.Schema{ + MarkdownDescription: "Creates an AWS Data Warehouse database catalog.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the database catalog.", + }, + "cluster_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The unique identifier of the cluster.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "last_updated": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Timestamp of the last Terraform update of the order.", + }, + "status": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The status of the database catalog.", + }, + "polling_options": schema.SingleNestedAttribute{ + MarkdownDescription: "Polling related configuration options that could specify various values that will be used during CDP resource creation.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "async": schema.BoolAttribute{ + MarkdownDescription: "Boolean value that specifies if Terraform should wait for resource creation/deletion.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "polling_timeout": schema.Int64Attribute{ + MarkdownDescription: "Timeout value in minutes that specifies for how long should the polling go for resource creation/deletion.", + Default: int64default.StaticInt64(40), + Computed: true, + Optional: true, + }, + "call_failure_threshold": schema.Int64Attribute{ + MarkdownDescription: "Threshold value that specifies how many times should a single call failure happen before giving up the polling.", + Default: int64default.StaticInt64(3), + Computed: true, + Optional: true, + }, + }, + }, + }, +} + +type MockTransport struct { + runtime.ClientTransport +} + +func NewDwApi(client *mocks.MockDwClientService) *dwDatabaseCatalogResource { + return &dwDatabaseCatalogResource{ + client: &cdp.Client{ + Dw: &dwclient.Dw{ + Operations: client, + Transport: MockTransport{}, + }}} +} + +func createRawCatalogResource() tftypes.Value { + return tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "cluster_id": tftypes.String, + "last_updated": tftypes.String, + "status": tftypes.String, + "polling_options": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "async": tftypes.Bool, + "polling_timeout": tftypes.Number, + "call_failure_threshold": tftypes.Number, + }, + }, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id"), + "name": tftypes.NewValue(tftypes.String, "name"), + "cluster_id": tftypes.NewValue(tftypes.String, "cluster-id"), + "last_updated": tftypes.NewValue(tftypes.String, ""), + "status": tftypes.NewValue(tftypes.String, "Accepted"), + "polling_options": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "async": tftypes.Bool, + "polling_timeout": tftypes.Number, + "call_failure_threshold": tftypes.Number, + }}, map[string]tftypes.Value{ + "async": tftypes.NewValue(tftypes.Bool, true), + "polling_timeout": tftypes.NewValue(tftypes.Number, 90), + "call_failure_threshold": tftypes.NewValue(tftypes.Number, 3), + }), + }) +} + +type DwDatabaseCatalogTestSuite struct { + suite.Suite + expectedListResponse *operations.ListDbcsOK +} + +func TestDwDatabaseCatalogTestSuite(t *testing.T) { + suite.Run(t, new(DwDatabaseCatalogTestSuite)) +} + +func (suite *DwDatabaseCatalogTestSuite) SetupTest() { + suite.expectedListResponse = &operations.ListDbcsOK{ + Payload: &models.ListDbcsResponse{ + Dbcs: []*models.DbcSummary{ + { + ID: "dbc-id", + Name: "name", + Status: "Accepted", + }, + }, + }, + } +} + +func (suite *DwDatabaseCatalogTestSuite) TestDwDatabaseCatalogMetadata() { + dwApi := NewDwApi(new(mocks.MockDwClientService)) + resp := resource.MetadataResponse{} + + // Function under test + dwApi.Metadata( + context.TODO(), + resource.MetadataRequest{ProviderTypeName: "cdp"}, + &resp, + ) + suite.Equal("cdp_dw_database_catalog", resp.TypeName) +} + +func (suite *DwDatabaseCatalogTestSuite) TestDwDatabaseCatalogSchema() { + dwApi := NewDwApi(new(mocks.MockDwClientService)) + resp := resource.SchemaResponse{} + + // Function under test + dwApi.Schema( + context.TODO(), + resource.SchemaRequest{}, + &resp, + ) + suite.Equal(testDatabaseCatalogSchema, resp.Schema) +} + +func (suite *DwDatabaseCatalogTestSuite) TestDwDatabaseCatalogCreate_Success() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + client.On("ListDbcs", mock.Anything).Return(suite.expectedListResponse, nil) + dwApi := NewDwApi(client) + + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawCatalogResource(), + Schema: testDatabaseCatalogSchema, + }, + } + + resp := &resource.CreateResponse{ + State: tfsdk.State{ + Schema: testDatabaseCatalogSchema, + }, + } + + // Function under test + dwApi.Create(ctx, req, resp) + var result resourceModel + resp.State.Get(ctx, &result) + suite.False(resp.Diagnostics.HasError()) + suite.Equal("dbc-id", result.ID.ValueString()) + suite.Equal("cluster-id", result.ClusterID.ValueString()) + suite.Equal("name", result.Name.ValueString()) + suite.Equal("Accepted", result.Status.ValueString()) +} + +func (suite *DwDatabaseCatalogTestSuite) TestDwDatabaseCatalogCreate_CreationError() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + expectedListResponse := &operations.ListDbcsOK{ + Payload: &models.ListDbcsResponse{ + Dbcs: []*models.DbcSummary{}, + }, + } + client.On("ListDbcs", mock.Anything).Return(expectedListResponse, nil) + dwApi := NewDwApi(client) + + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawCatalogResource(), + Schema: testDatabaseCatalogSchema, + }, + } + + resp := &resource.CreateResponse{ + State: tfsdk.State{ + Schema: testDatabaseCatalogSchema, + }, + } + + // 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 finding Data Warehouse database catalog") + suite.Contains(resp.Diagnostics.Errors()[0].Detail(), + "exactly one Data Warehouse database catalog should be deployed for cluster") +} + +func (suite *DwDatabaseCatalogTestSuite) TestDwDatabaseCatalogCreate_TooManyDbcsError() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + expectedListResponse := &operations.ListDbcsOK{ + Payload: &models.ListDbcsResponse{ + Dbcs: []*models.DbcSummary{ + { + ID: "dbc-id", + Name: "name", + Status: "Accepted", + }, + { + ID: "custom-dbc-id", + Name: "custom-name", + Status: "Accepted", + }, + }, + }, + } + client.On("ListDbcs", mock.Anything).Return(expectedListResponse, nil) + dwApi := NewDwApi(client) + + req := resource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: createRawCatalogResource(), + Schema: testDatabaseCatalogSchema, + }, + } + + resp := &resource.CreateResponse{ + State: tfsdk.State{ + Schema: testDatabaseCatalogSchema, + }, + } + + // 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 finding Data Warehouse database catalog") + suite.Contains(resp.Diagnostics.Errors()[0].Detail(), "exactly one Data Warehouse database catalog should be deployed for cluster") +} + +func (suite *DwDatabaseCatalogTestSuite) TestDwDatabaseCatalogDeletion_Success() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + dwApi := NewDwApi(client) + + req := resource.DeleteRequest{ + State: tfsdk.State{ + Schema: testDatabaseCatalogSchema, + Raw: createRawCatalogResource(), + }, + } + resp := &resource.DeleteResponse{ + State: tfsdk.State{ + Schema: testDatabaseCatalogSchema, + }, + } + + // Function under test + dwApi.Delete(ctx, req, resp) + var result resourceModel + resp.State.Get(ctx, &result) + suite.False(resp.Diagnostics.HasError()) + suite.Equal("Deleted", result.Status.ValueString()) +} + +func (suite *DwDatabaseCatalogTestSuite) TestStateRefresh_Success() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + client.On("ListDbcs", mock.Anything).Return( + &operations.ListDbcsOK{ + Payload: &models.ListDbcsResponse{ + Dbcs: []*models.DbcSummary{ + { + ID: "dbc-id", + Name: "name", + Status: "Running", + }, + }, + }, + }, + nil) + dwApi := NewDwApi(client) + + clusterID := "cluster-id" + callFailedCount := 0 + callFailureThreshold := 3 + + // Function under test + refresh := dwApi.stateRefresh(ctx, &clusterID, &callFailedCount, callFailureThreshold) + _, status, err := refresh() + suite.NoError(err) + suite.Equal("Running", status) +} + +func (suite *DwDatabaseCatalogTestSuite) TestStateRefresh_FailureThresholdReached() { + ctx := context.TODO() + client := new(mocks.MockDwClientService) + client.On("ListDbcs", mock.Anything).Return( + &operations.ListDbcsOK{}, fmt.Errorf("unknown error")) + dwApi := NewDwApi(client) + + clusterID := "cluster-id" + callFailedCount := 0 + callFailureThreshold := 3 + + // Function under test + refresh := dwApi.stateRefresh(ctx, &clusterID, &callFailedCount, callFailureThreshold) + var err error + for i := 0; i <= callFailureThreshold; i++ { + _, _, err = refresh() + } + suite.Error(err, "unknown error") +} + +func (suite *DwDatabaseCatalogTestSuite) TestGetDatabaseCatalog_Success() { + +} diff --git a/resources/dw/databasecatalog/schema_catalog.go b/resources/dw/databasecatalog/schema_catalog.go new file mode 100644 index 00000000..dbe5a235 --- /dev/null +++ b/resources/dw/databasecatalog/schema_catalog.go @@ -0,0 +1,78 @@ +// 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 databasecatalog + +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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +var dwDefaultDatabaseCatalogSchema = schema.Schema{ + MarkdownDescription: "Creates an AWS Data Warehouse database catalog.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the database catalog.", + }, + "cluster_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The unique identifier of the cluster.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "last_updated": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Timestamp of the last Terraform update of the order.", + }, + "status": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The status of the database catalog.", + }, + "polling_options": schema.SingleNestedAttribute{ + MarkdownDescription: "Polling related configuration options that could specify various values that will be used during CDP resource creation.", + Optional: true, + Attributes: map[string]schema.Attribute{ + "async": schema.BoolAttribute{ + MarkdownDescription: "Boolean value that specifies if Terraform should wait for resource creation/deletion.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "polling_timeout": schema.Int64Attribute{ + MarkdownDescription: "Timeout value in minutes that specifies for how long should the polling go for resource creation/deletion.", + Default: int64default.StaticInt64(40), + Computed: true, + Optional: true, + }, + "call_failure_threshold": schema.Int64Attribute{ + MarkdownDescription: "Threshold value that specifies how many times should a single call failure happen before giving up the polling.", + Default: int64default.StaticInt64(3), + Computed: true, + Optional: true, + }, + }, + }, + }, +}