diff --git a/account_settings.go b/account_settings.go index 7e87c9691..38d8b27c9 100644 --- a/account_settings.go +++ b/account_settings.go @@ -4,6 +4,15 @@ import ( "context" ) +type InterfacesForNewLinodes string + +const ( + LegacyConfigOnly InterfacesForNewLinodes = "legacy_config_only" + LegacyConfigDefaultButLinodeAllowed InterfacesForNewLinodes = "legacy_config_default_but_linode_allowed" + LinodeDefaultButLegacyConfigAllowed InterfacesForNewLinodes = "linode_default_but_legacy_config_allowed" + LinodeOnly InterfacesForNewLinodes = "linode_only" +) + // AccountSettings are the account wide flags or plans that effect new resources type AccountSettings struct { // The default backups enrollment status for all new Linodes for all users on the account. When enabled, backups are mandatory per instance. @@ -20,6 +29,10 @@ type AccountSettings struct { // A string like "disabled", "suspended", or "active" describing the status of this account’s Object Storage service enrollment. ObjectStorage *string `json:"object_storage"` + + // NOTE: Interfaces for new linode setting may not currently be available to all users. + // A new configuration flag defines whether new Linodes can use Linode and/or legacy config interfaces. + InterfacesForNewLinodes InterfacesForNewLinodes `json:"interfaces_for_new_linodes"` } // AccountSettingsUpdateOptions are the updateable account wide flags or plans that effect new resources. @@ -27,12 +40,12 @@ type AccountSettingsUpdateOptions struct { // The default backups enrollment status for all new Linodes for all users on the account. When enabled, backups are mandatory per instance. BackupsEnabled *bool `json:"backups_enabled,omitempty"` - // A plan name like "longview-3"..."longview-100", or a nil value for to cancel any existing subscription plan. - // Deprecated: Use PUT /longview/plan instead to update the LongviewSubscription - LongviewSubscription *string `json:"longview_subscription,omitempty"` - // The default network helper setting for all new Linodes and Linode Configs for all users on the account. NetworkHelper *bool `json:"network_helper,omitempty"` + + // NOTE: Interfaces for new linode setting may not currently be available to all users. + // A new configuration flag defines whether new Linodes can use Linode and/or legacy config interfaces. + InterfacesForNewLinodes *InterfacesForNewLinodes `json:"interfaces_for_new_linodes"` } // GetAccountSettings gets the account wide flags or plans that effect new resources diff --git a/firewall_devices.go b/firewall_devices.go index c2db07981..dfdb0f06a 100644 --- a/firewall_devices.go +++ b/firewall_devices.go @@ -15,6 +15,7 @@ type FirewallDeviceType string const ( FirewallDeviceLinode FirewallDeviceType = "linode" FirewallDeviceNodeBalancer FirewallDeviceType = "nodebalancer" + FirewallDeviceInterface FirewallDeviceType = "interface" ) // FirewallDevice represents a device governed by a Firewall diff --git a/firewall_templates.go b/firewall_templates.go new file mode 100644 index 000000000..e3be54fda --- /dev/null +++ b/firewall_templates.go @@ -0,0 +1,23 @@ +package linodego + +import ( + "context" +) + +type FirewallTemplate struct { + Slug string `json:"slug"` + Rules FirewallRuleSet `json:"rules"` +} + +// NOTE: This feature may not currently be available to all users. +// GetFirewallTemplate gets a FirewallTemplate given a slug. +func (c *Client) GetFirewallTemplate(ctx context.Context, slug string) (*FirewallTemplate, error) { + e := formatAPIPath("networking/firewalls/templates/%s", slug) + return doGETRequest[FirewallTemplate](ctx, c, e) +} + +// NOTE: This feature may not currently be available to all users. +// ListFirewallTemplates gets all available firewall templates for the account. +func (c *Client) ListFirewallTemplates(ctx context.Context, opts *ListOptions) ([]FirewallTemplate, error) { + return getPaginatedResults[FirewallTemplate](ctx, c, "networking/firewalls/templates", opts) +} diff --git a/firewalls.go b/firewalls.go index 4eb35dd60..40363365c 100644 --- a/firewalls.go +++ b/firewalls.go @@ -33,6 +33,7 @@ type Firewall struct { type DevicesCreationOptions struct { Linodes []int `json:"linodes,omitempty"` NodeBalancers []int `json:"nodebalancers,omitempty"` + Interfaces []int `json:"interfaces,omitempty"` } // FirewallCreateOptions fields are those accepted by CreateFirewall @@ -50,6 +51,31 @@ type FirewallUpdateOptions struct { Tags *[]string `json:"tags,omitempty"` } +// FirewallSettings represents the default firewalls for Linodes, +// Linode VPC and public interfaces, and NodeBalancers. +type FirewallSettings struct { + DefaultFirewallIDs DefaultFirewallIDs `json:"default_firewall_ids"` +} + +type DefaultFirewallIDs struct { + Linode int `json:"linode"` + NodeBalancer int `json:"nodebalancer"` + PublicInterface int `json:"public_interface"` + VPCInterface int `json:"vpc_interface"` +} + +// FirewallSettingsUpdateOptions is an options struct used when Updating FirewallSettings +type FirewallSettingsUpdateOptions struct { + DefaultFirewallIDs DefaultFirewallIDsOptions `json:"default_firewall_ids"` +} + +type DefaultFirewallIDsOptions struct { + Linode *int `json:"linode,omitempty"` + NodeBalancer *int `json:"nodebalancer,omitempty"` + PublicInterface *int `json:"public_interface,omitempty"` + VPCInterface *int `json:"vpc_interface,omitempty"` +} + // GetUpdateOptions converts a Firewall to FirewallUpdateOptions for use in Client.UpdateFirewall. func (f *Firewall) GetUpdateOptions() FirewallUpdateOptions { return FirewallUpdateOptions{ @@ -107,3 +133,13 @@ func (c *Client) DeleteFirewall(ctx context.Context, firewallID int) error { e := formatAPIPath("networking/firewalls/%d", firewallID) return doDELETERequest(ctx, c, e) } + +// GetFirewallSettings returns default firewalls for Linodes, Linode VPC and public interfaces, and NodeBalancers. +func (c *Client) GetFirewallSettings(ctx context.Context) (*FirewallSettings, error) { + return doGETRequest[FirewallSettings](ctx, c, "networking/firewalls/settings") +} + +// UpdateFirewallSettings updates the default firewalls for Linodes, Linode VPC and public interfaces, and NodeBalancers. +func (c *Client) UpdateFirewallSettings(ctx context.Context, opts FirewallSettingsUpdateOptions) (*FirewallSettings, error) { + return doPUTRequest[FirewallSettings](ctx, c, "networking/firewalls/settings", opts) +} diff --git a/go.work.sum b/go.work.sum index c6a00f684..befc41184 100644 --- a/go.work.sum +++ b/go.work.sum @@ -56,6 +56,7 @@ golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5 golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -77,17 +78,16 @@ golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= diff --git a/instance_ips.go b/instance_ips.go index ae80c3ff3..87f4ed142 100644 --- a/instance_ips.go +++ b/instance_ips.go @@ -21,17 +21,18 @@ type InstanceIPv4Response struct { // InstanceIP represents an Instance IP with additional DNS and networking details type InstanceIP struct { - Address string `json:"address"` - Gateway string `json:"gateway"` - SubnetMask string `json:"subnet_mask"` - Prefix int `json:"prefix"` - Type InstanceIPType `json:"type"` - Public bool `json:"public"` - RDNS string `json:"rdns"` - LinodeID int `json:"linode_id"` - Region string `json:"region"` - VPCNAT1To1 *InstanceIPNAT1To1 `json:"vpc_nat_1_1"` - Reserved bool `json:"reserved"` + Address string `json:"address"` + Gateway string `json:"gateway"` + SubnetMask string `json:"subnet_mask"` + Prefix int `json:"prefix"` + Type InstanceIPType `json:"type"` + Public bool `json:"public"` + RDNS string `json:"rdns"` + LinodeID int `json:"linode_id"` + InterfaceID *int `json:"interface_id"` + Region string `json:"region"` + VPCNAT1To1 *InstanceIPNAT1To1 `json:"vpc_nat_1_1"` + Reserved bool `json:"reserved"` } // VPCIP represents a private IP address in a VPC subnet with additional networking details diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 000000000..15ca1892b --- /dev/null +++ b/interfaces.go @@ -0,0 +1,244 @@ +package linodego + +import ( + "context" + "encoding/json" + "time" + + "github.com/linode/linodego/internal/parseabletime" +) + +type LinodeInterface struct { + ID int `json:"id"` + Version int `json:"version"` + MACAddress string `json:"mac_address"` + Created *time.Time `json:"-"` + Updated *time.Time `json:"-"` + DefaultRoute *InterfaceDefaultRoute `json:"default_route"` + Public *PublicInterface `json:"public"` + VPC *VPCInterface `json:"vpc"` + VLAN *VLANInterface `json:"vlan"` +} + +type InterfaceDefaultRoute struct { + IPv4 *bool `json:"ipv4,omitempty"` + IPv6 *bool `json:"ipv6,omitempty"` +} + +type PublicInterface struct { + IPv4 *PublicInterfaceIPv4 `json:"ipv4"` + IPv6 *PublicInterfaceIPv6 `json:"ipv6"` +} + +type PublicInterfaceIPv4 struct { + Addresses []PublicInterfaceIPv4Address `json:"addresses"` + Shared []PublicInterfaceIPv4Shared `json:"shared"` +} + +type PublicInterfaceIPv6 struct { + Ranges []PublicInterfaceIPv6Range `json:"ranges"` + Shared []PublicInterfaceIPv6Range `json:"shared"` + SLAAC []PublicInterfaceIPv6SLAAC `json:"slaac"` +} + +type PublicInterfaceIPv4Address struct { + Address string `json:"address"` + Primary bool `json:"primary"` +} + +type PublicInterfaceIPv4Shared struct { + Address string `json:"address"` + LinodeID string `json:"linode_id"` +} + +type PublicInterfaceIPv6Range struct { + Range string `json:"range"` + RouteTarget *string `json:"route_target"` +} + +type PublicInterfaceIPv6SLAAC struct { + Prefix int `json:"prefix"` + Address string `json:"address"` +} + +type VPCInterface struct { + VPCID int `json:"vpc_id"` + SubnetID int `json:"subnet_id"` + IPv4 VPCInterfaceIPv4 `json:"ipv4"` +} + +type VPCInterfaceIPv4 struct { + Addresses []VPCInterfaceIPv4Address `json:"addresses"` + Ranges []VPCInterfaceIPv4Range `json:"ranges"` +} + +type VPCInterfaceIPv4Address struct { + Address string `json:"address"` + Primary bool `json:"primary"` + NAT1To1Address *string `json:"nat_1_1_address"` +} + +type VPCInterfaceIPv4Range struct { + Range string `json:"range"` +} + +type VLANInterface struct { + Label string `json:"vlan_label"` + IPAMAddress *string `json:"ipam_address,omitempty"` +} + +type LinodeInterfaceCreateOptions struct { + FirewallID *int `json:"firewall_id,omitempty"` + DefaultRoute *InterfaceDefaultRoute `json:"default_route,omitempty"` + Public *PublicInterfaceCreateOptions `json:"public,omitempty"` + VPC *VPCInterfaceCreateOptions `json:"vpc,omitempty"` + VLAN *VLANInterface `json:"vlan,omitempty"` +} + +type LinodeInterfaceUpdateOptions struct { + DefaultRoute *InterfaceDefaultRoute `json:"default_route,omitempty"` + Public *PublicInterfaceCreateOptions `json:"public,omitempty"` + VPC *VPCInterfaceCreateOptions `json:"vpc,omitempty"` + VLAN *VLANInterface `json:"vlan,omitempty"` +} + +type PublicInterfaceCreateOptions struct { + IPv4 []PublicInterfaceIPv4CreateOptions `json:"ipv4,omitempty"` + IPv6 []PublicInterfaceIPv6CreateOptions `json:"ipv6,omitempty"` +} + +type PublicInterfaceIPv4CreateOptions struct { + Addresses []PublicInterfaceIPv4AddressCreateOptions `json:"addresses,omitempty"` +} + +type PublicInterfaceIPv4AddressCreateOptions struct { + Address string `json:"address"` + Primary *bool `json:"primary,omitempty"` +} + +type PublicInterfaceIPv6CreateOptions struct { + Ranges []PublicInterfaceIPv6RangeCreateOptions `json:"ranges,omitempty"` +} + +type PublicInterfaceIPv6RangeCreateOptions struct { + Range string `json:"range"` +} + +type VPCInterfaceCreateOptions struct { + SubnetID int `json:"subnet_id"` + IPv4 []VPCInterfaceIPv4CreateOptions `json:"ipv4,omitempty"` +} + +type VPCInterfaceIPv4CreateOptions struct { + Addresses []VPCInterfaceIPv4AddressCreateOptions `json:"addresses,omitempty"` + Ranges []VPCInterfaceIPv4RangeCreateOptions `json:"ranges,omitempty"` +} + +type VPCInterfaceIPv4AddressCreateOptions struct { + Address string `json:"address"` + Primary *bool `json:"primary,omitempty"` + NAT1To1Address *string `json:"nat_1_1_address,omitempty"` +} + +type VPCInterfaceIPv4RangeCreateOptions struct { + Range string `json:"range"` +} + +type LinodeInterfacesUpgrade struct { + ConfigID int `json:"config_id"` + DryRun bool `json:"dry_run"` + Interfaces []LinodeInterface `json:"interfaces"` +} + +type LinodeInterfacesUpgradeOptions struct { + ConfigID *int `json:"config_id,omitempty"` + DryRun *bool `json:"dry_run,omitempty"` +} + +type InterfaceSettings struct { + NetworkHelper bool `json:"network_helper"` + DefaultRoute InterfaceDefaultRouteSetting `json:"default_route"` +} + +type InterfaceSettingsUpdateOptions struct { + NetworkHelper *bool `json:"network_helper,omitempty"` + DefaultRoute *InterfaceDefaultRouteSettingUpdateOptions `json:"default_route,omitempty"` +} + +type InterfaceDefaultRouteSettingUpdateOptions struct { + IPv4InterfaceID *int `json:"ipv4_interface_id,omitempty"` + IPv6InterfaceID *int `json:"ipv6_interface_id,omitempty"` +} + +type InterfaceDefaultRouteSetting struct { + IPv4InterfaceID *int `json:"ipv4_interface_id"` + IPv4EligibleInterfaceIDs []int `json:"ipv4_eligible_interface_ids"` + IPv6InterfaceID *int `json:"ipv6_interface_id"` + IPv6EligibleInterfaceIDs []int `json:"ipv6_eligible_interface_ids"` +} + +func (i *LinodeInterface) UnmarshalJSON(b []byte) error { + type Mask LinodeInterface + + p := struct { + *Mask + Created *parseabletime.ParseableTime `json:"created"` + Updated *parseabletime.ParseableTime `json:"updated"` + }{ + Mask: (*Mask)(i), + } + + if err := json.Unmarshal(b, &p); err != nil { + return err + } + + i.Created = (*time.Time)(p.Created) + i.Updated = (*time.Time)(p.Updated) + + return nil +} + +func (c *Client) ListInterfaces(ctx context.Context, linodeID int, opts *ListOptions) ([]LinodeInterface, error) { + e := formatAPIPath("linode/instances/%d/interfaces", linodeID) + return getPaginatedResults[LinodeInterface](ctx, c, e, opts) +} + +func (c *Client) GetInterface(ctx context.Context, linodeID int, interfaceID int) (*LinodeInterface, error) { + e := formatAPIPath("linode/instances/%d/interfaces/%d", linodeID, interfaceID) + return doGETRequest[LinodeInterface](ctx, c, e) +} + +func (c *Client) CreateInterface(ctx context.Context, linodeID int, opts LinodeInterfaceCreateOptions) (*LinodeInterface, error) { + e := formatAPIPath("linode/instances/%d/interfaces", linodeID) + return doPOSTRequest[LinodeInterface](ctx, c, e, opts) +} + +func (c *Client) UpdateInterface(ctx context.Context, linodeID int, interfaceID int, opts LinodeInterfaceUpdateOptions) (*LinodeInterface, error) { + e := formatAPIPath("linode/instances/%d/interfaces/%d", linodeID, interfaceID) + return doPUTRequest[LinodeInterface](ctx, c, e, opts) +} + +func (c *Client) DeleteInterface(ctx context.Context, linodeID int, interfaceID int) error { + e := formatAPIPath("linode/instances/%d/interfaces/%d", linodeID, interfaceID) + return doDELETERequest(ctx, c, e) +} + +func (c *Client) UpgradeInterfaces(ctx context.Context, linodeID int, opts LinodeInterfacesUpgradeOptions) (*LinodeInterfacesUpgrade, error) { + e := formatAPIPath("linode/instances/%d/upgrade-interfaces", linodeID) + return doPOSTRequest[LinodeInterfacesUpgrade](ctx, c, e, opts) +} + +func (c *Client) ListInterfaceFirewalls(ctx context.Context, linodeID int, interfaceID int, opts *ListOptions) ([]Firewall, error) { + e := formatAPIPath("linode/instances/%d/interfaces/%d/firewalls", linodeID, interfaceID) + return getPaginatedResults[Firewall](ctx, c, e, opts) +} + +func (c *Client) GetInterfaceSettings(ctx context.Context, linodeID int) (*InterfaceSettings, error) { + e := formatAPIPath("linode/instances/%d/interfaces/settings", linodeID) + return doGETRequest[InterfaceSettings](ctx, c, e) +} + +func (c *Client) UpdateInterfaceSettings(ctx context.Context, linodeID int, opts InterfaceSettingsUpdateOptions) (*InterfaceSettings, error) { + e := formatAPIPath("linode/instances/%d/interfaces/settings", linodeID) + return doPUTRequest[InterfaceSettings](ctx, c, e, opts) +} diff --git a/regions.go b/regions.go index d31e8b20d..3fd44a802 100644 --- a/regions.go +++ b/regions.go @@ -22,6 +22,7 @@ const ( CapabilityEdgePlans string = "Edge Plans" CapabilityGPU string = "GPU Linodes" CapabilityKubernetesEnterprise string = "Kubernetes Enterprise" + CapabilityLinodeInterfaces string = "Linode Interfaces" CapabilityLADiskEncryption string = "LA Disk Encryption" CapabilityLKE string = "Kubernetes" CapabilityLKEControlPlaneACL string = "LKE Network Access Control List (IP ACL)" diff --git a/test/integration/account_settings_test.go b/test/integration/account_settings_test.go deleted file mode 100644 index ecba9393d..000000000 --- a/test/integration/account_settings_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package integration - -import ( - "context" - "encoding/json" - "testing" - - "github.com/jarcoal/httpmock" - "github.com/linode/linodego" - "github.com/stretchr/testify/require" -) - -func TestAccountSettings_Get(t *testing.T) { - client, teardown := createTestClient(t, "fixtures/TestAccountSettings") - defer teardown() - - // Mocking the API response - httpmock.Activate() - defer httpmock.DeactivateAndReset() - - mockSettings := linodego.AccountSettings{ - BackupsEnabled: true, - Managed: true, - NetworkHelper: true, - LongviewSubscription: String("longview-3"), - ObjectStorage: String("active"), - } - mockResponse, _ := json.Marshal(mockSettings) - - httpmock.RegisterResponder("GET", "https://api.linode.com/v4/account/settings", - httpmock.NewStringResponder(200, string(mockResponse))) - - settings, err := client.GetAccountSettings(context.Background()) - require.NoError(t, err, "Error getting Account Settings") - - require.True(t, settings.BackupsEnabled, "Expected BackupsEnabled to be true") - require.True(t, settings.Managed, "Expected Managed to be true") - require.True(t, settings.NetworkHelper, "Expected NetworkHelper to be true") - require.NotNil(t, settings.LongviewSubscription, "Expected LongviewSubscription to be non-nil") - require.Equal(t, "longview-3", *settings.LongviewSubscription, "Expected LongviewSubscription to be 'longview-3'") - require.NotNil(t, settings.ObjectStorage, "Expected ObjectStorage to be non-nil") - require.Equal(t, "active", *settings.ObjectStorage, "Expected ObjectStorage to be 'active'") -} - -func TestAccountSettings_Update(t *testing.T) { - client, teardown := createTestClient(t, "fixtures/TestAccountSettings") - defer teardown() - - // Mocking the API response - httpmock.Activate() - defer httpmock.DeactivateAndReset() - - opts := linodego.AccountSettingsUpdateOptions{ - BackupsEnabled: Bool(false), - LongviewSubscription: String("longview-10"), - NetworkHelper: Bool(false), - } - - mockSettings := linodego.AccountSettings{ - BackupsEnabled: false, - NetworkHelper: false, - LongviewSubscription: String("longview-10"), - } - mockResponse, _ := json.Marshal(mockSettings) - - httpmock.RegisterResponder("PUT", "https://api.linode.com/v4/account/settings", - httpmock.NewStringResponder(200, string(mockResponse))) - - settings, err := client.UpdateAccountSettings(context.Background(), opts) - require.NoError(t, err, "Error updating Account Settings") - - require.False(t, settings.BackupsEnabled, "Expected BackupsEnabled to be false") - require.False(t, settings.NetworkHelper, "Expected NetworkHelper to be false") - require.NotNil(t, settings.LongviewSubscription, "Expected LongviewSubscription to be non-nil") - require.Equal(t, "longview-10", *settings.LongviewSubscription, "Expected LongviewSubscription to be 'longview-10'") -} - -func Bool(v bool) *bool { return &v } -func String(v string) *string { return &v } diff --git a/test/integration/fiirewall_templates_test.go b/test/integration/fiirewall_templates_test.go new file mode 100644 index 000000000..8d760c6b3 --- /dev/null +++ b/test/integration/fiirewall_templates_test.go @@ -0,0 +1,44 @@ +package integration + +import ( + "context" + "testing" +) + +func TestFirewallTemplates_List(t *testing.T) { + client, fixtureTeardown := createTestClient(t, "fixtures/TestFirewallTemplates_List") + defer fixtureTeardown() + + result, err := client.ListFirewallTemplates(context.Background(), nil) + if err != nil { + t.Errorf("Error listing firewall templates, expected struct, got error %v", err) + } + + if len(result) == 0 { + t.Errorf("Expected a list of Firewalls, but got none: %v", err) + } +} + +func TestFirewallTemplate_Get(t *testing.T) { + client, fixtureTeardown := createTestClient(t, "fixtures/TestFirewallTemplate_Get") + defer fixtureTeardown() + + result, err := client.GetFirewallTemplate(context.Background(), "public") + if err != nil { + t.Errorf("Error listing firewall templates, expected struct, got error %v", err) + } + + if result.Rules.InboundPolicy != "DROP" { + t.Errorf( + "Expected inbound_policy for the public firewall template to be 'DROP', but got: %q", + result.Rules.InboundPolicy, + ) + } + + if result.Rules.OutboundPolicy != "ACCEPT" { + t.Errorf( + "Expected outbound_policy for the public firewall template to be 'ACCEPT', but got: %q", + result.Rules.OutboundPolicy, + ) + } +} diff --git a/test/integration/fixtures/TestAccountSettings.yaml b/test/integration/fixtures/TestAccountSettings.yaml deleted file mode 100644 index 3f789524c..000000000 --- a/test/integration/fixtures/TestAccountSettings.yaml +++ /dev/null @@ -1,51 +0,0 @@ -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.linode.com/v4beta/account/settings - method: GET - response: - body: '{ - "backups_enabled": true, - "managed": true, - "network_helper": true, - "longview_subscription": "longview-3", - "object_storage": "active" - }' - headers: - Content-Type: - - application/json - status: 200 - code: 200 - duration: "" - -- request: - body: '{"backups_enabled":false,"longview_subscription":"longview-10","network_helper":false}' - form: {} - headers: - Accept: - - application/json - Content-Type: - - application/json - url: https://api.linode.com/v4beta/account/settings - method: PUT - response: - body: '{ - "backups_enabled": false, - "managed": true, - "network_helper": false, - "longview_subscription": "longview-10", - "object_storage": "active" - }' - headers: - Content-Type: - - application/json - status: 200 - code: 200 - duration: "" diff --git a/test/integration/fixtures/TestFirewallTemplate_Get.yaml b/test/integration/fixtures/TestFirewallTemplate_Get.yaml new file mode 100644 index 000000000..a3eb6eba3 --- /dev/null +++ b/test/integration/fixtures/TestFirewallTemplate_Get.yaml @@ -0,0 +1,70 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/networking/firewalls/templates/public + method: GET + response: + body: '{"slug": "public", "rules": {"inbound": [{"action": "ACCEPT", "addresses": + {"ipv4": ["0.0.0.0/0"], "ipv6": ["1234::5678/0"]}, "ports": "22", "protocol": "TCP", + "label": "accept-inbound-ssh", "description": "Accept inbound SSH"}, {"action": + "ACCEPT", "addresses": {"ipv4": ["0.0.0.0/0"], "ipv6": ["1234::5678/0"]}, "protocol": + "ICMP", "label": "accept-inbound-icmp", "description": "Accept inbound ICMP"}], + "inbound_policy": "DROP", "outbound": [], "outbound_policy": "ACCEPT"}}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Length: + - "461" + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Mon, 21 Apr 2025 07:03:52 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + - Authorization, X-Filter + X-Accepted-Oauth-Scopes: + - '*' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - '*' + X-Ratelimit-Limit: + - "1600" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" diff --git a/test/integration/fixtures/TestFirewallTemplates_List.yaml b/test/integration/fixtures/TestFirewallTemplates_List.yaml new file mode 100644 index 000000000..bf2f1666f --- /dev/null +++ b/test/integration/fixtures/TestFirewallTemplates_List.yaml @@ -0,0 +1,94 @@ +--- +version: 1 +interactions: +- request: + body: "" + form: {} + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - linodego/dev https://github.com/linode/linodego + url: https://api.linode.com/v4beta/networking/firewalls/templates?page=1 + method: GET + response: + body: '{"data": [{"slug": "akamai-non-prod", "rules": {"inbound": [{"action": + "ACCEPT", "addresses": {"ipv4": ["0.0.0.0/0"], "ipv6": ["1234::5678/0"]}, "protocol": + "ICMP", "label": "accept-inbound-icmp", "description": "Accept inbound ICMP"}, + {"action": "ACCEPT", "addresses": {"ipv4": ["172.236.119.4/30", "172.234.160.4/30", + "172.236.94.4/30"], "ipv6": ["1234::5678/128", "1234::5678/128", + "1234::5678/128", "1234::5678/128", + "1234::5678/128", "1234::5678/128", + "1234::5678/128", "1234::5678/128", + "1234::5678/128"]}, "ports": "22,443", "protocol": "TCP", + "label": "accept-inbound-tcp-ssh-proxies", "description": "Accept inbound TCP + from SSH proxies"}, {"action": "ACCEPT", "addresses": {"ipv4": ["139.144.212.168/31", + "172.232.23.164/31"]}, "ports": "22,443", "protocol": "TCP", "label": "accept-inbound-tcp-eaa-proxies", + "description": "Accept inbound TCP from EAA proxies"}], "inbound_policy": "DROP", + "outbound": [], "outbound_policy": "ACCEPT"}}, {"slug": "vpc", "rules": {"inbound": + [{"action": "ACCEPT", "addresses": {"ipv4": ["0.0.0.0/0"], "ipv6": ["1234::5678/0"]}, + "ports": "22", "protocol": "TCP", "label": "accept-inbound-ssh", "description": + "Accept inbound SSH"}, {"action": "ACCEPT", "addresses": {"ipv4": ["0.0.0.0/0"], + "ipv6": ["1234::5678/0"]}, "protocol": "ICMP", "label": "accept-inbound-icmp", "description": + "Accept inbound ICMP"}, {"action": "ACCEPT", "addresses": {"ipv4": ["10.0.0.0/8", + "192.168.0.0/17", "172.16.0.0/12"]}, "ports": "1-65535", "protocol": "TCP", + "label": "accept-inbound-rfc1918", "description": "Accept inbound RFC-1918"}, + {"action": "ACCEPT", "addresses": {"ipv4": ["10.0.0.0/8", "192.168.0.0/17", + "172.16.0.0/12"]}, "description": "Accept inbound RFC-1918", "label": "accept-inbound-rfc1918", + "ports": "1-65535", "protocol": "UDP"}], "inbound_policy": "DROP", "outbound": + [], "outbound_policy": "ACCEPT"}}, {"slug": "public", "rules": {"inbound": [{"action": + "ACCEPT", "addresses": {"ipv4": ["0.0.0.0/0"], "ipv6": ["1234::5678/0"]}, "ports": "22", + "protocol": "TCP", "label": "accept-inbound-ssh", "description": "Accept inbound + SSH"}, {"action": "ACCEPT", "addresses": {"ipv4": ["0.0.0.0/0"], "ipv6": ["1234::5678/0"]}, + "protocol": "ICMP", "label": "accept-inbound-icmp", "description": "Accept inbound + ICMP"}], "inbound_policy": "DROP", "outbound": [], "outbound_policy": "ACCEPT"}}], + "page": 1, "pages": 1, "results": 3}' + headers: + Access-Control-Allow-Credentials: + - "true" + Access-Control-Allow-Headers: + - Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS, POST, PUT, DELETE + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status + Akamai-Internal-Account: + - '*' + Cache-Control: + - max-age=0, no-cache, no-store + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json + Expires: + - Mon, 21 Apr 2025 07:03:52 GMT + Pragma: + - no-cache + Strict-Transport-Security: + - max-age=31536000 + Vary: + - Authorization, X-Filter + - Authorization, X-Filter + - Accept-Encoding + X-Accepted-Oauth-Scopes: + - '*' + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + - DENY + X-Oauth-Scopes: + - '*' + X-Ratelimit-Limit: + - "1600" + X-Xss-Protection: + - 1; mode=block + status: 200 OK + code: 200 + duration: "" diff --git a/test/unit/account_settings_test.go b/test/unit/account_settings_test.go index 56436c79a..7915d82e5 100644 --- a/test/unit/account_settings_test.go +++ b/test/unit/account_settings_test.go @@ -31,6 +31,10 @@ func TestAccountSettings_Get(t *testing.T) { assert.Nil(t, accountSettings.LongviewSubscription, "Expected 'longview_subscription' to be nil") assert.True(t, accountSettings.BackupsEnabled, "Expected 'backups_enabled' to be true") assert.Equal(t, "active", *accountSettings.ObjectStorage, "Expected 'object_storage' to be 'active'") + assert.Equal( + t, linodego.LegacyConfigDefaultButLinodeAllowed, accountSettings.InterfacesForNewLinodes, + "Expected 'object_storage' to be 'active'", + ) } func TestAccountSettings_Update(t *testing.T) { @@ -41,9 +45,11 @@ func TestAccountSettings_Update(t *testing.T) { base.SetUp(t) defer base.TearDown(t) + i := linodego.LegacyConfigDefaultButLinodeAllowed requestData := linodego.AccountSettingsUpdateOptions{ - BackupsEnabled: Bool(true), - NetworkHelper: Bool(true), + BackupsEnabled: Bool(true), + NetworkHelper: Bool(true), + InterfacesForNewLinodes: &i, } base.MockPut("account/settings", fixtureData) @@ -55,4 +61,8 @@ func TestAccountSettings_Update(t *testing.T) { assert.Nil(t, accountSettings.LongviewSubscription, "Expected 'longview_subscription' to be nil") assert.True(t, accountSettings.BackupsEnabled, "Expected 'backups_enabled' to be true") assert.Equal(t, "active", *accountSettings.ObjectStorage, "Expected 'object_storage' to be 'active'") + assert.Equal( + t, linodego.LegacyConfigDefaultButLinodeAllowed, accountSettings.InterfacesForNewLinodes, + "Expected 'object_storage' to be 'active'", + ) } diff --git a/test/unit/firewalls_test.go b/test/unit/firewalls_test.go index 67bf0b563..2089dc6dc 100644 --- a/test/unit/firewalls_test.go +++ b/test/unit/firewalls_test.go @@ -98,6 +98,9 @@ func TestFirewall_Create(t *testing.T) { }, }, Tags: []string{"example tag", "another example"}, + Devices: linodego.DevicesCreationOptions{ + Interfaces: []int{1, 2, 3}, + }, } base.MockPost(formatMockAPIPath("networking/firewalls"), fixtureData) @@ -250,3 +253,54 @@ func TestFirewall_Delete(t *testing.T) { t.Fatal(err) } } + +func TestDefaultFirewall_Get(t *testing.T) { + fixtureData, err := fixtures.GetFixture("default_firewalls_get") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet(formatMockAPIPath("networking/firewalls/settings"), fixtureData) + + defaultFirewalls, err := base.Client.GetFirewallSettings(context.Background()) + + assert.NoError(t, err) + assert.NotNil(t, defaultFirewalls) + + assert.Equal(t, 101, defaultFirewalls.DefaultFirewallIDs.NodeBalancer) + assert.Equal(t, 100, defaultFirewalls.DefaultFirewallIDs.Linode) + assert.Equal(t, 200, defaultFirewalls.DefaultFirewallIDs.PublicInterface) + assert.Equal(t, 200, defaultFirewalls.DefaultFirewallIDs.VPCInterface) +} + +func TestDefaultFirewall_Update(t *testing.T) { + fixtureData, err := fixtures.GetFixture("default_firewalls_update") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPut(formatMockAPIPath("networking/firewalls/settings"), fixtureData) + + requestData := linodego.FirewallSettingsUpdateOptions{ + DefaultFirewallIDs: linodego.DefaultFirewallIDsOptions{ + Linode: linodego.Pointer(1), + NodeBalancer: linodego.Pointer(1), + VPCInterface: linodego.Pointer(1), + PublicInterface: linodego.Pointer(1), + }, + } + + defaultFirewalls, err := base.Client.UpdateFirewallSettings(context.Background(), requestData) + + assert.NoError(t, err) + assert.NotNil(t, defaultFirewalls) + + assert.Equal(t, 1, defaultFirewalls.DefaultFirewallIDs.NodeBalancer) + assert.Equal(t, 1, defaultFirewalls.DefaultFirewallIDs.Linode) + assert.Equal(t, 1, defaultFirewalls.DefaultFirewallIDs.PublicInterface) + assert.Equal(t, 1, defaultFirewalls.DefaultFirewallIDs.VPCInterface) +} diff --git a/test/unit/fixtures/account_settings_get.json b/test/unit/fixtures/account_settings_get.json index 38d5a68c9..3b0af95a0 100644 --- a/test/unit/fixtures/account_settings_get.json +++ b/test/unit/fixtures/account_settings_get.json @@ -3,5 +3,6 @@ "network_helper": true, "longview_subscription": null, "backups_enabled": true, - "object_storage": "active" + "object_storage": "active", + "interfaces_for_new_linodes": "legacy_config_default_but_linode_allowed" } \ No newline at end of file diff --git a/test/unit/fixtures/account_settings_update.json b/test/unit/fixtures/account_settings_update.json index 38d5a68c9..3b0af95a0 100644 --- a/test/unit/fixtures/account_settings_update.json +++ b/test/unit/fixtures/account_settings_update.json @@ -3,5 +3,6 @@ "network_helper": true, "longview_subscription": null, "backups_enabled": true, - "object_storage": "active" + "object_storage": "active", + "interfaces_for_new_linodes": "legacy_config_default_but_linode_allowed" } \ No newline at end of file diff --git a/test/unit/fixtures/default_firewalls_get.json b/test/unit/fixtures/default_firewalls_get.json new file mode 100644 index 000000000..77652f6bd --- /dev/null +++ b/test/unit/fixtures/default_firewalls_get.json @@ -0,0 +1,8 @@ +{ + "default_firewall_ids": { + "linode": 100, + "nodebalancer": 101, + "public_interface": 200, + "vpc_interface": 200 + } +} \ No newline at end of file diff --git a/test/unit/fixtures/default_firewalls_update.json b/test/unit/fixtures/default_firewalls_update.json new file mode 100644 index 000000000..f5e1fae70 --- /dev/null +++ b/test/unit/fixtures/default_firewalls_update.json @@ -0,0 +1,8 @@ +{ + "default_firewall_ids": { + "linode": 1, + "nodebalancer": 1, + "public_interface": 1, + "vpc_interface": 1 + } +} \ No newline at end of file diff --git a/test/unit/fixtures/interface_create.json b/test/unit/fixtures/interface_create.json new file mode 100644 index 000000000..09664d474 --- /dev/null +++ b/test/unit/fixtures/interface_create.json @@ -0,0 +1,23 @@ +{ + "id": 123, + "mac_address": "22:00:AB:CD:EF:01", + "created": "2024-01-01T00:01:01", + "updated": "2024-01-01T00:01:01", + "default_route": { + "ipv4": true, + "ipv6": false + }, + "version": 1, + "vpc": null, + "public": { + "ipv4": { + "addresses": [ + { + "address": "auto", + "primary": true + } + ] + } + }, + "vlan": null +} diff --git a/test/unit/fixtures/interface_get.json b/test/unit/fixtures/interface_get.json new file mode 100644 index 000000000..8111f61ca --- /dev/null +++ b/test/unit/fixtures/interface_get.json @@ -0,0 +1,17 @@ +{ + "id": 123, + "mac_address": "22:00:AB:CD:EF:01", + "created": "2024-01-01T00:01:01", + "updated": "2024-01-01T00:01:01", + "default_route": { + "ipv4": false, + "ipv6": false + }, + "version": 1, + "vpc": null, + "public": null, + "vlan": { + "vlan_label": "my_vlan", + "ipam_address": "10.0.0.1/24" + } +} diff --git a/test/unit/fixtures/interface_list.json b/test/unit/fixtures/interface_list.json new file mode 100644 index 000000000..5574ae3ca --- /dev/null +++ b/test/unit/fixtures/interface_list.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "id": 123, + "mac_address": "22:00:AB:CD:EF:01", + "created": "2024-01-01T00:01:01", + "updated": "2024-01-01T00:01:01", + "default_route": { + "ipv4": false, + "ipv6": false + }, + "version": 1, + "vpc": null, + "public": null, + "vlan": { + "vlan_label": "my_vlan", + "ipam_address": "10.0.0.1/24" + } + } + ], + "page": 1, + "pages": 1, + "results": 1 +} diff --git a/test/unit/fixtures/interface_settings_get.json b/test/unit/fixtures/interface_settings_get.json new file mode 100644 index 000000000..2b8aeb25f --- /dev/null +++ b/test/unit/fixtures/interface_settings_get.json @@ -0,0 +1,9 @@ +{ + "network_helper": false, + "default_route": { + "ipv4_interface_id": 1, + "ipv4_eligible_interface_ids": [1, 2], + "ipv6_interface_id": 3, + "ipv6_eligible_interface_ids": [3, 4] + } +} diff --git a/test/unit/fixtures/interface_settings_update.json b/test/unit/fixtures/interface_settings_update.json new file mode 100644 index 000000000..2402f4794 --- /dev/null +++ b/test/unit/fixtures/interface_settings_update.json @@ -0,0 +1,9 @@ +{ + "network_helper": true, + "default_route": { + "ipv4_interface_id": 1, + "ipv4_eligible_interface_ids": [1, 2], + "ipv6_interface_id": 3, + "ipv6_eligible_interface_ids": [3, 4] + } +} diff --git a/test/unit/fixtures/interface_update.json b/test/unit/fixtures/interface_update.json new file mode 100644 index 000000000..b87bc4614 --- /dev/null +++ b/test/unit/fixtures/interface_update.json @@ -0,0 +1,17 @@ +{ + "id": 123, + "mac_address": "22:00:AB:CD:EF:01", + "created": "2024-01-01T00:01:01", + "updated": "2024-01-01T00:01:01", + "default_route": { + "ipv4": false, + "ipv6": true + }, + "version": 1, + "vpc": null, + "public": null, + "vlan": { + "vlan_label": "my_vlan", + "ipam_address": "10.0.0.1/24" + } +} diff --git a/test/unit/fixtures/interface_upgrade.json b/test/unit/fixtures/interface_upgrade.json new file mode 100644 index 000000000..777daf727 --- /dev/null +++ b/test/unit/fixtures/interface_upgrade.json @@ -0,0 +1,23 @@ +{ + "config_id": 123, + "dry_run": false, + "interfaces": [ + { + "id": 123, + "mac_address": "22:00:AB:CD:EF:01", + "created": "2024-01-01T00:01:01", + "updated": "2024-01-01T00:01:01", + "default_route": { + "ipv4": false, + "ipv6": false + }, + "version": 1, + "vpc": null, + "public": null, + "vlan": { + "vlan_label": "my_vlan", + "ipam_address": "10.0.0.1/24" + } + } + ] +} diff --git a/test/unit/fixtures/network_ip_address_get.json b/test/unit/fixtures/network_ip_address_get.json index cc4beded2..4f00e6397 100644 --- a/test/unit/fixtures/network_ip_address_get.json +++ b/test/unit/fixtures/network_ip_address_get.json @@ -1,5 +1,17 @@ { - "address": "192.168.1.1", - "linode_id": 12345, - "reserved": false -} + "address": "97.107.143.141", + "gateway": "97.107.143.1", + "interface_id": 456, + "linode_id": 123, + "prefix": 24, + "public": true, + "rdns": "test.example.org", + "region": "us-east", + "subnet_mask": "255.255.255.0", + "type": "ipv4", + "vpc_nat_1_1": { + "address": "192.168.0.42", + "subnet_id": 101, + "vpc_id": 111 + } +} \ No newline at end of file diff --git a/test/unit/fixtures/network_ip_addresses_list.json b/test/unit/fixtures/network_ip_addresses_list.json index 0579d1e8a..0fedeb9da 100644 --- a/test/unit/fixtures/network_ip_addresses_list.json +++ b/test/unit/fixtures/network_ip_addresses_list.json @@ -1,16 +1,24 @@ { "data": [ { - "address": "192.168.1.1", - "linode_id": 12345, - "reserved": false - }, - { - "address": "192.168.1.2", - "linode_id": 67890, - "reserved": true + "address": "197.1O7.143.141", + "gateway": "197.1O7.143.1", + "interface_id": 456, + "linode_id": 123, + "prefix": 24, + "public": true, + "rdns": "test.example.org", + "region": "us-east", + "subnet_mask": "192.0.2.139", + "type": "ipv4", + "vpc_nat_1_1": { + "address": "192.0.2.1", + "subnet_id": 101, + "vpc_id": 111 + } } ], + "page": 1, "pages": 1, - "results": 2 -} + "results": 1 +} \ No newline at end of file diff --git a/test/unit/fixtures/vpc_get.json b/test/unit/fixtures/vpc_get.json index 49ec79cd5..b3cd338d2 100644 --- a/test/unit/fixtures/vpc_get.json +++ b/test/unit/fixtures/vpc_get.json @@ -1,9 +1,29 @@ { + "created": "2023-07-11T00:00:00", + "description": "A description of my VPC.", "id": 123, - "label": "test-vpc", - "description": "Test VPC description", + "label": "cool-vpc", "region": "us-east", - "subnets": [], - "created": "2023-01-01T12:00:00", - "updated": "2023-01-02T12:00:00" + "subnets": [ + { + "created": "2023-07-11T00:00:00", + "id": 456, + "ipv4": "10.0.1.0/24", + "label": "cool-vpc-subnet", + "linodes": [ + { + "id": 111, + "interfaces": [ + { + "active": true, + "config_id": 4567, + "id": 421 + } + ] + } + ], + "updated": "2023-09-11T00:00:00" + } + ], + "updated": "2023-09-11T00:00:00" } \ No newline at end of file diff --git a/test/unit/fixtures/vpc_list.json b/test/unit/fixtures/vpc_list.json index 549e47f96..612a4c4a2 100644 --- a/test/unit/fixtures/vpc_list.json +++ b/test/unit/fixtures/vpc_list.json @@ -1,21 +1,36 @@ { "data": [ { + "created": "2023-07-11T00:00:00", + "description": "A description of my VPC.", "id": 123, - "label": "test-vpc", - "description": "Test VPC description", + "label": "cool-vpc", "region": "us-east", "subnets": [ { + "created": "2023-07-11T00:00:00", "id": 456, - "label": "subnet-1", - "ipv4": "192.168.1.0/24", - "linodes": [] + "ipv4": "192.0.2.210/24", + "label": "cool-vpc-subnet", + "linodes": [ + { + "id": 111, + "interfaces": [ + { + "active": true, + "config_id": null, + "id": 421 + } + ] + } + ], + "updated": "2023-09-11T00:00:00" } - ] + ], + "updated": "2023-09-11T00:00:00" } ], "page": 1, "pages": 1, "results": 1 -} +} \ No newline at end of file diff --git a/test/unit/fixtures/vpc_subnet_get.json b/test/unit/fixtures/vpc_subnet_get.json index e1dfdf0c9..e1950b87a 100644 --- a/test/unit/fixtures/vpc_subnet_get.json +++ b/test/unit/fixtures/vpc_subnet_get.json @@ -1,22 +1,19 @@ { - "id": 456, - "label": "Existing Subnet", - "ipv4": "192.168.2.0/24", - "linodes": [ - { - "id": 101, - "interfaces": [ - { - "id": 1, - "active": true - }, - { - "id": 2, - "active": false - } - ] - } - ], - "created": "2025-01-01T10:00:00", - "updated": "2025-01-02T10:00:00" + "created": "2023-07-11T00:00:00", + "id": 456, + "ipv4": "10.0.1.0/24", + "label": "cool-vpc-subnet", + "linodes": [ + { + "id": 111, + "interfaces": [ + { + "active": true, + "config_id": 4567, + "id": 421 + } + ] + } + ], + "updated": "2023-09-11T00:00:00" } \ No newline at end of file diff --git a/test/unit/fixtures/vpc_subnets_list.json b/test/unit/fixtures/vpc_subnets_list.json index b13895a53..5e0a55315 100644 --- a/test/unit/fixtures/vpc_subnets_list.json +++ b/test/unit/fixtures/vpc_subnets_list.json @@ -1,17 +1,26 @@ { - "data": [ - { - "id": 123, - "label": "Subnet A", - "ipv4": "192.168.3.0/24" - }, - { - "id": 124, - "label": "Subnet B", - "ipv4": "192.168.4.0/24" - } - ], - "page": 1, - "pages": 1 -} - \ No newline at end of file + "data": [ + { + "created": "2023-07-11T00:00:00", + "id": 456, + "ipv4": "192.0.2.13/24", + "label": "cool-vpc-subnet", + "linodes": [ + { + "id": 111, + "interfaces": [ + { + "active": true, + "config_id": null, + "id": 421 + } + ] + } + ], + "updated": "2023-09-11T00:00:00" + } + ], + "page": 1, + "pages": 1, + "results": 1 +} \ No newline at end of file diff --git a/test/unit/interface_test.go b/test/unit/interface_test.go new file mode 100644 index 000000000..3df7e9416 --- /dev/null +++ b/test/unit/interface_test.go @@ -0,0 +1,225 @@ +package unit + +import ( + "context" + "testing" + + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" +) + +func TestInterface_Get(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("interface_get") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("linode/instances/123/interfaces/123", fixtureData) + + iface, err := base.Client.GetInterface(context.Background(), 123, 123) + if err != nil { + t.Fatalf("Error fetching interfaces: %v", err) + } + + assert.Equal(t, 123, iface.ID) + assert.Equal(t, 1, iface.Version) + assert.Equal(t, false, *iface.DefaultRoute.IPv4) + assert.Equal(t, "my_vlan", iface.VLAN.Label) +} + +func TestInterface_List(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("interface_list") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("linode/instances/123/interfaces", fixtureData) + + ifaces, err := base.Client.ListInterfaces(context.Background(), 123, nil) + if err != nil { + t.Fatalf("Error fetching interfaces: %v", err) + } + + assert.Equal(t, 123, ifaces[0].ID) + assert.Equal(t, 1, ifaces[0].Version) +} + +func TestInterface_Delete(t *testing.T) { + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockDelete("linode/instances/123/interfaces/123", nil) + + err := base.Client.DeleteInterface(context.Background(), 123, 123) + assert.NoError(t, err) +} + +func TestInterface_Create(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("interface_create") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPost("linode/instances/123/interfaces", fixtureData) + + opts := linodego.LinodeInterfaceCreateOptions{ + FirewallID: linodego.Pointer(123), + Public: nil, + } + + iface, err := base.Client.CreateInterface(context.Background(), 123, opts) + if err != nil { + t.Fatalf("Error fetching interfaces: %v", err) + } + + assert.Equal(t, 123, iface.ID) + assert.Equal(t, "auto", iface.Public.IPv4.Addresses[0].Address) +} + +func TestInterface_Update(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("interface_update") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPut("linode/instances/123/interfaces/123", fixtureData) + + opts := linodego.LinodeInterfaceUpdateOptions{ + DefaultRoute: &linodego.InterfaceDefaultRoute{ + IPv6: linodego.Pointer(true), + }, + } + + iface, err := base.Client.UpdateInterface(context.Background(), 123, 123, opts) + if err != nil { + t.Fatalf("Error fetching interfaces: %v", err) + } + + assert.Equal(t, 123, iface.ID) + assert.Equal(t, false, *iface.DefaultRoute.IPv4) + assert.Equal(t, true, *iface.DefaultRoute.IPv6) +} + +func TestInterface_Upgrade(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("interface_upgrade") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPost("linode/instances/123/upgrade-interfaces", fixtureData) + + opts := linodego.LinodeInterfacesUpgradeOptions{ + ConfigID: linodego.Pointer(123), + DryRun: linodego.Pointer(false), + } + + iface, err := base.Client.UpgradeInterfaces(context.Background(), 123, opts) + if err != nil { + t.Fatalf("Error fetching interfaces: %v", err) + } + + assert.Equal(t, 123, iface.ConfigID) + assert.Equal(t, false, iface.DryRun) + assert.Equal(t, 123, iface.Interfaces[0].ID) +} + +func TestInteface_ListFirewalls(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("firewall_list") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("linode/instances/123/interfaces/123/firewalls", fixtureData) + + firewalls, err := base.Client.ListInterfaceFirewalls(context.Background(), 123, 123, nil) + if err != nil { + t.Fatalf("Error fetching firewalls: %v", err) + } + + assert.Equal(t, 123, firewalls[0].ID) +} + +func TestInteface_GetSettings(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("interface_settings_get") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("linode/instances/123/interfaces/settings", fixtureData) + + settings, err := base.Client.GetInterfaceSettings(context.Background(), 123) + if err != nil { + t.Fatalf("Error fetching firewalls: %v", err) + } + + assert.Equal(t, false, settings.NetworkHelper) +} + +func TestInterface_UpdateSettings(t *testing.T) { + fixtures := NewTestFixtures() + + fixtureData, err := fixtures.GetFixture("interface_settings_update") + if err != nil { + t.Fatalf("Failed to load fixture: %v", err) + } + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockPut("linode/instances/123/interfaces/settings", fixtureData) + + opts := linodego.InterfaceSettingsUpdateOptions{ + NetworkHelper: linodego.Pointer(true), + } + + settings, err := base.Client.UpdateInterfaceSettings(context.Background(), 123, opts) + if err != nil { + t.Fatalf("Error fetching interfaces: %v", err) + } + + assert.Equal(t, true, settings.NetworkHelper) +} diff --git a/test/unit/network_ips_test.go b/test/unit/network_ips_test.go index 40e8f7e71..11042d4bd 100644 --- a/test/unit/network_ips_test.go +++ b/test/unit/network_ips_test.go @@ -53,6 +53,7 @@ func TestIPAllocateReserve(t *testing.T) { assert.Equal(t, "192.168.1.3", ip.Address, "Expected allocated IP address to match") assert.Equal(t, "us-east", ip.Region, "Expected Region to match") assert.True(t, ip.Public, "Expected Public to be true") + assert.Nil(t, ip.InterfaceID) } func TestIPAssignInstances(t *testing.T) { @@ -86,3 +87,61 @@ func TestIPShareAddresses(t *testing.T) { }) assert.NoError(t, err, "Expected no error when sharing IP addresses") } + +func TestIPAddresses_List(t *testing.T) { + fixtureData, err := fixtures.GetFixture("network_ip_addresses_list") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("networking/ips", fixtureData) + + ips, err := base.Client.ListIPAddresses(context.Background(), nil) + assert.NoError(t, err) + assert.NotEmpty(t, ips, "Expected ips to be returned.") + + ip := ips[0] + assert.Equal(t, "197.1O7.143.141", ip.Address) + assert.Equal(t, "197.1O7.143.1", ip.Gateway) + assert.Equal(t, 456, *ip.InterfaceID) + assert.Equal(t, 123, ip.LinodeID) + assert.Equal(t, 24, ip.Prefix) + assert.Equal(t, true, ip.Public) + assert.Equal(t, "test.example.org", ip.RDNS) + assert.Equal(t, "us-east", ip.Region) + assert.Equal(t, "192.0.2.139", ip.SubnetMask) + assert.Equal(t, linodego.InstanceIPType("ipv4"), ip.Type) + assert.Equal(t, "192.0.2.1", ip.VPCNAT1To1.Address) + assert.Equal(t, 101, ip.VPCNAT1To1.SubnetID) + assert.Equal(t, 111, ip.VPCNAT1To1.VPCID) +} + +func TestIPAddresses_Get(t *testing.T) { + fixtureData, err := fixtures.GetFixture("network_ip_address_get") + assert.NoError(t, err) + + var base ClientBaseCase + base.SetUp(t) + defer base.TearDown(t) + + base.MockGet("networking/ips/97.107.143.141", fixtureData) + + ip, err := base.Client.GetIPAddress(context.Background(), "97.107.143.141") + assert.NoError(t, err) + + assert.Equal(t, "97.107.143.141", ip.Address) + assert.Equal(t, "97.107.143.1", ip.Gateway) + assert.Equal(t, 456, *ip.InterfaceID) + assert.Equal(t, 123, ip.LinodeID) + assert.Equal(t, 24, ip.Prefix) + assert.Equal(t, true, ip.Public) + assert.Equal(t, "test.example.org", ip.RDNS) + assert.Equal(t, "us-east", ip.Region) + assert.Equal(t, "255.255.255.0", ip.SubnetMask) + assert.Equal(t, linodego.InstanceIPType("ipv4"), ip.Type) + assert.Equal(t, "192.168.0.42", ip.VPCNAT1To1.Address) + assert.Equal(t, 101, ip.VPCNAT1To1.SubnetID) + assert.Equal(t, 111, ip.VPCNAT1To1.VPCID) +} diff --git a/test/unit/vpc_subnets_test.go b/test/unit/vpc_subnets_test.go index 464819019..aa92e3274 100644 --- a/test/unit/vpc_subnets_test.go +++ b/test/unit/vpc_subnets_test.go @@ -43,11 +43,13 @@ func TestVPCSubnet_Get(t *testing.T) { subnet, err := base.Client.GetVPCSubnet(context.Background(), 123, 456) assert.NoError(t, err) - assert.Equal(t, 456, subnet.ID, "Expected subnet ID to match") - assert.Equal(t, "Existing Subnet", subnet.Label, "Expected subnet label to match") - assert.Equal(t, "192.168.2.0/24", subnet.IPv4, "Expected subnet IPv4 to match") - assert.Equal(t, 101, subnet.Linodes[0].ID, "Expected Linode ID to match") - assert.True(t, subnet.Linodes[0].Interfaces[0].Active, "Expected interface to be active") + assert.Equal(t, 456, subnet.ID) + assert.Equal(t, "10.0.1.0/24", subnet.IPv4) + assert.Equal(t, "cool-vpc-subnet", subnet.Label) + assert.Equal(t, 111, subnet.Linodes[0].ID) + assert.Equal(t, true, subnet.Linodes[0].Interfaces[0].Active) + assert.Equal(t, 4567, *subnet.Linodes[0].Interfaces[0].ConfigID) + assert.Equal(t, 421, subnet.Linodes[0].Interfaces[0].ID) } func TestVPCSubnets_List(t *testing.T) { @@ -61,12 +63,17 @@ func TestVPCSubnets_List(t *testing.T) { base.MockGet("vpcs/123/subnets", fixtureData) subnets, err := base.Client.ListVPCSubnets(context.Background(), 123, &linodego.ListOptions{}) + subnet := subnets[0] assert.NoError(t, err, "Expected no error when listing subnets") - assert.Len(t, subnets, 2, "Expected two subnets in the list") - - assert.Equal(t, 123, subnets[0].ID, "Expected first subnet ID to match") - assert.Equal(t, "Subnet A", subnets[0].Label, "Expected first subnet label to match") - assert.Equal(t, "192.168.3.0/24", subnets[0].IPv4, "Expected first subnet IPv4 to match") + assert.Len(t, subnets, 1, "Expected two subnets in the list") + + assert.Equal(t, 456, subnet.ID) + assert.Equal(t, "192.0.2.13/24", subnet.IPv4) + assert.Equal(t, "cool-vpc-subnet", subnet.Label) + assert.Equal(t, 111, subnet.Linodes[0].ID) + assert.Equal(t, true, subnet.Linodes[0].Interfaces[0].Active) + assert.Nil(t, subnet.Linodes[0].Interfaces[0].ConfigID) + assert.Equal(t, 421, subnet.Linodes[0].Interfaces[0].ID) } func TestVPCSubnet_Update(t *testing.T) { diff --git a/test/unit/vpc_test.go b/test/unit/vpc_test.go index f8b24c19f..8e27425ba 100644 --- a/test/unit/vpc_test.go +++ b/test/unit/vpc_test.go @@ -48,17 +48,23 @@ func TestVPC_Get(t *testing.T) { base.SetUp(t) defer base.TearDown(t) - mockVPC := linodego.VPC{ - ID: 123, - Label: "test-vpc", - } - base.MockGet("vpcs/123", mockVPC) + fixtureData, err := fixtures.GetFixture("vpc_get") + assert.NoError(t, err) + base.MockGet("vpcs/123", fixtureData) vpc, err := base.Client.GetVPC(context.Background(), 123) assert.NoError(t, err, "Expected no error when getting VPC") assert.NotNil(t, vpc, "Expected non-nil VPC") - assert.Equal(t, mockVPC.ID, vpc.ID, "Expected VPC ID to match") - assert.Equal(t, mockVPC.Label, vpc.Label, "Expected VPC label to match") + assert.Equal(t, "A description of my VPC.", vpc.Description) + assert.Equal(t, 123, vpc.ID) + assert.Equal(t, "cool-vpc", vpc.Label) + assert.Equal(t, 456, vpc.Subnets[0].ID) + assert.Equal(t, "10.0.1.0/24", vpc.Subnets[0].IPv4) + assert.Equal(t, "cool-vpc-subnet", vpc.Subnets[0].Label) + assert.Equal(t, 111, vpc.Subnets[0].Linodes[0].ID) + assert.Equal(t, true, vpc.Subnets[0].Linodes[0].Interfaces[0].Active) + assert.Equal(t, 4567, *vpc.Subnets[0].Linodes[0].Interfaces[0].ConfigID) + assert.Equal(t, 421, vpc.Subnets[0].Linodes[0].Interfaces[0].ID) } func TestVPC_List(t *testing.T) { @@ -76,14 +82,18 @@ func TestVPC_List(t *testing.T) { assert.NotEmpty(t, vpcs, "Expected non-empty VPC list") - assert.Equal(t, 123, vpcs[0].ID, "Expected VPC ID to match") - assert.Equal(t, "test-vpc", vpcs[0].Label, "Expected VPC label to match") - assert.Equal(t, "Test VPC description", vpcs[0].Description, "Expected VPC description to match") - assert.Equal(t, "us-east", vpcs[0].Region, "Expected VPC region to match") - assert.NotEmpty(t, vpcs[0].Subnets, "Expected VPC to have subnets") - assert.Equal(t, 456, vpcs[0].Subnets[0].ID, "Expected subnet ID to match") - assert.Equal(t, "subnet-1", vpcs[0].Subnets[0].Label, "Expected subnet label to match") - assert.Equal(t, "192.168.1.0/24", vpcs[0].Subnets[0].IPv4, "Expected subnet IPv4 to match") + vpc := vpcs[0] + + assert.Equal(t, "A description of my VPC.", vpc.Description) + assert.Equal(t, 123, vpc.ID) + assert.Equal(t, "cool-vpc", vpc.Label) + assert.Equal(t, 456, vpc.Subnets[0].ID) + assert.Equal(t, "192.0.2.210/24", vpc.Subnets[0].IPv4) + assert.Equal(t, "cool-vpc-subnet", vpc.Subnets[0].Label) + assert.Equal(t, 111, vpc.Subnets[0].Linodes[0].ID) + assert.Equal(t, true, vpc.Subnets[0].Linodes[0].Interfaces[0].Active) + assert.Nil(t, vpc.Subnets[0].Linodes[0].Interfaces[0].ConfigID) + assert.Equal(t, 421, vpc.Subnets[0].Linodes[0].Interfaces[0].ID) } func TestVPC_Update(t *testing.T) { diff --git a/vpc_subnet.go b/vpc_subnet.go index 60a8554fc..65b080625 100644 --- a/vpc_subnet.go +++ b/vpc_subnet.go @@ -11,8 +11,9 @@ import ( // VPCSubnetLinodeInterface represents an interface on a Linode that is currently // assigned to this VPC subnet. type VPCSubnetLinodeInterface struct { - ID int `json:"id"` - Active bool `json:"active"` + ID int `json:"id"` + Active bool `json:"active"` + ConfigID *int `json:"config_id"` } // VPCSubnetLinode represents a Linode currently assigned to a VPC subnet.