Skip to content

Commit d6e8d17

Browse files
Typed resource ID helper (#1395)
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!
1 parent 1820388 commit d6e8d17

7 files changed

+167
-62
lines changed

internal/common/resource_id.go

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,58 @@ import (
55
"log"
66
"os"
77
"path/filepath"
8+
"reflect"
9+
"strconv"
810
"strings"
911
)
1012

13+
type ResourceIDFieldType string
14+
1115
var (
12-
defaultSeparator = ":"
13-
allIDs = []*ResourceID{}
16+
defaultSeparator = ":"
17+
ResourceIDFieldTypeInt = ResourceIDFieldType("int")
18+
ResourceIDFieldTypeString = ResourceIDFieldType("string")
19+
allIDs = []*ResourceID{}
1420
)
1521

22+
type ResourceIDField struct {
23+
Name string
24+
Type ResourceIDFieldType
25+
// Optional bool // Unimplemented. Will be used for org ID
26+
}
27+
28+
func StringIDField(name string) ResourceIDField {
29+
return ResourceIDField{
30+
Name: name,
31+
Type: ResourceIDFieldTypeString,
32+
}
33+
}
34+
35+
func IntIDField(name string) ResourceIDField {
36+
return ResourceIDField{
37+
Name: name,
38+
Type: ResourceIDFieldTypeInt,
39+
}
40+
}
41+
1642
type ResourceID struct {
1743
resourceName string
1844
separators []string
19-
expectedFields []string
45+
expectedFields []ResourceIDField
2046
}
2147

22-
func NewResourceID(resourceName string, expectedFields ...string) *ResourceID {
48+
func NewResourceID(resourceName string, expectedFields ...ResourceIDField) *ResourceID {
2349
return newResourceIDWithSeparators(resourceName, []string{defaultSeparator}, expectedFields...)
2450
}
2551

2652
// Deprecated: Use NewResourceID instead
2753
// We should standardize on a single separator, so that function should only be used for old resources
2854
// On major versions, switch to NewResourceID and remove uses of this function
29-
func NewResourceIDWithLegacySeparator(resourceName, legacySeparator string, expectedFields ...string) *ResourceID {
55+
func NewResourceIDWithLegacySeparator(resourceName, legacySeparator string, expectedFields ...ResourceIDField) *ResourceID {
3056
return newResourceIDWithSeparators(resourceName, []string{defaultSeparator, legacySeparator}, expectedFields...)
3157
}
3258

33-
func newResourceIDWithSeparators(resourceName string, separators []string, expectedFields ...string) *ResourceID {
59+
func newResourceIDWithSeparators(resourceName string, separators []string, expectedFields ...ResourceIDField) *ResourceID {
3460
tfID := &ResourceID{
3561
resourceName: resourceName,
3662
separators: separators,
@@ -43,31 +69,83 @@ func newResourceIDWithSeparators(resourceName string, separators []string, expec
4369
func (id *ResourceID) Example() string {
4470
fields := make([]string, len(id.expectedFields))
4571
for i := range fields {
46-
fields[i] = fmt.Sprintf("{{ %s }}", id.expectedFields[i])
72+
fields[i] = fmt.Sprintf("{{ %s }}", id.expectedFields[i].Name)
4773
}
4874
return fmt.Sprintf(`terraform import %s.name %q
4975
`, id.resourceName, strings.Join(fields, defaultSeparator))
5076
}
5177

78+
// Make creates a resource ID from the given parts
79+
// The parts must have the correct number of fields and types
5280
func (id *ResourceID) Make(parts ...any) string {
5381
if len(parts) != len(id.expectedFields) {
5482
panic(fmt.Sprintf("expected %d fields, got %d", len(id.expectedFields), len(parts))) // This is a coding error, so panic is appropriate
5583
}
5684
stringParts := make([]string, len(parts))
5785
for i, part := range parts {
58-
stringParts[i] = fmt.Sprintf("%v", part)
86+
// Unwrap pointers
87+
if reflect.ValueOf(part).Kind() == reflect.Ptr {
88+
part = reflect.ValueOf(part).Elem().Interface()
89+
}
90+
expectedField := id.expectedFields[i]
91+
switch expectedField.Type {
92+
case ResourceIDFieldTypeInt:
93+
asInt, ok := part.(int64)
94+
if !ok {
95+
panic(fmt.Sprintf("expected int64 for field %q, got %T", expectedField.Name, part)) // This is a coding error, so panic is appropriate
96+
}
97+
stringParts[i] = strconv.FormatInt(asInt, 10)
98+
case ResourceIDFieldTypeString:
99+
asString, ok := part.(string)
100+
if !ok {
101+
panic(fmt.Sprintf("expected string for field %q, got %T", expectedField.Name, part)) // This is a coding error, so panic is appropriate
102+
}
103+
stringParts[i] = asString
104+
}
59105
}
106+
60107
return strings.Join(stringParts, defaultSeparator)
61108
}
62109

63-
func (id *ResourceID) Split(resourceID string) ([]string, error) {
110+
// Single parses a resource ID into a single value
111+
func (id *ResourceID) Single(resourceID string) (any, error) {
112+
parts, err := id.Split(resourceID)
113+
if err != nil {
114+
return nil, err
115+
}
116+
return parts[0], nil
117+
}
118+
119+
// Split parses a resource ID into its parts
120+
// The parts will be cast to the expected types
121+
func (id *ResourceID) Split(resourceID string) ([]any, error) {
64122
for _, sep := range id.separators {
65123
parts := strings.Split(resourceID, sep)
66124
if len(parts) == len(id.expectedFields) {
67-
return parts, nil
125+
partsAsAny := make([]any, len(parts))
126+
for i, part := range parts {
127+
expectedField := id.expectedFields[i]
128+
switch expectedField.Type {
129+
case ResourceIDFieldTypeInt:
130+
asInt, err := strconv.ParseInt(part, 10, 64)
131+
if err != nil {
132+
return nil, fmt.Errorf("expected int for field %q, got %q", expectedField.Name, part)
133+
}
134+
partsAsAny[i] = asInt
135+
case ResourceIDFieldTypeString:
136+
partsAsAny[i] = part
137+
}
138+
}
139+
140+
return partsAsAny, nil
68141
}
69142
}
70-
return nil, fmt.Errorf("id %q does not match expected format. Should be in the format: %s", resourceID, strings.Join(id.expectedFields, defaultSeparator))
143+
144+
expectedFieldNames := make([]string, len(id.expectedFields))
145+
for i, f := range id.expectedFields {
146+
expectedFieldNames[i] = f.Name
147+
}
148+
return nil, fmt.Errorf("id %q does not match expected format. Should be in the format: %s", resourceID, strings.Join(expectedFieldNames, defaultSeparator))
71149
}
72150

73151
// GenerateImportFiles generates import files for all resources that use a helper defined in this package

internal/resources/cloud/resource_cloud_access_policy.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ import (
1313
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1414
)
1515

16-
var ResourceAccessPolicyID = common.NewResourceIDWithLegacySeparator("grafana_cloud_access_policy", "/", "region", "policyId") //nolint:staticcheck
16+
var (
17+
//nolint:staticcheck
18+
resourceAccessPolicyID = common.NewResourceIDWithLegacySeparator(
19+
"grafana_cloud_access_policy",
20+
"/",
21+
common.StringIDField("region"),
22+
common.StringIDField("policyId"),
23+
)
24+
)
1725

1826
func resourceAccessPolicy() *schema.Resource {
1927
return &schema.Resource{
@@ -146,13 +154,13 @@ func createCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client
146154
return apiError(err)
147155
}
148156

149-
d.SetId(ResourceAccessPolicyID.Make(region, result.Id))
157+
d.SetId(resourceAccessPolicyID.Make(region, result.Id))
150158

151159
return readCloudAccessPolicy(ctx, d, client)
152160
}
153161

154162
func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
155-
split, err := ResourceAccessPolicyID.Split(d.Id())
163+
split, err := resourceAccessPolicyID.Split(d.Id())
156164
if err != nil {
157165
return diag.FromErr(err)
158166
}
@@ -163,7 +171,7 @@ func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client
163171
displayName = d.Get("name").(string)
164172
}
165173

166-
req := client.AccesspoliciesAPI.PostAccessPolicy(ctx, id).Region(region).XRequestId(ClientRequestID()).
174+
req := client.AccesspoliciesAPI.PostAccessPolicy(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()).
167175
PostAccessPolicyRequest(gcom.PostAccessPolicyRequest{
168176
DisplayName: &displayName,
169177
Scopes: common.ListToStringSlice(d.Get("scopes").(*schema.Set).List()),
@@ -177,13 +185,13 @@ func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client
177185
}
178186

179187
func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
180-
split, err := ResourceAccessPolicyID.Split(d.Id())
188+
split, err := resourceAccessPolicyID.Split(d.Id())
181189
if err != nil {
182190
return diag.FromErr(err)
183191
}
184192
region, id := split[0], split[1]
185193

186-
result, _, err := client.AccesspoliciesAPI.GetAccessPolicy(ctx, id).Region(region).Execute()
194+
result, _, err := client.AccesspoliciesAPI.GetAccessPolicy(ctx, id.(string)).Region(region.(string)).Execute()
187195
if err, shouldReturn := common.CheckReadError("access policy", d, err); shouldReturn {
188196
return err
189197
}
@@ -198,19 +206,19 @@ func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *
198206
if updated := result.UpdatedAt; updated != nil {
199207
d.Set("updated_at", updated.Format(time.RFC3339))
200208
}
201-
d.SetId(ResourceAccessPolicyID.Make(region, result.Id))
209+
d.SetId(resourceAccessPolicyID.Make(region, result.Id))
202210

203211
return nil
204212
}
205213

206214
func deleteCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
207-
split, err := ResourceAccessPolicyID.Split(d.Id())
215+
split, err := resourceAccessPolicyID.Split(d.Id())
208216
if err != nil {
209217
return diag.FromErr(err)
210218
}
211219
region, id := split[0], split[1]
212220

213-
_, _, err = client.AccesspoliciesAPI.DeleteAccessPolicy(ctx, id).Region(region).XRequestId(ClientRequestID()).Execute()
221+
_, _, err = client.AccesspoliciesAPI.DeleteAccessPolicy(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()).Execute()
214222
return apiError(err)
215223
}
216224

internal/resources/cloud/resource_cloud_access_policy_token.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ import (
1111
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1212
)
1313

14-
var ResourceAccessPolicyTokenID = common.NewResourceIDWithLegacySeparator("grafana_cloud_access_policy_token", "/", "region", "tokenId") //nolint:staticcheck
14+
var (
15+
//nolint:staticcheck
16+
resourceAccessPolicyTokenID = common.NewResourceIDWithLegacySeparator(
17+
"grafana_cloud_access_policy_token",
18+
"/",
19+
common.StringIDField("region"),
20+
common.StringIDField("tokenId"),
21+
)
22+
)
1523

1624
func resourceAccessPolicyToken() *schema.Resource {
1725
return &schema.Resource{
@@ -117,14 +125,14 @@ func createCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c
117125
return apiError(err)
118126
}
119127

120-
d.SetId(ResourceAccessPolicyTokenID.Make(region, result.Id))
128+
d.SetId(resourceAccessPolicyTokenID.Make(region, result.Id))
121129
d.Set("token", result.Token)
122130

123131
return readCloudAccessPolicyToken(ctx, d, client)
124132
}
125133

126134
func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
127-
split, err := ResourceAccessPolicyTokenID.Split(d.Id())
135+
split, err := resourceAccessPolicyTokenID.Split(d.Id())
128136
if err != nil {
129137
return diag.FromErr(err)
130138
}
@@ -135,7 +143,7 @@ func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c
135143
displayName = d.Get("name").(string)
136144
}
137145

138-
req := client.TokensAPI.PostToken(ctx, id).Region(region).XRequestId(ClientRequestID()).PostTokenRequest(gcom.PostTokenRequest{
146+
req := client.TokensAPI.PostToken(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()).PostTokenRequest(gcom.PostTokenRequest{
139147
DisplayName: &displayName,
140148
})
141149
if _, _, err := req.Execute(); err != nil {
@@ -146,13 +154,13 @@ func updateCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c
146154
}
147155

148156
func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
149-
split, err := ResourceAccessPolicyTokenID.Split(d.Id())
157+
split, err := resourceAccessPolicyTokenID.Split(d.Id())
150158
if err != nil {
151159
return diag.FromErr(err)
152160
}
153161
region, id := split[0], split[1]
154162

155-
result, _, err := client.TokensAPI.GetToken(ctx, id).Region(region).Execute()
163+
result, _, err := client.TokensAPI.GetToken(ctx, id.(string)).Region(region.(string)).Execute()
156164
if err, shouldReturn := common.CheckReadError("policy token", d, err); shouldReturn {
157165
return err
158166
}
@@ -168,18 +176,18 @@ func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, cli
168176
if result.UpdatedAt != nil {
169177
d.Set("updated_at", result.UpdatedAt.Format(time.RFC3339))
170178
}
171-
d.SetId(ResourceAccessPolicyTokenID.Make(region, result.Id))
179+
d.SetId(resourceAccessPolicyTokenID.Make(region, result.Id))
172180

173181
return nil
174182
}
175183

176184
func deleteCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
177-
split, err := ResourceAccessPolicyTokenID.Split(d.Id())
185+
split, err := resourceAccessPolicyTokenID.Split(d.Id())
178186
if err != nil {
179187
return diag.FromErr(err)
180188
}
181189
region, id := split[0], split[1]
182190

183-
_, _, err = client.TokensAPI.DeleteToken(ctx, id).Region(region).XRequestId(ClientRequestID()).Execute()
191+
_, _, err = client.TokensAPI.DeleteToken(ctx, id.(string)).Region(region.(string)).XRequestId(ClientRequestID()).Execute()
184192
return apiError(err)
185193
}

internal/resources/cloud/resource_cloud_access_policy_token_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func testAccCloudAccessPolicyCheckExists(rn string, a *gcom.AuthAccessPolicy) re
154154
return fmt.Errorf("resource id not set")
155155
}
156156

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

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

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

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

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

208211
func testAccCloudAccessPolicyTokenCheckDestroy(region string, a *gcom.AuthToken) resource.TestCheckFunc {
209212
return func(s *terraform.State) error {
213+
if a == nil {
214+
return nil
215+
}
210216
client := testutils.Provider.Meta().(*common.Client).GrafanaCloudAPI
211217
token, _, err := client.TokensAPI.GetToken(context.Background(), *a.Id).Region(region).Execute()
212218
if err == nil && token.Name != "" {

0 commit comments

Comments
 (0)