diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76b4058..cea5287 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -158,7 +158,7 @@ Derive the data source model from the generated `resources.*Model`. Add a `Configure` method to the data source. ```diff -+ func (d *vpcDataSource) Configure( ++ func (d *exampleDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, @@ -182,7 +182,7 @@ Add a `Configure` method to the data source. Use the generated schema to define the data source schema. ```diff - func (d *vpcDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + func (d *exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ @@ -190,7 +190,12 @@ Use the generated schema to define the data source schema. - }, - }, - } -+ resp.Schema = *convertResourceSchemaToDataSourceSchema(ctx, resources.ExampleResourceSchema(ctx)) ++ ds, err := convertResourceSchemaToDataSourceSchema(resources.ExampleResourceSchema(ctx)) ++ if err != nil { ++ resp.Diagnostics.AddError("Failed to convert resource schema to data source schema", err.Error()) ++ return ++ } ++ resp.Schema = *ds + resp.Schema.Description = "TODO" } ``` diff --git a/docs/data-sources/vpc_route_entries.md b/docs/data-sources/vpc_route_entries.md new file mode 100644 index 0000000..84bd920 --- /dev/null +++ b/docs/data-sources/vpc_route_entries.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "binarylane_vpc_route_entries Data Source - terraform-provider-binarylane" +subcategory: "" +description: |- + TODO +--- + +# binarylane_vpc_route_entries (Data Source) + +TODO + + + + +## Schema + +### Required + +- `vpc_id` (Number) The target vpc id. + +### Optional + +- `route_entries` (List of Object) Submit null to leave unaltered, submit an empty list to clear all route entries. It is not possible to PATCH individual route entries, to alter a route entry submit the entire list of route entries you wish to save. (see [below for nested schema](#nestedatt--route_entries)) + + +### Nested Schema for `route_entries` + +Optional: + +- `description` (String) +- `destination` (String) +- `router` (String) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 2bede30..3afa1b4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -145,6 +145,7 @@ func (p *binarylaneProvider) DataSources(_ context.Context) []func() datasource. NewServerDataSource, NewSshKeyDataSource, NewVpcDataSource, + NewVpcRouteEntriesDataSource, } } diff --git a/internal/provider/vpc_data_source.go b/internal/provider/vpc_data_source.go index 4cf289e..137ccf3 100644 --- a/internal/provider/vpc_data_source.go +++ b/internal/provider/vpc_data_source.go @@ -88,13 +88,6 @@ func (d *vpcDataSource) Read(ctx context.Context, req datasource.ReadRequest, re data.IpRange = types.StringValue(*vpcResp.JSON200.Vpc.IpRange) data.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name) - // routeEntries, routeEntriesDiags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) - // resp.Diagnostics.Append(routeEntriesDiags...) - // if resp.Diagnostics.HasError() { - // return - // } - // data.RouteEntries = routeEntries - // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/provider/vpc_resource.go b/internal/provider/vpc_resource.go index 6ad59b5..73554b4 100644 --- a/internal/provider/vpc_resource.go +++ b/internal/provider/vpc_resource.go @@ -4,18 +4,21 @@ import ( "context" "fmt" "net/http" + "reflect" + "strconv" "terraform-provider-binarylane/internal/binarylane" "terraform-provider-binarylane/internal/resources" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" ) var ( - _ resource.Resource = &vpcResource{} - _ resource.ResourceWithConfigure = &vpcResource{} - // _ resource.ResourceWithImportState = &exampleResource{} + _ resource.Resource = &vpcResource{} + _ resource.ResourceWithConfigure = &vpcResource{} + _ resource.ResourceWithImportState = &vpcResource{} ) func NewVpcResource() resource.Resource { @@ -121,6 +124,7 @@ func (r *vpcResource) Read(ctx context.Context, req resource.ReadRequest, resp * fmt.Sprintf("Received %s reading VPC: id=%d. Details: %s", vpcResp.Status(), data.Id.ValueInt64(), vpcResp.Body)) return } + data.Id = types.Int64Value(*vpcResp.JSON200.Vpc.Id) data.IpRange = types.StringValue(*vpcResp.JSON200.Vpc.IpRange) data.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name) @@ -129,23 +133,31 @@ func (r *vpcResource) Read(ctx context.Context, req resource.ReadRequest, resp * } func (r *vpcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data vpcResourceModel + var plan, state vpcResourceModel // Read Terraform plan data into the model - resp.Diagnostics.Append() + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } // Update API call logic - tflog.Debug(ctx, fmt.Sprintf("Updating VPC: id=%d", data.Id.ValueInt64())) + patchRequest := binarylane.PatchVpcRequest{} - vpcResp, err := r.bc.client.PutVpcsVpcIdWithResponse(ctx, data.Id.ValueInt64(), binarylane.UpdateVpcRequest{ - Name: data.Name.ValueString(), - }) + if !plan.Name.Equal(state.Name) { + patchRequest.Name = plan.Name.ValueStringPointer() + } + + if reflect.ValueOf(plan).NumField() == 0 { + return + } + + tflog.Debug(ctx, fmt.Sprintf("Updating VPC: id=%d", state.Id.ValueInt64())) + vpcResp, err := r.bc.client.PatchVpcsVpcIdWithResponse(ctx, state.Id.ValueInt64(), patchRequest) if err != nil { resp.Diagnostics.AddError( - fmt.Sprintf("Error updating VPC: id=%d", data.Id.ValueInt64()), + fmt.Sprintf("Error updating VPC: id=%d", state.Id.ValueInt64()), err.Error(), ) return @@ -154,15 +166,15 @@ func (r *vpcResource) Update(ctx context.Context, req resource.UpdateRequest, re if vpcResp.StatusCode() != http.StatusOK { resp.Diagnostics.AddError( "Unexpected HTTP status code updating VPC", - fmt.Sprintf("Received %s updating VPC: id=%d. Details: %s", vpcResp.Status(), data.Id.ValueInt64(), vpcResp.Body)) + fmt.Sprintf("Received %s updating VPC: id=%d. Details: %s", vpcResp.Status(), state.Id.ValueInt64(), vpcResp.Body)) return } - data.Id = types.Int64Value(*vpcResp.JSON200.Vpc.Id) - data.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name) + state.Id = types.Int64Value(*vpcResp.JSON200.Vpc.Id) + state.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name) // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } func (r *vpcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { @@ -192,3 +204,16 @@ func (r *vpcResource) Delete(ctx context.Context, req resource.DeleteRequest, re return } } + +func (r *vpcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "Error importing VPC", + fmt.Sprintf("Could not import VPC, unable to parse ID (should be an integer): %s ", err.Error()), + ) + return + } + diags := resp.State.SetAttribute(ctx, path.Root("id"), id) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/provider/vpc_resource_test.go b/internal/provider/vpc_resource_test.go index c601c8c..ce79b92 100644 --- a/internal/provider/vpc_resource_test.go +++ b/internal/provider/vpc_resource_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestVpcResource(t *testing.T) { @@ -34,25 +35,82 @@ resource "binarylane_vpc_route_entries" "test" { } ] } + +data "binarylane_vpc_route_entries" "test" { + vpc_id = binarylane_vpc_route_entries.test.vpc_id +} `, Check: resource.ComposeAggregateTestCheckFunc( - // Verify resource values + // Verify binarylane_vpc resource values resource.TestCheckResourceAttr("binarylane_vpc.test", "name", "tf-test-vpc"), resource.TestCheckResourceAttr("binarylane_vpc.test", "ip_range", "10.240.0.0/16"), resource.TestCheckResourceAttrSet("binarylane_vpc.test", "id"), - // Verify data source values + // Verify binarylane_vpc data source values resource.TestCheckResourceAttr("data.binarylane_vpc.test", "name", "tf-test-vpc"), resource.TestCheckResourceAttr("data.binarylane_vpc.test", "ip_range", "10.240.0.0/16"), resource.TestCheckResourceAttrSet("data.binarylane_vpc.test", "id"), - - // Verify resource values + // Verify binarylane_vpc_route_entries resource values resource.TestCheckResourceAttrSet("binarylane_vpc_route_entries.test", "vpc_id"), resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.#", "1"), resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.destination", "0.0.0.0/0"), resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.router", "10.240.0.1"), resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.description", "test"), + // Verify binarylane_vpc_route_entries data source values + resource.TestCheckResourceAttrSet("data.binarylane_vpc_route_entries.test", "vpc_id"), + resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.#", "1"), + resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.0.destination", "0.0.0.0/0"), + resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.0.router", "10.240.0.1"), + resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.0.description", "test"), ), }, + // ImportState testing + { + ResourceName: "binarylane_vpc.test", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "binarylane_vpc_route_entries.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "vpc_id", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + resourceState := s.RootModule().Resources["binarylane_vpc_route_entries.test"] + return resourceState.Primary.Attributes["vpc_id"], nil + }, + }, + // Update and Read testing + { + Config: providerConfig + ` +resource "binarylane_vpc" "test" { + name = "tf-test-vpc-renamed" + ip_range = "10.240.0.0/16" +} + +resource "binarylane_vpc_route_entries" "test" { + vpc_id = binarylane_vpc.test.id + route_entries = [ + { + description = "test-renamed" + destination = "0.0.0.0/0" + router = "10.240.0.2" + } + ] +}`, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify binarylane_vpc resource values updated + resource.TestCheckResourceAttr("binarylane_vpc.test", "name", "tf-test-vpc-renamed"), // Updated name + resource.TestCheckResourceAttr("binarylane_vpc.test", "ip_range", "10.240.0.0/16"), + resource.TestCheckResourceAttrSet("binarylane_vpc.test", "id"), + // Verify binarylane_vpc_route_entries resource values updated + resource.TestCheckResourceAttrSet("binarylane_vpc_route_entries.test", "vpc_id"), + resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.#", "1"), + resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.destination", "0.0.0.0/0"), + resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.router", "10.240.0.2"), // Updated router + resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.description", "test-renamed"), // Updated description + ), + }, + // Delete testing automatically occurs in TestCase }, }) } diff --git a/internal/provider/vpc_route_entries_data_source.go b/internal/provider/vpc_route_entries_data_source.go new file mode 100644 index 0000000..73359cd --- /dev/null +++ b/internal/provider/vpc_route_entries_data_source.go @@ -0,0 +1,109 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + "terraform-provider-binarylane/internal/resources" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" +) + +var ( + _ datasource.DataSource = &vpcRouteEntriesDataSource{} + _ datasource.DataSourceWithConfigure = &vpcRouteEntriesDataSource{} +) + +func NewVpcRouteEntriesDataSource() datasource.DataSource { + return &vpcRouteEntriesDataSource{} +} + +type vpcRouteEntriesDataSource struct { + bc *BinarylaneClient +} + +type vpcRouteEntriesDataSourceModel struct { + resources.VpcRouteEntriesModel +} + +func (d *vpcRouteEntriesDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vpc_route_entries" +} + +func (d *vpcRouteEntriesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + ds, err := convertResourceSchemaToDataSourceSchema(resources.VpcRouteEntriesResourceSchema(ctx)) + if err != nil { + resp.Diagnostics.AddError("Failed to convert resource schema to data source schema", err.Error()) + return + } + resp.Schema = *ds + resp.Schema.Description = "TODO" + + // Overrides + vpcId := resp.Schema.Attributes["vpc_id"] + resp.Schema.Attributes["vpc_id"] = &schema.Int64Attribute{ + Description: vpcId.GetDescription(), + MarkdownDescription: vpcId.GetMarkdownDescription(), + Required: true, // VPC ID is required to define the route entries + } +} + +func (d *vpcRouteEntriesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data vpcRouteEntriesDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Read API call logic + vpcResp, err := d.bc.client.GetVpcsVpcIdWithResponse(ctx, data.VpcId.ValueInt64()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error reading VPC: vpc_id=%d", data.VpcId.ValueInt64()), + err.Error(), + ) + return + } + if vpcResp.StatusCode() != http.StatusOK { + resp.Diagnostics.AddError( + "Unexpected HTTP status code reading VPC", + fmt.Sprintf("Received %s reading VPC: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body)) + return + } + + routeEntries, routeEntriesDiags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) + resp.Diagnostics.Append(routeEntriesDiags...) + if resp.Diagnostics.HasError() { + return + } + data.RouteEntries = routeEntries + + // Example data value setting + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *vpcRouteEntriesDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + bc, ok := req.ProviderData.(BinarylaneClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData)) + return + } + + d.bc = &bc +} diff --git a/internal/provider/vpc_route_entries_resource.go b/internal/provider/vpc_route_entries_resource.go index b712b8a..01d457d 100644 --- a/internal/provider/vpc_route_entries_resource.go +++ b/internal/provider/vpc_route_entries_resource.go @@ -4,21 +4,25 @@ import ( "context" "fmt" "net/http" + "strconv" "terraform-provider-binarylane/internal/binarylane" "terraform-provider-binarylane/internal/resources" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) var ( - _ resource.Resource = &vpcRouteEntriesResource{} - _ resource.ResourceWithConfigure = &vpcRouteEntriesResource{} - // _ resource.ResourceWithImportState = &vpcRouteEntriesResource{} + _ resource.Resource = &vpcRouteEntriesResource{} + _ resource.ResourceWithConfigure = &vpcRouteEntriesResource{} + _ resource.ResourceWithImportState = &vpcRouteEntriesResource{} ) func NewVpcRouteEntriesResource() resource.Resource { @@ -47,6 +51,9 @@ func (r *vpcRouteEntriesResource) Schema(ctx context.Context, req resource.Schem Description: vpcId.GetDescription(), MarkdownDescription: vpcId.GetMarkdownDescription(), Required: true, // VPC ID is required to define the route entries + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, } } @@ -86,7 +93,7 @@ func (r *vpcRouteEntriesResource) Create(ctx context.Context, req resource.Creat return } - routeEntriesState, diags := getRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) + routeEntriesState, diags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -111,19 +118,19 @@ func (r *vpcRouteEntriesResource) Read(ctx context.Context, req resource.ReadReq vpcResp, err := r.bc.client.GetVpcsVpcIdWithResponse(ctx, data.VpcId.ValueInt64()) if err != nil { resp.Diagnostics.AddError( - fmt.Sprintf("Error reading VPC: vpc_id=%d", data.VpcId.ValueInt64()), + fmt.Sprintf("Error reading VPC route entries: vpc_id=%d", data.VpcId.ValueInt64()), err.Error(), ) return } if vpcResp.StatusCode() != http.StatusOK { resp.Diagnostics.AddError( - "Unexpected HTTP status code reading VPC", - fmt.Sprintf("Received %s reading VPC: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body)) + "Unexpected HTTP status code reading VPC route entries", + fmt.Sprintf("Received %s reading VPC route entries: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body)) return } - routeEntries, routeEntriesDiags := getRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) + routeEntries, routeEntriesDiags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) resp.Diagnostics.Append(routeEntriesDiags...) if resp.Diagnostics.HasError() { return @@ -156,7 +163,7 @@ func (r *vpcRouteEntriesResource) Update(ctx context.Context, req resource.Updat }) if err != nil { resp.Diagnostics.AddError( - fmt.Sprintf("Error creating VPC route entries: vpc_id=%d", data.VpcId.ValueInt64()), + fmt.Sprintf("Error updating VPC route entries: vpc_id=%d", data.VpcId.ValueInt64()), err.Error(), ) return @@ -164,12 +171,12 @@ func (r *vpcRouteEntriesResource) Update(ctx context.Context, req resource.Updat if vpcResp.StatusCode() != http.StatusOK { resp.Diagnostics.AddError( - "Unexpected HTTP status code creating VPC route entries", - fmt.Sprintf("Received %s creating VPC route entries: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body)) + "Unexpected HTTP status code updating VPC route entries", + fmt.Sprintf("Received %s updating VPC route entries: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body)) return } - routeEntriesState, diags := getRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) + routeEntriesState, diags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -226,7 +233,7 @@ func (d *vpcRouteEntriesResource) Configure(_ context.Context, req resource.Conf d.bc = &bc } -func getRouteEntriesState(ctx context.Context, routeEntries *[]binarylane.RouteEntry) (basetypes.ListValue, diag.Diagnostics) { +func GetRouteEntriesState(ctx context.Context, routeEntries *[]binarylane.RouteEntry) (basetypes.ListValue, diag.Diagnostics) { var diags diag.Diagnostics routeEntriesValue := resources.RouteEntriesValue{} @@ -253,3 +260,18 @@ func getRouteEntriesState(ctx context.Context, routeEntries *[]binarylane.RouteE return routeEntriesListValue, diags } + +func (r *vpcRouteEntriesResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + + if err != nil { + resp.Diagnostics.AddError( + "Error importing VPC route entries", + "Could not import VPC route entries, unexpected error (ID should be an integer): "+err.Error(), + ) + return + } + + diags := resp.State.SetAttribute(ctx, path.Root("vpc_id"), id) + resp.Diagnostics.Append(diags...) +}