diff --git a/internal/common/resource.go b/internal/common/resource.go index f84a1b8b3..03d4d9455 100644 --- a/internal/common/resource.go +++ b/internal/common/resource.go @@ -14,11 +14,11 @@ var allResources = []*Resource{} type Resource struct { Name string - IDType *TFID + IDType *ResourceID Schema *schema.Resource } -func NewResource(name string, idType *TFID, schema *schema.Resource) *Resource { +func NewResource(name string, idType *ResourceID, schema *schema.Resource) *Resource { r := &Resource{ Name: name, IDType: idType, @@ -32,7 +32,7 @@ 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]) + fields[i] = fmt.Sprintf("{{ %s }}", id.expectedFields[i].Name) } return fmt.Sprintf(`terraform import %s.name %q `, r.Name, strings.Join(fields, defaultSeparator)) diff --git a/internal/common/resource_id.go b/internal/common/resource_id.go index 18cfe21b7..614072a88 100644 --- a/internal/common/resource_id.go +++ b/internal/common/resource_id.go @@ -6,68 +6,123 @@ import ( "strings" ) -var ( - defaultSeparator = ":" +type ResourceIDFieldType string + +const ( + defaultSeparator = ":" + ResourceIDFieldTypeInt = ResourceIDFieldType("int") + ResourceIDFieldTypeString = ResourceIDFieldType("string") ) -type TFID struct { +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 []string + expectedFields []ResourceIDField } -func NewTFID(expectedFields ...string) *TFID { - return newTFIDWithSeparators([]string{defaultSeparator}, expectedFields...) +func NewResourceID(expectedFields ...ResourceIDField) *ResourceID { + return newResourceIDWithSeparators([]string{defaultSeparator}, expectedFields...) } -// Deprecated: Use NewTFID instead +// 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 NewTFID and remove uses of this function -func NewTFIDWithLegacySeparator(legacySeparator string, expectedFields ...string) *TFID { - return newTFIDWithSeparators([]string{defaultSeparator, legacySeparator}, expectedFields...) +// 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 newTFIDWithSeparators(separators []string, expectedFields ...string) *TFID { - tfID := &TFID{ +func newResourceIDWithSeparators(separators []string, expectedFields ...ResourceIDField) *ResourceID { + tfID := &ResourceID{ separators: separators, expectedFields: expectedFields, } return tfID } -func (id *TFID) Make(parts ...any) string { +// 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 { - stringParts[i] = fmt.Sprintf("%v", part) + 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) -} -func (id *TFID) AsInt64(resourceID string) (int64, error) { - parts, err := id.Split(resourceID) - if err != nil { - return 0, err - } - return strconv.ParseInt(parts[0], 10, 64) + return strings.Join(stringParts, defaultSeparator) } -func (id *TFID) AsString(resourceID string) (string, error) { +// 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 "", err + return nil, err } - return parts[0], nil } -func (id *TFID) Split(resourceID string) ([]string, error) { +// 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) { - return parts, nil + 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 } } - return nil, fmt.Errorf("id %q does not match expected format. Should be in the format: %s", resourceID, strings.Join(id.expectedFields, defaultSeparator)) + + 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/resources/cloud/resource_cloud_access_policy.go b/internal/resources/cloud/resource_cloud_access_policy.go index 3dad70df2..4e3ccf294 100644 --- a/internal/resources/cloud/resource_cloud_access_policy.go +++ b/internal/resources/cloud/resource_cloud_access_policy.go @@ -14,7 +14,11 @@ import ( ) var ( - resourceAccessPolicyID = common.NewTFIDWithLegacySeparator("/", "region", "policyId") //nolint:staticcheck + //nolint:staticcheck + resourceAccessPolicyID = common.NewResourceIDWithLegacySeparator("/", + common.StringIDField("region"), + common.StringIDField("policyId"), + ) ) func resourceAccessPolicy() *common.Resource { @@ -170,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()), @@ -190,7 +194,7 @@ func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client * } 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 } @@ -217,7 +221,7 @@ func deleteCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client } 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 98fc73819..8f07a1965 100644 --- a/internal/resources/cloud/resource_cloud_access_policy_token.go +++ b/internal/resources/cloud/resource_cloud_access_policy_token.go @@ -12,7 +12,11 @@ import ( ) var ( - resourceAccessPolicyTokenID = common.NewTFIDWithLegacySeparator("/", "region", "tokenId") //nolint:staticcheck + //nolint:staticcheck + resourceAccessPolicyTokenID = common.NewResourceIDWithLegacySeparator("/", + common.StringIDField("region"), + common.StringIDField("tokenId"), + ) ) func resourceAccessPolicyToken() *common.Resource { @@ -143,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 { @@ -160,7 +164,7 @@ func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, cli } 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 } @@ -188,6 +192,6 @@ func deleteCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c } 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_api_key.go b/internal/resources/cloud/resource_cloud_api_key.go index 571c5fc9b..4e7084189 100644 --- a/internal/resources/cloud/resource_cloud_api_key.go +++ b/internal/resources/cloud/resource_cloud_api_key.go @@ -13,7 +13,11 @@ import ( var ( cloudAPIKeyRoles = []string{"Viewer", "Editor", "Admin", "MetricsPublisher", "PluginPublisher"} - resourceAPIKeyID = common.NewTFIDWithLegacySeparator("-", "orgSlug", "apiKeyName") //nolint:staticcheck + //nolint:staticcheck + resourceAPIKeyID = common.NewResourceIDWithLegacySeparator("-", + common.StringIDField("orgSlug"), + common.StringIDField("apiKeyName"), + ) ) func resourceAPIKey() *common.Resource { @@ -101,7 +105,7 @@ func resourceAPIKeyRead(ctx context.Context, d *schema.ResourceData, c *gcom.API } 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) } @@ -121,7 +125,7 @@ func resourceAPIKeyDelete(ctx context.Context, d *schema.ResourceData, c *gcom.A } 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 2e950e1e4..fc83cc672 100644 --- a/internal/resources/cloud/resource_cloud_plugin.go +++ b/internal/resources/cloud/resource_cloud_plugin.go @@ -10,7 +10,11 @@ import ( ) var ( - resourcePluginInstallationID = common.NewTFIDWithLegacySeparator("_", "stackSlug", "pluginSlug") //nolint:staticcheck + //nolint:staticcheck + resourcePluginInstallationID = common.NewResourceIDWithLegacySeparator("_", + common.StringIDField("stackSlug"), + common.StringIDField("pluginSlug"), + ) ) func resourcePluginInstallation() *common.Resource { @@ -89,7 +93,7 @@ func resourcePluginInstallationRead(ctx context.Context, d *schema.ResourceData, } 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 } @@ -109,6 +113,6 @@ func resourcePluginInstallationDelete(ctx context.Context, d *schema.ResourceDat } 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 ccffcd3da..4d89851cf 100644 --- a/internal/resources/cloud/resource_cloud_stack.go +++ b/internal/resources/cloud/resource_cloud_stack.go @@ -26,7 +26,7 @@ const defaultReadinessTimeout = time.Minute * 5 var ( stackLabelRegex = regexp.MustCompile(`^[a-zA-Z0-9/\-.]+$`) stackSlugRegex = regexp.MustCompile(`^[a-z][a-z0-9]+$`) - resourceStackID = common.NewTFID("stackSlugOrID") + resourceStackID = common.NewResourceID(common.StringIDField("stackSlugOrID")) ) func resourceStack() *common.Resource { @@ -254,7 +254,7 @@ 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.AsString(d.Id()) + id, err := resourceStackID.Single(d.Id()) if err != nil { return diag.FromErr(err) } @@ -272,7 +272,7 @@ 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, id).PostInstanceRequest(stack).XRequestId(ClientRequestID()) + req := client.InstancesAPI.PostInstance(ctx, id.(string)).PostInstanceRequest(stack).XRequestId(ClientRequestID()) _, _, err = req.Execute() if err != nil { return apiError(err) @@ -286,23 +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 { - id, err := resourceStackID.AsString(d.Id()) + id, err := resourceStackID.Single(d.Id()) if err != nil { return diag.FromErr(err) } - req := client.InstancesAPI.DeleteInstance(ctx, id).XRequestId(ClientRequestID()) + 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 { - id, err := resourceStackID.AsString(d.Id()) + id, err := resourceStackID.Single(d.Id()) if err != nil { return diag.FromErr(err) } - req := client.InstancesAPI.GetInstance(ctx, id) + req := client.InstancesAPI.GetInstance(ctx, id.(string)) stack, _, err := req.Execute() if err, shouldReturn := common.CheckReadError("stack", d, err); shouldReturn { return err @@ -314,7 +314,7 @@ func readStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClie return nil } - connectionsReq := client.InstancesAPI.GetConnections(ctx, id) + connectionsReq := client.InstancesAPI.GetConnections(ctx, id.(string)) connections, _, err := connectionsReq.Execute() if err != nil { return apiError(err) diff --git a/internal/resources/cloud/resource_cloud_stack_service_account.go b/internal/resources/cloud/resource_cloud_stack_service_account.go index 2d03412f2..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" @@ -18,7 +17,10 @@ import ( ) var ( - resourceStackServiceAccountID = common.NewTFID("stackSlug", "serviceAccountID") + resourceStackServiceAccountID = common.NewResourceID( + common.StringIDField("stackSlug"), + common.IntIDField("serviceAccountID"), + ) ) func resourceStackServiceAccount() *common.Resource { @@ -106,20 +108,15 @@ func readStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudC if err != nil { return diag.FromErr(err) } - stackSlug, serviceAccountIDStr := split[0], split[1] - - serviceAccountID, err := strconv.ParseInt(serviceAccountIDStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } + stackSlug, serviceAccountID := split[0], split[1] - client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-") + client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug.(string), "terraform-temp-") if err != nil { return diag.FromErr(err) } defer cleanup() - return readStackServiceAccountWithClient(client, d, serviceAccountID) + return readStackServiceAccountWithClient(client, d, serviceAccountID.(int64)) } func readStackServiceAccountWithClient(client *goapi.GrafanaHTTPAPI, d *schema.ResourceData, serviceAccountID int64) diag.Diagnostics { @@ -150,14 +147,9 @@ func updateStackServiceAccount(ctx context.Context, d *schema.ResourceData, clou if err != nil { return diag.FromErr(err) } - stackSlug, serviceAccountIDStr := split[0], split[1] - - serviceAccountID, err := strconv.ParseInt(serviceAccountIDStr, 10, 64) - if err != nil { - return diag.FromErr(err) - } + stackSlug, serviceAccountID := split[0], split[1] - client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-") + client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug.(string), "terraform-temp-") if err != nil { return diag.FromErr(err) } @@ -169,13 +161,13 @@ func updateStackServiceAccount(ctx context.Context, d *schema.ResourceData, clou Role: d.Get("role").(string), IsDisabled: d.Get("is_disabled").(bool), }). - WithServiceAccountID(serviceAccountID) + WithServiceAccountID(serviceAccountID.(int64)) if _, err := client.ServiceAccounts.UpdateServiceAccount(updateRequest); err != nil { return diag.FromErr(err) } - return readStackServiceAccountWithClient(client, d, serviceAccountID) + return readStackServiceAccountWithClient(client, d, serviceAccountID.(int64)) } func deleteStackServiceAccount(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics { @@ -185,18 +177,13 @@ func deleteStackServiceAccount(ctx context.Context, d *schema.ResourceData, clou } stackSlug, serviceAccountID := split[0], split[1] - client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug, "terraform-temp-") + client, cleanup, err := CreateTemporaryStackGrafanaClient(ctx, cloudClient, stackSlug.(string), "terraform-temp-") if err != nil { return diag.FromErr(err) } defer cleanup() - id, err := strconv.ParseInt(serviceAccountID, 10, 64) - if err != nil { - return diag.FromErr(err) - } - - _, err = client.ServiceAccounts.DeleteServiceAccount(id) + _, err = client.ServiceAccounts.DeleteServiceAccount(serviceAccountID.(int64)) return diag.FromErr(err) }