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 dd4c963e8..929094c83 100644 --- a/internal/provider/legacy_provider.go +++ b/internal/provider/legacy_provider.go @@ -215,7 +215,7 @@ func Provider(version string) *schema.Provider { slo.ResourcesMap, syntheticmonitoring.ResourcesMap, oncall.ResourcesMap, - 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 798fdddb1..4d89851cf 100644 --- a/internal/resources/cloud/resource_cloud_stack.go +++ b/internal/resources/cloud/resource_cloud_stack.go @@ -26,12 +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("grafana_cloud_stack", common.StringIDField("stackSlugOrID")) + 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/) @@ -199,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 { 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 5442dd3a2..5a59ec25a 100644 --- a/internal/resources/cloud/resource_cloud_stack_service_account.go +++ b/internal/resources/cloud/resource_cloud_stack_service_account.go @@ -18,14 +18,13 @@ import ( var ( resourceStackServiceAccountID = common.NewResourceID( - "grafana_cloud_stack_service_account", common.StringIDField("stackSlug"), common.IntIDField("serviceAccountID"), ) ) -func resourceStackServiceAccount() *schema.Resource { - return &schema.Resource{ +func resourceStackServiceAccount() *common.Resource { + schema := &schema.Resource{ Description: ` Manages service accounts of a Grafana Cloud stack using the Cloud API @@ -74,6 +73,12 @@ 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 { 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 67a82cfd8..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 { 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 }