From 6fd6cd6f889cd561af2dc36635a75e3398d63b54 Mon Sep 17 00:00:00 2001 From: Julien Duchesne Date: Thu, 29 Feb 2024 14:04:40 -0500 Subject: [PATCH] Convert Cloud resources to new "resource" framework - Add typing to the resource ID system. Makes it more robust and easier to use. - Makes sure all resources have an ID helper (to generate imports) - Paves the way for Terraform code gen --- docs/resources/cloud_stack.md | 3 +- docs/resources/cloud_stack_service_account.md | 8 ++ .../resources/grafana_cloud_stack/import.sh | 3 +- .../import.sh | 1 + internal/common/id.go | 84 ----------- internal/common/resource.go | 60 ++++++++ internal/common/resource_id.go | 133 ++++++++++++++++++ internal/provider/legacy_provider.go | 2 +- .../cloud/data_source_cloud_stack.go | 2 +- .../cloud/resource_cloud_access_policy.go | 89 +++++++----- .../resource_cloud_access_policy_token.go | 34 +++-- ...resource_cloud_access_policy_token_test.go | 10 +- .../resources/cloud/resource_cloud_api_key.go | 32 +++-- .../resources/cloud/resource_cloud_plugin.go | 30 ++-- .../resources/cloud/resource_cloud_stack.go | 43 ++++-- .../cloud/resource_cloud_stack_api_key.go | 10 +- .../resource_cloud_stack_service_account.go | 62 +++++--- ...source_cloud_stack_service_account_test.go | 21 +-- ...ource_cloud_stack_service_account_token.go | 20 ++- ...ource_synthetic_monitoring_installation.go | 11 +- internal/resources/cloud/resources.go | 29 ++-- 21 files changed, 457 insertions(+), 230 deletions(-) create mode 100644 examples/resources/grafana_cloud_stack_service_account/import.sh delete mode 100644 internal/common/id.go create mode 100644 internal/common/resource.go create mode 100644 internal/common/resource_id.go diff --git a/docs/resources/cloud_stack.md b/docs/resources/cloud_stack.md index 6a2b414bb..49e092439 100644 --- a/docs/resources/cloud_stack.md +++ b/docs/resources/cloud_stack.md @@ -86,6 +86,5 @@ resource "grafana_cloud_stack" "test" { Import is supported using the following syntax: ```shell -terraform import grafana_cloud_stack.stack_name {{stack_id}} // import by numerical ID -terraform import grafana_cloud_stack.stack_name {{stack_slug}} // or import by slug +terraform import grafana_cloud_stack.name "{{ stackSlugOrID }}" ``` diff --git a/docs/resources/cloud_stack_service_account.md b/docs/resources/cloud_stack_service_account.md index e0145e3a8..cb1d376b0 100644 --- a/docs/resources/cloud_stack_service_account.md +++ b/docs/resources/cloud_stack_service_account.md @@ -50,3 +50,11 @@ resource "grafana_cloud_stack_service_account" "cloud_sa" { ### Read-Only - `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import grafana_cloud_stack_service_account.name "{{ stackSlug }}:{{ serviceAccountID }}" +``` diff --git a/examples/resources/grafana_cloud_stack/import.sh b/examples/resources/grafana_cloud_stack/import.sh index f5f7c55a1..9bcb10685 100644 --- a/examples/resources/grafana_cloud_stack/import.sh +++ b/examples/resources/grafana_cloud_stack/import.sh @@ -1,2 +1 @@ -terraform import grafana_cloud_stack.stack_name {{stack_id}} // import by numerical ID -terraform import grafana_cloud_stack.stack_name {{stack_slug}} // or import by slug +terraform import grafana_cloud_stack.name "{{ stackSlugOrID }}" diff --git a/examples/resources/grafana_cloud_stack_service_account/import.sh b/examples/resources/grafana_cloud_stack_service_account/import.sh new file mode 100644 index 000000000..3e23832c8 --- /dev/null +++ b/examples/resources/grafana_cloud_stack_service_account/import.sh @@ -0,0 +1 @@ +terraform import grafana_cloud_stack_service_account.name "{{ stackSlug }}:{{ serviceAccountID }}" diff --git a/internal/common/id.go b/internal/common/id.go deleted file mode 100644 index 1e4ba7a54..000000000 --- a/internal/common/id.go +++ /dev/null @@ -1,84 +0,0 @@ -package common - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" -) - -var ( - defaultSeparator = ":" - allIDs = []*TFID{} -) - -type TFID struct { - resourceName string - separators []string - expectedFields []string -} - -func NewTFID(resourceName string, expectedFields ...string) *TFID { - return newTFIDWithSeparators(resourceName, []string{defaultSeparator}, expectedFields...) -} - -// Deprecated: Use NewTFID instead -// We should standardize on a single separator, so that function should only be used for old resources -// On major versions, switch to NewTFID and remove uses of this function -func NewTFIDWithLegacySeparator(resourceName, legacySeparator string, expectedFields ...string) *TFID { - return newTFIDWithSeparators(resourceName, []string{defaultSeparator, legacySeparator}, expectedFields...) -} - -func newTFIDWithSeparators(resourceName string, separators []string, expectedFields ...string) *TFID { - tfID := &TFID{ - resourceName: resourceName, - separators: separators, - expectedFields: expectedFields, - } - allIDs = append(allIDs, tfID) - return tfID -} - -func (id *TFID) Example() string { - fields := make([]string, len(id.expectedFields)) - for i := range fields { - fields[i] = fmt.Sprintf("{{ %s }}", id.expectedFields[i]) - } - return fmt.Sprintf(`terraform import %s.name %q -`, id.resourceName, strings.Join(fields, defaultSeparator)) -} - -func (id *TFID) Make(parts ...any) string { - if len(parts) != len(id.expectedFields) { - panic(fmt.Sprintf("expected %d fields, got %d", len(id.expectedFields), len(parts))) // This is a coding error, so panic is appropriate - } - stringParts := make([]string, len(parts)) - for i, part := range parts { - stringParts[i] = fmt.Sprintf("%v", part) - } - return strings.Join(stringParts, defaultSeparator) -} - -func (id *TFID) Split(resourceID string) ([]string, error) { - for _, sep := range id.separators { - parts := strings.Split(resourceID, sep) - if len(parts) == len(id.expectedFields) { - return parts, nil - } - } - return nil, fmt.Errorf("id %q does not match expected format. Should be in the format: %s", resourceID, strings.Join(id.expectedFields, defaultSeparator)) -} - -// GenerateImportFiles generates import files for all resources that use a helper defined in this package -func GenerateImportFiles(path string) error { - for _, id := range allIDs { - resourcePath := filepath.Join(path, "resources", id.resourceName, "import.sh") - log.Printf("Generating import file for %s (writing to %s)\n", id.resourceName, resourcePath) - err := os.WriteFile(resourcePath, []byte(id.Example()), 0600) - if err != nil { - return err - } - } - return nil -} diff --git a/internal/common/resource.go b/internal/common/resource.go new file mode 100644 index 000000000..03d4d9455 --- /dev/null +++ b/internal/common/resource.go @@ -0,0 +1,60 @@ +package common + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var allResources = []*Resource{} + +type Resource struct { + Name string + IDType *ResourceID + Schema *schema.Resource +} + +func NewResource(name string, idType *ResourceID, schema *schema.Resource) *Resource { + r := &Resource{ + Name: name, + IDType: idType, + Schema: schema, + } + allResources = append(allResources, r) + return r +} + +func (r *Resource) ImportExample() string { + id := r.IDType + fields := make([]string, len(id.expectedFields)) + for i := range fields { + fields[i] = fmt.Sprintf("{{ %s }}", id.expectedFields[i].Name) + } + return fmt.Sprintf(`terraform import %s.name %q +`, r.Name, strings.Join(fields, defaultSeparator)) +} + +// GenerateImportFiles generates import files for all resources that use a helper defined in this package +func GenerateImportFiles(path string) error { + for _, r := range allResources { + resourcePath := filepath.Join(path, "resources", r.Name, "import.sh") + if err := os.RemoveAll(resourcePath); err != nil { // Remove the file if it exists + return err + } + + if r.IDType == nil { + log.Printf("Skipping import file generation for %s because it does not have an ID type\n", r.Name) + continue + } + + log.Printf("Generating import file for %s (writing to %s)\n", r.Name, resourcePath) + if err := os.WriteFile(resourcePath, []byte(r.ImportExample()), 0600); err != nil { + return err + } + } + return nil +} diff --git a/internal/common/resource_id.go b/internal/common/resource_id.go new file mode 100644 index 000000000..033ff28a8 --- /dev/null +++ b/internal/common/resource_id.go @@ -0,0 +1,133 @@ +package common + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +type ResourceIDFieldType string + +const ( + defaultSeparator = ":" + ResourceIDFieldTypeInt = ResourceIDFieldType("int") + ResourceIDFieldTypeString = ResourceIDFieldType("string") +) + +type ResourceIDField struct { + Name string + Type ResourceIDFieldType + // Optional bool // Unimplemented. Will be used for org ID +} + +func StringIDField(name string) ResourceIDField { + return ResourceIDField{ + Name: name, + Type: ResourceIDFieldTypeString, + } +} + +func IntIDField(name string) ResourceIDField { + return ResourceIDField{ + Name: name, + Type: ResourceIDFieldTypeInt, + } +} + +type ResourceID struct { + separators []string + expectedFields []ResourceIDField +} + +func NewResourceID(expectedFields ...ResourceIDField) *ResourceID { + return newResourceIDWithSeparators([]string{defaultSeparator}, expectedFields...) +} + +// Deprecated: Use NewResourceID instead +// We should standardize on a single separator, so that function should only be used for old resources +// On major versions, switch to NewResourceID and remove uses of this function +func NewResourceIDWithLegacySeparator(legacySeparator string, expectedFields ...ResourceIDField) *ResourceID { + return newResourceIDWithSeparators([]string{defaultSeparator, legacySeparator}, expectedFields...) +} + +func newResourceIDWithSeparators(separators []string, expectedFields ...ResourceIDField) *ResourceID { + tfID := &ResourceID{ + separators: separators, + expectedFields: expectedFields, + } + return tfID +} + +// Make creates a resource ID from the given parts +// The parts must have the correct number of fields and types +func (id *ResourceID) Make(parts ...any) string { + if len(parts) != len(id.expectedFields) { + panic(fmt.Sprintf("expected %d fields, got %d", len(id.expectedFields), len(parts))) // This is a coding error, so panic is appropriate + } + stringParts := make([]string, len(parts)) + for i, part := range parts { + // Unwrap pointers + if reflect.ValueOf(part).Kind() == reflect.Ptr { + part = reflect.ValueOf(part).Elem().Interface() + } + expectedField := id.expectedFields[i] + switch expectedField.Type { + case ResourceIDFieldTypeInt: + asInt, ok := part.(int64) + if !ok { + panic(fmt.Sprintf("expected int64 for field %q, got %T", expectedField.Name, part)) // This is a coding error, so panic is appropriate + } + stringParts[i] = strconv.FormatInt(asInt, 10) + case ResourceIDFieldTypeString: + asString, ok := part.(string) + if !ok { + panic(fmt.Sprintf("expected string for field %q, got %T", expectedField.Name, part)) // This is a coding error, so panic is appropriate + } + stringParts[i] = asString + } + } + + return strings.Join(stringParts, defaultSeparator) +} + +// Single parses a resource ID into a single value +func (id *ResourceID) Single(resourceID string) (any, error) { + parts, err := id.Split(resourceID) + if err != nil { + return nil, err + } + return parts[0], nil +} + +// Split parses a resource ID into its parts +// The parts will be cast to the expected types +func (id *ResourceID) Split(resourceID string) ([]any, error) { + for _, sep := range id.separators { + parts := strings.Split(resourceID, sep) + if len(parts) == len(id.expectedFields) { + partsAsAny := make([]any, len(parts)) + for i, part := range parts { + expectedField := id.expectedFields[i] + switch expectedField.Type { + case ResourceIDFieldTypeInt: + asInt, err := strconv.ParseInt(part, 10, 64) + if err != nil { + return nil, fmt.Errorf("expected int for field %q, got %q", expectedField.Name, part) + } + partsAsAny[i] = asInt + case ResourceIDFieldTypeString: + partsAsAny[i] = part + } + } + + return partsAsAny, nil + } + } + + expectedFieldNames := make([]string, len(id.expectedFields)) + for i, f := range id.expectedFields { + expectedFieldNames[i] = f.Name + } + return nil, fmt.Errorf("id %q does not match expected format. Should be in the format: %s", resourceID, strings.Join(expectedFieldNames, defaultSeparator)) +} diff --git a/internal/provider/legacy_provider.go b/internal/provider/legacy_provider.go index ab5ce9b68..2d7e66f71 100644 --- a/internal/provider/legacy_provider.go +++ b/internal/provider/legacy_provider.go @@ -259,7 +259,7 @@ func Provider(version string) *schema.Provider { grafanaClientResources, smClientResources, onCallClientResources, - cloud.ResourcesMap, + cloud.ResourcesMap(), ), DataSourcesMap: mergeResourceMaps( diff --git a/internal/resources/cloud/data_source_cloud_stack.go b/internal/resources/cloud/data_source_cloud_stack.go index 4b19691c4..c4df2cb9d 100644 --- a/internal/resources/cloud/data_source_cloud_stack.go +++ b/internal/resources/cloud/data_source_cloud_stack.go @@ -13,7 +13,7 @@ func datasourceStack() *schema.Resource { return &schema.Resource{ Description: "Data source for Grafana Stack", ReadContext: withClient[schema.ReadContextFunc](datasourceStackRead), - Schema: common.CloneResourceSchemaForDatasource(resourceStack(), map[string]*schema.Schema{ + Schema: common.CloneResourceSchemaForDatasource(resourceStack().Schema, map[string]*schema.Schema{ "slug": { Type: schema.TypeString, Required: true, diff --git a/internal/resources/cloud/resource_cloud_access_policy.go b/internal/resources/cloud/resource_cloud_access_policy.go index 6a08a17fe..4e3ccf294 100644 --- a/internal/resources/cloud/resource_cloud_access_policy.go +++ b/internal/resources/cloud/resource_cloud_access_policy.go @@ -13,11 +13,45 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -var ResourceAccessPolicyID = common.NewTFIDWithLegacySeparator("grafana_cloud_access_policy", "/", "region", "policyId") //nolint:staticcheck +var ( + //nolint:staticcheck + resourceAccessPolicyID = common.NewResourceIDWithLegacySeparator("/", + common.StringIDField("region"), + common.StringIDField("policyId"), + ) +) -func resourceAccessPolicy() *schema.Resource { - return &schema.Resource{ +func resourceAccessPolicy() *common.Resource { + cloudAccessPolicyRealmSchema := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: "Whether a policy applies to a Cloud org or a specific stack. Should be one of `org` or `stack`.", + ValidateFunc: validation.StringInSlice([]string{"org", "stack"}, false), + }, + "identifier": { + Type: schema.TypeString, + Required: true, + Description: "The identifier of the org or stack. For orgs, this is the slug, for stacks, this is the stack ID.", + }, + "label_policy": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "selector": { + Type: schema.TypeString, + Required: true, + Description: "The label selector to match in metrics or logs query. Should be in PromQL or LogQL format.", + }, + }, + }, + }, + }, + } + schema := &schema.Resource{ Description: ` * [Official documentation](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) * [API documentation](https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/cloud-api/#create-an-access-policy) @@ -95,35 +129,12 @@ Required access policy scopes: }, }, } -} -var cloudAccessPolicyRealmSchema = &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - Description: "Whether a policy applies to a Cloud org or a specific stack. Should be one of `org` or `stack`.", - ValidateFunc: validation.StringInSlice([]string{"org", "stack"}, false), - }, - "identifier": { - Type: schema.TypeString, - Required: true, - Description: "The identifier of the org or stack. For orgs, this is the slug, for stacks, this is the stack ID.", - }, - "label_policy": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "selector": { - Type: schema.TypeString, - Required: true, - Description: "The label selector to match in metrics or logs query. Should be in PromQL or LogQL format.", - }, - }, - }, - }, - }, + return common.NewResource( + "grafana_cloud_access_policy", + resourceAccessPolicyID, + schema, + ) } func createCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { @@ -146,13 +157,13 @@ func createCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client return apiError(err) } - d.SetId(ResourceAccessPolicyID.Make(region, result.Id)) + d.SetId(resourceAccessPolicyID.Make(region, result.Id)) return readCloudAccessPolicy(ctx, d, client) } func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAccessPolicyID.Split(d.Id()) + split, err := resourceAccessPolicyID.Split(d.Id()) if err != nil { return diag.FromErr(err) } @@ -163,7 +174,7 @@ func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client displayName = d.Get("name").(string) } - req := client.AccesspoliciesAPI.PostAccessPolicy(ctx, id).Region(region).XRequestId(ClientRequestID()). + req := client.AccesspoliciesAPI.PostAccessPolicy(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()). PostAccessPolicyRequest(gcom.PostAccessPolicyRequest{ DisplayName: &displayName, Scopes: common.ListToStringSlice(d.Get("scopes").(*schema.Set).List()), @@ -177,13 +188,13 @@ func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client } func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAccessPolicyID.Split(d.Id()) + split, err := resourceAccessPolicyID.Split(d.Id()) if err != nil { return diag.FromErr(err) } region, id := split[0], split[1] - result, _, err := client.AccesspoliciesAPI.GetAccessPolicy(ctx, id).Region(region).Execute() + result, _, err := client.AccesspoliciesAPI.GetAccessPolicy(ctx, id.(string)).Region(region.(string)).Execute() if err, shouldReturn := common.CheckReadError("access policy", d, err); shouldReturn { return err } @@ -198,19 +209,19 @@ func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client * if updated := result.UpdatedAt; updated != nil { d.Set("updated_at", updated.Format(time.RFC3339)) } - d.SetId(ResourceAccessPolicyID.Make(region, result.Id)) + d.SetId(resourceAccessPolicyID.Make(region, result.Id)) return nil } func deleteCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAccessPolicyID.Split(d.Id()) + split, err := resourceAccessPolicyID.Split(d.Id()) if err != nil { return diag.FromErr(err) } region, id := split[0], split[1] - _, _, err = client.AccesspoliciesAPI.DeleteAccessPolicy(ctx, id).Region(region).XRequestId(ClientRequestID()).Execute() + _, _, err = client.AccesspoliciesAPI.DeleteAccessPolicy(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()).Execute() return apiError(err) } diff --git a/internal/resources/cloud/resource_cloud_access_policy_token.go b/internal/resources/cloud/resource_cloud_access_policy_token.go index 269c39f32..8f07a1965 100644 --- a/internal/resources/cloud/resource_cloud_access_policy_token.go +++ b/internal/resources/cloud/resource_cloud_access_policy_token.go @@ -11,10 +11,16 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -var ResourceAccessPolicyTokenID = common.NewTFIDWithLegacySeparator("grafana_cloud_access_policy_token", "/", "region", "tokenId") //nolint:staticcheck +var ( + //nolint:staticcheck + resourceAccessPolicyTokenID = common.NewResourceIDWithLegacySeparator("/", + common.StringIDField("region"), + common.StringIDField("tokenId"), + ) +) -func resourceAccessPolicyToken() *schema.Resource { - return &schema.Resource{ +func resourceAccessPolicyToken() *common.Resource { + schema := &schema.Resource{ Description: ` * [Official documentation](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) @@ -92,6 +98,12 @@ Required access policy scopes: }, }, } + + return common.NewResource( + "grafana_cloud_access_policy_token", + resourceAccessPolicyTokenID, + schema, + ) } func createCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { @@ -117,14 +129,14 @@ func createCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c return apiError(err) } - d.SetId(ResourceAccessPolicyTokenID.Make(region, result.Id)) + d.SetId(resourceAccessPolicyTokenID.Make(region, result.Id)) d.Set("token", result.Token) return readCloudAccessPolicyToken(ctx, d, client) } func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAccessPolicyTokenID.Split(d.Id()) + split, err := resourceAccessPolicyTokenID.Split(d.Id()) if err != nil { return diag.FromErr(err) } @@ -135,7 +147,7 @@ func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c displayName = d.Get("name").(string) } - req := client.TokensAPI.PostToken(ctx, id).Region(region).XRequestId(ClientRequestID()).PostTokenRequest(gcom.PostTokenRequest{ + req := client.TokensAPI.PostToken(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()).PostTokenRequest(gcom.PostTokenRequest{ DisplayName: &displayName, }) if _, _, err := req.Execute(); err != nil { @@ -146,13 +158,13 @@ func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c } func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAccessPolicyTokenID.Split(d.Id()) + split, err := resourceAccessPolicyTokenID.Split(d.Id()) if err != nil { return diag.FromErr(err) } region, id := split[0], split[1] - result, _, err := client.TokensAPI.GetToken(ctx, id).Region(region).Execute() + result, _, err := client.TokensAPI.GetToken(ctx, id.(string)).Region(region.(string)).Execute() if err, shouldReturn := common.CheckReadError("policy token", d, err); shouldReturn { return err } @@ -168,18 +180,18 @@ func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, cli if result.UpdatedAt != nil { d.Set("updated_at", result.UpdatedAt.Format(time.RFC3339)) } - d.SetId(ResourceAccessPolicyTokenID.Make(region, result.Id)) + d.SetId(resourceAccessPolicyTokenID.Make(region, result.Id)) return nil } func deleteCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAccessPolicyTokenID.Split(d.Id()) + split, err := resourceAccessPolicyTokenID.Split(d.Id()) if err != nil { return diag.FromErr(err) } region, id := split[0], split[1] - _, _, err = client.TokensAPI.DeleteToken(ctx, id).Region(region).XRequestId(ClientRequestID()).Execute() + _, _, err = client.TokensAPI.DeleteToken(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()).Execute() return apiError(err) } diff --git a/internal/resources/cloud/resource_cloud_access_policy_token_test.go b/internal/resources/cloud/resource_cloud_access_policy_token_test.go index bb0d8b983..3edcad190 100644 --- a/internal/resources/cloud/resource_cloud_access_policy_token_test.go +++ b/internal/resources/cloud/resource_cloud_access_policy_token_test.go @@ -154,7 +154,7 @@ func testAccCloudAccessPolicyCheckExists(rn string, a *gcom.AuthAccessPolicy) re return fmt.Errorf("resource id not set") } - region, id, _ := strings.Cut(rs.Primary.ID, "/") + region, id, _ := strings.Cut(rs.Primary.ID, ":") client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI policy, _, err := client.AccesspoliciesAPI.GetAccessPolicy(context.Background(), id).Region(region).Execute() @@ -179,7 +179,7 @@ func testAccCloudAccessPolicyTokenCheckExists(rn string, a *gcom.AuthToken) reso return fmt.Errorf("resource id not set") } - region, id, _ := strings.Cut(rs.Primary.ID, "/") + region, id, _ := strings.Cut(rs.Primary.ID, ":") client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI token, _, err := client.TokensAPI.GetToken(context.Background(), id).Region(region).Execute() @@ -195,6 +195,9 @@ func testAccCloudAccessPolicyTokenCheckExists(rn string, a *gcom.AuthToken) reso func testAccCloudAccessPolicyCheckDestroy(region string, a *gcom.AuthAccessPolicy) resource.TestCheckFunc { return func(s *terraform.State) error { + if a == nil { + return nil + } client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI policy, _, err := client.AccesspoliciesAPI.GetAccessPolicy(context.Background(), *a.Id).Region(region).Execute() if err == nil && policy.Name != "" { @@ -207,6 +210,9 @@ func testAccCloudAccessPolicyCheckDestroy(region string, a *gcom.AuthAccessPolic func testAccCloudAccessPolicyTokenCheckDestroy(region string, a *gcom.AuthToken) resource.TestCheckFunc { return func(s *terraform.State) error { + if a == nil { + return nil + } client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI token, _, err := client.TokensAPI.GetToken(context.Background(), *a.Id).Region(region).Execute() if err == nil && token.Name != "" { diff --git a/internal/resources/cloud/resource_cloud_api_key.go b/internal/resources/cloud/resource_cloud_api_key.go index 932a5581d..4e7084189 100644 --- a/internal/resources/cloud/resource_cloud_api_key.go +++ b/internal/resources/cloud/resource_cloud_api_key.go @@ -11,11 +11,17 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -var ResourceAPIKeyID = common.NewTFIDWithLegacySeparator("grafana_cloud_api_key", "-", "orgSlug", "apiKeyName") //nolint:staticcheck -var cloudAPIKeyRoles = []string{"Viewer", "Editor", "Admin", "MetricsPublisher", "PluginPublisher"} +var ( + cloudAPIKeyRoles = []string{"Viewer", "Editor", "Admin", "MetricsPublisher", "PluginPublisher"} + //nolint:staticcheck + resourceAPIKeyID = common.NewResourceIDWithLegacySeparator("-", + common.StringIDField("orgSlug"), + common.StringIDField("apiKeyName"), + ) +) -func resourceAPIKey() *schema.Resource { - return &schema.Resource{ +func resourceAPIKey() *common.Resource { + schema := &schema.Resource{ Description: `This resource is deprecated and will be removed in a future release. Please use grafana_cloud_access_policy instead. Manages a single API key on the Grafana Cloud portal (on the organization level) @@ -63,6 +69,12 @@ Required access policy scopes: }, }, } + + return common.NewResource( + "grafana_cloud_api_key", + resourceAPIKeyID, + schema, + ) } func resourceAPIKeyCreate(ctx context.Context, d *schema.ResourceData, c *gcom.APIClient) diag.Diagnostics { @@ -81,19 +93,19 @@ func resourceAPIKeyCreate(ctx context.Context, d *schema.ResourceData, c *gcom.A } d.Set("key", *resp.Token) - d.SetId(ResourceAPIKeyID.Make(org, resp.Name)) + d.SetId(resourceAPIKeyID.Make(org, resp.Name)) return resourceAPIKeyRead(ctx, d, c) } func resourceAPIKeyRead(ctx context.Context, d *schema.ResourceData, c *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAPIKeyID.Split(d.Id()) + split, err := resourceAPIKeyID.Split(d.Id()) if err != nil { return diag.FromErr(err) } org, name := split[0], split[1] - resp, _, err := c.OrgsAPI.GetApiKey(ctx, name, org).Execute() + resp, _, err := c.OrgsAPI.GetApiKey(ctx, name.(string), org.(string)).Execute() if err != nil { return apiError(err) } @@ -101,19 +113,19 @@ func resourceAPIKeyRead(ctx context.Context, d *schema.ResourceData, c *gcom.API d.Set("name", resp.Name) d.Set("role", resp.Role) d.Set("cloud_org_slug", org) - d.SetId(ResourceAPIKeyID.Make(org, resp.Name)) + d.SetId(resourceAPIKeyID.Make(org, resp.Name)) return nil } func resourceAPIKeyDelete(ctx context.Context, d *schema.ResourceData, c *gcom.APIClient) diag.Diagnostics { - split, err := ResourceAPIKeyID.Split(d.Id()) + split, err := resourceAPIKeyID.Split(d.Id()) if err != nil { return diag.FromErr(err) } org, name := split[0], split[1] - _, err = c.OrgsAPI.DelApiKey(ctx, name, org).XRequestId(ClientRequestID()).Execute() + _, err = c.OrgsAPI.DelApiKey(ctx, name.(string), org.(string)).XRequestId(ClientRequestID()).Execute() d.SetId("") return apiError(err) } diff --git a/internal/resources/cloud/resource_cloud_plugin.go b/internal/resources/cloud/resource_cloud_plugin.go index e78980b63..fc83cc672 100644 --- a/internal/resources/cloud/resource_cloud_plugin.go +++ b/internal/resources/cloud/resource_cloud_plugin.go @@ -9,10 +9,16 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -var ResourcePluginInstallationID = common.NewTFIDWithLegacySeparator("grafana_cloud_plugin_installation", "_", "stackSlug", "pluginSlug") //nolint:staticcheck +var ( + //nolint:staticcheck + resourcePluginInstallationID = common.NewResourceIDWithLegacySeparator("_", + common.StringIDField("stackSlug"), + common.StringIDField("pluginSlug"), + ) +) -func resourcePluginInstallation() *schema.Resource { - return &schema.Resource{ +func resourcePluginInstallation() *common.Resource { + schema := &schema.Resource{ Description: ` Manages Grafana Cloud Plugin Installations. @@ -52,6 +58,12 @@ Required access policy scopes: StateContext: schema.ImportStatePassthroughContext, }, } + + return common.NewResource( + "grafana_cloud_plugin_installation", + resourcePluginInstallationID, + schema, + ) } func resourcePluginInstallationCreate(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { @@ -69,19 +81,19 @@ func resourcePluginInstallationCreate(ctx context.Context, d *schema.ResourceDat return apiError(err) } - d.SetId(ResourcePluginInstallationID.Make(stackSlug, pluginSlug)) + d.SetId(resourcePluginInstallationID.Make(stackSlug, pluginSlug)) return nil } func resourcePluginInstallationRead(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourcePluginInstallationID.Split(d.Id()) + split, err := resourcePluginInstallationID.Split(d.Id()) if err != nil { return diag.FromErr(err) } stackSlug, pluginSlug := split[0], split[1] - installation, _, err := client.InstancesAPI.GetInstancePlugin(ctx, stackSlug, pluginSlug).Execute() + installation, _, err := client.InstancesAPI.GetInstancePlugin(ctx, stackSlug.(string), pluginSlug.(string)).Execute() if err, shouldReturn := common.CheckReadError("plugin", d, err); shouldReturn { return err } @@ -89,18 +101,18 @@ func resourcePluginInstallationRead(ctx context.Context, d *schema.ResourceData, d.Set("stack_slug", installation.InstanceSlug) d.Set("slug", installation.PluginSlug) d.Set("version", installation.Version) - d.SetId(ResourcePluginInstallationID.Make(stackSlug, pluginSlug)) + d.SetId(resourcePluginInstallationID.Make(stackSlug, pluginSlug)) return nil } func resourcePluginInstallationDelete(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - split, err := ResourcePluginInstallationID.Split(d.Id()) + split, err := resourcePluginInstallationID.Split(d.Id()) if err != nil { return diag.FromErr(err) } stackSlug, pluginSlug := split[0], split[1] - _, _, err = client.InstancesAPI.DeleteInstancePlugin(ctx, stackSlug, pluginSlug).XRequestId(ClientRequestID()).Execute() + _, _, err = client.InstancesAPI.DeleteInstancePlugin(ctx, stackSlug.(string), pluginSlug.(string)).XRequestId(ClientRequestID()).Execute() return apiError(err) } diff --git a/internal/resources/cloud/resource_cloud_stack.go b/internal/resources/cloud/resource_cloud_stack.go index c6e2e8167..4d89851cf 100644 --- a/internal/resources/cloud/resource_cloud_stack.go +++ b/internal/resources/cloud/resource_cloud_stack.go @@ -26,11 +26,11 @@ const defaultReadinessTimeout = time.Minute * 5 var ( stackLabelRegex = regexp.MustCompile(`^[a-zA-Z0-9/\-.]+$`) stackSlugRegex = regexp.MustCompile(`^[a-z][a-z0-9]+$`) + resourceStackID = common.NewResourceID(common.StringIDField("stackSlugOrID")) ) -func resourceStack() *schema.Resource { - return &schema.Resource{ - +func resourceStack() *common.Resource { + schema := &schema.Resource{ Description: ` * [Official documentation](https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/cloud-api/#stacks/) @@ -198,6 +198,12 @@ Required access policy scopes: }), ), } + + return common.NewResource( + "grafana_cloud_stack", + resourceStackID, + schema, + ) } func createStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { @@ -248,6 +254,11 @@ func createStack(ctx context.Context, d *schema.ResourceData, client *gcom.APICl } func updateStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { + id, err := resourceStackID.Single(d.Id()) + if err != nil { + return diag.FromErr(err) + } + // Default to the slug if the URL is not set url := d.Get("url").(string) if url == "" { @@ -261,8 +272,8 @@ func updateStack(ctx context.Context, d *schema.ResourceData, client *gcom.APICl Url: &url, Labels: common.Ref(common.UnpackMap[string](d.Get("labels"))), } - req := client.InstancesAPI.PostInstance(ctx, d.Id()).PostInstanceRequest(stack).XRequestId(ClientRequestID()) - _, _, err := req.Execute() + req := client.InstancesAPI.PostInstance(ctx, id.(string)).PostInstanceRequest(stack).XRequestId(ClientRequestID()) + _, _, err = req.Execute() if err != nil { return apiError(err) } @@ -275,13 +286,23 @@ func updateStack(ctx context.Context, d *schema.ResourceData, client *gcom.APICl } func deleteStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - req := client.InstancesAPI.DeleteInstance(ctx, d.Id()).XRequestId(ClientRequestID()) - _, _, err := req.Execute() + id, err := resourceStackID.Single(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + req := client.InstancesAPI.DeleteInstance(ctx, id.(string)).XRequestId(ClientRequestID()) + _, _, err = req.Execute() return apiError(err) } func readStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics { - req := client.InstancesAPI.GetInstance(ctx, d.Id()) + id, err := resourceStackID.Single(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + req := client.InstancesAPI.GetInstance(ctx, id.(string)) stack, _, err := req.Execute() if err, shouldReturn := common.CheckReadError("stack", d, err); shouldReturn { return err @@ -293,13 +314,13 @@ func readStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClie return nil } - connectionsReq := client.InstancesAPI.GetConnections(ctx, d.Id()) + connectionsReq := client.InstancesAPI.GetConnections(ctx, id.(string)) connections, _, err := connectionsReq.Execute() if err != nil { return apiError(err) } - if err := FlattenStack(d, stack, connections); err != nil { + if err := flattenStack(d, stack, connections); err != nil { return diag.FromErr(err) } // Always set the wait attribute to true after creation @@ -312,7 +333,7 @@ func readStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClie return nil } -func FlattenStack(d *schema.ResourceData, stack *gcom.FormattedApiInstance, connections *gcom.FormattedApiInstanceConnections) error { +func flattenStack(d *schema.ResourceData, stack *gcom.FormattedApiInstance, connections *gcom.FormattedApiInstanceConnections) error { id := strconv.FormatInt(int64(stack.Id), 10) d.SetId(id) d.Set("name", stack.Name) diff --git a/internal/resources/cloud/resource_cloud_stack_api_key.go b/internal/resources/cloud/resource_cloud_stack_api_key.go index b96834338..dfdeba1c8 100644 --- a/internal/resources/cloud/resource_cloud_stack_api_key.go +++ b/internal/resources/cloud/resource_cloud_stack_api_key.go @@ -17,8 +17,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -func resourceStackAPIKey() *schema.Resource { - return &schema.Resource{ +func resourceStackAPIKey() *common.Resource { + schema := &schema.Resource{ Description: ` Manages API keys of a Grafana Cloud stack using the Cloud API This can be used to bootstrap a management API key for a new stack @@ -75,6 +75,12 @@ Required access policy scopes: }, }, } + + return common.NewResource( + "grafana_cloud_stack_api_key", + nil, + schema, + ) } func resourceStackAPIKeyCreate(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { diff --git a/internal/resources/cloud/resource_cloud_stack_service_account.go b/internal/resources/cloud/resource_cloud_stack_service_account.go index 18270d287..63b550c02 100644 --- a/internal/resources/cloud/resource_cloud_stack_service_account.go +++ b/internal/resources/cloud/resource_cloud_stack_service_account.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/url" - "strconv" "time" "github.com/grafana/grafana-com-public-clients/go/gcom" @@ -17,8 +16,15 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -func resourceStackServiceAccount() *schema.Resource { - return &schema.Resource{ +var ( + resourceStackServiceAccountID = common.NewResourceID( + common.StringIDField("stackSlug"), + common.IntIDField("serviceAccountID"), + ) +) + +func resourceStackServiceAccount() *common.Resource { + schema := &schema.Resource{ Description: ` Manages service accounts of a Grafana Cloud stack using the Cloud API @@ -66,10 +72,17 @@ Required access policy scopes: }, }, } + + return common.NewResource( + "grafana_cloud_stack_service_account", + resourceStackServiceAccountID, + schema, + ) } func createStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { - client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, d.Get("stack_slug").(string), "terraform-temp-") + stackSlug := d.Get("stack_slug").(string) + client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-") if err != nil { return diag.FromErr(err) } @@ -86,27 +99,28 @@ func createStackServiceAccount(ctx context.Context, d *schema.ResourceData, clou } sa := resp.Payload - d.SetId(strconv.FormatInt(sa.ID, 10)) - return readStackServiceAccountWithClient(client, d) + d.SetId(resourceStackServiceAccountID.Make(stackSlug, sa.ID)) + return readStackServiceAccountWithClient(client, d, sa.ID) } func readStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { - client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, d.Get("stack_slug").(string), "terraform-temp-") + split, err := resourceStackServiceAccountID.Split(d.Id()) if err != nil { return diag.FromErr(err) } - defer cleanup() + stackSlug, serviceAccountID := split[0], split[1] - return readStackServiceAccountWithClient(client, d) -} - -func readStackServiceAccountWithClient(client *goapi.GrafanaHTTPAPI, d *schema.ResourceData) diag.Diagnostics { - id, err := strconv.ParseInt(d.Id(), 10, 64) + client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug.(string), "terraform-temp-") if err != nil { return diag.FromErr(err) } + defer cleanup() - resp, err := client.ServiceAccounts.RetrieveServiceAccount(id) + return readStackServiceAccountWithClient(client, d, serviceAccountID.(int64)) +} + +func readStackServiceAccountWithClient(client *goapi.GrafanaHTTPAPI, d *schema.ResourceData, serviceAccountID int64) diag.Diagnostics { + resp, err := client.ServiceAccounts.RetrieveServiceAccount(serviceAccountID) if err != nil { return diag.FromErr(err) } @@ -129,16 +143,17 @@ func readStackServiceAccountWithClient(client *goapi.GrafanaHTTPAPI, d *schema.R } func updateStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { - client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, d.Get("stack_slug").(string), "terraform-temp-") + split, err := resourceStackServiceAccountID.Split(d.Id()) if err != nil { return diag.FromErr(err) } - defer cleanup() + stackSlug, serviceAccountID := split[0], split[1] - id, err := strconv.ParseInt(d.Id(), 10, 64) + client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug.(string), "terraform-temp-") if err != nil { return diag.FromErr(err) } + defer cleanup() updateRequest := service_accounts.NewUpdateServiceAccountParams(). WithBody(&models.UpdateServiceAccountForm{ @@ -146,28 +161,29 @@ func updateStackServiceAccount(ctx context.Context, d *schema.ResourceData, clou Role: d.Get("role").(string), IsDisabled: d.Get("is_disabled").(bool), }). - WithServiceAccountID(id) + WithServiceAccountID(serviceAccountID.(int64)) if _, err := client.ServiceAccounts.UpdateServiceAccount(updateRequest); err != nil { return diag.FromErr(err) } - return readStackServiceAccountWithClient(client, d) + return readStackServiceAccountWithClient(client, d, serviceAccountID.(int64)) } func deleteStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { - client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, d.Get("stack_slug").(string), "terraform-temp-") + split, err := resourceStackServiceAccountID.Split(d.Id()) if err != nil { return diag.FromErr(err) } - defer cleanup() + stackSlug, serviceAccountID := split[0], split[1] - id, err := strconv.ParseInt(d.Id(), 10, 64) + client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug.(string), "terraform-temp-") if err != nil { return diag.FromErr(err) } + defer cleanup() - _, err = client.ServiceAccounts.DeleteServiceAccount(id) + _, err = client.ServiceAccounts.DeleteServiceAccount(serviceAccountID.(int64)) return diag.FromErr(err) } diff --git a/internal/resources/cloud/resource_cloud_stack_service_account_test.go b/internal/resources/cloud/resource_cloud_stack_service_account_test.go index 49e9a5fdf..4849e01c9 100644 --- a/internal/resources/cloud/resource_cloud_stack_service_account_test.go +++ b/internal/resources/cloud/resource_cloud_stack_service_account_test.go @@ -3,7 +3,6 @@ package cloud_test import ( "context" "fmt" - "strings" "testing" "github.com/grafana/grafana-com-public-clients/go/gcom" @@ -87,24 +86,14 @@ func testAccGrafanaAuthCheckServiceAccounts(stack *gcom.FormattedApiInstance, ex return fmt.Errorf("failed to get service accounts: %w", err) } - var foundSAs []string - for _, sa := range response.Payload.ServiceAccounts { - if !strings.HasPrefix(sa.Name, "test-api-key-") { - foundSAs = append(foundSAs, sa.Name) - if sa.Tokens == 0 { - return fmt.Errorf("expected to find at least one token for service account %s", sa.Name) - } - } - } - - if len(foundSAs) != len(expectedSAs) { - return fmt.Errorf("expected %d keys, got %d", len(expectedSAs), len(foundSAs)) - } for _, expectedSA := range expectedSAs { found := false - for _, foundSA := range foundSAs { - if expectedSA == foundSA { + for _, sa := range response.Payload.ServiceAccounts { + if sa.Name == expectedSA { found = true + if sa.Tokens == 0 { + return fmt.Errorf("expected to find at least one token for service account %s", sa.Name) + } break } } diff --git a/internal/resources/cloud/resource_cloud_stack_service_account_token.go b/internal/resources/cloud/resource_cloud_stack_service_account_token.go index f713826ac..d1a815fb5 100644 --- a/internal/resources/cloud/resource_cloud_stack_service_account_token.go +++ b/internal/resources/cloud/resource_cloud_stack_service_account_token.go @@ -9,12 +9,13 @@ import ( goapi "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana-openapi-client-go/client/service_accounts" "github.com/grafana/grafana-openapi-client-go/models" + "github.com/grafana/terraform-provider-grafana/internal/common" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func resourceStackServiceAccountToken() *schema.Resource { - return &schema.Resource{ +func resourceStackServiceAccountToken() *common.Resource { + schema := &schema.Resource{ Description: ` Manages service account tokens of a Grafana Cloud stack using the Cloud API This can be used to bootstrap a management service account token for a new stack @@ -67,6 +68,12 @@ Required access policy scopes: }, }, } + + return common.NewResource( + "grafana_cloud_stack_service_account_token", + nil, + schema, + ) } func stackServiceAccountTokenCreate(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { @@ -76,10 +83,11 @@ func stackServiceAccountTokenCreate(ctx context.Context, d *schema.ResourceData, } defer cleanup() - serviceAccountID, err := strconv.ParseInt(d.Get("service_account_id").(string), 10, 64) + split, err := resourceStackServiceAccountID.Split(d.Get("service_account_id").(string)) if err != nil { return diag.FromErr(err) } + serviceAccountID := split[1].(int64) name := d.Get("name").(string) ttl := d.Get("seconds_to_live").(int) @@ -115,10 +123,11 @@ func stackServiceAccountTokenRead(ctx context.Context, d *schema.ResourceData, c } func stackServiceAccountTokenReadWithClient(c *goapi.GrafanaHTTPAPI, d *schema.ResourceData) diag.Diagnostics { - serviceAccountID, err := strconv.ParseInt(d.Get("service_account_id").(string), 10, 64) + split, err := resourceStackServiceAccountID.Split(d.Get("service_account_id").(string)) if err != nil { return diag.FromErr(err) } + serviceAccountID := split[1].(int64) response, err := c.ServiceAccounts.ListTokens(serviceAccountID) if err != nil { @@ -161,10 +170,11 @@ func stackServiceAccountTokenDelete(ctx context.Context, d *schema.ResourceData, } defer cleanup() - serviceAccountID, err := strconv.ParseInt(d.Get("service_account_id").(string), 10, 64) + split, err := resourceStackServiceAccountID.Split(d.Get("service_account_id").(string)) if err != nil { return diag.FromErr(err) } + serviceAccountID := split[1].(int64) id, err := strconv.ParseInt(d.Id(), 10, 32) if err != nil { diff --git a/internal/resources/cloud/resource_synthetic_monitoring_installation.go b/internal/resources/cloud/resource_synthetic_monitoring_installation.go index 8b696ae31..2252f05c3 100644 --- a/internal/resources/cloud/resource_synthetic_monitoring_installation.go +++ b/internal/resources/cloud/resource_synthetic_monitoring_installation.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana-com-public-clients/go/gcom" SMAPI "github.com/grafana/synthetic-monitoring-api-go-client" + "github.com/grafana/terraform-provider-grafana/internal/common" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -20,8 +21,8 @@ var smAPIURLsExceptions = map[string]string{ "us-azure": "https://synthetic-monitoring-api-us-central2.grafana.net", } -func resourceSyntheticMonitoringInstallation() *schema.Resource { - return &schema.Resource{ +func resourceSyntheticMonitoringInstallation() *common.Resource { + schema := &schema.Resource{ Description: ` Sets up Synthetic Monitoring on a Grafana cloud stack and generates a token. @@ -69,6 +70,12 @@ Required access policy scopes: }, }, } + + return common.NewResource( + "grafana_synthetic_monitoring_installation", + nil, + schema, + ) } func resourceInstallationCreate(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { diff --git a/internal/resources/cloud/resources.go b/internal/resources/cloud/resources.go index 9ae0bf54a..fa5f9b205 100644 --- a/internal/resources/cloud/resources.go +++ b/internal/resources/cloud/resources.go @@ -1,6 +1,7 @@ package cloud import ( + "github.com/grafana/terraform-provider-grafana/internal/common" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -10,14 +11,22 @@ var DatasourcesMap = map[string]*schema.Resource{ "grafana_cloud_stack": datasourceStack(), } -var ResourcesMap = map[string]*schema.Resource{ - "grafana_cloud_access_policy": resourceAccessPolicy(), - "grafana_cloud_access_policy_token": resourceAccessPolicyToken(), - "grafana_cloud_api_key": resourceAPIKey(), - "grafana_cloud_plugin_installation": resourcePluginInstallation(), - "grafana_cloud_stack": resourceStack(), - "grafana_cloud_stack_api_key": resourceStackAPIKey(), - "grafana_cloud_stack_service_account": resourceStackServiceAccount(), - "grafana_cloud_stack_service_account_token": resourceStackServiceAccountToken(), - "grafana_synthetic_monitoring_installation": resourceSyntheticMonitoringInstallation(), +var Resources = []*common.Resource{ + resourceAccessPolicy(), + resourceAccessPolicyToken(), + resourceAPIKey(), + resourcePluginInstallation(), + resourceStack(), + resourceStackAPIKey(), + resourceStackServiceAccount(), + resourceStackServiceAccountToken(), + resourceSyntheticMonitoringInstallation(), +} + +func ResourcesMap() map[string]*schema.Resource { + m := make(map[string]*schema.Resource) + for _, r := range Resources { + m[r.Name] = r.Schema + } + return m }