From b4f2f685cd1d72f3ec450d5bbbc5c295cc9977ef Mon Sep 17 00:00:00 2001 From: Adam Weingarten <6517820+aweingarten@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:08:17 -0500 Subject: [PATCH 1/6] LKE Cluster: enterprise zero-pool provisioning and ruleset discovery --- docs/data-sources/lke_cluster.md | 6 ++ docs/resources/lke_cluster.md | 8 +- linode/lke/resource.go | 122 ++++++++++++++++++++++++++----- linode/lke/schema_resource.go | 20 +++++ 4 files changed, 135 insertions(+), 21 deletions(-) diff --git a/docs/data-sources/lke_cluster.md b/docs/data-sources/lke_cluster.md index 9df7a4ae7..b4b6690a4 100644 --- a/docs/data-sources/lke_cluster.md +++ b/docs/data-sources/lke_cluster.md @@ -49,6 +49,12 @@ In addition to all arguments above, the following attributes are exported: * `apl_enabled` - Enables the App Platform Layer +* `ruleset_ids` - The IDs of the service-managed firewall rulesets automatically created for LKE Enterprise clusters. + + * `inbound` - The ID of the inbound service-managed ruleset. + + * `outbound` - The ID of the outbound service-managed ruleset. + * `subnet_id` - The ID of the VPC subnet to use for the Kubernetes cluster. This subnet must be dual stack (IPv4 and IPv6 should both be enabled). * `vpc_id` - The ID of the VPC to use for the Kubernetes cluster. diff --git a/docs/resources/lke_cluster.md b/docs/resources/lke_cluster.md index ce19440f7..2f984b91c 100644 --- a/docs/resources/lke_cluster.md +++ b/docs/resources/lke_cluster.md @@ -175,7 +175,7 @@ The following arguments are supported: * `region` - (Required) This Kubernetes cluster's location. -* [`pool`](#pool) - (Required) The Node Pool specifications for the Kubernetes cluster. At least one Node Pool is required. +* [`pool`](#pool) - (Required) The Node Pool specifications for the Kubernetes cluster. At least one Node Pool is required for standard tier clusters. Enterprise tier clusters (`tier = "enterprise"`) may be created with zero inline pools. * [`control_plane`](#control_plane) (Optional) Defines settings for the Kubernetes Control Plane. @@ -275,6 +275,12 @@ In addition to all arguments above, the following attributes are exported: * `apl_enabled` - Enables the App Platform Layer +* `ruleset_ids` - The IDs of the service-managed firewall rulesets automatically created for LKE Enterprise clusters. Only populated for enterprise tier clusters. + + * `inbound` - The ID of the inbound service-managed ruleset. + + * `outbound` - The ID of the outbound service-managed ruleset. + * `pool` - Additional nested attributes: * `id` - The ID of the Node Pool. diff --git a/linode/lke/resource.go b/linode/lke/resource.go index 7763e70a5..7894526a8 100644 --- a/linode/lke/resource.go +++ b/linode/lke/resource.go @@ -139,6 +139,22 @@ func readResource(ctx context.Context, d *schema.ResourceData, meta any) diag.Di d.Set("vpc_id", cluster.VpcID) d.Set("stack_type", cluster.StackType) + // Neither POST nor GET /lke/clusters/{id} returns ruleset_ids. + // For enterprise clusters, discover them via GET /networking/firewalls/rulesets + // matching the lke{id}-inbound / lke{id}-outbound label convention. + if cluster.Tier == TierEnterprise { + rulesetIDs, rsDiags := discoverClusterRulesets(ctx, client, id) + if rsDiags.HasError() { + return rsDiags + } + if rulesetIDs != nil { + d.Set("ruleset_ids", []map[string]any{{ + "inbound": rulesetIDs.Inbound, + "outbound": rulesetIDs.Outbound, + }}) + } + } + matchedPools, err := matchPoolsWithSchema(ctx, pools, declaredPools) if err != nil { return diag.Errorf("failed to match api pools with schema: %s", err) @@ -251,6 +267,8 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta any) diag. } d.SetId(strconv.Itoa(cluster.ID)) + // ruleset_ids are discovered by readResource via ListFirewallRuleSets. + // Currently the enterprise cluster kube config takes long time to generate. // Wait for it to be ready before start waiting for nodes and allow a longer timeout for retrying // to avoid context exceeded or canceled before getting a meaningful result. @@ -266,27 +284,34 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta any) diag. } ctx = tflog.SetField(ctx, "cluster_id", cluster.ID) - tflog.Debug(ctx, "Waiting for a single LKE cluster node to be ready") - - // Sometimes the K8S API will raise an EOF error if polling immediately after - // a cluster is created. We should retry accordingly. - // NOTE: This routine has a short retry period because we want to raise - // and meaningful errors quickly. - diag.FromErr(retry.RetryContext(ctx, retryContextTimeout, func() *retry.RetryError { - tflog.Debug(ctx, "client.WaitForLKEClusterCondition(...)", map[string]any{ - "condition": "ClusterHasReadyNode", - }) - err := client.WaitForLKEClusterConditions(ctx, cluster.ID, linodego.LKEClusterPollOptions{ - TimeoutSeconds: 15 * 60, - }, k8scondition.ClusterHasReadyNode) - if err != nil { - tflog.Debug(ctx, err.Error()) - return retry.RetryableError(err) - } + // Only wait for ready nodes when pools were requested. + // Enterprise tier clusters can be created with zero pools. + if len(createOpts.NodePools) > 0 { + tflog.Debug(ctx, "Waiting for a single LKE cluster node to be ready") + + // Sometimes the K8S API will raise an EOF error if polling immediately after + // a cluster is created. We should retry accordingly. + // NOTE: This routine has a short retry period because we want to raise + // and meaningful errors quickly. + diag.FromErr(retry.RetryContext(ctx, retryContextTimeout, func() *retry.RetryError { + tflog.Debug(ctx, "client.WaitForLKEClusterCondition(...)", map[string]any{ + "condition": "ClusterHasReadyNode", + }) + + err := client.WaitForLKEClusterConditions(ctx, cluster.ID, linodego.LKEClusterPollOptions{ + TimeoutSeconds: 15 * 60, + }, k8scondition.ClusterHasReadyNode) + if err != nil { + tflog.Debug(ctx, err.Error()) + return retry.RetryableError(err) + } - return nil - })) + return nil + })) + } else { + tflog.Debug(ctx, "No pools defined, skipping wait for ready node") + } return readResource(ctx, d, meta) } @@ -546,7 +571,12 @@ func populateLogAttributes(ctx context.Context, d *schema.ResourceData) context. func customDiffValidateOptionalCount(ctx context.Context, diff *schema.ResourceDiff, meta any) error { invalidPools := make([]string, 0) - poolIterator := diff.GetRawConfig().GetAttr("pool").ElementIterator() + pool := diff.GetRawConfig().GetAttr("pool") + if pool.IsNull() || !pool.IsKnown() || pool.LengthInt() == 0 { + return nil + } + + poolIterator := pool.ElementIterator() for poolIterator.Next() { rawKey, rawPool := poolIterator.Element() @@ -601,3 +631,55 @@ func customDiffValidatePoolForStandardTier(ctx context.Context, diff *schema.Res return nil } + +// discoverClusterRulesets looks up the LKE-E service-managed rulesets via +// GET /networking/firewalls/rulesets, matching the label convention +// lke{clusterID}-inbound / lke{clusterID}-outbound. +// Neither POST nor GET /lke/clusters/{id} returns these IDs, so this is +// the only way to populate ruleset_ids during read and import. +func discoverClusterRulesets( + ctx context.Context, client linodego.Client, clusterID int, +) (*linodego.LKEClusterRuleSetIDs, diag.Diagnostics) { + inboundLabel := fmt.Sprintf("lke%d-inbound", clusterID) + outboundLabel := fmt.Sprintf("lke%d-outbound", clusterID) + + tflog.Debug(ctx, "Discovering LKE-E rulesets", map[string]any{ + "inbound_label": inboundLabel, + "outbound_label": outboundLabel, + }) + + rulesets, err := client.ListFirewallRuleSets(ctx, &linodego.ListOptions{}) + if err != nil { + return nil, diag.Errorf( + "failed to list firewall rulesets for cluster %d: %s", clusterID, err, + ) + } + + var inboundID, outboundID int + for _, rs := range rulesets { + switch rs.Label { + case inboundLabel: + inboundID = rs.ID + case outboundLabel: + outboundID = rs.ID + } + } + + if inboundID == 0 || outboundID == 0 { + tflog.Warn(ctx, "LKE-E rulesets not found", map[string]any{ + "inbound_id": inboundID, + "outbound_id": outboundID, + }) + return nil, nil + } + + tflog.Info(ctx, "Found LKE-E rulesets", map[string]any{ + "inbound_id": inboundID, + "outbound_id": outboundID, + }) + + return &linodego.LKEClusterRuleSetIDs{ + Inbound: inboundID, + Outbound: outboundID, + }, nil +} diff --git a/linode/lke/schema_resource.go b/linode/lke/schema_resource.go index 0ffeaf7d2..b696e0017 100644 --- a/linode/lke/schema_resource.go +++ b/linode/lke/schema_resource.go @@ -102,6 +102,26 @@ var resourceSchema = map[string]*schema.Schema{ }, false), ), }, + "ruleset_ids": { + Type: schema.TypeList, + Computed: true, + Description: "The IDs of the service-managed firewall rulesets automatically " + + "created for LKE Enterprise clusters.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "inbound": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the inbound service ruleset.", + }, + "outbound": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the outbound service ruleset.", + }, + }, + }, + }, "pool": { Type: schema.TypeList, Elem: &schema.Resource{ From 2d97828b4415fd0a91d203018b886e12ae7510a4 Mon Sep 17 00:00:00 2001 From: Adam Weingarten <6517820+aweingarten@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:10:48 -0500 Subject: [PATCH 2/6] LKE Node Pool: add isolation and disk encryption support with documentation --- docs/data-sources/lke_cluster.md | 6 +++ docs/data-sources/lke_node_pool.md | 6 +++ docs/resources/lke_cluster.md | 18 ++++++++ docs/resources/lke_node_pool.md | 18 ++++++++ linode/lke/cluster.go | 36 ++++++++++++++++ linode/lke/resource.go | 19 ++++++++- linode/lke/schema_resource.go | 28 +++++++++++++ linode/lkenodepool/framework_models.go | 41 +++++++++++++++++++ .../lkenodepool/framework_resource_schema.go | 31 +++++++++++++- 9 files changed, 200 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/lke_cluster.md b/docs/data-sources/lke_cluster.md index b4b6690a4..5d72605ad 100644 --- a/docs/data-sources/lke_cluster.md +++ b/docs/data-sources/lke_cluster.md @@ -75,6 +75,12 @@ In addition to all arguments above, the following attributes are exported: * `disk_encryption` - The disk encryption policy for nodes in this pool. + * `isolation` - Network isolation settings for the node pool. + + * `public_ipv4` - Whether nodes have public IPv4 addresses. + + * `public_ipv6` - Whether nodes have public IPv6 addresses. + * `tags` - An array of tags applied to this object. Tags are case-insensitive and are for organizational purposes only. * `tier` - The desired Kubernetes tier. **NOTE: This field may not be available to all users and is only populated when api_version is set to `v4beta`.** diff --git a/docs/data-sources/lke_node_pool.md b/docs/data-sources/lke_node_pool.md index 8b5172963..c91aa88c6 100644 --- a/docs/data-sources/lke_node_pool.md +++ b/docs/data-sources/lke_node_pool.md @@ -40,6 +40,12 @@ In addition to all arguments above, the following attributes are exported: * `disk_encryption` - Indicates the local disk encryption setting for this LKE node pool. +* `isolation` - Network isolation settings for this node pool. + + * `public_ipv4` - Whether nodes have public IPv4 addresses. + + * `public_ipv6` - Whether nodes have public IPv6 addresses. + * `disks` - This node pool's custom disk layout. * `size` - The size of this custom disk partition in MB. diff --git a/docs/resources/lke_cluster.md b/docs/resources/lke_cluster.md index 2f984b91c..aca46aa4a 100644 --- a/docs/resources/lke_cluster.md +++ b/docs/resources/lke_cluster.md @@ -221,6 +221,10 @@ The following arguments are supported in the `pool` specification block: * [`autoscaler`](#autoscaler) - (Optional) If defined, an autoscaler will be enabled with the given configuration. +* `disk_encryption` - (Optional) The disk encryption policy for nodes in this pool. Accepted values are `enabled` and `disabled`. Changing this forces recreation of the pool. + +* [`isolation`](#isolation) - (Optional) Network isolation settings for the node pool. + * `k8s_version` - (Optional) The k8s version of the nodes in this Node Pool. For LKE enterprise only and may not currently available to all users even under v4beta. * `update_strategy` - (Optional) The strategy for updating the Node Pool k8s version. For LKE enterprise only and may not currently available to all users even under v4beta. @@ -233,6 +237,14 @@ The following arguments are supported in the `autoscaler` specification block: * `max` - (Required) The maximum number of nodes to autoscale to. +### isolation + +The following arguments are supported in the `isolation` specification block: + +* `public_ipv4` - (Optional) Whether nodes in this pool should have public IPv4 addresses. Defaults to `true`. + +* `public_ipv6` - (Optional) Whether nodes in this pool should have public IPv6 addresses. Defaults to `true`. + ### control_plane The following arguments are supported in the `control_plane` specification block: @@ -287,6 +299,12 @@ In addition to all arguments above, the following attributes are exported: * `disk_encryption` - The disk encryption policy for nodes in this pool. + * `isolation` - Network isolation settings for the node pool. + + * `public_ipv4` - Whether nodes have public IPv4 addresses. + + * `public_ipv6` - Whether nodes have public IPv6 addresses. + * [`nodes`](#nodes) - The nodes in the Node Pool. ### nodes diff --git a/docs/resources/lke_node_pool.md b/docs/resources/lke_node_pool.md index 7d9a6bc72..515509c5e 100644 --- a/docs/resources/lke_node_pool.md +++ b/docs/resources/lke_node_pool.md @@ -125,6 +125,10 @@ The following arguments are supported: * [`autoscaler`](#autoscaler) - (Optional) If defined, an autoscaler will be enabled with the given configuration. +* `disk_encryption` - (Optional) The disk encryption policy for nodes in this pool. Accepted values are `enabled` and `disabled`. Changing this forces recreation of the pool. + +* [`isolation`](#isolation) - (Optional) Network isolation settings for the node pool. + * [`taint`](#taint) - (Optional) Kubernetes taints to add to node pool nodes. Taints help control how pods are scheduled onto nodes, specifically allowing them to repel certain pods. To learn more, review [Add Labels and Taints to your LKE Node Pools](https://www.linode.com/docs/products/compute/kubernetes/guides/deploy-and-manage-cluster-with-the-linode-api/#add-labels-and-taints-to-your-lke-node-pools). ### autoscaler @@ -145,6 +149,14 @@ The following arguments are supported in the `taint` specification block: * `value` - (Required) The Kubernetes taint value. +### isolation + +The following arguments are supported in the `isolation` specification block: + +* `public_ipv4` - (Optional) Whether nodes in this pool should have public IPv4 addresses. Defaults to `true`. + +* `public_ipv6` - (Optional) Whether nodes in this pool should have public IPv6 addresses. Defaults to `true`. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: @@ -153,6 +165,12 @@ In addition to all arguments above, the following attributes are exported: * `disk_encryption` - The disk encryption policy for nodes in this pool. +* `isolation` - Network isolation settings for the node pool. + + * `public_ipv4` - Whether nodes have public IPv4 addresses. + + * `public_ipv6` - Whether nodes have public IPv6 addresses. + * [`nodes`](#nodes) - The nodes in the Node Pool. ### nodes diff --git a/linode/lke/cluster.go b/linode/lke/cluster.go index 87abe4415..8ea2df7ac 100644 --- a/linode/lke/cluster.go +++ b/linode/lke/cluster.go @@ -29,6 +29,8 @@ type NodePoolSpec struct { UpdateStrategy *string Label *string FirewallID *int + DiskEncryption string + Isolation *linodego.LKENodePoolIsolation } type NodePoolUpdates struct { @@ -62,6 +64,14 @@ func ReconcileLKENodePoolSpecs( UpdateStrategy: updateStrategy, } + if spec.DiskEncryption != "" { + createOpts.DiskEncryption = linodego.InstanceDiskEncryption(spec.DiskEncryption) + } + + if spec.Isolation != nil { + createOpts.Isolation = spec.Isolation + } + if spec.Label != nil && *spec.Label != "" { createOpts.Label = spec.Label } @@ -516,6 +526,21 @@ func expandLinodeLKENodePoolSpecs(pool []any, preserveNoTarget bool) (poolSpecs firewallIdPtr = &v } + var diskEncryption string + if v, ok := specMap["disk_encryption"].(string); ok && v != "" { + diskEncryption = v + } + + var isolation *linodego.LKENodePoolIsolation + if isoList, ok := specMap["isolation"].([]any); ok && len(isoList) > 0 { + if isoMap, ok := isoList[0].(map[string]any); ok { + isolation = &linodego.LKENodePoolIsolation{ + PublicIPv4: isoMap["public_ipv4"].(bool), + PublicIPv6: isoMap["public_ipv6"].(bool), + } + } + } + poolSpecs = append(poolSpecs, NodePoolSpec{ ID: specMap["id"].(int), Label: labelPtr, @@ -530,6 +555,8 @@ func expandLinodeLKENodePoolSpecs(pool []any, preserveNoTarget bool) (poolSpecs AutoScalerMax: autoscaler.Max, K8sVersion: k8sVersionPtr, UpdateStrategy: updateStrategyPtr, + DiskEncryption: diskEncryption, + Isolation: isolation, }) } return poolSpecs @@ -574,6 +601,15 @@ func flattenLKENodePools(pools []linodego.LKENodePool) []map[string]any { "update_strategy": pool.UpdateStrategy, "firewall_id": pool.FirewallID, } + + if pool.Isolation != nil { + flattened[i]["isolation"] = []map[string]any{ + { + "public_ipv4": pool.Isolation.PublicIPv4, + "public_ipv6": pool.Isolation.PublicIPv6, + }, + } + } } return flattened } diff --git a/linode/lke/resource.go b/linode/lke/resource.go index 7894526a8..66fba6938 100644 --- a/linode/lke/resource.go +++ b/linode/lke/resource.go @@ -241,7 +241,7 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta any) diag. firewallId = linodego.Pointer(poolSpec["firewall_id"].(int)) } - createOpts.NodePools = append(createOpts.NodePools, linodego.LKENodePoolCreateOptions{ + poolCreateOpts := linodego.LKENodePoolCreateOptions{ Label: label, FirewallID: firewallId, Type: poolSpec["type"].(string), @@ -250,7 +250,22 @@ func createResource(ctx context.Context, d *schema.ResourceData, meta any) diag. Labels: helper.StringAnyMapToTyped[string](poolSpec["labels"].(map[string]any)), Count: count, Autoscaler: autoscaler, - }) + } + + if v, ok := poolSpec["disk_encryption"].(string); ok && v != "" { + poolCreateOpts.DiskEncryption = linodego.InstanceDiskEncryption(v) + } + + if isoList, ok := poolSpec["isolation"].([]any); ok && len(isoList) > 0 { + if isoMap, ok := isoList[0].(map[string]any); ok { + poolCreateOpts.Isolation = &linodego.LKENodePoolIsolation{ + PublicIPv4: isoMap["public_ipv4"].(bool), + PublicIPv6: isoMap["public_ipv6"].(bool), + } + } + } + + createOpts.NodePools = append(createOpts.NodePools, poolCreateOpts) } if tagsRaw, tagsOk := d.GetOk("tags"); tagsOk { diff --git a/linode/lke/schema_resource.go b/linode/lke/schema_resource.go index b696e0017..662ff88b6 100644 --- a/linode/lke/schema_resource.go +++ b/linode/lke/schema_resource.go @@ -232,7 +232,35 @@ var resourceSchema = map[string]*schema.Schema{ "disk_encryption": { Type: schema.TypeString, Description: "The disk encryption policy for the nodes in this pool.", + Optional: true, Computed: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "enabled", "disabled", + }, false), + }, + "isolation": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "Isolation configuration for the node pool.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "public_ipv4": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Whether public IPv4 is enabled for nodes in this pool.", + }, + "public_ipv6": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Whether public IPv6 is enabled for nodes in this pool.", + }, + }, + }, }, "nodes": { Type: schema.TypeList, diff --git a/linode/lkenodepool/framework_models.go b/linode/lkenodepool/framework_models.go index 1e631e6f1..49e16d347 100644 --- a/linode/lkenodepool/framework_models.go +++ b/linode/lkenodepool/framework_models.go @@ -27,6 +27,7 @@ type NodePoolModel struct { UpdateStrategy types.String `tfsdk:"update_strategy"` Label types.String `tfsdk:"label"` FirewallID types.Int64 `tfsdk:"firewall_id"` + Isolation []NodePoolIsolationModel `tfsdk:"isolation"` } type NodePoolAutoscalerModel struct { @@ -39,6 +40,11 @@ type NodePoolTaintModel struct { Key types.String `tfsdk:"key"` Value types.String `tfsdk:"value"` } + +type NodePoolIsolationModel struct { + PublicIPv4 types.Bool `tfsdk:"public_ipv4"` + PublicIPv6 types.Bool `tfsdk:"public_ipv6"` +} type nodePoolDataSourceModel struct { ID types.Int64 `tfsdk:"id"` ClusterID types.Int64 `tfsdk:"cluster_id"` @@ -221,6 +227,15 @@ func (pool *NodePoolModel) FlattenLKENodePool( } else { pool.UpdateStrategy = helper.KeepOrUpdateString(pool.UpdateStrategy, "", preserveKnown) } + + if !preserveKnown && p.Isolation != nil { + pool.Isolation = []NodePoolIsolationModel{ + { + PublicIPv4: types.BoolValue(p.Isolation.PublicIPv4), + PublicIPv6: types.BoolValue(p.Isolation.PublicIPv6), + }, + } + } } func (pool *NodePoolModel) SetNodePoolCreateOptions( @@ -263,6 +278,17 @@ func (pool *NodePoolModel) SetNodePoolCreateOptions( p.UpdateStrategy = linodego.Pointer(linodego.LKENodePoolUpdateStrategy(pool.UpdateStrategy.ValueString())) } } + + if !pool.DiskEncryption.IsNull() && !pool.DiskEncryption.IsUnknown() { + p.DiskEncryption = linodego.InstanceDiskEncryption(pool.DiskEncryption.ValueString()) + } + + if len(pool.Isolation) > 0 { + p.Isolation = &linodego.LKENodePoolIsolation{ + PublicIPv4: pool.Isolation[0].PublicIPv4.ValueBool(), + PublicIPv6: pool.Isolation[0].PublicIPv6.ValueBool(), + } + } } func (pool *NodePoolModel) SetNodePoolUpdateOptions( @@ -354,6 +380,20 @@ func (pool *NodePoolModel) SetNodePoolUpdateOptions( } } + // Isolation updates + if len(pool.Isolation) > 0 { + newIso := &linodego.LKENodePoolIsolation{ + PublicIPv4: pool.Isolation[0].PublicIPv4.ValueBool(), + PublicIPv6: pool.Isolation[0].PublicIPv6.ValueBool(), + } + if len(state.Isolation) == 0 || + !state.Isolation[0].PublicIPv4.Equal(pool.Isolation[0].PublicIPv4) || + !state.Isolation[0].PublicIPv6.Equal(pool.Isolation[0].PublicIPv6) { + p.Isolation = newIso + shouldUpdate = true + } + } + return shouldUpdate } @@ -451,5 +491,6 @@ func (data *NodePoolModel) CopyFrom(other NodePoolModel, preserveKnown bool) { if !preserveKnown { data.Autoscaler = other.Autoscaler data.Taints = other.Taints + data.Isolation = other.Isolation } } diff --git a/linode/lkenodepool/framework_resource_schema.go b/linode/lkenodepool/framework_resource_schema.go index 9fef67348..267886335 100644 --- a/linode/lkenodepool/framework_resource_schema.go +++ b/linode/lkenodepool/framework_resource_schema.go @@ -71,9 +71,16 @@ var resourceSchema = schema.Schema{ }, "disk_encryption": schema.StringAttribute{ Description: "The disk encryption policy for nodes in this pool.", + Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), +\ stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.OneOf( + string(linodego.InstanceDiskEncryptionEnabled), + string(linodego.InstanceDiskEncryptionDisabled), + ), }, }, "tags": schema.SetAttribute{ @@ -174,6 +181,28 @@ var resourceSchema = schema.Schema{ }, }, }, + + "isolation": schema.ListNestedBlock{ + Description: "Network isolation settings for the node pool. " + + "Controls whether nodes have public IPv4/IPv6 addresses.", + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "public_ipv4": schema.BoolAttribute{ + Description: "Whether nodes in this pool have public IPv4 addresses.", + Optional: true, + Computed: true, + }, + "public_ipv6": schema.BoolAttribute{ + Description: "Whether nodes in this pool have public IPv6 addresses.", + Optional: true, + Computed: true, + }, + }, + }, + }, }, } From c4707dda9f19211e599d7e27fb0d7ede753bc225 Mon Sep 17 00:00:00 2001 From: Adam Weingarten <6517820+aweingarten@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:44:10 -0500 Subject: [PATCH 3/6] Firewall: add ruleset and prefix list support with tests and documentation --- docs/data-sources/firewall.md | 8 +- docs/resources/firewall.md | 8 +- linode/firewall/firewall_helpers_unit_test.go | 219 +++++++++++++++++- linode/firewall/framework_models.go | 106 +++++++-- .../firewall/framework_schema_datasource.go | 5 +- linode/firewall/framework_schema_resource.go | 21 +- 6 files changed, 329 insertions(+), 38 deletions(-) diff --git a/docs/data-sources/firewall.md b/docs/data-sources/firewall.md index 6b293c522..04dec2b55 100644 --- a/docs/data-sources/firewall.md +++ b/docs/data-sources/firewall.md @@ -35,10 +35,14 @@ In addition to all arguments above, the following attributes are exported: * [`inbound`](#inbound-and-outbound) - A firewall rule that specifies what inbound network traffic is allowed. +* `inbound_ruleset` - A list of Firewall Rule Set IDs referenced as inbound rules. + * `inbound_policy` - The default behavior for inbound traffic. (`ACCEPT`, `DROP`) * [`outbound`](#inbound-and-outbound) - A firewall rule that specifies what outbound network traffic is allowed. +* `outbound_ruleset` - A list of Firewall Rule Set IDs referenced as outbound rules. + * `outbound_policy` - The default behavior for outbound traffic. (`ACCEPT`, `DROP`) * `linodes` - The IDs of Linodes assigned to this Firewall. @@ -67,9 +71,9 @@ The following arguments are supported in the inbound and outbound rule blocks: * `ports` - A string representation of ports and/or port ranges (i.e. "443" or "80-90, 91"). -* `ipv4` - A list of IPv4 addresses or networks. Must be in IP/mask format. +* `ipv4` - A list of IPv4 addresses or networks in CIDR format, or prefix list tokens. -* `ipv6` - A list of IPv6 addresses or networks. Must be in IP/mask format. +* `ipv6` - A list of IPv6 addresses or networks in CIDR format, or prefix list tokens. ### devices diff --git a/docs/resources/firewall.md b/docs/resources/firewall.md index 4c31d98aa..b795f0808 100644 --- a/docs/resources/firewall.md +++ b/docs/resources/firewall.md @@ -79,10 +79,14 @@ The following arguments are supported: * `disabled` - (Optional) If `true`, the Firewall's rules are not enforced (defaults to `false`). * [`inbound`](#inbound) - (Optional) A firewall rule that specifies what inbound network traffic is allowed. + +* `inbound_ruleset` - (Optional) A list of Firewall Rule Set IDs to reference as inbound rules. Ruleset references are prepended before any inline `inbound` rules. * `inbound_policy` - (Required) The default behavior for inbound traffic. This setting can be overridden by updating the inbound.action property of the Firewall Rule. (`ACCEPT`, `DROP`) * [`outbound`](#outbound) - (Optional) A firewall rule that specifies what outbound network traffic is allowed. + +* `outbound_ruleset` - (Optional) A list of Firewall Rule Set IDs to reference as outbound rules. Ruleset references are prepended before any inline `outbound` rules. * `outbound_policy` - (Required) The default behavior for outbound traffic. This setting can be overridden by updating the outbound.action property for an individual Firewall Rule. (`ACCEPT`, `DROP`) @@ -108,9 +112,9 @@ The following arguments are supported in the inbound and outbound rule blocks: * `ports` - (Optional) A string representation of ports and/or port ranges (i.e. "443" or "80-90, 91"). -* `ipv4` - (Optional) A list of IPv4 addresses or networks. Must be in IP/mask (CIDR) format. +* `ipv4` - (Optional) A list of IPv4 addresses or networks in CIDR format, or prefix list tokens (e.g. `pl::subnets:123`, `pl:system:ps:managed:container:registry`). -* `ipv6` - (Optional) A list of IPv6 addresses or networks. Must be in IP/mask (CIDR) format. +* `ipv6` - (Optional) A list of IPv6 addresses or networks in CIDR format, or prefix list tokens (e.g. `pl::subnets:123`, `pl:system:ps:managed:container:registry`). ## Attributes Reference diff --git a/linode/firewall/firewall_helpers_unit_test.go b/linode/firewall/firewall_helpers_unit_test.go index e9890e873..024bf3581 100644 --- a/linode/firewall/firewall_helpers_unit_test.go +++ b/linode/firewall/firewall_helpers_unit_test.go @@ -7,7 +7,6 @@ import ( "reflect" "testing" - "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" @@ -63,13 +62,13 @@ func TestExpandFirewallRules(t *testing.T) { Protocol: types.StringValue("SSH"), Ports: types.StringValue("22"), IPv4: types.ListValueMust( - cidrtypes.IPv4PrefixType{}, + types.StringType, []attr.Value{ - cidrtypes.NewIPv4PrefixValue("192.168.1.1/24"), + types.StringValue("192.168.1.1/24"), }, ), IPv6: types.ListValueMust( - cidrtypes.IPv6PrefixType{}, + types.StringType, []attr.Value{}, ), }, @@ -274,3 +273,215 @@ func TestFlattenFirewallDevices(t *testing.T) { } } } + +func TestSeparateRulesetRefs_MixedRules(t *testing.T) { + rules := []linodego.FirewallRule{ + {RuleSet: 4010}, + { + Action: "ACCEPT", + Label: "allow-ssh", + Ports: "22", + Protocol: "TCP", + Addresses: linodego.NetworkAddresses{ + IPv4: &[]string{"0.0.0.0/0"}, + IPv6: &[]string{"::/0"}, + }, + }, + {RuleSet: 4011}, + { + Action: "ACCEPT", + Label: "allow-http", + Ports: "80", + Protocol: "TCP", + Addresses: linodego.NetworkAddresses{ + IPv4: &[]string{"10.0.0.0/8"}, + IPv6: &[]string{}, + }, + }, + } + + rulesetIDs, inlineRules := separateRulesetRefs(rules) + + assert.Equal(t, []int64{4010, 4011}, rulesetIDs) + assert.Len(t, inlineRules, 2) + assert.Equal(t, "allow-ssh", inlineRules[0].Label) + assert.Equal(t, "allow-http", inlineRules[1].Label) +} + +func TestSeparateRulesetRefs_NoRulesets(t *testing.T) { + rules := []linodego.FirewallRule{ + { + Action: "ACCEPT", + Label: "allow-all", + Protocol: "TCP", + Addresses: linodego.NetworkAddresses{ + IPv4: &[]string{"0.0.0.0/0"}, + IPv6: &[]string{"::/0"}, + }, + }, + } + + rulesetIDs, inlineRules := separateRulesetRefs(rules) + + assert.Nil(t, rulesetIDs) + assert.Len(t, inlineRules, 1) + assert.Equal(t, "allow-all", inlineRules[0].Label) +} + +func TestSeparateRulesetRefs_OnlyRulesets(t *testing.T) { + rules := []linodego.FirewallRule{ + {RuleSet: 100}, + {RuleSet: 200}, + } + + rulesetIDs, inlineRules := separateRulesetRefs(rules) + + assert.Equal(t, []int64{100, 200}, rulesetIDs) + assert.Nil(t, inlineRules) +} + +func TestSeparateRulesetRefs_Empty(t *testing.T) { + rulesetIDs, inlineRules := separateRulesetRefs(nil) + + assert.Nil(t, rulesetIDs) + assert.Nil(t, inlineRules) +} + +func TestExpandFirewallRuleSet_WithRulesets(t *testing.T) { + ctx := context.Background() + var diags diag.Diagnostics + + data := &FirewallResourceModel{ + InboundRuleSet: types.ListValueMust(types.Int64Type, []attr.Value{ + types.Int64Value(4010), + }), + OutboundRuleSet: types.ListValueMust(types.Int64Type, []attr.Value{ + types.Int64Value(4011), + }), + Inbound: []RuleModel{ + { + Label: types.StringValue("allow-ssh"), + Action: types.StringValue("ACCEPT"), + Protocol: types.StringValue("TCP"), + Ports: types.StringValue("22"), + IPv4: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("0.0.0.0/0"), + }), + IPv6: types.ListValueMust(types.StringType, []attr.Value{}), + }, + }, + Outbound: []RuleModel{ + { + Label: types.StringValue("outbound-tcp"), + Action: types.StringValue("ACCEPT"), + Protocol: types.StringValue("TCP"), + Ports: types.StringValue("1-65535"), + IPv4: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("pl::subnets:2010"), + }), + IPv6: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("pl::subnets:2010"), + }), + }, + }, + InboundPolicy: types.StringValue("DROP"), + OutboundPolicy: types.StringValue("DROP"), + } + + result := data.ExpandFirewallRuleSet(ctx, &diags) + assert.False(t, diags.HasError()) + + // Inbound: 1 ruleset ref + 1 inline rule + assert.Len(t, result.Inbound, 2) + assert.Equal(t, 4010, result.Inbound[0].RuleSet) + assert.Equal(t, "", result.Inbound[0].Label) + assert.Equal(t, "allow-ssh", result.Inbound[1].Label) + assert.Equal(t, 0, result.Inbound[1].RuleSet) + + // Outbound: 1 ruleset ref + 1 inline rule + assert.Len(t, result.Outbound, 2) + assert.Equal(t, 4011, result.Outbound[0].RuleSet) + assert.Equal(t, "outbound-tcp", result.Outbound[1].Label) + + assert.Equal(t, "DROP", result.InboundPolicy) + assert.Equal(t, "DROP", result.OutboundPolicy) +} + +func TestExpandFirewallRuleSet_NoRulesets(t *testing.T) { + ctx := context.Background() + var diags diag.Diagnostics + + data := &FirewallResourceModel{ + InboundRuleSet: types.ListNull(types.Int64Type), + OutboundRuleSet: types.ListNull(types.Int64Type), + Inbound: []RuleModel{ + { + Label: types.StringValue("allow-ssh"), + Action: types.StringValue("ACCEPT"), + Protocol: types.StringValue("TCP"), + Ports: types.StringValue("22"), + IPv4: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("0.0.0.0/0"), + }), + IPv6: types.ListValueMust(types.StringType, []attr.Value{}), + }, + }, + Outbound: nil, + InboundPolicy: types.StringValue("ACCEPT"), + OutboundPolicy: types.StringValue("DROP"), + } + + result := data.ExpandFirewallRuleSet(ctx, &diags) + assert.False(t, diags.HasError()) + + // Only the inline rule, no ruleset refs + assert.Len(t, result.Inbound, 1) + assert.Equal(t, 0, result.Inbound[0].RuleSet) + assert.Equal(t, "allow-ssh", result.Inbound[0].Label) + assert.Nil(t, result.Outbound) +} + +func TestFlattenFirewallRules_PrefixListStrings(t *testing.T) { + // Validate that prefix list tokens survive flatten (not rejected by CIDR validation) + rules := []linodego.FirewallRule{ + { + Action: "ACCEPT", + Label: "outbound-subnet-tcp", + Ports: "1-65535", + Protocol: "TCP", + Addresses: linodego.NetworkAddresses{ + IPv4: &[]string{"pl::subnets:2010"}, + IPv6: &[]string{"pl::subnets:2010"}, + }, + }, + { + Action: "ACCEPT", + Label: "outbound-registry", + Ports: "443", + Protocol: "TCP", + Addresses: linodego.NetworkAddresses{ + IPv4: &[]string{"pl:system:ps:managed:container:registry"}, + IPv6: &[]string{"pl:system:ps:managed:container:registry"}, + }, + }, + } + + var diags diag.Diagnostics + result := FlattenFirewallRules(context.Background(), rules, nil, false, &diags) + assert.False(t, diags.HasError()) + assert.Len(t, result, 2) + + // First rule: subnet prefix list + assert.Equal(t, "outbound-subnet-tcp", result[0].Label.ValueString()) + var ipv4Vals []string + diags.Append(result[0].IPv4.ElementsAs(context.Background(), &ipv4Vals, false)...) + assert.False(t, diags.HasError()) + assert.Equal(t, []string{"pl::subnets:2010"}, ipv4Vals) + + // Second rule: registry prefix list + assert.Equal(t, "outbound-registry", result[1].Label.ValueString()) + var registryVals []string + diags.Append(result[1].IPv4.ElementsAs(context.Background(), ®istryVals, false)...) + assert.False(t, diags.HasError()) + assert.Equal(t, []string{"pl:system:ps:managed:container:registry"}, registryVals) +} diff --git a/linode/firewall/framework_models.go b/linode/firewall/framework_models.go index e424c2583..b409a2baa 100644 --- a/linode/firewall/framework_models.go +++ b/linode/firewall/framework_models.go @@ -34,21 +34,23 @@ type FirewallDataSourceModel struct { // FirewallResourceModel describes the Terraform resource data model to match the // resource schema. type FirewallResourceModel struct { - ID types.String `tfsdk:"id"` - Label types.String `tfsdk:"label"` - Tags types.Set `tfsdk:"tags"` - Disabled types.Bool `tfsdk:"disabled"` - Inbound []RuleModel `tfsdk:"inbound"` - InboundPolicy types.String `tfsdk:"inbound_policy"` - Outbound []RuleModel `tfsdk:"outbound"` - OutboundPolicy types.String `tfsdk:"outbound_policy"` - Linodes types.Set `tfsdk:"linodes"` - NodeBalancers types.Set `tfsdk:"nodebalancers"` - Interfaces types.Set `tfsdk:"interfaces"` - Devices types.List `tfsdk:"devices"` - Status types.String `tfsdk:"status"` - Created timetypes.RFC3339 `tfsdk:"created"` - Updated timetypes.RFC3339 `tfsdk:"updated"` + ID types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + Tags types.Set `tfsdk:"tags"` + Disabled types.Bool `tfsdk:"disabled"` + Inbound []RuleModel `tfsdk:"inbound"` + InboundRuleSet types.List `tfsdk:"inbound_ruleset"` + InboundPolicy types.String `tfsdk:"inbound_policy"` + Outbound []RuleModel `tfsdk:"outbound"` + OutboundRuleSet types.List `tfsdk:"outbound_ruleset"` + OutboundPolicy types.String `tfsdk:"outbound_policy"` + Linodes types.Set `tfsdk:"linodes"` + NodeBalancers types.Set `tfsdk:"nodebalancers"` + Interfaces types.Set `tfsdk:"interfaces"` + Devices types.List `tfsdk:"devices"` + Status types.String `tfsdk:"status"` + Created timetypes.RFC3339 `tfsdk:"created"` + Updated timetypes.RFC3339 `tfsdk:"updated"` } type RuleModel struct { @@ -107,15 +109,41 @@ func isDisabled(firewall linodego.Firewall) bool { func (data *FirewallResourceModel) ExpandFirewallRuleSet( ctx context.Context, diags *diag.Diagnostics, ) (rules linodego.FirewallRuleSet) { - rules.Inbound = ExpandFirewallRules(ctx, data.Inbound, diags) + // Prepend inbound ruleset references + var inboundRulesetIDs []int64 + if !data.InboundRuleSet.IsNull() && !data.InboundRuleSet.IsUnknown() { + diags.Append(data.InboundRuleSet.ElementsAs(ctx, &inboundRulesetIDs, false)...) + if diags.HasError() { + return rules + } + } + for _, rsID := range inboundRulesetIDs { + rules.Inbound = append(rules.Inbound, linodego.FirewallRule{RuleSet: int(rsID)}) + } + + inlineInbound := ExpandFirewallRules(ctx, data.Inbound, diags) if diags.HasError() { return rules } + rules.Inbound = append(rules.Inbound, inlineInbound...) - rules.Outbound = ExpandFirewallRules(ctx, data.Outbound, diags) + // Prepend outbound ruleset references + var outboundRulesetIDs []int64 + if !data.OutboundRuleSet.IsNull() && !data.OutboundRuleSet.IsUnknown() { + diags.Append(data.OutboundRuleSet.ElementsAs(ctx, &outboundRulesetIDs, false)...) + if diags.HasError() { + return rules + } + } + for _, rsID := range outboundRulesetIDs { + rules.Outbound = append(rules.Outbound, linodego.FirewallRule{RuleSet: int(rsID)}) + } + + inlineOutbound := ExpandFirewallRules(ctx, data.Outbound, diags) if diags.HasError() { return rules } + rules.Outbound = append(rules.Outbound, inlineOutbound...) rules.InboundPolicy = data.InboundPolicy.ValueString() rules.OutboundPolicy = data.OutboundPolicy.ValueString() @@ -218,19 +246,49 @@ func (data *FirewallResourceModel) flattenRules( preserveKnown bool, diags *diag.Diagnostics, ) { - inboundRules := FlattenFirewallRules(ctx, ruleSet.Inbound, data.Inbound, preserveKnown, diags) + // Separate ruleset references from inline rules + inboundRulesetIDs, inlineInbound := separateRulesetRefs(ruleSet.Inbound) + outboundRulesetIDs, inlineOutbound := separateRulesetRefs(ruleSet.Outbound) + + inboundRules := FlattenFirewallRules(ctx, inlineInbound, data.Inbound, preserveKnown, diags) if diags.HasError() { return } - data.Inbound = inboundRules - outboundRules := FlattenFirewallRules(ctx, ruleSet.Outbound, data.Outbound, preserveKnown, diags) + outboundRules := FlattenFirewallRules(ctx, inlineOutbound, data.Outbound, preserveKnown, diags) if diags.HasError() { return } - data.Outbound = outboundRules + + // Flatten inbound ruleset IDs + inboundRSList, newDiags := types.ListValueFrom(ctx, types.Int64Type, inboundRulesetIDs) + diags.Append(newDiags...) + if diags.HasError() { + return + } + data.InboundRuleSet = helper.KeepOrUpdateValue(data.InboundRuleSet, inboundRSList, preserveKnown) + + // Flatten outbound ruleset IDs + outboundRSList, newDiags := types.ListValueFrom(ctx, types.Int64Type, outboundRulesetIDs) + diags.Append(newDiags...) + if diags.HasError() { + return + } + data.OutboundRuleSet = helper.KeepOrUpdateValue(data.OutboundRuleSet, outboundRSList, preserveKnown) +} + +// separateRulesetRefs splits a rule slice into ruleset ID references and inline rules. +func separateRulesetRefs(rules []linodego.FirewallRule) (rulesetIDs []int64, inlineRules []linodego.FirewallRule) { + for _, r := range rules { + if r.RuleSet != 0 { + rulesetIDs = append(rulesetIDs, int64(r.RuleSet)) + } else { + inlineRules = append(inlineRules, r) + } + } + return } func (data *FirewallResourceModel) flattenFirewallForResource( @@ -349,6 +407,9 @@ func (data *FirewallResourceModel) CopyFrom( data.Inbound = other.Inbound data.Outbound = other.Outbound } + + data.InboundRuleSet = helper.KeepOrUpdateValue(data.InboundRuleSet, other.InboundRuleSet, preserveKnown) + data.OutboundRuleSet = helper.KeepOrUpdateValue(data.OutboundRuleSet, other.OutboundRuleSet, preserveKnown) } func (state *FirewallResourceModel) RulesAndPoliciesHaveChanges( @@ -371,7 +432,8 @@ func (state *FirewallResourceModel) RulesAndPoliciesHaveChanges( } return (!oldInbound.Equal(newInbound) || !oldOutbound.Equal(newOutbound) || - !state.InboundPolicy.Equal(plan.InboundPolicy) || !state.OutboundPolicy.Equal(plan.OutboundPolicy)) + !state.InboundPolicy.Equal(plan.InboundPolicy) || !state.OutboundPolicy.Equal(plan.OutboundPolicy) || + !state.InboundRuleSet.Equal(plan.InboundRuleSet) || !state.OutboundRuleSet.Equal(plan.OutboundRuleSet)) } func (state *FirewallResourceModel) LinodesOrNodeBalancersOrInterfacesHaveChanges( diff --git a/linode/firewall/framework_schema_datasource.go b/linode/firewall/framework_schema_datasource.go index bb086a147..f2f3f032b 100644 --- a/linode/firewall/framework_schema_datasource.go +++ b/linode/firewall/framework_schema_datasource.go @@ -1,7 +1,6 @@ package firewall import ( - "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" @@ -24,8 +23,8 @@ var RuleObjectType = types.ObjectType{ "ports": types.StringType, "protocol": types.StringType, "description": types.StringType, - "ipv4": types.ListType{ElemType: cidrtypes.IPv4PrefixType{}}, - "ipv6": types.ListType{ElemType: cidrtypes.IPv6PrefixType{}}, + "ipv4": types.ListType{ElemType: types.StringType}, + "ipv6": types.ListType{ElemType: types.StringType}, }, } diff --git a/linode/firewall/framework_schema_resource.go b/linode/firewall/framework_schema_resource.go index 521f82964..1b4ce8e53 100644 --- a/linode/firewall/framework_schema_resource.go +++ b/linode/firewall/framework_schema_resource.go @@ -1,7 +1,6 @@ package firewall import ( - "github.com/hashicorp/terraform-plugin-framework-nettypes/cidrtypes" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -64,17 +63,17 @@ var ruleNestedObject = schema.NestedBlockObject{ }, }, "ipv4": schema.ListAttribute{ - Description: "A list of CIDR blocks or 0.0.0.0/0 (to allow all) this rule applies to.", + Description: "A list of IPv4 addresses or CIDRs, or prefix list tokens (e.g. pl::subnets:123) this rule applies to.", Optional: true, - ElementType: cidrtypes.IPv4PrefixType{}, + ElementType: types.StringType, Validators: []validator.List{ listvalidator.SizeAtLeast(1), }, }, "ipv6": schema.ListAttribute{ - Description: "A list of IPv6 addresses or networks this rule applies to.", + Description: "A list of IPv6 addresses or networks, or prefix list tokens (e.g. pl::subnets:123) this rule applies to.", Optional: true, - ElementType: cidrtypes.IPv6PrefixType{}, + ElementType: types.StringType, Validators: []validator.List{ listvalidator.SizeAtLeast(1), }, @@ -94,6 +93,18 @@ var frameworkResourceSchema = schema.Schema{ }, }, Attributes: map[string]schema.Attribute{ + "inbound_ruleset": schema.ListAttribute{ + Description: "A list of Firewall Rule Set IDs to reference as inbound rules. " + + "Ruleset references are prepended before any inline inbound rules.", + Optional: true, + ElementType: types.Int64Type, + }, + "outbound_ruleset": schema.ListAttribute{ + Description: "A list of Firewall Rule Set IDs to reference as outbound rules. " + + "Ruleset references are prepended before any inline outbound rules.", + Optional: true, + ElementType: types.Int64Type, + }, "id": schema.StringAttribute{ Description: "The unique ID of this Object Storage key.", Computed: true, From 39e8d8e19427643cf53516060b1e59eaa16d3dc5 Mon Sep 17 00:00:00 2001 From: Adam Weingarten <6517820+aweingarten@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:38:12 -0500 Subject: [PATCH 4/6] Add TF resources/datasources: firewall rulesets, prefix lists, rules expansion --- go.mod | 24 +-- go.sum | 38 ++-- .../firewallruleset/framework_datasource.go | 65 ++++++ linode/firewallruleset/framework_models.go | 103 +++++++++ linode/firewallruleset/framework_resource.go | 201 ++++++++++++++++++ .../framework_schema_datasource.go | 49 +++++ .../framework_schema_resource.go | 116 ++++++++++ .../firewallrulesets/framework_datasource.go | 85 ++++++++ linode/firewallrulesets/framework_models.go | 28 +++ linode/firewallrulesets/framework_schema.go | 68 ++++++ .../framework_datasource.go | 62 ++++++ .../framework_models.go | 46 ++++ .../framework_schema.go | 43 ++++ linode/framework_provider.go | 11 + linode/prefixlist/framework_datasource.go | 65 ++++++ linode/prefixlist/framework_models.go | 87 ++++++++ linode/prefixlist/framework_schema.go | 54 +++++ linode/prefixlists/framework_datasource.go | 85 ++++++++ linode/prefixlists/framework_models.go | 28 +++ linode/prefixlists/framework_schema.go | 73 +++++++ 20 files changed, 1299 insertions(+), 32 deletions(-) create mode 100644 linode/firewallruleset/framework_datasource.go create mode 100644 linode/firewallruleset/framework_models.go create mode 100644 linode/firewallruleset/framework_resource.go create mode 100644 linode/firewallruleset/framework_schema_datasource.go create mode 100644 linode/firewallruleset/framework_schema_resource.go create mode 100644 linode/firewallrulesets/framework_datasource.go create mode 100644 linode/firewallrulesets/framework_models.go create mode 100644 linode/firewallrulesets/framework_schema.go create mode 100644 linode/firewallrulesexpansion/framework_datasource.go create mode 100644 linode/firewallrulesexpansion/framework_models.go create mode 100644 linode/firewallrulesexpansion/framework_schema.go create mode 100644 linode/prefixlist/framework_datasource.go create mode 100644 linode/prefixlist/framework_models.go create mode 100644 linode/prefixlist/framework_schema.go create mode 100644 linode/prefixlists/framework_datasource.go create mode 100644 linode/prefixlists/framework_models.go create mode 100644 linode/prefixlists/framework_schema.go diff --git a/go.mod b/go.mod index 48023c619..aa2db8f56 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/linode/terraform-provider-linode/v3 -go 1.24.0 - -toolchain go1.24.1 +go 1.25.0 require ( github.com/aws/aws-sdk-go-v2 v1.41.3 @@ -11,7 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 github.com/aws/smithy-go v1.24.2 - github.com/go-resty/resty/v2 v2.17.1 + github.com/go-resty/resty/v2 v2.17.2 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.5.0 @@ -30,8 +28,8 @@ require ( github.com/linode/linodego v1.65.0 github.com/linode/linodego/k8s v1.25.2 github.com/stretchr/testify v1.11.1 - golang.org/x/crypto v0.47.0 - golang.org/x/net v0.49.0 + golang.org/x/crypto v0.48.0 + golang.org/x/net v0.51.0 golang.org/x/sync v0.19.0 ) @@ -102,13 +100,13 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.17.0 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.40.0 // indirect + golang.org/x/tools v0.41.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc v1.79.1 // indirect @@ -127,3 +125,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +replace github.com/linode/linodego => ../linodego diff --git a/go.sum b/go.sum index 509a6e011..a27037d73 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= +github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -199,8 +199,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/linode/linodego v1.65.0 h1:SdsuGD8VSsPWeShXpE7ihl5vec+fD3MgwhnfYC/rj7k= -github.com/linode/linodego v1.65.0/go.mod h1:tOFiTErdjkbVnV+4S0+NmIE9dqqZUEM2HsJaGu8wMh8= github.com/linode/linodego/k8s v1.25.2 h1:PY6S0sAD3xANVvM9WY38bz9GqMTjIbytC8IJJ9Cv23o= github.com/linode/linodego/k8s v1.25.2/go.mod h1:DC1XCSRZRGsmaa/ggpDPSDUmOM6aK1bhSIP6+f9Cwhc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -293,23 +291,23 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -330,18 +328,18 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -349,8 +347,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/linode/firewallruleset/framework_datasource.go b/linode/firewallruleset/framework_datasource.go new file mode 100644 index 000000000..4af05e486 --- /dev/null +++ b/linode/firewallruleset/framework_datasource.go @@ -0,0 +1,65 @@ +package firewallruleset + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +type DataSource struct { + helper.BaseDataSource +} + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_firewall_ruleset", + Schema: &frameworkDatasourceSchema, + }, + ), + } +} + +func (d *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data."+d.Config.Name) + + var data RuleSetDataSourceModel + client := d.Meta.Client + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id := helper.FrameworkSafeStringToInt(data.ID.ValueString(), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "client.GetFirewallRuleSet(...)", map[string]any{ + "id": id, + }) + ruleset, err := client.GetFirewallRuleSet(ctx, id) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to Get Firewall RuleSet %d", id), + err.Error(), + ) + return + } + + data.parseRuleSet(ctx, *ruleset, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/linode/firewallruleset/framework_models.go b/linode/firewallruleset/framework_models.go new file mode 100644 index 000000000..9fd757484 --- /dev/null +++ b/linode/firewallruleset/framework_models.go @@ -0,0 +1,103 @@ +package firewallruleset + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/firewall" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +// RuleSetBaseModel contains the shared fields used by both the +// resource model and the data-source model. +type RuleSetBaseModel struct { + Label types.String `tfsdk:"label"` + Description types.String `tfsdk:"description"` + Type types.String `tfsdk:"type"` + Rules []firewall.RuleModel `tfsdk:"rules"` + IsServiceDefined types.Bool `tfsdk:"is_service_defined"` + Version types.Int64 `tfsdk:"version"` + Created types.String `tfsdk:"created"` + Updated types.String `tfsdk:"updated"` +} + +// FlattenRuleSet maps a linodego.RuleSet into the base model fields. +func (data *RuleSetBaseModel) FlattenRuleSet( + ctx context.Context, + rs linodego.RuleSet, + diags *diag.Diagnostics, + preserveKnown bool, +) { + data.Label = helper.KeepOrUpdateString(data.Label, rs.Label, preserveKnown) + data.Description = helper.KeepOrUpdateString(data.Description, rs.Description, preserveKnown) + data.Type = helper.KeepOrUpdateString(data.Type, string(rs.Type), preserveKnown) + data.IsServiceDefined = helper.KeepOrUpdateBool(data.IsServiceDefined, rs.IsServiceDefined, preserveKnown) + data.Version = helper.KeepOrUpdateInt64(data.Version, int64(rs.Version), preserveKnown) + + if rs.Created != nil { + data.Created = helper.KeepOrUpdateString(data.Created, rs.Created.Format("2006-01-02T15:04:05"), preserveKnown) + } + if rs.Updated != nil { + data.Updated = helper.KeepOrUpdateString(data.Updated, rs.Updated.Format("2006-01-02T15:04:05"), preserveKnown) + } + + data.Rules = firewall.FlattenFirewallRules(ctx, rs.Rules, data.Rules, preserveKnown, diags) +} + +// RuleSetResourceModel is used by the resource (CRUD). +type RuleSetResourceModel struct { + ID types.String `tfsdk:"id"` + RuleSetBaseModel +} + +func (data *RuleSetResourceModel) GetCreateOptions( + ctx context.Context, diags *diag.Diagnostics, +) linodego.RuleSetCreateOptions { + rules := firewall.ExpandFirewallRules(ctx, data.Rules, diags) + + return linodego.RuleSetCreateOptions{ + Label: data.Label.ValueString(), + Description: data.Description.ValueString(), + Type: linodego.FirewallRuleSetType(data.Type.ValueString()), + Rules: rules, + } +} + +func (data *RuleSetResourceModel) GetUpdateOptions( + ctx context.Context, diags *diag.Diagnostics, +) linodego.RuleSetUpdateOptions { + opts := linodego.RuleSetUpdateOptions{} + + if !data.Label.IsNull() && !data.Label.IsUnknown() { + v := data.Label.ValueString() + opts.Label = &v + } + + if !data.Description.IsNull() && !data.Description.IsUnknown() { + v := data.Description.ValueString() + opts.Description = &v + } + + rules := firewall.ExpandFirewallRules(ctx, data.Rules, diags) + opts.Rules = &rules + + return opts +} + +// RuleSetDataSourceModel is used by the single-item data source. +type RuleSetDataSourceModel struct { + ID types.String `tfsdk:"id"` + RuleSetBaseModel +} + +func (data *RuleSetDataSourceModel) parseRuleSet( + ctx context.Context, + rs linodego.RuleSet, + diags *diag.Diagnostics, +) { + data.ID = helper.KeepOrUpdateString(data.ID, strconv.Itoa(rs.ID), false) + data.FlattenRuleSet(ctx, rs, diags, false) +} diff --git a/linode/firewallruleset/framework_resource.go b/linode/firewallruleset/framework_resource.go new file mode 100644 index 000000000..f3a7533a2 --- /dev/null +++ b/linode/firewallruleset/framework_resource.go @@ -0,0 +1,201 @@ +package firewallruleset + +import ( + "context" + "fmt" + "strconv" + + "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" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +func NewResource() resource.Resource { + return &Resource{ + BaseResource: helper.NewBaseResource( + helper.BaseResourceConfig{ + Name: "linode_firewall_ruleset", + IDType: types.StringType, + Schema: &frameworkResourceSchema, + }, + ), + } +} + +type Resource struct { + helper.BaseResource +} + +func (r *Resource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + tflog.Debug(ctx, "Create "+r.Config.Name) + + var plan RuleSetResourceModel + client := r.Meta.Client + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + createOpts := plan.GetCreateOptions(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "client.CreateFirewallRuleSet(...)") + ruleset, err := client.CreateFirewallRuleSet(ctx, createOpts) + if ruleset != nil && ruleset.ID != 0 { + resp.State.SetAttribute(ctx, path.Root("id"), types.StringValue(strconv.Itoa(ruleset.ID))) + } + + if err != nil { + resp.Diagnostics.AddError("Failed to Create Firewall RuleSet", err.Error()) + return + } + + plan.ID = types.StringValue(strconv.Itoa(ruleset.ID)) + plan.FlattenRuleSet(ctx, *ruleset, &resp.Diagnostics, true) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *Resource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + tflog.Debug(ctx, "Read "+r.Config.Name) + + client := r.Meta.Client + var state RuleSetResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if helper.FrameworkAttemptRemoveResourceForEmptyID(ctx, state.ID, resp) { + return + } + + id := helper.FrameworkSafeStringToInt(state.ID.ValueString(), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "client.GetFirewallRuleSet(...)", map[string]any{ + "id": id, + }) + ruleset, err := client.GetFirewallRuleSet(ctx, id) + if err != nil { + if linodego.IsNotFound(err) { + resp.Diagnostics.AddWarning( + fmt.Sprintf("Removing RuleSet %d from State", id), + "Removing the Firewall RuleSet from state because it no longer exists", + ) + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to Get Firewall RuleSet %d", id), + err.Error(), + ) + } + return + } + + state.FlattenRuleSet(ctx, *ruleset, &resp.Diagnostics, false) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *Resource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + tflog.Debug(ctx, "Update "+r.Config.Name) + + client := r.Meta.Client + var plan, state RuleSetResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + id := helper.FrameworkSafeStringToInt(state.ID.ValueString(), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + updateOpts := plan.GetUpdateOptions(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "client.UpdateFirewallRuleSet(...)", map[string]any{ + "id": id, + }) + ruleset, err := client.UpdateFirewallRuleSet(ctx, id, updateOpts) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to Update Firewall RuleSet %d", id), + err.Error(), + ) + return + } + + plan.ID = state.ID + plan.FlattenRuleSet(ctx, *ruleset, &resp.Diagnostics, true) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *Resource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + tflog.Debug(ctx, "Delete "+r.Config.Name) + + var state RuleSetResourceModel + client := r.Meta.Client + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + id := helper.FrameworkSafeStringToInt(state.ID.ValueString(), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "client.DeleteFirewallRuleSet(...)", map[string]any{ + "id": id, + }) + if err := client.DeleteFirewallRuleSet(ctx, id); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to Delete Firewall RuleSet %d", id), + err.Error(), + ) + return + } +} diff --git a/linode/firewallruleset/framework_schema_datasource.go b/linode/firewallruleset/framework_schema_datasource.go new file mode 100644 index 000000000..5fd67ac5e --- /dev/null +++ b/linode/firewallruleset/framework_schema_datasource.go @@ -0,0 +1,49 @@ +package firewallruleset + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/linode/terraform-provider-linode/v3/linode/firewall" +) + +var frameworkDatasourceSchema = schema.Schema{ + Description: "Provides details about a specific Linode Firewall Rule Set.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique ID of the Rule Set.", + Required: true, + }, + "label": schema.StringAttribute{ + Description: "The label for the Rule Set.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "A description for this Rule Set.", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Whether the rules in this set are inbound or outbound.", + Computed: true, + }, + "rules": schema.ListAttribute{ + ElementType: firewall.RuleObjectType, + Description: "The ordered list of firewall rules in this rule set.", + Computed: true, + }, + "is_service_defined": schema.BoolAttribute{ + Description: "Whether this Rule Set is read-only and defined by a Linode service.", + Computed: true, + }, + "version": schema.Int64Attribute{ + Description: "The version of this Rule Set.", + Computed: true, + }, + "created": schema.StringAttribute{ + Description: "When the Rule Set was created.", + Computed: true, + }, + "updated": schema.StringAttribute{ + Description: "When the Rule Set was last updated.", + Computed: true, + }, + }, +} diff --git a/linode/firewallruleset/framework_schema_resource.go b/linode/firewallruleset/framework_schema_resource.go new file mode 100644 index 000000000..8f1b7658e --- /dev/null +++ b/linode/firewallruleset/framework_schema_resource.go @@ -0,0 +1,116 @@ +package firewallruleset + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" +) + +var ruleNestedObject = schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "label": schema.StringAttribute{ + Description: "Used to identify this rule. For display purposes only.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(3, 32), + }, + }, + "action": schema.StringAttribute{ + Description: "Controls whether traffic is accepted or dropped by this rule.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("ACCEPT", "DROP"), + }, + }, + "protocol": schema.StringAttribute{ + Description: "The network protocol this rule controls.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + string(linodego.TCP), + string(linodego.UDP), + string(linodego.ICMP), + string(linodego.IPENCAP), + ), + }, + }, + "description": schema.StringAttribute{ + Description: "Used to describe this rule. For display purposes only.", + Optional: true, + }, + "ports": schema.StringAttribute{ + Description: "A string representation of ports and/or port ranges (e.g. \"443\" or \"80-90, 91\").", + Optional: true, + }, + "ipv4": schema.ListAttribute{ + Description: "A list of IPv4 addresses, CIDRs, or prefix list tokens this rule applies to.", + Optional: true, + ElementType: types.StringType, + }, + "ipv6": schema.ListAttribute{ + Description: "A list of IPv6 addresses, networks, or prefix list tokens this rule applies to.", + Optional: true, + ElementType: types.StringType, + }, + }, +} + +var frameworkResourceSchema = schema.Schema{ + Description: "Manages a Linode Firewall Rule Set.", + Blocks: map[string]schema.Block{ + "rules": schema.ListNestedBlock{ + Description: "An ordered list of firewall rules in this rule set.", + NestedObject: ruleNestedObject, + }, + }, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique ID of this Rule Set.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "label": schema.StringAttribute{ + Description: "The label for the Rule Set.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(3, 32), + }, + }, + "description": schema.StringAttribute{ + Description: "A description for this Rule Set.", + Optional: true, + }, + "type": schema.StringAttribute{ + Description: "Whether the rules in this set are inbound or outbound. One of: inbound, outbound.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("inbound", "outbound"), + }, + }, + "is_service_defined": schema.BoolAttribute{ + Description: "Whether this Rule Set is read-only and defined by a Linode service.", + Computed: true, + }, + "version": schema.Int64Attribute{ + Description: "The version of this Rule Set, incremented on each update.", + Computed: true, + }, + "created": schema.StringAttribute{ + Description: "When the Rule Set was created.", + Computed: true, + }, + "updated": schema.StringAttribute{ + Description: "When the Rule Set was last updated.", + Computed: true, + }, + }, +} diff --git a/linode/firewallrulesets/framework_datasource.go b/linode/firewallrulesets/framework_datasource.go new file mode 100644 index 000000000..58dd4abcb --- /dev/null +++ b/linode/firewallrulesets/framework_datasource.go @@ -0,0 +1,85 @@ +package firewallrulesets + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +type DataSource struct { + helper.BaseDataSource +} + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_firewall_rulesets", + Schema: &frameworkDatasourceSchema, + }, + ), + } +} + +func (d *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data."+d.Config.Name) + + var data RuleSetFilterModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id, diag := filterConfig.GenerateID(data.Filters) + if diag != nil { + resp.Diagnostics.Append(diag) + return + } + data.ID = id + + result, diag := filterConfig.GetAndFilter( + ctx, d.Meta.Client, data.Filters, listFirewallRuleSets, + types.StringNull(), types.StringNull()) + if diag != nil { + resp.Diagnostics.Append(diag) + return + } + + data.parseRuleSets( + ctx, + helper.AnySliceToTyped[linodego.RuleSet](result), + &resp.Diagnostics, + ) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func listFirewallRuleSets( + ctx context.Context, + client *linodego.Client, + filter string, +) ([]any, error) { + tflog.Trace(ctx, "client.ListFirewallRuleSets(...)", map[string]any{ + "filter": filter, + }) + rulesets, err := client.ListFirewallRuleSets(ctx, &linodego.ListOptions{ + Filter: filter, + }) + if err != nil { + return nil, err + } + + return helper.TypedSliceToAny(rulesets), nil +} diff --git a/linode/firewallrulesets/framework_models.go b/linode/firewallrulesets/framework_models.go new file mode 100644 index 000000000..ae7bcfb0a --- /dev/null +++ b/linode/firewallrulesets/framework_models.go @@ -0,0 +1,28 @@ +package firewallrulesets + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/firewallruleset" + "github.com/linode/terraform-provider-linode/v3/linode/helper/frameworkfilter" +) + +type RuleSetFilterModel struct { + ID types.String `tfsdk:"id"` + Filters frameworkfilter.FiltersModelType `tfsdk:"filter"` + RuleSets []firewallruleset.RuleSetBaseModel `tfsdk:"rulesets"` +} + +func (data *RuleSetFilterModel) parseRuleSets( + ctx context.Context, + rulesets []linodego.RuleSet, + diags *diag.Diagnostics, +) { + data.RuleSets = make([]firewallruleset.RuleSetBaseModel, len(rulesets)) + for i, rs := range rulesets { + data.RuleSets[i].FlattenRuleSet(ctx, rs, diags, false) + } +} diff --git a/linode/firewallrulesets/framework_schema.go b/linode/firewallrulesets/framework_schema.go new file mode 100644 index 000000000..e70d25e53 --- /dev/null +++ b/linode/firewallrulesets/framework_schema.go @@ -0,0 +1,68 @@ +package firewallrulesets + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/linode/terraform-provider-linode/v3/linode/firewall" + "github.com/linode/terraform-provider-linode/v3/linode/helper/frameworkfilter" +) + +var filterConfig = frameworkfilter.Config{ + "label": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, + "type": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, +} + +var rulesetAttributes = map[string]schema.Attribute{ + "label": schema.StringAttribute{ + Description: "The label for the Rule Set.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "A description for this Rule Set.", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Whether the rules in this set are inbound or outbound.", + Computed: true, + }, + "rules": schema.ListAttribute{ + ElementType: firewall.RuleObjectType, + Description: "The ordered list of firewall rules in this rule set.", + Computed: true, + }, + "is_service_defined": schema.BoolAttribute{ + Description: "Whether this Rule Set is read-only and defined by a Linode service.", + Computed: true, + }, + "version": schema.Int64Attribute{ + Description: "The version of this Rule Set.", + Computed: true, + }, + "created": schema.StringAttribute{ + Description: "When the Rule Set was created.", + Computed: true, + }, + "updated": schema.StringAttribute{ + Description: "When the Rule Set was last updated.", + Computed: true, + }, +} + +var frameworkDatasourceSchema = schema.Schema{ + Description: "Provides a list of Linode Firewall Rule Sets matching optional filters.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The data source's unique ID.", + Computed: true, + }, + "rulesets": schema.ListNestedAttribute{ + Description: "The returned list of firewall rule sets.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: rulesetAttributes, + }, + }, + }, + Blocks: map[string]schema.Block{ + "filter": filterConfig.Schema(), + }, +} diff --git a/linode/firewallrulesexpansion/framework_datasource.go b/linode/firewallrulesexpansion/framework_datasource.go new file mode 100644 index 000000000..1318e93b5 --- /dev/null +++ b/linode/firewallrulesexpansion/framework_datasource.go @@ -0,0 +1,62 @@ +package firewallrulesexpansion + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +type DataSource struct { + helper.BaseDataSource +} + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_firewall_rules_expansion", + Schema: &frameworkDatasourceSchema, + }, + ), + } +} + +func (d *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data."+d.Config.Name) + + var data FirewallRulesExpansionModel + client := d.Meta.Client + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + firewallID := int(data.FirewallID.ValueInt64()) + + tflog.Trace(ctx, "client.GetFirewallRulesExpansion(...)", map[string]any{ + "firewall_id": firewallID, + }) + ruleSet, err := client.GetFirewallRulesExpansion(ctx, firewallID) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to Get Expanded Firewall Rules for Firewall %d", firewallID), + err.Error(), + ) + return + } + + data.parseExpansion(ctx, firewallID, *ruleSet, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/linode/firewallrulesexpansion/framework_models.go b/linode/firewallrulesexpansion/framework_models.go new file mode 100644 index 000000000..8b3f8e075 --- /dev/null +++ b/linode/firewallrulesexpansion/framework_models.go @@ -0,0 +1,46 @@ +package firewallrulesexpansion + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/firewall" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +// FirewallRulesExpansionModel describes the data model for the expanded +// firewall rules data source. +type FirewallRulesExpansionModel struct { + ID types.String `tfsdk:"id"` + FirewallID types.Int64 `tfsdk:"firewall_id"` + Inbound []firewall.RuleModel `tfsdk:"inbound"` + InboundPolicy types.String `tfsdk:"inbound_policy"` + Outbound []firewall.RuleModel `tfsdk:"outbound"` + OutboundPolicy types.String `tfsdk:"outbound_policy"` + Version types.Int64 `tfsdk:"version"` +} + +func (data *FirewallRulesExpansionModel) parseExpansion( + ctx context.Context, + firewallID int, + ruleSet linodego.FirewallRuleSet, + diags *diag.Diagnostics, +) { + data.ID = helper.KeepOrUpdateString(data.ID, data.FirewallID.String(), false) + + data.Inbound = firewall.FlattenFirewallRules(ctx, ruleSet.Inbound, nil, false, diags) + if diags.HasError() { + return + } + + data.Outbound = firewall.FlattenFirewallRules(ctx, ruleSet.Outbound, nil, false, diags) + if diags.HasError() { + return + } + + data.InboundPolicy = helper.KeepOrUpdateString(data.InboundPolicy, ruleSet.InboundPolicy, false) + data.OutboundPolicy = helper.KeepOrUpdateString(data.OutboundPolicy, ruleSet.OutboundPolicy, false) + data.Version = helper.KeepOrUpdateInt64(data.Version, int64(ruleSet.Version), false) +} diff --git a/linode/firewallrulesexpansion/framework_schema.go b/linode/firewallrulesexpansion/framework_schema.go new file mode 100644 index 000000000..d11f3a935 --- /dev/null +++ b/linode/firewallrulesexpansion/framework_schema.go @@ -0,0 +1,43 @@ +package firewallrulesexpansion + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/linode/terraform-provider-linode/v3/linode/firewall" +) + +var frameworkDatasourceSchema = schema.Schema{ + Description: "Provides the expanded (fully resolved) firewall rules for a Linode Firewall. " + + "Rule Set references and prefix list tokens are expanded into concrete rules.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The data source's unique ID.", + Computed: true, + }, + "firewall_id": schema.Int64Attribute{ + Description: "The ID of the Firewall to get expanded rules for.", + Required: true, + }, + "inbound": schema.ListAttribute{ + ElementType: firewall.RuleObjectType, + Description: "The expanded inbound firewall rules.", + Computed: true, + }, + "inbound_policy": schema.StringAttribute{ + Description: "The default behavior for inbound traffic.", + Computed: true, + }, + "outbound": schema.ListAttribute{ + ElementType: firewall.RuleObjectType, + Description: "The expanded outbound firewall rules.", + Computed: true, + }, + "outbound_policy": schema.StringAttribute{ + Description: "The default behavior for outbound traffic.", + Computed: true, + }, + "version": schema.Int64Attribute{ + Description: "The version of the firewall rules.", + Computed: true, + }, + }, +} diff --git a/linode/framework_provider.go b/linode/framework_provider.go index ee6af3b28..20dd996d1 100644 --- a/linode/framework_provider.go +++ b/linode/framework_provider.go @@ -32,6 +32,9 @@ import ( "github.com/linode/terraform-provider-linode/v3/linode/domainzonefile" "github.com/linode/terraform-provider-linode/v3/linode/firewall" "github.com/linode/terraform-provider-linode/v3/linode/firewalldevice" + "github.com/linode/terraform-provider-linode/v3/linode/firewallrulesexpansion" + "github.com/linode/terraform-provider-linode/v3/linode/firewallruleset" + "github.com/linode/terraform-provider-linode/v3/linode/firewallrulesets" "github.com/linode/terraform-provider-linode/v3/linode/firewalls" "github.com/linode/terraform-provider-linode/v3/linode/firewallsettings" "github.com/linode/terraform-provider-linode/v3/linode/firewalltemplate" @@ -82,6 +85,8 @@ import ( "github.com/linode/terraform-provider-linode/v3/linode/placementgroup" "github.com/linode/terraform-provider-linode/v3/linode/placementgroupassignment" "github.com/linode/terraform-provider-linode/v3/linode/placementgroups" + "github.com/linode/terraform-provider-linode/v3/linode/prefixlist" + "github.com/linode/terraform-provider-linode/v3/linode/prefixlists" "github.com/linode/terraform-provider-linode/v3/linode/producerimagesharegroup" "github.com/linode/terraform-provider-linode/v3/linode/producerimagesharegroupimageshares" "github.com/linode/terraform-provider-linode/v3/linode/producerimagesharegroupmember" @@ -243,6 +248,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res accountsettings.NewResource, firewall.NewResource, firewalldevice.NewResource, + firewallruleset.NewResource, image.NewResource, instancedisk.NewResource, instanceip.NewResource, @@ -283,6 +289,9 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource account.NewDataSource, backup.NewDataSource, firewall.NewDataSource, + firewallrulesexpansion.NewDataSource, + firewallruleset.NewDataSource, + firewallrulesets.NewDataSource, kernel.NewDataSource, stackscript.NewDataSource, stackscripts.NewDataSource, @@ -367,5 +376,7 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource consumerimagesharegroup.NewDataSource, consumerimagesharegroupimageshares.NewDataSource, lkenodepool.NewDataSource, + prefixlist.NewDataSource, + prefixlists.NewDataSource, } } diff --git a/linode/prefixlist/framework_datasource.go b/linode/prefixlist/framework_datasource.go new file mode 100644 index 000000000..142d24802 --- /dev/null +++ b/linode/prefixlist/framework_datasource.go @@ -0,0 +1,65 @@ +package prefixlist + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +type DataSource struct { + helper.BaseDataSource +} + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_prefix_list", + Schema: &frameworkDatasourceSchema, + }, + ), + } +} + +func (d *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data."+d.Config.Name) + + var data PrefixListDataSourceModel + client := d.Meta.Client + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id := helper.FrameworkSafeStringToInt(data.ID.ValueString(), &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "client.GetPrefixList(...)", map[string]any{ + "id": id, + }) + prefixList, err := client.GetPrefixList(ctx, id) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to Get Prefix List %d", id), + err.Error(), + ) + return + } + + data.parsePrefixList(ctx, *prefixList, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/linode/prefixlist/framework_models.go b/linode/prefixlist/framework_models.go new file mode 100644 index 000000000..90c68d226 --- /dev/null +++ b/linode/prefixlist/framework_models.go @@ -0,0 +1,87 @@ +package prefixlist + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +// PrefixListBaseModel contains the shared fields for both single and list data sources. +type PrefixListBaseModel struct { + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Visibility types.String `tfsdk:"visibility"` + SourcePrefixListID types.Int64 `tfsdk:"source_prefixlist_id"` + IPv4 types.List `tfsdk:"ipv4"` + IPv6 types.List `tfsdk:"ipv6"` + Version types.Int64 `tfsdk:"version"` + Created types.String `tfsdk:"created"` + Updated types.String `tfsdk:"updated"` +} + +// FlattenPrefixList maps a linodego.PrefixList into the base model fields. +func (data *PrefixListBaseModel) FlattenPrefixList( + ctx context.Context, + pl linodego.PrefixList, + diags *diag.Diagnostics, + preserveKnown bool, +) { + data.Name = helper.KeepOrUpdateString(data.Name, pl.Name, preserveKnown) + data.Description = helper.KeepOrUpdateString(data.Description, pl.Description, preserveKnown) + data.Visibility = helper.KeepOrUpdateString(data.Visibility, pl.Visibility, preserveKnown) + data.Version = helper.KeepOrUpdateInt64(data.Version, int64(pl.Version), preserveKnown) + + if pl.SourcePrefixListID != nil { + data.SourcePrefixListID = helper.KeepOrUpdateInt64(data.SourcePrefixListID, int64(*pl.SourcePrefixListID), preserveKnown) + } else { + data.SourcePrefixListID = helper.KeepOrUpdateValue(data.SourcePrefixListID, types.Int64Null(), preserveKnown) + } + + if pl.Created != nil { + data.Created = helper.KeepOrUpdateString(data.Created, pl.Created.Format("2006-01-02T15:04:05"), preserveKnown) + } + if pl.Updated != nil { + data.Updated = helper.KeepOrUpdateString(data.Updated, pl.Updated.Format("2006-01-02T15:04:05"), preserveKnown) + } + + if pl.IPv4 != nil { + ipv4, newDiags := types.ListValueFrom(ctx, types.StringType, *pl.IPv4) + diags.Append(newDiags...) + if diags.HasError() { + return + } + data.IPv4 = helper.KeepOrUpdateValue(data.IPv4, ipv4, preserveKnown) + } else { + data.IPv4 = helper.KeepOrUpdateValue(data.IPv4, types.ListNull(types.StringType), preserveKnown) + } + + if pl.IPv6 != nil { + ipv6, newDiags := types.ListValueFrom(ctx, types.StringType, *pl.IPv6) + diags.Append(newDiags...) + if diags.HasError() { + return + } + data.IPv6 = helper.KeepOrUpdateValue(data.IPv6, ipv6, preserveKnown) + } else { + data.IPv6 = helper.KeepOrUpdateValue(data.IPv6, types.ListNull(types.StringType), preserveKnown) + } +} + +// PrefixListDataSourceModel is the data source read model for a single prefix list. +type PrefixListDataSourceModel struct { + ID types.String `tfsdk:"id"` + PrefixListBaseModel +} + +func (data *PrefixListDataSourceModel) parsePrefixList( + ctx context.Context, + pl linodego.PrefixList, + diags *diag.Diagnostics, +) { + data.ID = helper.KeepOrUpdateString(data.ID, strconv.Itoa(pl.ID), false) + data.FlattenPrefixList(ctx, pl, diags, false) +} diff --git a/linode/prefixlist/framework_schema.go b/linode/prefixlist/framework_schema.go new file mode 100644 index 000000000..c00f63db7 --- /dev/null +++ b/linode/prefixlist/framework_schema.go @@ -0,0 +1,54 @@ +package prefixlist + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var frameworkDatasourceSchema = schema.Schema{ + Description: "Provides details about a specific Linode Prefix List.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique ID of the Prefix List.", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "The name of the Prefix List (e.g. pl:system:object-storage:us-iad).", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "A description for this Prefix List.", + Computed: true, + }, + "visibility": schema.StringAttribute{ + Description: "The visibility of the Prefix List (e.g. system, user).", + Computed: true, + }, + "source_prefixlist_id": schema.Int64Attribute{ + Description: "If this Prefix List was cloned, the ID of the source Prefix List.", + Computed: true, + }, + "ipv4": schema.ListAttribute{ + Description: "The IPv4 prefixes in this Prefix List.", + ElementType: types.StringType, + Computed: true, + }, + "ipv6": schema.ListAttribute{ + Description: "The IPv6 prefixes in this Prefix List.", + ElementType: types.StringType, + Computed: true, + }, + "version": schema.Int64Attribute{ + Description: "The version of this Prefix List.", + Computed: true, + }, + "created": schema.StringAttribute{ + Description: "When the Prefix List was created.", + Computed: true, + }, + "updated": schema.StringAttribute{ + Description: "When the Prefix List was last updated.", + Computed: true, + }, + }, +} diff --git a/linode/prefixlists/framework_datasource.go b/linode/prefixlists/framework_datasource.go new file mode 100644 index 000000000..6f7c48b76 --- /dev/null +++ b/linode/prefixlists/framework_datasource.go @@ -0,0 +1,85 @@ +package prefixlists + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +type DataSource struct { + helper.BaseDataSource +} + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_prefix_lists", + Schema: &frameworkDatasourceSchema, + }, + ), + } +} + +func (d *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data."+d.Config.Name) + + var data PrefixListFilterModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id, diag := filterConfig.GenerateID(data.Filters) + if diag != nil { + resp.Diagnostics.Append(diag) + return + } + data.ID = id + + result, diag := filterConfig.GetAndFilter( + ctx, d.Meta.Client, data.Filters, listPrefixLists, + types.StringNull(), types.StringNull()) + if diag != nil { + resp.Diagnostics.Append(diag) + return + } + + data.parsePrefixLists( + ctx, + helper.AnySliceToTyped[linodego.PrefixList](result), + &resp.Diagnostics, + ) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func listPrefixLists( + ctx context.Context, + client *linodego.Client, + filter string, +) ([]any, error) { + tflog.Trace(ctx, "client.ListPrefixLists(...)", map[string]any{ + "filter": filter, + }) + lists, err := client.ListPrefixLists(ctx, &linodego.ListOptions{ + Filter: filter, + }) + if err != nil { + return nil, err + } + + return helper.TypedSliceToAny(lists), nil +} diff --git a/linode/prefixlists/framework_models.go b/linode/prefixlists/framework_models.go new file mode 100644 index 000000000..77830fac1 --- /dev/null +++ b/linode/prefixlists/framework_models.go @@ -0,0 +1,28 @@ +package prefixlists + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/helper/frameworkfilter" + "github.com/linode/terraform-provider-linode/v3/linode/prefixlist" +) + +type PrefixListFilterModel struct { + ID types.String `tfsdk:"id"` + Filters frameworkfilter.FiltersModelType `tfsdk:"filter"` + PrefixLists []prefixlist.PrefixListBaseModel `tfsdk:"prefix_lists"` +} + +func (data *PrefixListFilterModel) parsePrefixLists( + ctx context.Context, + lists []linodego.PrefixList, + diags *diag.Diagnostics, +) { + data.PrefixLists = make([]prefixlist.PrefixListBaseModel, len(lists)) + for i, pl := range lists { + data.PrefixLists[i].FlattenPrefixList(ctx, pl, diags, false) + } +} diff --git a/linode/prefixlists/framework_schema.go b/linode/prefixlists/framework_schema.go new file mode 100644 index 000000000..a8afac007 --- /dev/null +++ b/linode/prefixlists/framework_schema.go @@ -0,0 +1,73 @@ +package prefixlists + +import ( + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/terraform-provider-linode/v3/linode/helper/frameworkfilter" +) + +var filterConfig = frameworkfilter.Config{ + "name": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, + "visibility": {APIFilterable: true, TypeFunc: frameworkfilter.FilterTypeString}, +} + +var prefixListAttributes = map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the Prefix List.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "A description for this Prefix List.", + Computed: true, + }, + "visibility": schema.StringAttribute{ + Description: "The visibility of the Prefix List (e.g. system, user).", + Computed: true, + }, + "source_prefixlist_id": schema.Int64Attribute{ + Description: "If this Prefix List was cloned, the ID of the source Prefix List.", + Computed: true, + }, + "ipv4": schema.ListAttribute{ + Description: "The IPv4 prefixes in this Prefix List.", + ElementType: types.StringType, + Computed: true, + }, + "ipv6": schema.ListAttribute{ + Description: "The IPv6 prefixes in this Prefix List.", + ElementType: types.StringType, + Computed: true, + }, + "version": schema.Int64Attribute{ + Description: "The version of this Prefix List.", + Computed: true, + }, + "created": schema.StringAttribute{ + Description: "When the Prefix List was created.", + Computed: true, + }, + "updated": schema.StringAttribute{ + Description: "When the Prefix List was last updated.", + Computed: true, + }, +} + +var frameworkDatasourceSchema = schema.Schema{ + Description: "Provides a list of Linode Prefix Lists matching optional filters.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The data source's unique ID.", + Computed: true, + }, + "prefix_lists": schema.ListNestedAttribute{ + Description: "The returned list of prefix lists.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: prefixListAttributes, + }, + }, + }, + Blocks: map[string]schema.Block{ + "filter": filterConfig.Schema(), + }, +} From 3a48f0f5582e5e291501014fad31cde73650a0f1 Mon Sep 17 00:00:00 2001 From: Adam Weingarten <6517820+aweingarten@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:38:42 -0500 Subject: [PATCH 5/6] Add documentation for new rulesets, prefix lists, and rules expansion resources --- docs/data-sources/firewall.md | 2 + docs/data-sources/firewall_rules_expansion.md | 72 ++++++++++++ docs/data-sources/firewall_ruleset.md | 62 ++++++++++ docs/data-sources/firewall_rulesets.md | 95 ++++++++++++++++ docs/data-sources/firewalls.md | 2 + docs/data-sources/prefix_list.md | 46 ++++++++ docs/data-sources/prefix_lists.md | 79 +++++++++++++ docs/resources/firewall.md | 2 + docs/resources/firewall_ruleset.md | 107 ++++++++++++++++++ 9 files changed, 467 insertions(+) create mode 100644 docs/data-sources/firewall_rules_expansion.md create mode 100644 docs/data-sources/firewall_ruleset.md create mode 100644 docs/data-sources/firewall_rulesets.md create mode 100644 docs/data-sources/prefix_list.md create mode 100644 docs/data-sources/prefix_lists.md create mode 100644 docs/resources/firewall_ruleset.md diff --git a/docs/data-sources/firewall.md b/docs/data-sources/firewall.md index 04dec2b55..81ccb416e 100644 --- a/docs/data-sources/firewall.md +++ b/docs/data-sources/firewall.md @@ -53,6 +53,8 @@ In addition to all arguments above, the following attributes are exported: * `status` - The status of the firewall. (`enabled`, `disabled`, `deleted`) +* `version` - The version number of the Firewall's rule configuration. This is incremented each time the Firewall's rules are changed. + * `created` - When this firewall was created. * `updated` - When this firewall was last updated. diff --git a/docs/data-sources/firewall_rules_expansion.md b/docs/data-sources/firewall_rules_expansion.md new file mode 100644 index 000000000..bffaa9404 --- /dev/null +++ b/docs/data-sources/firewall_rules_expansion.md @@ -0,0 +1,72 @@ +--- +page_title: "Linode: linode_firewall_rules_expansion" +description: |- + Provides the expanded (resolved) firewall rules for a Firewall. +--- + +# Data Source: linode\_firewall\_rules\_expansion + +Provides the expanded (resolved) firewall rules for a Linode Firewall. This data source resolves all prefix list tokens and rule set references into their concrete IP addresses and individual rules, giving you the effective rule set that the firewall is currently enforcing. + +For more information, see the [Linode APIv4 docs](https://techdocs.akamai.com/linode-api/reference/get-firewall-rules-expansion). + +## Example Usage + +```terraform +resource "linode_firewall" "my_firewall" { + label = "my-firewall" + + inbound_ruleset = [linode_firewall_ruleset.allow_web.id] + + inbound_policy = "DROP" + outbound_policy = "ACCEPT" + + linodes = [linode_instance.my_instance.id] +} + +data "linode_firewall_rules_expansion" "expanded" { + firewall_id = linode_firewall.my_firewall.id +} + +output "effective_inbound_rules" { + value = data.linode_firewall_rules_expansion.expanded.inbound +} +``` + +## Argument Reference + +The following arguments are supported: + +* `firewall_id` - (Required) The ID of the Firewall to get the expanded rules for. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* [`inbound`](#rules) - The expanded inbound firewall rules with all prefix list tokens and rule set references resolved. + +* `inbound_policy` - The default behavior for inbound traffic. (`ACCEPT`, `DROP`) + +* [`outbound`](#rules) - The expanded outbound firewall rules with all prefix list tokens and rule set references resolved. + +* `outbound_policy` - The default behavior for outbound traffic. (`ACCEPT`, `DROP`) + +* `version` - The version number of the Firewall's rule configuration. + +### rules + +Each expanded rule exports the following attributes: + +* `label` - The label for this rule. + +* `action` - Controls whether traffic is accepted or dropped by this rule. (`ACCEPT`, `DROP`) + +* `protocol` - The network protocol this rule controls. (`TCP`, `UDP`, `ICMP`, `IPENCAP`) + +* `description` - The description for this rule. + +* `ports` - A string representation of ports and/or port ranges (i.e. "443" or "80-90, 91"). + +* `ipv4` - A list of resolved IPv4 addresses or networks in CIDR format. + +* `ipv6` - A list of resolved IPv6 addresses or networks in CIDR format. diff --git a/docs/data-sources/firewall_ruleset.md b/docs/data-sources/firewall_ruleset.md new file mode 100644 index 000000000..aab2cd15b --- /dev/null +++ b/docs/data-sources/firewall_ruleset.md @@ -0,0 +1,62 @@ +--- +page_title: "Linode: linode_firewall_ruleset" +description: |- + Provides details about a Firewall Rule Set. +--- + +# Data Source: linode\_firewall\_ruleset + +Provides details about a Linode Firewall Rule Set. +For more information, see the [Linode APIv4 docs](https://techdocs.akamai.com/linode-api/reference/get-firewall-rule-set). + +## Example Usage + +```terraform +data "linode_firewall_ruleset" "example" { + id = "12345" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - (Required) The ID of the Firewall Rule Set. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `label` - The label for the Rule Set. + +* `description` - The description of the Rule Set. + +* `type` - The type of rule set (`inbound` or `outbound`). + +* [`rules`](#rules) - The firewall rules defined in this set. + +* `is_service_defined` - Whether this Rule Set is service-defined (managed by Linode). + +* `version` - The version number of this Rule Set. + +* `created` - When this Rule Set was created. + +* `updated` - When this Rule Set was last updated. + +### rules + +Each rule exports the following attributes: + +* `label` - The label for this rule. + +* `action` - Controls whether traffic is accepted or dropped by this rule. (`ACCEPT`, `DROP`) + +* `protocol` - The network protocol this rule controls. (`TCP`, `UDP`, `ICMP`, `IPENCAP`) + +* `description` - The description for this rule. + +* `ports` - A string representation of ports and/or port ranges (i.e. "443" or "80-90, 91"). + +* `ipv4` - A list of IPv4 addresses or networks in CIDR format, or prefix list tokens. + +* `ipv6` - A list of IPv6 addresses or networks in CIDR format, or prefix list tokens. diff --git a/docs/data-sources/firewall_rulesets.md b/docs/data-sources/firewall_rulesets.md new file mode 100644 index 000000000..dce31e3c8 --- /dev/null +++ b/docs/data-sources/firewall_rulesets.md @@ -0,0 +1,95 @@ +--- +page_title: "Linode: linode_firewall_rulesets" +description: |- + Provides information about Firewall Rule Sets that match a set of filters. +--- + +# Data Source: linode\_firewall\_rulesets + +Provides information about Linode Firewall Rule Sets that match a set of filters. +For more information, see the [Linode APIv4 docs](https://techdocs.akamai.com/linode-api/reference/get-firewall-rule-sets). + +## Example Usage + +Get information about all inbound rule sets: + +```terraform +data "linode_firewall_rulesets" "inbound" { + filter { + name = "type" + values = ["inbound"] + } +} + +output "ruleset_labels" { + value = data.linode_firewall_rulesets.inbound.rulesets.*.label +} +``` + +Get all rule sets: + +```terraform +data "linode_firewall_rulesets" "all" {} + +output "ruleset_ids" { + value = data.linode_firewall_rulesets.all.rulesets.*.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* [`filter`](#filter) - (Optional) A set of filters used to select Firewall Rule Sets that meet certain requirements. + +### Filter + +* `name` - (Required) The name of the field to filter by. See the [Filterable Fields section](#filterable-fields) for a complete list of filterable fields. + +* `values` - (Required) A list of values for the filter to allow. These values should all be in string form. + +* `match_by` - (Optional) The method to match the field by. (`exact`, `regex`, `substring`; default `exact`) + +## Attributes Reference + +Each Firewall Rule Set will be stored in the `rulesets` attribute and will export the following attributes: + +* `label` - The label for the Rule Set. + +* `description` - The description of the Rule Set. + +* `type` - The type of rule set (`inbound` or `outbound`). + +* [`rules`](#rules) - The firewall rules defined in this set. + +* `is_service_defined` - Whether this Rule Set is service-defined (managed by Linode). + +* `version` - The version number of the Rule Set. + +* `created` - When this Rule Set was created. + +* `updated` - When this Rule Set was last updated. + +### rules + +Each rule exports the following attributes: + +* `label` - The label for this rule. + +* `action` - Controls whether traffic is accepted or dropped by this rule. (`ACCEPT`, `DROP`) + +* `protocol` - The network protocol this rule controls. (`TCP`, `UDP`, `ICMP`, `IPENCAP`) + +* `description` - The description for this rule. + +* `ports` - A string representation of ports and/or port ranges (i.e. "443" or "80-90, 91"). + +* `ipv4` - A list of IPv4 addresses or networks in CIDR format, or prefix list tokens. + +* `ipv6` - A list of IPv6 addresses or networks in CIDR format, or prefix list tokens. + +## Filterable Fields + +* `label` + +* `type` diff --git a/docs/data-sources/firewalls.md b/docs/data-sources/firewalls.md index b32a5a0b7..af2441fe9 100644 --- a/docs/data-sources/firewalls.md +++ b/docs/data-sources/firewalls.md @@ -89,6 +89,8 @@ Each Linode firewall will be stored in the `firewalls` attribute and will export * `status` - The status of the firewall. +* `version` - The version number of the Firewall's rule configuration. + * `created` - When this firewall was created. * `updated` - When this firewall was last updated. diff --git a/docs/data-sources/prefix_list.md b/docs/data-sources/prefix_list.md new file mode 100644 index 000000000..04e6b86e0 --- /dev/null +++ b/docs/data-sources/prefix_list.md @@ -0,0 +1,46 @@ +--- +page_title: "Linode: linode_prefix_list" +description: |- + Provides details about a Prefix List. +--- + +# Data Source: linode\_prefix\_list + +Provides details about a Linode Prefix List. +For more information, see the [Linode APIv4 docs](https://techdocs.akamai.com/linode-api/reference/get-prefix-list). + +## Example Usage + +```terraform +data "linode_prefix_list" "example" { + id = "12345" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - (Required) The ID of the Prefix List. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `name` - The name of the Prefix List (e.g. `pl:system:object-storage:us-iad`, `pl::customer:my-list`). + +* `description` - A description of the Prefix List. + +* `visibility` - The visibility of the Prefix List. (`account`, `restricted`) + +* `source_prefixlist_id` - The ID of the source prefix list, if this is a derived list. + +* `ipv4` - A list of IPv4 addresses or networks in CIDR format contained in this prefix list. + +* `ipv6` - A list of IPv6 addresses or networks in CIDR format contained in this prefix list. + +* `version` - The version number of this Prefix List. + +* `created` - When this Prefix List was created. + +* `updated` - When this Prefix List was last updated. diff --git a/docs/data-sources/prefix_lists.md b/docs/data-sources/prefix_lists.md new file mode 100644 index 000000000..3003085fc --- /dev/null +++ b/docs/data-sources/prefix_lists.md @@ -0,0 +1,79 @@ +--- +page_title: "Linode: linode_prefix_lists" +description: |- + Provides information about Prefix Lists that match a set of filters. +--- + +# Data Source: linode\_prefix\_lists + +Provides information about Linode Prefix Lists that match a set of filters. +For more information, see the [Linode APIv4 docs](https://techdocs.akamai.com/linode-api/reference/get-prefix-lists). + +## Example Usage + +Get information about all account-visible prefix lists: + +```terraform +data "linode_prefix_lists" "account" { + filter { + name = "visibility" + values = ["account"] + } +} + +output "prefix_list_names" { + value = data.linode_prefix_lists.account.prefix_lists.*.name +} +``` + +Get all prefix lists: + +```terraform +data "linode_prefix_lists" "all" {} + +output "prefix_list_ids" { + value = data.linode_prefix_lists.all.prefix_lists.*.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* [`filter`](#filter) - (Optional) A set of filters used to select Prefix Lists that meet certain requirements. + +### Filter + +* `name` - (Required) The name of the field to filter by. See the [Filterable Fields section](#filterable-fields) for a complete list of filterable fields. + +* `values` - (Required) A list of values for the filter to allow. These values should all be in string form. + +* `match_by` - (Optional) The method to match the field by. (`exact`, `regex`, `substring`; default `exact`) + +## Attributes Reference + +Each Prefix List will be stored in the `prefix_lists` attribute and will export the following attributes: + +* `name` - The name of the Prefix List. + +* `description` - A description of the Prefix List. + +* `visibility` - The visibility of the Prefix List. (`account`, `restricted`) + +* `source_prefixlist_id` - The ID of the source prefix list, if this is a derived list. + +* `ipv4` - A list of IPv4 addresses or networks in CIDR format contained in this prefix list. + +* `ipv6` - A list of IPv6 addresses or networks in CIDR format contained in this prefix list. + +* `version` - The version number of this Prefix List. + +* `created` - When this Prefix List was created. + +* `updated` - When this Prefix List was last updated. + +## Filterable Fields + +* `name` + +* `visibility` diff --git a/docs/resources/firewall.md b/docs/resources/firewall.md index b795f0808..5b56d8f46 100644 --- a/docs/resources/firewall.md +++ b/docs/resources/firewall.md @@ -124,6 +124,8 @@ In addition to all arguments above, the following attributes are exported: * `status` - The status of the Firewall. +* `version` - The version number of the Firewall's rule configuration. This is incremented each time the Firewall's rules are changed. + * [`devices`](#devices) - The devices governed by the Firewall. ### devices diff --git a/docs/resources/firewall_ruleset.md b/docs/resources/firewall_ruleset.md new file mode 100644 index 000000000..7df898337 --- /dev/null +++ b/docs/resources/firewall_ruleset.md @@ -0,0 +1,107 @@ +--- +page_title: "Linode: linode_firewall_ruleset" +description: |- + Manages a Linode Firewall Rule Set. +--- + +# linode\_firewall\_ruleset + +Manages a Linode Firewall Rule Set. +For more information, see the [Linode APIv4 docs](https://techdocs.akamai.com/linode-api/reference/post-firewall-rule-set). + +## Example Usage + +Create a reusable inbound rule set that allows SSH and HTTP traffic: + +```terraform +resource "linode_firewall_ruleset" "allow_web_ssh" { + label = "allow-web-ssh" + description = "Allow inbound SSH and HTTP traffic" + type = "inbound" + + rules { + label = "allow-ssh" + action = "ACCEPT" + protocol = "TCP" + ports = "22" + ipv4 = ["0.0.0.0/0"] + ipv6 = ["::/0"] + } + + rules { + label = "allow-http" + action = "ACCEPT" + protocol = "TCP" + ports = "80, 443" + ipv4 = ["0.0.0.0/0"] + ipv6 = ["::/0"] + } +} +``` + +Reference a rule set in a firewall: + +```terraform +resource "linode_firewall" "my_firewall" { + label = "my-firewall" + + inbound_ruleset = [linode_firewall_ruleset.allow_web_ssh.id] + + inbound_policy = "DROP" + outbound_policy = "ACCEPT" + + linodes = [linode_instance.my_instance.id] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `label` - (Required) The label for this Rule Set. Must be between 3 and 32 characters. + +* `type` - (Required) The type of rule set. Must be `inbound` or `outbound`. Changing this forces a new resource to be created. + +* `description` - (Optional) A description for this Rule Set. + +* [`rules`](#rules) - (Optional) One or more rule blocks defining the firewall rules in this set. + +### rules + +The following arguments are supported in the `rules` block: + +* `label` - (Required) The label for this rule. Must be between 3 and 32 characters. For display purposes only. + +* `action` - (Required) Controls whether traffic is accepted or dropped by this rule. (`ACCEPT`, `DROP`) + +* `protocol` - (Required) The network protocol this rule controls. (`TCP`, `UDP`, `ICMP`, `IPENCAP`) + +* `description` - (Optional) A description for this rule. + +* `ports` - (Optional) A string representation of ports and/or port ranges (i.e. "443" or "80-90, 91"). + +* `ipv4` - (Optional) A list of IPv4 addresses or networks in CIDR format, or prefix list tokens (e.g. `pl::subnets:123`). + +* `ipv6` - (Optional) A list of IPv6 addresses or networks in CIDR format, or prefix list tokens (e.g. `pl::subnets:123`). + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the Firewall Rule Set. + +* `is_service_defined` - Whether this Rule Set is service-defined (managed by Linode). + +* `version` - The version number of this Rule Set. This is incremented each time the rules are updated. + +* `created` - When this Rule Set was created. + +* `updated` - When this Rule Set was last updated. + +## Import + +Firewall Rule Sets can be imported using the `id`, e.g. + +```sh +terraform import linode_firewall_ruleset.allow_web_ssh 12345 +``` From 5df74ccf9c6b19689eab7769703754cd813d3cd8 Mon Sep 17 00:00:00 2001 From: Adam Weingarten <6517820+aweingarten@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:51:21 -0400 Subject: [PATCH 6/6] Added support for LKE-E cluster metrics. --- linode/lke/cluster.go | 6 ++++++ linode/lke/framework_datasource_schema.go | 4 ++++ linode/lke/framework_models.go | 2 ++ linode/lke/schema_resource.go | 6 ++++++ linode/lkeclusters/framework_datasource_schema.go | 4 ++++ linode/lkeclusters/framework_models.go | 2 ++ linode/lkenodepool/framework_resource_schema.go | 2 +- 7 files changed, 25 insertions(+), 1 deletion(-) diff --git a/linode/lke/cluster.go b/linode/lke/cluster.go index 8ea2df7ac..8f7f085ba 100644 --- a/linode/lke/cluster.go +++ b/linode/lke/cluster.go @@ -649,6 +649,7 @@ func flattenLKEClusterControlPlane(controlPlane linodego.LKEClusterControlPlane, flattened["high_availability"] = controlPlane.HighAvailability flattened["audit_logs_enabled"] = controlPlane.AuditLogsEnabled + flattened["metrics_enabled"] = controlPlane.MetricsEnabled return flattened } @@ -667,6 +668,11 @@ func expandControlPlaneOptions(controlPlane map[string]any) ( result.AuditLogsEnabled = &v } + if value, ok := controlPlane["metrics_enabled"]; ok { + v := value.(bool) + result.MetricsEnabled = &v + } + // default to disabled enabled := false result.ACL = &linodego.LKEClusterControlPlaneACLOptions{Enabled: &enabled} diff --git a/linode/lke/framework_datasource_schema.go b/linode/lke/framework_datasource_schema.go index 56817ee62..87490441d 100644 --- a/linode/lke/framework_datasource_schema.go +++ b/linode/lke/framework_datasource_schema.go @@ -98,6 +98,10 @@ var frameworkDataSourceSchema = schema.Schema{ Description: "Enables audit logs on the cluster's control plane.", Computed: true, }, + "metrics_enabled": schema.BoolAttribute{ + Description: "Enables metrics on the cluster's control plane.", + Computed: true, + }, "acl": schema.ListNestedAttribute{ Computed: true, Description: "The ACL configuration for an LKE cluster's control plane.", diff --git a/linode/lke/framework_models.go b/linode/lke/framework_models.go index 0ad3231b8..758757d4e 100644 --- a/linode/lke/framework_models.go +++ b/linode/lke/framework_models.go @@ -46,6 +46,7 @@ type LKEDataModel struct { type LKEControlPlane struct { HighAvailability types.Bool `tfsdk:"high_availability"` AuditLogsEnabled types.Bool `tfsdk:"audit_logs_enabled"` + MetricsEnabled types.Bool `tfsdk:"metrics_enabled"` ACL []LKEControlPlaneACL `tfsdk:"acl"` } @@ -270,6 +271,7 @@ func parseControlPlane( cp.HighAvailability = types.BoolValue(controlPlane.HighAvailability) cp.AuditLogsEnabled = types.BoolValue(controlPlane.AuditLogsEnabled) + cp.MetricsEnabled = types.BoolValue(controlPlane.MetricsEnabled) return cp, nil } diff --git a/linode/lke/schema_resource.go b/linode/lke/schema_resource.go index 662ff88b6..4325cf515 100644 --- a/linode/lke/schema_resource.go +++ b/linode/lke/schema_resource.go @@ -350,6 +350,12 @@ var resourceSchema = map[string]*schema.Schema{ Optional: true, Computed: true, }, + "metrics_enabled": { + Type: schema.TypeBool, + Description: "Enables metrics on the cluster's control plane.", + Optional: true, + Computed: true, + }, "acl": { Type: schema.TypeList, Description: "Defines the ACL configuration for an LKE cluster's control plane.", diff --git a/linode/lkeclusters/framework_datasource_schema.go b/linode/lkeclusters/framework_datasource_schema.go index 0252904ed..1694908c3 100644 --- a/linode/lkeclusters/framework_datasource_schema.go +++ b/linode/lkeclusters/framework_datasource_schema.go @@ -99,6 +99,10 @@ var frameworkDatasourceSchema = schema.Schema{ Description: "Enables audit logs on the cluster's control plane.", Computed: true, }, + "metrics_enabled": schema.BoolAttribute{ + Description: "Enables metrics on the cluster's control plane.", + Computed: true, + }, }, }, }, diff --git a/linode/lkeclusters/framework_models.go b/linode/lkeclusters/framework_models.go index cba89c778..1ad8dbc27 100644 --- a/linode/lkeclusters/framework_models.go +++ b/linode/lkeclusters/framework_models.go @@ -40,6 +40,7 @@ type LKEClusterModel struct { type LKEControlPlaneModel struct { HighAvailability types.Bool `tfsdk:"high_availability"` AuditLogsEnabled types.Bool `tfsdk:"audit_logs_enabled"` + MetricsEnabled types.Bool `tfsdk:"metrics_enabled"` } func (data *LKEClusterFilterModel) parseLKEClusters( @@ -86,6 +87,7 @@ func (data *LKEClusterModel) parseLKECluster( var cp LKEControlPlaneModel cp.HighAvailability = types.BoolValue(cluster.ControlPlane.HighAvailability) cp.AuditLogsEnabled = types.BoolValue(cluster.ControlPlane.AuditLogsEnabled) + cp.MetricsEnabled = types.BoolValue(cluster.ControlPlane.MetricsEnabled) return cp } diff --git a/linode/lkenodepool/framework_resource_schema.go b/linode/lkenodepool/framework_resource_schema.go index 267886335..4c1f888ba 100644 --- a/linode/lkenodepool/framework_resource_schema.go +++ b/linode/lkenodepool/framework_resource_schema.go @@ -74,7 +74,7 @@ var resourceSchema = schema.Schema{ Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ -\ stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.UseStateForUnknown(), }, Validators: []validator.String{ stringvalidator.OneOf(