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

Typed resource ID helper #1395

Merged
merged 1 commit into from
Mar 6, 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
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
Loading