diff --git a/Makefile b/Makefile index f1029a1..d2b1091 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ default: build -build: +build: @echo "==> Building package..." go build ./... diff --git a/examples/resources/host/main.tf b/examples/resources/host/main.tf new file mode 100644 index 0000000..9654638 --- /dev/null +++ b/examples/resources/host/main.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + aap = { + source = "ansible/aap" + } + } +} + +provider "aap" { + host = "https://localhost:8043" + username = "test" + password = "test" + insecure_skip_verify = true +} + +resource "aap_host" "sample" { + inventory_id = 1 + name = "tf_host" + variables = jsonencode( + { + "foo": "bar" + } + ) + group_id = 2 + disassociate_group = true +} + +output "host" { + value = aap_host.sample +} diff --git a/internal/provider/group_resource.go b/internal/provider/group_resource.go index c01d404..7b4e966 100644 --- a/internal/provider/group_resource.go +++ b/internal/provider/group_resource.go @@ -174,19 +174,7 @@ func (r GroupResource) CreateGroup(data GroupResourceModelInterface) diag.Diagno } resp, body, err := r.client.doRequest(http.MethodPost, "/api/v2/groups/", req_data) - if err != nil { - diags.AddError("Body JSON Marshal Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusCreated { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusCreated, resp.Status)) - return diags - } + diags.Append(IsResponseValid(resp, err, http.StatusCreated)...) err = data.ParseHttpResponse(body) if err != nil { diags.AddError("error while parsing the json response: ", err.Error()) @@ -218,19 +206,7 @@ func (r GroupResource) DeleteGroup(data GroupResourceModelInterface) diag.Diagno var diags diag.Diagnostics resp, _, err := r.client.doRequest(http.MethodDelete, data.GetURL(), nil) - if err != nil { - diags.AddError("Body JSON Marshal Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusNoContent { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusNoContent, resp.Status)) - return diags - } + diags.Append(IsResponseValid(resp, err, http.StatusNoContent)...) return diags } @@ -261,20 +237,8 @@ func (r GroupResource) UpdateGroup(data GroupResourceModelInterface) diag.Diagno req_data = bytes.NewReader(req_body) } resp, body, err := r.client.doRequest(http.MethodPut, data.GetURL(), req_data) + diags.Append(IsResponseValid(resp, err, http.StatusOK)...) - if err != nil { - diags.AddError("Body JSON Marshal Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusOK { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusOK, resp.Status)) - return diags - } err = data.ParseHttpResponse(body) if err != nil { diags.AddError("error while parsing the json response: ", err.Error()) @@ -301,18 +265,7 @@ func (r GroupResource) ReadGroup(data GroupResourceModelInterface) diag.Diagnost // Read existing Group group_url := data.GetURL() resp, body, err := r.client.doRequest(http.MethodGet, group_url, nil) - if err != nil { - diags.AddError("Get Error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusOK { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%s)", http.StatusOK, resp.Status)) - } + diags.Append(IsResponseValid(resp, err, http.StatusOK)...) err = data.ParseHttpResponse(body) if err != nil { diff --git a/internal/provider/host_resource.go b/internal/provider/host_resource.go new file mode 100644 index 0000000..4d2c5c5 --- /dev/null +++ b/internal/provider/host_resource.go @@ -0,0 +1,390 @@ +package provider + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &HostResource{} + _ resource.ResourceWithConfigure = &HostResource{} +) + +// NewHostResource is a helper function to simplify the provider implementation +func NewHostResource() resource.Resource { + return &HostResource{} +} + +type HostResourceModelInterface interface { + ParseHttpResponse(body []byte) error + CreateRequestBody() ([]byte, diag.Diagnostics) + GetURL() string +} + +// HostResource is the resource implementation. +type HostResource struct { + client ProviderHTTPClient +} + +// Metadata returns the resource type name. +func (r *HostResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_host" +} + +// Schema defines the schema for the host resource +func (r *HostResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + + "inventory_id": schema.Int64Attribute{ + Required: true, + }, + "instance_id": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "host_url": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "variables": schema.StringAttribute{ + Optional: true, + }, + "enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: "Defaults true.", + }, + "group_id": schema.Int64Attribute{ + Optional: true, + Description: "Set this option to associate an existing group with a host.", + }, + "disassociate_group": schema.BoolAttribute{ + Optional: true, + Description: "Set group_id and and disassociate_group options to remove " + + "the group from a host without deleting the group.", + }, + }, + } +} + +// HostResourceModel maps the resource schema data. +type HostResourceModel struct { + InventoryId types.Int64 `tfsdk:"inventory_id"` + InstanceId types.String `tfsdk:"instance_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + URL types.String `tfsdk:"host_url"` + Variables types.String `tfsdk:"variables"` + Enabled types.Bool `tfsdk:"enabled"` + GroupId types.Int64 `tfsdk:"group_id"` + DisassociateGroup types.Bool `tfsdk:"disassociate_group"` +} + +func (d *HostResourceModel) GetURL() string { + if IsValueProvided(d.URL) { + return d.URL.ValueString() + } + return "" +} + +func (d *HostResourceModel) CreateRequestBody() ([]byte, diag.Diagnostics) { + body := make(map[string]interface{}) + var diags diag.Diagnostics + + // Inventory id + body["inventory"] = d.InventoryId.ValueInt64() + + // Instance id + body["instance_id"] = d.InstanceId.ValueString() + + // Name + body["name"] = d.Name.ValueString() + + // Variables + if IsValueProvided(d.Variables) { + body["variables"] = d.Variables.ValueString() + } + + // Groups + if IsValueProvided(d.GroupId) { + body["id"] = d.GroupId.ValueInt64() + } + + // DisassociateGroup + if IsValueProvided(d.DisassociateGroup) { + // DisassociateGroup value does not really matter + // To remove a group from a host you only need to pass this parameter + // Add it to the body only if set to true + if d.DisassociateGroup.ValueBool() { + body["disassociate_group"] = true + } + } + + // Description + if IsValueProvided(d.Description) { + body["description"] = d.Description.ValueString() + } + + // Enabled + if IsValueProvided(d.Enabled) { + body["enabled"] = d.Enabled.ValueBool() + } + + json_raw, err := json.Marshal(body) + if err != nil { + diags.Append(diag.NewErrorDiagnostic("Body JSON Marshal Error", err.Error())) + return nil, diags + } + + return json_raw, diags +} + +func (d *HostResourceModel) ParseHttpResponse(body []byte) error { + /* Unmarshal the json string */ + result := make(map[string]interface{}) + + err := json.Unmarshal(body, &result) + if err != nil { + return err + } + + d.Name = types.StringValue(result["name"].(string)) + d.URL = types.StringValue(result["url"].(string)) + + if r, ok := result["instance_id"]; ok { + d.InstanceId = types.StringValue(r.(string)) + } + + if r, ok := result["inventory"]; ok { + d.InventoryId = types.Int64Value(int64(r.(float64))) + } + + if result["description"] != "" { + d.Description = types.StringValue(result["description"].(string)) + } else { + d.Description = types.StringNull() + } + + if result["variables"] != "" { + d.Variables = types.StringValue(result["variables"].(string)) + } else { + d.Variables = types.StringNull() + } + + if r, ok := result["enabled"]; ok && r != nil { + d.Enabled = basetypes.NewBoolValue(r.(bool)) + } + + return nil +} + +// Configure adds the provider configured client to the resource. +func (d *HostResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*AAPClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *AAPClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func MakeReqData(data HostResourceModelInterface) (io.Reader, diag.Diagnostics) { + var diags diag.Diagnostics + var req_data io.Reader = nil + + req_body, diagCreateReq := data.CreateRequestBody() + diags.Append(diagCreateReq...) + + if diags.HasError() { + return nil, diags + } + + if req_body != nil { + req_data = bytes.NewReader(req_body) + } + + return req_data, diags +} + +func (r HostResource) CreateHost(data HostResourceModelInterface) diag.Diagnostics { + req_data, diags := MakeReqData(data) + resp, body, err := r.client.doRequest(http.MethodPost, "/api/v2/hosts/", req_data) + diags.Append(IsResponseValid(resp, err, http.StatusCreated)...) + + err = data.ParseHttpResponse(body) + if err != nil { + diags.AddError("error while parsing the json response: ", err.Error()) + return diags + } + + return diags +} + +func (r HostResource) AssociateGroup(data HostResourceModelInterface) diag.Diagnostics { + req_data, diags := MakeReqData(data) + resp, _, err := r.client.doRequest(http.MethodPost, data.GetURL()+"/groups/", req_data) + diags.Append(IsResponseValid(resp, err, http.StatusNoContent)...) + + return diags +} + +func (r HostResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HostResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(r.CreateHost(&data)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + if IsValueProvided((&data).GroupId) { + resp.Diagnostics.Append(r.AssociateGroup(&data)...) + if resp.Diagnostics.HasError() { + return + } + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + } +} + +func (r HostResource) DeleteHost(data HostResourceModelInterface) diag.Diagnostics { + var diags diag.Diagnostics + + resp, _, err := r.client.doRequest(http.MethodDelete, data.GetURL(), nil) + diags.Append(IsResponseValid(resp, err, http.StatusNoContent)...) + + return diags +} + +func (r HostResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data HostResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + resp.Diagnostics.Append(r.DeleteHost(&data)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r HostResource) UpdateHost(data HostResourceModelInterface) diag.Diagnostics { + req_data, diags := MakeReqData(data) + resp, body, err := r.client.doRequest(http.MethodPut, data.GetURL(), req_data) + diags.Append(IsResponseValid(resp, err, http.StatusOK)...) + + err = data.ParseHttpResponse(body) + if err != nil { + diags.AddError("error while parsing the json response: ", err.Error()) + return diags + } + + return diags +} + +func (r HostResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data HostResourceModel + var data_with_URL HostResourceModel + + // Read Terraform plan and state data into the model + // The URL is generated once the host is created. + // To update the correct host, we retrieve the state data + // and append the URL from the state data to the plan data. + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &data_with_URL)...) + data.URL = data_with_URL.URL + + resp.Diagnostics.Append(r.UpdateHost(&data)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + if IsValueProvided((&data).GroupId) { + resp.Diagnostics.Append(r.AssociateGroup(&data)...) + if resp.Diagnostics.HasError() { + return + } + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + } +} + +func (r HostResource) ReadHost(data HostResourceModelInterface) diag.Diagnostics { + var diags diag.Diagnostics + // Read existing Host + host_url := data.GetURL() + resp, body, err := r.client.doRequest(http.MethodGet, host_url, nil) + diags.Append(IsResponseValid(resp, err, http.StatusOK)...) + + err = data.ParseHttpResponse(body) + if err != nil { + diags.AddError("error while parsing the json response: ", err.Error()) + return diags + } + return diags +} + +func (r HostResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data HostResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(r.ReadHost(&data)...) + if resp.Diagnostics.HasError() { + return + } + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/host_resource_test.go b/internal/provider/host_resource_test.go new file mode 100644 index 0000000..dab82e1 --- /dev/null +++ b/internal/provider/host_resource_test.go @@ -0,0 +1,181 @@ +package provider + +import ( + "bytes" + "context" + "testing" + + fwresource "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestHostResourceSchema(t *testing.T) { + t.Parallel() + + ctx := context.Background() + schemaRequest := fwresource.SchemaRequest{} + schemaResponse := &fwresource.SchemaResponse{} + + // Instantiate the HostResource and call its Schema method + NewHostResource().Schema(ctx, schemaRequest, schemaResponse) + + if schemaResponse.Diagnostics.HasError() { + t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) + } + + // Validate the schema + diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) + + if diagnostics.HasError() { + t.Fatalf("Schema validation diagnostics: %+v", diagnostics) + } +} + +func TestHostResourceCreateRequestBody(t *testing.T) { + var testTable = []struct { + name string + input HostResourceModel + expected []byte + }{ + { + name: "test with unknown values", + input: HostResourceModel{ + Name: types.StringValue("test host"), + Description: types.StringUnknown(), + URL: types.StringUnknown(), + Variables: types.StringNull(), + GroupId: types.Int64Unknown(), + DisassociateGroup: basetypes.NewBoolValue(false), + Enabled: basetypes.NewBoolValue(false), + InventoryId: types.Int64Unknown(), + InstanceId: types.StringNull(), + }, + expected: []byte(`{"enabled":false,"instance_id":"","inventory":0,"name":"test host"}`), + }, + { + name: "test with null values", + input: HostResourceModel{ + Name: types.StringValue("test host"), + Description: types.StringNull(), + URL: types.StringNull(), + Variables: types.StringNull(), + GroupId: types.Int64Null(), + DisassociateGroup: basetypes.NewBoolValue(false), + Enabled: basetypes.NewBoolValue(false), + InventoryId: types.Int64Null(), + InstanceId: types.StringNull(), + }, + expected: []byte(`{"enabled":false,"instance_id":"","inventory":0,"name":"test host"}`), + }, + { + name: "test with some values", + input: HostResourceModel{ + InventoryId: types.Int64Value(1), + Name: types.StringValue("host1"), + Description: types.StringNull(), + URL: types.StringValue("/api/v2/hosts/1/"), + Variables: types.StringValue("{\"foo\":\"bar\"}"), + }, + expected: []byte( + `{"instance_id":"","inventory":1,"name":"host1","variables":"{\"foo\":\"bar\"}"}`, + ), + }, + { + name: "test with group id", + input: HostResourceModel{ + Name: types.StringValue("host1"), + Description: types.StringNull(), + URL: types.StringValue("/api/v2/hosts/1/"), + Variables: types.StringValue("{\"foo\":\"bar\"}"), + GroupId: basetypes.NewInt64Value(2), + }, + expected: []byte( + `{"id":2,"instance_id":"","inventory":0,"name":"host1","variables":"{\"foo\":\"bar\"}"}`, + ), + }, + } + + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + actual, diags := test.input.CreateRequestBody() + if diags.HasError() { + t.Fatal(diags.Errors()) + } + if !bytes.Equal(test.expected, actual) { + t.Errorf("Expected (%s) not equal to actual (%s)", test.expected, actual) + } + }) + } +} + +// CustomError is a custom error type +type CustomError struct { + Message string +} + +// Implement the error interface for Cu +func (e CustomError) Error() string { + return e.Message +} + +func TestHostResourceParseHttpResponse(t *testing.T) { + customErr := CustomError{ + Message: "invalid character 'N' looking for beginning of value", + } + emptyError := CustomError{} + + var testTable = []struct { + name string + input []byte + expected HostResourceModel + errors error + }{ + { + name: "test with JSON error", + input: []byte("Not valid JSON"), + expected: HostResourceModel{}, + errors: customErr, + }, + { + name: "test with missing values", + input: []byte(`{"inventory":1,"name": "host1", "url": "/api/v2/hosts/1/", "description": "", "variables": "", "group_id": 2}`), + expected: HostResourceModel{ + InventoryId: types.Int64Value(1), + Name: types.StringValue("host1"), + URL: types.StringValue("/api/v2/hosts/1/"), + Description: types.StringNull(), + Variables: types.StringNull(), + }, + errors: emptyError, + }, + { + name: "test with all values", + input: []byte( + `{"description":"A basic test host","group_id":1,"name":"host1","disassociate_group":false,` + + `"enabled":false,"url":"/api/v2/hosts/1/","variables":"{\"foo\":\"bar\",\"nested\":{\"foobar\":\"baz\"}}"}`, + ), + expected: HostResourceModel{ + Name: types.StringValue("host1"), + URL: types.StringValue("/api/v2/hosts/1/"), + Description: types.StringValue("A basic test host"), + Variables: types.StringValue("{\"foo\":\"bar\",\"nested\":{\"foobar\":\"baz\"}}"), + Enabled: basetypes.NewBoolValue(false), + }, + errors: emptyError, + }, + } + + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + resource := HostResourceModel{} + err := resource.ParseHttpResponse(test.input) + if test.errors != nil && err != nil && test.errors.Error() != err.Error() { + t.Errorf("Expected error diagnostics (%s), actual was (%s)", test.errors, err) + } + if test.expected != resource { + t.Errorf("Expected (%s) not equal to actual (%s)", test.expected, resource) + } + }) + } +} diff --git a/internal/provider/job_resource.go b/internal/provider/job_resource.go index 684c3cf..93d8ffc 100644 --- a/internal/provider/job_resource.go +++ b/internal/provider/job_resource.go @@ -203,20 +203,8 @@ func (r JobResource) CreateJob(data JobResourceModelInterface) diag.Diagnostics var postURL = "/api/v2/job_templates/" + data.GetTemplateID() + "/launch/" resp, body, err := r.client.doRequest(http.MethodPost, postURL, reqData) + diags.Append(IsResponseValid(resp, err, http.StatusCreated)...) - if err != nil { - diags.AddError("client request error", err.Error()) - return diags - } - if resp == nil { - diags.AddError("Http response Error", "no http response from server") - return diags - } - if resp.StatusCode != http.StatusCreated { - diags.AddError("Unexpected Http Status code", - fmt.Sprintf("expected (%d) got (%d)", http.StatusCreated, resp.StatusCode)) - return diags - } err = data.ParseHTTPResponse(body) if err != nil { diags.AddError("error while parsing the json response: ", err.Error()) @@ -227,18 +215,11 @@ func (r JobResource) CreateJob(data JobResourceModelInterface) diag.Diagnostics func (r JobResource) ReadJob(data JobResourceModelInterface) error { // Read existing Job + var diags diag.Diagnostics jobURL := data.GetURL() if len(jobURL) > 0 { resp, body, err := r.client.doRequest("GET", jobURL, nil) - if err != nil { - return err - } - if resp == nil { - return fmt.Errorf("the server response is null") - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("the server returned status code %d while attempting to Get from URL %s", resp.StatusCode, jobURL) - } + diags.Append(IsResponseValid(resp, err, http.StatusOK)...) err = data.ParseHTTPResponse(body) if err != nil { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b3d4821..b9779b6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -161,6 +161,7 @@ func (p *aapProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ NewJobResource, NewGroupResource, + NewHostResource, } } diff --git a/internal/provider/test_utils.go b/internal/provider/test_utils.go index ae33d85..f749920 100644 --- a/internal/provider/test_utils.go +++ b/internal/provider/test_utils.go @@ -2,11 +2,15 @@ package provider import ( "encoding/json" + "fmt" "io" "net/http" "reflect" "slices" "strings" + "testing" + + "github.com/stretchr/testify/assert" ) // DeepEqualJSONByte compares the JSON in two byte slices. @@ -73,3 +77,51 @@ func (c *MockHTTPClient) doRequest(method string, path string, data io.Reader) ( } return &http.Response{StatusCode: c.httpCode}, result, nil } + +const ( + expectedStatusCode = 200 + unexpectedStatusCode = 404 +) + +func TestIsResponseValid(t *testing.T) { + var testTable = []struct { + resp *http.Response + err error + expected int + }{ + { + resp: &http.Response{ + StatusCode: expectedStatusCode, + Status: "OK", + }, + err: nil, + }, + { + resp: nil, + err: fmt.Errorf("sample error message"), + }, + { + resp: nil, + err: nil, + }, + { + resp: &http.Response{ + StatusCode: unexpectedStatusCode, + Status: "Not Found", + }, + err: nil, + }, + } + + for _, test := range testTable { + t.Run(fmt.Sprintf("Status: %d", expectedStatusCode), func(t *testing.T) { + diags := IsResponseValid(test.resp, test.err, expectedStatusCode) + + if test.resp != nil && test.err == nil && test.resp.StatusCode == expectedStatusCode { + assert.Empty(t, diags, "No errors expected for a successful response") + } else { + assert.NotEmpty(t, diags, "Error expected") + } + }) + } +} diff --git a/internal/provider/utils.go b/internal/provider/utils.go index 4562c9e..45a1ac9 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -1,9 +1,33 @@ package provider import ( + "fmt" + "net/http" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" ) func IsValueProvided(value attr.Value) bool { return !value.IsNull() && !value.IsUnknown() } + +func IsResponseValid(resp *http.Response, err error, expected_status int) diag.Diagnostics { + var diags diag.Diagnostics + + if err != nil { + diags.AddError("Body JSON Marshal Error", err.Error()) + return diags + } + if resp == nil { + diags.AddError("Http response Error", "no http response from server") + return diags + } + if resp.StatusCode != expected_status { + diags.AddError("Unexpected Http Status code", + fmt.Sprintf("expected (%d) got (%s)", expected_status, resp.Status)) + return diags + } + + return diags +}