Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Cloud resources to new "resource" framework #1391

Merged
merged 1 commit into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions internal/common/resource.go
Original file line number Diff line number Diff line change
@@ -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
}
41 changes: 6 additions & 35 deletions internal/common/resource_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@ package common

import (
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
)

type ResourceIDFieldType string

var (
const (
defaultSeparator = ":"
ResourceIDFieldTypeInt = ResourceIDFieldType("int")
ResourceIDFieldTypeString = ResourceIDFieldType("string")
allIDs = []*ResourceID{}
)

type ResourceIDField struct {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion internal/provider/legacy_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func Provider(version string) *schema.Provider {
slo.ResourcesMap,
syntheticmonitoring.ResourcesMap,
oncall.ResourcesMap,
cloud.ResourcesMap,
cloud.ResourcesMap(),
),

DataSourcesMap: mergeResourceMaps(
Expand Down
2 changes: 1 addition & 1 deletion internal/resources/cloud/data_source_cloud_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
69 changes: 36 additions & 33 deletions internal/resources/cloud/resource_cloud_access_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 9 additions & 5 deletions internal/resources/cloud/resource_cloud_access_policy_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 9 additions & 5 deletions internal/resources/cloud/resource_cloud_api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 9 additions & 5 deletions internal/resources/cloud/resource_cloud_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 9 additions & 4 deletions internal/resources/cloud/resource_cloud_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading