From e7eb95a1f1a9c66c98b29cd74256d630d8c82c1b Mon Sep 17 00:00:00 2001 From: Jonatas Ferreira Date: Mon, 2 Dec 2024 17:48:43 -0300 Subject: [PATCH 1/3] config: Add network_integration resource Signed-off-by: Jonatas Ferreira --- .../network/resource_network_integration.go | 282 ++++++++++++++++++ internal/provider/provider.go | 1 + 2 files changed, 283 insertions(+) create mode 100644 internal/network/resource_network_integration.go diff --git a/internal/network/resource_network_integration.go b/internal/network/resource_network_integration.go new file mode 100644 index 0000000..51836cc --- /dev/null +++ b/internal/network/resource_network_integration.go @@ -0,0 +1,282 @@ +package network + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/mapdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + incus "github.com/lxc/incus/v6/client" + "github.com/lxc/incus/v6/shared/api" + + "github.com/lxc/terraform-provider-incus/internal/common" + "github.com/lxc/terraform-provider-incus/internal/errors" + provider_config "github.com/lxc/terraform-provider-incus/internal/provider-config" +) + +// NetworkIntegrationModel resource data model that matches the schema. +type NetworkIntegrationModel struct { + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + Description types.String `tfsdk:"description"` + Project types.String `tfsdk:"project"` + Remote types.String `tfsdk:"remote"` + Config types.Map `tfsdk:"config"` +} + +// NetworkIntegrationResource represent network integration resource. +type NetworkIntegrationResource struct { + provider *provider_config.IncusProviderConfig +} + +// NewNetworkIntegrationResource returns a new network integration resource. +func NewNetworkIntegrationResource() resource.Resource { + return &NetworkIntegrationResource{} +} + +// Metadata for NetworkIntegrationResource. +func (r NetworkIntegrationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_network_integration", req.ProviderTypeName) +} + +// Schema for NetworkIntegrationResource. +func (r NetworkIntegrationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "type": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("ovn"), + }, + }, + + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + + "remote": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + + "project": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "config": schema.MapAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Default: mapdefault.StaticValue(types.MapValueMust(types.StringType, map[string]attr.Value{})), + }, + }, + } +} + +func (r *NetworkIntegrationResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + data := req.ProviderData + if data == nil { + return + } + + provider, ok := data.(*provider_config.IncusProviderConfig) + if !ok { + resp.Diagnostics.Append(errors.NewProviderDataTypeError(req.ProviderData)) + } + + r.provider = provider +} + +func (r NetworkIntegrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NetworkIntegrationModel + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + remote := plan.Remote.ValueString() + project := plan.Project.ValueString() + server, err := r.provider.InstanceServer(remote, project, "") + if err != nil { + resp.Diagnostics.Append(errors.NewInstanceServerError(err)) + return + } + + config, diag := common.ToConfigMap(ctx, plan.Config) + resp.Diagnostics.Append(diag...) + + networkIntegration := api.NetworkIntegrationsPost{ + Name: plan.Name.ValueString(), + Type: plan.Type.ValueString(), + NetworkIntegrationPut: api.NetworkIntegrationPut{ + Description: plan.Description.ValueString(), + Config: config, + }, + } + + err = server.CreateNetworkIntegration(networkIntegration) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to create network integration %q", networkIntegration.Name), err.Error()) + return + } + + diags = r.SyncState(ctx, &resp.State, server, plan) + resp.Diagnostics.Append(diags...) +} + +func (r NetworkIntegrationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NetworkIntegrationModel + + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + remote := state.Remote.ValueString() + project := state.Project.ValueString() + server, err := r.provider.InstanceServer(remote, project, "") + if err != nil { + resp.Diagnostics.Append(errors.NewInstanceServerError(err)) + } + + diags = r.SyncState(ctx, &resp.State, server, state) + resp.Diagnostics.Append(diags...) +} + +func (r NetworkIntegrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NetworkIntegrationModel + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + remote := plan.Remote.ValueString() + project := plan.Project.ValueString() + server, err := r.provider.InstanceServer(remote, project, "") + if err != nil { + resp.Diagnostics.Append(errors.NewInstanceServerError(err)) + return + } + + name := plan.Name.ValueString() + + config, diag := common.ToConfigMap(ctx, plan.Config) + resp.Diagnostics.Append(diag...) + if resp.Diagnostics.HasError() { + return + } + + _, etag, err := server.GetNetworkIntegration(name) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve existing network integration %q", name), err.Error()) + return + } + + networkIntegrationRequest := api.NetworkIntegrationPut{ + Description: plan.Description.ValueString(), + Config: config, + } + + err = server.UpdateNetworkIntegration(name, networkIntegrationRequest, etag) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to update network integration %q", name), err.Error()) + } + + diags = r.SyncState(ctx, &resp.State, server, plan) + resp.Diagnostics.Append(diags...) +} + +func (r NetworkIntegrationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NetworkIntegrationModel + + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + remote := state.Remote.ValueString() + project := state.Project.ValueString() + server, err := r.provider.InstanceServer(remote, project, "") + if err != nil { + resp.Diagnostics.Append(errors.NewInstanceServerError(err)) + return + } + + name := state.Name.ValueString() + + err = server.DeleteNetworkIntegration(name) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to remove network integration %q", name), err.Error()) + } +} + +// SyncState fetches the server's current state for a network integration and updates +// the provided model. It then applies this updated model as the new state +// in Terraform. +func (r NetworkIntegrationResource) SyncState(ctx context.Context, tfState *tfsdk.State, server incus.InstanceServer, m NetworkIntegrationModel) diag.Diagnostics { + var respDiags diag.Diagnostics + + networkIntegrationName := m.Name.ValueString() + networkIntegration, _, err := server.GetNetworkIntegration(networkIntegrationName) + if err != nil { + if errors.IsNotFoundError(err) { + tfState.RemoveResource(ctx) + return nil + } + + respDiags.AddError(fmt.Sprintf("Failed to retrieve network integration %q", networkIntegrationName), err.Error()) + return respDiags + } + + config, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(networkIntegration.Config), m.Config) + respDiags.Append(diags...) + + m.Description = types.StringValue(networkIntegration.Description) + m.Type = types.StringValue(networkIntegration.Type) + m.Config = config + + if respDiags.HasError() { + return respDiags + } + + return tfState.Set(ctx, &m) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c18519b..30a403d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -272,6 +272,7 @@ func (p *IncusProvider) Resources(_ context.Context) []func() resource.Resource instance.NewInstanceSnapshotResource, network.NewNetworkAclResource, network.NewNetworkForwardResource, + network.NewNetworkIntegrationResource, network.NewNetworkLBResource, network.NewNetworkResource, network.NewNetworkZoneRecordResource, From e137ec496b9b2a18fbcd096221b4ad1c7ebdc441 Mon Sep 17 00:00:00 2001 From: Jonatas Ferreira Date: Mon, 2 Dec 2024 17:49:05 -0300 Subject: [PATCH 2/3] tests: Add network_integration resource Signed-off-by: Jonatas Ferreira --- .../resource_network_integration_test.go | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 internal/network/resource_network_integration_test.go diff --git a/internal/network/resource_network_integration_test.go b/internal/network/resource_network_integration_test.go new file mode 100644 index 0000000..47bc6c7 --- /dev/null +++ b/internal/network/resource_network_integration_test.go @@ -0,0 +1,168 @@ +package network_test + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/lxc/terraform-provider-incus/internal/acctest" +) + +func TestAccNetworkIntegration_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckAPIExtensions(t, "network_integrations") + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkIntegration_basic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network_integration.test", "name", "test"), + resource.TestCheckResourceAttr("incus_network_integration.test", "type", "ovn"), + resource.TestCheckResourceAttr("incus_network_integration.test", "description", "Basic Network Integration"), + resource.TestCheckResourceAttr("incus_network_integration.test", "config.%", "0"), + ), + }, + }, + }) +} + +func TestAccNetworkIntegration_withConfig(t *testing.T) { + networkIntegrationConfig := map[string]string{ + "ovn.northbound_connection": "tcp:[192.0.2.12]:6645,tcp:[192.0.3.13]:6645,tcp:[192.0.3.14]:6645", + "ovn.southbound_connection": "tcp:[192.0.2.12]:6646,tcp:[192.0.3.13]:6646,tcp:[192.0.3.14]:6646", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckAPIExtensions(t, "network_integrations") + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkIntegration_withConfig(networkIntegrationConfig), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network_integration.test", "name", "test"), + resource.TestCheckResourceAttr("incus_network_integration.test", "type", "ovn"), + resource.TestCheckResourceAttr("incus_network_integration.test", "config.%", "2"), + resource.TestCheckResourceAttr("incus_network_integration.test", "config.ovn.northbound_connection", networkIntegrationConfig["ovn.northbound_connection"]), + resource.TestCheckResourceAttr("incus_network_integration.test", "config.ovn.southbound_connection", networkIntegrationConfig["ovn.southbound_connection"]), + ), + }, + }, + }) +} + +func TestAccNetworkIntegration_withValidType(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckAPIExtensions(t, "network_integrations") + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkIntegration_withType("ovn"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network_integration.test", "name", "test"), + resource.TestCheckResourceAttr("incus_network_integration.test", "type", "ovn"), + resource.TestCheckResourceAttr("incus_network_integration.test", "config.%", "0"), + ), + }, + }, + }) +} + +func TestAccNetworkIntegration_withInvalidType(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckAPIExtensions(t, "network_integrations") + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkIntegration_withType("invalid"), + ExpectError: regexp.MustCompile(`Attribute type value must be one of: \["ovn"\], got: "invalid"`), + }, + }, + }) +} + +// Waiting for https://github.com/lxc/terraform-provider-incus/issues/123 +// func TestAccNetworkIntegration_attach(t *testing.T) { +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { +// acctest.PreCheck(t) +// acctest.PreCheckAPIExtensions(t, "network_integrations") +// }, +// ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, +// Steps: []resource.TestStep{ +// { +// Config: testAccNetworkIntegration_attach(), +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr("incus_network_integration.test", "name", "test"), +// ), +// }, +// }, +// }) +// } + +func testAccNetworkIntegration_basic() string { + return ` +resource "incus_network_integration" "test" { + name = "test" + description = "Basic Network Integration" + type = "ovn" +} +` +} + +func testAccNetworkIntegration_withConfig(config map[string]string) string { + entries := strings.Builder{} + for k, v := range config { + entry := fmt.Sprintf("%q = %q\n", k, v) + entries.WriteString(entry) + } + + return fmt.Sprintf(` +resource "incus_network_integration" "test" { + name = "test" + description = "Network Integration with Config" + type = "ovn" + + config = { + %s + } +} +`, entries.String()) +} + +func testAccNetworkIntegration_withType(networkIntegrationType string) string { + return fmt.Sprintf(` +resource "incus_network_integration" "test" { + name = "test" + description = "Network Integration with Type" + type = "%s" +} +`, networkIntegrationType) +} + +// Waiting for https://github.com/lxc/terraform-provider-incus/issues/123 +// func testAccNetworkIntegration_attach() string { +// networkIntegrationRes := ` +// resource "incus_network_peer" "test" { +// name = "ovn-lan1" +// network = incus_network.ovn.name +// target_integration = incus_network_integration.test.name +// type = "ovn" +// } +// ` +// return fmt.Sprintf("%s\n%s\n%s", ovnNetworkResource(), testAccNetworkIntegration_basic(), networkIntegrationRes) +// } From 52fe8ed472504f7e97649657177218e842a08adf Mon Sep 17 00:00:00 2001 From: Jonatas Ferreira Date: Mon, 2 Dec 2024 17:49:16 -0300 Subject: [PATCH 3/3] docs: Add network_integration resource Signed-off-by: Jonatas Ferreira --- docs/resources/network_integration.md | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/resources/network_integration.md diff --git a/docs/resources/network_integration.md b/docs/resources/network_integration.md new file mode 100644 index 0000000..ddb2c06 --- /dev/null +++ b/docs/resources/network_integration.md @@ -0,0 +1,62 @@ +# incus_network_integration + +Manage integrations between the local Incus deployment and remote networks hosted on Incus or other platforms. Currently available only for [OVN networks](https://linuxcontainers.org/incus/docs/main/reference/network_ovn/#network-ovn). + +## Basic Example + +```hcl +resource "incus_network_integration" "this" { + name = "ovn-region" + type = "ovn" + + config = { + "ovn.northbound_connection" = "tcp:[192.0.2.12]:6645,tcp:[192.0.3.13]:6645,tcp:[192.0.3.14]:6645" + "ovn.southbound_connection" = "tcp:[192.0.2.12]:6646,tcp:[192.0.3.13]:6646,tcp:[192.0.3.14]:6646" + } +} +``` + +## Peer Example + +```hcl +resource "incus_network" "default" { + name = "default" + type = "ovn" + + config = { + "ipv4.address" = "192.168.2.0/24" + "ipv4.nat" = "true" + } +} + +resource "incus_network_integration" "this" { + name = "ovn-region" + type = "ovn" + + config = { + "ovn.northbound_connection" = "tcp:[192.0.2.12]:6645,tcp:[192.0.3.13]:6645,tcp:[192.0.3.14]:6645" + "ovn.southbound_connection" = "tcp:[192.0.2.12]:6646,tcp:[192.0.3.13]:6646,tcp:[192.0.3.14]:6646" + } +} + +resource "incus_network_peer" "this" { + name = "ovn-peer" + network = incus_network.default.name + target_integration = incus_network_integration.this.name +} +``` + +## Argument Reference + +* `name` - **Required** - Name of the network integration. + +* `type` **Required** - The type of the network integration. Currently, only supports `ovn` type. + +* `description` *Optional* - Description of the network integration. + +* `project` - *Optional* - Name of the project where the network will be created. + +* `remote` - *Optional* - The remote in which the resource will be created. If + not provided, the provider's default remote will be used. + +* `config` - *Optional* - Map of key/value pairs of [network integration config settings](https://linuxcontainers.org/incus/docs/main/howto/network_integrations/)