From 5bb915efed980097455e97326909d1382d463bf3 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Mon, 23 May 2022 14:02:28 -0400 Subject: [PATCH 1/4] Replace upstream godo with fork temporarily --- go.mod | 2 + go.sum | 4 +- .../github.com/digitalocean/godo/CHANGELOG.md | 4 - .../github.com/digitalocean/godo/account.go | 1 + .../github.com/digitalocean/godo/apps.gen.go | 7 +- vendor/github.com/digitalocean/godo/godo.go | 6 +- .../digitalocean/godo/reserved_ips.go | 145 ++++++++++++++++++ .../digitalocean/godo/reserved_ips_actions.go | 109 +++++++++++++ vendor/modules.txt | 3 +- 9 files changed, 269 insertions(+), 12 deletions(-) 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/go.mod b/go.mod index 1ed67a3c0..b68c5df9b 100644 --- a/go.mod +++ b/go.mod @@ -86,3 +86,5 @@ require ( k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect ) + +replace github.com/digitalocean/godo => github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f diff --git a/go.sum b/go.sum index 5d250cbca..fd1aa24c8 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,6 @@ 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/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= @@ -675,6 +673,8 @@ github.com/sclevine/spec v1.3.0 h1:iTB51CYlnju5oRh0/l67fg1+RlQ2nqmFecwdvN+5TrI= github.com/sclevine/spec v1.3.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f h1:tLrogn7Lm6ojWMNEi6H+6InHXpQ4AHL2rs4tGQzGPVA= +github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f/go.mod h1:BPCqvwbjbGqxuUnIKB4EvS/AX7IDnNmt5fwvIkWo+ew= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md index 2ca0a1582..a199843dd 100644 --- a/vendor/github.com/digitalocean/godo/CHANGELOG.md +++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md @@ -1,9 +1,5 @@ # Change Log -## [v1.80.0] - 2022-05-23 - -- #533 - @ElanHasson - APPS-5636 - App Platform updates - ## [v1.79.0] - 2022-04-29 - #530 - @anitgandhi - monitoring: alerts for Load Balancers TLS conns/s utilization diff --git a/vendor/github.com/digitalocean/godo/account.go b/vendor/github.com/digitalocean/godo/account.go index a6691e84a..cba4a3dd7 100644 --- a/vendor/github.com/digitalocean/godo/account.go +++ b/vendor/github.com/digitalocean/godo/account.go @@ -24,6 +24,7 @@ var _ AccountService = &AccountServiceOp{} type Account struct { 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"` diff --git a/vendor/github.com/digitalocean/godo/apps.gen.go b/vendor/github.com/digitalocean/godo/apps.gen.go index ac912e723..bfc1fa441 100644 --- a/vendor/github.com/digitalocean/godo/apps.gen.go +++ b/vendor/github.com/digitalocean/godo/apps.gen.go @@ -117,7 +117,7 @@ const ( AppAlertSpecOperator_LessThan AppAlertSpecOperator = "LESS_THAN" ) -// AppAlertSpecRule - CPU_UTILIZATION: Represents CPU for a given container instance. Only applicable at the component level. - MEM_UTILIZATION: Represents RAM for a given container instance. Only applicable at the component level. - RESTART_COUNT: Represents restart count for a given container instance. Only applicable at the component level. - DEPLOYMENT_FAILED: Represents whether a deployment has failed. Only applicable at the app level. - DEPLOYMENT_LIVE: Represents whether a deployment has succeeded. Only applicable at the app level. - DOMAIN_FAILED: Represents whether a domain configuration has failed. Only applicable at the app level. - DOMAIN_LIVE: Represents whether a domain configuration has succeeded. Only applicable at the app level. - FUNCTIONS_ACTIVATION_COUNT: Represents an activation count for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_DURATION_MS: Represents the average duration for function runtimes. Only applicable to functions components. - FUNCTIONS_ERROR_RATE_PER_MINUTE: Represents an error rate per minute for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_WAIT_TIME_MS: Represents the average wait time for functions. Only applicable to functions components. - FUNCTIONS_ERROR_COUNT: Represents an error count for a given functions instance. Only applicable to functions components. - FUNCTIONS_GB_RATE_PER_SECOND: Represents the rate of memory consumption (GB x seconds) for functions. Only applicable to functions components. +// AppAlertSpecRule - CPU_UTILIZATION: Represents CPU for a given container instance. Only applicable at the component level. - MEM_UTILIZATION: Represents RAM for a given container instance. Only applicable at the component level. - RESTART_COUNT: Represents restart count for a given container instance. Only applicable at the component level. - DEPLOYMENT_FAILED: Represents whether a deployment has failed. Only applicable at the app level. - DEPLOYMENT_LIVE: Represents whether a deployment has succeeded. Only applicable at the app level. - DOMAIN_FAILED: Represents whether a domain configuration has failed. Only applicable at the app level. - DOMAIN_LIVE: Represents whether a domain configuration has succeeded. Only applicable at the app level. - FUNCTIONS_ACTIVATION_COUNT: Represents an activation count for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_DURATION_MS: Represents the average duration for function runtimes. Only applicable to functions components. - FUNCTIONS_ERROR_RATE_PER_MINUTE: Represents an error rate per minute for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_WAIT_TIME_MS: Represents the average wait time for functions. Only applicable to functions components. - FUNCTIONS_ERROR_COUNT: Represents an error count for a given functions instance. Only applicable to functions components. type AppAlertSpecRule string // List of AppAlertSpecRule @@ -135,7 +135,6 @@ const ( AppAlertSpecRule_FunctionsErrorRatePerMinute AppAlertSpecRule = "FUNCTIONS_ERROR_RATE_PER_MINUTE" AppAlertSpecRule_FunctionsAverageWaitTimeMs AppAlertSpecRule = "FUNCTIONS_AVERAGE_WAIT_TIME_MS" AppAlertSpecRule_FunctionsErrorCount AppAlertSpecRule = "FUNCTIONS_ERROR_COUNT" - AppAlertSpecRule_FunctionsGBRatePerSecond AppAlertSpecRule = "FUNCTIONS_GB_RATE_PER_SECOND" ) // AppAlertSpecWindow the model 'AppAlertSpecWindow' @@ -190,7 +189,7 @@ type AppDomainSpec struct { // Optional. If the domain uses DigitalOcean DNS and you would like App Platform to automatically manage it for you, set this to the name of the domain on your account. For example, If the domain you are adding is `app.domain.com`, the zone could be `domain.com`. Zone string `json:"zone,omitempty"` Certificate string `json:"certificate,omitempty"` - // Optional. The minimum version of TLS a client application can use to access resources for the domain. Must be one of the following values wrapped within quotations: `\"1.2\"` or `\"1.3\"`. + // Optional. The minimum version of TLS a client application can use to access resources for the domain. Must be one of the following values wrapped within quotations: `\"1.0\"`, `\"1.1\"`, `\"1.2\"`, or `\"1.3\"`. MinimumTLSVersion string `json:"minimum_tls_version,omitempty"` } @@ -388,7 +387,7 @@ type AppServiceSpecHealthCheck struct { type AppSpec struct { // The name of the app. Must be unique across all apps in the same account. Name string `json:"name"` - // Workloads which expose publicly-accessible HTTP services. + // Workloads which expose publicy-accessible HTTP services. Services []*AppServiceSpec `json:"services,omitempty"` // Content which can be rendered to static web assets. StaticSites []*AppStaticSiteSpec `json:"static_sites,omitempty"` diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index 5679cbbf4..2d8bbf58f 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.79.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..bde9c137e 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.80.0 => github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f ## explicit; go 1.18 github.com/digitalocean/godo github.com/digitalocean/godo/metrics @@ -435,3 +435,4 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.2.0 ## explicit; go 1.12 sigs.k8s.io/yaml +# github.com/digitalocean/godo => github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f From 48c580e5894771f2958bc704373bf5c055eb0371 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Mon, 23 May 2022 14:10:41 -0400 Subject: [PATCH 2/4] Add reserved IPs services. --- 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 ++++++++++++++++++ scripts/regenmocks.sh | 4 +- 6 files changed, 170 insertions(+), 170 deletions(-) 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 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/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 From 430879231eddce2ac7fb236f563013d395f0731a Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Mon, 23 May 2022 17:04:41 -0400 Subject: [PATCH 3/4] Deprecate floating-ip commands in favor of reserved-ip. --- 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 ++--- .../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 ++++++++++++ 21 files changed, 850 insertions(+), 265 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%) 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 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/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 + } +} +` +) From 81f001b6ad5b01bf6690b0dfcabbaee411bb2fea Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Wed, 15 Jun 2022 14:10:38 -0400 Subject: [PATCH 4/4] Vendor godo v1.81.0 --- go.mod | 4 +-- go.sum | 4 +-- .../github.com/digitalocean/godo/CHANGELOG.md | 12 +++++++ .../github.com/digitalocean/godo/account.go | 25 ++++++++----- .../github.com/digitalocean/godo/apps.gen.go | 35 ++++++++++++------- .../github.com/digitalocean/godo/databases.go | 2 ++ vendor/github.com/digitalocean/godo/godo.go | 2 +- vendor/modules.txt | 3 +- 8 files changed, 58 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index b68c5df9b..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 @@ -86,5 +86,3 @@ require ( k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect ) - -replace github.com/digitalocean/godo => github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f diff --git a/go.sum b/go.sum index fd1aa24c8..543834e7b 100644 --- a/go.sum +++ b/go.sum @@ -256,6 +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.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= @@ -673,8 +675,6 @@ github.com/sclevine/spec v1.3.0 h1:iTB51CYlnju5oRh0/l67fg1+RlQ2nqmFecwdvN+5TrI= github.com/sclevine/spec v1.3.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f h1:tLrogn7Lm6ojWMNEi6H+6InHXpQ4AHL2rs4tGQzGPVA= -github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f/go.mod h1:BPCqvwbjbGqxuUnIKB4EvS/AX7IDnNmt5fwvIkWo+ew= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md index a199843dd..5dfd0a263 100644 --- a/vendor/github.com/digitalocean/godo/CHANGELOG.md +++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md @@ -1,5 +1,17 @@ # 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 + ## [v1.79.0] - 2022-04-29 - #530 - @anitgandhi - monitoring: alerts for Load Balancers TLS conns/s utilization diff --git a/vendor/github.com/digitalocean/godo/account.go b/vendor/github.com/digitalocean/godo/account.go index cba4a3dd7..48582c9ee 100644 --- a/vendor/github.com/digitalocean/godo/account.go +++ b/vendor/github.com/digitalocean/godo/account.go @@ -22,15 +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"` - 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"` + 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 bfc1fa441..1daa84e8c 100644 --- a/vendor/github.com/digitalocean/godo/apps.gen.go +++ b/vendor/github.com/digitalocean/godo/apps.gen.go @@ -117,7 +117,7 @@ const ( AppAlertSpecOperator_LessThan AppAlertSpecOperator = "LESS_THAN" ) -// AppAlertSpecRule - CPU_UTILIZATION: Represents CPU for a given container instance. Only applicable at the component level. - MEM_UTILIZATION: Represents RAM for a given container instance. Only applicable at the component level. - RESTART_COUNT: Represents restart count for a given container instance. Only applicable at the component level. - DEPLOYMENT_FAILED: Represents whether a deployment has failed. Only applicable at the app level. - DEPLOYMENT_LIVE: Represents whether a deployment has succeeded. Only applicable at the app level. - DOMAIN_FAILED: Represents whether a domain configuration has failed. Only applicable at the app level. - DOMAIN_LIVE: Represents whether a domain configuration has succeeded. Only applicable at the app level. - FUNCTIONS_ACTIVATION_COUNT: Represents an activation count for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_DURATION_MS: Represents the average duration for function runtimes. Only applicable to functions components. - FUNCTIONS_ERROR_RATE_PER_MINUTE: Represents an error rate per minute for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_WAIT_TIME_MS: Represents the average wait time for functions. Only applicable to functions components. - FUNCTIONS_ERROR_COUNT: Represents an error count for a given functions instance. Only applicable to functions components. +// AppAlertSpecRule - CPU_UTILIZATION: Represents CPU for a given container instance. Only applicable at the component level. - MEM_UTILIZATION: Represents RAM for a given container instance. Only applicable at the component level. - RESTART_COUNT: Represents restart count for a given container instance. Only applicable at the component level. - DEPLOYMENT_FAILED: Represents whether a deployment has failed. Only applicable at the app level. - DEPLOYMENT_LIVE: Represents whether a deployment has succeeded. Only applicable at the app level. - DOMAIN_FAILED: Represents whether a domain configuration has failed. Only applicable at the app level. - DOMAIN_LIVE: Represents whether a domain configuration has succeeded. Only applicable at the app level. - FUNCTIONS_ACTIVATION_COUNT: Represents an activation count for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_DURATION_MS: Represents the average duration for function runtimes. Only applicable to functions components. - FUNCTIONS_ERROR_RATE_PER_MINUTE: Represents an error rate per minute for a given functions instance. Only applicable to functions components. - FUNCTIONS_AVERAGE_WAIT_TIME_MS: Represents the average wait time for functions. Only applicable to functions components. - FUNCTIONS_ERROR_COUNT: Represents an error count for a given functions instance. Only applicable to functions components. - FUNCTIONS_GB_RATE_PER_SECOND: Represents the rate of memory consumption (GB x seconds) for functions. Only applicable to functions components. type AppAlertSpecRule string // List of AppAlertSpecRule @@ -135,6 +135,7 @@ const ( AppAlertSpecRule_FunctionsErrorRatePerMinute AppAlertSpecRule = "FUNCTIONS_ERROR_RATE_PER_MINUTE" AppAlertSpecRule_FunctionsAverageWaitTimeMs AppAlertSpecRule = "FUNCTIONS_AVERAGE_WAIT_TIME_MS" AppAlertSpecRule_FunctionsErrorCount AppAlertSpecRule = "FUNCTIONS_ERROR_COUNT" + AppAlertSpecRule_FunctionsGBRatePerSecond AppAlertSpecRule = "FUNCTIONS_GB_RATE_PER_SECOND" ) // AppAlertSpecWindow the model 'AppAlertSpecWindow' @@ -189,7 +190,7 @@ type AppDomainSpec struct { // Optional. If the domain uses DigitalOcean DNS and you would like App Platform to automatically manage it for you, set this to the name of the domain on your account. For example, If the domain you are adding is `app.domain.com`, the zone could be `domain.com`. Zone string `json:"zone,omitempty"` Certificate string `json:"certificate,omitempty"` - // Optional. The minimum version of TLS a client application can use to access resources for the domain. Must be one of the following values wrapped within quotations: `\"1.0\"`, `\"1.1\"`, `\"1.2\"`, or `\"1.3\"`. + // Optional. The minimum version of TLS a client application can use to access resources for the domain. Must be one of the following values wrapped within quotations: `\"1.2\"` or `\"1.3\"`. MinimumTLSVersion string `json:"minimum_tls_version,omitempty"` } @@ -323,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"` } @@ -387,14 +388,15 @@ type AppServiceSpecHealthCheck struct { type AppSpec struct { // The name of the app. Must be unique across all apps in the same account. Name string `json:"name"` - // Workloads which expose publicy-accessible HTTP services. + // Workloads which expose publicly-accessible HTTP services. Services []*AppServiceSpec `json:"services,omitempty"` // Content which can be rendered to static web assets. StaticSites []*AppStaticSiteSpec `json:"static_sites,omitempty"` // 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"` @@ -510,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"` @@ -803,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 2d8bbf58f..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.79.0" + libraryVersion = "1.81.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" diff --git a/vendor/modules.txt b/vendor/modules.txt index bde9c137e..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/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f +# github.com/digitalocean/godo v1.81.0 ## explicit; go 1.18 github.com/digitalocean/godo github.com/digitalocean/godo/metrics @@ -435,4 +435,3 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.2.0 ## explicit; go 1.12 sigs.k8s.io/yaml -# github.com/digitalocean/godo => github.com/senorprogrammer/godo v1.75.1-0.20220511210934-477acfa9ed5f