Skip to content

Commit

Permalink
Typed resource ID helper (#1395)
Browse files Browse the repository at this point in the history
Each resource's ID is composed of n (>=1) string or integer elements
We can generalize this behavior and do the parsing/formatting in the helper function
This is extracted from #1391 and is part of a push to have standardized IDs for all resources, allowing for easier generation of TF code!
  • Loading branch information
julienduchesne authored Mar 6, 2024
1 parent 1820388 commit d6e8d17
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 62 deletions.
100 changes: 89 additions & 11 deletions internal/common/resource_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,58 @@ import (
"log"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
)

type ResourceIDFieldType string

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

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 {
resourceName string
separators []string
expectedFields []string
expectedFields []ResourceIDField
}

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

func newResourceIDWithSeparators(resourceName string, separators []string, expectedFields ...string) *ResourceID {
func newResourceIDWithSeparators(resourceName string, separators []string, expectedFields ...ResourceIDField) *ResourceID {
tfID := &ResourceID{
resourceName: resourceName,
separators: separators,
Expand All @@ -43,31 +69,83 @@ func newResourceIDWithSeparators(resourceName string, separators []string, expec
func (id *ResourceID) Example() string {
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
`, 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 {
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)
// 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)
}

func (id *ResourceID) Split(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 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) {
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))
}

// GenerateImportFiles generates import files for all resources that use a helper defined in this package
Expand Down
26 changes: 17 additions & 9 deletions internal/resources/cloud/resource_cloud_access_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

var ResourceAccessPolicyID = common.NewResourceIDWithLegacySeparator("grafana_cloud_access_policy", "/", "region", "policyId") //nolint:staticcheck
var (
//nolint:staticcheck
resourceAccessPolicyID = common.NewResourceIDWithLegacySeparator(
"grafana_cloud_access_policy",
"/",
common.StringIDField("region"),
common.StringIDField("policyId"),
)
)

func resourceAccessPolicy() *schema.Resource {
return &schema.Resource{
Expand Down Expand Up @@ -146,13 +154,13 @@ func createCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client
return apiError(err)
}

d.SetId(ResourceAccessPolicyID.Make(region, result.Id))
d.SetId(resourceAccessPolicyID.Make(region, result.Id))

return readCloudAccessPolicy(ctx, d, client)
}

func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
split, err := ResourceAccessPolicyID.Split(d.Id())
split, err := resourceAccessPolicyID.Split(d.Id())
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -163,7 +171,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()),
Expand All @@ -177,13 +185,13 @@ func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client
}

func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
split, err := ResourceAccessPolicyID.Split(d.Id())
split, err := resourceAccessPolicyID.Split(d.Id())
if err != nil {
return diag.FromErr(err)
}
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
}
Expand All @@ -198,19 +206,19 @@ func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *
if updated := result.UpdatedAt; updated != nil {
d.Set("updated_at", updated.Format(time.RFC3339))
}
d.SetId(ResourceAccessPolicyID.Make(region, result.Id))
d.SetId(resourceAccessPolicyID.Make(region, result.Id))

return nil
}

func deleteCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
split, err := ResourceAccessPolicyID.Split(d.Id())
split, err := resourceAccessPolicyID.Split(d.Id())
if err != nil {
return diag.FromErr(err)
}
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)
}

Expand Down
26 changes: 17 additions & 9 deletions internal/resources/cloud/resource_cloud_access_policy_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

var ResourceAccessPolicyTokenID = common.NewResourceIDWithLegacySeparator("grafana_cloud_access_policy_token", "/", "region", "tokenId") //nolint:staticcheck
var (
//nolint:staticcheck
resourceAccessPolicyTokenID = common.NewResourceIDWithLegacySeparator(
"grafana_cloud_access_policy_token",
"/",
common.StringIDField("region"),
common.StringIDField("tokenId"),
)
)

func resourceAccessPolicyToken() *schema.Resource {
return &schema.Resource{
Expand Down Expand Up @@ -117,14 +125,14 @@ func createCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c
return apiError(err)
}

d.SetId(ResourceAccessPolicyTokenID.Make(region, result.Id))
d.SetId(resourceAccessPolicyTokenID.Make(region, result.Id))
d.Set("token", result.Token)

return readCloudAccessPolicyToken(ctx, d, client)
}

func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
split, err := ResourceAccessPolicyTokenID.Split(d.Id())
split, err := resourceAccessPolicyTokenID.Split(d.Id())
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -135,7 +143,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 {
Expand All @@ -146,13 +154,13 @@ func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c
}

func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
split, err := ResourceAccessPolicyTokenID.Split(d.Id())
split, err := resourceAccessPolicyTokenID.Split(d.Id())
if err != nil {
return diag.FromErr(err)
}
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
}
Expand All @@ -168,18 +176,18 @@ func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, cli
if result.UpdatedAt != nil {
d.Set("updated_at", result.UpdatedAt.Format(time.RFC3339))
}
d.SetId(ResourceAccessPolicyTokenID.Make(region, result.Id))
d.SetId(resourceAccessPolicyTokenID.Make(region, result.Id))

return nil
}

func deleteCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
split, err := ResourceAccessPolicyTokenID.Split(d.Id())
split, err := resourceAccessPolicyTokenID.Split(d.Id())
if err != nil {
return diag.FromErr(err)
}
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func testAccCloudAccessPolicyCheckExists(rn string, a *gcom.AuthAccessPolicy) re
return fmt.Errorf("resource id not set")
}

region, id, _ := strings.Cut(rs.Primary.ID, "/")
region, id, _ := strings.Cut(rs.Primary.ID, ":")

client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI
policy, _, err := client.AccesspoliciesAPI.GetAccessPolicy(context.Background(), id).Region(region).Execute()
Expand All @@ -179,7 +179,7 @@ func testAccCloudAccessPolicyTokenCheckExists(rn string, a *gcom.AuthToken) reso
return fmt.Errorf("resource id not set")
}

region, id, _ := strings.Cut(rs.Primary.ID, "/")
region, id, _ := strings.Cut(rs.Primary.ID, ":")

client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI
token, _, err := client.TokensAPI.GetToken(context.Background(), id).Region(region).Execute()
Expand All @@ -195,6 +195,9 @@ func testAccCloudAccessPolicyTokenCheckExists(rn string, a *gcom.AuthToken) reso

func testAccCloudAccessPolicyCheckDestroy(region string, a *gcom.AuthAccessPolicy) resource.TestCheckFunc {
return func(s *terraform.State) error {
if a == nil {
return nil
}
client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI
policy, _, err := client.AccesspoliciesAPI.GetAccessPolicy(context.Background(), *a.Id).Region(region).Execute()
if err == nil && policy.Name != "" {
Expand All @@ -207,6 +210,9 @@ func testAccCloudAccessPolicyCheckDestroy(region string, a *gcom.AuthAccessPolic

func testAccCloudAccessPolicyTokenCheckDestroy(region string, a *gcom.AuthToken) resource.TestCheckFunc {
return func(s *terraform.State) error {
if a == nil {
return nil
}
client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI
token, _, err := client.TokensAPI.GetToken(context.Background(), *a.Id).Region(region).Execute()
if err == nil && token.Name != "" {
Expand Down
Loading

0 comments on commit d6e8d17

Please sign in to comment.