From 26f949eab4f5ce8a2d702a39ff59341451b0e1dd Mon Sep 17 00:00:00 2001 From: Fabian Mettler Date: Thu, 20 Jun 2024 09:19:45 +0200 Subject: [PATCH 1/3] network: Add network ACL resource Signed-off-by: Fabian Mettler --- internal/network/resource_network_acl.go | 467 +++++++++++++++++++++++ internal/provider/provider.go | 1 + 2 files changed, 468 insertions(+) create mode 100644 internal/network/resource_network_acl.go diff --git a/internal/network/resource_network_acl.go b/internal/network/resource_network_acl.go new file mode 100644 index 0000000..b82389d --- /dev/null +++ b/internal/network/resource_network_acl.go @@ -0,0 +1,467 @@ +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/path" + "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/setdefault" + "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" +) + +// NetworkAclModel resource data model that matches the schema. +type NetworkAclModel struct { + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Project types.String `tfsdk:"project"` + Remote types.String `tfsdk:"remote"` + Config types.Map `tfsdk:"config"` + Egress types.Set `tfsdk:"egress"` + Ingress types.Set `tfsdk:"ingress"` +} + +// NetworkAclRuleModel resource data model that matches the schema. +type NetworkAclRuleModel struct { + Action types.String `tfsdk:"action"` + Destination types.String `tfsdk:"destination"` + DestinationPort types.String `tfsdk:"destination_port"` + Protocol types.String `tfsdk:"protocol"` + Description types.String `tfsdk:"description"` + State types.String `tfsdk:"state"` + Source types.String `tfsdk:"source"` + ICMPType types.String `tfsdk:"icmp_type"` + ICMPCode types.String `tfsdk:"icmp_code"` +} + +// NetworkAclResource represent Incus network ACL resource. +type NetworkAclResource struct { + provider *provider_config.IncusProviderConfig +} + +func NewNetworkAclResource() resource.Resource { + return &NetworkAclResource{} +} + +func (r *NetworkAclResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_network_acl", req.ProviderTypeName) +} +func (r *NetworkAclResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + aclRuleObjectType := getAclRuleObjectType() + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), + }, + "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(), + }, + }, + "config": schema.MapAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Default: mapdefault.StaticValue(types.MapValueMust(types.StringType, map[string]attr.Value{})), + }, + "egress": schema.SetNestedAttribute{ + Optional: true, + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: ruleAttributes(), + }, + Default: setdefault.StaticValue(types.SetNull(aclRuleObjectType)), + }, + "ingress": schema.SetNestedAttribute{ + Optional: true, + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: ruleAttributes(), + }, + Default: setdefault.StaticValue(types.SetNull(aclRuleObjectType)), + }, + }, + } +} + +func getAclRuleObjectType() types.ObjectType { + return types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "action": types.StringType, + "destination": types.StringType, + "destination_port": types.StringType, + "protocol": types.StringType, + "description": types.StringType, + "state": types.StringType, + "source": types.StringType, + "icmp_type": types.StringType, + "icmp_code": types.StringType, + }, + } +} + +func ruleAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "action": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("allow", "allow-stateless", "drop", "reject"), + }, + }, + "destination": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "destination_port": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "protocol": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "state": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("enabled", "disabled", "logged"), + }, + }, + "source": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "icmp_type": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "icmp_code": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + } +} + +func (r *NetworkAclResource) 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 *NetworkAclResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NetworkAclModel + + 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, diags := common.ToConfigMap(ctx, plan.Config) + resp.Diagnostics.Append(diags...) + + egress, diags := ToNetworkAclRules(ctx, plan.Egress) + resp.Diagnostics.Append(diags...) + + ingress, diags := ToNetworkAclRules(ctx, plan.Ingress) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + aclName := plan.Name.ValueString() + aclReq := api.NetworkACLsPost{ + NetworkACLPost: api.NetworkACLPost{ + Name: aclName, + }, + NetworkACLPut: api.NetworkACLPut{ + Description: plan.Description.ValueString(), + Config: config, + Egress: egress, + Ingress: ingress, + }, + } + + err = server.CreateNetworkACL(aclReq) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to create network ACL %q", aclName), err.Error()) + return + } + + diags = r.SyncState(ctx, &resp.State, server, plan) + resp.Diagnostics.Append(diags...) +} + +func ToNetworkAclRules(ctx context.Context, aclRuleList types.Set) ([]api.NetworkACLRule, diag.Diagnostics) { + if aclRuleList.IsNull() { + return []api.NetworkACLRule{}, nil + } + + aclRuleModelList := make([]NetworkAclRuleModel, 0, len(aclRuleList.Elements())) + diags := aclRuleList.ElementsAs(ctx, &aclRuleModelList, false) + if diags.HasError() { + return nil, diags + } + + aclRules := make([]api.NetworkACLRule, len(aclRuleModelList)) + for i, aclRuleModel := range aclRuleModelList { + protocol := aclRuleModel.Protocol.ValueString() + + aclRule := api.NetworkACLRule{ + Action: aclRuleModel.Action.ValueString(), + Destination: aclRuleModel.Destination.ValueString(), + DestinationPort: aclRuleModel.DestinationPort.ValueString(), + Protocol: protocol, + Description: aclRuleModel.Description.ValueString(), + State: aclRuleModel.State.ValueString(), + Source: aclRuleModel.Source.ValueString(), + } + + if protocol == "icmp4" || protocol == "icmp6" { + aclRule.ICMPType = aclRuleModel.ICMPType.ValueString() + aclRule.ICMPCode = aclRuleModel.ICMPCode.ValueString() + } + + aclRules[i] = aclRule + } + + return aclRules, nil +} + +func (r *NetworkAclResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NetworkAclModel + + 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 + } + + diags = r.SyncState(ctx, &resp.State, server, state) + resp.Diagnostics.Append(diags...) +} + +func (r *NetworkAclResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NetworkAclModel + + 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 + } + + aclName := plan.Name.ValueString() + _, etag, err := server.GetNetworkACL(aclName) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve existing network ACL %q", aclName), err.Error()) + return + } + + config, diags := common.ToConfigMap(ctx, plan.Config) + resp.Diagnostics.Append(diags...) + + egress, diags := ToNetworkAclRules(ctx, plan.Egress) + resp.Diagnostics.Append(diags...) + + ingress, diags := ToNetworkAclRules(ctx, plan.Ingress) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + aclReq := api.NetworkACLPut{ + Description: plan.Description.ValueString(), + Config: config, + Egress: egress, + Ingress: ingress, + } + + err = server.UpdateNetworkACL(aclName, aclReq, etag) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to update network ACL %q", aclName), err.Error()) + return + } + + diags = r.SyncState(ctx, &resp.State, server, plan) + resp.Diagnostics.Append(diags...) +} + +func (r *NetworkAclResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NetworkAclModel + + 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 + } + + aclName := state.Name.ValueString() + err = server.DeleteNetworkACL(aclName) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to remove network ACL %q", aclName), err.Error()) + } +} + +func (r *NetworkAclResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + meta := common.ImportMetadata{ + ResourceName: "network_acl", + RequiredFields: []string{"name"}, + } + + fields, diags := meta.ParseImportID(req.ID) + if diags != nil { + resp.Diagnostics.Append(diags) + return + } + + for k, v := range fields { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(k), v)...) + } +} + +func (r *NetworkAclResource) SyncState(ctx context.Context, tfState *tfsdk.State, server incus.InstanceServer, m NetworkAclModel) diag.Diagnostics { + aclName := m.Name.ValueString() + acl, _, err := server.GetNetworkACL(aclName) + if err != nil { + if errors.IsNotFoundError(err) { + tfState.RemoveResource(ctx) + return nil + } + + return diag.Diagnostics{diag.NewErrorDiagnostic( + fmt.Sprintf("Failed to retrieve network ACL %q", aclName), err.Error(), + )} + } + + config, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(acl.Config), m.Config) + if diags.HasError() { + return diags + } + + egress, diags := ToNetworkAclRulesListType(acl.Egress) + if diags.HasError() { + return diags + } + + ingress, diags := ToNetworkAclRulesListType(acl.Ingress) + if diags.HasError() { + return diags + } + + m.Name = types.StringValue(acl.Name) + m.Description = types.StringValue(acl.Description) + m.Config = config + m.Egress = egress + m.Ingress = ingress + + return tfState.Set(ctx, &m) +} + +func ToNetworkAclRulesListType(networkACLRules []api.NetworkACLRule) (types.Set, diag.Diagnostics) { + aclRuleObjectType := getAclRuleObjectType() + nilSet := types.SetNull(aclRuleObjectType) + + if len(networkACLRules) == 0 { + return nilSet, nil + } + + var aclRuleList []attr.Value + for _, rule := range networkACLRules { + // Create the attribute map for each rule + aclRuleMap := map[string]attr.Value{ + "action": types.StringValue(rule.Action), + "destination": types.StringValue(rule.Destination), + "destination_port": types.StringValue(rule.DestinationPort), + "protocol": types.StringValue(rule.Protocol), + "description": types.StringValue(rule.Description), + "state": types.StringValue(rule.State), + "source": types.StringValue(rule.Source), + "icmp_type": types.StringValue(rule.ICMPType), + "icmp_code": types.StringValue(rule.ICMPCode), + } + + aclRuleObject, diags := types.ObjectValue(aclRuleObjectType.AttrTypes, aclRuleMap) + if diags.HasError() { + return nilSet, diags + } + aclRuleList = append(aclRuleList, aclRuleObject) + } + + return types.SetValue(aclRuleObjectType, aclRuleList) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index dd00d12..4089fe3 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -270,6 +270,7 @@ func (p *IncusProvider) Resources(_ context.Context) []func() resource.Resource network.NewNetworkLBResource, network.NewNetworkZoneResource, network.NewNetworkZoneRecordResource, + network.NewNetworkAclResource, profile.NewProfileResource, project.NewProjectResource, storage.NewStoragePoolResource, From f36ed672f67469718348673cd2551597c7bfd926 Mon Sep 17 00:00:00 2001 From: Fabian Mettler Date: Thu, 20 Jun 2024 09:19:54 +0200 Subject: [PATCH 2/3] tests: Add network ACL resource Signed-off-by: Fabian Mettler --- internal/network/resource_network_acl_test.go | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 internal/network/resource_network_acl_test.go diff --git a/internal/network/resource_network_acl_test.go b/internal/network/resource_network_acl_test.go new file mode 100644 index 0000000..25c30b9 --- /dev/null +++ b/internal/network/resource_network_acl_test.go @@ -0,0 +1,238 @@ +package network_test + +import ( + "fmt" + "testing" + + petname "github.com/dustinkirkland/golang-petname" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/lxc/terraform-provider-incus/internal/acctest" +) + +func TestAccNetworkACL_basic(t *testing.T) { + aclName := petname.Generate(2, "-") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkACL(aclName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network_acl.acl", "name", aclName), + resource.TestCheckResourceAttr("incus_network_acl.acl", "description", "Network ACL"), + ), + }, + }, + }) +} + +func TestAccNetworkACL_egress(t *testing.T) { + aclName := petname.Generate(2, "-") + + entry1 := map[string]string{ + "action": "allow", + "destination": "1.1.1.1,1.0.0.1", + "destination_port": "53", + "protocol": "udp", + "description": "DNS to cloudflare public resolvers (UDP)", + "state": "enabled", + } + + entry2 := map[string]string{ + "action": "allow", + "destination": "1.1.1.1,1.0.0.1", + "destination_port": "53", + "protocol": "tcp", + "description": "DNS to cloudflare public resolvers (TCP)", + "state": "enabled", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkACL_withEgressRules(aclName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network_acl.acl", "name", aclName), + resource.TestCheckResourceAttr("incus_network_acl.acl", "description", "Network ACL"), + resource.TestCheckTypeSetElemNestedAttrs("incus_network_acl.acl", "egress.*", entry1), + resource.TestCheckTypeSetElemNestedAttrs("incus_network_acl.acl", "egress.*", entry2), + ), + }, + }, + }) +} + +func TestAccNetworkACL_ingress(t *testing.T) { + aclName := petname.Generate(2, "-") + + entry := map[string]string{ + "action": "allow", + "source": "@external", + "destination_port": "22", + "protocol": "tcp", + "description": "Incoming SSH connections", + "state": "logged", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkACL_withIngressRules(aclName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network_acl.acl", "name", aclName), + resource.TestCheckResourceAttr("incus_network_acl.acl", "description", "Network ACL"), + resource.TestCheckTypeSetElemNestedAttrs("incus_network_acl.acl", "ingress.*", entry), + ), + }, + }, + }) +} + +func TestAccNetworkACL_egressAndIngress(t *testing.T) { + aclName := petname.Generate(2, "-") + + ingresEntry := map[string]string{ + "action": "allow", + "source": "@external", + "destination_port": "22", + "protocol": "tcp", + "description": "Incoming SSH connections", + "state": "logged", + } + + egressEntry1 := map[string]string{ + "action": "allow", + "destination": "1.1.1.1,1.0.0.1", + "destination_port": "53", + "protocol": "udp", + "description": "DNS to cloudflare public resolvers (UDP)", + "state": "enabled", + } + + egressEntry2 := map[string]string{ + "action": "allow", + "destination": "1.1.1.1,1.0.0.1", + "destination_port": "53", + "protocol": "tcp", + "description": "DNS to cloudflare public resolvers (TCP)", + "state": "enabled", + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccNetworkACL_withTrafficRules(aclName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("incus_network_acl.acl", "name", aclName), + resource.TestCheckResourceAttr("incus_network_acl.acl", "description", "Network ACL"), + resource.TestCheckTypeSetElemNestedAttrs("incus_network_acl.acl", "egress.*", egressEntry1), + resource.TestCheckTypeSetElemNestedAttrs("incus_network_acl.acl", "egress.*", egressEntry2), + resource.TestCheckTypeSetElemNestedAttrs("incus_network_acl.acl", "ingress.*", ingresEntry), + ), + }, + }, + }) +} + +func testAccNetworkACL(aclName string) string { + return fmt.Sprintf(` +resource "incus_network_acl" "acl" { + name = "%s" + description = "Network ACL" +} +`, aclName) +} + +func testAccNetworkACL_withEgressRules(aclName string) string { + return fmt.Sprintf(` +resource "incus_network_acl" "acl" { + name = "%[1]s" + description = "Network ACL" + + egress = [ + { + action = "allow" + destination = "1.1.1.1,1.0.0.1" + destination_port = "53" + protocol = "udp" + description = "DNS to cloudflare public resolvers (UDP)" + state = "enabled" + }, + { + action = "allow" + destination = "1.1.1.1,1.0.0.1" + destination_port = "53" + protocol = "tcp" + description = "DNS to cloudflare public resolvers (TCP)" + state = "enabled" + } + ] +} +`, aclName) +} + +func testAccNetworkACL_withIngressRules(aclName string) string { + return fmt.Sprintf(` +resource "incus_network_acl" "acl" { + name = "%[1]s" + description = "Network ACL" + + ingress = [ + { + action = "allow" + source = "@external" + destination_port = "22" + protocol = "tcp" + description = "Incoming SSH connections" + state = "logged" + } + ] +} +`, aclName) +} + +func testAccNetworkACL_withTrafficRules(aclName string) string { + return fmt.Sprintf(` +resource "incus_network_acl" "acl" { + name = "%[1]s" + description = "Network ACL" + + egress = [ + { + action = "allow" + destination = "1.1.1.1,1.0.0.1" + destination_port = "53" + protocol = "udp" + description = "DNS to cloudflare public resolvers (UDP)" + state = "enabled" + }, + { + action = "allow" + destination = "1.1.1.1,1.0.0.1" + destination_port = "53" + protocol = "tcp" + description = "DNS to cloudflare public resolvers (TCP)" + state = "enabled" + } + ] + + ingress = [ + { + action = "allow" + source = "@external" + destination_port = "22" + protocol = "tcp" + description = "Incoming SSH connections" + state = "logged" + } + ] +} +`, aclName) +} From 4d5b9d5324b32c9c01065e8a66440ce380097856 Mon Sep 17 00:00:00 2001 From: Fabian Mettler Date: Thu, 20 Jun 2024 09:20:06 +0200 Subject: [PATCH 3/3] docs: Add network ACL resource Signed-off-by: Fabian Mettler --- docs/resources/network_acl.md | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 docs/resources/network_acl.md diff --git a/docs/resources/network_acl.md b/docs/resources/network_acl.md new file mode 100644 index 0000000..59ea25c --- /dev/null +++ b/docs/resources/network_acl.md @@ -0,0 +1,111 @@ +# incus_network_acl + +Manages an Incus network ACL. + +See Incus network ACL [configuration reference](https://linuxcontainers.org/incus/docs/main/howto/network_acls/) for how to configure network ACLs. + +## Example Usage + +```hcl +resource "incus_network_acl" "acl1" { + name = "my-acl" + + egress = [ + { + action = "allow" + destination = "1.1.1.1,1.0.0.1" + destination_port = "53" + protocol = "udp" + description = "DNS to cloudflare public resolvers (UDP)" + state = "enabled" + }, + { + action = "allow" + destination = "1.1.1.1,1.0.0.1" + destination_port = "53" + protocol = "tcp" + description = "DNS to cloudflare public resolvers (TCP)" + state = "enabled" + }, + ] + + ingress = [ + { + action = "allow" + source = "@external" + destination_port = "22" + protocol = "tcp" + description = "Incoming SSH connections" + state = "logged" + } + ] +} +``` + +## Argument Reference + +* `name` - **Required** - Name of the network ACL. + +* `description` - *Optional* - Description of the network ACL. + +* `config` - *Optional* - Map of key/value pairs of + [network ACL config settings](hhttps://linuxcontainers.org/incus/docs/main/howto/network_acls/). + +* `project` - *Optional* - Name of the project where the network ACL 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. + +* `egress` - *Optional* - List of network ACL rules for egress traffic. See reference below. + +* `ingress` - *Optional* - List of network ACL rules for ingress traffic. See reference below. + +The network ACL rule supports: + +* `action` - **Required** - Action to take for matching traffic , must be one of allow, allow-stateless, drop, reject + +* `description` - *Optional* - Description of the network ACL rule. + +* `destination_port` - *Optional* - If protocol is `udp` or tcp, then a comma-separated list of ports or port ranges (start-end inclusive), or empty for any + +* `destination` - *Optional* - Comma-separated list of CIDR or IP ranges, destination subject name selectors (for egress rules), or empty for any + +* `icmp_code` - *Optional* - If protocol is `icmp4` or `icmp6`, then ICMP code number, or empty for any + +* `icmp_type` - *Optional* - If protocol is `icmp4` or `icmp6`, then ICMP type number, or empty for any + +* `protocol` - *Optional* - If protocol is `udp` or `tcp`, then a comma-separated list of ports or port ranges (start-end inclusive), or empty for any + +* `source` - *Optional* - Comma-separated list of CIDR or IP ranges, source subject name selectors (for ingress rules), or empty for any + +* `state` - *Optional* - State of the rule (enabled, disabled or logged), defaulting to enabled if not specified + +## Importing + +Import ID syntax: `[:][/]` + +* `` - *Optional* - Remote name. +* `` - *Optional* - Project name. +* `` - **Required** - Network name. + +### Import example + +Example using terraform import command: + +```shell +$ terraform import incus_network_acl.acl1 proj/acl1 +``` + +Example using the import block (only available in Terraform v1.5.0 and later): + +```hcl +resource "incus_network_acl" "acl1" { + name = "acl1" + project = "proj" +} + +import { + to = incus_network_acl.acl1 + id = "proj/acl1" +} +``` \ No newline at end of file