Skip to content

Commit

Permalink
support MoveState
Browse files Browse the repository at this point in the history
  • Loading branch information
ms-henglu committed Nov 13, 2024
1 parent 92a7b1f commit d56fda0
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 84 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## Unreleased
FEATURES:
- `azapi_resource` resource: Support resource move operation, it allows moving resources from `azurerm` provider.

BUG FIXES:
- Fix a bug when `body` contains an unknown float number, the provider will crash.
- Fix the crash that occurs when no tenant ID is configured in Azure CLI.

## v2.0.1
BREAKING CHANGES:
Expand Down
12 changes: 7 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ require (
github.com/hashicorp/go-azure-helpers v0.70.1
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/terraform-plugin-docs v0.19.2
github.com/hashicorp/terraform-plugin-framework v1.11.0
github.com/hashicorp/terraform-plugin-framework v1.12.0
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0
github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-go v0.24.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0
github.com/jmespath/go-jmespath v0.4.0
Expand Down Expand Up @@ -89,11 +89,13 @@ require (
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.23.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
google.golang.org/grpc v1.66.2 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

go 1.21
go 1.22.0

toolchain go1.23.2
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,16 @@ github.com/hashicorp/terraform-plugin-docs v0.19.2 h1:YjdKa1vuqt9EnPYkkrv9HnGZz1
github.com/hashicorp/terraform-plugin-docs v0.19.2/go.mod h1:gad2aP6uObFKhgNE8DR9nsEuEQnibp7il0jZYYOunWY=
github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE=
github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
github.com/hashicorp/terraform-plugin-framework v1.12.0 h1:7HKaueHPaikX5/7cbC1r9d1m12iYHY+FlNZEGxQ42CQ=
github.com/hashicorp/terraform-plugin-framework v1.12.0/go.mod h1:N/IOQ2uYjW60Jp39Cp3mw7I/OpC/GfZ0385R0YibmkE=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U=
github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE=
Expand Down Expand Up @@ -292,8 +296,12 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
Expand Down
187 changes: 112 additions & 75 deletions internal/services/azapi_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"time"
Expand Down Expand Up @@ -47,6 +46,9 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog"
)

const PrivateStateKeyCreateMode = "create_mode"
const FlagMoveState = "move_state"

type AzapiResourceModel struct {
Body types.Dynamic `tfsdk:"body"`
ID types.String `tfsdk:"id"`
Expand Down Expand Up @@ -82,6 +84,7 @@ var _ resource.ResourceWithModifyPlan = &AzapiResource{}
var _ resource.ResourceWithValidateConfig = &AzapiResource{}
var _ resource.ResourceWithImportState = &AzapiResource{}
var _ resource.ResourceWithUpgradeState = &AzapiResource{}
var _ resource.ResourceWithMoveState = &AzapiResource{}

type AzapiResource struct {
ProviderData *clients.Client
Expand Down Expand Up @@ -905,6 +908,15 @@ func (r *AzapiResource) Read(ctx context.Context, request resource.ReadRequest,
state.Body = payload
}

if v, _ := request.Private.GetKey(ctx, PrivateStateKeyCreateMode); v != nil && string(v) == FlagMoveState {
payload, err := flattenBody(responseBody, id.ResourceDef)
if err != nil {
response.Diagnostics.AddError("Invalid body", err.Error())
return
}
state.Body = payload
}

response.Diagnostics.Append(response.State.Set(ctx, state)...)
}

Expand Down Expand Up @@ -956,54 +968,19 @@ func (r *AzapiResource) Delete(ctx context.Context, request resource.DeleteReque
func (r *AzapiResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
tflog.Debug(ctx, fmt.Sprintf("Importing Resource - parsing %q", request.ID))

input := request.ID
idUrl, err := url.Parse(input)
if err != nil {
response.Diagnostics.AddError("Invalid Resource ID", fmt.Errorf("parsing Resource ID %q: %+v", input, err).Error())
return
}
apiVersion := idUrl.Query().Get("api-version")
if apiVersion == "" {
resourceType := utils.GetResourceType(input)
apiVersions := azure.GetApiVersions(resourceType)
if len(apiVersions) != 0 {
input = fmt.Sprintf("%s?api-version=%s", input, apiVersions[len(apiVersions)-1])
}
}

id, err := parse.ResourceIDWithApiVersion(input)
id, err := parse.ResourceID(request.ID)
if err != nil {
response.Diagnostics.AddError("Invalid Resource ID", fmt.Errorf("parsing Resource ID %q: %+v", input, err).Error())
response.Diagnostics.AddError("Invalid Resource ID", fmt.Errorf("parsing Resource ID %q: %+v", request.ID, err).Error())
return
}

client := r.ProviderData.ResourceClient

state := AzapiResourceModel{
ID: types.StringValue(id.ID()),
Name: types.StringValue(id.Name),
ParentID: types.StringValue(id.ParentId),
Type: types.StringValue(fmt.Sprintf("%s@%s", id.AzureResourceType, id.ApiVersion)),
Locks: types.ListNull(types.StringType),
Identity: types.ListNull(identity.Model{}.ModelType()),
Body: types.DynamicNull(),
SchemaValidationEnabled: types.BoolValue(true),
IgnoreCasing: types.BoolValue(false),
IgnoreMissingProperty: types.BoolValue(true),
ResponseExportValues: types.DynamicNull(),
Output: types.DynamicNull(),
ReplaceTriggersExternalValues: types.DynamicNull(),
ReplaceTriggersRefs: types.ListNull(types.StringType),
Tags: types.MapNull(types.StringType),
Timeouts: timeouts.Value{
Object: types.ObjectNull(map[string]attr.Type{
"create": types.StringType,
"update": types.StringType,
"read": types.StringType,
"delete": types.StringType,
}),
},
}
state := r.defaultAzapiResourceModel()
state.ID = types.StringValue(id.ID())
state.Name = types.StringValue(id.Name)
state.ParentID = types.StringValue(id.ParentId)
state.Type = types.StringValue(fmt.Sprintf("%s@%s", id.AzureResourceType, id.ApiVersion))

responseBody, err := client.Get(ctx, id.AzureResourceId, id.ApiVersion, clients.NewRequestOptions(state.ReadHeaders, state.ReadQueryParameters))
if err != nil {
Expand All @@ -1017,39 +994,13 @@ func (r *AzapiResource) ImportState(ctx context.Context, request resource.Import
}

tflog.Info(ctx, fmt.Sprintf("resource %q is imported", id.ID()))
if id.ResourceDef != nil {
writeOnlyBody := (*id.ResourceDef).GetWriteOnly(utils.NormalizeObject(responseBody))
if bodyMap, ok := writeOnlyBody.(map[string]interface{}); ok {
delete(bodyMap, "location")
delete(bodyMap, "tags")
delete(bodyMap, "name")
delete(bodyMap, "identity")
writeOnlyBody = bodyMap
}
data, err := json.Marshal(writeOnlyBody)
if err != nil {
response.Diagnostics.AddError("Invalid body", err.Error())
return
}
payload, err := dynamic.FromJSONImplied(data)
if err != nil {
response.Diagnostics.AddError("Invalid payload", err.Error())
return
}
state.Body = payload
} else {
data, err := json.Marshal(responseBody)
if err != nil {
response.Diagnostics.AddError("Invalid body", err.Error())
return
}
payload, err := dynamic.FromJSONImplied(data)
if err != nil {
response.Diagnostics.AddError("Invalid payload", err.Error())
return
}
state.Body = payload
payload, err := flattenBody(responseBody, id.ResourceDef)
if err != nil {
response.Diagnostics.AddError("Invalid body", err.Error())
return
}
state.Body = payload

if bodyMap, ok := responseBody.(map[string]interface{}); ok {
if v, ok := bodyMap["location"]; ok && v != nil {
state.Location = types.StringValue(location.Normalize(v.(string)))
Expand All @@ -1076,6 +1027,54 @@ func (r *AzapiResource) ImportState(ctx context.Context, request resource.Import
response.Diagnostics.Append(response.State.Set(ctx, state)...)
}

func (r *AzapiResource) MoveState(ctx context.Context) []resource.StateMover {
return []resource.StateMover{
{
SourceSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
},
},
StateMover: func(ctx context.Context, request resource.MoveStateRequest, response *resource.MoveStateResponse) {
if !strings.HasPrefix(request.SourceTypeName, "azurerm") {
response.Diagnostics.AddError("Invalid source type", "The `azapi_resource` resource can only be moved from an `azurerm` resource")
return
}

if request.SourceState == nil {
response.Diagnostics.AddError("Invalid source state", "The source state is nil")
return
}

requestID := ""
if response.Diagnostics.Append(request.SourceState.GetAttribute(ctx, path.Root("id"), &requestID)...); response.Diagnostics.HasError() {
return
}
if requestID == "" {
response.Diagnostics.AddError("Invalid source state", "The source state does not contain an id")
return
}
id, err := parse.ResourceID(requestID)
if err != nil {
response.Diagnostics.AddError("Invalid Resource ID", fmt.Errorf("parsing Resource ID %q: %+v", request, err).Error())

Check failure on line 1061 in internal/services/azapi_resource.go

View workflow job for this annotation

GitHub Actions / test

fmt.Errorf format %q has arg request of wrong type github.com/hashicorp/terraform-plugin-framework/resource.MoveStateRequest
return
}

state := r.defaultAzapiResourceModel()
state.ID = types.StringValue(id.ID())
state.Name = types.StringValue(id.Name)
state.ParentID = types.StringValue(id.ParentId)
state.Type = types.StringValue(fmt.Sprintf("%s@%s", id.AzureResourceType, id.ApiVersion))

response.TargetPrivate.SetKey(ctx, PrivateStateKeyCreateMode, []byte(FlagMoveState))
response.Diagnostics.Append(response.TargetState.Set(ctx, state)...)
},
},
}
}

func (r *AzapiResource) nameWithDefaultNaming(config types.String) (types.String, diag.Diagnostics) {
if !config.IsNull() {
return config, diag.Diagnostics{}
Expand Down Expand Up @@ -1139,6 +1138,44 @@ func (r *AzapiResource) locationWithDefaultLocation(config types.String, body ma
return config
}

func (r *AzapiResource) defaultAzapiResourceModel() AzapiResourceModel {
return AzapiResourceModel{
ID: types.StringNull(),
Name: types.StringNull(),
ParentID: types.StringNull(),
Type: types.StringNull(),
Location: types.StringNull(),
Body: types.Dynamic{},
Identity: types.ListNull(identity.Model{}.ModelType()),
IgnoreCasing: types.BoolValue(false),
IgnoreMissingProperty: types.BoolValue(true),
Locks: types.ListNull(types.StringType),
Output: types.DynamicNull(),
ReplaceTriggersExternalValues: types.DynamicNull(),
ReplaceTriggersRefs: types.ListNull(types.StringType),
ResponseExportValues: types.DynamicNull(),
Retry: retry.RetryValue{},
SchemaValidationEnabled: types.BoolValue(true),
Tags: types.MapNull(types.StringType),
Timeouts: timeouts.Value{
Object: types.ObjectNull(map[string]attr.Type{
"create": types.StringType,
"update": types.StringType,
"read": types.StringType,
"delete": types.StringType,
}),
},
CreateHeaders: nil,
CreateQueryParameters: nil,
UpdateHeaders: nil,
UpdateQueryParameters: nil,
DeleteHeaders: nil,
DeleteQueryParameters: nil,
ReadHeaders: nil,
ReadQueryParameters: nil,
}
}

func expandBody(body map[string]interface{}, model AzapiResourceModel) diag.Diagnostics {
if body == nil {
return diag.Diagnostics{}
Expand Down
24 changes: 22 additions & 2 deletions internal/services/parse/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ func ResourceIDWithResourceType(azureResourceId, resourceType string) (ResourceI
return id, nil
}

// ResourceIDWithApiVersion parses a Resource ID which contains api-version into an ResourceId struct
func ResourceIDWithApiVersion(input string) (ResourceId, error) {
// ResourceIDContainsApiVersion parses a Resource ID which contains api-version into an ResourceId struct
func ResourceIDContainsApiVersion(input string) (ResourceId, error) {
idUrl, err := url.Parse(input)
if err != nil {
return ResourceId{}, err
Expand All @@ -204,6 +204,26 @@ func ResourceIDWithApiVersion(input string) (ResourceId, error) {
return id, nil
}

// ResourceID parses a Resource ID which might not contain api-version into an ResourceId struct, it will append the latest api-version if not provided
func ResourceID(input string) (ResourceId, error) {
idUrl, err := url.Parse(input)
if err != nil {
return ResourceId{}, err
}
apiVersion := idUrl.Query().Get("api-version")
if apiVersion == "" {
resourceType := utils.GetResourceType(input)
apiVersions := azure.GetApiVersions(resourceType)
if len(apiVersions) != 0 {
input = fmt.Sprintf("%s?api-version=%s", input, apiVersions[len(apiVersions)-1])
} else {
return ResourceId{}, fmt.Errorf("ID was missing the `api-version` element")
}
}

return ResourceIDContainsApiVersion(input)
}

func (id ResourceId) String() string {
segments := []string{
fmt.Sprintf("ResourceId %q", id.AzureResourceId),
Expand Down
4 changes: 2 additions & 2 deletions internal/services/parse/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ func Test_ResourceIDWithResourceType(t *testing.T) {
}
}

func Test_ResourceIDWithApiVersion(t *testing.T) {
func Test_ResourceIDContainsApiVersion(t *testing.T) {
testData := []struct {
Input string
Error bool
Expand Down Expand Up @@ -485,7 +485,7 @@ func Test_ResourceIDWithApiVersion(t *testing.T) {
for _, v := range testData {
t.Logf("[DEBUG] Testing %q", v.Input)

actual, err := ResourceIDWithApiVersion(v.Input)
actual, err := ResourceIDContainsApiVersion(v.Input)
if err != nil {
if v.Error {
continue
Expand Down
Loading

0 comments on commit d56fda0

Please sign in to comment.