Skip to content

Commit

Permalink
Add typing to the ID system. Makes it more robust and it will be need…
Browse files Browse the repository at this point in the history
…ed for feature parity with the org ID code in OSS
  • Loading branch information
julienduchesne committed Mar 2, 2024
1 parent b540234 commit 8132ea2
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 79 deletions.
6 changes: 3 additions & 3 deletions internal/common/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ var allResources = []*Resource{}

type Resource struct {
Name string
IDType *TFID
IDType *ResourceID
Schema *schema.Resource
}

func NewResource(name string, idType *TFID, schema *schema.Resource) *Resource {
func NewResource(name string, idType *ResourceID, schema *schema.Resource) *Resource {
r := &Resource{
Name: name,
IDType: idType,
Expand All @@ -32,7 +32,7 @@ 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])
fields[i] = fmt.Sprintf("{{ %s }}", id.expectedFields[i].Name)
}
return fmt.Sprintf(`terraform import %s.name %q
`, r.Name, strings.Join(fields, defaultSeparator))
Expand Down
111 changes: 83 additions & 28 deletions internal/common/resource_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,123 @@ import (
"strings"
)

var (
defaultSeparator = ":"
type ResourceIDFieldType string

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

type TFID struct {
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 []string
expectedFields []ResourceIDField
}

func NewTFID(expectedFields ...string) *TFID {
return newTFIDWithSeparators([]string{defaultSeparator}, expectedFields...)
func NewResourceID(expectedFields ...ResourceIDField) *ResourceID {
return newResourceIDWithSeparators([]string{defaultSeparator}, expectedFields...)
}

// Deprecated: Use NewTFID instead
// 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 NewTFID and remove uses of this function
func NewTFIDWithLegacySeparator(legacySeparator string, expectedFields ...string) *TFID {
return newTFIDWithSeparators([]string{defaultSeparator, legacySeparator}, expectedFields...)
// 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 newTFIDWithSeparators(separators []string, expectedFields ...string) *TFID {
tfID := &TFID{
func newResourceIDWithSeparators(separators []string, expectedFields ...ResourceIDField) *ResourceID {
tfID := &ResourceID{
separators: separators,
expectedFields: expectedFields,
}
return tfID
}

func (id *TFID) Make(parts ...any) string {
// 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)
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 *TFID) AsInt64(resourceID string) (int64, error) {
parts, err := id.Split(resourceID)
if err != nil {
return 0, err
}
return strconv.ParseInt(parts[0], 10, 64)
return strings.Join(stringParts, defaultSeparator)
}

func (id *TFID) AsString(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 "", err
return nil, err
}

return parts[0], nil
}

func (id *TFID) Split(resourceID string) ([]string, error) {
// 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))
}
12 changes: 8 additions & 4 deletions internal/resources/cloud/resource_cloud_access_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import (
)

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

func resourceAccessPolicy() *common.Resource {
Expand Down Expand Up @@ -170,7 +174,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 @@ -190,7 +194,7 @@ func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client *
}
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 @@ -217,7 +221,7 @@ func deleteCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client
}
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
12 changes: 8 additions & 4 deletions internal/resources/cloud/resource_cloud_access_policy_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import (
)

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

func resourceAccessPolicyToken() *common.Resource {
Expand Down Expand Up @@ -143,7 +147,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 @@ -160,7 +164,7 @@ func readCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, cli
}
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 Down Expand Up @@ -188,6 +192,6 @@ func deleteCloudAccessPolicyToken(ctx context.Context, d *schema.ResourceData, c
}
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)
}
10 changes: 7 additions & 3 deletions internal/resources/cloud/resource_cloud_api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import (

var (
cloudAPIKeyRoles = []string{"Viewer", "Editor", "Admin", "MetricsPublisher", "PluginPublisher"}
resourceAPIKeyID = common.NewTFIDWithLegacySeparator("-", "orgSlug", "apiKeyName") //nolint:staticcheck
//nolint:staticcheck
resourceAPIKeyID = common.NewResourceIDWithLegacySeparator("-",
common.StringIDField("orgSlug"),
common.StringIDField("apiKeyName"),
)
)

func resourceAPIKey() *common.Resource {
Expand Down Expand Up @@ -101,7 +105,7 @@ func resourceAPIKeyRead(ctx context.Context, d *schema.ResourceData, c *gcom.API
}
org, name := split[0], split[1]

resp, _, err := c.OrgsAPI.GetApiKey(ctx, name, org).Execute()
resp, _, err := c.OrgsAPI.GetApiKey(ctx, name.(string), org.(string)).Execute()
if err != nil {
return apiError(err)
}
Expand All @@ -121,7 +125,7 @@ func resourceAPIKeyDelete(ctx context.Context, d *schema.ResourceData, c *gcom.A
}
org, name := split[0], split[1]

_, err = c.OrgsAPI.DelApiKey(ctx, name, org).XRequestId(ClientRequestID()).Execute()
_, err = c.OrgsAPI.DelApiKey(ctx, name.(string), org.(string)).XRequestId(ClientRequestID()).Execute()
d.SetId("")
return apiError(err)
}
10 changes: 7 additions & 3 deletions internal/resources/cloud/resource_cloud_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import (
)

var (
resourcePluginInstallationID = common.NewTFIDWithLegacySeparator("_", "stackSlug", "pluginSlug") //nolint:staticcheck
//nolint:staticcheck
resourcePluginInstallationID = common.NewResourceIDWithLegacySeparator("_",
common.StringIDField("stackSlug"),
common.StringIDField("pluginSlug"),
)
)

func resourcePluginInstallation() *common.Resource {
Expand Down Expand Up @@ -89,7 +93,7 @@ func resourcePluginInstallationRead(ctx context.Context, d *schema.ResourceData,
}
stackSlug, pluginSlug := split[0], split[1]

installation, _, err := client.InstancesAPI.GetInstancePlugin(ctx, stackSlug, pluginSlug).Execute()
installation, _, err := client.InstancesAPI.GetInstancePlugin(ctx, stackSlug.(string), pluginSlug.(string)).Execute()
if err, shouldReturn := common.CheckReadError("plugin", d, err); shouldReturn {
return err
}
Expand All @@ -109,6 +113,6 @@ func resourcePluginInstallationDelete(ctx context.Context, d *schema.ResourceDat
}
stackSlug, pluginSlug := split[0], split[1]

_, _, err = client.InstancesAPI.DeleteInstancePlugin(ctx, stackSlug, pluginSlug).XRequestId(ClientRequestID()).Execute()
_, _, err = client.InstancesAPI.DeleteInstancePlugin(ctx, stackSlug.(string), pluginSlug.(string)).XRequestId(ClientRequestID()).Execute()
return apiError(err)
}
16 changes: 8 additions & 8 deletions internal/resources/cloud/resource_cloud_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const defaultReadinessTimeout = time.Minute * 5
var (
stackLabelRegex = regexp.MustCompile(`^[a-zA-Z0-9/\-.]+$`)
stackSlugRegex = regexp.MustCompile(`^[a-z][a-z0-9]+$`)
resourceStackID = common.NewTFID("stackSlugOrID")
resourceStackID = common.NewResourceID(common.StringIDField("stackSlugOrID"))
)

func resourceStack() *common.Resource {
Expand Down Expand Up @@ -254,7 +254,7 @@ func createStack(ctx context.Context, d *schema.ResourceData, client *gcom.APICl
}

func updateStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
id, err := resourceStackID.AsString(d.Id())
id, err := resourceStackID.Single(d.Id())
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -272,7 +272,7 @@ func updateStack(ctx context.Context, d *schema.ResourceData, client *gcom.APICl
Url: &url,
Labels: common.Ref(common.UnpackMap[string](d.Get("labels"))),
}
req := client.InstancesAPI.PostInstance(ctx, id).PostInstanceRequest(stack).XRequestId(ClientRequestID())
req := client.InstancesAPI.PostInstance(ctx, id.(string)).PostInstanceRequest(stack).XRequestId(ClientRequestID())
_, _, err = req.Execute()
if err != nil {
return apiError(err)
Expand All @@ -286,23 +286,23 @@ func updateStack(ctx context.Context, d *schema.ResourceData, client *gcom.APICl
}

func deleteStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
id, err := resourceStackID.AsString(d.Id())
id, err := resourceStackID.Single(d.Id())
if err != nil {
return diag.FromErr(err)
}

req := client.InstancesAPI.DeleteInstance(ctx, id).XRequestId(ClientRequestID())
req := client.InstancesAPI.DeleteInstance(ctx, id.(string)).XRequestId(ClientRequestID())
_, _, err = req.Execute()
return apiError(err)
}

func readStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClient) diag.Diagnostics {
id, err := resourceStackID.AsString(d.Id())
id, err := resourceStackID.Single(d.Id())
if err != nil {
return diag.FromErr(err)
}

req := client.InstancesAPI.GetInstance(ctx, id)
req := client.InstancesAPI.GetInstance(ctx, id.(string))
stack, _, err := req.Execute()
if err, shouldReturn := common.CheckReadError("stack", d, err); shouldReturn {
return err
Expand All @@ -314,7 +314,7 @@ func readStack(ctx context.Context, d *schema.ResourceData, client *gcom.APIClie
return nil
}

connectionsReq := client.InstancesAPI.GetConnections(ctx, id)
connectionsReq := client.InstancesAPI.GetConnections(ctx, id.(string))
connections, _, err := connectionsReq.Execute()
if err != nil {
return apiError(err)
Expand Down
Loading

0 comments on commit 8132ea2

Please sign in to comment.