diff --git a/docs/resources/network_peer.md b/docs/resources/network_peer.md new file mode 100644 index 0000000..f465aa1 --- /dev/null +++ b/docs/resources/network_peer.md @@ -0,0 +1,76 @@ +# incus_network_peer + +Incus allows creating peer routing relationships between two OVN networks. Using this method, traffic between the two +networks can go directly from one OVN network to the other and thus stays within the OVN subsystem, rather than transiting +through the uplink network. + +-> The peer resource is exclusively compatible with OVN (Open Virtual Network). + +For more information, please refer to [How to create peer routing relationships](https://linuxcontainers.org/incus/docs/main/howto/network_ovn_peers/) +in the official Incus documentation. + +## Example Usage + +```hcl +resource "incus_network" "network1" { + name = "lan0" + type = "ovn" + + config = { + # ... + } +} + +resource "incus_network" "network2" { + name = "lan1" + type = "ovn" + + config = { + # ... + } +} +resource "incus_network_peer" "lan0_lan1"{ + name = "lab0-lan1" + description = "A meaningful description" + network = "lan0" + project = "default" + target_network = "lan1" + target_project = "default" +} + +resource "incus_network_peer" "lan1_lan0"{ + name = "lab1-lan0" + description = "A meaningful description" + network = "lan1" + project = "default" + target_network = "lan0" + target_project = "default" +} +``` + +## Argument Reference + +* `name` - **required** - Name of the network peering on the local network + +* `network` - **Required** - Name of the local network. + +* `target_network` - **required** - Which network to create a peering with (required at create time for local peers) + +* `description` - *Optional* - Description of the network peering + +* `config` - *Optional* - Configuration options as key/value pairs (only user.* custom keys supported) + +* `type` - *Optional* - Type of network peering + +* `target_intergration` - *Optional* - Name of the integration (required at create time for remote peers) + +* `target_project` - *Optional* - Which project the target network exists in (required at create time for local peers) + +* `project` - *Optional* - Name of the project where the network is located. + +* `remote` - *Optional* - The remote in which the resource will be created. If + not provided, the provider's default remote will be used. + +## Attribute Reference + +No attributes are exported. diff --git a/internal/network/resource_network_integration_test.go b/internal/network/resource_network_integration_test.go index 47bc6c7..baffdd0 100644 --- a/internal/network/resource_network_integration_test.go +++ b/internal/network/resource_network_integration_test.go @@ -95,24 +95,23 @@ func TestAccNetworkIntegration_withInvalidType(t *testing.T) { }) } -// 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_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 ` @@ -154,15 +153,14 @@ resource "incus_network_integration" "test" { `, 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) -// } +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) +} diff --git a/internal/network/resource_network_peer.go b/internal/network/resource_network_peer.go new file mode 100644 index 0000000..d0c8b8e --- /dev/null +++ b/internal/network/resource_network_peer.go @@ -0,0 +1,360 @@ +package network + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/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" + "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" +) + +// NetworkPeerModel resource data model that matches the schema. +type NetworkPeerModel struct { + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Config types.Map `tfsdk:"config"` + Project types.String `tfsdk:"project"` + Network types.String `tfsdk:"network"` + TargetProject types.String `tfsdk:"target_project"` + TargetNetwork types.String `tfsdk:"target_network"` + Remote types.String `tfsdk:"remote"` + Type types.String `tfsdk:"type"` + TargetIntegration types.String `tfsdk:"target_integration"` + Status types.String `tfsdk:"status"` +} + +// IncusNetworkPeerResource represent Incus network peer resource. +type IncusNetworkPeerResource struct { + provider *provider_config.IncusProviderConfig +} + +// NewNetworkPeerResource returns a new network peer resource. +func NewNetworkPeerResource() resource.Resource { + return &IncusNetworkPeerResource{} +} + +func (r IncusNetworkPeerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_network_peer", req.ProviderTypeName) +} + +func (r IncusNetworkPeerResource) 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(), + }, + }, + + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + + "config": schema.MapAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + + "target_project": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "network": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "target_network": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "target_integration": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "project": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + + "remote": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + + "status": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (r *IncusNetworkPeerResource) 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)) + return + } + + r.provider = provider +} + +func (r IncusNetworkPeerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NetworkPeerModel + + 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...) + + if resp.Diagnostics.HasError() { + return + } + + peerName := plan.Name.ValueString() + description := plan.Description.ValueString() + networkName := plan.Network.ValueString() + targetProject := plan.TargetProject.ValueString() + targetNetwork := plan.TargetNetwork.ValueString() + _type := plan.Type.ValueString() + targetIntegration := plan.TargetIntegration.ValueString() + + networkPeerReq := api.NetworkPeersPost{ + Name: peerName, + TargetProject: targetProject, + TargetNetwork: targetNetwork, + Type: _type, + TargetIntegration: targetIntegration, + NetworkPeerPut: api.NetworkPeerPut{ + Description: description, + Config: config, + }, + } + + // Create Network Peer. + err = server.CreateNetworkPeer(networkName, networkPeerReq) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to create network peer %q", peerName), err.Error()) + return + } + + // Update Terraform state. + diags = r.SyncState(ctx, &resp.State, server, plan) + resp.Diagnostics.Append(diags...) +} + +func (r IncusNetworkPeerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NetworkPeerModel + + 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 + } + + // Update Terraform state. + diags = r.SyncState(ctx, &resp.State, server, state) + resp.Diagnostics.Append(diags...) +} + +func (r IncusNetworkPeerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NetworkPeerModel + + 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...) + + if resp.Diagnostics.HasError() { + return + } + + peerName := plan.Name.ValueString() + networkName := plan.Network.ValueString() + description := plan.Description.ValueString() + + peerReq := api.NetworkPeerPut{ + Description: description, + Config: config, + } + + // Update network peer. + _, etag, err := server.GetNetworkPeer(networkName, peerName) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve existing network peer %q", peerName), err.Error()) + return + } + + err = server.UpdateNetworkPeer(networkName, peerName, peerReq, etag) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to update network peer %q", peerName), err.Error()) + return + } + + // Update Terraform state. + diags = r.SyncState(ctx, &resp.State, server, plan) + resp.Diagnostics.Append(diags...) +} + +func (r IncusNetworkPeerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NetworkPeerModel + + 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 + } + + peerName := state.Name.ValueString() + networkName := state.Network.ValueString() + + err = server.DeleteNetworkPeer(networkName, peerName) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to remove network peer %q", peerName), err.Error()) + } +} + +// SyncState fetches the server's current state for an network peer +// and updates the provided model. It then applies this updated model as the +// new state in Terraform. +func (r IncusNetworkPeerResource) SyncState(ctx context.Context, tfState *tfsdk.State, server incus.InstanceServer, m NetworkPeerModel) diag.Diagnostics { + var respDiags diag.Diagnostics + + peerName := m.Name.ValueString() + networkName := m.Network.ValueString() + + fmt.Printf("%s:%s\n", peerName, networkName) + networkPeer, _, err := server.GetNetworkPeer(networkName, peerName) + if err != nil { + if errors.IsNotFoundError(err) { + tfState.RemoveResource(ctx) + return nil + } + + respDiags.AddError(fmt.Sprintf("Failed to retrieve network peer %q", peerName), err.Error()) + return respDiags + } + + config, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(networkPeer.Config), m.Config) + respDiags.Append(diags...) + + m.Description = types.StringValue(networkPeer.Description) + m.TargetNetwork = types.StringValue(networkPeer.TargetNetwork) + m.TargetProject = types.StringValue(networkPeer.TargetProject) + m.Type = types.StringValue(networkPeer.Type) + m.TargetIntegration = types.StringValue(networkPeer.TargetIntegration) + m.Status = types.StringValue(networkPeer.Status) + m.Config = config + + if respDiags.HasError() { + return respDiags + } + + return tfState.Set(ctx, &m) +} diff --git a/internal/network/resource_network_peer_test.go b/internal/network/resource_network_peer_test.go new file mode 100644 index 0000000..2981569 --- /dev/null +++ b/internal/network/resource_network_peer_test.go @@ -0,0 +1,185 @@ +package network_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/lxc/terraform-provider-incus/internal/acctest" +) + +func TestAccNetworkPeer_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckAPIExtensions(t, "network_peer") + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkPeer_basic(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network.ovnbr", "name", "ovnbr"), + resource.TestCheckResourceAttr("incus_network.ovnbr", "type", "bridge"), + resource.TestCheckResourceAttr("incus_network.lan0", "name", "lan0"), + resource.TestCheckResourceAttr("incus_network.lan0", "type", "ovn"), + resource.TestCheckResourceAttr("incus_network.lan0", "config.ipv4.address", "10.0.0.1/24"), + resource.TestCheckResourceAttr("incus_network.lan1", "name", "lan1"), + resource.TestCheckResourceAttr("incus_network.lan1", "type", "ovn"), + resource.TestCheckResourceAttr("incus_network.lan1", "config.ipv4.address", "10.0.1.1/24"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "name", "lab0-lan1"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "network", "lan0"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "target_network", "lan1"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "config.%", "0"), + ), + }, + }, + }) +} + +// Creates a network peering +func testAccNetworkPeer_basic() string { + return fmt.Sprintf(` +%s%s%s + +resource "incus_network_peer" "lan0_lan1"{ + name = "lab0-lan1" + network = incus_network.lan0.name + target_network = incus_network.lan1.name +} + +resource "incus_network_peer" "lan1_lan0"{ + name = "lab1-lan0" + network = incus_network.lan1.name + target_network = incus_network.lan0.name +} + `, + testAccNetworkPeer_ovnbr(), + testAccNetworkPeer_network("lan0", "10.0.0.1"), + testAccNetworkPeer_network("lan1", "10.0.1.1")) +} + +func TestAccNetworkPeer_acrossProjects(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckAPIExtensions(t, "network_peer") + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkPeer_acrossProjects(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network.ovnbr", "name", "ovnbr"), + resource.TestCheckResourceAttr("incus_network.ovnbr", "type", "bridge"), + resource.TestCheckResourceAttr("incus_project.projectA", "name", "projectA"), + resource.TestCheckResourceAttr("incus_project.projectB", "name", "projectB"), + resource.TestCheckResourceAttr("incus_network.projectA_lan0", "name", "lan0"), + resource.TestCheckResourceAttr("incus_network.projectA_lan0", "type", "ovn"), + resource.TestCheckResourceAttr("incus_network.projectA_lan0", "config.ipv4.address", "10.0.0.1/24"), + resource.TestCheckResourceAttr("incus_network.projectB_lan1", "name", "lan1"), + resource.TestCheckResourceAttr("incus_network.projectB_lan1", "type", "ovn"), + resource.TestCheckResourceAttr("incus_network.projectB_lan1", "config.ipv4.address", "10.0.1.1/24"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "name", "lab0-lan1"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "network", "lan0"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "target_network", "lan1"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "target_project", "projectB"), + resource.TestCheckResourceAttr("incus_network_peer.lan0_lan1", "config.%", "0"), + ), + }, + }, + }) +} + +// Creates a network peering between projects +func testAccNetworkPeer_acrossProjects() string { + return fmt.Sprintf(` +%s%s%s%s%s + +resource "incus_network_peer" "lan0_lan1"{ + name = "lab0-lan1" + project = incus_project.projectA.name + network = incus_network.projectA_lan0.name + target_project = incus_project.projectB.name + target_network = incus_network.projectB_lan1.name +} + +resource "incus_network_peer" "lan1_lan0"{ + name = "lab1-lan0" + project = incus_project.projectB.name + network = incus_network.projectB_lan1.name + target_project = incus_project.projectA.name + target_network = incus_network.projectA_lan0.name +} + `, + testAccNetworkPeer_ovnbr(), + testAccNetworkPeer_project("projectA"), + testAccNetworkPeer_project("projectB"), + testAccNetworkPeer_projectNetwork("projectA", "lan0", "10.0.0.1"), + testAccNetworkPeer_projectNetwork("projectB", "lan1", "10.0.1.1")) +} + +func testAccNetworkPeer_ovnbr() string { + return ` +resource "incus_network" "ovnbr" { + name = "ovnbr" + type = "bridge" + config = { + "ipv4.address" = "10.10.10.1/24" + "ipv4.routes" = "10.10.10.192/26" + "ipv4.ovn.ranges" = "10.10.10.193-10.10.10.254" + "ipv4.dhcp.ranges" = "10.10.10.100-10.10.10.150" + "ipv6.address" = "fd42:1000:1000:1000::1/64" + "ipv6.dhcp.ranges" = "fd42:1000:1000:1000:a::-fd42:1000:1000:1000:a::ffff" + "ipv6.ovn.ranges" = "fd42:1000:1000:1000:b::-fd42:1000:1000:1000:b::ffff" + } +} +` +} + +func testAccNetworkPeer_project(project string) string { + return fmt.Sprintf(` +resource "incus_project" "%s" { + name = "%s" + config = { + "features.networks" = true + } +} +`, project, project) +} + +func testAccNetworkPeer_network(network string, ipv4 string) string { + return fmt.Sprintf(` +resource "incus_network" "%s" { + name = "%s" + type = "ovn" + + config = { + "ipv4.address" = "%s/24" + "ipv4.nat" = "true" + "ipv6.address" = "none" + "ipv6.nat" = "false" + "network" = incus_network.ovnbr.name + } +} +`, network, network, ipv4) +} + +func testAccNetworkPeer_projectNetwork(project string, network string, ipv4 string) string { + return fmt.Sprintf(` +resource "incus_network" "%s_%s" { + name = "%s" + type = "ovn" + project = incus_project.%s.name + + config = { + "ipv4.address" = "%s/24" + "ipv4.nat" = "true" + "ipv6.address" = "none" + "ipv6.nat" = "false" + "network" = incus_network.ovnbr.name + } +} +`, project, network, network, project, ipv4) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 30a403d..7f9a557 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -274,6 +274,7 @@ func (p *IncusProvider) Resources(_ context.Context) []func() resource.Resource network.NewNetworkForwardResource, network.NewNetworkIntegrationResource, network.NewNetworkLBResource, + network.NewNetworkPeerResource, network.NewNetworkResource, network.NewNetworkZoneRecordResource, network.NewNetworkZoneResource,