diff --git a/internal/client/client.go b/internal/client/client.go index af41b9b..7d47ede 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -11,6 +11,8 @@ import ( "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/magodo/terraform-provider-restful/internal/dynamic" "golang.org/x/net/publicsuffix" ) @@ -253,16 +255,42 @@ type OperationOption struct { Header Header } -func (c *Client) Operation(ctx context.Context, path string, body interface{}, opt OperationOption) (*resty.Response, error) { +func (c *Client) Operation(ctx context.Context, path string, body basetypes.DynamicValue, opt OperationOption) (*resty.Response, error) { c.SetLoggerContext(ctx) req := c.R().SetContext(ctx) - if body != "" { - req.SetBody(body) - } req.SetQueryParamsFromValues(url.Values(opt.Query)) - req.SetHeaders(opt.Header) + + // By default set the content-type to application/json + // This can be replaced by the opt.Header if defined. req = req.SetHeader("Content-Type", "application/json") + req.SetHeaders(opt.Header) + + if !body.IsNull() { + switch req.Header.Get("Content-Type") { + case "application/json": + b, err := dynamic.ToJSON(body) + if err != nil { + return nil, fmt.Errorf("convert body from dynamic to json: %v", err) + } + + req.SetBody(b) + case "application/x-www-form-urlencoded": + ov, ok := body.UnderlyingValue().(types.Object) + if !ok { + return nil, fmt.Errorf("body expects to be an object, got=%T", body.UnderlyingValue()) + } + m := map[string]string{} + for k, v := range ov.Attributes() { + vs, ok := v.(types.String) + if !ok { + return nil, fmt.Errorf("body value expects to be a string, got=%T", v) + } + m[k] = vs.ValueString() + } + req.SetFormData(m) + } + } switch opt.Method { case "POST": diff --git a/internal/provider/operation_jsonserver_test.go b/internal/provider/operation_jsonserver_test.go index bcff632..beb7a59 100644 --- a/internal/provider/operation_jsonserver_test.go +++ b/internal/provider/operation_jsonserver_test.go @@ -20,6 +20,14 @@ func (d jsonServerOperation) precheck(t *testing.T) { return } +func (d jsonServerOperation) precheckMigrate(t *testing.T) { + d.precheck(t) + if _, ok := os.LookupEnv(RESTFUL_MIGRATE_TEST); !ok { + t.Skipf("%q is not specified", RESTFUL_MIGRATE_TEST) + } + return +} + func newJsonServerOperation() jsonServerOperation { return jsonServerOperation{ url: os.Getenv(RESTFUL_JSON_SERVER_URL), @@ -76,7 +84,7 @@ func TestOperation_JSONServer_withDelete(t *testing.T) { func TestOperation_JSONServer_MigrateV0ToV1(t *testing.T) { d := newJsonServerOperation() resource.Test(t, resource.TestCase{ - PreCheck: func() { d.precheck(t) }, + PreCheck: func() { d.precheckMigrate(t) }, Steps: []resource.TestStep{ { ProtoV6ProviderFactories: nil, diff --git a/internal/provider/operation_resource.go b/internal/provider/operation_resource.go index 214675d..7b0efd4 100644 --- a/internal/provider/operation_resource.go +++ b/internal/provider/operation_resource.go @@ -205,16 +205,7 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla } defer unlockFunc() - b, err := dynamic.ToJSON(plan.Body) - if err != nil { - diagnostics.AddError( - "Error to marshal body", - err.Error(), - ) - return - } - - response, err := c.Operation(ctx, plan.Path.ValueString(), string(b), *opt) + response, err := c.Operation(ctx, plan.Path.ValueString(), plan.Body, *opt) if err != nil { diagnostics.AddError( "Error to call operation", @@ -236,7 +227,7 @@ func (r *OperationResource) createOrUpdate(ctx context.Context, tfplan tfsdk.Pla if err != nil { diagnostics.AddError( fmt.Sprintf("Failed to build the id for this resource"), - fmt.Sprintf("Can't build resource id with `id_builder`: %q, `path`: %q, `body`: %q: %v", plan.IdBuilder.ValueString(), plan.Path.ValueString(), string(b), err), + fmt.Sprintf("Can't build resource id with `id_builder`: %q, `path`: %q: %v", plan.IdBuilder.ValueString(), plan.Path.ValueString(), err), ) return } @@ -377,16 +368,7 @@ func (r *OperationResource) Delete(ctx context.Context, req resource.DeleteReque } } - b, err := dynamic.ToJSON(state.DeleteBody) - if err != nil { - resp.Diagnostics.AddError( - "Error to marshal delete body", - err.Error(), - ) - return - } - - response, err := c.Operation(ctx, path, string(b), *opt) + response, err := c.Operation(ctx, path, state.DeleteBody, *opt) if err != nil { resp.Diagnostics.AddError( "Delete: Error to call operation", diff --git a/internal/provider/resource_azure_test.go b/internal/provider/resource_azure_test.go index 9e7f873..6bc1a37 100644 --- a/internal/provider/resource_azure_test.go +++ b/internal/provider/resource_azure_test.go @@ -307,6 +307,23 @@ func TestOperationResource_Azure_Register_RP(t *testing.T) { }) } +func TestOperationResource_Azure_GetToken(t *testing.T) { + addr := "restful_operation.test" + d := newAzureData() + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { d.precheck(t) }, + ProtoV6ProviderFactories: acceptance.ProviderFactory(), + Steps: []resource.TestStep{ + { + Config: d.getToken(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(addr, "output.%"), + ), + }, + }, + }) +} + func (d azureData) CheckDestroy(addr string) func(*terraform.State) error { return func(s *terraform.State) error { c, err := client.New(context.TODO(), d.url, &client.BuildOption{ @@ -1005,3 +1022,26 @@ resource "restful_operation" "test" { } `, d.url, d.clientId, d.clientSecret, d.tenantId, d.subscriptionId, rp) } + +func (d azureData) getToken() string { + return fmt.Sprintf(` +provider "restful" { + base_url = "https://login.microsoftonline.com" +} + +resource "restful_operation" "test" { + path = "/%s/oauth2/v2.0/token" + method = "POST" + header = { + Accept : "application/json", + Content-Type : "application/x-www-form-urlencoded", + } + body = { + client_id = "%s" + client_secret = "%s" + grant_type = "client_credentials" + scope = "https://management.azure.com/.default" + } +} +`, d.tenantId, d.clientId, d.clientSecret) +} diff --git a/internal/provider/resource_jsonserver_test.go b/internal/provider/resource_jsonserver_test.go index 4984e26..7af41aa 100644 --- a/internal/provider/resource_jsonserver_test.go +++ b/internal/provider/resource_jsonserver_test.go @@ -14,6 +14,7 @@ import ( ) const RESTFUL_JSON_SERVER_URL = "RESTFUL_JSON_SERVER_URL" +const RESTFUL_MIGRATE_TEST = "RESTFUL_MIGRATE_TEST" type jsonServerData struct { url string @@ -26,6 +27,14 @@ func (d jsonServerData) precheck(t *testing.T) { return } +func (d jsonServerData) precheckMigrate(t *testing.T) { + d.precheck(t) + if _, ok := os.LookupEnv(RESTFUL_MIGRATE_TEST); !ok { + t.Skipf("%q is not specified", RESTFUL_MIGRATE_TEST) + } + return +} + func newJsonServerData() jsonServerData { return jsonServerData{ url: os.Getenv(RESTFUL_JSON_SERVER_URL), @@ -181,7 +190,7 @@ func TestResource_JSONServer_MigrateV0ToV1(t *testing.T) { addr := "restful_resource.test" d := newJsonServerData() resource.Test(t, resource.TestCase{ - PreCheck: func() { d.precheck(t) }, + PreCheck: func() { d.precheckMigrate(t) }, CheckDestroy: d.CheckDestroy(addr), Steps: []resource.TestStep{ {