diff --git a/internal/api/client.go b/internal/api/client.go index f93d1d96..426efb2e 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" @@ -49,7 +51,10 @@ type EndpointCfg struct { SuccessStatus int } -// Execute is used to construct and execute an HTTP request. +// defaultWaitAttempt re-attempt http request after 2 seconds. +const defaultWaitAttempt = time.Second * 2 + +// Execute is used to construct and execute a HTTP request. // It then returns the response. func (c *Client) Execute( endpointCfg EndpointCfg, @@ -102,3 +107,103 @@ 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) + } + if apiError.Code == 0 { + 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 * 10 + + 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, nil + case !goer.Is(err, errors.ErrGatewayTimeout): + return response, err + } + timer.Reset(waitOnReattempt) + } + } +} diff --git a/internal/api/pagination.go b/internal/api/pagination.go index 2541cc0f..bedd4e9c 100644 --- a/internal/api/pagination.go +++ b/internal/api/pagination.go @@ -84,7 +84,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 b13e0746..f19593ef 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 a74bc862..f1f6f78e 100644 --- a/internal/datasources/certificate.go +++ b/internal/datasources/certificate.go @@ -83,7 +83,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 2c5c4439..f7cb59de 100644 --- a/internal/datasources/organization.go +++ b/internal/datasources/organization.go @@ -72,7 +72,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/errors/errors.go b/internal/errors/errors.go index dfa862b2..df750567 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -149,4 +149,7 @@ var ( // ErrClusterCreationTimeoutAfterInitiation is returned when cluster creation // is timeout after initiation. ErrClusterCreationTimeoutAfterInitiation = errors.New("cluster creation status transition timed out after initiation") + + // ErrGatewayTimeout is returned when a gateway operation times out. + ErrGatewayTimeout = errors.New("gateway timeout") ) diff --git a/internal/resources/acceptance_tests/allowlist_acceptance_test.go b/internal/resources/acceptance_tests/allowlist_acceptance_test.go index 28671573..97f2d7c6 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" @@ -366,7 +367,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/apikey_acceptance_test.go b/internal/resources/acceptance_tests/apikey_acceptance_test.go new file mode 100644 index 00000000..41473a7d --- /dev/null +++ b/internal/resources/acceptance_tests/apikey_acceptance_test.go @@ -0,0 +1,388 @@ +package acceptance_tests_test + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + "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" +) + +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/acceptance_tests/cluster_acceptance_test.go b/internal/resources/acceptance_tests/cluster_acceptance_test.go index 4322f1d2..6b3fad5c 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"), @@ -627,30 +627,30 @@ 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"), 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 5c91a42b..36aa57bf 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" @@ -311,7 +312,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 7e20e21a..32816945 100644 --- a/internal/resources/allowlist.go +++ b/internal/resources/allowlist.go @@ -23,6 +23,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 @@ -83,7 +91,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, @@ -92,7 +101,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,16 +111,22 @@ 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 } + 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), + errorMessageAfterAllowListCreation+api.ParseError(err), ) return } @@ -223,7 +238,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, @@ -256,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, @@ -266,7 +282,8 @@ func (r *AllowList) getAllowList(_ context.Context, organizationId, projectId, c 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, @@ -317,3 +334,14 @@ 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()) + 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 844ca600..f74661bc 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 @@ -123,7 +131,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, @@ -131,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 } @@ -142,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) @@ -224,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) @@ -312,8 +291,9 @@ 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( + cfg := api.EndpointCfg{Url: url, Method: http.MethodPost, SuccessStatus: http.StatusCreated} + response, err := a.Client.ExecuteWithRetry( + ctx, cfg, rotateApiRequest, a.Token, @@ -352,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) @@ -411,7 +374,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 +405,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, @@ -482,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") } @@ -532,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() { @@ -545,3 +510,31 @@ 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 +} + +// 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() { + 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 ca5ddf66..3d94cc8f 100644 --- a/internal/resources/appservice.go +++ b/internal/resources/appservice.go @@ -27,6 +27,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 @@ -95,7 +103,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, @@ -104,7 +113,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 } @@ -114,24 +123,30 @@ 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 } + 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), + errorMessageAfterAppServiceCreationInitiation+api.ParseError(err), ) return } 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), + errorMessageAfterAppServiceCreationInitiation+api.ParseError(err), ) return } @@ -251,7 +266,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, @@ -323,7 +339,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, @@ -424,7 +441,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) } @@ -473,7 +490,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) { @@ -491,11 +508,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, @@ -513,3 +531,25 @@ func (a *AppService) getAppService(organizationId, projectId, clusterId, appServ appServiceResp.Etag = response.Response.Header.Get("ETag") 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") + 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 c5aacd78..b923dd6e 100644 --- a/internal/resources/attributes.go +++ b/internal/resources/attributes.go @@ -4,11 +4,13 @@ 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" "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" @@ -179,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 { @@ -206,6 +215,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 returns 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 e520d0a6..170127d7 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 @@ -71,7 +74,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 +90,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, @@ -96,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) @@ -117,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 } @@ -127,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 } @@ -269,7 +277,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, @@ -328,7 +337,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, @@ -419,7 +429,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. @@ -444,7 +454,8 @@ func (b *Backup) checkLatestBackupStatus(ctx context.Context, organizationId, pr func (b *Backup) retrieveBackup(ctx context.Context, organizationId, projectId, clusterId, 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, @@ -478,10 +489,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 109b76a5..a081dfe0 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 @@ -85,7 +93,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, @@ -94,16 +103,22 @@ 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 } + 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), + "Could not read Capella Backup Schedule for the bucket: %s "+bucketId+"."+errorMessageAfterBackupScheduleCreation+api.ParseError(err), ) return } @@ -216,7 +231,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 +298,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 +369,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 1e4ed2a6..cd22729c 100644 --- a/internal/resources/bucket.go +++ b/internal/resources/bucket.go @@ -24,6 +24,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 @@ -106,8 +114,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 } @@ -115,8 +123,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 } @@ -124,7 +132,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, @@ -133,7 +142,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 } @@ -143,16 +152,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 } @@ -279,7 +294,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, @@ -307,10 +323,11 @@ 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.Execute( + response, err := c.Client.ExecuteWithRetry( + ctx, cfg, nil, c.Token, @@ -387,7 +404,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, @@ -423,3 +441,17 @@ func (c *Bucket) Update(ctx context.Context, req resource.UpdateRequest, resp *r return } } + +// 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() { + 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 9227bbd4..0a0cbbfd 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 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 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: " + // 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, @@ -476,10 +493,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, @@ -500,7 +518,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) } @@ -543,11 +561,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) { @@ -717,3 +733,32 @@ func getCouchbaseServer(ctx context.Context, config tfsdk.Config, diags *diag.Di tflog.Info(ctx, fmt.Sprintf("couchbase_server: %+v", couchbaseServer)) 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") + 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.AppServiceId = 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 783b6d5a..a098f4c6 100644 --- a/internal/resources/cluster_schema.go +++ b/internal/resources/cluster_schema.go @@ -39,7 +39,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{ @@ -67,7 +67,7 @@ func ClusterSchema() schema.Schema { }, }, "num_of_nodes": int64Attribute(required), - "services": stringListAttribute(required), + "services": stringSetAttribute(required), }, }, }, @@ -88,7 +88,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 e7ee8415..91f34299 100644 --- a/internal/resources/database_credential.go +++ b/internal/resources/database_credential.go @@ -23,6 +23,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 +63,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 +118,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 +128,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 +138,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 } @@ -255,7 +269,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, @@ -274,13 +289,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. @@ -329,7 +344,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, @@ -366,7 +382,8 @@ func (r *DatabaseCredential) ImportState(ctx context.Context, req resource.Impor func (r *DatabaseCredential) retrieveDatabaseCredential(ctx context.Context, organizationId, projectId, clusterId, dbId string) (*providerschema.DatabaseCredential, 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, @@ -470,3 +487,14 @@ 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 != "" { + 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 df528fe3..af292181 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, requiresReplace), "cluster_id": stringAttribute(required, requiresReplace), "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 f759ace3..fc9861f8 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 @@ -87,7 +95,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, @@ -96,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 } @@ -106,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.AddError( + resp.Diagnostics.AddWarning( "Error creating project", - "Could not create project, unexpected error: "+api.ParseError(err), + errorMessageAfterProjectCreation+api.ParseError(err), ) return } @@ -217,7 +232,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, @@ -290,7 +306,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, @@ -322,10 +339,11 @@ 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.Execute( + response, err := r.Client.ExecuteWithRetry( + ctx, cfg, nil, r.Token, @@ -360,3 +378,16 @@ func (r *Project) retrieveProject(_ context.Context, organizationId, projectId s 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 36289e61..bdc163db 100644 --- a/internal/resources/user.go +++ b/internal/resources/user.go @@ -27,6 +27,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 @@ -101,7 +109,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, @@ -110,7 +119,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 } @@ -120,16 +129,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 } @@ -255,7 +270,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( @@ -446,11 +461,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, @@ -497,7 +513,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, @@ -519,7 +536,7 @@ func (r *User) Delete(ctx context.Context, req resource.DeleteRequest, resp *res } // getUser is used to retrieve an existing user. -func (r *User) getUser(_ context.Context, organizationId, userId string) (*api.GetUserResponse, error) { +func (r *User) getUser(ctx context.Context, organizationId, userId string) (*api.GetUserResponse, error) { url := fmt.Sprintf( "%s/v4/organizations/%s/users/%s", r.HostURL, @@ -528,7 +545,8 @@ func (r *User) getUser(_ context.Context, organizationId, userId string) (*api.G ) 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, @@ -595,3 +613,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 +} diff --git a/internal/resources/user_schema.go b/internal/resources/user_schema.go index d7cd802d..660e7f19 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), + "type": stringDefaultAttribute("project", optional, computed), "id": stringAttribute(required), - "roles": stringListAttribute(required), + "roles": stringSetAttribute(required), }, }, }, diff --git a/internal/schema/apikey.go b/internal/schema/apikey.go index 41cc6389..4b0d47a7 100644 --- a/internal/schema/apikey.go +++ b/internal/schema/apikey.go @@ -37,7 +37,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"` OrganizationId types.String `tfsdk:"organization_id"` Audit types.Object `tfsdk:"audit"` Description types.String `tfsdk:"description"` @@ -87,15 +87,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 diff --git a/internal/schema/bucket.go b/internal/schema/bucket.go index 87ca800d..cbe0fb69 100644 --- a/internal/schema/bucket.go +++ b/internal/schema/bucket.go @@ -5,6 +5,7 @@ import ( "github.com/couchbasecloud/terraform-provider-couchbase-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" ) @@ -131,6 +132,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.