-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
network_integration: add network_integration resource #173
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is description There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, you're correct, this isn't a field which Incus computes for you, so what's pushed by Terraform is what's stored, therefore it shouldn't be marked as computed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't use a default value if the field is not
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @maveonair any idea what's going on here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jonatas-lima is right, if we provide a default value then we must set the attribute There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, that's a bit counter-intuitive :) |
||
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) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, we need an additional check here to make sure, that the user does not provide the empty string
""
as name, since this causes an error on Incus side while Terraform does accept it"" != null
.@maveonair, @stgraber this is maybe a general question, since I see this to be missing also for other resources on the
name
attribute.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yeah, empty names are usually not allowed in the API.
The one exception there being for instances where an empty name is allowed but leads to Incus generating a random instance name for you, which may by itself cause issues with Terraform.
I think it'd make sense for all name properties to be required to be at least 1 character long, then leave the more detailed validation after that to Incus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100%, we should make the name mandatory if we need it.... I have to say I've never tried using an empty string before. Great catch!