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 2, 2024
1 parent 0b826cb commit 6fd6cd6
Show file tree
Hide file tree
Showing 21 changed files with 457 additions and 230 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 @@ -50,3 +50,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 }}"
84 changes: 0 additions & 84 deletions internal/common/id.go

This file was deleted.

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
}
133 changes: 133 additions & 0 deletions internal/common/resource_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package common

import (
"fmt"
"reflect"
"strconv"
"strings"
)

type ResourceIDFieldType string

const (
defaultSeparator = ":"
ResourceIDFieldTypeInt = ResourceIDFieldType("int")
ResourceIDFieldTypeString = ResourceIDFieldType("string")
)

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 []ResourceIDField
}

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(legacySeparator string, expectedFields ...ResourceIDField) *ResourceID {
return newResourceIDWithSeparators([]string{defaultSeparator, legacySeparator}, expectedFields...)
}

func newResourceIDWithSeparators(separators []string, expectedFields ...ResourceIDField) *ResourceID {
tfID := &ResourceID{
separators: separators,
expectedFields: expectedFields,
}
return tfID
}

// 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 {
// Unwrap pointers
if reflect.ValueOf(part).Kind() == reflect.Ptr {
part = reflect.ValueOf(part).Elem().Interface()
}
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)
}

// 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 nil, err
}
return parts[0], nil
}

// 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) {
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
}
}

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))
}
2 changes: 1 addition & 1 deletion internal/provider/legacy_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func Provider(version string) *schema.Provider {
grafanaClientResources,
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
Loading

0 comments on commit 6fd6cd6

Please sign in to comment.