Skip to content

Commit

Permalink
Convert Cloud resources to new "resource" framework
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
julienduchesne committed Mar 11, 2024
1 parent 3d08dfc commit 49a21e2
Show file tree
Hide file tree
Showing 18 changed files with 264 additions and 142 deletions.
3 changes: 1 addition & 2 deletions docs/resources/cloud_stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
```
8 changes: 8 additions & 0 deletions docs/resources/cloud_stack_service_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
```
3 changes: 1 addition & 2 deletions examples/resources/grafana_cloud_stack/import.sh
Original file line number Diff line number Diff line change
@@ -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 }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import grafana_cloud_stack_service_account.name "{{ stackSlug }}:{{ serviceAccountID }}"
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 @@ -250,7 +250,7 @@ func Provider(version string) *schema.Provider {
slo.ResourcesMap,
smClientResources,
onCallClientResources,
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
Loading

0 comments on commit 49a21e2

Please sign in to comment.