Skip to content

Commit

Permalink
restful_operation - Hornor Content-Type header when constructing …
Browse files Browse the repository at this point in the history
…the request body
  • Loading branch information
magodo committed Aug 12, 2024
1 parent b021ba9 commit 76ab171
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 28 deletions.
38 changes: 33 additions & 5 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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":
Expand Down
10 changes: 9 additions & 1 deletion internal/provider/operation_jsonserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 3 additions & 21 deletions internal/provider/operation_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
}
Expand Down Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions internal/provider/resource_azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
11 changes: 10 additions & 1 deletion internal/provider/resource_jsonserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand Down Expand Up @@ -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{
{
Expand Down

0 comments on commit 76ab171

Please sign in to comment.