diff --git a/firewalls.go b/firewalls.go index 1c698e1cc..fc63e9334 100644 --- a/firewalls.go +++ b/firewalls.go @@ -26,6 +26,7 @@ type Firewall struct { Tags []string `json:"tags"` Rules FirewallRuleSet `json:"rules"` Entities []FirewallDeviceEntity `json:"entities"` + Version int `json:"version"` Created *time.Time `json:"-"` Updated *time.Time `json:"-"` } diff --git a/lke_clusters.go b/lke_clusters.go index 786b6faaf..f46c00807 100644 --- a/lke_clusters.go +++ b/lke_clusters.go @@ -47,6 +47,16 @@ type LKECluster struct { SubnetID int `json:"subnet_id"` VpcID int `json:"vpc_id"` StackType LKEClusterStackType `json:"stack_type"` + + // RuleSetIDs contains the IDs of the service-managed firewall rulesets + // automatically created for LKE Enterprise clusters. + RuleSetIDs *LKEClusterRuleSetIDs `json:"ruleset_ids,omitempty"` +} + +// LKEClusterRuleSetIDs contains the inbound and outbound ruleset IDs for an LKE-E cluster. +type LKEClusterRuleSetIDs struct { + Inbound int `json:"inbound"` + Outbound int `json:"outbound"` } // LKEClusterCreateOptions fields are those accepted by CreateLKECluster diff --git a/lke_node_pools.go b/lke_node_pools.go index b50507d6d..a1d9bc147 100644 --- a/lke_node_pools.go +++ b/lke_node_pools.go @@ -59,6 +59,12 @@ type LKENodePoolTaint struct { Effect LKENodePoolTaintEffect `json:"effect"` } +// LKENodePoolIsolation controls network isolation for nodes in the pool. +type LKENodePoolIsolation struct { + PublicIPv4 bool `json:"public_ipv4"` + PublicIPv6 bool `json:"public_ipv6"` +} + // LKENodePoolLabels represents Kubernetes labels to add to an LKENodePool type LKENodePoolLabels map[string]string @@ -79,6 +85,8 @@ type LKENodePool struct { DiskEncryption InstanceDiskEncryption `json:"disk_encryption,omitempty"` + Isolation *LKENodePoolIsolation `json:"isolation,omitempty"` + // K8sVersion and UpdateStrategy are only for LKE Enterprise to support node pool upgrades. // It may not currently be available to all users and is under v4beta. K8sVersion *string `json:"k8s_version,omitempty"` @@ -98,6 +106,10 @@ type LKENodePoolCreateOptions struct { Autoscaler *LKENodePoolAutoscaler `json:"autoscaler,omitempty"` FirewallID *int `json:"firewall_id,omitempty"` + // NOTE: Disk encryption may not currently be available to all users. + DiskEncryption InstanceDiskEncryption `json:"disk_encryption,omitempty"` + Isolation *LKENodePoolIsolation `json:"isolation,omitempty"` + // K8sVersion and UpdateStrategy only works for LKE Enterprise to support node pool upgrades. // It may not currently be available to all users and is under v4beta. K8sVersion *string `json:"k8s_version,omitempty"` @@ -121,6 +133,8 @@ type LKENodePoolUpdateOptions struct { // It may not currently be available to all users and is under v4beta. K8sVersion *string `json:"k8s_version,omitempty"` UpdateStrategy *LKENodePoolUpdateStrategy `json:"update_strategy,omitempty"` + + Isolation *LKENodePoolIsolation `json:"isolation,omitempty"` } // GetCreateOptions converts a LKENodePool to LKENodePoolCreateOptions for @@ -136,6 +150,7 @@ func (l LKENodePool) GetCreateOptions() (o LKENodePoolCreateOptions) { o.UpdateStrategy = l.UpdateStrategy o.Label = l.Label o.FirewallID = l.FirewallID + o.Isolation = l.Isolation o.DiskEncryption = &l.DiskEncryption return o @@ -152,6 +167,7 @@ func (l LKENodePool) GetUpdateOptions() (o LKENodePoolUpdateOptions) { o.UpdateStrategy = l.UpdateStrategy o.Label = l.Label o.FirewallID = l.FirewallID + o.Isolation = l.Isolation return o } diff --git a/prefixlists.go b/prefixlists.go index 37aede165..9118655d7 100644 --- a/prefixlists.go +++ b/prefixlists.go @@ -3,6 +3,7 @@ package linodego import ( "context" "encoding/json" + "fmt" "time" "github.com/linode/linodego/internal/parseabletime" @@ -59,3 +60,28 @@ func (c *Client) GetPrefixList(ctx context.Context, id int) (*PrefixList, error) endpoint := formatAPIPath("networking/prefixlists/%d", id) return doGETRequest[PrefixList](ctx, c, endpoint) } + +// GetPrefixListByName finds a Prefix List by its name (e.g., "pl:system:object-storage:us-iad"). +// Returns nil and an error if no matching prefix list is found. +func (c *Client) GetPrefixListByName(ctx context.Context, name string) (*PrefixList, error) { + f := Filter{} + f.AddField(Eq, "name", name) + + fJSON, err := f.MarshalJSON() + if err != nil { + return nil, err + } + + opts := ListOptions{Filter: string(fJSON)} + + lists, err := c.ListPrefixLists(ctx, &opts) + if err != nil { + return nil, err + } + + if len(lists) == 0 { + return nil, fmt.Errorf("prefix list with name %q not found", name) + } + + return &lists[0], nil +} diff --git a/test/unit/firewall_rules_test.go b/test/unit/firewall_rules_test.go index 2e04f9f77..d934e144c 100644 --- a/test/unit/firewall_rules_test.go +++ b/test/unit/firewall_rules_test.go @@ -124,6 +124,7 @@ func TestFirewallRule_Update(t *testing.T) { assert.Equal(t, 1, firewallRule.Version) assert.Equal(t, "997dd135", firewallRule.Fingerprint) + assert.Equal(t, 1, firewallRule.Version) assert.Equal(t, "DROP", firewallRule.InboundPolicy) assert.Equal(t, 1, len(firewallRule.Inbound)) assert.Equal(t, "ACCEPT", firewallRule.Inbound[0].Action) @@ -152,6 +153,7 @@ func TestFirewallRule_GetExpansion(t *testing.T) { outboundIPv6 := []string{"pl::vpcs:"} mockResponse := linodego.FirewallRuleSet{ + Version: 2, Inbound: []linodego.FirewallRule{ { Action: "ACCEPT", diff --git a/test/unit/firewalls_test.go b/test/unit/firewalls_test.go index f2161d65e..7f176f406 100644 --- a/test/unit/firewalls_test.go +++ b/test/unit/firewalls_test.go @@ -30,8 +30,10 @@ func TestFirewall_List(t *testing.T) { assert.Equal(t, 123, firewall.ID) assert.Equal(t, "firewall123", firewall.Label) assert.Equal(t, linodego.FirewallStatus("enabled"), firewall.Status) + assert.Equal(t, 2, firewall.Version) assert.Equal(t, "DROP", firewall.Rules.InboundPolicy) + assert.Equal(t, 1, firewall.Rules.Version) assert.Len(t, firewall.Rules.Inbound, 1) inboundRule := firewall.Rules.Inbound[0] @@ -112,6 +114,7 @@ func TestFirewall_Create(t *testing.T) { assert.Equal(t, 123, firewall.ID) assert.Equal(t, "firewall123", firewall.Label) assert.Equal(t, linodego.FirewallStatus("enabled"), firewall.Status) + assert.Equal(t, 1, firewall.Version) assert.ElementsMatch(t, []string{"example tag", "another example"}, firewall.Tags) assert.Equal(t, "2018-01-01T00:01:01Z", firewall.Created.Format(time.RFC3339)) assert.Equal(t, "2018-01-02T00:01:01Z", firewall.Updated.Format(time.RFC3339)) @@ -119,6 +122,7 @@ func TestFirewall_Create(t *testing.T) { assert.NotNil(t, firewall.Rules) assert.Equal(t, "DROP", firewall.Rules.InboundPolicy) assert.Equal(t, "DROP", firewall.Rules.OutboundPolicy) + assert.Equal(t, 1, firewall.Rules.Version) assert.Len(t, firewall.Rules.Inbound, 1) inboundRule := firewall.Rules.Inbound[0] @@ -173,6 +177,7 @@ func TestFirewall_Get(t *testing.T) { assert.Equal(t, 123, firewall.ID) assert.Equal(t, "firewall123", firewall.Label) assert.Equal(t, linodego.FirewallStatus("enabled"), firewall.Status) + assert.Equal(t, 2, firewall.Version) assert.Equal(t, "2018-01-01T00:01:01Z", firewall.Created.Format(time.RFC3339)) assert.Equal(t, "2018-01-02T00:01:01Z", firewall.Updated.Format(time.RFC3339)) assert.ElementsMatch(t, []string{"example tag", "another example"}, firewall.Tags) @@ -180,6 +185,7 @@ func TestFirewall_Get(t *testing.T) { assert.NotNil(t, firewall.Rules) assert.Equal(t, "DROP", firewall.Rules.InboundPolicy) assert.Equal(t, "DROP", firewall.Rules.OutboundPolicy) + assert.Equal(t, 1, firewall.Rules.Version) assert.Len(t, firewall.Rules.Inbound, 1) inboundRule := firewall.Rules.Inbound[0] @@ -227,6 +233,7 @@ func TestFirewall_Update(t *testing.T) { assert.Equal(t, 123, firewall.ID) assert.Equal(t, "firewall123", firewall.Label) assert.Equal(t, linodego.FirewallStatus("enabled"), firewall.Status) + assert.Equal(t, 3, firewall.Version) assert.Equal(t, "2018-01-01T00:01:01Z", firewall.Created.Format(time.RFC3339)) assert.Equal(t, "2018-01-02T00:01:01Z", firewall.Updated.Format(time.RFC3339)) assert.ElementsMatch(t, []string{"updated tag", "another updated tag"}, firewall.Tags) @@ -234,6 +241,7 @@ func TestFirewall_Update(t *testing.T) { assert.NotNil(t, firewall.Rules) assert.Equal(t, "DROP", firewall.Rules.InboundPolicy) assert.Equal(t, "DROP", firewall.Rules.OutboundPolicy) + assert.Equal(t, 1, firewall.Rules.Version) assert.Len(t, firewall.Rules.Inbound, 1) inboundRule := firewall.Rules.Inbound[0] diff --git a/test/unit/fixtures/firewall_create.json b/test/unit/fixtures/firewall_create.json index 18e37afea..21e09742c 100644 --- a/test/unit/fixtures/firewall_create.json +++ b/test/unit/fixtures/firewall_create.json @@ -49,6 +49,7 @@ "example tag", "another example" ], + "version": 1 "entities": [ { "id": 189031, diff --git a/test/unit/fixtures/firewall_get.json b/test/unit/fixtures/firewall_get.json index b425fca39..117da619c 100644 --- a/test/unit/fixtures/firewall_get.json +++ b/test/unit/fixtures/firewall_get.json @@ -49,5 +49,6 @@ "example tag", "another example" ], - "updated": "2018-01-02T00:01:01" + "updated": "2018-01-02T00:01:01", + "version": 2 } \ No newline at end of file diff --git a/test/unit/fixtures/firewall_list.json b/test/unit/fixtures/firewall_list.json index 3417d9f9a..a9b441b23 100644 --- a/test/unit/fixtures/firewall_list.json +++ b/test/unit/fixtures/firewall_list.json @@ -51,7 +51,8 @@ "example tag", "another example" ], - "updated": "2018-01-02T00:01:01" + "updated": "2018-01-02T00:01:01", + "version": 2 } ], "page": 1, diff --git a/test/unit/fixtures/firewall_update.json b/test/unit/fixtures/firewall_update.json index b738a2677..05c7f2eca 100644 --- a/test/unit/fixtures/firewall_update.json +++ b/test/unit/fixtures/firewall_update.json @@ -49,5 +49,6 @@ "updated tag", "another updated tag" ], - "updated": "2018-01-02T00:01:01" + "updated": "2018-01-02T00:01:01", + "version": 3 } \ No newline at end of file diff --git a/test/unit/fixtures/lke_cluster_enterprise_create.json b/test/unit/fixtures/lke_cluster_enterprise_create.json new file mode 100644 index 000000000..8fe4783d0 --- /dev/null +++ b/test/unit/fixtures/lke_cluster_enterprise_create.json @@ -0,0 +1,18 @@ +{ + "id": 3010, + "label": "enterprise-cluster", + "region": "us-east", + "status": "ready", + "tier": "enterprise", + "subnet_id": 2010, + "vpc_id": 1010, + "stack_type": "ipv4-ipv6", + "control_plane": { + "high_availability": true, + "audit_logs_enabled": true + }, + "ruleset_ids": { + "inbound": 4010, + "outbound": 4011 + } +} diff --git a/test/unit/lke_clusters_test.go b/test/unit/lke_clusters_test.go index 0dfad0d51..39d279395 100644 --- a/test/unit/lke_clusters_test.go +++ b/test/unit/lke_clusters_test.go @@ -121,6 +121,60 @@ func TestLKECluster_Create(t *testing.T) { assert.Equal(t, false, cluster.ControlPlane.AuditLogsEnabled) } +func TestLKECluster_Create_Enterprise_RuleSetIDs(t *testing.T) { + fixtureData, err := fixtures.GetFixture("lke_cluster_enterprise_create") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + createOptions := linodego.LKEClusterCreateOptions{ + Label: "enterprise-cluster", + Region: "us-east", + K8sVersion: "1.31", + Tier: "enterprise", + SubnetID: linodego.Pointer(2010), + VpcID: linodego.Pointer(1010), + StackType: linodego.Pointer(linodego.LKEClusterDualStack), + ControlPlane: &linodego.LKEClusterControlPlaneOptions{ + HighAvailability: linodego.Pointer(true), + AuditLogsEnabled: linodego.Pointer(true), + }, + } + + base.MockPost("lke/clusters", fixtureData) + + cluster, err := base.Client.CreateLKECluster(context.Background(), createOptions) + assert.NoError(t, err) + assert.Equal(t, 3010, cluster.ID) + assert.Equal(t, "enterprise", cluster.Tier) + assert.Equal(t, 2010, cluster.SubnetID) + assert.Equal(t, 1010, cluster.VpcID) + assert.Equal(t, linodego.LKEClusterDualStack, cluster.StackType) + + // Validate ruleset_ids deserialization + assert.NotNil(t, cluster.RuleSetIDs, "RuleSetIDs should not be nil for enterprise clusters") + assert.Equal(t, 4010, cluster.RuleSetIDs.Inbound) + assert.Equal(t, 4011, cluster.RuleSetIDs.Outbound) +} + +func TestLKECluster_Get_NoRuleSetIDs(t *testing.T) { + // Standard clusters do not return ruleset_ids; the field should be nil + fixtureData, err := fixtures.GetFixture("lke_cluster_get") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("lke/clusters/123", fixtureData) + + cluster, err := base.Client.GetLKECluster(context.Background(), 123) + assert.NoError(t, err) + assert.Nil(t, cluster.RuleSetIDs, "RuleSetIDs should be nil for standard clusters") +} + func TestLKECluster_Update(t *testing.T) { fixtureData, err := fixtures.GetFixture("lke_cluster_update") assert.NoError(t, err) diff --git a/test/unit/prefixlists_test.go b/test/unit/prefixlists_test.go index 69cf95744..0bffd2f2a 100644 --- a/test/unit/prefixlists_test.go +++ b/test/unit/prefixlists_test.go @@ -119,3 +119,68 @@ func TestPrefixList_UnmarshalJSON(t *testing.T) { } assert.Nil(t, prefixList.Deleted) } + +func TestPrefixLists_GetByName(t *testing.T) { + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + response := map[string]any{ + "data": []map[string]any{ + { + "id": 999, + "name": "pl:system:resolvers:us-iad:staging", + "description": "Resolver ACL", + "visibility": "restricted", + "source_prefixlist_id": nil, + "ipv4": []string{"139.144.192.62"}, + "ipv6": []string{"2600:3c05:e001:bc::1"}, + "version": 7, + "created": "2021-01-01T00:00:00", + "updated": "2021-06-01T00:00:00", + "deleted": nil, + }, + }, + "page": 1, + "pages": 1, + "results": 1, + } + + base.MockGet("networking/prefixlists", response) + + pl, err := base.Client.GetPrefixListByName(context.Background(), "pl:system:resolvers:us-iad:staging") + assert.NoError(t, err) + assert.NotNil(t, pl) + + assert.Equal(t, 999, pl.ID) + assert.Equal(t, "pl:system:resolvers:us-iad:staging", pl.Name) + assert.Equal(t, "Resolver ACL", pl.Description) + assert.Equal(t, "restricted", pl.Visibility) + assert.Equal(t, 7, pl.Version) + if assert.NotNil(t, pl.IPv4) { + assert.Equal(t, []string{"139.144.192.62"}, *pl.IPv4) + } + if assert.NotNil(t, pl.IPv6) { + assert.Equal(t, []string{"2600:3c05:e001:bc::1"}, *pl.IPv6) + } +} + +func TestPrefixLists_GetByName_NotFound(t *testing.T) { + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + response := map[string]any{ + "data": []map[string]any{}, + "page": 1, + "pages": 1, + "results": 0, + } + + base.MockGet("networking/prefixlists", response) + + pl, err := base.Client.GetPrefixListByName(context.Background(), "pl:nonexistent") + assert.Error(t, err) + assert.Nil(t, pl) + assert.Contains(t, err.Error(), "not found") +}