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 a91a04661..a2f4b455a 100644 --- a/docs/resources/cloud_stack_service_account.md +++ b/docs/resources/cloud_stack_service_account.md @@ -51,3 +51,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/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 index b797e1fe9..033ff28a8 100644 --- a/internal/common/resource_id.go +++ b/internal/common/resource_id.go @@ -2,9 +2,6 @@ package common import ( "fmt" - "log" - "os" - "path/filepath" "reflect" "strconv" "strings" @@ -12,11 +9,10 @@ import ( type ResourceIDFieldType string -var ( +const ( defaultSeparator = ":" ResourceIDFieldTypeInt = ResourceIDFieldType("int") ResourceIDFieldTypeString = ResourceIDFieldType("string") - allIDs = []*ResourceID{} ) type ResourceIDField struct { @@ -40,41 +36,29 @@ func IntIDField(name string) ResourceIDField { } type ResourceID struct { - resourceName string separators []string expectedFields []ResourceIDField } -func NewResourceID(resourceName string, expectedFields ...ResourceIDField) *ResourceID { - return newResourceIDWithSeparators(resourceName, []string{defaultSeparator}, expectedFields...) +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(resourceName, legacySeparator string, expectedFields ...ResourceIDField) *ResourceID { - return newResourceIDWithSeparators(resourceName, []string{defaultSeparator, legacySeparator}, expectedFields...) +func NewResourceIDWithLegacySeparator(legacySeparator string, expectedFields ...ResourceIDField) *ResourceID { + return newResourceIDWithSeparators([]string{defaultSeparator, legacySeparator}, expectedFields...) } -func newResourceIDWithSeparators(resourceName string, separators []string, expectedFields ...ResourceIDField) *ResourceID { +func newResourceIDWithSeparators(separators []string, expectedFields ...ResourceIDField) *ResourceID { tfID := &ResourceID{ - resourceName: resourceName, separators: separators, expectedFields: expectedFields, } - allIDs = append(allIDs, tfID) return tfID } -func (id *ResourceID) Example() string { - 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 -`, id.resourceName, strings.Join(fields, defaultSeparator)) -} - // 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 { @@ -147,16 +131,3 @@ func (id *ResourceID) Split(resourceID string) ([]any, error) { } return nil, fmt.Errorf("id %q does not match expected format. Should be in the format: %s", resourceID, strings.Join(expectedFieldNames, 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/provider/legacy_provider.go b/internal/provider/legacy_provider.go index bbfcaa07c..b7d6456d3 100644 --- a/internal/provider/legacy_provider.go +++ b/internal/provider/legacy_provider.go @@ -250,7 +250,7 @@ func Provider(version string) *schema.Provider { slo.ResourcesMap, 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 5d0223870..4e3ccf294 100644 --- a/internal/resources/cloud/resource_cloud_access_policy.go +++ b/internal/resources/cloud/resource_cloud_access_policy.go @@ -15,17 +15,43 @@ import ( var ( //nolint:staticcheck - resourceAccessPolicyID = common.NewResourceIDWithLegacySeparator( - "grafana_cloud_access_policy", - "/", + 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) @@ -103,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 { diff --git a/internal/resources/cloud/resource_cloud_access_policy_token.go b/internal/resources/cloud/resource_cloud_access_policy_token.go index b56f1bd68..8f07a1965 100644 --- a/internal/resources/cloud/resource_cloud_access_policy_token.go +++ b/internal/resources/cloud/resource_cloud_access_policy_token.go @@ -13,16 +13,14 @@ import ( var ( //nolint:staticcheck - resourceAccessPolicyTokenID = common.NewResourceIDWithLegacySeparator( - "grafana_cloud_access_policy_token", - "/", + 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/) @@ -100,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 { diff --git a/internal/resources/cloud/resource_cloud_api_key.go b/internal/resources/cloud/resource_cloud_api_key.go index 9ed76462c..4e7084189 100644 --- a/internal/resources/cloud/resource_cloud_api_key.go +++ b/internal/resources/cloud/resource_cloud_api_key.go @@ -14,16 +14,14 @@ import ( var ( cloudAPIKeyRoles = []string{"Viewer", "Editor", "Admin", "MetricsPublisher", "PluginPublisher"} //nolint:staticcheck - resourceAPIKeyID = common.NewResourceIDWithLegacySeparator( - "grafana_cloud_api_key", - "-", + 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) @@ -71,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 { diff --git a/internal/resources/cloud/resource_cloud_plugin.go b/internal/resources/cloud/resource_cloud_plugin.go index 91c9ec156..fc83cc672 100644 --- a/internal/resources/cloud/resource_cloud_plugin.go +++ b/internal/resources/cloud/resource_cloud_plugin.go @@ -11,16 +11,14 @@ import ( var ( //nolint:staticcheck - resourcePluginInstallationID = common.NewResourceIDWithLegacySeparator( - "grafana_cloud_plugin_installation", - "_", + 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. @@ -60,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 { 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 bef42532f..79b4ccd1d 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 @@ -67,10 +73,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) } @@ -87,27 +100,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) } @@ -130,16 +144,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{ @@ -147,28 +162,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_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 }