From 0e3d4cb95fbe0291cd450e7bed8fd95c7cb071d6 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Wed, 15 Jun 2022 16:36:46 -0400 Subject: [PATCH] Deprecate floating-ip commands in favor of reserved-ip. (#1169) * Replace upstream godo with fork temporarily * Add reserved IPs services. * Deprecate floating-ip commands in favor of reserved-ip. * Vendor godo v1.81.0 --- commands/command_config.go | 8 +- commands/commands_test.go | 18 +- .../{floating_ip.go => reserved_ip.go} | 20 +-- commands/doit.go | 4 +- commands/floating_ips.go | 169 ------------------ commands/projects.go | 4 +- ...g_ip_actions.go => reserved_ip_actions.go} | 48 ++--- ...ns_test.go => reserved_ip_actions_test.go} | 22 +-- commands/reserved_ips.go | 168 +++++++++++++++++ ...ating_ips_test.go => reserved_ips_test.go} | 46 ++--- do/floating_ips.go | 101 ----------- ...Service.go => ReservedIPActionsService.go} | 48 ++--- ...ingIPsService.go => ReservedIPsService.go} | 54 +++--- ...g_ip_actions.go => reserved_ip_actions.go} | 32 ++-- do/reserved_ips.go | 101 +++++++++++ go.mod | 2 +- go.sum | 4 +- .../compute_floating_ip_action_test.go | 6 +- .../compute_reserved_ip_action_test.go | 168 +++++++++++++++++ integration/floating_ip_create_test.go | 4 +- integration/floating_ip_delete_test.go | 2 +- integration/floating_ip_get_test.go | 4 +- integration/floating_ip_list_test.go | 4 +- integration/projects_resources_get_test.go | 21 ++- integration/reserved_ip_create_test.go | 117 ++++++++++++ integration/reserved_ip_delete_test.go | 72 ++++++++ integration/reserved_ip_get_test.go | 96 ++++++++++ integration/reserved_ip_list_test.go | 114 ++++++++++++ scripts/regenmocks.sh | 4 +- .../github.com/digitalocean/godo/CHANGELOG.md | 8 + .../github.com/digitalocean/godo/account.go | 24 ++- .../github.com/digitalocean/godo/apps.gen.go | 28 ++- .../github.com/digitalocean/godo/databases.go | 2 + vendor/github.com/digitalocean/godo/godo.go | 6 +- .../digitalocean/godo/reserved_ips.go | 145 +++++++++++++++ .../digitalocean/godo/reserved_ips_actions.go | 109 +++++++++++ vendor/modules.txt | 2 +- 37 files changed, 1328 insertions(+), 457 deletions(-) rename commands/displayers/{floating_ip.go => reserved_ip.go} (73%) delete mode 100644 commands/floating_ips.go rename commands/{floating_ip_actions.go => reserved_ip_actions.go} (61%) rename commands/{floating_ip_actions_test.go => reserved_ip_actions_test.go} (69%) create mode 100644 commands/reserved_ips.go rename commands/{floating_ips_test.go => reserved_ips_test.go} (62%) delete mode 100644 do/floating_ips.go rename do/mocks/{FloatingIPActionsService.go => ReservedIPActionsService.go} (56%) rename do/mocks/{FloatingIPsService.go => ReservedIPsService.go} (50%) rename do/{floating_ip_actions.go => reserved_ip_actions.go} (64%) create mode 100644 do/reserved_ips.go create mode 100644 integration/compute_reserved_ip_action_test.go create mode 100644 integration/reserved_ip_create_test.go create mode 100644 integration/reserved_ip_delete_test.go create mode 100644 integration/reserved_ip_get_test.go create mode 100644 integration/reserved_ip_list_test.go create mode 100644 vendor/github.com/digitalocean/godo/reserved_ips.go create mode 100644 vendor/github.com/digitalocean/godo/reserved_ips_actions.go diff --git a/commands/command_config.go b/commands/command_config.go index 6e56cc458..f647d6376 100644 --- a/commands/command_config.go +++ b/commands/command_config.go @@ -46,8 +46,8 @@ type CmdConfig struct { Images func() do.ImagesService ImageActions func() do.ImageActionsService LoadBalancers func() do.LoadBalancersService - FloatingIPs func() do.FloatingIPsService - FloatingIPActions func() do.FloatingIPActionsService + ReservedIPs func() do.ReservedIPsService + ReservedIPActions func() do.ReservedIPActionsService Droplets func() do.DropletsService DropletActions func() do.DropletActionsService Domains func() do.DomainsService @@ -95,8 +95,8 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init c.Regions = func() do.RegionsService { return do.NewRegionsService(godoClient) } c.Images = func() do.ImagesService { return do.NewImagesService(godoClient) } c.ImageActions = func() do.ImageActionsService { return do.NewImageActionsService(godoClient) } - c.FloatingIPs = func() do.FloatingIPsService { return do.NewFloatingIPsService(godoClient) } - c.FloatingIPActions = func() do.FloatingIPActionsService { return do.NewFloatingIPActionsService(godoClient) } + c.ReservedIPs = func() do.ReservedIPsService { return do.NewReservedIPsService(godoClient) } + c.ReservedIPActions = func() do.ReservedIPActionsService { return do.NewReservedIPActionsService(godoClient) } c.Droplets = func() do.DropletsService { return do.NewDropletsService(godoClient) } c.DropletActions = func() do.DropletActionsService { return do.NewDropletActionsService(godoClient) } c.Domains = func() do.DomainsService { return do.NewDomainsService(godoClient) } diff --git a/commands/commands_test.go b/commands/commands_test.go index 553f9ca9e..8eefd9cb6 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -97,14 +97,14 @@ var ( testPrivateDropletList = do.Droplets{testPrivateDroplet} testKernel = do.Kernel{Kernel: &godo.Kernel{ID: 1}} testKernelList = do.Kernels{testKernel} - testFloatingIP = do.FloatingIP{ - FloatingIP: &godo.FloatingIP{ + testReservedIP = do.ReservedIP{ + ReservedIP: &godo.ReservedIP{ Droplet: testDroplet.Droplet, Region: testDroplet.Region, IP: "127.0.0.1", }, } - testFloatingIPList = do.FloatingIPs{testFloatingIP} + testReservedIPList = do.ReservedIPs{testReservedIP} testSnapshot = do.Snapshot{ Snapshot: &godo.Snapshot{ @@ -160,8 +160,8 @@ type tcMocks struct { images *domocks.MockImagesService imageActions *domocks.MockImageActionsService invoices *domocks.MockInvoicesService - floatingIPs *domocks.MockFloatingIPsService - floatingIPActions *domocks.MockFloatingIPActionsService + reservedIPs *domocks.MockReservedIPsService + reservedIPActions *domocks.MockReservedIPActionsService domains *domocks.MockDomainsService volumes *domocks.MockVolumesService volumeActions *domocks.MockVolumeActionsService @@ -198,8 +198,8 @@ func withTestClient(t *testing.T, tFn testFn) { images: domocks.NewMockImagesService(ctrl), imageActions: domocks.NewMockImageActionsService(ctrl), invoices: domocks.NewMockInvoicesService(ctrl), - floatingIPs: domocks.NewMockFloatingIPsService(ctrl), - floatingIPActions: domocks.NewMockFloatingIPActionsService(ctrl), + reservedIPs: domocks.NewMockReservedIPsService(ctrl), + reservedIPActions: domocks.NewMockReservedIPActionsService(ctrl), droplets: domocks.NewMockDropletsService(ctrl), dropletActions: domocks.NewMockDropletActionsService(ctrl), domains: domocks.NewMockDomainsService(ctrl), @@ -246,8 +246,8 @@ func withTestClient(t *testing.T, tFn testFn) { Regions: func() do.RegionsService { return tm.regions }, Images: func() do.ImagesService { return tm.images }, ImageActions: func() do.ImageActionsService { return tm.imageActions }, - FloatingIPs: func() do.FloatingIPsService { return tm.floatingIPs }, - FloatingIPActions: func() do.FloatingIPActionsService { return tm.floatingIPActions }, + ReservedIPs: func() do.ReservedIPsService { return tm.reservedIPs }, + ReservedIPActions: func() do.ReservedIPActionsService { return tm.reservedIPActions }, Droplets: func() do.DropletsService { return tm.droplets }, DropletActions: func() do.DropletActionsService { return tm.dropletActions }, Domains: func() do.DomainsService { return tm.domains }, diff --git a/commands/displayers/floating_ip.go b/commands/displayers/reserved_ip.go similarity index 73% rename from commands/displayers/floating_ip.go rename to commands/displayers/reserved_ip.go index 943636996..241a4980a 100644 --- a/commands/displayers/floating_ip.go +++ b/commands/displayers/reserved_ip.go @@ -20,32 +20,32 @@ import ( "github.com/digitalocean/doctl/do" ) -type FloatingIP struct { - FloatingIPs do.FloatingIPs +type ReservedIP struct { + ReservedIPs do.ReservedIPs } -var _ Displayable = &FloatingIP{} +var _ Displayable = &ReservedIP{} -func (fi *FloatingIP) JSON(out io.Writer) error { - return writeJSON(fi.FloatingIPs, out) +func (rip *ReservedIP) JSON(out io.Writer) error { + return writeJSON(rip.ReservedIPs, out) } -func (fi *FloatingIP) Cols() []string { +func (rip *ReservedIP) Cols() []string { return []string{ "IP", "Region", "DropletID", "DropletName", } } -func (fi *FloatingIP) ColMap() map[string]string { +func (rip *ReservedIP) ColMap() map[string]string { return map[string]string{ "IP": "IP", "Region": "Region", "DropletID": "Droplet ID", "DropletName": "Droplet Name", } } -func (fi *FloatingIP) KV() []map[string]interface{} { - out := make([]map[string]interface{}, 0, len(fi.FloatingIPs)) +func (rip *ReservedIP) KV() []map[string]interface{} { + out := make([]map[string]interface{}, 0, len(rip.ReservedIPs)) - for _, f := range fi.FloatingIPs { + for _, f := range rip.ReservedIPs { var dropletID, dropletName string if f.Droplet != nil { dropletID = fmt.Sprintf("%d", f.Droplet.ID) diff --git a/commands/doit.go b/commands/doit.go index 22112644b..875ed2fd2 100644 --- a/commands/doit.go +++ b/commands/doit.go @@ -168,8 +168,8 @@ func computeCmd() *Command { cmd.AddCommand(Droplet()) cmd.AddCommand(Domain()) cmd.AddCommand(Firewall()) - cmd.AddCommand(FloatingIP()) - cmd.AddCommand(FloatingIPAction()) + cmd.AddCommand(ReservedIP()) + cmd.AddCommand(ReservedIPAction()) cmd.AddCommand(Images()) cmd.AddCommand(ImageAction()) cmd.AddCommand(LoadBalancer()) diff --git a/commands/floating_ips.go b/commands/floating_ips.go deleted file mode 100644 index c184d6924..000000000 --- a/commands/floating_ips.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright 2018 The Doctl Authors All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package commands - -import ( - "errors" - "fmt" - - "github.com/digitalocean/doctl" - "github.com/digitalocean/doctl/commands/displayers" - "github.com/digitalocean/doctl/do" - "github.com/digitalocean/godo" - "github.com/spf13/cobra" -) - -// FloatingIP creates the command hierarchy for floating ips. -func FloatingIP() *Command { - cmd := &Command{ - Command: &cobra.Command{ - Use: "floating-ip", - Short: "Display commands to manage floating IP addresses", - Long: `The sub-commands of ` + "`" + `doctl compute floating-ip` + "`" + ` manage floating IP addresses. -Floating IPs are publicly-accessible static IP addresses that can be mapped to one of your Droplets. They can be used to create highly available setups or other configurations requiring movable addresses. -Floating IPs are bound to a specific region.`, - Aliases: []string{"fip"}, - }, - } - - cmdFloatingIPCreate := CmdBuilder(cmd, RunFloatingIPCreate, "create", "Create a new floating IP address", `Use this command to create a new floating IP address. - -A floating IP address must be either assigned to a Droplet or reserved to a region.`, Writer, - aliasOpt("c"), displayerType(&displayers.FloatingIP{})) - AddStringFlag(cmdFloatingIPCreate, doctl.ArgRegionSlug, "", "", - fmt.Sprintf("Region where to create the floating IP address. (mutually exclusive with `--%s`)", - doctl.ArgDropletID)) - AddIntFlag(cmdFloatingIPCreate, doctl.ArgDropletID, "", 0, - fmt.Sprintf("The ID of the Droplet to assign the floating IP to (mutually exclusive with `--%s`).", - doctl.ArgRegionSlug)) - - CmdBuilder(cmd, RunFloatingIPGet, "get ", "Retrieve information about a floating IP address", "Use this command to retrieve detailed information about a floating IP address.", Writer, - aliasOpt("g"), displayerType(&displayers.FloatingIP{})) - - cmdRunFloatingIPDelete := CmdBuilder(cmd, RunFloatingIPDelete, "delete ", "Permanently delete a floating IP address", "Use this command to permanently delete a floating IP address. This is irreversible.", Writer, aliasOpt("d", "rm")) - AddBoolFlag(cmdRunFloatingIPDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Force floating IP delete") - - cmdFloatingIPList := CmdBuilder(cmd, RunFloatingIPList, "list", "List all floating IP addresses on your account", "Use this command to list all the floating IP addresses on your account.", Writer, - aliasOpt("ls"), displayerType(&displayers.FloatingIP{})) - AddStringFlag(cmdFloatingIPList, doctl.ArgRegionSlug, "", "", "The region the floating IP address resides in") - - return cmd -} - -// RunFloatingIPCreate runs floating IP create. -func RunFloatingIPCreate(c *CmdConfig) error { - fis := c.FloatingIPs() - - // ignore errors since we don't know which one is valid - region, _ := c.Doit.GetString(c.NS, doctl.ArgRegionSlug) - dropletID, _ := c.Doit.GetInt(c.NS, doctl.ArgDropletID) - - if region == "" && dropletID == 0 { - return doctl.NewMissingArgsErr("Region and Droplet ID can't both be blank.") - } - - if region != "" && dropletID != 0 { - return fmt.Errorf("Specify region or Droplet ID when creating a floating IP address.") - } - - req := &godo.FloatingIPCreateRequest{ - Region: region, - DropletID: dropletID, - } - - ip, err := fis.Create(req) - if err != nil { - fmt.Println(err) - return err - } - - item := &displayers.FloatingIP{FloatingIPs: do.FloatingIPs{*ip}} - return c.Display(item) -} - -// RunFloatingIPGet retrieves a floating IP's details. -func RunFloatingIPGet(c *CmdConfig) error { - fis := c.FloatingIPs() - - err := ensureOneArg(c) - if err != nil { - return err - } - - ip := c.Args[0] - - if len(ip) < 1 { - return errors.New("Invalid IP address") - } - - fip, err := fis.Get(ip) - if err != nil { - return err - } - - item := &displayers.FloatingIP{FloatingIPs: do.FloatingIPs{*fip}} - return c.Display(item) -} - -// RunFloatingIPDelete runs floating IP delete. -func RunFloatingIPDelete(c *CmdConfig) error { - fis := c.FloatingIPs() - - err := ensureOneArg(c) - if err != nil { - return err - } - - force, err := c.Doit.GetBool(c.NS, doctl.ArgForce) - if err != nil { - return err - } - - if force || AskForConfirmDelete("floating IP", 1) == nil { - ip := c.Args[0] - return fis.Delete(ip) - } - - return errOperationAborted -} - -// RunFloatingIPList runs floating IP create. -func RunFloatingIPList(c *CmdConfig) error { - fis := c.FloatingIPs() - - region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug) - if err != nil { - return err - } - - list, err := fis.List() - if err != nil { - return err - } - - fips := &displayers.FloatingIP{FloatingIPs: do.FloatingIPs{}} - for _, fip := range list { - var skip bool - if region != "" && region != fip.Region.Slug { - skip = true - } - - if !skip { - fips.FloatingIPs = append(fips.FloatingIPs, fip) - } - } - - item := fips - return c.Display(item) -} diff --git a/commands/projects.go b/commands/projects.go index 595ccde66..7a3e0eeee 100644 --- a/commands/projects.go +++ b/commands/projects.go @@ -259,7 +259,9 @@ func RunProjectResourcesGet(c *CmdConfig) error { case "droplet": return RunDropletGet(c) case "floatingip": - return RunFloatingIPGet(c) + return RunReservedIPGet(c) + case "reservedip": + return RunReservedIPGet(c) case "loadbalancer": return RunLoadBalancerGet(c) case "domain": diff --git a/commands/floating_ip_actions.go b/commands/reserved_ip_actions.go similarity index 61% rename from commands/floating_ip_actions.go rename to commands/reserved_ip_actions.go index d2f3184e4..b5b2350de 100644 --- a/commands/floating_ip_actions.go +++ b/commands/reserved_ip_actions.go @@ -23,20 +23,20 @@ import ( "github.com/spf13/cobra" ) -// FloatingIPAction creates the floating IP action command. -func FloatingIPAction() *Command { +// ReservedIPAction creates the reserved IP action command. +func ReservedIPAction() *Command { cmd := &Command{ Command: &cobra.Command{ - Use: "floating-ip-action", - Short: "Display commands to associate floating IP addresses with Droplets", - Long: "Floating IP actions are commands that are used to manage DigitalOcean floating IP addresses.", - Aliases: []string{"fipa"}, + Use: "reserved-ip-action", + Short: "Display commands to associate reserved IP addresses with Droplets", + Long: "Reserved IP actions are commands that are used to manage DigitalOcean reserved IP addresses.", + Aliases: []string{"fipa", "floating-ip-action", "floating-ip-actions", "reserved-ip-actions"}, }, } - flipactionDetail := ` + flipActionDetail := ` - - The unique numeric ID used to identify and reference a floating IP action. - - The status of the floating IP action. This will be either "in-progress", "completed", or "errored". + - The unique numeric ID used to identify and reference a reserved IP action. + - The status of the reserved IP action. This will be either "in-progress", "completed", or "errored". - A time value given in ISO8601 combined date and time format that represents when the action was initiated. - A time value given in ISO8601 combined date and time format that represents when the action was completed. - The resource ID, which is a unique identifier for the resource that the action is associated with. @@ -44,30 +44,30 @@ func FloatingIPAction() *Command { - The region where the action occurred. - The slug for the region where the action occurred. ` - CmdBuilder(cmd, RunFloatingIPActionsGet, - "get ", "Retrieve the status of a floating IP action", `Use this command to retrieve the status of a floating IP action. Outputs the following information:`+flipactionDetail, Writer, + CmdBuilder(cmd, RunReservedIPActionsGet, + "get ", "Retrieve the status of a reserved IP action", `Use this command to retrieve the status of a reserved IP action. Outputs the following information:`+flipActionDetail, Writer, displayerType(&displayers.Action{})) - CmdBuilder(cmd, RunFloatingIPActionsAssign, - "assign ", "Assign a floating IP address to a Droplet", "Use this command to assign a floating IP address to a Droplet by specifying the `droplet_id`.", Writer, + CmdBuilder(cmd, RunReservedIPActionsAssign, + "assign ", "Assign a reserved IP address to a Droplet", "Use this command to assign a reserved IP address to a Droplet by specifying the `droplet_id`.", Writer, displayerType(&displayers.Action{})) - CmdBuilder(cmd, RunFloatingIPActionsUnassign, - "unassign ", "Unassign a floating IP address from a Droplet", `Use this command to unassign a floating IP address from a Droplet. The floating IP address will be reserved in the region but not assigned to a Droplet.`, Writer, + CmdBuilder(cmd, RunReservedIPActionsUnassign, + "unassign ", "Unassign a reserved IP address from a Droplet", `Use this command to unassign a reserved IP address from a Droplet. The reserved IP address will be reserved in the region but not assigned to a Droplet.`, Writer, displayerType(&displayers.Action{})) return cmd } -// RunFloatingIPActionsGet retrieves an action for a floating IP. -func RunFloatingIPActionsGet(c *CmdConfig) error { +// RunReservedIPActionsGet retrieves an action for a reserved IP. +func RunReservedIPActionsGet(c *CmdConfig) error { if len(c.Args) != 2 { return doctl.NewMissingArgsErr(c.NS) } ip := c.Args[0] - fia := c.FloatingIPActions() + fia := c.ReservedIPActions() actionID, err := strconv.Atoi(c.Args[1]) if err != nil { @@ -83,15 +83,15 @@ func RunFloatingIPActionsGet(c *CmdConfig) error { return c.Display(item) } -// RunFloatingIPActionsAssign assigns a floating IP to a droplet. -func RunFloatingIPActionsAssign(c *CmdConfig) error { +// RunReservedIPActionsAssign assigns a reserved IP to a droplet. +func RunReservedIPActionsAssign(c *CmdConfig) error { if len(c.Args) != 2 { return doctl.NewMissingArgsErr(c.NS) } ip := c.Args[0] - fia := c.FloatingIPActions() + fia := c.ReservedIPActions() dropletID, err := strconv.Atoi(c.Args[1]) if err != nil { @@ -107,8 +107,8 @@ func RunFloatingIPActionsAssign(c *CmdConfig) error { return c.Display(item) } -// RunFloatingIPActionsUnassign unassigns a floating IP to a droplet. -func RunFloatingIPActionsUnassign(c *CmdConfig) error { +// RunReservedIPActionsUnassign unassigns a reserved IP to a droplet. +func RunReservedIPActionsUnassign(c *CmdConfig) error { err := ensureOneArg(c) if err != nil { return err @@ -116,7 +116,7 @@ func RunFloatingIPActionsUnassign(c *CmdConfig) error { ip := c.Args[0] - fia := c.FloatingIPActions() + fia := c.ReservedIPActions() a, err := fia.Unassign(ip) if err != nil { diff --git a/commands/floating_ip_actions_test.go b/commands/reserved_ip_actions_test.go similarity index 69% rename from commands/floating_ip_actions_test.go rename to commands/reserved_ip_actions_test.go index 1ae3c4cab..f0b7c6db1 100644 --- a/commands/floating_ip_actions_test.go +++ b/commands/reserved_ip_actions_test.go @@ -19,42 +19,42 @@ import ( "github.com/stretchr/testify/assert" ) -func TestFloatingIPActionCommand(t *testing.T) { - cmd := FloatingIPAction() +func TestReservedIPActionCommand(t *testing.T) { + cmd := ReservedIPAction() assert.NotNil(t, cmd) assertCommandNames(t, cmd, "assign", "get", "unassign") } -func TestFloatingIPActionsGet(t *testing.T) { +func TestReservedIPActionsGet(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - tm.floatingIPActions.EXPECT().Get("127.0.0.1", 2).Return(&testAction, nil) + tm.reservedIPActions.EXPECT().Get("127.0.0.1", 2).Return(&testAction, nil) config.Args = append(config.Args, "127.0.0.1", "2") - err := RunFloatingIPActionsGet(config) + err := RunReservedIPActionsGet(config) assert.NoError(t, err) }) } -func TestFloatingIPActionsAssign(t *testing.T) { +func TestReservedIPActionsAssign(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - tm.floatingIPActions.EXPECT().Assign("127.0.0.1", 2).Return(&testAction, nil) + tm.reservedIPActions.EXPECT().Assign("127.0.0.1", 2).Return(&testAction, nil) config.Args = append(config.Args, "127.0.0.1", "2") - err := RunFloatingIPActionsAssign(config) + err := RunReservedIPActionsAssign(config) assert.NoError(t, err) }) } -func TestFloatingIPActionsUnassign(t *testing.T) { +func TestReservedIPActionsUnassign(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - tm.floatingIPActions.EXPECT().Unassign("127.0.0.1").Return(&testAction, nil) + tm.reservedIPActions.EXPECT().Unassign("127.0.0.1").Return(&testAction, nil) config.Args = append(config.Args, "127.0.0.1") - err := RunFloatingIPActionsUnassign(config) + err := RunReservedIPActionsUnassign(config) assert.NoError(t, err) }) } diff --git a/commands/reserved_ips.go b/commands/reserved_ips.go new file mode 100644 index 000000000..f12eb63ab --- /dev/null +++ b/commands/reserved_ips.go @@ -0,0 +1,168 @@ +/* +Copyright 2018 The Doctl Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "errors" + "fmt" + + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/commands/displayers" + "github.com/digitalocean/doctl/do" + "github.com/digitalocean/godo" + "github.com/spf13/cobra" +) + +// ReservedIP creates the command hierarchy for reserved ips. +func ReservedIP() *Command { + cmd := &Command{ + Command: &cobra.Command{ + Use: "reserved-ip", + Short: "Display commands to manage reserved IP addresses", + Long: `The sub-commands of ` + "`" + `doctl compute reserved-ip` + "`" + ` manage reserved IP addresses. +Reserved IPs are publicly-accessible static IP addresses that can be mapped to one of your Droplets. They can be used to create highly available setups or other configurations requiring movable addresses. Reserved IPs are bound to a specific region.`, + Aliases: []string{"fip", "floating-ip", "floating-ips", "reserved-ips"}, + }, + } + + cmdReservedIPCreate := CmdBuilder(cmd, RunReservedIPCreate, "create", "Create a new reserved IP address", `Use this command to create a new reserved IP address. + +A reserved IP address must be either assigned to a Droplet or reserved to a region.`, Writer, + aliasOpt("c"), displayerType(&displayers.ReservedIP{})) + AddStringFlag(cmdReservedIPCreate, doctl.ArgRegionSlug, "", "", + fmt.Sprintf("Region where to create the reserved IP address. (mutually exclusive with `--%s`)", + doctl.ArgDropletID)) + AddIntFlag(cmdReservedIPCreate, doctl.ArgDropletID, "", 0, + fmt.Sprintf("The ID of the Droplet to assign the reserved IP to (mutually exclusive with `--%s`).", + doctl.ArgRegionSlug)) + + CmdBuilder(cmd, RunReservedIPGet, "get ", "Retrieve information about a reserved IP address", "Use this command to retrieve detailed information about a reserved IP address.", Writer, + aliasOpt("g"), displayerType(&displayers.ReservedIP{})) + + cmdRunReservedIPDelete := CmdBuilder(cmd, RunReservedIPDelete, "delete ", "Permanently delete a reserved IP address", "Use this command to permanently delete a reserved IP address. This is irreversible.", Writer, aliasOpt("d", "rm")) + AddBoolFlag(cmdRunReservedIPDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Force reserved IP delete") + + cmdReservedIPList := CmdBuilder(cmd, RunReservedIPList, "list", "List all reserved IP addresses on your account", "Use this command to list all the reserved IP addresses on your account.", Writer, + aliasOpt("ls"), displayerType(&displayers.ReservedIP{})) + AddStringFlag(cmdReservedIPList, doctl.ArgRegionSlug, "", "", "The region the reserved IP address resides in") + + return cmd +} + +// RunReservedIPCreate runs reserved IP create. +func RunReservedIPCreate(c *CmdConfig) error { + ris := c.ReservedIPs() + + // ignore errors since we don't know which one is valid + region, _ := c.Doit.GetString(c.NS, doctl.ArgRegionSlug) + dropletID, _ := c.Doit.GetInt(c.NS, doctl.ArgDropletID) + + if region == "" && dropletID == 0 { + return doctl.NewMissingArgsErr("Region and Droplet ID can't both be blank.") + } + + if region != "" && dropletID != 0 { + return fmt.Errorf("Specify region or Droplet ID when creating a reserved IP address.") + } + + req := &godo.ReservedIPCreateRequest{ + Region: region, + DropletID: dropletID, + } + + ip, err := ris.Create(req) + if err != nil { + fmt.Println(err) + return err + } + + item := &displayers.ReservedIP{ReservedIPs: do.ReservedIPs{*ip}} + return c.Display(item) +} + +// RunReservedIPGet retrieves a reserved IP's details. +func RunReservedIPGet(c *CmdConfig) error { + ris := c.ReservedIPs() + + err := ensureOneArg(c) + if err != nil { + return err + } + + ip := c.Args[0] + + if len(ip) < 1 { + return errors.New("Invalid IP address") + } + + rip, err := ris.Get(ip) + if err != nil { + return err + } + + item := &displayers.ReservedIP{ReservedIPs: do.ReservedIPs{*rip}} + return c.Display(item) +} + +// RunReservedIPDelete runs reserved IP delete. +func RunReservedIPDelete(c *CmdConfig) error { + ris := c.ReservedIPs() + + err := ensureOneArg(c) + if err != nil { + return err + } + + force, err := c.Doit.GetBool(c.NS, doctl.ArgForce) + if err != nil { + return err + } + + if force || AskForConfirmDelete("reserved IP", 1) == nil { + ip := c.Args[0] + return ris.Delete(ip) + } + + return errOperationAborted +} + +// RunReservedIPList runs reserved IP create. +func RunReservedIPList(c *CmdConfig) error { + ris := c.ReservedIPs() + + region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug) + if err != nil { + return err + } + + list, err := ris.List() + if err != nil { + return err + } + + rips := &displayers.ReservedIP{ReservedIPs: do.ReservedIPs{}} + for _, rip := range list { + var skip bool + if region != "" && region != rip.Region.Slug { + skip = true + } + + if !skip { + rips.ReservedIPs = append(rips.ReservedIPs, rip) + } + } + + item := rips + return c.Display(item) +} diff --git a/commands/floating_ips_test.go b/commands/reserved_ips_test.go similarity index 62% rename from commands/floating_ips_test.go rename to commands/reserved_ips_test.go index c2fb9892f..4204f762f 100644 --- a/commands/floating_ips_test.go +++ b/commands/reserved_ips_test.go @@ -21,79 +21,79 @@ import ( "github.com/stretchr/testify/assert" ) -func TestFloatingIPCommands(t *testing.T) { - cmd := FloatingIP() +func TestReservedIPCommands(t *testing.T) { + cmd := ReservedIP() assert.NotNil(t, cmd) assertCommandNames(t, cmd, "create", "delete", "get", "list") } -func TestFloatingIPsList(t *testing.T) { +func TestReservedIPsList(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - tm.floatingIPs.EXPECT().List().Return(testFloatingIPList, nil) + tm.reservedIPs.EXPECT().List().Return(testReservedIPList, nil) - RunFloatingIPList(config) + RunReservedIPList(config) }) } -func TestFloatingIPsGet(t *testing.T) { +func TestReservedIPsGet(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - tm.floatingIPs.EXPECT().Get("127.0.0.1").Return(&testFloatingIP, nil) + tm.reservedIPs.EXPECT().Get("127.0.0.1").Return(&testReservedIP, nil) config.Args = append(config.Args, "127.0.0.1") - RunFloatingIPGet(config) + RunReservedIPGet(config) }) } -func TestFloatingIPsCreate_Droplet(t *testing.T) { +func TestReservedIPsCreate_Droplet(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - ficr := &godo.FloatingIPCreateRequest{DropletID: 1} - tm.floatingIPs.EXPECT().Create(ficr).Return(&testFloatingIP, nil) + ficr := &godo.ReservedIPCreateRequest{DropletID: 1} + tm.reservedIPs.EXPECT().Create(ficr).Return(&testReservedIP, nil) config.Doit.Set(config.NS, doctl.ArgDropletID, 1) - err := RunFloatingIPCreate(config) + err := RunReservedIPCreate(config) assert.NoError(t, err) }) } -func TestFloatingIPsCreate_Region(t *testing.T) { +func TestReservedIPsCreate_Region(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - ficr := &godo.FloatingIPCreateRequest{Region: "dev0"} - tm.floatingIPs.EXPECT().Create(ficr).Return(&testFloatingIP, nil) + ficr := &godo.ReservedIPCreateRequest{Region: "dev0"} + tm.reservedIPs.EXPECT().Create(ficr).Return(&testReservedIP, nil) config.Doit.Set(config.NS, doctl.ArgRegionSlug, "dev0") - err := RunFloatingIPCreate(config) + err := RunReservedIPCreate(config) assert.NoError(t, err) }) } -func TestFloatingIPsCreate_fail_with_no_args(t *testing.T) { +func TestReservedIPsCreate_fail_with_no_args(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - err := RunFloatingIPCreate(config) + err := RunReservedIPCreate(config) assert.Error(t, err) }) } -func TestFloatingIPsCreate_fail_with_both_args(t *testing.T) { +func TestReservedIPsCreate_fail_with_both_args(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { config.Doit.Set(config.NS, doctl.ArgDropletID, 1) config.Doit.Set(config.NS, doctl.ArgRegionSlug, "dev0") - err := RunFloatingIPCreate(config) + err := RunReservedIPCreate(config) assert.Error(t, err) }) } -func TestFloatingIPsDelete(t *testing.T) { +func TestReservedIPsDelete(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { - tm.floatingIPs.EXPECT().Delete("127.0.0.1").Return(nil) + tm.reservedIPs.EXPECT().Delete("127.0.0.1").Return(nil) config.Args = append(config.Args, "127.0.0.1") config.Doit.Set(config.NS, doctl.ArgForce, true) - RunFloatingIPDelete(config) + RunReservedIPDelete(config) }) } diff --git a/do/floating_ips.go b/do/floating_ips.go deleted file mode 100644 index 71157f449..000000000 --- a/do/floating_ips.go +++ /dev/null @@ -1,101 +0,0 @@ -/* -Copyright 2018 The Doctl Authors All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package do - -import ( - "context" - - "github.com/digitalocean/godo" -) - -// FloatingIP wraps a godo FloatingIP. -type FloatingIP struct { - *godo.FloatingIP -} - -// FloatingIPs is a slice of FloatingIP. -type FloatingIPs []FloatingIP - -// FloatingIPsService is the godo FloatingIPsService interface. -type FloatingIPsService interface { - List() (FloatingIPs, error) - Get(ip string) (*FloatingIP, error) - Create(ficr *godo.FloatingIPCreateRequest) (*FloatingIP, error) - Delete(ip string) error -} - -type floatingIPsService struct { - client *godo.Client -} - -var _ FloatingIPsService = &floatingIPsService{} - -// NewFloatingIPsService builds an instance of FloatingIPsService. -func NewFloatingIPsService(client *godo.Client) FloatingIPsService { - return &floatingIPsService{ - client: client, - } -} - -func (fis *floatingIPsService) List() (FloatingIPs, error) { - f := func(opt *godo.ListOptions) ([]interface{}, *godo.Response, error) { - list, resp, err := fis.client.FloatingIPs.List(context.TODO(), opt) - if err != nil { - return nil, nil, err - } - - si := make([]interface{}, len(list)) - for i := range list { - si[i] = list[i] - } - - return si, resp, err - } - - si, err := PaginateResp(f) - if err != nil { - return nil, err - } - - list := make(FloatingIPs, 0, len(si)) - for _, x := range si { - fip := x.(godo.FloatingIP) - list = append(list, FloatingIP{FloatingIP: &fip}) - } - - return list, nil -} - -func (fis *floatingIPsService) Get(ip string) (*FloatingIP, error) { - fip, _, err := fis.client.FloatingIPs.Get(context.TODO(), ip) - if err != nil { - return nil, err - } - - return &FloatingIP{FloatingIP: fip}, nil -} - -func (fis *floatingIPsService) Create(ficr *godo.FloatingIPCreateRequest) (*FloatingIP, error) { - fip, _, err := fis.client.FloatingIPs.Create(context.TODO(), ficr) - if err != nil { - return nil, err - } - - return &FloatingIP{FloatingIP: fip}, nil -} - -func (fis *floatingIPsService) Delete(ip string) error { - _, err := fis.client.FloatingIPs.Delete(context.TODO(), ip) - return err -} diff --git a/do/mocks/FloatingIPActionsService.go b/do/mocks/ReservedIPActionsService.go similarity index 56% rename from do/mocks/FloatingIPActionsService.go rename to do/mocks/ReservedIPActionsService.go index 53ae491c3..c8db2cbbf 100644 --- a/do/mocks/FloatingIPActionsService.go +++ b/do/mocks/ReservedIPActionsService.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: floating_ip_actions.go +// Source: reserved_ip_actions.go // Package mocks is a generated GoMock package. package mocks @@ -12,31 +12,31 @@ import ( gomock "github.com/golang/mock/gomock" ) -// MockFloatingIPActionsService is a mock of FloatingIPActionsService interface. -type MockFloatingIPActionsService struct { +// MockReservedIPActionsService is a mock of ReservedIPActionsService interface. +type MockReservedIPActionsService struct { ctrl *gomock.Controller - recorder *MockFloatingIPActionsServiceMockRecorder + recorder *MockReservedIPActionsServiceMockRecorder } -// MockFloatingIPActionsServiceMockRecorder is the mock recorder for MockFloatingIPActionsService. -type MockFloatingIPActionsServiceMockRecorder struct { - mock *MockFloatingIPActionsService +// MockReservedIPActionsServiceMockRecorder is the mock recorder for MockReservedIPActionsService. +type MockReservedIPActionsServiceMockRecorder struct { + mock *MockReservedIPActionsService } -// NewMockFloatingIPActionsService creates a new mock instance. -func NewMockFloatingIPActionsService(ctrl *gomock.Controller) *MockFloatingIPActionsService { - mock := &MockFloatingIPActionsService{ctrl: ctrl} - mock.recorder = &MockFloatingIPActionsServiceMockRecorder{mock} +// NewMockReservedIPActionsService creates a new mock instance. +func NewMockReservedIPActionsService(ctrl *gomock.Controller) *MockReservedIPActionsService { + mock := &MockReservedIPActionsService{ctrl: ctrl} + mock.recorder = &MockReservedIPActionsServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockFloatingIPActionsService) EXPECT() *MockFloatingIPActionsServiceMockRecorder { +func (m *MockReservedIPActionsService) EXPECT() *MockReservedIPActionsServiceMockRecorder { return m.recorder } // Assign mocks base method. -func (m *MockFloatingIPActionsService) Assign(ip string, dropletID int) (*do.Action, error) { +func (m *MockReservedIPActionsService) Assign(ip string, dropletID int) (*do.Action, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Assign", ip, dropletID) ret0, _ := ret[0].(*do.Action) @@ -45,13 +45,13 @@ func (m *MockFloatingIPActionsService) Assign(ip string, dropletID int) (*do.Act } // Assign indicates an expected call of Assign. -func (mr *MockFloatingIPActionsServiceMockRecorder) Assign(ip, dropletID interface{}) *gomock.Call { +func (mr *MockReservedIPActionsServiceMockRecorder) Assign(ip, dropletID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Assign", reflect.TypeOf((*MockFloatingIPActionsService)(nil).Assign), ip, dropletID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Assign", reflect.TypeOf((*MockReservedIPActionsService)(nil).Assign), ip, dropletID) } // Get mocks base method. -func (m *MockFloatingIPActionsService) Get(ip string, actionID int) (*do.Action, error) { +func (m *MockReservedIPActionsService) Get(ip string, actionID int) (*do.Action, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ip, actionID) ret0, _ := ret[0].(*do.Action) @@ -60,13 +60,13 @@ func (m *MockFloatingIPActionsService) Get(ip string, actionID int) (*do.Action, } // Get indicates an expected call of Get. -func (mr *MockFloatingIPActionsServiceMockRecorder) Get(ip, actionID interface{}) *gomock.Call { +func (mr *MockReservedIPActionsServiceMockRecorder) Get(ip, actionID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockFloatingIPActionsService)(nil).Get), ip, actionID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockReservedIPActionsService)(nil).Get), ip, actionID) } // List mocks base method. -func (m *MockFloatingIPActionsService) List(ip string, opt *godo.ListOptions) ([]do.Action, error) { +func (m *MockReservedIPActionsService) List(ip string, opt *godo.ListOptions) ([]do.Action, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", ip, opt) ret0, _ := ret[0].([]do.Action) @@ -75,13 +75,13 @@ func (m *MockFloatingIPActionsService) List(ip string, opt *godo.ListOptions) ([ } // List indicates an expected call of List. -func (mr *MockFloatingIPActionsServiceMockRecorder) List(ip, opt interface{}) *gomock.Call { +func (mr *MockReservedIPActionsServiceMockRecorder) List(ip, opt interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockFloatingIPActionsService)(nil).List), ip, opt) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockReservedIPActionsService)(nil).List), ip, opt) } // Unassign mocks base method. -func (m *MockFloatingIPActionsService) Unassign(ip string) (*do.Action, error) { +func (m *MockReservedIPActionsService) Unassign(ip string) (*do.Action, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Unassign", ip) ret0, _ := ret[0].(*do.Action) @@ -90,7 +90,7 @@ func (m *MockFloatingIPActionsService) Unassign(ip string) (*do.Action, error) { } // Unassign indicates an expected call of Unassign. -func (mr *MockFloatingIPActionsServiceMockRecorder) Unassign(ip interface{}) *gomock.Call { +func (mr *MockReservedIPActionsServiceMockRecorder) Unassign(ip interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unassign", reflect.TypeOf((*MockFloatingIPActionsService)(nil).Unassign), ip) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unassign", reflect.TypeOf((*MockReservedIPActionsService)(nil).Unassign), ip) } diff --git a/do/mocks/FloatingIPsService.go b/do/mocks/ReservedIPsService.go similarity index 50% rename from do/mocks/FloatingIPsService.go rename to do/mocks/ReservedIPsService.go index cdd269476..30e4ae56c 100644 --- a/do/mocks/FloatingIPsService.go +++ b/do/mocks/ReservedIPsService.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: floating_ips.go +// Source: reserved_ips.go // Package mocks is a generated GoMock package. package mocks @@ -12,46 +12,46 @@ import ( gomock "github.com/golang/mock/gomock" ) -// MockFloatingIPsService is a mock of FloatingIPsService interface. -type MockFloatingIPsService struct { +// MockReservedIPsService is a mock of ReservedIPsService interface. +type MockReservedIPsService struct { ctrl *gomock.Controller - recorder *MockFloatingIPsServiceMockRecorder + recorder *MockReservedIPsServiceMockRecorder } -// MockFloatingIPsServiceMockRecorder is the mock recorder for MockFloatingIPsService. -type MockFloatingIPsServiceMockRecorder struct { - mock *MockFloatingIPsService +// MockReservedIPsServiceMockRecorder is the mock recorder for MockReservedIPsService. +type MockReservedIPsServiceMockRecorder struct { + mock *MockReservedIPsService } -// NewMockFloatingIPsService creates a new mock instance. -func NewMockFloatingIPsService(ctrl *gomock.Controller) *MockFloatingIPsService { - mock := &MockFloatingIPsService{ctrl: ctrl} - mock.recorder = &MockFloatingIPsServiceMockRecorder{mock} +// NewMockReservedIPsService creates a new mock instance. +func NewMockReservedIPsService(ctrl *gomock.Controller) *MockReservedIPsService { + mock := &MockReservedIPsService{ctrl: ctrl} + mock.recorder = &MockReservedIPsServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockFloatingIPsService) EXPECT() *MockFloatingIPsServiceMockRecorder { +func (m *MockReservedIPsService) EXPECT() *MockReservedIPsServiceMockRecorder { return m.recorder } // Create mocks base method. -func (m *MockFloatingIPsService) Create(ficr *godo.FloatingIPCreateRequest) (*do.FloatingIP, error) { +func (m *MockReservedIPsService) Create(ficr *godo.ReservedIPCreateRequest) (*do.ReservedIP, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", ficr) - ret0, _ := ret[0].(*do.FloatingIP) + ret0, _ := ret[0].(*do.ReservedIP) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. -func (mr *MockFloatingIPsServiceMockRecorder) Create(ficr interface{}) *gomock.Call { +func (mr *MockReservedIPsServiceMockRecorder) Create(ficr interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFloatingIPsService)(nil).Create), ficr) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockReservedIPsService)(nil).Create), ficr) } // Delete mocks base method. -func (m *MockFloatingIPsService) Delete(ip string) error { +func (m *MockReservedIPsService) Delete(ip string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", ip) ret0, _ := ret[0].(error) @@ -59,37 +59,37 @@ func (m *MockFloatingIPsService) Delete(ip string) error { } // Delete indicates an expected call of Delete. -func (mr *MockFloatingIPsServiceMockRecorder) Delete(ip interface{}) *gomock.Call { +func (mr *MockReservedIPsServiceMockRecorder) Delete(ip interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockFloatingIPsService)(nil).Delete), ip) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockReservedIPsService)(nil).Delete), ip) } // Get mocks base method. -func (m *MockFloatingIPsService) Get(ip string) (*do.FloatingIP, error) { +func (m *MockReservedIPsService) Get(ip string) (*do.ReservedIP, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", ip) - ret0, _ := ret[0].(*do.FloatingIP) + ret0, _ := ret[0].(*do.ReservedIP) ret1, _ := ret[1].(error) return ret0, ret1 } // Get indicates an expected call of Get. -func (mr *MockFloatingIPsServiceMockRecorder) Get(ip interface{}) *gomock.Call { +func (mr *MockReservedIPsServiceMockRecorder) Get(ip interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockFloatingIPsService)(nil).Get), ip) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockReservedIPsService)(nil).Get), ip) } // List mocks base method. -func (m *MockFloatingIPsService) List() (do.FloatingIPs, error) { +func (m *MockReservedIPsService) List() (do.ReservedIPs, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].(do.FloatingIPs) + ret0, _ := ret[0].(do.ReservedIPs) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. -func (mr *MockFloatingIPsServiceMockRecorder) List() *gomock.Call { +func (mr *MockReservedIPsServiceMockRecorder) List() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockFloatingIPsService)(nil).List)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockReservedIPsService)(nil).List)) } diff --git a/do/floating_ip_actions.go b/do/reserved_ip_actions.go similarity index 64% rename from do/floating_ip_actions.go rename to do/reserved_ip_actions.go index 2d602f7f0..3284de07d 100644 --- a/do/floating_ip_actions.go +++ b/do/reserved_ip_actions.go @@ -19,30 +19,30 @@ import ( "github.com/digitalocean/godo" ) -// FloatingIPActionsService is an interface for interacting with -// DigitalOcean's floating ip action api. -type FloatingIPActionsService interface { +// ReservedIPActionsService is an interface for interacting with +// DigitalOcean's reserved ip action api. +type ReservedIPActionsService interface { Assign(ip string, dropletID int) (*Action, error) Unassign(ip string) (*Action, error) Get(ip string, actionID int) (*Action, error) List(ip string, opt *godo.ListOptions) ([]Action, error) } -type floatingIPActionsService struct { +type reservedIPActionsService struct { client *godo.Client } -var _ FloatingIPActionsService = &floatingIPActionsService{} +var _ ReservedIPActionsService = &reservedIPActionsService{} -// NewFloatingIPActionsService builds a FloatingIPActionsService instance. -func NewFloatingIPActionsService(godoClient *godo.Client) FloatingIPActionsService { - return &floatingIPActionsService{ +// NewReservedIPActionsService builds a ReservedIPActionsService instance. +func NewReservedIPActionsService(godoClient *godo.Client) ReservedIPActionsService { + return &reservedIPActionsService{ client: godoClient, } } -func (fia *floatingIPActionsService) Assign(ip string, dropletID int) (*Action, error) { - a, _, err := fia.client.FloatingIPActions.Assign(context.TODO(), ip, dropletID) +func (fia *reservedIPActionsService) Assign(ip string, dropletID int) (*Action, error) { + a, _, err := fia.client.ReservedIPActions.Assign(context.TODO(), ip, dropletID) if err != nil { return nil, err } @@ -50,8 +50,8 @@ func (fia *floatingIPActionsService) Assign(ip string, dropletID int) (*Action, return &Action{Action: a}, nil } -func (fia *floatingIPActionsService) Unassign(ip string) (*Action, error) { - a, _, err := fia.client.FloatingIPActions.Unassign(context.TODO(), ip) +func (fia *reservedIPActionsService) Unassign(ip string) (*Action, error) { + a, _, err := fia.client.ReservedIPActions.Unassign(context.TODO(), ip) if err != nil { return nil, err } @@ -59,8 +59,8 @@ func (fia *floatingIPActionsService) Unassign(ip string) (*Action, error) { return &Action{Action: a}, nil } -func (fia *floatingIPActionsService) Get(ip string, actionID int) (*Action, error) { - a, _, err := fia.client.FloatingIPActions.Get(context.TODO(), ip, actionID) +func (fia *reservedIPActionsService) Get(ip string, actionID int) (*Action, error) { + a, _, err := fia.client.ReservedIPActions.Get(context.TODO(), ip, actionID) if err != nil { return nil, err } @@ -68,9 +68,9 @@ func (fia *floatingIPActionsService) Get(ip string, actionID int) (*Action, erro return &Action{Action: a}, nil } -func (fia *floatingIPActionsService) List(ip string, opt *godo.ListOptions) ([]Action, error) { +func (fia *reservedIPActionsService) List(ip string, opt *godo.ListOptions) ([]Action, error) { f := func(opt *godo.ListOptions) ([]interface{}, *godo.Response, error) { - list, resp, err := fia.client.FloatingIPActions.List(context.TODO(), ip, opt) + list, resp, err := fia.client.ReservedIPActions.List(context.TODO(), ip, opt) if err != nil { return nil, nil, err } diff --git a/do/reserved_ips.go b/do/reserved_ips.go new file mode 100644 index 000000000..df4b03e2d --- /dev/null +++ b/do/reserved_ips.go @@ -0,0 +1,101 @@ +/* +Copyright 2018 The Doctl Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package do + +import ( + "context" + + "github.com/digitalocean/godo" +) + +// ReservedIP wraps a godo ReservedIP. +type ReservedIP struct { + *godo.ReservedIP +} + +// ReservedIPs is a slice of ReservedIP. +type ReservedIPs []ReservedIP + +// ReservedIPsService is the godo ReservedIPsService interface. +type ReservedIPsService interface { + List() (ReservedIPs, error) + Get(ip string) (*ReservedIP, error) + Create(ficr *godo.ReservedIPCreateRequest) (*ReservedIP, error) + Delete(ip string) error +} + +type reservedIPsService struct { + client *godo.Client +} + +var _ ReservedIPsService = &reservedIPsService{} + +// NewReservedIPsService builds an instance of ReservedIPsService. +func NewReservedIPsService(client *godo.Client) ReservedIPsService { + return &reservedIPsService{ + client: client, + } +} + +func (fis *reservedIPsService) List() (ReservedIPs, error) { + f := func(opt *godo.ListOptions) ([]interface{}, *godo.Response, error) { + list, resp, err := fis.client.ReservedIPs.List(context.TODO(), opt) + if err != nil { + return nil, nil, err + } + + si := make([]interface{}, len(list)) + for i := range list { + si[i] = list[i] + } + + return si, resp, err + } + + si, err := PaginateResp(f) + if err != nil { + return nil, err + } + + list := make(ReservedIPs, 0, len(si)) + for _, x := range si { + fip := x.(godo.ReservedIP) + list = append(list, ReservedIP{ReservedIP: &fip}) + } + + return list, nil +} + +func (fis *reservedIPsService) Get(ip string) (*ReservedIP, error) { + fip, _, err := fis.client.ReservedIPs.Get(context.TODO(), ip) + if err != nil { + return nil, err + } + + return &ReservedIP{ReservedIP: fip}, nil +} + +func (fis *reservedIPsService) Create(ficr *godo.ReservedIPCreateRequest) (*ReservedIP, error) { + fip, _, err := fis.client.ReservedIPs.Create(context.TODO(), ficr) + if err != nil { + return nil, err + } + + return &ReservedIP{ReservedIP: fip}, nil +} + +func (fis *reservedIPsService) Delete(ip string) error { + _, err := fis.client.ReservedIPs.Delete(context.TODO(), ip) + return err +} diff --git a/go.mod b/go.mod index 1ed67a3c0..78a1317c7 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/containerd/continuity v0.2.2 // indirect github.com/creack/pty v1.1.11 - github.com/digitalocean/godo v1.80.0 + github.com/digitalocean/godo v1.81.0 github.com/docker/cli v20.10.14+incompatible github.com/docker/docker v17.12.0-ce-rc1.0.20200531234253-77e06fda0c94+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect diff --git a/go.sum b/go.sum index 5d250cbca..543834e7b 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.80.0 h1:ZULJ/fWDM97YtO7Fa+K6hzJLd7+smCu4N+0n+B/xtj4= -github.com/digitalocean/godo v1.80.0/go.mod h1:BPCqvwbjbGqxuUnIKB4EvS/AX7IDnNmt5fwvIkWo+ew= +github.com/digitalocean/godo v1.81.0 h1:sjb3fOfPfSlUQUK22E87BcI8Zx2qtnF7VUCCO4UK3C8= +github.com/digitalocean/godo v1.81.0/go.mod h1:BPCqvwbjbGqxuUnIKB4EvS/AX7IDnNmt5fwvIkWo+ew= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.14+incompatible h1:dSBKJOVesDgHo7rbxlYjYsXe7gPzrTT+/cKQgpDAazg= diff --git a/integration/compute_floating_ip_action_test.go b/integration/compute_floating_ip_action_test.go index ea02de425..446d8e1df 100644 --- a/integration/compute_floating_ip_action_test.go +++ b/integration/compute_floating_ip_action_test.go @@ -25,7 +25,7 @@ var _ = suite("compute/floating-ip-action", func(t *testing.T, when spec.G, it s server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { - case "/v2/floating_ips/77/actions/66": + case "/v2/reserved_ips/77/actions/66": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) @@ -38,7 +38,7 @@ var _ = suite("compute/floating-ip-action", func(t *testing.T, when spec.G, it s } w.Write([]byte(floatingIPActionResponse)) - case "/v2/floating_ips/1/actions": + case "/v2/reserved_ips/1/actions": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) @@ -56,7 +56,7 @@ var _ = suite("compute/floating-ip-action", func(t *testing.T, when spec.G, it s expect.JSONEq(`{"type":"unassign"}`, string(reqBody)) w.Write([]byte(floatingIPActionResponse)) - case "/v2/floating_ips/1313/actions": + case "/v2/reserved_ips/1313/actions": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) diff --git a/integration/compute_reserved_ip_action_test.go b/integration/compute_reserved_ip_action_test.go new file mode 100644 index 000000000..55f00bb7c --- /dev/null +++ b/integration/compute_reserved_ip_action_test.go @@ -0,0 +1,168 @@ +package integration + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/reserved-ip-action", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/reserved_ips/77/actions/66": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(reservedIPActionResponse)) + case "/v2/reserved_ips/1/actions": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + reqBody, err := ioutil.ReadAll(req.Body) + expect.NoError(err) + + expect.JSONEq(`{"type":"unassign"}`, string(reqBody)) + + w.Write([]byte(reservedIPActionResponse)) + case "/v2/reserved_ips/1313/actions": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + reqBody, err := ioutil.ReadAll(req.Body) + expect.NoError(err) + + expect.JSONEq(`{"droplet_id":1414,"type":"assign"}`, string(reqBody)) + + w.Write([]byte(reservedIPActionResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("command is get", func() { + it("gets the specified reserved-ip action", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "reserved-ip-action", + "get", + "77", + "66", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(reservedIPActionOutput), strings.TrimSpace(string(output))) + }) + }) + + when("command is assign", func() { + it("assigns the image", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "reserved-ip-action", + "assign", + "1313", + "1414", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(reservedIPActionOutput), strings.TrimSpace(string(output))) + }) + }) + + when("command is unassign", func() { + it("unassigns the image", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "reserved-ip-action", + "unassign", + "1", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(reservedIPActionOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + reservedIPActionOutput = ` +ID Status Type Started At Completed At Resource ID Resource Type Region +68212728 in-progress assign_ip 2015-10-15 17:45:44 +0000 UTC 758603823 reserved_ip nyc3 + ` + reservedIPActionResponse = ` +{ + "action": { + "id": 68212728, + "status": "in-progress", + "type": "assign_ip", + "started_at": "2015-10-15T17:45:44Z", + "completed_at": null, + "resource_id": 758603823, + "resource_type": "reserved_ip", + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ "s-32vcpu-192gb" ], + "features": [ "metadata" ], + "available": true + }, + "region_slug": "nyc3" + } +} +` +) diff --git a/integration/floating_ip_create_test.go b/integration/floating_ip_create_test.go index 9c182e250..49d079231 100644 --- a/integration/floating_ip_create_test.go +++ b/integration/floating_ip_create_test.go @@ -26,7 +26,7 @@ var _ = suite("compute/floating-ip/create", func(t *testing.T, when spec.G, it s server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { - case "/v2/floating_ips": + case "/v2/reserved_ips": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) @@ -90,7 +90,7 @@ IP Region Droplet ID Droplet Name ` floatingIPCreateResponse = ` { - "floating_ip": { + "reserved_ip": { "ip": "45.55.96.47", "droplet": { "id": 1212, diff --git a/integration/floating_ip_delete_test.go b/integration/floating_ip_delete_test.go index 29f49596c..b7bde5631 100644 --- a/integration/floating_ip_delete_test.go +++ b/integration/floating_ip_delete_test.go @@ -24,7 +24,7 @@ var _ = suite("compute/floating-ip/delete", func(t *testing.T, when spec.G, it s server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { - case "/v2/floating_ips/1.1.1.1": + case "/v2/reserved_ips/1.1.1.1": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) diff --git a/integration/floating_ip_get_test.go b/integration/floating_ip_get_test.go index 199874356..a4fd70bdb 100644 --- a/integration/floating_ip_get_test.go +++ b/integration/floating_ip_get_test.go @@ -25,7 +25,7 @@ var _ = suite("compute/floating-ip/get", func(t *testing.T, when spec.G, it spec server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { - case "/v2/floating_ips/1.1.1.1": + case "/v2/reserved_ips/1.1.1.1": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) @@ -79,7 +79,7 @@ IP Region Droplet ID Droplet Name ` floatingIPGetResponse = ` { - "floating_ip": { + "reserved_ip": { "ip": "1.1.1.1", "droplet": null, "region": { diff --git a/integration/floating_ip_list_test.go b/integration/floating_ip_list_test.go index 0e87010da..0d81fbebe 100644 --- a/integration/floating_ip_list_test.go +++ b/integration/floating_ip_list_test.go @@ -25,7 +25,7 @@ var _ = suite("compute/floating-ip/list", func(t *testing.T, when spec.G, it spe server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { - case "/v2/floating_ips": + case "/v2/reserved_ips": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) @@ -79,7 +79,7 @@ IP Region Droplet ID Droplet Name ` floatingIPListResponse = ` { - "floating_ips": [ + "reserved_ips": [ { "ip": "8.8.8.8", "droplet": {"id": 8888, "name": "hello"}, diff --git a/integration/projects_resources_get_test.go b/integration/projects_resources_get_test.go index 53793b671..0e03698d3 100644 --- a/integration/projects_resources_get_test.go +++ b/integration/projects_resources_get_test.go @@ -37,7 +37,7 @@ var _ = suite("projects/resources/get", func(t *testing.T, when spec.G, it spec. } w.Write([]byte(dropletGetResponse)) - case "/v2/floating_ips/1111": + case "/v2/reserved_ips/1111": auth := req.Header.Get("Authorization") if auth != "Bearer some-magic-token" { w.WriteHeader(http.StatusUnauthorized) @@ -134,6 +134,23 @@ var _ = suite("projects/resources/get", func(t *testing.T, when spec.G, it spec. }) }) + when("passing a reserved ip urn", func() { + it("gets that resource for the project", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "projects", + "resources", + "get", + "do:reservedip:1111", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(projectsResourcesGetFloatingIPOutput), strings.TrimSpace(string(output))) + }) + }) + when("passing a loadbalancer urn", func() { it("gets that resource for the project", func() { cmd := exec.Command(builtBinaryPath, @@ -197,7 +214,7 @@ IP Region Droplet ID Droplet Name ` projectsResourcesGetFloatingIPResponse = ` { - "floating_ip": { + "reserved_ip": { "ip": "45.55.96.47", "droplet": null, "region": { diff --git a/integration/reserved_ip_create_test.go b/integration/reserved_ip_create_test.go new file mode 100644 index 000000000..de02d0a74 --- /dev/null +++ b/integration/reserved_ip_create_test.go @@ -0,0 +1,117 @@ +package integration + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/reserved-ip/create", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/reserved_ips": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + reqBody, err := ioutil.ReadAll(req.Body) + expect.NoError(err) + + matchedRequest := reservedIPCreateRequest + if !strings.Contains(string(reqBody), "droplet_id") { + matchedRequest = reservedIPRegionCreateRequest + } + + expect.JSONEq(matchedRequest, string(reqBody)) + + w.Write([]byte(reservedIPCreateResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + + }) + + when("the minimum flags are provided", func() { + it("creates the reserved-ip", func() { + aliases := []string{"create", "c"} + + for _, alias := range aliases { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "reserved-ip", + alias, + "--droplet-id", "1212", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(reservedIPCreateOutput), strings.TrimSpace(string(output))) + } + }) + }) +}) + +const ( + reservedIPCreateOutput = ` +IP Region Droplet ID Droplet Name +45.55.96.47 nyc3 1212 magic-name +` + reservedIPCreateResponse = ` +{ + "reserved_ip": { + "ip": "45.55.96.47", + "droplet": { + "id": 1212, + "name": "magic-name" + }, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ "s-32vcpu-192gb" ], + "features": [ "metadata" ], + "available": true + }, + "locked": false + }, + "links": {} +} +` + reservedIPCreateRequest = ` +{"droplet_id":1212} +` + reservedIPRegionCreateRequest = ` +{"region":"newark"} +` +) diff --git a/integration/reserved_ip_delete_test.go b/integration/reserved_ip_delete_test.go new file mode 100644 index 000000000..b7bde5631 --- /dev/null +++ b/integration/reserved_ip_delete_test.go @@ -0,0 +1,72 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/floating-ip/delete", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/reserved_ips/1.1.1.1": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.WriteHeader(http.StatusNoContent) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + + }) + + when("required flags are passed", func() { + it("deletes the specified floating-ip", func() { + aliases := []string{"rm", "d", "delete"} + for _, alias := range aliases { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "floating-ip", + alias, + "1.1.1.1", + "-f", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Empty(output) + } + }) + }) +}) diff --git a/integration/reserved_ip_get_test.go b/integration/reserved_ip_get_test.go new file mode 100644 index 000000000..4980fa151 --- /dev/null +++ b/integration/reserved_ip_get_test.go @@ -0,0 +1,96 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/reserved-ip/get", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/reserved_ips/1.1.1.1": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(reservedIPGetResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + + }) + + when("required flags are passed", func() { + it("gets the specified load balancer", func() { + aliases := []string{"get", "g"} + + for _, alias := range aliases { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "reserved-ip", + alias, + "1.1.1.1", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(reservedIPGetOutput), strings.TrimSpace(string(output))) + } + }) + }) +}) + +const ( + reservedIPGetOutput = ` +IP Region Droplet ID Droplet Name +1.1.1.1 nyc3 +` + reservedIPGetResponse = ` +{ + "reserved_ip": { + "ip": "1.1.1.1", + "droplet": null, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ "s-32vcpu-192gb" ], + "features": [ "metadata" ], + "available": true + }, + "locked": false + } +} +` +) diff --git a/integration/reserved_ip_list_test.go b/integration/reserved_ip_list_test.go new file mode 100644 index 000000000..d97ed4a16 --- /dev/null +++ b/integration/reserved_ip_list_test.go @@ -0,0 +1,114 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/reserved-ip/list", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/reserved_ips": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(reservedIPListResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + + }) + + when("required flags are passed", func() { + it("lists all reserved-ips", func() { + aliases := []string{"list", "ls"} + + for _, alias := range aliases { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "reserved-ip", + alias, + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(reservedIPListOutput), strings.TrimSpace(string(output))) + } + }) + }) +}) + +const ( + reservedIPListOutput = ` +IP Region Droplet ID Droplet Name +8.8.8.8 nyc3 8888 hello +1.1.1.1 nyc3 1111 +` + reservedIPListResponse = ` +{ + "reserved_ips": [ + { + "ip": "8.8.8.8", + "droplet": {"id": 8888, "name": "hello"}, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ "s-1vcpu-1gb" ], + "features": [ "metadata" ], + "available": true + }, + "locked": false + }, + { + "ip": "1.1.1.1", + "droplet": {"id": 1111}, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ "s-1vcpu-1gb" ], + "features": [ "metadata" ], + "available": true + }, + "locked": false + } + ], + "links": {}, + "meta": { + "total": 2 + } +} +` +) diff --git a/scripts/regenmocks.sh b/scripts/regenmocks.sh index ac3770d13..8a0ddcdbd 100755 --- a/scripts/regenmocks.sh +++ b/scripts/regenmocks.sh @@ -20,8 +20,6 @@ mockgen -source domains.go -package=mocks DomainService > mocks/DomainService.go mockgen -source droplet_actions.go -package=mocks DropletActionsService > mocks/DropletActionService.go mockgen -source droplets.go -package=mocks DropletsService > mocks/DropletsService.go mockgen -source firewalls.go -package=mocks FirewallsService > mocks/FirewallsService.go -mockgen -source floating_ip_actions.go -package=mocks FloatingIPActionsService > mocks/FloatingIPActionsService.go -mockgen -source floating_ips.go -package=mocks FloatingIPsService > mocks/FloatingIPsService.go mockgen -source image_actions.go -package=mocks ImageActionsService > mocks/ImageActionsService.go mockgen -source images.go -package=mocks ImageService > mocks/ImageService.go mockgen -source invoices.go -package=mocks InvoicesService > mocks/InvoicesService.go @@ -41,4 +39,6 @@ mockgen -source 1_clicks.go -package=mocks OneClickService > mocks/OneClickServi mockgen -source ../pkg/runner/runner.go -package=mocks Runner > mocks/Runner.go mockgen -source ../pkg/listen/listen.go -package=mocks Listen > mocks/Listen.go mockgen -source monitoring.go -package=mocks MonitoringService > mocks/MonitoringService.go +mockgen -source reserved_ip_actions.go -package=mocks ReservedIPActionsService > mocks/ReservedIPActionsService.go +mockgen -source reserved_ips.go -package=mocks ReservedIPsService > mocks/ReservedIPsService.go mockgen -source sandbox.go -package=mocks SandboxService > mocks/SandboxService.go \ No newline at end of file diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md index 2ca0a1582..5dfd0a263 100644 --- a/vendor/github.com/digitalocean/godo/CHANGELOG.md +++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [v1.81.0] - 2022-06-15 + +- #532 - @senorprogrammer - Add support for Reserved IP addresses +- #538 - @bentranter - util: update droplet create example +- #537 - @rpmoore - Adding project_id to databases +- #536 - @andrewsomething - account: Now may include info on current team. +- #535 - @ElanHasson - APPS-5636 Update App Platform for functions and Starter Tier App Proposals. + ## [v1.80.0] - 2022-05-23 - #533 - @ElanHasson - APPS-5636 - App Platform updates diff --git a/vendor/github.com/digitalocean/godo/account.go b/vendor/github.com/digitalocean/godo/account.go index a6691e84a..48582c9ee 100644 --- a/vendor/github.com/digitalocean/godo/account.go +++ b/vendor/github.com/digitalocean/godo/account.go @@ -22,14 +22,22 @@ var _ AccountService = &AccountServiceOp{} // Account represents a DigitalOcean Account type Account struct { - DropletLimit int `json:"droplet_limit,omitempty"` - FloatingIPLimit int `json:"floating_ip_limit,omitempty"` - VolumeLimit int `json:"volume_limit,omitempty"` - Email string `json:"email,omitempty"` - UUID string `json:"uuid,omitempty"` - EmailVerified bool `json:"email_verified,omitempty"` - Status string `json:"status,omitempty"` - StatusMessage string `json:"status_message,omitempty"` + DropletLimit int `json:"droplet_limit,omitempty"` + FloatingIPLimit int `json:"floating_ip_limit,omitempty"` + ReservedIPLimit int `json:"reserved_ip_limit,omitempty"` + VolumeLimit int `json:"volume_limit,omitempty"` + Email string `json:"email,omitempty"` + UUID string `json:"uuid,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` + Status string `json:"status,omitempty"` + StatusMessage string `json:"status_message,omitempty"` + Team *TeamInfo `json:"team,omitempty"` +} + +// TeamInfo contains information about the currently team context. +type TeamInfo struct { + Name string `json:"name,omitempty"` + UUID string `json:"uuid,omitempty"` } type accountRoot struct { diff --git a/vendor/github.com/digitalocean/godo/apps.gen.go b/vendor/github.com/digitalocean/godo/apps.gen.go index ac912e723..1daa84e8c 100644 --- a/vendor/github.com/digitalocean/godo/apps.gen.go +++ b/vendor/github.com/digitalocean/godo/apps.gen.go @@ -324,7 +324,7 @@ type AppLogDestinationSpecPapertrail struct { type AppRouteSpec struct { // An HTTP path prefix. Paths must start with / and must be unique across all components within an app. Path string `json:"path,omitempty"` - // An optional flag to preserve the path that is forwarded to the backend service. By default, the HTTP request path will be trimmed from the left when forwarded to the component. For example, a component with `path=/api` will have requests to `/api/list` trimmed to `/list`. If this value is `true`, the path will remain `/api/list`. + // An optional flag to preserve the path that is forwarded to the backend service. By default, the HTTP request path will be trimmed from the left when forwarded to the component. For example, a component with `path=/api` will have requests to `/api/list` trimmed to `/list`. If this value is `true`, the path will remain `/api/list`. Note: this is not applicable for Functions Components. PreservePathPrefix bool `json:"preserve_path_prefix,omitempty"` } @@ -395,7 +395,8 @@ type AppSpec struct { // Workloads which do not expose publicly-accessible HTTP services. Workers []*AppWorkerSpec `json:"workers,omitempty"` // Pre and post deployment workloads which do not expose publicly-accessible HTTP routes. - Jobs []*AppJobSpec `json:"jobs,omitempty"` + Jobs []*AppJobSpec `json:"jobs,omitempty"` + // Workloads which expose publicly-accessible HTTP services via Functions Components. Functions []*AppFunctionsSpec `json:"functions,omitempty"` // Database instances which can provide persistence to workloads within the application. Databases []*AppDatabaseSpec `json:"databases,omitempty"` @@ -511,7 +512,7 @@ type AppCORSPolicy struct { AllowHeaders []string `json:"allow_headers,omitempty"` // The set of HTTP response headers that browsers are allowed to access. This configures the Access-Control-Expose-Headers header. ExposeHeaders []string `json:"expose_headers,omitempty"` - // An optional duration specifiying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`. + // An optional duration specifying how long browsers can cache the results of a preflight request. This configures the Access-Control-Max-Age header. Example: `5h30m`. MaxAge string `json:"max_age,omitempty"` // Whether browsers should expose the response to the client-side JavaScript code when the request's credentials mode is `include`. This configures the Access-Control-Allow-Credentials header. AllowCredentials bool `json:"allow_credentials,omitempty"` @@ -804,20 +805,29 @@ type AppProposeRequest struct { // AppProposeResponse struct for AppProposeResponse type AppProposeResponse struct { - AppIsStatic bool `json:"app_is_static,omitempty"` + // Deprecated. Please use AppIsStarter instead. + AppIsStatic bool `json:"app_is_static,omitempty"` + // Indicates whether the app name is available. AppNameAvailable bool `json:"app_name_available,omitempty"` // If the app name is unavailable, this will be set to a suggested available name. AppNameSuggestion string `json:"app_name_suggestion,omitempty"` - // The number of existing static apps the account has. + // Deprecated. Please use ExistingStarterApps instead. ExistingStaticApps string `json:"existing_static_apps,omitempty"` - // The maximum number of free static apps the account can have. Any additional static apps will be charged for. + // Deprecated. Please use MaxFreeStarterApps instead. MaxFreeStaticApps string `json:"max_free_static_apps,omitempty"` Spec *AppSpec `json:"spec,omitempty"` - AppCost float32 `json:"app_cost,omitempty"` - // The monthly cost of the proposed app in USD using the next pricing plan tier. For example, if you propose an app that uses the Basic tier, the `app_tier_upgrade_cost` field displays the monthly cost of the app if it were to use the Professional tier. If the proposed app already uses the most expensive tier, the field is empty. + // The monthly cost of the proposed app in USD. + AppCost float32 `json:"app_cost,omitempty"` + // The monthly cost of the proposed app in USD using the next pricing plan tier. For example, if you propose an app that uses the Basic tier, the `AppTierUpgradeCost` field displays the monthly cost of the app if it were to use the Professional tier. If the proposed app already uses the most expensive tier, the field is empty. AppTierUpgradeCost float32 `json:"app_tier_upgrade_cost,omitempty"` - // The monthly cost of the proposed app in USD using the previous pricing plan tier. For example, if you propose an app that uses the Professional tier, the `app_tier_downgrade_cost` field displays the monthly cost of the app if it were to use the Basic tier. If the proposed app already uses the lest expensive tier, the field is empty. + // The monthly cost of the proposed app in USD using the previous pricing plan tier. For example, if you propose an app that uses the Professional tier, the `AppTierDowngradeCost` field displays the monthly cost of the app if it were to use the Basic tier. If the proposed app already uses the lest expensive tier, the field is empty. AppTierDowngradeCost float32 `json:"app_tier_downgrade_cost,omitempty"` + // The number of existing starter tier apps the account has. + ExistingStarterApps string `json:"existing_starter_apps,omitempty"` + // The maximum number of free starter apps the account can have. Any additional starter apps will be charged for. These include apps with only static sites, functions, and databases. + MaxFreeStarterApps string `json:"max_free_starter_apps,omitempty"` + // Indicates whether the app is a starter tier app. + AppIsStarter bool `json:"app_is_starter,omitempty"` } // AppRegion struct for AppRegion diff --git a/vendor/github.com/digitalocean/godo/databases.go b/vendor/github.com/digitalocean/godo/databases.go index 6d0adbded..a3f5e8d44 100644 --- a/vendor/github.com/digitalocean/godo/databases.go +++ b/vendor/github.com/digitalocean/godo/databases.go @@ -154,6 +154,7 @@ type Database struct { CreatedAt time.Time `json:"created_at,omitempty"` PrivateNetworkUUID string `json:"private_network_uuid,omitempty"` Tags []string `json:"tags,omitempty"` + ProjectID string `json:"project_id,omitempty"` } // DatabaseCA represents a database ca. @@ -217,6 +218,7 @@ type DatabaseCreateRequest struct { PrivateNetworkUUID string `json:"private_network_uuid"` Tags []string `json:"tags,omitempty"` BackupRestore *DatabaseBackupRestore `json:"backup_restore,omitempty"` + ProjectID string `json:"project_id"` } // DatabaseResizeRequest can be used to initiate a database resize operation. diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index 5679cbbf4..1e73fb875 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -20,7 +20,7 @@ import ( ) const ( - libraryVersion = "1.80.0" + libraryVersion = "1.81.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -64,6 +64,8 @@ type Client struct { Sizes SizesService FloatingIPs FloatingIPsService FloatingIPActions FloatingIPActionsService + ReservedIPs ReservedIPsService + ReservedIPActions ReservedIPActionsService Snapshots SnapshotsService Storage StorageService StorageActions StorageActionsService @@ -219,6 +221,8 @@ func NewClient(httpClient *http.Client) *Client { c.Firewalls = &FirewallsServiceOp{client: c} c.FloatingIPs = &FloatingIPsServiceOp{client: c} c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c} + c.ReservedIPs = &ReservedIPsServiceOp{client: c} + c.ReservedIPActions = &ReservedIPActionsServiceOp{client: c} c.Images = &ImagesServiceOp{client: c} c.ImageActions = &ImageActionsServiceOp{client: c} c.Invoices = &InvoicesServiceOp{client: c} diff --git a/vendor/github.com/digitalocean/godo/reserved_ips.go b/vendor/github.com/digitalocean/godo/reserved_ips.go new file mode 100644 index 000000000..f767f86c0 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/reserved_ips.go @@ -0,0 +1,145 @@ +package godo + +import ( + "context" + "fmt" + "net/http" +) + +const resourceType = "ReservedIP" +const reservedIPsBasePath = "v2/reserved_ips" + +// ReservedIPsService is an interface for interfacing with the reserved IPs +// endpoints of the Digital Ocean API. +// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Reserved-IPs +type ReservedIPsService interface { + List(context.Context, *ListOptions) ([]ReservedIP, *Response, error) + Get(context.Context, string) (*ReservedIP, *Response, error) + Create(context.Context, *ReservedIPCreateRequest) (*ReservedIP, *Response, error) + Delete(context.Context, string) (*Response, error) +} + +// ReservedIPsServiceOp handles communication with the reserved IPs related methods of the +// DigitalOcean API. +type ReservedIPsServiceOp struct { + client *Client +} + +var _ ReservedIPsService = &ReservedIPsServiceOp{} + +// ReservedIP represents a Digital Ocean reserved IP. +type ReservedIP struct { + Region *Region `json:"region"` + Droplet *Droplet `json:"droplet"` + IP string `json:"ip"` +} + +func (f ReservedIP) String() string { + return Stringify(f) +} + +// URN returns the reserved IP in a valid DO API URN form. +func (f ReservedIP) URN() string { + return ToURN(resourceType, f.IP) +} + +type reservedIPsRoot struct { + ReservedIPs []ReservedIP `json:"reserved_ips"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +type reservedIPRoot struct { + ReservedIP *ReservedIP `json:"reserved_ip"` + Links *Links `json:"links,omitempty"` +} + +// ReservedIPCreateRequest represents a request to create a reserved IP. +// Specify DropletID to assign the reserved IP to a Droplet or Region +// to reserve it to the region. +type ReservedIPCreateRequest struct { + Region string `json:"region,omitempty"` + DropletID int `json:"droplet_id,omitempty"` +} + +// List all reserved IPs. +func (r *ReservedIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]ReservedIP, *Response, error) { + path := reservedIPsBasePath + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + req, err := r.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(reservedIPsRoot) + resp, err := r.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + if m := root.Meta; m != nil { + resp.Meta = m + } + + return root.ReservedIPs, resp, err +} + +// Get an individual reserved IP. +func (r *ReservedIPsServiceOp) Get(ctx context.Context, ip string) (*ReservedIP, *Response, error) { + path := fmt.Sprintf("%s/%s", reservedIPsBasePath, ip) + + req, err := r.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(reservedIPRoot) + resp, err := r.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.ReservedIP, resp, err +} + +// Create a reserved IP. If the DropletID field of the request is not empty, +// the reserved IP will also be assigned to the droplet. +func (r *ReservedIPsServiceOp) Create(ctx context.Context, createRequest *ReservedIPCreateRequest) (*ReservedIP, *Response, error) { + path := reservedIPsBasePath + + req, err := r.client.NewRequest(ctx, http.MethodPost, path, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(reservedIPRoot) + resp, err := r.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.ReservedIP, resp, err +} + +// Delete a reserved IP. +func (r *ReservedIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) { + path := fmt.Sprintf("%s/%s", reservedIPsBasePath, ip) + + req, err := r.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + resp, err := r.client.Do(ctx, req, nil) + + return resp, err +} diff --git a/vendor/github.com/digitalocean/godo/reserved_ips_actions.go b/vendor/github.com/digitalocean/godo/reserved_ips_actions.go new file mode 100644 index 000000000..8a9e2408c --- /dev/null +++ b/vendor/github.com/digitalocean/godo/reserved_ips_actions.go @@ -0,0 +1,109 @@ +package godo + +import ( + "context" + "fmt" + "net/http" +) + +// ReservedIPActionsService is an interface for interfacing with the +// reserved IPs actions endpoints of the Digital Ocean API. +// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Reserved-IP-Actions +type ReservedIPActionsService interface { + Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) + Unassign(ctx context.Context, ip string) (*Action, *Response, error) + Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error) + List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error) +} + +// ReservedIPActionsServiceOp handles communication with the reserved IPs +// action related methods of the DigitalOcean API. +type ReservedIPActionsServiceOp struct { + client *Client +} + +// Assign a reserved IP to a droplet. +func (s *ReservedIPActionsServiceOp) Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) { + request := &ActionRequest{ + "type": "assign", + "droplet_id": dropletID, + } + return s.doAction(ctx, ip, request) +} + +// Unassign a rerserved IP from the droplet it is currently assigned to. +func (s *ReservedIPActionsServiceOp) Unassign(ctx context.Context, ip string) (*Action, *Response, error) { + request := &ActionRequest{"type": "unassign"} + return s.doAction(ctx, ip, request) +} + +// Get an action for a particular reserved IP by id. +func (s *ReservedIPActionsServiceOp) Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error) { + path := fmt.Sprintf("%s/%d", reservedIPActionPath(ip), actionID) + return s.get(ctx, path) +} + +// List the actions for a particular reserved IP. +func (s *ReservedIPActionsServiceOp) List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error) { + path := reservedIPActionPath(ip) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + return s.list(ctx, path) +} + +func (s *ReservedIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) { + path := reservedIPActionPath(ip) + + req, err := s.client.NewRequest(ctx, http.MethodPost, path, request) + if err != nil { + return nil, nil, err + } + + root := new(actionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Event, resp, err +} + +func (s *ReservedIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) { + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(actionRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Event, resp, err +} + +func (s *ReservedIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) { + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(actionsRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Actions, resp, err +} + +func reservedIPActionPath(ip string) string { + return fmt.Sprintf("%s/%s/actions", reservedIPsBasePath, ip) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4816789b0..703fd4d1b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -20,7 +20,7 @@ github.com/creack/pty # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/digitalocean/godo v1.80.0 +# github.com/digitalocean/godo v1.81.0 ## explicit; go 1.18 github.com/digitalocean/godo github.com/digitalocean/godo/metrics