From 2e544e8fe57f2bc6d506c481a7c470d12192de90 Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Thu, 30 Nov 2023 00:12:00 +0530 Subject: [PATCH 1/7] hanging resource issue is fixed for cluster and added retry logic --- internal/api/client.go | 98 +++++++++++++++++++++++++++++++++++ internal/errors/errors.go | 4 ++ internal/resources/cluster.go | 81 +++++++++++++++++++---------- internal/resources/project.go | 1 + 4 files changed, 158 insertions(+), 26 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index 1653e867..f1729fb7 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -2,7 +2,9 @@ package api import ( "bytes" + "context" "encoding/json" + goer "errors" "fmt" "io" "net/http" @@ -44,6 +46,8 @@ type EndpointCfg struct { SuccessStatus int } +const defaultWaitAttempt = time.Second * 2 + // Execute is used to construct and execute a HTTP request. // It then returns the response. func (c *Client) Execute( @@ -96,3 +100,97 @@ func (c *Client) Execute( Body: responseBody, }, nil } + +// ExecuteWithRetry is used to construct and execute a HTTP request with retry. +// It then returns the response. +func (c *Client) ExecuteWithRetry( + ctx context.Context, + endpointCfg EndpointCfg, + payload any, + authToken string, + headers map[string]string, +) (response *Response, err error) { + var requestBody []byte + if payload != nil { + requestBody, err = json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("%s: %w", errors.ErrMarshallingPayload, err) + } + } + + req, err := http.NewRequest(endpointCfg.Method, endpointCfg.Url, bytes.NewReader(requestBody)) + if err != nil { + return nil, fmt.Errorf("%s: %w", errors.ErrConstructingRequest, err) + } + + req.Header.Set("Authorization", "Bearer "+authToken) + for header, value := range headers { + req.Header.Set(header, value) + } + + var fn = func() (response *Response, err error) { + apiRes, err := c.Do(req) + if err != nil { + return nil, fmt.Errorf("%s: %w", errors.ErrExecutingRequest, err) + } + defer apiRes.Body.Close() + + responseBody, err := io.ReadAll(apiRes.Body) + if err != nil { + return + } + + switch apiRes.StatusCode { + case endpointCfg.SuccessStatus: + // success case + case http.StatusGatewayTimeout: + return nil, errors.ErrGatewayTimeout + default: + var apiError Error + if err := json.Unmarshal(responseBody, &apiError); err != nil { + return nil, fmt.Errorf( + "unexpected code: %d, expected: %d, body: %s", + apiRes.StatusCode, endpointCfg.SuccessStatus, responseBody) + } + return nil, &apiError + } + + return &Response{ + Response: apiRes, + Body: responseBody, + }, nil + } + + return exec(ctx, fn, defaultWaitAttempt) +} + +func exec(ctx context.Context, fn func() (response *Response, err error), waitOnReattempt time.Duration) (*Response, error) { + timer := time.NewTimer(time.Millisecond) + + var ( + err error + response *Response + ) + + const timeout = time.Minute * 5 + + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("timed out executing request against api: %w", err) + case <-timer.C: + response, err = fn() + switch { + case err == nil: + return response, err + case !goer.Is(err, errors.ErrGatewayTimeout): + return response, err + } + timer.Reset(waitOnReattempt) + } + } +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go index a59d01c7..998abb2f 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -149,4 +149,8 @@ var ( // ErrClusterCreationTimeoutAfterInitiation is returned when cluster creation // is timeout after initiation. ErrClusterCreationTimeoutAfterInitiation = errors.New("cluster creation status transition timed out after initiation") + + ErrNotFinalState = errors.New("not final state") + + ErrGatewayTimeout = errors.New("gateway timeout") ) diff --git a/internal/resources/cluster.go b/internal/resources/cluster.go index fda33e4c..3c12c8a7 100644 --- a/internal/resources/cluster.go +++ b/internal/resources/cluster.go @@ -28,6 +28,14 @@ var ( _ resource.ResourceWithImportState = &Cluster{} ) +const errorMessageAfterClusterCreationInitiation = "Cluster creation is initiated, but encounters an error while checking the current" + + " state of the cluster.Please run `terraform plan` after 4-5 minutes to know the" + + " current status of the cluster. Additionally, run `terraform apply --refresh-only` to update" + + " the status from remote, unexpected error: " + +const errorMessageWhileClusterCreation = "There is an error during cluster creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // Cluster is the Cluster resource implementation. type Cluster struct { *providerschema.Data @@ -58,7 +66,7 @@ func (c *Cluster) Create(ctx context.Context, req resource.CreateRequest, resp * return } - ClusterRequest := clusterapi.CreateClusterRequest{ + clusterRequest := clusterapi.CreateClusterRequest{ Name: plan.Name.ValueString(), Availability: clusterapi.Availability{ Type: clusterapi.AvailabilityType(plan.Availability.Type.ValueString()), @@ -75,7 +83,7 @@ func (c *Cluster) Create(ctx context.Context, req resource.CreateRequest, resp * } if !plan.Description.IsNull() && !plan.Description.IsUnknown() { - ClusterRequest.Description = plan.Description.ValueStringPointer() + clusterRequest.Description = plan.Description.ValueStringPointer() } var couchbaseServer providerschema.CouchbaseServer @@ -86,13 +94,13 @@ func (c *Cluster) Create(ctx context.Context, req resource.CreateRequest, resp * if !couchbaseServer.Version.IsNull() && !couchbaseServer.Version.IsUnknown() { version := couchbaseServer.Version.ValueString() - ClusterRequest.CouchbaseServer = &clusterapi.CouchbaseServer{ + clusterRequest.CouchbaseServer = &clusterapi.CouchbaseServer{ Version: &version, } } if !plan.ConfigurationType.IsNull() && !plan.ConfigurationType.IsUnknown() { - ClusterRequest.ConfigurationType = clusterapi.ConfigurationType(plan.ConfigurationType.ValueString()) + clusterRequest.ConfigurationType = clusterapi.ConfigurationType(plan.ConfigurationType.ValueString()) } serviceGroups, err := c.morphToApiServiceGroups(plan) @@ -104,7 +112,7 @@ func (c *Cluster) Create(ctx context.Context, req resource.CreateRequest, resp * return } - ClusterRequest.ServiceGroups = serviceGroups + clusterRequest.ServiceGroups = serviceGroups if plan.OrganizationId.IsNull() { resp.Diagnostics.AddError( @@ -126,44 +134,51 @@ func (c *Cluster) Create(ctx context.Context, req resource.CreateRequest, resp * url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters", c.HostURL, organizationId, projectId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusAccepted} - response, err := c.Client.Execute( + response, err := c.Client.ExecuteWithRetry( + ctx, cfg, - ClusterRequest, + clusterRequest, c.Token, nil, ) if err != nil { resp.Diagnostics.AddError( "Error creating cluster", - "Could not create cluster, unexpected error: "+api.ParseError(err), + errorMessageWhileClusterCreation+api.ParseError(err), ) return } - ClusterResponse := clusterapi.GetClusterResponse{} - err = json.Unmarshal(response.Body, &ClusterResponse) + clusterResponse := clusterapi.GetClusterResponse{} + err = json.Unmarshal(response.Body, &clusterResponse) if err != nil { resp.Diagnostics.AddError( "Error creating Cluster", - "Could not create Cluster, error during unmarshalling:"+err.Error(), + errorMessageWhileClusterCreation+"error during unmarshalling:"+err.Error(), ) return } - err = c.checkClusterStatus(ctx, organizationId, projectId, ClusterResponse.Id.String()) + diags = resp.State.Set(ctx, initializePendingClusterWithPlanAndId(plan, clusterResponse.Id.String())) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err = c.checkClusterStatus(ctx, organizationId, projectId, clusterResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error creating cluster", - "Could not create cluster, unexpected error: "+api.ParseError(err), + errorMessageAfterClusterCreationInitiation+api.ParseError(err), ) return } - refreshedState, err := c.retrieveCluster(ctx, organizationId, projectId, ClusterResponse.Id.String()) + refreshedState, err := c.retrieveCluster(ctx, organizationId, projectId, clusterResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error creating cluster", - "Could not create cluster, unexpected error: "+api.ParseError(err), + errorMessageAfterClusterCreationInitiation+api.ParseError(err), ) return } @@ -327,7 +342,8 @@ func (c *Cluster) Update(ctx context.Context, req resource.UpdateRequest, resp * // Update existing Cluster url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", c.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPut, SuccessStatus: http.StatusNoContent} - _, err = c.Client.Execute( + _, err = c.Client.ExecuteWithRetry( + ctx, cfg, ClusterRequest, c.Token, @@ -411,7 +427,8 @@ func (r *Cluster) Delete(ctx context.Context, req resource.DeleteRequest, resp * // Delete existing Cluster url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", r.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusAccepted} - _, err = r.Client.Execute( + _, err = r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -477,10 +494,11 @@ func (c *Cluster) ImportState(ctx context.Context, req resource.ImportStateReque // getCluster retrieves cluster information from the specified organization and project // using the provided cluster ID by open-api call -func (c *Cluster) getCluster(organizationId, projectId, clusterId string) (*clusterapi.GetClusterResponse, error) { +func (c *Cluster) getCluster(ctx context.Context, organizationId, projectId, clusterId string) (*clusterapi.GetClusterResponse, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", c.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := c.Client.Execute( + response, err := c.Client.ExecuteWithRetry( + ctx, cfg, nil, c.Token, @@ -501,7 +519,7 @@ func (c *Cluster) getCluster(organizationId, projectId, clusterId string) (*clus // retrieveCluster retrieves cluster information for a specified organization, project, and cluster ID. func (c *Cluster) retrieveCluster(ctx context.Context, organizationId, projectId, clusterId string) (*providerschema.Cluster, error) { - clusterResp, err := c.getCluster(organizationId, projectId, clusterId) + clusterResp, err := c.getCluster(ctx, organizationId, projectId, clusterId) if err != nil { return nil, fmt.Errorf("%s: %w", errors.ErrNotFound, err) } @@ -544,11 +562,9 @@ func (c *Cluster) checkClusterStatus(ctx context.Context, organizationId, projec for { select { case <-ctx.Done(): - const msg = "cluster creation status transition timed out after initiation" - return fmt.Errorf(msg) - + return fmt.Errorf("cluster creation status transition timed out after initiation, unexpected error: %w", err) case <-timer.C: - clusterResp, err = c.getCluster(organizationId, projectId, ClusterId) + clusterResp, err = c.getCluster(ctx, organizationId, projectId, ClusterId) switch err { case nil: if clusterapi.IsFinalState(clusterResp.CurrentState) { @@ -718,3 +734,16 @@ func getCouchbaseServer(ctx context.Context, config tfsdk.Config, diags *diag.Di tflog.Info(ctx, fmt.Sprintf("couchbase_server: %+v", couchbaseServer)) return couchbaseServer } + +func initializePendingClusterWithPlanAndId(plan providerschema.Cluster, id string) providerschema.Cluster { + plan.Id = types.StringValue(id) + plan.CurrentState = types.StringValue("pending") + if plan.CouchbaseServer.IsNull() || plan.CouchbaseServer.IsUnknown() { + plan.CouchbaseServer = types.ObjectNull(providerschema.CouchbaseServer{}.AttributeTypes()) + } + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) + plan.AppServiceId = types.StringNull() + plan.ConfigurationType = types.StringNull() + plan.Etag = types.StringNull() + return plan +} diff --git a/internal/resources/project.go b/internal/resources/project.go index 225a1aed..90322e6d 100644 --- a/internal/resources/project.go +++ b/internal/resources/project.go @@ -98,6 +98,7 @@ func (r *Project) Create(ctx context.Context, req resource.CreateRequest, resp * "Error creating project", "Could not create project, unexpected error: "+api.ParseError(err), ) + return } projectResponse := api.GetProjectResponse{} From f0a93d5409f6da5725d3022962c6b640e2119ad7 Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Tue, 5 Dec 2023 10:34:04 +0530 Subject: [PATCH 2/7] WIP --- internal/api/client.go | 2 +- internal/api/pagination.go | 3 +- internal/datasources/backups.go | 6 +- internal/datasources/certificate.go | 3 +- internal/datasources/organization.go | 3 +- .../allowlist_acceptance_test.go | 4 +- .../appservice_acceptance_test.go | 3 +- .../cluster_acceptance_test.go | 123 +++++++++--------- .../project_acceptance_test.go | 4 +- internal/resources/allowlist.go | 26 +++- internal/resources/apikey.go | 12 +- internal/resources/appservice.go | 48 +++++-- internal/resources/attributes.go | 28 ++++ internal/resources/backup.go | 21 +-- internal/resources/backup_schedule.go | 20 ++- internal/resources/bucket.go | 47 +++++-- internal/resources/cluster.go | 20 ++- internal/resources/cluster_schema.go | 6 +- internal/resources/database_credential.go | 56 +++++--- .../resources/database_credential_schema.go | 12 +- internal/resources/project.go | 12 +- internal/resources/user.go | 16 ++- internal/resources/user_schema.go | 4 +- internal/schema/bucket.go | 10 ++ 24 files changed, 342 insertions(+), 147 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index f1729fb7..7d004ed5 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -172,7 +172,7 @@ func exec(ctx context.Context, fn func() (response *Response, err error), waitOn response *Response ) - const timeout = time.Minute * 5 + const timeout = time.Minute * 10 var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) diff --git a/internal/api/pagination.go b/internal/api/pagination.go index c2bda5f4..65794927 100644 --- a/internal/api/pagination.go +++ b/internal/api/pagination.go @@ -87,7 +87,8 @@ func GetPaginated[DataSchema ~[]T, T any]( cfg.Url = baseUrl + fmt.Sprintf("?page=%d&perPage=%d&sortBy=%s", page, perPage, string(sortBy)) cfg.Method = http.MethodGet - response, err := client.Execute( + response, err := client.ExecuteWithRetry( + ctx, cfg, nil, token, diff --git a/internal/datasources/backups.go b/internal/datasources/backups.go index 62bf2f33..61fbfa29 100644 --- a/internal/datasources/backups.go +++ b/internal/datasources/backups.go @@ -61,7 +61,8 @@ func (d *Backups) Read(ctx context.Context, req datasource.ReadRequest, resp *da // Get all the cycles url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backup/cycles", d.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := d.Client.Execute( + response, err := d.Client.ExecuteWithRetry( + ctx, cfg, nil, d.Token, @@ -89,7 +90,8 @@ func (d *Backups) Read(ctx context.Context, req datasource.ReadRequest, resp *da for _, cycle := range cyclesResp.Data { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backup/cycles/%s", d.HostURL, organizationId, projectId, clusterId, bucketId, cycle.CycleId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := d.Client.Execute( + response, err := d.Client.ExecuteWithRetry( + ctx, cfg, nil, d.Token, diff --git a/internal/datasources/certificate.go b/internal/datasources/certificate.go index ffe15208..2928e76f 100644 --- a/internal/datasources/certificate.go +++ b/internal/datasources/certificate.go @@ -82,7 +82,8 @@ func (c *Certificate) Read(ctx context.Context, req datasource.ReadRequest, resp url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/certificates", c.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := c.Client.Execute( + response, err := c.Client.ExecuteWithRetry( + ctx, cfg, nil, c.Token, diff --git a/internal/datasources/organization.go b/internal/datasources/organization.go index b87d7293..fab20b92 100644 --- a/internal/datasources/organization.go +++ b/internal/datasources/organization.go @@ -71,7 +71,8 @@ func (o *Organization) Read(ctx context.Context, req datasource.ReadRequest, res // Make request to get organization url := fmt.Sprintf("%s/v4/organizations/%s", o.HostURL, organizationId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := o.Client.Execute( + response, err := o.Client.ExecuteWithRetry( + ctx, cfg, nil, o.Token, diff --git a/internal/resources/acceptance_tests/allowlist_acceptance_test.go b/internal/resources/acceptance_tests/allowlist_acceptance_test.go index a39be3b8..5d72158c 100644 --- a/internal/resources/acceptance_tests/allowlist_acceptance_test.go +++ b/internal/resources/acceptance_tests/allowlist_acceptance_test.go @@ -1,6 +1,7 @@ package acceptance_tests import ( + "context" "fmt" "log" "net/http" @@ -364,7 +365,8 @@ func testAccDeleteAllowIP(clusterResourceReference, projectResourceReference, al authToken := os.Getenv("TF_VAR_auth_token") url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/allowedcidrs/%s", host, orgid, projectState["id"], clusterState["id"], allowListState["id"]) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err = data.Client.Execute( + _, err = data.Client.ExecuteWithRetry( + context.Background(), cfg, nil, authToken, diff --git a/internal/resources/acceptance_tests/appservice_acceptance_test.go b/internal/resources/acceptance_tests/appservice_acceptance_test.go index 7e161171..abaea523 100644 --- a/internal/resources/acceptance_tests/appservice_acceptance_test.go +++ b/internal/resources/acceptance_tests/appservice_acceptance_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + acctest "terraform-provider-capella/internal/testing" cfg "terraform-provider-capella/internal/testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -12,7 +13,7 @@ import ( func TestAppServiceResource(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { acctest.TestAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and Read testing diff --git a/internal/resources/acceptance_tests/cluster_acceptance_test.go b/internal/resources/acceptance_tests/cluster_acceptance_test.go index f94633f7..6f022d10 100644 --- a/internal/resources/acceptance_tests/cluster_acceptance_test.go +++ b/internal/resources/acceptance_tests/cluster_acceptance_test.go @@ -82,7 +82,7 @@ func TestAccClusterResourceWithOnlyReqFieldAWS(t *testing.T) { ResourceName: resourceReference, ImportStateIdFunc: generateClusterImportIdForResource(resourceReference), ImportState: true, - ImportStateVerify: false, + ImportStateVerify: true, }, // Update number of nodes, compute type, disk size and type, cluster name, support plan, time zone and description from empty string, // and Read testing @@ -97,23 +97,23 @@ func TestAccClusterResourceWithOnlyReqFieldAWS(t *testing.T) { resource.TestCheckResourceAttr(resourceReference, "cloud_provider.cidr", cidr), resource.TestCheckResourceAttr(resourceReference, "couchbase_server.version", "7.2"), resource.TestCheckResourceAttr(resourceReference, "configuration_type", "multiNode"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "8"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "32"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "51"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "gp3"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.iops", "3001"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "index"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.1", "query"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "4"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.ram", "16"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "8"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.ram", "32"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.storage", "51"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.type", "gp3"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.iops", "3001"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "3"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "1"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "data"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "index"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.1", "query"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "4"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "16"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "51"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "gp3"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.iops", "3001"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "3"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "1"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "data"), resource.TestCheckResourceAttr(resourceReference, "availability.type", "multi"), resource.TestCheckResourceAttr(resourceReference, "support.plan", "enterprise"), resource.TestCheckResourceAttr(resourceReference, "support.timezone", "IST"), @@ -130,23 +130,23 @@ func TestAccClusterResourceWithOnlyReqFieldAWS(t *testing.T) { resource.TestCheckResourceAttr(resourceReference, "cloud_provider.cidr", cidr), resource.TestCheckResourceAttr(resourceReference, "couchbase_server.version", "7.2"), resource.TestCheckResourceAttr(resourceReference, "configuration_type", "multiNode"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "8"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "32"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "51"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "gp3"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.iops", "3001"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "index"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.1", "query"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "4"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.ram", "16"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "8"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.ram", "32"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.storage", "51"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.type", "gp3"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.iops", "3001"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "3"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "1"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "data"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "index"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.1", "query"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "4"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "16"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "51"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "gp3"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.iops", "3001"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "3"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "1"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "data"), resource.TestCheckResourceAttr(resourceReference, "availability.type", "multi"), resource.TestCheckResourceAttr(resourceReference, "support.plan", "enterprise"), resource.TestCheckResourceAttr(resourceReference, "support.timezone", "IST"), @@ -424,7 +424,7 @@ func TestAccClusterResourceGCP(t *testing.T) { ResourceName: resourceReference, ImportStateIdFunc: generateClusterImportIdForResource(resourceReference), ImportState: true, - ImportStateVerify: false, + ImportStateVerify: true, }, { @@ -438,21 +438,21 @@ func TestAccClusterResourceGCP(t *testing.T) { resource.TestCheckResourceAttr(resourceReference, "cloud_provider.cidr", cidr), resource.TestCheckResourceAttr(resourceReference, "couchbase_server.version", "7.1"), resource.TestCheckResourceAttr(resourceReference, "configuration_type", "multiNode"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "8"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "16"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "51"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "pd-ssd"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "3"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "1"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "data"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "4"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "8"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.ram", "16"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.storage", "52"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.storage", "51"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.type", "pd-ssd"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "query"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.1", "index"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "3"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "1"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "data"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "4"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "16"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "52"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "pd-ssd"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "index"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.1", "query"), resource.TestCheckResourceAttr(resourceReference, "availability.type", "multi"), resource.TestCheckResourceAttr(resourceReference, "support.plan", "enterprise"), resource.TestCheckResourceAttr(resourceReference, "support.timezone", "ET"), @@ -634,23 +634,23 @@ func TestAccClusterResourceNotFound(t *testing.T) { resource.TestCheckResourceAttr(resourceReference, "cloud_provider.cidr", cidr), resource.TestCheckResourceAttr(resourceReference, "couchbase_server.version", "7.2"), resource.TestCheckResourceAttr(resourceReference, "configuration_type", "multiNode"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "8"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "32"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "51"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "gp3"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.iops", "3001"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "2"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "index"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.1", "query"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "4"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.ram", "16"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.cpu", "8"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.compute.ram", "32"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.storage", "51"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.type", "gp3"), resource.TestCheckResourceAttr(resourceReference, "service_groups.1.node.disk.iops", "3001"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "3"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "1"), - resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "data"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.num_of_nodes", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.#", "2"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.0", "index"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.1.services.1", "query"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.cpu", "4"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.compute.ram", "16"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.storage", "51"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.type", "gp3"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.node.disk.iops", "3001"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.num_of_nodes", "3"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.#", "1"), + resource.TestCheckResourceAttr(resourceReference, "service_groups.0.services.0", "data"), resource.TestCheckResourceAttr(resourceReference, "availability.type", "multi"), resource.TestCheckResourceAttr(resourceReference, "support.plan", "enterprise"), resource.TestCheckResourceAttr(resourceReference, "support.timezone", "IST"), @@ -1473,7 +1473,8 @@ func testAccDeleteClusterResource(resourceReference string) resource.TestCheckFu func deleteClusterFromServer(data *providerschema.Data, organizationId, projectId, clusterId string) error { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", data.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusAccepted} - _, err := data.Client.Execute( + _, err := data.Client.ExecuteWithRetry( + context.Background(), cfg, nil, data.Token, @@ -1566,7 +1567,8 @@ func testAccDeleteCluster(clusterResourceReference, projectResourceReference str authToken := os.Getenv("TF_VAR_auth_token") url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", host, orgid, projectState["id"], clusterState["id"]) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusAccepted} - _, err = data.Client.Execute( + _, err = data.Client.ExecuteWithRetry( + context.Background(), cfg, nil, authToken, @@ -1592,7 +1594,8 @@ func testAccDeleteCluster(clusterResourceReference, projectResourceReference str func retrieveClusterFromServer(data *providerschema.Data, organizationId, projectId, clusterId string) (*clusterapi.GetClusterResponse, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", data.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := data.Client.Execute( + response, err := data.Client.ExecuteWithRetry( + context.Background(), cfg, nil, data.Token, diff --git a/internal/resources/acceptance_tests/project_acceptance_test.go b/internal/resources/acceptance_tests/project_acceptance_test.go index 11ccb32f..7d4c3428 100644 --- a/internal/resources/acceptance_tests/project_acceptance_test.go +++ b/internal/resources/acceptance_tests/project_acceptance_test.go @@ -1,6 +1,7 @@ package acceptance_tests import ( + "context" "fmt" "log" "net/http" @@ -304,7 +305,8 @@ func testAccDeleteProject(projectResourceReference string) resource.TestCheckFun authToken := os.Getenv("TF_VAR_auth_token") url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s", host, orgid, projectState["id"]) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err = data.Client.Execute( + _, err = data.Client.ExecuteWithRetry( + context.Background(), cfg, nil, authToken, diff --git a/internal/resources/allowlist.go b/internal/resources/allowlist.go index ca7a6094..f0d7a0bf 100644 --- a/internal/resources/allowlist.go +++ b/internal/resources/allowlist.go @@ -82,7 +82,8 @@ func (r *AllowList) Create(ctx context.Context, req resource.CreateRequest, resp plan.ClusterId.ValueString(), ) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, allowListRequest, r.Token, @@ -106,9 +107,15 @@ func (r *AllowList) Create(ctx context.Context, req resource.CreateRequest, resp return } + diags = resp.State.Set(ctx, initializeAllowListWithPlanAndId(plan, allowListResponse.Id.String())) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + refreshedState, err := r.refreshAllowList(ctx, plan.OrganizationId.ValueString(), plan.ProjectId.ValueString(), plan.ClusterId.ValueString(), allowListResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error reading Capella AllowList", "Could not read Capella AllowList "+allowListResponse.Id.String()+": "+api.ParseError(err), ) @@ -221,7 +228,8 @@ func (r *AllowList) Delete(ctx context.Context, req resource.DeleteRequest, resp allowListId, ) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err = r.Client.Execute( + _, err = r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -264,7 +272,8 @@ func (r *AllowList) getAllowList(ctx context.Context, organizationId, projectId, allowListId, ) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -315,3 +324,12 @@ func (r *AllowList) refreshAllowList(ctx context.Context, organizationId, projec return &refreshedState, nil } + +func initializeAllowListWithPlanAndId(plan providerschema.AllowList, id string) providerschema.AllowList { + plan.Id = types.StringValue(id) + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) + if plan.Comment.IsNull() || plan.Comment.IsUnknown() { + plan.Comment = types.StringNull() + } + return plan +} diff --git a/internal/resources/apikey.go b/internal/resources/apikey.go index 513a7102..0e520a69 100644 --- a/internal/resources/apikey.go +++ b/internal/resources/apikey.go @@ -123,7 +123,8 @@ func (a *ApiKey) Create(ctx context.Context, req resource.CreateRequest, resp *r url := fmt.Sprintf("%s/v4/organizations/%s/apikeys", a.HostURL, organizationId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := a.Client.Execute( + response, err := a.Client.ExecuteWithRetry( + ctx, cfg, apiKeyRequest, a.Token, @@ -313,7 +314,8 @@ func (a *ApiKey) Update(ctx context.Context, req resource.UpdateRequest, resp *r url := fmt.Sprintf("%s/v4/organizations/%s/apikeys/%s/rotate", a.HostURL, organizationId, apiKeyId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusOK} - response, err := a.Client.Execute( + response, err := a.Client.ExecuteWithRetry( + ctx, cfg, rotateApiRequest, a.Token, @@ -411,7 +413,8 @@ func (a *ApiKey) Delete(ctx context.Context, req resource.DeleteRequest, resp *r // Delete existing api key url := fmt.Sprintf("%s/v4/organizations/%s/apikeys/%s", a.HostURL, organizationId, apiKeyId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err = a.Client.Execute( + _, err = a.Client.ExecuteWithRetry( + ctx, cfg, nil, a.Token, @@ -441,7 +444,8 @@ func (a *ApiKey) ImportState(ctx context.Context, req resource.ImportStateReques func (a *ApiKey) retrieveApiKey(ctx context.Context, organizationId, apiKeyId string) (*providerschema.ApiKey, error) { url := fmt.Sprintf("%s/v4/organizations/%s/apikeys/%s", a.HostURL, organizationId, apiKeyId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := a.Client.Execute( + response, err := a.Client.ExecuteWithRetry( + ctx, cfg, nil, a.Token, diff --git a/internal/resources/appservice.go b/internal/resources/appservice.go index 961448ab..a68b1e17 100644 --- a/internal/resources/appservice.go +++ b/internal/resources/appservice.go @@ -93,7 +93,8 @@ func (a *AppService) Create(ctx context.Context, req resource.CreateRequest, res url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/appservices", a.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := a.Client.Execute( + response, err := a.Client.ExecuteWithRetry( + ctx, cfg, appServiceRequest, a.Token, @@ -117,9 +118,15 @@ func (a *AppService) Create(ctx context.Context, req resource.CreateRequest, res return } + diags = resp.State.Set(ctx, initializePendingAppServiceWithPlanAndId(plan, createAppServiceResponse.Id.String())) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + err = a.checkAppServiceStatus(ctx, organizationId, projectId, clusterId, createAppServiceResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error creating app service", "Could not create app service, unexpected error: "+api.ParseError(err), ) @@ -127,7 +134,7 @@ func (a *AppService) Create(ctx context.Context, req resource.CreateRequest, res } refreshedState, err := a.refreshAppService(ctx, organizationId, projectId, clusterId, createAppServiceResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error creating app service", "Could not create app service, unexpected error: "+api.ParseError(err), ) @@ -249,7 +256,8 @@ func (a *AppService) Update(ctx context.Context, req resource.UpdateRequest, res url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/appservices/%s", a.HostURL, organizationId, projectId, clusterId, appServiceId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPut, SuccessStatus: http.StatusNoContent} - _, err = a.Client.Execute( + _, err = a.Client.ExecuteWithRetry( + ctx, cfg, appServiceRequest, a.Token, @@ -321,7 +329,8 @@ func (a *AppService) Delete(ctx context.Context, req resource.DeleteRequest, res url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/appservices/%s", a.HostURL, organizationId, projectId, clusterId, appServiceId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusAccepted} // Delete existing App Service - _, err = a.Client.Execute( + _, err = a.Client.ExecuteWithRetry( + ctx, cfg, nil, a.Token, @@ -423,7 +432,7 @@ func (a *AppService) validateCreateAppServiceRequest(plan providerschema.AppServ // refreshAppService is used to pass an existing AppService to the refreshed state func (a *AppService) refreshAppService(ctx context.Context, organizationId, projectId, clusterId, appServiceId string) (*providerschema.AppService, error) { - appServiceResponse, err := a.getAppService(organizationId, projectId, clusterId, appServiceId) + appServiceResponse, err := a.getAppService(ctx, organizationId, projectId, clusterId, appServiceId) if err != nil { return nil, fmt.Errorf("%s: %w", errors.ErrNotFound, err) } @@ -472,7 +481,7 @@ func (a *AppService) checkAppServiceStatus(ctx context.Context, organizationId, return fmt.Errorf(msg) case <-timer.C: - appServiceResp, err = a.getAppService(organizationId, projectId, clusterId, appServiceId) + appServiceResp, err = a.getAppService(ctx, organizationId, projectId, clusterId, appServiceId) switch err { case nil: if appservice.IsFinalState(appServiceResp.CurrentState) { @@ -490,11 +499,12 @@ func (a *AppService) checkAppServiceStatus(ctx context.Context, organizationId, // getAppService retrieves app service information from the specified organization, project and cluster // using the provided app service ID by open-api call -func (a *AppService) getAppService(organizationId, projectId, clusterId, appServiceId string) (*appservice.GetAppServiceResponse, error) { +func (a *AppService) getAppService(ctx context.Context, organizationId, projectId, clusterId, appServiceId string) (*appservice.GetAppServiceResponse, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/appservices/%s", a.HostURL, organizationId, projectId, clusterId, appServiceId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := a.Client.Execute( + response, err := a.Client.ExecuteWithRetry( + ctx, cfg, nil, a.Token, @@ -512,3 +522,23 @@ func (a *AppService) getAppService(organizationId, projectId, clusterId, appServ appServiceResp.Etag = response.Response.Header.Get("ETag") return &appServiceResp, nil } + +func initializePendingAppServiceWithPlanAndId(plan providerschema.AppService, id string) providerschema.AppService { + plan.Id = types.StringValue(id) + plan.CurrentState = types.StringValue("pending") + if plan.Description.IsNull() || plan.Description.IsUnknown() { + plan.Description = types.StringNull() + } + if plan.Nodes.IsNull() || plan.Nodes.IsUnknown() { + plan.Nodes = types.Int64Null() + } + if plan.CloudProvider.IsNull() || plan.CloudProvider.IsUnknown() { + plan.CloudProvider = types.StringNull() + } + if plan.Version.IsNull() || plan.Version.IsUnknown() { + plan.Version = types.StringNull() + } + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) + plan.Etag = types.StringNull() + return plan +} diff --git a/internal/resources/attributes.go b/internal/resources/attributes.go index d7732171..924a315a 100644 --- a/internal/resources/attributes.go +++ b/internal/resources/attributes.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" @@ -206,6 +207,33 @@ func stringListAttribute(fields ...string) *schema.ListAttribute { return &attribute } +// stringSetAttribute returns a Terraform string set schema attribute +// which is configured to be of type string. +func stringSetAttribute(fields ...string) *schema.SetAttribute { + attribute := schema.SetAttribute{ + ElementType: types.StringType, + } + + for _, field := range fields { + switch field { + case required: + attribute.Required = true + case optional: + attribute.Optional = true + case computed: + attribute.Computed = true + case sensitive: + attribute.Sensitive = true + case requiresReplace: + var planModifiers = []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + } + attribute.PlanModifiers = planModifiers + } + } + return &attribute +} + // computedAuditAttribute retuns a SingleNestedAttribute to // represent couchbase audit data using terraform schema types. func computedAuditAttribute() *schema.SingleNestedAttribute { diff --git a/internal/resources/backup.go b/internal/resources/backup.go index ca92eaf8..eb2a45be 100644 --- a/internal/resources/backup.go +++ b/internal/resources/backup.go @@ -71,7 +71,7 @@ func (b *Backup) Create(ctx context.Context, req resource.CreateRequest, resp *r var clusterId = plan.ClusterId.ValueString() var bucketId = plan.BucketId.ValueString() - latestBackup, err := b.getLatestBackup(organizationId, projectId, clusterId, bucketId) + latestBackup, err := b.getLatestBackup(ctx, organizationId, projectId, clusterId, bucketId) if err != nil { resp.Diagnostics.AddError( "Error getting latest bucket backup in a cluster", @@ -87,7 +87,8 @@ func (b *Backup) Create(ctx context.Context, req resource.CreateRequest, resp *r url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backups", b.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusAccepted} - _, err = b.Client.Execute( + _, err = b.Client.ExecuteWithRetry( + ctx, cfg, BackupRequest, b.Token, @@ -268,7 +269,8 @@ func (b *Backup) Update(ctx context.Context, req resource.UpdateRequest, resp *r url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/backups/%s/restore", b.HostURL, organizationId, projectId, clusterId, backupId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusAccepted} - _, err = b.Client.Execute( + _, err = b.Client.ExecuteWithRetry( + ctx, cfg, restoreRequest, b.Token, @@ -327,7 +329,8 @@ func (b *Backup) Delete(ctx context.Context, req resource.DeleteRequest, resp *r // Delete existing Backup url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/backups/%s", b.HostURL, organizationId, projectId, clusterId, backupId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusAccepted} - _, err = b.Client.Execute( + _, err = b.Client.ExecuteWithRetry( + ctx, cfg, nil, b.Token, @@ -418,7 +421,7 @@ func (b *Backup) checkLatestBackupStatus(ctx context.Context, organizationId, pr return nil, fmt.Errorf(msg) case <-timer.C: - backupResp, err = b.getLatestBackup(organizationId, projectId, clusterId, bucketId) + backupResp, err = b.getLatestBackup(ctx, organizationId, projectId, clusterId, bucketId) switch err { case nil: // If there is no existing backup for a bucket, check for a new backup record to be created. @@ -443,7 +446,8 @@ func (b *Backup) checkLatestBackupStatus(ctx context.Context, organizationId, pr func (b *Backup) retrieveBackup(ctx context.Context, organizationId, projectId, clusterId, bucketId, backupId string) (*providerschema.Backup, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/backups/%s", b.HostURL, organizationId, projectId, clusterId, backupId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := b.Client.Execute( + response, err := b.Client.ExecuteWithRetry( + ctx, cfg, nil, b.Token, @@ -477,10 +481,11 @@ func (b *Backup) retrieveBackup(ctx context.Context, organizationId, projectId, // getLatestBackup retrieves the latest backup information for a specified bucket in a cluster // from the specified organization, project and cluster using the provided bucket ID by open-api call -func (b *Backup) getLatestBackup(organizationId, projectId, clusterId, bucketId string) (*backupapi.GetBackupResponse, error) { +func (b *Backup) getLatestBackup(ctx context.Context, organizationId, projectId, clusterId, bucketId string) (*backupapi.GetBackupResponse, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/backups", b.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := b.Client.Execute( + response, err := b.Client.ExecuteWithRetry( + ctx, cfg, nil, b.Token, diff --git a/internal/resources/backup_schedule.go b/internal/resources/backup_schedule.go index 2c6f70dd..42a52d61 100644 --- a/internal/resources/backup_schedule.go +++ b/internal/resources/backup_schedule.go @@ -85,7 +85,8 @@ func (b *BackupSchedule) Create(ctx context.Context, req resource.CreateRequest, } url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backup/schedules", b.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusAccepted} - _, err = b.Client.Execute( + _, err = b.Client.ExecuteWithRetry( + ctx, cfg, BackupScheduleRequest, b.Token, @@ -99,9 +100,15 @@ func (b *BackupSchedule) Create(ctx context.Context, req resource.CreateRequest, return } + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + refreshedState, err := b.retrieveBackupSchedule(ctx, organizationId, projectId, clusterId, bucketId, weeklySchedule.DayOfWeek.ValueString()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error Reading Capella Backup Schedule", "Could not read Capella Backup Schedule for the bucket: %s "+bucketId+": "+api.ParseError(err), ) @@ -216,7 +223,8 @@ func (b *BackupSchedule) Update(ctx context.Context, req resource.UpdateRequest, url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backup/schedules", b.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPut, SuccessStatus: http.StatusNoContent} - _, err = b.Client.Execute( + _, err = b.Client.ExecuteWithRetry( + ctx, cfg, BackupScheduleRequest, b.Token, @@ -282,7 +290,8 @@ func (b *BackupSchedule) Delete(ctx context.Context, req resource.DeleteRequest, url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backup/schedules", b.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusAccepted} // Delete existing backup schedule - _, err = b.Client.Execute( + _, err = b.Client.ExecuteWithRetry( + ctx, cfg, nil, b.Token, @@ -352,7 +361,8 @@ func (a *BackupSchedule) validateCreateBackupScheduleRequest(plan providerschema func (b *BackupSchedule) retrieveBackupSchedule(ctx context.Context, organizationId, projectId, clusterId, bucketId, planDayOfWeek string) (*providerschema.BackupSchedule, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s/backup/schedules", b.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := b.Client.Execute( + response, err := b.Client.ExecuteWithRetry( + ctx, cfg, nil, b.Token, diff --git a/internal/resources/bucket.go b/internal/resources/bucket.go index 2ac53efd..2c5dd836 100644 --- a/internal/resources/bucket.go +++ b/internal/resources/bucket.go @@ -23,6 +23,14 @@ var ( _ resource.ResourceWithImportState = &Bucket{} ) +const errorMessageAfterBucketCreation = "Bucket creation is successful, but encountered an error while checking the current" + + " state of the bucket. Please run `terraform plan` after 1-2 minutes to know the" + + " current bucket state. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileBucketCreation = "There is an error during bucket creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // Bucket is the bucket resource implementation. type Bucket struct { *providerschema.Data @@ -123,7 +131,8 @@ func (c *Bucket) Create(ctx context.Context, req resource.CreateRequest, resp *r url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets", c.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := c.Client.Execute( + response, err := c.Client.ExecuteWithRetry( + ctx, cfg, BucketRequest, c.Token, @@ -132,7 +141,7 @@ func (c *Bucket) Create(ctx context.Context, req resource.CreateRequest, resp *r if err != nil { resp.Diagnostics.AddError( "Error creating bucket", - "Could not create bucket, unexpected error: "+api.ParseError(err), + errorMessageWhileBucketCreation+api.ParseError(err), ) return } @@ -142,16 +151,22 @@ func (c *Bucket) Create(ctx context.Context, req resource.CreateRequest, resp *r if err != nil { resp.Diagnostics.AddError( "Error creating bucket", - "Could not create bucket, error during unmarshalling:"+err.Error(), + errorMessageWhileBucketCreation+"error during unmarshalling: "+err.Error(), ) return } + diags = resp.State.Set(ctx, initializeBucketWithPlanAndId(plan, BucketResponse.Id)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + refreshedState, err := c.retrieveBucket(ctx, organizationId, projectId, clusterId, BucketResponse.Id) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error creating bucket", - "Could not create bucket, unexpected error:"+api.ParseError(err), + errorMessageAfterBucketCreation+api.ParseError(err), ) return } @@ -278,7 +293,8 @@ func (r *Bucket) Delete(ctx context.Context, req resource.DeleteRequest, resp *r url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", r.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err := r.Client.Execute( + _, err := r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -309,7 +325,8 @@ func (c *Bucket) ImportState(ctx context.Context, req resource.ImportStateReques func (c *Bucket) retrieveBucket(ctx context.Context, organizationId, projectId, clusterId, bucketId string) (*providerschema.OneBucket, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", c.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := c.Client.Execute( + response, err := c.Client.ExecuteWithRetry( + ctx, cfg, nil, c.Token, @@ -386,7 +403,8 @@ func (c *Bucket) Update(ctx context.Context, req resource.UpdateRequest, resp *r url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", c.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPut, SuccessStatus: http.StatusNoContent} - _, err = c.Client.Execute( + _, err = c.Client.ExecuteWithRetry( + ctx, cfg, bucketUpdateRequest, c.Token, @@ -412,6 +430,7 @@ func (c *Bucket) Update(ctx context.Context, req resource.UpdateRequest, resp *r "Error updating bucket", "Could not update Capella bucket with ID "+bucketId+": "+api.ParseError(err), ) + return } // Set state to fully populated data @@ -421,3 +440,15 @@ func (c *Bucket) Update(ctx context.Context, req resource.UpdateRequest, resp *r return } } + +func initializeBucketWithPlanAndId(plan providerschema.Bucket, id string) providerschema.Bucket { + plan.Id = types.StringValue(id) + if plan.StorageBackend.IsNull() || plan.StorageBackend.IsUnknown() { + plan.StorageBackend = types.StringNull() + } + if plan.EvictionPolicy.IsNull() || plan.EvictionPolicy.IsUnknown() { + plan.EvictionPolicy = types.StringNull() + } + plan.Stats = types.ObjectNull(providerschema.Stats{}.AttributeTypes()) + return plan +} diff --git a/internal/resources/cluster.go b/internal/resources/cluster.go index 3c12c8a7..3540ae84 100644 --- a/internal/resources/cluster.go +++ b/internal/resources/cluster.go @@ -28,7 +28,7 @@ var ( _ resource.ResourceWithImportState = &Cluster{} ) -const errorMessageAfterClusterCreationInitiation = "Cluster creation is initiated, but encounters an error while checking the current" + +const errorMessageAfterClusterCreationInitiation = "Cluster creation is initiated, but encountered an error while checking the current" + " state of the cluster.Please run `terraform plan` after 4-5 minutes to know the" + " current status of the cluster. Additionally, run `terraform apply --refresh-only` to update" + " the status from remote, unexpected error: " @@ -738,12 +738,26 @@ func getCouchbaseServer(ctx context.Context, config tfsdk.Config, diags *diag.Di func initializePendingClusterWithPlanAndId(plan providerschema.Cluster, id string) providerschema.Cluster { plan.Id = types.StringValue(id) plan.CurrentState = types.StringValue("pending") + if plan.Description.IsNull() || plan.Description.IsUnknown() { + plan.Description = types.StringNull() + } + if plan.ConfigurationType.IsNull() || plan.ConfigurationType.IsUnknown() { + plan.ConfigurationType = types.StringNull() + } if plan.CouchbaseServer.IsNull() || plan.CouchbaseServer.IsUnknown() { plan.CouchbaseServer = types.ObjectNull(providerschema.CouchbaseServer{}.AttributeTypes()) } - plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) plan.AppServiceId = types.StringNull() - plan.ConfigurationType = types.StringNull() + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) plan.Etag = types.StringNull() + + for _, serviceGroup := range plan.ServiceGroups { + if serviceGroup.Node != nil && (serviceGroup.Node.Disk.Storage.IsNull() || serviceGroup.Node.Disk.Storage.IsUnknown()) { + serviceGroup.Node.Disk.Storage = types.Int64Null() + } + if serviceGroup.Node != nil && (serviceGroup.Node.Disk.IOPS.IsNull() || serviceGroup.Node.Disk.IOPS.IsUnknown()) { + serviceGroup.Node.Disk.IOPS = types.Int64Null() + } + } return plan } diff --git a/internal/resources/cluster_schema.go b/internal/resources/cluster_schema.go index d39357b5..40ca178a 100644 --- a/internal/resources/cluster_schema.go +++ b/internal/resources/cluster_schema.go @@ -35,7 +35,7 @@ func ClusterSchema() schema.Schema { "version": stringAttribute(optional, computed), }, }, - "service_groups": schema.ListNestedAttribute{ + "service_groups": schema.SetNestedAttribute{ Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ @@ -63,7 +63,7 @@ func ClusterSchema() schema.Schema { }, }, "num_of_nodes": int64Attribute(required), - "services": stringListAttribute(required), + "services": stringSetAttribute(required), }, }, }, @@ -81,7 +81,7 @@ func ClusterSchema() schema.Schema { }, }, "current_state": stringAttribute(computed), - "app_service_id": stringAttribute(optional, computed), + "app_service_id": stringAttribute(computed), "audit": computedAuditAttribute(), // if_match is only required during update call "if_match": stringAttribute(optional), diff --git a/internal/resources/database_credential.go b/internal/resources/database_credential.go index 482ca30b..f6f3e972 100644 --- a/internal/resources/database_credential.go +++ b/internal/resources/database_credential.go @@ -4,16 +4,14 @@ import ( "context" "encoding/json" "fmt" - "net/http" - - "terraform-provider-capella/internal/api" - "terraform-provider-capella/internal/errors" - providerschema "terraform-provider-capella/internal/schema" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "net/http" + "terraform-provider-capella/internal/api" + "terraform-provider-capella/internal/errors" + providerschema "terraform-provider-capella/internal/schema" ) // Ensure the implementation satisfies the expected interfaces. @@ -23,6 +21,14 @@ var ( _ resource.ResourceWithImportState = &DatabaseCredential{} ) +const errorMessageAfterDatabaseCredentialCreation = "Bucket creation is successful, but encountered an error while checking the current" + + " state of the bucket. Please run `terraform plan` after 1-2 minutes to know the" + + " current bucket state. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileDatabaseCredentialCreation = "There is an error during bucket creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // DatabaseCredential is the database credential resource implementation. type DatabaseCredential struct { *providerschema.Data @@ -55,7 +61,6 @@ func (r *DatabaseCredential) Configure(_ context.Context, req resource.Configure "Unexpected Resource Configure Type", fmt.Sprintf("Expected *ProviderSourceData, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) - return } @@ -111,7 +116,8 @@ func (r *DatabaseCredential) Create(ctx context.Context, req resource.CreateRequ url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/users", r.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, dbCredRequest, r.Token, @@ -120,7 +126,7 @@ func (r *DatabaseCredential) Create(ctx context.Context, req resource.CreateRequ if err != nil { resp.Diagnostics.AddError( "Error creating database credential", - "Could not create database credential, unexpected error: "+api.ParseError(err), + errorMessageWhileDatabaseCredentialCreation+api.ParseError(err), ) return } @@ -130,16 +136,22 @@ func (r *DatabaseCredential) Create(ctx context.Context, req resource.CreateRequ if err != nil { resp.Diagnostics.AddError( "Error creating database credential", - "Could not create database credential, unexpected error: "+err.Error(), + errorMessageWhileDatabaseCredentialCreation+"error during unmarshalling: "+err.Error(), ) return } + diags = resp.State.Set(ctx, initializeDataBaseCredentialWithPlanPasswordAndId(plan, dbResponse.Password, dbResponse.Id.String())) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + refreshedState, err := r.retrieveDatabaseCredential(ctx, organizationId, projectId, clusterId, dbResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error Reading Capella Database Credentials", - "Could not read Capella database credential with ID "+dbResponse.Id.String()+": "+api.ParseError(err), + errorMessageAfterDatabaseCredentialCreation+api.ParseError(err), ) return } @@ -261,7 +273,8 @@ func (r *DatabaseCredential) Update(ctx context.Context, req resource.UpdateRequ url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/users/%s", r.HostURL, organizationId, projectId, clusterId, dbId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPut, SuccessStatus: http.StatusNoContent} - _, err = r.Client.Execute( + _, err = r.Client.ExecuteWithRetry( + ctx, cfg, dbCredRequest, r.Token, @@ -280,13 +293,13 @@ func (r *DatabaseCredential) Update(ctx context.Context, req resource.UpdateRequ ) return } - currentState, err := r.retrieveDatabaseCredential(ctx, organizationId, projectId, clusterId, dbId) if err != nil { resp.Diagnostics.AddError( "Error updating database credential", "Could not update an existing database credential, unexpected error: "+api.ParseError(err), ) + return } // this will ensure that the state file stores the new updated password, if password is not to be updated, it will retain the older one. @@ -335,7 +348,8 @@ func (r *DatabaseCredential) Delete(ctx context.Context, req resource.DeleteRequ url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/users/%s", r.HostURL, organizationId, projectId, clusterId, dbId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err = r.Client.Execute( + _, err = r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -372,7 +386,8 @@ func (r *DatabaseCredential) ImportState(ctx context.Context, req resource.Impor func (r *DatabaseCredential) retrieveDatabaseCredential(ctx context.Context, organizationId, projectId, clusterId, dbId string) (*providerschema.OneDatabaseCredential, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/users/%s", r.HostURL, organizationId, projectId, clusterId, dbId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -491,3 +506,12 @@ func mapAccess(plan providerschema.DatabaseCredential) []providerschema.Access { return access } + +func initializeDataBaseCredentialWithPlanPasswordAndId(plan providerschema.DatabaseCredential, password, id string) providerschema.DatabaseCredential { + plan.Id = types.StringValue(id) + if password != "" { + plan.Password = types.StringValue(password) + } + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) + return plan +} diff --git a/internal/resources/database_credential_schema.go b/internal/resources/database_credential_schema.go index d785c704..e9f35e55 100644 --- a/internal/resources/database_credential_schema.go +++ b/internal/resources/database_credential_schema.go @@ -24,25 +24,25 @@ func DatabaseCredentialSchema() schema.Schema { "project_id": stringAttribute(required), "cluster_id": stringAttribute(required), "audit": computedAuditAttribute(), - "access": schema.ListNestedAttribute{ - Optional: true, + "access": schema.SetNestedAttribute{ + Required: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "privileges": stringListAttribute(required), + "privileges": stringSetAttribute(required), "resources": schema.SingleNestedAttribute{ Optional: true, Attributes: map[string]schema.Attribute{ - "buckets": schema.ListNestedAttribute{ + "buckets": schema.SetNestedAttribute{ Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "name": stringAttribute(required), - "scopes": schema.ListNestedAttribute{ + "scopes": schema.SetNestedAttribute{ Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "name": stringAttribute(required), - "collections": stringListAttribute(optional), + "collections": stringSetAttribute(optional), }, }, }, diff --git a/internal/resources/project.go b/internal/resources/project.go index 90322e6d..3b4c1568 100644 --- a/internal/resources/project.go +++ b/internal/resources/project.go @@ -87,7 +87,8 @@ func (r *Project) Create(ctx context.Context, req resource.CreateRequest, resp * url := fmt.Sprintf("%s/v4/organizations/%s/projects", r.HostURL, organizationId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, projectRequest, r.Token, @@ -216,7 +217,8 @@ func (r *Project) Update(ctx context.Context, req resource.UpdateRequest, resp * url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s", r.HostURL, organizationId, projectId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPut, SuccessStatus: http.StatusNoContent} - _, err = r.Client.Execute( + _, err = r.Client.ExecuteWithRetry( + ctx, cfg, projectRequest, r.Token, @@ -288,7 +290,8 @@ func (r *Project) Delete(ctx context.Context, req resource.DeleteRequest, resp * url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s", r.HostURL, organizationId, projectId) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err = r.Client.Execute( + _, err = r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -323,7 +326,8 @@ func (r *Project) ImportState(ctx context.Context, req resource.ImportStateReque func (r *Project) retrieveProject(ctx context.Context, organizationId, projectId string) (*providerschema.OneProject, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s", r.HostURL, organizationId, projectId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, diff --git a/internal/resources/user.go b/internal/resources/user.go index cb451372..a038c3d5 100644 --- a/internal/resources/user.go +++ b/internal/resources/user.go @@ -99,7 +99,8 @@ func (r *User) Create(ctx context.Context, req resource.CreateRequest, resp *res // Execute request url := fmt.Sprintf("%s/v4/organizations/%s/users", r.HostURL, organizationId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, createUserRequest, r.Token, @@ -232,7 +233,7 @@ func (r *User) Update(ctx context.Context, req resource.UpdateRequest, resp *res patch := constructPatch(state, plan) - err = r.updateUser(organizationId, userId, patch) + err = r.updateUser(ctx, organizationId, userId, patch) if err != nil { resourceNotFound, errString := api.CheckResourceNotFoundError(err) resp.Diagnostics.AddError( @@ -405,11 +406,12 @@ func compare(existing, proposed []basetypes.StringValue) ([]basetypes.StringValu } // updateUser is used to execute the patch request to update a user. -func (r *User) updateUser(organizationId, userId string, patch []api.PatchEntry) error { +func (r *User) updateUser(ctx context.Context, organizationId, userId string, patch []api.PatchEntry) error { // Update existing user url := fmt.Sprintf("%s/v4/organizations/%s/users/%s", r.HostURL, organizationId, userId) cfg := api.EndpointCfg{Url: url, Method: http.MethodPatch, SuccessStatus: http.StatusOK} - _, err := r.Client.Execute( + _, err := r.Client.ExecuteWithRetry( + ctx, cfg, patch, r.Token, @@ -456,7 +458,8 @@ func (r *User) Delete(ctx context.Context, req resource.DeleteRequest, resp *res userId, ) cfg := api.EndpointCfg{Url: url, Method: http.MethodDelete, SuccessStatus: http.StatusNoContent} - _, err = r.Client.Execute( + _, err = r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -487,7 +490,8 @@ func (r *User) getUser(ctx context.Context, organizationId, userId string) (*api ) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} - response, err := r.Client.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, diff --git a/internal/resources/user_schema.go b/internal/resources/user_schema.go index 87d660c9..5b7c973c 100644 --- a/internal/resources/user_schema.go +++ b/internal/resources/user_schema.go @@ -19,13 +19,13 @@ func UserSchema() schema.Schema { "time_zone": stringAttribute(computed), "enable_notifications": boolAttribute(computed), "expires_at": stringAttribute(computed), - "resources": schema.ListNestedAttribute{ + "resources": schema.SetNestedAttribute{ Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "type": stringAttribute(optional, computed), "id": stringAttribute(required), - "roles": stringListAttribute(required), + "roles": stringSetAttribute(required), }, }, }, diff --git a/internal/schema/bucket.go b/internal/schema/bucket.go index 59c277ee..180a2453 100644 --- a/internal/schema/bucket.go +++ b/internal/schema/bucket.go @@ -4,6 +4,7 @@ import ( "fmt" "terraform-provider-capella/internal/errors" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -130,6 +131,15 @@ type Stats struct { MemoryUsedInMiB types.Int64 `tfsdk:"memory_used_in_mib"` } +func (s Stats) AttributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "item_count": types.Int64Type, + "ops_per_second": types.Int64Type, + "disk_used_in_mib": types.Int64Type, + "memory_used_in_mib": types.Int64Type, + } +} + // Buckets defines attributes for the LIST buckets response received from V4 Capella Public API. type Buckets struct { // OrganizationId The organizationId of the capella. From f52f76990730dfd26486ffe710e6db4e9f38acaf Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Tue, 5 Dec 2023 21:16:20 +0530 Subject: [PATCH 3/7] error handling added --- internal/api/client.go | 6 + .../apikey_acceptance_test.go | 387 ++++++++++++++++++ internal/resources/apikey.go | 119 +++--- internal/resources/apikey_schema.go | 36 +- internal/resources/appservice.go | 16 +- internal/resources/attributes.go | 8 + internal/resources/backup.go | 27 +- internal/resources/cluster.go | 4 +- internal/resources/project.go | 2 +- internal/resources/user_schema.go | 4 +- internal/schema/apikey.go | 103 ++--- 11 files changed, 560 insertions(+), 152 deletions(-) create mode 100644 internal/resources/acceptance_tests/apikey_acceptance_test.go diff --git a/internal/api/client.go b/internal/api/client.go index 7d004ed5..10e6018e 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -152,6 +152,12 @@ func (c *Client) ExecuteWithRetry( "unexpected code: %d, expected: %d, body: %s", apiRes.StatusCode, endpointCfg.SuccessStatus, responseBody) } + if apiError.Code == 0 { + return nil, fmt.Errorf( + "unexpected code: %d, expected: %d, body: %s", + apiRes.StatusCode, endpointCfg.SuccessStatus, responseBody) + + } return nil, &apiError } diff --git a/internal/resources/acceptance_tests/apikey_acceptance_test.go b/internal/resources/acceptance_tests/apikey_acceptance_test.go new file mode 100644 index 00000000..fdd0f74d --- /dev/null +++ b/internal/resources/acceptance_tests/apikey_acceptance_test.go @@ -0,0 +1,387 @@ +package acceptance_tests_test + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + "terraform-provider-capella/internal/api" + providerschema "terraform-provider-capella/internal/schema" + acctest "terraform-provider-capella/internal/testing" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccApiKeyResource(t *testing.T) { + resourceName := "acc_apikey_" + acctest.GenerateRandomResourceName() + resourceReference := "capella_apikey." + resourceName + projectResourceName := "acc_project_" + acctest.GenerateRandomResourceName() + projectResourceReference := "capella_project." + projectResourceName + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccApiKeyResourceConfig(acctest.Cfg, resourceName, projectResourceName, projectResourceReference), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsApiKeyResource(resourceReference), + resource.TestCheckResourceAttr(resourceReference, "name", resourceName), + resource.TestCheckResourceAttr(resourceReference, "description", "description"), + resource.TestCheckResourceAttr(resourceReference, "expiry", "150"), + resource.TestCheckResourceAttr(resourceReference, "allowed_cidrs.0", "10.1.42.0/23"), + resource.TestCheckResourceAttr(resourceReference, "allowed_cidrs.1", "10.1.42.1/23"), + resource.TestCheckResourceAttr(resourceReference, "organization_roles.0", "organizationMember"), + resource.TestCheckResourceAttr(resourceReference, "resources.#", "1"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.roles.0", "projectDataReader"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.roles.1", "projectManager"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.type", "project"), + ), + }, + //// ImportState testing + { + ResourceName: resourceReference, + ImportStateIdFunc: generateApiKeyImportIdForResource(resourceReference), + ImportState: true, + ImportStateVerify: false, + }, + }, + }) +} + +func TestAccApiKeyResourceWithMultipleResources(t *testing.T) { + resourceName := "acc_apikey_" + acctest.GenerateRandomResourceName() + resourceReference := "capella_apikey." + resourceName + projectResourceName := "acc_project_" + acctest.GenerateRandomResourceName() + projectResourceReference := "capella_project." + projectResourceName + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccApiKeyResourceConfigWithMultipleResources(acctest.Cfg, resourceName, projectResourceName, projectResourceReference), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsApiKeyResource(resourceReference), + resource.TestCheckResourceAttr(resourceReference, "name", resourceName), + resource.TestCheckResourceAttr(resourceReference, "description", ""), + resource.TestCheckResourceAttr(resourceReference, "expiry", "180"), + resource.TestCheckResourceAttr(resourceReference, "allowed_cidrs.0", "10.1.42.0/23"), + resource.TestCheckResourceAttr(resourceReference, "allowed_cidrs.1", "10.1.42.1/23"), + resource.TestCheckResourceAttr(resourceReference, "organization_roles.0", "organizationMember"), + resource.TestCheckResourceAttr(resourceReference, "resources.#", "2"), + resource.TestCheckResourceAttr(resourceReference, "resources.1.roles.0", "projectDataReader"), + resource.TestCheckResourceAttr(resourceReference, "resources.1.roles.1", "projectManager"), + resource.TestCheckResourceAttr(resourceReference, "resources.1.type", "project"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.roles.0", "projectDataReader"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.type", "project"), + ), + }, + //// ImportState testing + { + ResourceName: resourceReference, + ImportStateIdFunc: generateApiKeyImportIdForResource(resourceReference), + ImportState: true, + ImportStateVerify: false, + }, + }, + }) +} + +func TestAccApiKeyResourceWithOnlyReqField(t *testing.T) { + resourceName := "acc_apikey_" + acctest.GenerateRandomResourceName() + resourceReference := "capella_apikey." + resourceName + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccApiKeyResourceConfigWithOnlyReqField(acctest.Cfg, resourceName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsApiKeyResource(resourceReference), + resource.TestCheckResourceAttr(resourceReference, "name", resourceName), + resource.TestCheckResourceAttr(resourceReference, "description", ""), + resource.TestCheckResourceAttr(resourceReference, "expiry", "180"), + resource.TestCheckResourceAttr(resourceReference, "allowed_cidrs.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr(resourceReference, "organization_roles.0", "organizationMember"), + resource.TestCheckResourceAttr(resourceReference, "organization_roles.1", "organizationOwner"), + ), + }, + //// ImportState testing + { + ResourceName: resourceReference, + ImportStateIdFunc: generateApiKeyImportIdForResource(resourceReference), + ImportState: true, + ImportStateVerify: false, + }, + // Rotate testing + { + Config: testAccApiKeyResourceConfigWithOnlyReqFieldRotate(acctest.Cfg, resourceName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsApiKeyResource(resourceReference), + resource.TestCheckResourceAttr(resourceReference, "name", resourceName), + resource.TestCheckResourceAttr(resourceReference, "description", ""), + resource.TestCheckResourceAttr(resourceReference, "expiry", "180"), + resource.TestCheckResourceAttr(resourceReference, "allowed_cidrs.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr(resourceReference, "organization_roles.0", "organizationMember"), + resource.TestCheckResourceAttr(resourceReference, "organization_roles.1", "organizationOwner"), + resource.TestCheckResourceAttr(resourceReference, "rotate", "1"), + resource.TestCheckResourceAttr(resourceReference, "secret", "abc"), + ), + }, + }, + }) +} + +func TestAccApiKeyResourceForOrgOwner(t *testing.T) { + resourceName := "acc_apikey_" + acctest.GenerateRandomResourceName() + resourceReference := "capella_apikey." + resourceName + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccApiKeyResourceConfigForOrgOwner(acctest.Cfg, resourceName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccExistsApiKeyResource(resourceReference), + resource.TestCheckResourceAttr(resourceReference, "name", resourceName), + resource.TestCheckResourceAttr(resourceReference, "description", ""), + resource.TestCheckResourceAttr(resourceReference, "expiry", "180"), + resource.TestCheckResourceAttr(resourceReference, "allowed_cidrs.0", "0.0.0.0/0"), + resource.TestCheckResourceAttr(resourceReference, "organization_roles.0", "organizationMember"), + resource.TestCheckResourceAttr(resourceReference, "resources.#", "1"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.roles.0", "projectDataReader"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.roles.1", "projectManager"), + resource.TestCheckResourceAttr(resourceReference, "resources.0.type", "project"), + ), + }, + //// ImportState testing + { + ResourceName: resourceReference, + ImportStateIdFunc: generateApiKeyImportIdForResource(resourceReference), + ImportState: true, + ImportStateVerify: false, + }, + }, + }) +} + +func TestAccApiKeyResourceInvalidScenarioRotateShouldNotPassedWhileCreate(t *testing.T) { + resourceName := "acc_apikey_" + acctest.GenerateRandomResourceName() + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccApiKeyResourceConfigRotateSet(acctest.Cfg, resourceName), + ExpectError: regexp.MustCompile("rotate value should not be set"), + }, + }, + }) +} + +func testAccApiKeyResourceConfig(cfg, resourceName, projectResourceName, projectResourceReference string) string { + return fmt.Sprintf(` +%[1]s + +resource "capella_apikey" "%[2]s" { + organization_id = var.organization_id + name = "%[2]s" + description = "description" + expiry = 150 + organization_roles = ["organizationMember"] + allowed_cidrs = ["10.1.42.1/23", "10.1.42.0/23"] + resources = [ + { + id = var.project_id + roles = ["projectManager", "projectDataReader"] + type = "project" + } + ] +} +`, cfg, resourceName) +} + +func testAccApiKeyResourceConfigWithMultipleResources(cfg, resourceName, projectResourceName, projectResourceReference string) string { + return fmt.Sprintf(` +%[1]s + +resource "capella_project" "%[3]s" { + organization_id = var.organization_id + name = "acc_test_project_name" + description = "description" +} + +resource "capella_apikey" "%[2]s" { + organization_id = var.organization_id + name = "%[2]s" + organization_roles = ["organizationMember"] + allowed_cidrs = ["10.1.42.1/23", "10.1.42.0/23"] + resources = [ + { + id = %[4]s.id + roles = ["projectManager", "projectDataReader"] + type = "project" + }, + { + id = var.project_id + roles = ["projectDataReader"] + type = "project" + } + ] +} +`, cfg, resourceName, projectResourceName, projectResourceReference) +} + +func testAccApiKeyResourceConfigWithOnlyReqField(cfg, resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "capella_apikey" "%[2]s" { + organization_id = var.organization_id + name = "%[2]s" + organization_roles = ["organizationOwner", "organizationMember"] +} +`, cfg, resourceName) +} + +func testAccApiKeyResourceConfigForOrgOwner(cfg, resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "capella_apikey" "%[2]s" { + organization_id = var.organization_id + name = "%[2]s" + organization_roles = [ "organizationMember"] + resources = [ + { + id = "1c50d827-cb90-49ca-a47e-dff850f53557" + roles = [ + "projectManager", + "projectDataReader" + ] + } + ] +} +`, cfg, resourceName) +} + +func testAccApiKeyResourceConfigRotateSet(cfg, resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "capella_apikey" "%[2]s" { + organization_id = var.organization_id + name = "%[2]s" + organization_roles = [ "organizationMember"] + resources = [ + { + id = "1c50d827-cb90-49ca-a47e-dff850f53557" + roles = [ + "projectManager", + "projectDataReader" + ] + } + ] + rotate = 1 +} +`, cfg, resourceName) +} + +func testAccApiKeyResourceConfigWithOnlyReqFieldRotate(cfg, resourceName string) string { + return fmt.Sprintf(` +%[1]s + +resource "capella_apikey" "%[2]s" { + organization_id = var.organization_id + name = "%[2]s" + organization_roles = ["organizationOwner", "organizationMember"] + rotate = 1 + secret = "abc" +} +`, cfg, resourceName) +} + +func testAccApiKeyResourceConfigWithoutResource(cfg, resourceName, projectResourceName, projectResourceReference string) string { + return fmt.Sprintf(` +%[1]s + +resource "capella_project" "%[3]s" { + organization_id = var.organization_id + name = "acc_test_project_name" + description = "description" +} + +resource "capella_apikey" "%[2]s" { + organization_id = var.organization_id + name = "%[2]s" + organization_roles = ["organizationOwner", "organizationMember"] + allowed_cidrs = ["10.1.42.0/23", "10.1.42.0/23"] +} +`, cfg, resourceName, projectResourceName, projectResourceReference) +} + +func testAccExistsApiKeyResource(resourceReference string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // retrieve the resource by name from state + + var rawState map[string]string + for _, m := range s.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[resourceReference]; ok { + rawState = v.Primary.Attributes + } + } + } + fmt.Printf("raw state %s", rawState) + data, err := acctest.TestClient() + if err != nil { + return err + } + _, err = retrieveApiKeyFromServer(data, rawState["organization_id"], rawState["id"]) + if err != nil { + return err + } + return nil + } +} + +func retrieveApiKeyFromServer(data *providerschema.Data, organizationId, apiKeyId string) (*api.GetApiKeyResponse, error) { + url := fmt.Sprintf("%s/v4/organizations/%s/apikeys/%s", data.HostURL, organizationId, apiKeyId) + cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} + response, err := data.Client.Execute( + cfg, + nil, + data.Token, + nil, + ) + if err != nil { + return nil, err + } + apiKeyResp := api.GetApiKeyResponse{} + err = json.Unmarshal(response.Body, &apiKeyResp) + if err != nil { + return nil, err + } + return &apiKeyResp, nil +} + +func generateApiKeyImportIdForResource(resourceReference string) resource.ImportStateIdFunc { + return func(state *terraform.State) (string, error) { + var rawState map[string]string + for _, m := range state.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[resourceReference]; ok { + rawState = v.Primary.Attributes + } + } + } + fmt.Printf("raw state %s", rawState) + return fmt.Sprintf("id=%s,organization_id=%s", rawState["id"], rawState["organization_id"]), nil + } +} diff --git a/internal/resources/apikey.go b/internal/resources/apikey.go index 02147eb8..f729925f 100644 --- a/internal/resources/apikey.go +++ b/internal/resources/apikey.go @@ -27,6 +27,14 @@ var ( _ resource.ResourceWithImportState = &ApiKey{} ) +const errorMessageAfterApiKeyCreation = "Api Key creation is successful, but encountered an error while checking the current" + + " state of the api key. Please run `terraform plan` after 1-2 minutes to know the" + + " current api key state. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileApiKeyCreation = "There is an error during api key creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // ApiKey is the ApiKey resource implementation. type ApiKey struct { *providerschema.Data @@ -132,8 +140,8 @@ func (a *ApiKey) Create(ctx context.Context, req resource.CreateRequest, resp *r ) if err != nil { resp.Diagnostics.AddError( - "Error creating ApiKey", - "Could not create ApiKey, unexpected error: "+api.ParseError(err), + "Error creating ApiKey Here", + errorMessageWhileApiKeyCreation+api.ParseError(err), ) return } @@ -143,39 +151,28 @@ func (a *ApiKey) Create(ctx context.Context, req resource.CreateRequest, resp *r if err != nil { resp.Diagnostics.AddError( "Error creating ApiKey", - "Could not create ApiKey, unexpected error: "+err.Error(), + errorMessageWhileApiKeyCreation+"error during unmarshalling: "+err.Error(), ) return } + diags = resp.State.Set(ctx, initializeApiKeyWithPlanAndId(plan, apiKeyResponse.Id)) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + refreshedState, err := a.retrieveApiKey(ctx, organizationId, apiKeyResponse.Id) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error creating ApiKey", - "Could not create ApiKey, unexpected error: "+api.ParseError(err), + errorMessageAfterApiKeyCreation+api.ParseError(err), ) return } - resources, err := providerschema.OrderList2(plan.Resources, refreshedState.Resources) - switch err { - case nil: - refreshedState.Resources = resources - default: - tflog.Error(ctx, err.Error()) - } - - for i, resource := range refreshedState.Resources { - if providerschema.AreEqual(resource.Roles, plan.Resources[i].Roles) { - refreshedState.Resources[i].Roles = plan.Resources[i].Roles - } - } - - if providerschema.AreEqual(refreshedState.OrganizationRoles, plan.OrganizationRoles) { - refreshedState.OrganizationRoles = plan.OrganizationRoles - } - refreshedState.Token = types.StringValue(apiKeyResponse.Token) + refreshedState = a.retainResourcesIfOrgOwner(&plan, refreshedState) // Set state to fully populated data diags = resp.State.Set(ctx, refreshedState) @@ -225,29 +222,10 @@ func (a *ApiKey) Read(ctx context.Context, req resource.ReadRequest, resp *resou return } - resources, err := providerschema.OrderList2(state.Resources, refreshedState.Resources) - switch err { - case nil: - refreshedState.Resources = resources - default: - tflog.Warn(ctx, err.Error()) - } - - if len(state.Resources) == len(refreshedState.Resources) { - for i, resource := range refreshedState.Resources { - if providerschema.AreEqual(resource.Roles, state.Resources[i].Roles) { - refreshedState.Resources[i].Roles = state.Resources[i].Roles - } - } - } - - if providerschema.AreEqual(refreshedState.OrganizationRoles, state.OrganizationRoles) { - refreshedState.OrganizationRoles = state.OrganizationRoles - } - refreshedState.Token = state.Token refreshedState.Rotate = state.Rotate refreshedState.Secret = state.Secret + refreshedState = a.retainResourcesIfOrgOwner(&state, refreshedState) // Set refreshed state diags = resp.State.Set(ctx, &refreshedState) @@ -313,7 +291,7 @@ func (a *ApiKey) Update(ctx context.Context, req resource.UpdateRequest, resp *r } url := fmt.Sprintf("%s/v4/organizations/%s/apikeys/%s/rotate", a.HostURL, organizationId, apiKeyId) - cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusOK} + cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} response, err := a.Client.ExecuteWithRetry( ctx, cfg, @@ -354,29 +332,12 @@ func (a *ApiKey) Update(ctx context.Context, req resource.UpdateRequest, resp *r return } - resources, err := providerschema.OrderList2(state.Resources, currentState.Resources) - switch err { - case nil: - currentState.Resources = resources - default: - tflog.Error(ctx, err.Error()) - } - - for i, resource := range currentState.Resources { - if providerschema.AreEqual(resource.Roles, state.Resources[i].Roles) { - currentState.Resources[i].Roles = state.Resources[i].Roles - } - } - - if providerschema.AreEqual(currentState.OrganizationRoles, state.OrganizationRoles) { - currentState.OrganizationRoles = state.OrganizationRoles - } - currentState.Secret = types.StringValue(rotateApiKeyResponse.SecretKey) if !currentState.Id.IsNull() && !currentState.Id.IsUnknown() && !currentState.Secret.IsNull() && !currentState.Secret.IsUnknown() { currentState.Token = types.StringValue(base64.StdEncoding.EncodeToString([]byte(currentState.Id.ValueString() + ":" + currentState.Secret.ValueString()))) } currentState.Rotate = plan.Rotate + currentState = a.retainResourcesIfOrgOwner(&plan, currentState) // Set state to fully populated data diags = resp.State.Set(ctx, currentState) @@ -486,9 +447,9 @@ func (a *ApiKey) validateCreateApiKeyRequest(plan providerschema.ApiKey) error { if plan.OrganizationRoles == nil { return fmt.Errorf("organizationRoles cannot be empty") } - if plan.Resources == nil { - return fmt.Errorf("resource cannot be nil") - } + //if plan.Resources == nil { + // return fmt.Errorf("resource cannot be nil") + //} if !plan.Rotate.IsNull() && !plan.Rotate.IsUnknown() { return fmt.Errorf("rotate value should not be set") } @@ -536,7 +497,7 @@ func (a *ApiKey) convertResources(resources []providerschema.ApiKeyResourcesItem } // convertAllowedCidrs is used to convert allowed cidrs in types.List to array of string. -func (a *ApiKey) convertAllowedCidrs(ctx context.Context, allowedCidrs types.List) ([]string, error) { +func (a *ApiKey) convertAllowedCidrs(ctx context.Context, allowedCidrs types.Set) ([]string, error) { elements := make([]types.String, 0, len(allowedCidrs.Elements())) diags := allowedCidrs.ElementsAs(ctx, &elements, false) if diags.HasError() { @@ -549,3 +510,29 @@ func (a *ApiKey) convertAllowedCidrs(ctx context.Context, allowedCidrs types.Lis } return convertedAllowedCidrs, nil } + +func (a *ApiKey) retainResourcesIfOrgOwner(apiKeyReq, apiKeyRes *providerschema.ApiKey) *providerschema.ApiKey { + isOrgOwner := false + for _, role := range apiKeyRes.OrganizationRoles { + if role.ValueString() == "organizationOwner" { + isOrgOwner = true + } + } + if isOrgOwner { + apiKeyRes.Resources = apiKeyReq.Resources + } + return apiKeyRes +} + +func initializeApiKeyWithPlanAndId(plan providerschema.ApiKey, id string) providerschema.ApiKey { + plan.Id = types.StringValue(id) + if plan.Secret.IsNull() || plan.Secret.IsUnknown() { + plan.Secret = types.StringNull() + } + if plan.Rotate.IsNull() || plan.Rotate.IsUnknown() { + plan.Rotate = types.NumberNull() + } + plan.Token = types.StringNull() + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) + return plan +} diff --git a/internal/resources/apikey_schema.go b/internal/resources/apikey_schema.go index c72bbc05..90e48818 100644 --- a/internal/resources/apikey_schema.go +++ b/internal/resources/apikey_schema.go @@ -1,12 +1,12 @@ package resources import ( - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -23,33 +23,33 @@ func ApiKeySchema() schema.Schema { }, "organization_id": stringAttribute(required, requiresReplace), "name": stringAttribute(required, requiresReplace), - "description": stringAttribute(optional, computed, requiresReplace, useStateForUnknown), - "expiry": float64Attribute(optional, computed, requiresReplace, useStateForUnknown), - "allowed_cidrs": schema.ListAttribute{ + "description": stringDefaultAttribute("", optional, computed, requiresReplace, useStateForUnknown), + "expiry": float64DefaultAttribute(180, optional, computed, requiresReplace, useStateForUnknown), + "allowed_cidrs": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - listplanmodifier.RequiresReplace(), + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + setplanmodifier.RequiresReplace(), }, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), }, - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{types.StringValue("0.0.0.0/0")})), + Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{types.StringValue("0.0.0.0/0")})), }, - "organization_roles": stringListAttribute(required, requiresReplace), - "resources": schema.ListNestedAttribute{ + "organization_roles": stringSetAttribute(required, requiresReplace), + "resources": schema.SetNestedAttribute{ Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": stringAttribute(required), - "roles": stringListAttribute(required), - "type": stringAttribute(optional, computed), + "roles": stringSetAttribute(required), + "type": stringDefaultAttribute("project", optional, computed), }, }, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), }, }, "rotate": schema.NumberAttribute{ diff --git a/internal/resources/appservice.go b/internal/resources/appservice.go index a68b1e17..5cb50d30 100644 --- a/internal/resources/appservice.go +++ b/internal/resources/appservice.go @@ -25,6 +25,14 @@ var ( _ resource.ResourceWithImportState = &AppService{} ) +const errorMessageAfterAppServiceCreationInitiation = "App Service creation is initiated, but encountered an error while checking the current" + + " state of the app service. Please run `terraform plan` after 4-5 minutes to know the" + + " current status of the app service. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileAppServiceCreation = "There is an error during app service creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // AppService is the AppService resource implementation. type AppService struct { *providerschema.Data @@ -103,7 +111,7 @@ func (a *AppService) Create(ctx context.Context, req resource.CreateRequest, res if err != nil { resp.Diagnostics.AddError( "Error executing request", - "Could not execute request, unexpected error: "+err.Error(), + errorMessageWhileAppServiceCreation+err.Error(), ) return } @@ -113,7 +121,7 @@ func (a *AppService) Create(ctx context.Context, req resource.CreateRequest, res if err != nil { resp.Diagnostics.AddError( "Error creating app service", - "Could not create app service, unexpected error: "+err.Error(), + errorMessageWhileAppServiceCreation+"error during unmarshalling:"+err.Error(), ) return } @@ -128,7 +136,7 @@ func (a *AppService) Create(ctx context.Context, req resource.CreateRequest, res if err != nil { resp.Diagnostics.AddWarning( "Error creating app service", - "Could not create app service, unexpected error: "+api.ParseError(err), + errorMessageAfterAppServiceCreationInitiation+api.ParseError(err), ) return } @@ -136,7 +144,7 @@ func (a *AppService) Create(ctx context.Context, req resource.CreateRequest, res if err != nil { resp.Diagnostics.AddWarning( "Error creating app service", - "Could not create app service, unexpected error: "+api.ParseError(err), + errorMessageAfterAppServiceCreationInitiation+api.ParseError(err), ) return } diff --git a/internal/resources/attributes.go b/internal/resources/attributes.go index 924a315a..bbebdb20 100644 --- a/internal/resources/attributes.go +++ b/internal/resources/attributes.go @@ -4,6 +4,7 @@ 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/float64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" @@ -180,6 +181,13 @@ func float64Attribute(fields ...string) *schema.Float64Attribute { return &attribute } +// float64DefaultAttribute sets the default values for an float field and returns the float64 attribute +func float64DefaultAttribute(defaultValue float64, fields ...string) *schema.Float64Attribute { + attribute := float64Attribute(fields...) + attribute.Default = float64default.StaticFloat64(defaultValue) + return attribute +} + // stringListAttribute returns a Terraform string list schema attribute // which is configured to be of type string. func stringListAttribute(fields ...string) *schema.ListAttribute { diff --git a/internal/resources/backup.go b/internal/resources/backup.go index 8e6a4b03..afe1b6f6 100644 --- a/internal/resources/backup.go +++ b/internal/resources/backup.go @@ -25,6 +25,9 @@ var ( _ resource.ResourceWithImportState = &Backup{} ) +const errorMessageWhileBackupCreation = "There is an error during backup creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // Backup is the Backup resource implementation. type Backup struct { *providerschema.Data @@ -97,20 +100,20 @@ func (b *Backup) Create(ctx context.Context, req resource.CreateRequest, resp *r if err != nil { resp.Diagnostics.AddError( "Error executing create backup request", - "Could not execute create backup request : unexpected error "+api.ParseError(err), + errorMessageWhileBackupCreation+api.ParseError(err), ) return } backupResponse, err := b.checkLatestBackupStatus(ctx, organizationId, projectId, clusterId, bucketId, backupFound, latestBackup) if err != nil { - if diags.HasError() { - resp.Diagnostics.AddError( - "Error whiling checking latest backup status", - fmt.Sprintf("Could not read check latest backup status, unexpected error: "+api.ParseError(err)), - ) - return - } + resp.Diagnostics.AddError( + "Error while checking latest backup status", + fmt.Sprintf("Could not read check latest backup status."+ + "Please check in Capella to see if any hanging resources have "+ + "been created, unexpected error: "+api.ParseError(err)), + ) + return } backupStats := providerschema.NewBackupStats(*backupResponse.BackupStats) @@ -118,7 +121,9 @@ func (b *Backup) Create(ctx context.Context, req resource.CreateRequest, resp *r if diags.HasError() { resp.Diagnostics.AddError( "Error Reading Backup Stats", - fmt.Sprintf("Could not read backup stats data in a backup record, unexpected error: %s", fmt.Errorf("error while backup stats conversion")), + fmt.Sprintf("Could not read backup stats data in a backup record, "+ + "please check in Capella to see if any hanging resources have been created, "+ + "unexpected error: %s", fmt.Errorf("error while backup stats conversion")), ) return } @@ -128,7 +133,9 @@ func (b *Backup) Create(ctx context.Context, req resource.CreateRequest, resp *r if diags.HasError() { resp.Diagnostics.AddError( "Error Error Reading Backup Schedule Info", - fmt.Sprintf("Could not read backup schedule info in a backup record, unexpected error: %s", fmt.Errorf("error while backup schedule info conversion")), + fmt.Sprintf("Could not read backup schedule info in a backup record, "+ + "please check in Capella to see if any hanging resources have been created, "+ + "unexpected error: %s", fmt.Errorf("error while backup schedule info conversion")), ) return } diff --git a/internal/resources/cluster.go b/internal/resources/cluster.go index 3540ae84..620f8b19 100644 --- a/internal/resources/cluster.go +++ b/internal/resources/cluster.go @@ -29,9 +29,9 @@ var ( ) const errorMessageAfterClusterCreationInitiation = "Cluster creation is initiated, but encountered an error while checking the current" + - " state of the cluster.Please run `terraform plan` after 4-5 minutes to know the" + + " state of the cluster. Please run `terraform plan` after 4-5 minutes to know the" + " current status of the cluster. Additionally, run `terraform apply --refresh-only` to update" + - " the status from remote, unexpected error: " + " the state from remote, unexpected error: " const errorMessageWhileClusterCreation = "There is an error during cluster creation. Please check in Capella to see if any hanging resources" + " have been created, unexpected error: " diff --git a/internal/resources/project.go b/internal/resources/project.go index bf6492af..bcce04ff 100644 --- a/internal/resources/project.go +++ b/internal/resources/project.go @@ -114,7 +114,7 @@ func (r *Project) Create(ctx context.Context, req resource.CreateRequest, resp * refreshedState, err := r.retrieveProject(ctx, organizationId, projectResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error creating project", "Could not create project, unexpected error: "+api.ParseError(err), ) diff --git a/internal/resources/user_schema.go b/internal/resources/user_schema.go index 5b7c973c..00078288 100644 --- a/internal/resources/user_schema.go +++ b/internal/resources/user_schema.go @@ -13,7 +13,7 @@ func UserSchema() schema.Schema { "inactive": boolAttribute(computed), "email": stringAttribute(required), "organization_id": stringAttribute(required), - "organization_roles": stringListAttribute(required), + "organization_roles": stringSetAttribute(required), "last_login": stringAttribute(computed), "region": stringAttribute(computed), "time_zone": stringAttribute(computed), @@ -23,7 +23,7 @@ func UserSchema() schema.Schema { Optional: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "type": stringAttribute(optional, computed), + "type": stringDefaultAttribute("project", optional, computed), "id": stringAttribute(required), "roles": stringSetAttribute(required), }, diff --git a/internal/schema/apikey.go b/internal/schema/apikey.go index b91799b3..970ed84d 100644 --- a/internal/schema/apikey.go +++ b/internal/schema/apikey.go @@ -33,7 +33,7 @@ type ApiKey struct { // AllowedCIDRs is the list of inbound CIDRs for the API key. // The system making a request must come from one of the allowed CIDRs. - AllowedCIDRs types.List `tfsdk:"allowed_cidrs"` + AllowedCIDRs types.Set `tfsdk:"allowed_cidrs"` Audit types.Object `tfsdk:"audit"` // Description is the description for the API key. @@ -100,15 +100,15 @@ func NewApiKey(apiKey *api.GetApiKeyResponse, organizationId string, auditObject // MorphAllowedCidrs is used to convert string list to basetypes.ListValue // TODO : add unit testing -func MorphAllowedCidrs(allowedCIDRs []string) (basetypes.ListValue, error) { +func MorphAllowedCidrs(allowedCIDRs []string) (basetypes.SetValue, error) { var newAllowedCidr []attr.Value for _, allowedCidr := range allowedCIDRs { newAllowedCidr = append(newAllowedCidr, types.StringValue(allowedCidr)) } - newAllowedCidrs, diags := types.ListValue(types.StringType, newAllowedCidr) + newAllowedCidrs, diags := types.SetValue(types.StringType, newAllowedCidr) if diags.HasError() { - return types.ListUnknown(types.StringType), fmt.Errorf("error while converting allowedcidrs") + return types.SetUnknown(types.StringType), fmt.Errorf("error while converting allowedcidrs") } return newAllowedCidrs, nil @@ -120,7 +120,7 @@ func MorphAllowedCidrs(allowedCIDRs []string) (basetypes.ListValue, error) { func MorphApiKeyOrganizationRoles(organizationRoles []string) []basetypes.StringValue { var newOrganizationRoles []types.String for _, organizationRole := range organizationRoles { - newOrganizationRoles = append(newOrganizationRoles, types.StringValue(string(organizationRole))) + newOrganizationRoles = append(newOrganizationRoles, types.StringValue(organizationRole)) } return newOrganizationRoles } @@ -129,6 +129,11 @@ func MorphApiKeyOrganizationRoles(organizationRoles []string) []basetypes.String // to terraform types.String // TODO : add unit testing func MorphApiKeyResources(resources api.Resources) []ApiKeyResourcesItems { + //if len(resources) == 0 { + // fmt.Println("*************************************") + // fmt.Println(len(resources)) + // return make([]ApiKeyResourcesItems, 0) + //} var newApiKeyResourcesItems []ApiKeyResourcesItems for _, resource := range resources { newResourceItem := ApiKeyResourcesItems{ @@ -139,7 +144,7 @@ func MorphApiKeyResources(resources api.Resources) []ApiKeyResourcesItems { } var newRoles []types.String for _, role := range resource.Roles { - newRoles = append(newRoles, types.StringValue(string(role))) + newRoles = append(newRoles, types.StringValue(role)) } newResourceItem.Roles = newRoles newApiKeyResourcesItems = append(newApiKeyResourcesItems, newResourceItem) @@ -263,46 +268,46 @@ func (a ApiKeys) Validate() (organizationId string, err error) { return a.OrganizationId.ValueString(), nil } -// OrderList2 function to order list2 based on list1's Ids -func OrderList2(list1, list2 []ApiKeyResourcesItems) ([]ApiKeyResourcesItems, error) { - if len(list1) != len(list2) { - return nil, fmt.Errorf("returned resources is not same as in plan") - } - // Create a map from Id to APIKeyResourcesItems for list2 - idToItem := make(map[string]ApiKeyResourcesItems) - for _, item := range list2 { - idToItem[item.Id.ValueString()] = item - } - - // Create a new ordered list2 based on the order of list1's Ids - orderedList2 := make([]ApiKeyResourcesItems, len(list1)) - for i, item1 := range list1 { - orderedList2[i] = idToItem[item1.Id.ValueString()] - } - - if len(orderedList2) != len(list2) { - return nil, fmt.Errorf("returned resources is not same as in plan") - } - - return orderedList2, nil -} - -// AreEqual returns true if the two arrays contain the same elements, -// without any extra values, False otherwise. -func AreEqual[T comparable](array1 []T, array2 []T) bool { - if len(array1) != len(array2) { - return false - } - set1 := make(map[T]bool) - for _, element := range array1 { - set1[element] = true - } - - for _, element := range array2 { - if !set1[element] { - return false - } - } - - return len(set1) == len(array1) -} +//// OrderList2 function to order list2 based on list1's Ids +//func OrderList2(list1, list2 []ApiKeyResourcesItems) ([]ApiKeyResourcesItems, error) { +// if len(list1) != len(list2) { +// return nil, fmt.Errorf("returned resources is not same as in plan") +// } +// // Create a map from Id to APIKeyResourcesItems for list2 +// idToItem := make(map[string]ApiKeyResourcesItems) +// for _, item := range list2 { +// idToItem[item.Id.ValueString()] = item +// } +// +// // Create a new ordered list2 based on the order of list1's Ids +// orderedList2 := make([]ApiKeyResourcesItems, len(list1)) +// for i, item1 := range list1 { +// orderedList2[i] = idToItem[item1.Id.ValueString()] +// } +// +// if len(orderedList2) != len(list2) { +// return nil, fmt.Errorf("returned resources is not same as in plan") +// } +// +// return orderedList2, nil +//} + +//// AreEqual returns true if the two arrays contain the same elements, +//// without any extra values, False otherwise. +//func AreEqual[T comparable](array1 []T, array2 []T) bool { +// if len(array1) != len(array2) { +// return false +// } +// set1 := make(map[T]bool) +// for _, element := range array1 { +// set1[element] = true +// } +// +// for _, element := range array2 { +// if !set1[element] { +// return false +// } +// } +// +// return len(set1) == len(array1) +//} From 34fbec9b9f0c5ed81f096dce21a5315c3eae31f1 Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Wed, 6 Dec 2023 11:08:08 +0530 Subject: [PATCH 4/7] removed unwanted code --- internal/schema/apikey.go | 91 ++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/internal/schema/apikey.go b/internal/schema/apikey.go index 970ed84d..f3dce4b6 100644 --- a/internal/schema/apikey.go +++ b/internal/schema/apikey.go @@ -129,11 +129,6 @@ func MorphApiKeyOrganizationRoles(organizationRoles []string) []basetypes.String // to terraform types.String // TODO : add unit testing func MorphApiKeyResources(resources api.Resources) []ApiKeyResourcesItems { - //if len(resources) == 0 { - // fmt.Println("*************************************") - // fmt.Println(len(resources)) - // return make([]ApiKeyResourcesItems, 0) - //} var newApiKeyResourcesItems []ApiKeyResourcesItems for _, resource := range resources { newResourceItem := ApiKeyResourcesItems{ @@ -268,46 +263,46 @@ func (a ApiKeys) Validate() (organizationId string, err error) { return a.OrganizationId.ValueString(), nil } -//// OrderList2 function to order list2 based on list1's Ids -//func OrderList2(list1, list2 []ApiKeyResourcesItems) ([]ApiKeyResourcesItems, error) { -// if len(list1) != len(list2) { -// return nil, fmt.Errorf("returned resources is not same as in plan") -// } -// // Create a map from Id to APIKeyResourcesItems for list2 -// idToItem := make(map[string]ApiKeyResourcesItems) -// for _, item := range list2 { -// idToItem[item.Id.ValueString()] = item -// } -// -// // Create a new ordered list2 based on the order of list1's Ids -// orderedList2 := make([]ApiKeyResourcesItems, len(list1)) -// for i, item1 := range list1 { -// orderedList2[i] = idToItem[item1.Id.ValueString()] -// } -// -// if len(orderedList2) != len(list2) { -// return nil, fmt.Errorf("returned resources is not same as in plan") -// } -// -// return orderedList2, nil -//} - -//// AreEqual returns true if the two arrays contain the same elements, -//// without any extra values, False otherwise. -//func AreEqual[T comparable](array1 []T, array2 []T) bool { -// if len(array1) != len(array2) { -// return false -// } -// set1 := make(map[T]bool) -// for _, element := range array1 { -// set1[element] = true -// } -// -// for _, element := range array2 { -// if !set1[element] { -// return false -// } -// } -// -// return len(set1) == len(array1) -//} +// OrderList2 function to order list2 based on list1's Ids +func OrderList2(list1, list2 []ApiKeyResourcesItems) ([]ApiKeyResourcesItems, error) { + if len(list1) != len(list2) { + return nil, fmt.Errorf("returned resources is not same as in plan") + } + // Create a map from Id to APIKeyResourcesItems for list2 + idToItem := make(map[string]ApiKeyResourcesItems) + for _, item := range list2 { + idToItem[item.Id.ValueString()] = item + } + + // Create a new ordered list2 based on the order of list1's Ids + orderedList2 := make([]ApiKeyResourcesItems, len(list1)) + for i, item1 := range list1 { + orderedList2[i] = idToItem[item1.Id.ValueString()] + } + + if len(orderedList2) != len(list2) { + return nil, fmt.Errorf("returned resources is not same as in plan") + } + + return orderedList2, nil +} + +// AreEqual returns true if the two arrays contain the same elements, +// without any extra values, False otherwise. +func AreEqual[T comparable](array1 []T, array2 []T) bool { + if len(array1) != len(array2) { + return false + } + set1 := make(map[T]bool) + for _, element := range array1 { + set1[element] = true + } + + for _, element := range array2 { + if !set1[element] { + return false + } + } + + return len(set1) == len(array1) +} From 87282b95cb67052ee0ab39cff25f5ed14567f1bb Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Wed, 6 Dec 2023 19:10:19 +0530 Subject: [PATCH 5/7] added fix for hanging resource in all the resources --- internal/api/client.go | 1 + internal/errors/errors.go | 3 +- internal/resources/allowlist.go | 16 +++++++-- internal/resources/apikey.go | 2 ++ internal/resources/appservice.go | 2 ++ internal/resources/backup_schedule.go | 12 +++++-- internal/resources/bucket.go | 10 +++--- internal/resources/cluster.go | 2 ++ internal/resources/database_credential.go | 2 ++ internal/resources/project.go | 33 +++++++++++++++++-- internal/resources/user.go | 40 ++++++++++++++++++++--- 11 files changed, 105 insertions(+), 18 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index 10e6018e..76b3e980 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -46,6 +46,7 @@ type EndpointCfg struct { SuccessStatus int } +// defaultWaitAttempt re-attempt http request after 2 seconds const defaultWaitAttempt = time.Second * 2 // Execute is used to construct and execute a HTTP request. diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 998abb2f..6076d7ce 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -150,7 +150,6 @@ var ( // is timeout after initiation. ErrClusterCreationTimeoutAfterInitiation = errors.New("cluster creation status transition timed out after initiation") - ErrNotFinalState = errors.New("not final state") - + // ErrGatewayTimeout is returned when a gateway operation times out. ErrGatewayTimeout = errors.New("gateway timeout") ) diff --git a/internal/resources/allowlist.go b/internal/resources/allowlist.go index 71194693..68bc930d 100644 --- a/internal/resources/allowlist.go +++ b/internal/resources/allowlist.go @@ -22,6 +22,14 @@ var ( _ resource.ResourceWithImportState = &AllowList{} ) +const errorMessageAfterAllowListCreation = "Allow list creation is successful, but encountered an error while checking the current" + + " state of the allow list. Please run `terraform plan` after 1-2 minutes to know the" + + " current allow list state. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileAllowListCreation = "There is an error during allow list creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // AllowList is the AllowList resource implementation. type AllowList struct { *providerschema.Data @@ -92,7 +100,7 @@ func (r *AllowList) Create(ctx context.Context, req resource.CreateRequest, resp if err != nil { resp.Diagnostics.AddError( "Error executing request", - "Could not execute request, unexpected error: "+api.ParseError(err), + errorMessageWhileAllowListCreation+api.ParseError(err), ) return } @@ -102,7 +110,7 @@ func (r *AllowList) Create(ctx context.Context, req resource.CreateRequest, resp if err != nil { resp.Diagnostics.AddError( "Error creating allow list", - "Could not create allow list, unexpected error: "+err.Error(), + errorMessageWhileAllowListCreation+"error during unmarshalling: "+err.Error(), ) return } @@ -117,7 +125,7 @@ func (r *AllowList) Create(ctx context.Context, req resource.CreateRequest, resp if err != nil { resp.Diagnostics.AddWarning( "Error reading Capella AllowList", - "Could not read Capella AllowList "+allowListResponse.Id.String()+": "+api.ParseError(err), + errorMessageAfterAllowListCreation+api.ParseError(err), ) return } @@ -326,6 +334,8 @@ func (r *AllowList) refreshAllowList(ctx context.Context, organizationId, projec return &refreshedState, nil } +// initializeAllowListWithPlanAndId initializes an instance of providerschema.AllowList +// with the specified plan and ID. It marks all computed fields as null. func initializeAllowListWithPlanAndId(plan providerschema.AllowList, id string) providerschema.AllowList { plan.Id = types.StringValue(id) plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) diff --git a/internal/resources/apikey.go b/internal/resources/apikey.go index f729925f..462c09c8 100644 --- a/internal/resources/apikey.go +++ b/internal/resources/apikey.go @@ -524,6 +524,8 @@ func (a *ApiKey) retainResourcesIfOrgOwner(apiKeyReq, apiKeyRes *providerschema. return apiKeyRes } +// initializeApiKeyWithPlanAndId initializes an instance of providerschema.ApiKey +// with the specified plan and ID. It marks all computed fields as null. func initializeApiKeyWithPlanAndId(plan providerschema.ApiKey, id string) providerschema.ApiKey { plan.Id = types.StringValue(id) if plan.Secret.IsNull() || plan.Secret.IsUnknown() { diff --git a/internal/resources/appservice.go b/internal/resources/appservice.go index 5cb50d30..528a6c9a 100644 --- a/internal/resources/appservice.go +++ b/internal/resources/appservice.go @@ -531,6 +531,8 @@ func (a *AppService) getAppService(ctx context.Context, organizationId, projectI return &appServiceResp, nil } +// initializePendingAppServiceWithPlanAndId initializes an instance of providerschema.AppService +// with the specified plan and ID. It marks all computed fields as null and state as pending. func initializePendingAppServiceWithPlanAndId(plan providerschema.AppService, id string) providerschema.AppService { plan.Id = types.StringValue(id) plan.CurrentState = types.StringValue("pending") diff --git a/internal/resources/backup_schedule.go b/internal/resources/backup_schedule.go index 42a52d61..4e7b8080 100644 --- a/internal/resources/backup_schedule.go +++ b/internal/resources/backup_schedule.go @@ -25,6 +25,14 @@ var ( _ resource.ResourceWithImportState = &BackupSchedule{} ) +const errorMessageAfterBackupScheduleCreation = "Backup Schedule creation is successful, but encountered an error while checking the current" + + " state of the backup schedule. Please run `terraform plan` after 1-2 minutes to know the" + + " current backup schedule state. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileBackupScheduleCreation = "There is an error during backup schedule creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // BackupSchedule is the BackupSchedule resource implementation. type BackupSchedule struct { *providerschema.Data @@ -95,7 +103,7 @@ func (b *BackupSchedule) Create(ctx context.Context, req resource.CreateRequest, if err != nil { resp.Diagnostics.AddError( "Error executing request", - "Could not execute request, unexpected error: "+api.ParseError(err), + errorMessageWhileBackupScheduleCreation+api.ParseError(err), ) return } @@ -110,7 +118,7 @@ func (b *BackupSchedule) Create(ctx context.Context, req resource.CreateRequest, if err != nil { resp.Diagnostics.AddWarning( "Error Reading Capella Backup Schedule", - "Could not read Capella Backup Schedule for the bucket: %s "+bucketId+": "+api.ParseError(err), + "Could not read Capella Backup Schedule for the bucket: %s "+bucketId+"."+errorMessageAfterBackupScheduleCreation+api.ParseError(err), ) return } diff --git a/internal/resources/bucket.go b/internal/resources/bucket.go index 2c5dd836..6db6f571 100644 --- a/internal/resources/bucket.go +++ b/internal/resources/bucket.go @@ -113,8 +113,8 @@ func (c *Bucket) Create(ctx context.Context, req resource.CreateRequest, resp *r if plan.ProjectId.IsNull() { resp.Diagnostics.AddError( - "Error creating database credential", - "Could not create database credential, unexpected error: "+errors.ErrProjectIdCannotBeEmpty.Error(), + "Error creating bucket", + "Could not create bucket, unexpected error: "+errors.ErrProjectIdCannotBeEmpty.Error(), ) return } @@ -122,8 +122,8 @@ func (c *Bucket) Create(ctx context.Context, req resource.CreateRequest, resp *r if plan.ClusterId.IsNull() { resp.Diagnostics.AddError( - "Error creating database credential", - "Could not create database credential, unexpected error: "+errors.ErrClusterIdCannotBeEmpty.Error(), + "Error creating bucket", + "Could not create bucket, unexpected error: "+errors.ErrClusterIdCannotBeEmpty.Error(), ) return } @@ -441,6 +441,8 @@ func (c *Bucket) Update(ctx context.Context, req resource.UpdateRequest, resp *r } } +// initializeBucketWithPlanAndId initializes an instance of providerschema.Bucket +// with the specified plan and ID. It marks all computed fields as null. func initializeBucketWithPlanAndId(plan providerschema.Bucket, id string) providerschema.Bucket { plan.Id = types.StringValue(id) if plan.StorageBackend.IsNull() || plan.StorageBackend.IsUnknown() { diff --git a/internal/resources/cluster.go b/internal/resources/cluster.go index 620f8b19..94f19c68 100644 --- a/internal/resources/cluster.go +++ b/internal/resources/cluster.go @@ -735,6 +735,8 @@ func getCouchbaseServer(ctx context.Context, config tfsdk.Config, diags *diag.Di return couchbaseServer } +// initializePendingClusterWithPlanAndId initializes an instance of providerschema.Cluster +// with the specified plan and ID. It marks all computed fields as null and state as pending. func initializePendingClusterWithPlanAndId(plan providerschema.Cluster, id string) providerschema.Cluster { plan.Id = types.StringValue(id) plan.CurrentState = types.StringValue("pending") diff --git a/internal/resources/database_credential.go b/internal/resources/database_credential.go index f6f3e972..7902c7a2 100644 --- a/internal/resources/database_credential.go +++ b/internal/resources/database_credential.go @@ -507,6 +507,8 @@ func mapAccess(plan providerschema.DatabaseCredential) []providerschema.Access { return access } +// initializeDataBaseCredentialWithPlanPasswordAndId initializes an instance of providerschema.DatabaseCredential +// with the specified plan and ID. It marks all computed fields as null. func initializeDataBaseCredentialWithPlanPasswordAndId(plan providerschema.DatabaseCredential, password, id string) providerschema.DatabaseCredential { plan.Id = types.StringValue(id) if password != "" { diff --git a/internal/resources/project.go b/internal/resources/project.go index bcce04ff..41cf810c 100644 --- a/internal/resources/project.go +++ b/internal/resources/project.go @@ -24,6 +24,14 @@ var ( _ resource.ResourceWithImportState = &Project{} ) +const errorMessageAfterProjectCreation = "Project creation is successful, but encountered an error while checking the current" + + " state of the project. Please run `terraform plan` after 1-2 minutes to know the" + + " current project state. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileProjectCreation = "There is an error during project creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // Project is the project resource implementation. type Project struct { *providerschema.Data @@ -97,7 +105,7 @@ func (r *Project) Create(ctx context.Context, req resource.CreateRequest, resp * if err != nil { resp.Diagnostics.AddError( "Error creating project", - "Could not create project, unexpected error: "+api.ParseError(err), + errorMessageWhileProjectCreation+api.ParseError(err), ) return } @@ -107,16 +115,22 @@ func (r *Project) Create(ctx context.Context, req resource.CreateRequest, resp * if err != nil { resp.Diagnostics.AddError( "Error creating project", - "Could not create project, unexpected error: "+err.Error(), + errorMessageWhileProjectCreation+"error during unmarshalling: "+err.Error(), ) return } + diags = resp.State.Set(ctx, initializeProjectWithPlanAndId(plan, projectResponse.Id.String())) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + refreshedState, err := r.retrieveProject(ctx, organizationId, projectResponse.Id.String()) if err != nil { resp.Diagnostics.AddWarning( "Error creating project", - "Could not create project, unexpected error: "+api.ParseError(err), + errorMessageAfterProjectCreation+api.ParseError(err), ) return } @@ -364,3 +378,16 @@ func (r *Project) retrieveProject(ctx context.Context, organizationId, projectId return &refreshedState, nil } + +// initializeProjectWithPlanAndId initializes an instance of providerschema.Project +// with the specified plan and ID. It marks all computed fields as null. +func initializeProjectWithPlanAndId(plan providerschema.Project, id string) providerschema.Project { + plan.Id = types.StringValue(id) + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) + plan.Etag = types.StringNull() + if plan.Description.IsNull() || plan.Description.IsUnknown() { + plan.Description = types.StringNull() + } + + return plan +} diff --git a/internal/resources/user.go b/internal/resources/user.go index fd616308..63c82e0a 100644 --- a/internal/resources/user.go +++ b/internal/resources/user.go @@ -26,6 +26,14 @@ var ( _ resource.ResourceWithImportState = &User{} ) +const errorMessageAfterUserCreation = "User creation is successful, but encountered an error while checking the current" + + " state of the user. Please run `terraform plan` after 1-2 minutes to know the" + + " current user state. Additionally, run `terraform apply --refresh-only` to update" + + " the state from remote, unexpected error: " + +const errorMessageWhileUserCreation = "There is an error during user creation. Please check in Capella to see if any hanging resources" + + " have been created, unexpected error: " + // User is the User resource implementation type User struct { *providerschema.Data @@ -109,7 +117,7 @@ func (r *User) Create(ctx context.Context, req resource.CreateRequest, resp *res if err != nil { resp.Diagnostics.AddError( "Error executing request", - "Could not execute request, unexpected error: "+api.ParseError(err), + errorMessageWhileUserCreation+api.ParseError(err), ) return } @@ -119,16 +127,22 @@ func (r *User) Create(ctx context.Context, req resource.CreateRequest, resp *res if err != nil { resp.Diagnostics.AddError( "Error creating user", - "Could not create user, unexpected error: "+err.Error(), + errorMessageWhileUserCreation+"error during unmarshalling: "+err.Error(), ) return } + diags = resp.State.Set(ctx, initializeUserWithPlanAndId(plan, createUserResponse.Id.String())) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + refreshedState, err := r.refreshUser(ctx, organizationId, createUserResponse.Id.String()) if err != nil { - resp.Diagnostics.AddError( + resp.Diagnostics.AddWarning( "Error executing request", - "Could not execute request, unexpected error: "+api.ParseError(err), + errorMessageAfterUserCreation+api.ParseError(err), ) return } @@ -561,3 +575,21 @@ func (r *User) ImportState(ctx context.Context, req resource.ImportStateRequest, // Retrieve import ID and save to id attribute resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +// initializeUserWithPlanAndId initializes an instance of providerschema.User +// with the specified plan and ID. It marks all computed fields as null. +func initializeUserWithPlanAndId(plan providerschema.User, id string) providerschema.User { + plan.Id = types.StringValue(id) + if plan.Name.IsNull() || plan.Name.IsUnknown() { + plan.Name = types.StringNull() + } + plan.Status = types.StringNull() + plan.Inactive = types.BoolNull() + plan.LastLogin = types.StringNull() + plan.Region = types.StringNull() + plan.TimeZone = types.StringNull() + plan.EnableNotifications = types.BoolNull() + plan.ExpiresAt = types.StringNull() + plan.Audit = types.ObjectNull(providerschema.CouchbaseAuditData{}.AttributeTypes()) + return plan +} From a9c6a095b87f886dac50900c7dcdf06b75f4aec0 Mon Sep 17 00:00:00 2001 From: Aniket Kumar Date: Wed, 6 Dec 2023 20:59:20 +0530 Subject: [PATCH 6/7] fixed test --- internal/resources/acceptance_tests/cluster_acceptance_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resources/acceptance_tests/cluster_acceptance_test.go b/internal/resources/acceptance_tests/cluster_acceptance_test.go index 6f022d10..d37da479 100644 --- a/internal/resources/acceptance_tests/cluster_acceptance_test.go +++ b/internal/resources/acceptance_tests/cluster_acceptance_test.go @@ -627,7 +627,7 @@ func TestAccClusterResourceNotFound(t *testing.T) { Config: testAccClusterResourceConfigUpdateWhenClusterCreatedWithReqFieldOnly(acctest.Cfg, resourceName, projectResourceName, projectResourceReference, cidr), Check: resource.ComposeAggregateTestCheckFunc( testAccExistsClusterResource(resourceReference), - resource.TestCheckResourceAttr(resourceReference, "name", "Terraform Acceptance Test Cluster Update"), + resource.TestCheckResourceAttr(resourceReference, "name", "Terraform Acceptance Test Cluster Update 2"), resource.TestCheckResourceAttr(resourceReference, "description", "Cluster Updated."), resource.TestCheckResourceAttr(resourceReference, "cloud_provider.type", "aws"), resource.TestCheckResourceAttr(resourceReference, "cloud_provider.region", "us-east-1"), From 1a2540a29d76f0e935f7ea4ef3530ca708342b93 Mon Sep 17 00:00:00 2001 From: Talina Shrotriya <5362897+Talina06@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:01:15 -0800 Subject: [PATCH 7/7] make check --- internal/api/client.go | 4 ++-- .../resources/acceptance_tests/apikey_acceptance_test.go | 7 ++++--- internal/resources/allowlist.go | 2 +- internal/resources/appservice.go | 2 +- internal/resources/attributes.go | 2 +- internal/resources/backup.go | 2 +- internal/resources/bucket.go | 2 +- internal/resources/cluster.go | 2 +- internal/resources/project.go | 2 +- internal/resources/user.go | 2 +- internal/schema/apikey.go | 2 +- 11 files changed, 15 insertions(+), 14 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index a1b5a3d2..426efb2e 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -51,7 +51,7 @@ type EndpointCfg struct { SuccessStatus int } -// defaultWaitAttempt re-attempt http request after 2 seconds +// defaultWaitAttempt re-attempt http request after 2 seconds. const defaultWaitAttempt = time.Second * 2 // Execute is used to construct and execute a HTTP request. @@ -199,7 +199,7 @@ func exec(ctx context.Context, fn func() (response *Response, err error), waitOn response, err = fn() switch { case err == nil: - return response, err + return response, nil case !goer.Is(err, errors.ErrGatewayTimeout): return response, err } diff --git a/internal/resources/acceptance_tests/apikey_acceptance_test.go b/internal/resources/acceptance_tests/apikey_acceptance_test.go index fdd0f74d..41473a7d 100644 --- a/internal/resources/acceptance_tests/apikey_acceptance_test.go +++ b/internal/resources/acceptance_tests/apikey_acceptance_test.go @@ -5,11 +5,12 @@ import ( "fmt" "net/http" "regexp" - "terraform-provider-capella/internal/api" - providerschema "terraform-provider-capella/internal/schema" - acctest "terraform-provider-capella/internal/testing" "testing" + "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/api" + providerschema "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/schema" + acctest "github.com/couchbasecloud/terraform-provider-couchbase-capella/internal/testing" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) diff --git a/internal/resources/allowlist.go b/internal/resources/allowlist.go index c6f46b6c..32816945 100644 --- a/internal/resources/allowlist.go +++ b/internal/resources/allowlist.go @@ -272,7 +272,7 @@ func (r *AllowList) ImportState(ctx context.Context, req resource.ImportStateReq } // getAllowList is used to retrieve an existing allow list. -func (r *AllowList) getAllowList(_ context.Context, organizationId, projectId, clusterId, allowListId string) (*api.GetAllowListResponse, error) { +func (r *AllowList) getAllowList(ctx context.Context, organizationId, projectId, clusterId, allowListId string) (*api.GetAllowListResponse, error) { url := fmt.Sprintf( "%s/v4/organizations/%s/projects/%s/clusters/%s/allowedcidrs/%s", r.HostURL, diff --git a/internal/resources/appservice.go b/internal/resources/appservice.go index 4de780e7..3d94cc8f 100644 --- a/internal/resources/appservice.go +++ b/internal/resources/appservice.go @@ -507,7 +507,7 @@ func (a *AppService) checkAppServiceStatus(ctx context.Context, organizationId, } // getAppService retrieves app service information from the specified organization, project and cluster -// using the provided app service ID by open-api call +// using the provided app service ID by open-api call. func (a *AppService) getAppService(ctx context.Context, organizationId, projectId, clusterId, appServiceId string) (*appservice.GetAppServiceResponse, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/appservices/%s", a.HostURL, organizationId, projectId, clusterId, appServiceId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} diff --git a/internal/resources/attributes.go b/internal/resources/attributes.go index 9d90581c..b923dd6e 100644 --- a/internal/resources/attributes.go +++ b/internal/resources/attributes.go @@ -181,7 +181,7 @@ func float64Attribute(fields ...string) *schema.Float64Attribute { return &attribute } -// float64DefaultAttribute sets the default values for an float field and returns the float64 attribute +// float64DefaultAttribute sets the default values for an float field and returns the float64 attribute. func float64DefaultAttribute(defaultValue float64, fields ...string) *schema.Float64Attribute { attribute := float64Attribute(fields...) attribute.Default = float64default.StaticFloat64(defaultValue) diff --git a/internal/resources/backup.go b/internal/resources/backup.go index b96c4884..170127d7 100644 --- a/internal/resources/backup.go +++ b/internal/resources/backup.go @@ -488,7 +488,7 @@ func (b *Backup) retrieveBackup(ctx context.Context, organizationId, projectId, } // getLatestBackup retrieves the latest backup information for a specified bucket in a cluster -// from the specified organization, project and cluster using the provided bucket ID by open-api call +// from the specified organization, project and cluster using the provided bucket ID by open-api call. func (b *Backup) getLatestBackup(ctx context.Context, organizationId, projectId, clusterId, bucketId string) (*backupapi.GetBackupResponse, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/backups", b.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} diff --git a/internal/resources/bucket.go b/internal/resources/bucket.go index 1d64dcf0..cd22729c 100644 --- a/internal/resources/bucket.go +++ b/internal/resources/bucket.go @@ -323,7 +323,7 @@ func (c *Bucket) ImportState(ctx context.Context, req resource.ImportStateReques } // retrieveBucket retrieves bucket information for a specified organization, project, cluster and bucket ID. -func (c *Bucket) retrieveBucket(_ context.Context, organizationId, projectId, clusterId, bucketId string) (*providerschema.OneBucket, error) { +func (c *Bucket) retrieveBucket(ctx context.Context, organizationId, projectId, clusterId, bucketId string) (*providerschema.OneBucket, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s/buckets/%s", c.HostURL, organizationId, projectId, clusterId, bucketId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} response, err := c.Client.ExecuteWithRetry( diff --git a/internal/resources/cluster.go b/internal/resources/cluster.go index 0e32be7c..0a0cbbfd 100644 --- a/internal/resources/cluster.go +++ b/internal/resources/cluster.go @@ -492,7 +492,7 @@ func (c *Cluster) ImportState(ctx context.Context, req resource.ImportStateReque } // getCluster retrieves cluster information from the specified organization and project -// using the provided cluster ID by open-api call +// using the provided cluster ID by open-api call. func (c *Cluster) getCluster(ctx context.Context, organizationId, projectId, clusterId string) (*clusterapi.GetClusterResponse, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s/clusters/%s", c.HostURL, organizationId, projectId, clusterId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} diff --git a/internal/resources/project.go b/internal/resources/project.go index 0f02f051..fc9861f8 100644 --- a/internal/resources/project.go +++ b/internal/resources/project.go @@ -339,7 +339,7 @@ func (r *Project) ImportState(ctx context.Context, req resource.ImportStateReque resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } -func (r *Project) retrieveProject(_ context.Context, organizationId, projectId string) (*providerschema.OneProject, error) { +func (r *Project) retrieveProject(ctx context.Context, organizationId, projectId string) (*providerschema.OneProject, error) { url := fmt.Sprintf("%s/v4/organizations/%s/projects/%s", r.HostURL, organizationId, projectId) cfg := api.EndpointCfg{Url: url, Method: http.MethodGet, SuccessStatus: http.StatusOK} response, err := r.Client.ExecuteWithRetry( diff --git a/internal/resources/user.go b/internal/resources/user.go index 468df2b6..bdc163db 100644 --- a/internal/resources/user.go +++ b/internal/resources/user.go @@ -35,7 +35,7 @@ const errorMessageAfterUserCreation = "User creation is successful, but encounte const errorMessageWhileUserCreation = "There is an error during user creation. Please check in Capella to see if any hanging resources" + " have been created, unexpected error: " -// User is the User resource implementation +// User is the User resource implementation. type User struct { *providerschema.Data } diff --git a/internal/schema/apikey.go b/internal/schema/apikey.go index 9dacf492..4b0d47a7 100644 --- a/internal/schema/apikey.go +++ b/internal/schema/apikey.go @@ -86,7 +86,7 @@ func NewApiKey(apiKey *api.GetApiKeyResponse, organizationId string, auditObject } // MorphAllowedCidrs is used to convert string list to basetypes.ListValue -// TODO : add unit testing +// TODO : add unit testing. func MorphAllowedCidrs(allowedCIDRs []string) (basetypes.SetValue, error) { var newAllowedCidr []attr.Value for _, allowedCidr := range allowedCIDRs {