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/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/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) +}