From 6992c3c0e533c6e00d7bf0fab3c802dfded52e65 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Sat, 11 Nov 2023 15:46:05 -0500 Subject: [PATCH 1/7] Stilly script to make JSON more HCL-like --- json2hcl.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100755 json2hcl.py diff --git a/json2hcl.py b/json2hcl.py new file mode 100755 index 0000000..ae5efe0 --- /dev/null +++ b/json2hcl.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +# Really just changes ":" to "=" for map entries. +# There seems to be no convenient way to eliminate the quotes around the keys. + +import json +from sys import stdin, stdout + +content = json.load( stdin ) +stdout.write("jsonencode(") +json.dump( content, stdout, indent='\t', separators=(',', ' = ')) +stdout.write(")") + From f9a29ddb387771fcd74906bb39cd9d3584f3f136 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Tue, 14 Nov 2023 16:05:05 -0500 Subject: [PATCH 2/7] Bosk client fixes --- internal/provider/bosk_client.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/provider/bosk_client.go b/internal/provider/bosk_client.go index 9f04da7..f48377f 100644 --- a/internal/provider/bosk_client.go +++ b/internal/provider/bosk_client.go @@ -29,6 +29,11 @@ func (client *BoskClient) GetJSONAsString(url string, diag *diag.Diagnostics) st defer httpResp.Body.Close() + if httpResp.StatusCode/100 != 2 { + diag.AddError("Client Error", fmt.Sprintf("GET returned unexpected status: %s", httpResp.Status)) + return "ERROR" + } + bytes, err := io.ReadAll(httpResp.Body) if err != nil { diag.AddError( @@ -105,6 +110,6 @@ func (client *BoskClient) Delete(url string, diag *diag.Diagnostics) { defer httpResp.Body.Close() if httpResp.StatusCode/100 != 2 { - diag.AddError("Client Error", fmt.Sprintf("PUT returned unexpected status: %s", httpResp.Status)) + diag.AddError("Client Error", fmt.Sprintf("DELETE returned unexpected status: %s", httpResp.Status)) } } From 7403028925d4200747a0dda81e0e3dbcf234e716 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Tue, 14 Nov 2023 19:37:45 -0500 Subject: [PATCH 3/7] Add provider settings: base_url and basic_auth_var_suffix --- internal/provider/bosk_client.go | 47 +++++++++++++-- internal/provider/node_data_source.go | 17 +++--- internal/provider/node_data_source_test.go | 13 ++++- internal/provider/node_resource.go | 39 ++++++------- internal/provider/node_resource_test.go | 23 +++++--- internal/provider/provider.go | 67 +++++++++++++++++++--- internal/provider/provider_test.go | 16 ++++++ main.go | 1 - 8 files changed, 168 insertions(+), 55 deletions(-) diff --git a/internal/provider/bosk_client.go b/internal/provider/bosk_client.go index f48377f..a2c3e0e 100644 --- a/internal/provider/bosk_client.go +++ b/internal/provider/bosk_client.go @@ -13,24 +13,55 @@ import ( type BoskClient struct { httpClient *http.Client + urlPrefix string + auth *BasicAuth } -func NewBoskClient(httpClient *http.Client) *BoskClient { - return &BoskClient{httpClient: httpClient} +type BasicAuth struct { + username string + password string +} + +func NewBoskClientWithoutAuth(httpClient *http.Client, urlPrefix string) *BoskClient { + return &BoskClient{ + httpClient: httpClient, + urlPrefix: urlPrefix, + auth: nil, + } +} + +func NewBoskClient(httpClient *http.Client, urlPrefix string, username string, password string) *BoskClient { + return &BoskClient{ + httpClient: httpClient, + urlPrefix: urlPrefix, + auth: &BasicAuth{ + username: username, + password: password, + }, + } } // Portions taken from: https://github.com/hashicorp/terraform-provider-http/blob/main/internal/provider/data_source_http.go func (client *BoskClient) GetJSONAsString(url string, diag *diag.Diagnostics) string { - httpResp, err := client.httpClient.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + diag.AddError("Client Error", fmt.Sprintf("Unable to create HTTP request: %s", err)) + return "ERROR" + } + + if client.auth != nil { + req.SetBasicAuth(client.auth.username, client.auth.password) + } + httpResp, err := client.httpClient.Do(req) if err != nil { - diag.AddError("Client Error", fmt.Sprintf("Unable to GET node: %s", err)) + diag.AddError("Client Error", fmt.Sprintf("Unable to %v %v: %s", req.Method, url, err)) return "ERROR" } defer httpResp.Body.Close() if httpResp.StatusCode/100 != 2 { - diag.AddError("Client Error", fmt.Sprintf("GET returned unexpected status: %s", httpResp.Status)) + diag.AddError("Client Error", fmt.Sprintf("%v %v returned unexpected status %s", req.Method, url, httpResp.Status)) return "ERROR" } @@ -80,6 +111,9 @@ func (client *BoskClient) PutJSONAsString(url string, value string, diag *diag.D diag.AddError("Client Error", fmt.Sprintf("Unable to create HTTP PUT request: %s", err)) return } + if client.auth != nil { + req.SetBasicAuth(client.auth.username, client.auth.password) + } httpResp, err := client.httpClient.Do(req) if err != nil { @@ -96,6 +130,9 @@ func (client *BoskClient) PutJSONAsString(url string, value string, diag *diag.D func (client *BoskClient) Delete(url string, diag *diag.Diagnostics) { req, err := http.NewRequest("DELETE", url, nil) + if client.auth != nil { + req.SetBasicAuth(client.auth.username, client.auth.password) + } if err != nil { diag.AddError("Client Error", fmt.Sprintf("Unable to create HTTP DELETE request: %s", err)) return diff --git a/internal/provider/node_data_source.go b/internal/provider/node_data_source.go index f127d3e..e00165d 100644 --- a/internal/provider/node_data_source.go +++ b/internal/provider/node_data_source.go @@ -6,7 +6,6 @@ package provider import ( "context" "fmt" - "net/http" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -28,7 +27,7 @@ type NodeDataSource struct { // NodeDataSourceModel describes the data source data model. type NodeDataSourceModel struct { - URL types.String `tfsdk:"url"` + Path types.String `tfsdk:"path"` Value_json types.String `tfsdk:"value_json"` } @@ -41,8 +40,8 @@ func (d *NodeDataSource) Schema(ctx context.Context, req datasource.SchemaReques MarkdownDescription: "Bosk state tree node data source", Attributes: map[string]schema.Attribute{ - "url": schema.StringAttribute{ // TODO: Separate base URL versus path? - MarkdownDescription: "The HTTP address of the node", + "path": schema.StringAttribute{ + MarkdownDescription: "When appended to the provider base_url, gives the HTTP address of the node", Required: true, }, "value_json": schema.StringAttribute{ @@ -59,18 +58,18 @@ func (d *NodeDataSource) Configure(ctx context.Context, req datasource.Configure return } - client, ok := req.ProviderData.(*http.Client) + client, ok := req.ProviderData.(*BoskClient) if !ok { resp.Diagnostics.AddError( "Unexpected Data Source Configure Type", - fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + fmt.Sprintf("Expected *BoskClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) return } - d.client = NewBoskClient(client) + d.client = client } func (d *NodeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { @@ -82,7 +81,7 @@ func (d *NodeDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } - result_json := d.client.GetJSONAsString(data.URL.ValueString(), &resp.Diagnostics) + result_json := d.client.GetJSONAsString(d.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -90,7 +89,7 @@ func (d *NodeDataSource) Read(ctx context.Context, req datasource.ReadRequest, r data.Value_json = types.StringValue(result_json) tflog.Debug(ctx, "read bosk node datasource", map[string]interface{}{ - "url": data.URL.ValueString(), + "path": data.Path.ValueString(), }) // Save data into Terraform state diff --git a/internal/provider/node_data_source_test.go b/internal/provider/node_data_source_test.go index 87f51d0..47e1929 100644 --- a/internal/provider/node_data_source_test.go +++ b/internal/provider/node_data_source_test.go @@ -28,6 +28,9 @@ func TestAccNodeDataSource(t *testing.T) { })) defer testServer.Close() + base, _ := splitURL(testServer.URL) + path := "/bosk/path/to/object" + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, @@ -35,13 +38,17 @@ func TestAccNodeDataSource(t *testing.T) { // Read testing { Config: fmt.Sprintf(` + provider "bosk" { + base_url = "%s" + basic_auth_var_suffix = "NO_AUTH" + } data "bosk_node" "test" { - url = "%s" + path = "%s" value_json = jsonencode([]) } - `, testServer.URL), + `, base, path), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.bosk_node.test", "url", testServer.URL), + resource.TestCheckResourceAttr("data.bosk_node.test", "path", path), resource.TestCheckResourceAttr("data.bosk_node.test", "value_json", "[{\"world\":{\"id\":\"world\"}}]"), ), }, diff --git a/internal/provider/node_resource.go b/internal/provider/node_resource.go index dcaf1d8..98b3d55 100644 --- a/internal/provider/node_resource.go +++ b/internal/provider/node_resource.go @@ -6,7 +6,6 @@ package provider import ( "context" "fmt" - "net/http" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -29,7 +28,7 @@ type NodeResource struct { // NodeResourceModel describes the resource data model. type NodeResourceModel struct { - URL types.String `tfsdk:"url"` + Path types.String `tfsdk:"path"` Value_json types.String `tfsdk:"value_json"` } @@ -42,8 +41,8 @@ func (r *NodeResource) Schema(ctx context.Context, req resource.SchemaRequest, r MarkdownDescription: "Bosk state tree node data source", Attributes: map[string]schema.Attribute{ - "url": schema.StringAttribute{ // TODO: Separate base URL versus path? - MarkdownDescription: "The HTTP address of the node", + "path": schema.StringAttribute{ + MarkdownDescription: "When appended to the provider base_url, gives the HTTP address of the node", Required: true, }, "value_json": schema.StringAttribute{ @@ -60,18 +59,18 @@ func (r *NodeResource) Configure(ctx context.Context, req resource.ConfigureRequ return } - client, ok := req.ProviderData.(*http.Client) + client, ok := req.ProviderData.(*BoskClient) if !ok { resp.Diagnostics.AddError( "Unexpected Data Source Configure Type", - fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + fmt.Sprintf("Expected *BoskClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) return } - r.client = NewBoskClient(client) + r.client = client } func (r *NodeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -84,14 +83,14 @@ func (r *NodeResource) Create(ctx context.Context, req resource.CreateRequest, r return } - r.client.PutJSONAsString(data.URL.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) + r.client.PutJSONAsString(r.client.urlPrefix + data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { tflog.Warn(ctx, "Error performing PUT", map[string]interface{}{"diagnostics": resp.Diagnostics}) return } tflog.Debug(ctx, "created bosk node", map[string]interface{}{ - "url": data.URL.ValueString(), + "path": data.Path.ValueString(), }) // Save data into Terraform state @@ -99,7 +98,7 @@ func (r *NodeResource) Create(ctx context.Context, req resource.CreateRequest, r } func (r *NodeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data NodeDataSourceModel + var data NodeResourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -108,7 +107,7 @@ func (r *NodeResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - result_json := r.client.GetJSONAsString(data.URL.ValueString(), &resp.Diagnostics) + result_json := r.client.GetJSONAsString(r.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { tflog.Warn(ctx, "Error performing GET", map[string]interface{}{"diagnostics": resp.Diagnostics}) return @@ -117,7 +116,7 @@ func (r *NodeResource) Read(ctx context.Context, req resource.ReadRequest, resp data.Value_json = types.StringValue(result_json) tflog.Debug(ctx, "read bosk node", map[string]interface{}{ - "url": data.URL.ValueString(), + "url": data.Path.ValueString(), }) // Save data into Terraform state @@ -137,13 +136,13 @@ func (r *NodeResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - r.client.PutJSONAsString(data.URL.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) + r.client.PutJSONAsString(r.client.urlPrefix + data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { return } tflog.Debug(ctx, "updated bosk node", map[string]interface{}{ - "url": data.URL.ValueString(), + "path": data.Path.ValueString(), }) // Save data into Terraform state @@ -158,13 +157,13 @@ func (r *NodeResource) Delete(ctx context.Context, req resource.DeleteRequest, r return } - r.client.Delete(data.URL.ValueString(), &resp.Diagnostics) + r.client.Delete(r.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { return } tflog.Debug(ctx, "deleted bosk node", map[string]interface{}{ - "url": data.URL.ValueString(), + "path": data.Path.ValueString(), }) // Save data into Terraform state @@ -172,20 +171,20 @@ func (r *NodeResource) Delete(ctx context.Context, req resource.DeleteRequest, r } func (r *NodeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - url := req.ID + path := req.ID - result_json := r.client.GetJSONAsString(url, &resp.Diagnostics) + result_json := r.client.GetJSONAsString(r.client.urlPrefix + path, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } data := NodeResourceModel{ - URL: types.StringValue(url), + Path: types.StringValue(path), Value_json: types.StringValue(result_json), } tflog.Debug(ctx, "imported bosk node", map[string]interface{}{ - "url": data.URL.ValueString(), + "path": data.Path.ValueString(), }) // Save data into Terraform state diff --git a/internal/provider/node_resource_test.go b/internal/provider/node_resource_test.go index 4371a25..e30558a 100644 --- a/internal/provider/node_resource_test.go +++ b/internal/provider/node_resource_test.go @@ -52,17 +52,20 @@ func TestAccNodeResource(t *testing.T) { })) defer testServer.Close() + base, _ := splitURL(testServer.URL) + path := "/bosk/path/to/object" + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Create and Read testing { - Config: testAccNodeResourceConfig(testServer.URL, []map[string]map[string]string{ + Config: testAccNodeResourceConfig(base, path, []map[string]map[string]string{ {"world": {"id": "world"}}, }), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("bosk_node.test", "url", testServer.URL), + resource.TestCheckResourceAttr("bosk_node.test", "path", path), resource.TestCheckResourceAttr("bosk_node.test", "value_json", "[{\"world\":{\"id\":\"world\"}}]"), ), }, @@ -71,12 +74,12 @@ func TestAccNodeResource(t *testing.T) { ResourceName: "bosk_node.test", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIdentifierAttribute: "url", - ImportStateId: testServer.URL, + ImportStateVerifyIdentifierAttribute: "path", + ImportStateId: path, }, // // Update and Read testing { - Config: testAccNodeResourceConfig(testServer.URL, []map[string]map[string]string{ + Config: testAccNodeResourceConfig(base, path, []map[string]map[string]string{ {"someone": {"id": "someone"}}, {"anyone": {"id": "anyone"}}, }), @@ -89,15 +92,19 @@ func TestAccNodeResource(t *testing.T) { }) } -func testAccNodeResourceConfig(url string, value any) string { +func testAccNodeResourceConfig(base, path string, value any) string { json, err := json.Marshal(value) if err != nil { panic(err) } return fmt.Sprintf(` + provider "bosk" { + base_url = "%s" + basic_auth_var_suffix = "NO_AUTH" + } resource "bosk_node" "test" { - url = "%s" + path = "%s" value_json = %s } - `, url, strconv.Quote(string(json))) + `, base, path, strconv.Quote(string(json))) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8c1b36a..29ea545 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -5,7 +5,10 @@ package provider import ( "context" + "fmt" "net/http" + "os" + "strings" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -27,7 +30,8 @@ type BoskProvider struct { // BoskProviderModel describes the provider data model. type BoskProviderModel struct { - Endpoint types.String `tfsdk:"endpoint"` + BaseURL types.String `tfsdk:"base_url"` + BasicAuthVarSuffix types.String `tfsdk:"basic_auth_var_suffix"` } func (p *BoskProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { @@ -38,9 +42,13 @@ func (p *BoskProvider) Metadata(ctx context.Context, req provider.MetadataReques func (p *BoskProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ - "endpoint": schema.StringAttribute{ - MarkdownDescription: "Node provider attribute", - Optional: true, + "base_url": schema.StringAttribute{ + MarkdownDescription: "Specifies the URL of the bosk API. Used as a prefix for all HTTP requests. Ends with a slash.", + Required: true, + }, + "basic_auth_var_suffix": schema.StringAttribute{ + MarkdownDescription: "Selects the environment variables to use for HTTP basic authentication; namely TF_BOSK_USERNAME_xxx and TF_BOSK_PASSWORD_xxx. If you don't want to use basic auth, set both these variables to blanks.", + Required: true, }, }, } @@ -50,16 +58,57 @@ func (p *BoskProvider) Configure(ctx context.Context, req provider.ConfigureRequ var data BoskProviderModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { return } - // Configuration values are now available. - // if data.Endpoint.IsNull() { /* ... */ } + var baseURL string = data.BaseURL.ValueString() + if (!(strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://"))) { + resp.Diagnostics.AddError( + "Base URL must be http or https", + fmt.Sprintf("Expected base_url field to start with either \"http://\" or \"https://\". Got: %v", baseURL), + ) + } + if (!(strings.HasSuffix(baseURL, "/"))) { + resp.Diagnostics.AddError( + "Base URL must end with \"/\"", + fmt.Sprintf("Expected base_url field to end with a slash character \"/\". Got: %v", baseURL), + ) + } + + var suffix = data.BasicAuthVarSuffix.ValueString() + var usernameVar = "TF_BOSK_USERNAME_" + suffix + var passwordVar = "TF_BOSK_PASSWORD_" + suffix + username, usernameExists := os.LookupEnv(usernameVar) + password, passwordExists := os.LookupEnv(passwordVar) + var client *BoskClient + + if (suffix == "NO_AUTH") { + client = NewBoskClientWithoutAuth(http.DefaultClient, baseURL) + if (usernameExists) { + resp.Diagnostics.AddWarning( + "NO_AUTH suffix overrides username environment variable", + fmt.Sprintf("Based on basic_auth_var_suffix of \"%v\", ignoring environment variable \"TF_BOSK_USERNAME_%v\"", suffix, suffix), + ) + } + if (passwordExists) { + resp.Diagnostics.AddWarning( + "NO_AUTH suffix overrides password environment variable", + fmt.Sprintf("Based on basic_auth_var_suffix of \"%v\", ignoring environment variable \"TF_BOSK_PASSWORD_%v\"", suffix, suffix), + ) + } + } else if (usernameExists && passwordExists) { + client = NewBoskClient(http.DefaultClient, baseURL, username, password) + } else { + resp.Diagnostics.AddError( + "Missing environment variables for authentication", + fmt.Sprintf("Based on basic_auth_var_suffix of \"%v\", expected to find environment variables \"TF_BOSK_USERNAME_%v\" and \"TF_BOSK_PASSWORD_%v\"", suffix, suffix, suffix), + ) + } + if resp.Diagnostics.HasError() { + return + } - // Node client configuration for data sources and resources - client := http.DefaultClient resp.DataSourceData = client resp.ResourceData = client } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 8e500e4..776248b 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -4,6 +4,8 @@ package provider import ( + "fmt" + "strings" "testing" "github.com/hashicorp/terraform-plugin-framework/providerserver" @@ -23,3 +25,17 @@ func testAccPreCheck(t *testing.T) { // about the appropriate environment variables being set are common to see in a pre-check // function. } + +func splitURL(url string) (base, path string) { + parts := strings.SplitN(url, "/", 4) + protocol := parts[0] + host := parts[2] + if (len(parts) == 4) { + path = parts[3] + } else { + path = "" + } + base = protocol + "//" + host + "/" + fmt.Printf("HEY HEY base: \"%v\", path: \"%v\"", base, path) + return base, path +} \ No newline at end of file diff --git a/main.go b/main.go index 476f16c..0d35577 100644 --- a/main.go +++ b/main.go @@ -38,7 +38,6 @@ func main() { flag.Parse() opts := providerserver.ServeOpts{ - // TODO: Update this string with the published name of your provider. Address: "registry.terraform.io/venacorp/bosk", Debug: debug, } From aedce7a8fd90239cd370345ee5d4a95038edafb8 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Tue, 14 Nov 2023 22:02:14 -0500 Subject: [PATCH 4/7] Move slash to path --- internal/provider/node_data_source.go | 1 - internal/provider/node_resource.go | 32 +++++++++++++++++++++++++++ internal/provider/provider.go | 10 ++++----- internal/provider/provider_test.go | 9 ++++---- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/internal/provider/node_data_source.go b/internal/provider/node_data_source.go index e00165d..89d9762 100644 --- a/internal/provider/node_data_source.go +++ b/internal/provider/node_data_source.go @@ -65,7 +65,6 @@ func (d *NodeDataSource) Configure(ctx context.Context, req datasource.Configure "Unexpected Data Source Configure Type", fmt.Sprintf("Expected *BoskClient, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) - return } diff --git a/internal/provider/node_resource.go b/internal/provider/node_resource.go index 98b3d55..f042f8b 100644 --- a/internal/provider/node_resource.go +++ b/internal/provider/node_resource.go @@ -6,7 +6,9 @@ package provider import ( "context" "fmt" + "strings" + "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/types" @@ -73,6 +75,15 @@ func (r *NodeResource) Configure(ctx context.Context, req resource.ConfigureRequ r.client = client } +func (m *NodeResourceModel) Validate(diag *diag.Diagnostics) { + if !strings.HasPrefix(m.Path.ValueString(), "/") { + diag.AddError( + "Path does not start with slash character \"/\"", + fmt.Sprintf("Bosk node paths must start with a slash. Got: %v", m.Path.ValueString()), + ) + } +} + func (r *NodeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data NodeResourceModel @@ -83,6 +94,12 @@ func (r *NodeResource) Create(ctx context.Context, req resource.CreateRequest, r return } + data.Validate(&resp.Diagnostics) + if resp.Diagnostics.HasError() { + tflog.Warn(ctx, "Invalid plan", map[string]interface{}{"diagnostics": resp.Diagnostics}) + return + } + r.client.PutJSONAsString(r.client.urlPrefix + data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { tflog.Warn(ctx, "Error performing PUT", map[string]interface{}{"diagnostics": resp.Diagnostics}) @@ -106,6 +123,11 @@ func (r *NodeResource) Read(ctx context.Context, req resource.ReadRequest, resp tflog.Warn(ctx, "Error getting plan data", map[string]interface{}{"diagnostics": resp.Diagnostics}) return } + data.Validate(&resp.Diagnostics) + if resp.Diagnostics.HasError() { + tflog.Warn(ctx, "Invalid state", map[string]interface{}{"diagnostics": resp.Diagnostics}) + return + } result_json := r.client.GetJSONAsString(r.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { @@ -135,6 +157,11 @@ func (r *NodeResource) Update(ctx context.Context, req resource.UpdateRequest, r if resp.Diagnostics.HasError() { return } + data.Validate(&resp.Diagnostics) + if resp.Diagnostics.HasError() { + tflog.Warn(ctx, "Invalid plan", map[string]interface{}{"diagnostics": resp.Diagnostics}) + return + } r.client.PutJSONAsString(r.client.urlPrefix + data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { @@ -156,6 +183,11 @@ func (r *NodeResource) Delete(ctx context.Context, req resource.DeleteRequest, r if resp.Diagnostics.HasError() { return } + data.Validate(&resp.Diagnostics) + if resp.Diagnostics.HasError() { + tflog.Warn(ctx, "Invalid state", map[string]interface{}{"diagnostics": resp.Diagnostics}) + return + } r.client.Delete(r.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 29ea545..65bbd00 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -43,11 +43,11 @@ func (p *BoskProvider) Schema(ctx context.Context, req provider.SchemaRequest, r resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "base_url": schema.StringAttribute{ - MarkdownDescription: "Specifies the URL of the bosk API. Used as a prefix for all HTTP requests. Ends with a slash.", + MarkdownDescription: "Specifies the URL of the bosk API. Used as a prefix for all HTTP requests. Does not end with a slash.", Required: true, }, "basic_auth_var_suffix": schema.StringAttribute{ - MarkdownDescription: "Selects the environment variables to use for HTTP basic authentication; namely TF_BOSK_USERNAME_xxx and TF_BOSK_PASSWORD_xxx. If you don't want to use basic auth, set both these variables to blanks.", + MarkdownDescription: "Selects the environment variables to use for HTTP basic authentication; namely TF_BOSK_USERNAME_xxx and TF_BOSK_PASSWORD_xxx. If you don't want to use basic auth, specify NO_AUTH.", Required: true, }, }, @@ -69,10 +69,10 @@ func (p *BoskProvider) Configure(ctx context.Context, req provider.ConfigureRequ fmt.Sprintf("Expected base_url field to start with either \"http://\" or \"https://\". Got: %v", baseURL), ) } - if (!(strings.HasSuffix(baseURL, "/"))) { + if (strings.HasSuffix(baseURL, "/")) { resp.Diagnostics.AddError( - "Base URL must end with \"/\"", - fmt.Sprintf("Expected base_url field to end with a slash character \"/\". Got: %v", baseURL), + "Base URL must not end with \"/\"", + fmt.Sprintf("The base_url field will be prepended to the bosk node paths, which start with slashes, and so the base_url itself must not end with a slash. Got: %v", baseURL), ) } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 776248b..a8f5fa0 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -4,7 +4,6 @@ package provider import ( - "fmt" "strings" "testing" @@ -31,11 +30,11 @@ func splitURL(url string) (base, path string) { protocol := parts[0] host := parts[2] if (len(parts) == 4) { - path = parts[3] + path = "/" + parts[3] } else { - path = "" + path = "/" } - base = protocol + "//" + host + "/" - fmt.Printf("HEY HEY base: \"%v\", path: \"%v\"", base, path) + base = protocol + "//" + host + //fmt.Printf("HEY HEY base: \"%v\", path: \"%v\"", base, path) return base, path } \ No newline at end of file From 7adad4cd195fb6c85905f1cdb6b227d6a56b1610 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Tue, 14 Nov 2023 22:05:38 -0500 Subject: [PATCH 5/7] docs --- docs/data-sources/node.md | 2 +- docs/index.md | 5 +++-- docs/resources/node.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/data-sources/node.md b/docs/data-sources/node.md index 09f348a..56e4ba9 100644 --- a/docs/data-sources/node.md +++ b/docs/data-sources/node.md @@ -17,5 +17,5 @@ Bosk state tree node data source ### Required -- `url` (String) The HTTP address of the node +- `path` (String) When appended to the provider base_url, gives the HTTP address of the node - `value_json` (String) The JSON-encoded contents of the node diff --git a/docs/index.md b/docs/index.md index f9b3d97..03795f0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,7 @@ provider "bosk" { ## Schema -### Optional +### Required -- `endpoint` (String) Node provider attribute +- `base_url` (String) Specifies the URL of the bosk API. Used as a prefix for all HTTP requests. Does not end with a slash. +- `basic_auth_var_suffix` (String) Selects the environment variables to use for HTTP basic authentication; namely TF_BOSK_USERNAME_xxx and TF_BOSK_PASSWORD_xxx. If you don't want to use basic auth, specify NO_AUTH. diff --git a/docs/resources/node.md b/docs/resources/node.md index b825373..536c571 100644 --- a/docs/resources/node.md +++ b/docs/resources/node.md @@ -17,5 +17,5 @@ Bosk state tree node data source ### Required -- `url` (String) The HTTP address of the node +- `path` (String) When appended to the provider base_url, gives the HTTP address of the node - `value_json` (String) The JSON-encoded contents of the node From 4478741b8ca1ab51c3e1f9f58e7e1ebf65415953 Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Tue, 14 Nov 2023 22:07:11 -0500 Subject: [PATCH 6/7] gofmt --- internal/provider/node_data_source.go | 2 +- internal/provider/node_resource.go | 10 +++++----- internal/provider/provider.go | 14 +++++++------- internal/provider/provider_test.go | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/provider/node_data_source.go b/internal/provider/node_data_source.go index 89d9762..91a4229 100644 --- a/internal/provider/node_data_source.go +++ b/internal/provider/node_data_source.go @@ -80,7 +80,7 @@ func (d *NodeDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } - result_json := d.client.GetJSONAsString(d.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) + result_json := d.client.GetJSONAsString(d.client.urlPrefix+data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { return } diff --git a/internal/provider/node_resource.go b/internal/provider/node_resource.go index f042f8b..b87aedd 100644 --- a/internal/provider/node_resource.go +++ b/internal/provider/node_resource.go @@ -100,7 +100,7 @@ func (r *NodeResource) Create(ctx context.Context, req resource.CreateRequest, r return } - r.client.PutJSONAsString(r.client.urlPrefix + data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) + r.client.PutJSONAsString(r.client.urlPrefix+data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { tflog.Warn(ctx, "Error performing PUT", map[string]interface{}{"diagnostics": resp.Diagnostics}) return @@ -129,7 +129,7 @@ func (r *NodeResource) Read(ctx context.Context, req resource.ReadRequest, resp return } - result_json := r.client.GetJSONAsString(r.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) + result_json := r.client.GetJSONAsString(r.client.urlPrefix+data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { tflog.Warn(ctx, "Error performing GET", map[string]interface{}{"diagnostics": resp.Diagnostics}) return @@ -163,7 +163,7 @@ func (r *NodeResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - r.client.PutJSONAsString(r.client.urlPrefix + data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) + r.client.PutJSONAsString(r.client.urlPrefix+data.Path.ValueString(), data.Value_json.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -189,7 +189,7 @@ func (r *NodeResource) Delete(ctx context.Context, req resource.DeleteRequest, r return } - r.client.Delete(r.client.urlPrefix + data.Path.ValueString(), &resp.Diagnostics) + r.client.Delete(r.client.urlPrefix+data.Path.ValueString(), &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -205,7 +205,7 @@ func (r *NodeResource) Delete(ctx context.Context, req resource.DeleteRequest, r func (r *NodeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { path := req.ID - result_json := r.client.GetJSONAsString(r.client.urlPrefix + path, &resp.Diagnostics) + result_json := r.client.GetJSONAsString(r.client.urlPrefix+path, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 65bbd00..fadae79 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -30,7 +30,7 @@ type BoskProvider struct { // BoskProviderModel describes the provider data model. type BoskProviderModel struct { - BaseURL types.String `tfsdk:"base_url"` + BaseURL types.String `tfsdk:"base_url"` BasicAuthVarSuffix types.String `tfsdk:"basic_auth_var_suffix"` } @@ -63,13 +63,13 @@ func (p *BoskProvider) Configure(ctx context.Context, req provider.ConfigureRequ } var baseURL string = data.BaseURL.ValueString() - if (!(strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://"))) { + if !(strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://")) { resp.Diagnostics.AddError( "Base URL must be http or https", fmt.Sprintf("Expected base_url field to start with either \"http://\" or \"https://\". Got: %v", baseURL), ) } - if (strings.HasSuffix(baseURL, "/")) { + if strings.HasSuffix(baseURL, "/") { resp.Diagnostics.AddError( "Base URL must not end with \"/\"", fmt.Sprintf("The base_url field will be prepended to the bosk node paths, which start with slashes, and so the base_url itself must not end with a slash. Got: %v", baseURL), @@ -83,21 +83,21 @@ func (p *BoskProvider) Configure(ctx context.Context, req provider.ConfigureRequ password, passwordExists := os.LookupEnv(passwordVar) var client *BoskClient - if (suffix == "NO_AUTH") { + if suffix == "NO_AUTH" { client = NewBoskClientWithoutAuth(http.DefaultClient, baseURL) - if (usernameExists) { + if usernameExists { resp.Diagnostics.AddWarning( "NO_AUTH suffix overrides username environment variable", fmt.Sprintf("Based on basic_auth_var_suffix of \"%v\", ignoring environment variable \"TF_BOSK_USERNAME_%v\"", suffix, suffix), ) } - if (passwordExists) { + if passwordExists { resp.Diagnostics.AddWarning( "NO_AUTH suffix overrides password environment variable", fmt.Sprintf("Based on basic_auth_var_suffix of \"%v\", ignoring environment variable \"TF_BOSK_PASSWORD_%v\"", suffix, suffix), ) } - } else if (usernameExists && passwordExists) { + } else if usernameExists && passwordExists { client = NewBoskClient(http.DefaultClient, baseURL, username, password) } else { resp.Diagnostics.AddError( diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index a8f5fa0..ae236b5 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -29,7 +29,7 @@ func splitURL(url string) (base, path string) { parts := strings.SplitN(url, "/", 4) protocol := parts[0] host := parts[2] - if (len(parts) == 4) { + if len(parts) == 4 { path = "/" + parts[3] } else { path = "/" @@ -37,4 +37,4 @@ func splitURL(url string) (base, path string) { base = protocol + "//" + host //fmt.Printf("HEY HEY base: \"%v\", path: \"%v\"", base, path) return base, path -} \ No newline at end of file +} From 5c950a527f766e0b90557f771d6bdc519db8a9af Mon Sep 17 00:00:00 2001 From: Patrick Doyle Date: Tue, 14 Nov 2023 22:10:12 -0500 Subject: [PATCH 7/7] Remove unused var to pass lint --- internal/provider/node_data_source_test.go | 2 +- internal/provider/node_resource_test.go | 2 +- internal/provider/provider_test.go | 13 +++---------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/internal/provider/node_data_source_test.go b/internal/provider/node_data_source_test.go index 47e1929..2e40c12 100644 --- a/internal/provider/node_data_source_test.go +++ b/internal/provider/node_data_source_test.go @@ -28,7 +28,7 @@ func TestAccNodeDataSource(t *testing.T) { })) defer testServer.Close() - base, _ := splitURL(testServer.URL) + base := baseURL(testServer.URL) path := "/bosk/path/to/object" resource.Test(t, resource.TestCase{ diff --git a/internal/provider/node_resource_test.go b/internal/provider/node_resource_test.go index e30558a..39315f7 100644 --- a/internal/provider/node_resource_test.go +++ b/internal/provider/node_resource_test.go @@ -52,7 +52,7 @@ func TestAccNodeResource(t *testing.T) { })) defer testServer.Close() - base, _ := splitURL(testServer.URL) + base := baseURL(testServer.URL) path := "/bosk/path/to/object" resource.Test(t, resource.TestCase{ diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index ae236b5..337ad9a 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -25,16 +25,9 @@ func testAccPreCheck(t *testing.T) { // function. } -func splitURL(url string) (base, path string) { - parts := strings.SplitN(url, "/", 4) +func baseURL(url string) string { + parts := strings.SplitN(url, "/", 3) protocol := parts[0] host := parts[2] - if len(parts) == 4 { - path = "/" + parts[3] - } else { - path = "/" - } - base = protocol + "//" + host - //fmt.Printf("HEY HEY base: \"%v\", path: \"%v\"", base, path) - return base, path + return protocol + "//" + host }