diff --git a/commands/command_config.go b/commands/command_config.go index cc2c002d3..3fbfc8e5a 100644 --- a/commands/command_config.go +++ b/commands/command_config.go @@ -41,16 +41,18 @@ type CmdConfig struct { componentBuilderFactory builder.ComponentBuilderFactory // services - Keys func() do.KeysService - Sizes func() do.SizesService - Regions func() do.RegionsService - Images func() do.ImagesService - ImageActions func() do.ImageActionsService - LoadBalancers func() do.LoadBalancersService - ReservedIPs func() do.ReservedIPsService - ReservedIPActions func() do.ReservedIPActionsService - ReservedIPv6s func() do.ReservedIPv6sService - BYOIPPrefixes func() do.BYOIPPrefixsService + Keys func() do.KeysService + Sizes func() do.SizesService + Regions func() do.RegionsService + Images func() do.ImagesService + ImageActions func() do.ImageActionsService + LoadBalancers func() do.LoadBalancersService + MicroDroplets func() do.MicroDropletsService + MicroDropletImages func() do.MicroDropletImagesService + ReservedIPs func() do.ReservedIPsService + ReservedIPActions func() do.ReservedIPActionsService + ReservedIPv6s func() do.ReservedIPv6sService + BYOIPPrefixes func() do.BYOIPPrefixsService Droplets func() do.DropletsService DropletActions func() do.DropletActionsService @@ -164,6 +166,10 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init } c.Nfs = func() do.NfsService { return do.NewNfsService(godoClient) } c.NfsActions = func() do.NfsActionsService { return do.NewNfsActionsService(godoClient) } + c.MicroDroplets = func() do.MicroDropletsService { return do.NewMicroDropletsService(godoClient) } + c.MicroDropletImages = func() do.MicroDropletImagesService { + return do.NewMicroDropletImagesService(godoClient) + } c.Security = func() do.SecurityService { return do.NewSecurityService(godoClient) } c.Secrets = func() do.SecretsService { return do.NewSecretsService(godoClient) } return nil diff --git a/commands/commands_test.go b/commands/commands_test.go index b27c16cee..e096de574 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -296,6 +296,8 @@ type tcMocks struct { inference *domocks.MockInferenceService nfs *domocks.MockNfsService nfsActions *domocks.MockNfsActionsService + microDroplets *domocks.MockMicroDropletsService + microDropletImages *domocks.MockMicroDropletImagesService security *domocks.MockSecurityService secrets *domocks.MockSecretsService vectorDBs *domocks.MockVectorDBsService @@ -358,6 +360,8 @@ func withTestClient(t *testing.T, tFn testFn) { inference: domocks.NewMockInferenceService(ctrl), nfs: domocks.NewMockNfsService(ctrl), nfsActions: domocks.NewMockNfsActionsService(ctrl), + microDroplets: domocks.NewMockMicroDropletsService(ctrl), + microDropletImages: domocks.NewMockMicroDropletImagesService(ctrl), security: domocks.NewMockSecurityService(ctrl), secrets: domocks.NewMockSecretsService(ctrl), vectorDBs: domocks.NewMockVectorDBsService(ctrl), @@ -428,6 +432,8 @@ func withTestClient(t *testing.T, tFn testFn) { Inference: func() do.InferenceService { return tm.inference }, Nfs: func() do.NfsService { return tm.nfs }, NfsActions: func() do.NfsActionsService { return tm.nfsActions }, + MicroDroplets: func() do.MicroDropletsService { return tm.microDroplets }, + MicroDropletImages: func() do.MicroDropletImagesService { return tm.microDropletImages }, Security: func() do.SecurityService { return tm.security }, Secrets: func() do.SecretsService { return tm.secrets }, VectorDBs: func() do.VectorDBsService { return tm.vectorDBs }, diff --git a/commands/displayers/micro_droplet.go b/commands/displayers/micro_droplet.go new file mode 100644 index 000000000..2c36f688c --- /dev/null +++ b/commands/displayers/micro_droplet.go @@ -0,0 +1,112 @@ +/* +Copyright 2025 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 displayers + +import ( + "io" + + "github.com/digitalocean/doctl/do" +) + +type MicroDroplet struct { + MicroDroplets do.MicroDroplets +} + +var _ Displayable = &MicroDroplet{} + +func (m *MicroDroplet) JSON(out io.Writer) error { + return writeJSON(m.MicroDroplets, out) +} + +func (m *MicroDroplet) Cols() []string { + return []string{ + "ID", "Name", "Region", "State", "Size", "Networking", "Image", "Endpoint", "Created", + } +} + +func (m *MicroDroplet) ColMap() map[string]string { + return map[string]string{ + "ID": "ID", + "Name": "Name", + "Region": "Region", + "State": "State", + "Size": "Size", + "Networking": "Networking", + "Image": "Image", + "Endpoint": "Endpoint", + "Created": "Created At", + } +} + +func (m *MicroDroplet) KV() []map[string]any { + out := make([]map[string]any, 0, len(m.MicroDroplets)) + for _, md := range m.MicroDroplets { + out = append(out, map[string]any{ + "ID": md.ID, + "Name": md.Name, + "Region": md.Region, + "State": string(md.State), + "Size": md.Size, + "Networking": string(md.Networking), + "Image": md.Image, + "Endpoint": md.Endpoint, + "Created": md.Created, + }) + } + return out +} + +type MicroDropletSnapshot struct { + Snapshots do.MicroDropletSnapshots +} + +var _ Displayable = &MicroDropletSnapshot{} + +func (s *MicroDropletSnapshot) JSON(out io.Writer) error { + return writeJSON(s.Snapshots, out) +} + +func (s *MicroDropletSnapshot) Cols() []string { + return []string{ + "ID", "MicroDropletID", "Name", "Status", "MemoryBytes", "DiskBytes", "Created", + } +} + +func (s *MicroDropletSnapshot) ColMap() map[string]string { + return map[string]string{ + "ID": "ID", + "MicroDropletID": "MicroDroplet ID", + "Name": "Name", + "Status": "Status", + "MemoryBytes": "Memory Bytes", + "DiskBytes": "Disk Bytes", + "Created": "Created At", + } +} + +func (s *MicroDropletSnapshot) KV() []map[string]any { + out := make([]map[string]any, 0, len(s.Snapshots)) + for _, snap := range s.Snapshots { + out = append(out, map[string]any{ + "ID": snap.ID, + "MicroDropletID": snap.MicroDropletID, + "Name": snap.Name, + "Status": string(snap.Status), + "MemoryBytes": snap.MemoryBytes, + "DiskBytes": snap.DiskBytes, + "Created": snap.Created, + }) + } + return out +} diff --git a/commands/displayers/micro_droplet_image.go b/commands/displayers/micro_droplet_image.go new file mode 100644 index 000000000..0c148340f --- /dev/null +++ b/commands/displayers/micro_droplet_image.go @@ -0,0 +1,58 @@ +/* +Copyright 2025 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 displayers + +import ( + "io" + + "github.com/digitalocean/doctl/do" +) + +type MicroDropletImage struct { + Images do.MicroDropletImages +} + +var _ Displayable = &MicroDropletImage{} + +func (i *MicroDropletImage) JSON(out io.Writer) error { + return writeJSON(i.Images, out) +} + +func (i *MicroDropletImage) Cols() []string { + return []string{"ID", "Name", "Source", "Status", "Created"} +} + +func (i *MicroDropletImage) ColMap() map[string]string { + return map[string]string{ + "ID": "ID", + "Name": "Name", + "Source": "Source", + "Status": "Status", + "Created": "Created At", + } +} + +func (i *MicroDropletImage) KV() []map[string]any { + out := make([]map[string]any, 0, len(i.Images)) + for _, img := range i.Images { + out = append(out, map[string]any{ + "ID": img.ID, + "Name": img.Name, + "Source": img.Source, + "Status": string(img.Status), + "Created": img.Created, + }) + } + return out +} diff --git a/commands/doit.go b/commands/doit.go index 1893b9b13..2f22c79f9 100644 --- a/commands/doit.go +++ b/commands/doit.go @@ -219,6 +219,7 @@ func computeCmd() *Command { cmd.AddCommand(Droplet()) cmd.AddCommand(DropletAutoscale()) cmd.AddCommand(Domain()) + cmd.AddCommand(MicroDroplet()) cmd.AddCommand(VPCNATGateway()) cmd.AddCommand(Firewall()) cmd.AddCommand(ReservedIP()) diff --git a/commands/microdroplets.go b/commands/microdroplets.go new file mode 100644 index 000000000..d113d497a --- /dev/null +++ b/commands/microdroplets.go @@ -0,0 +1,411 @@ +/* +Copyright 2025 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 ( + "fmt" + "strings" + + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/commands/displayers" + "github.com/digitalocean/doctl/do" + "github.com/digitalocean/godo" + "github.com/spf13/cobra" +) + +// MicroDroplet creates the microdroplet command tree. +func MicroDroplet() *Command { + cmd := &Command{ + Command: &cobra.Command{ + Use: "microdroplet", + Short: "Manage MicroDroplets", + Long: `The subcommands under ` + "`" + `doctl compute microdroplet` + "`" + ` manage MicroDroplets — lightweight ` + + `microVM sandboxes that pause when idle and resume on demand. Use these commands to ` + + `create, inspect, pause, resume, and delete MicroDroplets, and to list their snapshots.`, + }, + } + + cmdMicroDropletList := CmdBuilder(cmd, RunMicroDropletList, "list", + "List MicroDroplets on your account", + "Retrieves a list of MicroDroplets on your account, optionally filtered by region.", + Writer, aliasOpt("ls"), displayerType(&displayers.MicroDroplet{})) + AddStringFlag(cmdMicroDropletList, doctl.ArgRegionSlug, "", "", + "Filter MicroDroplets by region slug, such as `nyc1`") + + CmdBuilder(cmd, RunMicroDropletGet, "get ", + "Retrieve information about a MicroDroplet", + "Retrieves information about a MicroDroplet by its UUID.", + Writer, aliasOpt("g"), displayerType(&displayers.MicroDroplet{})) + + cmdMicroDropletCreate := CmdBuilder(cmd, RunMicroDropletCreate, "create ", + "Create a new MicroDroplet", + "Creates a new MicroDroplet in the specified region from the specified image. "+ + "`--region`, `--size`, and `--image` are required. The MicroDroplet UUID and current state are returned on success.", + Writer, aliasOpt("c"), displayerType(&displayers.MicroDroplet{})) + AddStringFlag(cmdMicroDropletCreate, doctl.ArgRegionSlug, "", "", + "A `slug` specifying the region to create the MicroDroplet in, such as `nyc1`", requiredOpt()) + AddStringFlag(cmdMicroDropletCreate, doctl.ArgSizeSlug, "", "", + "A `slug` indicating the MicroDroplet's size", requiredOpt()) + AddStringFlag(cmdMicroDropletCreate, doctl.ArgImage, "", "", + "The URN or UUID of a MicroDroplet image to launch", requiredOpt()) + AddStringFlag(cmdMicroDropletCreate, "networking", "", "", + "Networking mode for the MicroDroplet: `public` or `vpc`") + AddStringFlag(cmdMicroDropletCreate, doctl.ArgVPCUUID, "", "", + "The UUID of a non-default VPC to place the MicroDroplet in (only valid when `--networking=vpc`)") + AddBoolFlag(cmdMicroDropletCreate, "auto-pause", "", false, + "Enable auto-pause when the MicroDroplet is idle") + AddStringFlag(cmdMicroDropletCreate, "auto-pause-idle-timeout", "", "", + "Idle duration before auto-pause (e.g. `5m`, `30s`); requires `--auto-pause`") + AddBoolFlag(cmdMicroDropletCreate, "auto-resume", "", false, + "Enable auto-resume when the MicroDroplet is paused and receives traffic") + AddIntFlag(cmdMicroDropletCreate, "http-port", "", 0, + "HTTP port exposed by the MicroDroplet workload") + AddStringFlag(cmdMicroDropletCreate, "http-protocol", "", "", + "HTTP protocol served by the MicroDroplet: `http`, `https`, or `http2`") + AddStringSliceFlag(cmdMicroDropletCreate, "env", "", []string{}, + "Environment variables to inject, in `KEY=VALUE` form. Repeatable.") + AddStringSliceFlag(cmdMicroDropletCreate, doctl.ArgTag, "", []string{}, + "Tags to apply to the MicroDroplet. Repeatable.") + + CmdBuilder(cmd, RunMicroDropletPause, "pause ", + "Pause a running MicroDroplet", + "Requests that the MicroDroplet transition to the `paused` state. Returns the mutated MicroDroplet.", + Writer, displayerType(&displayers.MicroDroplet{})) + + CmdBuilder(cmd, RunMicroDropletResume, "resume ", + "Resume a paused MicroDroplet", + "Requests that the MicroDroplet transition to the `running` state. Returns the mutated MicroDroplet.", + Writer, displayerType(&displayers.MicroDroplet{})) + + cmdMicroDropletDelete := CmdBuilder(cmd, RunMicroDropletDelete, "delete ...", + "Permanently delete one or more MicroDroplets", + "Permanently deletes the specified MicroDroplets. This is irreversible.", + Writer, aliasOpt("d", "rm")) + AddBoolFlag(cmdMicroDropletDelete, doctl.ArgForce, doctl.ArgShortForce, false, + "Delete the MicroDroplet(s) without a confirmation prompt") + + CmdBuilder(cmd, RunMicroDropletSnapshots, "snapshots ", + "List snapshots for a MicroDroplet", + "Retrieves a list of snapshots for the specified MicroDroplet.", + Writer, aliasOpt("s"), displayerType(&displayers.MicroDropletSnapshot{})) + + cmd.AddCommand(microDropletImages()) + + return cmd +} + +func microDropletImages() *Command { + cmd := &Command{ + Command: &cobra.Command{ + Use: "image", + Short: "Manage MicroDroplet images", + Long: `The subcommands under ` + "`" + `doctl compute microdroplet image` + "`" + ` manage OCI images ` + + `imported for use with MicroDroplets.`, + }, + } + + CmdBuilder(cmd, RunMicroDropletImageList, "list", + "List MicroDroplet images on your account", + "Retrieves a list of MicroDroplet images on your account.", + Writer, aliasOpt("ls"), displayerType(&displayers.MicroDropletImage{})) + + CmdBuilder(cmd, RunMicroDropletImageGet, "get ", + "Retrieve information about a MicroDroplet image", + "Retrieves information about a MicroDroplet image by its UUID.", + Writer, aliasOpt("g"), displayerType(&displayers.MicroDropletImage{})) + + cmdImageCreate := CmdBuilder(cmd, RunMicroDropletImageCreate, "create ", + "Import a new MicroDroplet image", + "Imports a new MicroDroplet image from a public OCI ref or a DOCR ref. Import is asynchronous.", + Writer, aliasOpt("c"), displayerType(&displayers.MicroDropletImage{})) + AddStringFlag(cmdImageCreate, "source", "", "", + "The OCI or DOCR source ref for the image", requiredOpt()) + + cmdImageDelete := CmdBuilder(cmd, RunMicroDropletImageDelete, "delete ...", + "Permanently delete one or more MicroDroplet images", + "Permanently deletes the specified MicroDroplet images. This is irreversible.", + Writer, aliasOpt("d", "rm")) + AddBoolFlag(cmdImageDelete, doctl.ArgForce, doctl.ArgShortForce, false, + "Delete the image(s) without a confirmation prompt") + + return cmd +} + +// RunMicroDropletList lists MicroDroplets, optionally filtered by region. +func RunMicroDropletList(c *CmdConfig) error { + region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug) + if err != nil { + return err + } + + svc := c.MicroDroplets() + + var list do.MicroDroplets + if region != "" { + list, err = svc.ListByRegion(region) + } else { + list, err = svc.List() + } + if err != nil { + return err + } + + return c.Display(&displayers.MicroDroplet{MicroDroplets: list}) +} + +// RunMicroDropletGet retrieves a MicroDroplet by its UUID. +func RunMicroDropletGet(c *CmdConfig) error { + if err := ensureOneArg(c); err != nil { + return err + } + md, err := c.MicroDroplets().Get(c.Args[0]) + if err != nil { + return err + } + return c.Display(&displayers.MicroDroplet{MicroDroplets: do.MicroDroplets{*md}}) +} + +// RunMicroDropletCreate creates a MicroDroplet with the provided configuration. +func RunMicroDropletCreate(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + name := c.Args[0] + + region, err := c.Doit.GetString(c.NS, doctl.ArgRegionSlug) + if err != nil { + return err + } + size, err := c.Doit.GetString(c.NS, doctl.ArgSizeSlug) + if err != nil { + return err + } + image, err := c.Doit.GetString(c.NS, doctl.ArgImage) + if err != nil { + return err + } + networking, err := c.Doit.GetString(c.NS, "networking") + if err != nil { + return err + } + vpcUUID, err := c.Doit.GetString(c.NS, doctl.ArgVPCUUID) + if err != nil { + return err + } + autoPauseEnabled, err := c.Doit.GetBool(c.NS, "auto-pause") + if err != nil { + return err + } + autoPauseIdle, err := c.Doit.GetString(c.NS, "auto-pause-idle-timeout") + if err != nil { + return err + } + autoResumeEnabled, err := c.Doit.GetBool(c.NS, "auto-resume") + if err != nil { + return err + } + httpPort, err := c.Doit.GetInt(c.NS, "http-port") + if err != nil { + return err + } + httpProtocol, err := c.Doit.GetString(c.NS, "http-protocol") + if err != nil { + return err + } + envPairs, err := c.Doit.GetStringSlice(c.NS, "env") + if err != nil { + return err + } + tags, err := c.Doit.GetStringSlice(c.NS, doctl.ArgTag) + if err != nil { + return err + } + + req := &godo.MicroDropletCreateRequest{ + Name: name, + Region: region, + Size: size, + Image: image, + } + if networking != "" { + req.Networking = godo.MicroDropletNetworking(networking) + } + if vpcUUID != "" { + req.VPCUUID = vpcUUID + } + if autoPauseEnabled || autoPauseIdle != "" { + req.AutoPause = &godo.AutoPauseConfig{IdleTimeout: autoPauseIdle} + if autoPauseEnabled { + enabled := true + req.AutoPause.Enabled = &enabled + } + } + if autoResumeEnabled { + enabled := true + req.AutoResume = &enabled + } + if httpPort > 0 { + req.HTTPPort = uint32(httpPort) + } + if httpProtocol != "" { + req.HTTPProtocol = godo.MicroDropletHTTPProtocol(httpProtocol) + } + if len(envPairs) > 0 { + env, err := parseEnvPairs(envPairs) + if err != nil { + return err + } + req.Environment = env + } + if len(tags) > 0 { + req.Tags = tags + } + + md, err := c.MicroDroplets().Create(req) + if err != nil { + return err + } + return c.Display(&displayers.MicroDroplet{MicroDroplets: do.MicroDroplets{*md}}) +} + +// RunMicroDropletPause transitions a MicroDroplet to the paused state. +func RunMicroDropletPause(c *CmdConfig) error { + return runMicroDropletStateTransition(c, godo.MicroDropletStatePaused) +} + +// RunMicroDropletResume transitions a MicroDroplet to the running state. +func RunMicroDropletResume(c *CmdConfig) error { + return runMicroDropletStateTransition(c, godo.MicroDropletStateRunning) +} + +func runMicroDropletStateTransition(c *CmdConfig, state godo.MicroDropletState) error { + if err := ensureOneArg(c); err != nil { + return err + } + md, err := c.MicroDroplets().Update(c.Args[0], &godo.MicroDropletUpdateRequest{State: state}) + if err != nil { + return err + } + return c.Display(&displayers.MicroDroplet{MicroDroplets: do.MicroDroplets{*md}}) +} + +// RunMicroDropletDelete deletes one or more MicroDroplets by UUID. +func RunMicroDropletDelete(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + force, err := c.Doit.GetBool(c.NS, doctl.ArgForce) + if err != nil { + return err + } + if !(force || AskForConfirmDelete("MicroDroplet", len(c.Args)) == nil) { + return errOperationAborted + } + + svc := c.MicroDroplets() + for _, id := range c.Args { + if err := svc.Delete(id); err != nil { + return fmt.Errorf("Unable to delete MicroDroplet %s: %v", id, err) + } + } + return nil +} + +// RunMicroDropletSnapshots lists snapshots for a MicroDroplet. +func RunMicroDropletSnapshots(c *CmdConfig) error { + if err := ensureOneArg(c); err != nil { + return err + } + snaps, err := c.MicroDroplets().ListSnapshots(c.Args[0]) + if err != nil { + return err + } + return c.Display(&displayers.MicroDropletSnapshot{Snapshots: snaps}) +} + +// RunMicroDropletImageList lists MicroDroplet images. +func RunMicroDropletImageList(c *CmdConfig) error { + imgs, err := c.MicroDropletImages().List() + if err != nil { + return err + } + return c.Display(&displayers.MicroDropletImage{Images: imgs}) +} + +// RunMicroDropletImageGet retrieves a MicroDroplet image by UUID. +func RunMicroDropletImageGet(c *CmdConfig) error { + if err := ensureOneArg(c); err != nil { + return err + } + img, err := c.MicroDropletImages().Get(c.Args[0]) + if err != nil { + return err + } + return c.Display(&displayers.MicroDropletImage{Images: do.MicroDropletImages{*img}}) +} + +// RunMicroDropletImageCreate imports a MicroDroplet image from an OCI ref. +func RunMicroDropletImageCreate(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + source, err := c.Doit.GetString(c.NS, "source") + if err != nil { + return err + } + img, err := c.MicroDropletImages().Create(&godo.MicroDropletImageCreateRequest{ + Name: c.Args[0], + Source: source, + }) + if err != nil { + return err + } + return c.Display(&displayers.MicroDropletImage{Images: do.MicroDropletImages{*img}}) +} + +// RunMicroDropletImageDelete deletes one or more MicroDroplet images. +func RunMicroDropletImageDelete(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + force, err := c.Doit.GetBool(c.NS, doctl.ArgForce) + if err != nil { + return err + } + if !(force || AskForConfirmDelete("MicroDroplet image", len(c.Args)) == nil) { + return errOperationAborted + } + + svc := c.MicroDropletImages() + for _, id := range c.Args { + if err := svc.Delete(id); err != nil { + return fmt.Errorf("Unable to delete MicroDroplet image %s: %v", id, err) + } + } + return nil +} + +// parseEnvPairs turns "KEY=VALUE" pairs into a map, returning an error on +// malformed entries. +func parseEnvPairs(pairs []string) (map[string]string, error) { + out := make(map[string]string, len(pairs)) + for _, p := range pairs { + key, value, ok := strings.Cut(p, "=") + if !ok || key == "" { + return nil, fmt.Errorf("invalid --env value %q: expected KEY=VALUE", p) + } + out[key] = value + } + return out, nil +} diff --git a/commands/microdroplets_test.go b/commands/microdroplets_test.go new file mode 100644 index 000000000..d02909174 --- /dev/null +++ b/commands/microdroplets_test.go @@ -0,0 +1,293 @@ +/* +Copyright 2025 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 ( + "testing" + + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/do" + "github.com/digitalocean/godo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testMicroDropletID = "b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2" + + testMicroDroplet = do.MicroDroplet{ + MicroDroplet: &godo.MicroDroplet{ + ID: testMicroDropletID, + Name: "sammy-microdroplet", + Region: "nyc1", + State: godo.MicroDropletStateRunning, + Size: "md-1vcpu-512mb", + Networking: godo.MicroDropletNetworkingPublic, + Image: "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", + Endpoint: "https://sammy.microdroplets.digitalocean.app", + Created: "2026-07-16T10:00:00Z", + }, + } + + testMicroDropletList = do.MicroDroplets{testMicroDroplet} + + testMicroDropletSnapshot = do.MicroDropletSnapshot{ + MicroDropletSnapshot: &godo.MicroDropletSnapshot{ + ID: "8f7a9a3f-5555-4444-9999-000000000001", + MicroDropletID: testMicroDropletID, + Name: "sammy-snap", + Status: godo.MicroDropletSnapshotStatusAvailable, + MemoryBytes: 512 * 1024 * 1024, + DiskBytes: 1024 * 1024 * 1024, + Created: "2026-07-16T10:05:00Z", + }, + } + + testMicroDropletImage = do.MicroDropletImage{ + MicroDropletImage: &godo.MicroDropletImage{ + ID: "aa11bb22-cc33-dd44-ee55-ff6600000000", + Name: "hello-world", + Source: "docker.io/library/hello-world:latest", + Status: godo.MicroDropletImageStatusAvailable, + Created: "2026-07-16T09:00:00Z", + }, + } +) + +func TestMicroDropletCommand(t *testing.T) { + cmd := MicroDroplet() + assert.NotNil(t, cmd) + assertCommandNames(t, cmd, + "create", "delete", "get", "image", "list", "pause", "resume", "snapshots", + ) +} + +func TestMicroDropletImageCommand(t *testing.T) { + cmd := microDropletImages() + assert.NotNil(t, cmd) + assertCommandNames(t, cmd, "create", "delete", "get", "list") +} + +func TestMicroDropletsList(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDroplets.EXPECT().List().Return(testMicroDropletList, nil) + err := RunMicroDropletList(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletsListByRegion(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDroplets.EXPECT().ListByRegion("nyc1").Return(testMicroDropletList, nil) + config.Doit.Set(config.NS, doctl.ArgRegionSlug, "nyc1") + err := RunMicroDropletList(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletGet(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDroplets.EXPECT().Get(testMicroDropletID).Return(&testMicroDroplet, nil) + config.Args = append(config.Args, testMicroDropletID) + err := RunMicroDropletGet(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletGet_missingArg(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + err := RunMicroDropletGet(config) + require.Error(t, err) + }) +} + +func TestMicroDropletCreate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + enabled := true + expected := &godo.MicroDropletCreateRequest{ + Name: "sammy-microdroplet", + Region: "nyc1", + Size: "md-1vcpu-512mb", + Image: "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", + Networking: godo.MicroDropletNetworkingVPC, + VPCUUID: "vpc-uuid-1234", + AutoPause: &godo.AutoPauseConfig{Enabled: &enabled, IdleTimeout: "5m"}, + AutoResume: &enabled, + HTTPPort: 8080, + HTTPProtocol: godo.MicroDropletHTTPProtocolHTTPS, + Environment: map[string]string{"FOO": "bar", "BAZ": "qux"}, + Tags: []string{"prod", "web"}, + } + tm.microDroplets.EXPECT().Create(expected).Return(&testMicroDroplet, nil) + + config.Args = append(config.Args, "sammy-microdroplet") + config.Doit.Set(config.NS, doctl.ArgRegionSlug, "nyc1") + config.Doit.Set(config.NS, doctl.ArgSizeSlug, "md-1vcpu-512mb") + config.Doit.Set(config.NS, doctl.ArgImage, "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000") + config.Doit.Set(config.NS, "networking", "vpc") + config.Doit.Set(config.NS, doctl.ArgVPCUUID, "vpc-uuid-1234") + config.Doit.Set(config.NS, "auto-pause", true) + config.Doit.Set(config.NS, "auto-pause-idle-timeout", "5m") + config.Doit.Set(config.NS, "auto-resume", true) + config.Doit.Set(config.NS, "http-port", 8080) + config.Doit.Set(config.NS, "http-protocol", "https") + config.Doit.Set(config.NS, "env", []string{"FOO=bar", "BAZ=qux"}) + config.Doit.Set(config.NS, doctl.ArgTag, []string{"prod", "web"}) + + err := RunMicroDropletCreate(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletCreate_minimal(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + expected := &godo.MicroDropletCreateRequest{ + Name: "sammy-microdroplet", + Region: "nyc1", + Size: "md-1vcpu-512mb", + Image: "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", + } + tm.microDroplets.EXPECT().Create(expected).Return(&testMicroDroplet, nil) + + config.Args = append(config.Args, "sammy-microdroplet") + config.Doit.Set(config.NS, doctl.ArgRegionSlug, "nyc1") + config.Doit.Set(config.NS, doctl.ArgSizeSlug, "md-1vcpu-512mb") + config.Doit.Set(config.NS, doctl.ArgImage, "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000") + + err := RunMicroDropletCreate(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletCreate_badEnv(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + config.Args = append(config.Args, "sammy-microdroplet") + config.Doit.Set(config.NS, doctl.ArgRegionSlug, "nyc1") + config.Doit.Set(config.NS, doctl.ArgSizeSlug, "md-1vcpu-512mb") + config.Doit.Set(config.NS, doctl.ArgImage, "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000") + config.Doit.Set(config.NS, "env", []string{"MALFORMED"}) + + err := RunMicroDropletCreate(config) + require.Error(t, err) + }) +} + +func TestMicroDropletPause(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + expected := &godo.MicroDropletUpdateRequest{State: godo.MicroDropletStatePaused} + tm.microDroplets.EXPECT().Update(testMicroDropletID, expected).Return(&testMicroDroplet, nil) + + config.Args = append(config.Args, testMicroDropletID) + err := RunMicroDropletPause(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletResume(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + expected := &godo.MicroDropletUpdateRequest{State: godo.MicroDropletStateRunning} + tm.microDroplets.EXPECT().Update(testMicroDropletID, expected).Return(&testMicroDroplet, nil) + + config.Args = append(config.Args, testMicroDropletID) + err := RunMicroDropletResume(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletDelete(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDroplets.EXPECT().Delete(testMicroDropletID).Return(nil) + + config.Args = append(config.Args, testMicroDropletID) + config.Doit.Set(config.NS, doctl.ArgForce, true) + + err := RunMicroDropletDelete(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletDelete_multiple(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + ids := []string{testMicroDropletID, "aabbccdd-1111-2222-3333-444455556666"} + for _, id := range ids { + tm.microDroplets.EXPECT().Delete(id).Return(nil) + } + + config.Args = append(config.Args, ids...) + config.Doit.Set(config.NS, doctl.ArgForce, true) + + err := RunMicroDropletDelete(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletSnapshots(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDroplets.EXPECT().ListSnapshots(testMicroDropletID).Return( + do.MicroDropletSnapshots{testMicroDropletSnapshot}, nil, + ) + + config.Args = append(config.Args, testMicroDropletID) + err := RunMicroDropletSnapshots(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletImageList(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDropletImages.EXPECT().List().Return( + do.MicroDropletImages{testMicroDropletImage}, nil, + ) + err := RunMicroDropletImageList(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletImageGet(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDropletImages.EXPECT().Get(testMicroDropletImage.ID).Return(&testMicroDropletImage, nil) + + config.Args = append(config.Args, testMicroDropletImage.ID) + err := RunMicroDropletImageGet(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletImageCreate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + expected := &godo.MicroDropletImageCreateRequest{ + Name: "hello-world", + Source: "docker.io/library/hello-world:latest", + } + tm.microDropletImages.EXPECT().Create(expected).Return(&testMicroDropletImage, nil) + + config.Args = append(config.Args, "hello-world") + config.Doit.Set(config.NS, "source", "docker.io/library/hello-world:latest") + + err := RunMicroDropletImageCreate(config) + require.NoError(t, err) + }) +} + +func TestMicroDropletImageDelete(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.microDropletImages.EXPECT().Delete(testMicroDropletImage.ID).Return(nil) + + config.Args = append(config.Args, testMicroDropletImage.ID) + config.Doit.Set(config.NS, doctl.ArgForce, true) + + err := RunMicroDropletImageDelete(config) + require.NoError(t, err) + }) +} diff --git a/do/micro_droplet_images.go b/do/micro_droplet_images.go new file mode 100644 index 000000000..599efa0e5 --- /dev/null +++ b/do/micro_droplet_images.go @@ -0,0 +1,98 @@ +/* +Copyright 2025 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" +) + +// MicroDropletImage wraps a godo.MicroDropletImage. +type MicroDropletImage struct { + *godo.MicroDropletImage +} + +// MicroDropletImages is a slice of MicroDropletImage. +type MicroDropletImages []MicroDropletImage + +// MicroDropletImagesService is an interface for interacting with DigitalOcean's +// MicroDroplet image API. +type MicroDropletImagesService interface { + List() (MicroDropletImages, error) + Get(id string) (*MicroDropletImage, error) + Create(req *godo.MicroDropletImageCreateRequest) (*MicroDropletImage, error) + Delete(id string) error +} + +type microDropletImagesService struct { + client *godo.Client +} + +var _ MicroDropletImagesService = µDropletImagesService{} + +// NewMicroDropletImagesService builds a MicroDropletImagesService backed by +// the provided godo client. +func NewMicroDropletImagesService(client *godo.Client) MicroDropletImagesService { + return µDropletImagesService{client: client} +} + +func (s *microDropletImagesService) List() (MicroDropletImages, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + list, resp, err := s.client.MicroDropletImages.List(context.TODO(), opt) + if err != nil { + return nil, nil, err + } + + si := make([]any, 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(MicroDropletImages, len(si)) + for i := range si { + img := si[i].(godo.MicroDropletImage) + list[i] = MicroDropletImage{MicroDropletImage: &img} + } + return list, nil +} + +func (s *microDropletImagesService) Get(id string) (*MicroDropletImage, error) { + img, _, err := s.client.MicroDropletImages.Get(context.TODO(), id) + if err != nil { + return nil, err + } + return &MicroDropletImage{MicroDropletImage: img}, nil +} + +func (s *microDropletImagesService) Create(req *godo.MicroDropletImageCreateRequest) (*MicroDropletImage, error) { + img, _, err := s.client.MicroDropletImages.Create(context.TODO(), req) + if err != nil { + return nil, err + } + return &MicroDropletImage{MicroDropletImage: img}, nil +} + +func (s *microDropletImagesService) Delete(id string) error { + _, err := s.client.MicroDropletImages.Delete(context.TODO(), id) + return err +} diff --git a/do/microdroplets.go b/do/microdroplets.go new file mode 100644 index 000000000..61ec37017 --- /dev/null +++ b/do/microdroplets.go @@ -0,0 +1,164 @@ +/* +Copyright 2025 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" +) + +// MicroDroplet wraps a godo.MicroDroplet. +type MicroDroplet struct { + *godo.MicroDroplet +} + +// MicroDroplets is a slice of MicroDroplet. +type MicroDroplets []MicroDroplet + +// MicroDropletSnapshot wraps a godo.MicroDropletSnapshot. +type MicroDropletSnapshot struct { + *godo.MicroDropletSnapshot +} + +// MicroDropletSnapshots is a slice of MicroDropletSnapshot. +type MicroDropletSnapshots []MicroDropletSnapshot + +// MicroDropletsService is an interface for interacting with DigitalOcean's +// MicroDroplet API. +type MicroDropletsService interface { + List() (MicroDroplets, error) + ListByRegion(region string) (MicroDroplets, error) + ListByName(name string) (MicroDroplets, error) + Get(id string) (*MicroDroplet, error) + Create(req *godo.MicroDropletCreateRequest) (*MicroDroplet, error) + Update(id string, req *godo.MicroDropletUpdateRequest) (*MicroDroplet, error) + Delete(id string) error + ListSnapshots(id string) (MicroDropletSnapshots, error) +} + +type microDropletsService struct { + client *godo.Client +} + +var _ MicroDropletsService = µDropletsService{} + +// NewMicroDropletsService builds a MicroDropletsService backed by the provided +// godo client. +func NewMicroDropletsService(client *godo.Client) MicroDropletsService { + return µDropletsService{client: client} +} + +func (s *microDropletsService) List() (MicroDroplets, error) { + return s.paginate(func(opt *godo.ListOptions) ([]godo.MicroDroplet, *godo.Response, error) { + return s.client.MicroDroplets.List(context.TODO(), opt) + }) +} + +func (s *microDropletsService) ListByRegion(region string) (MicroDroplets, error) { + return s.paginate(func(opt *godo.ListOptions) ([]godo.MicroDroplet, *godo.Response, error) { + return s.client.MicroDroplets.ListByRegion(context.TODO(), region, opt) + }) +} + +func (s *microDropletsService) ListByName(name string) (MicroDroplets, error) { + return s.paginate(func(opt *godo.ListOptions) ([]godo.MicroDroplet, *godo.Response, error) { + return s.client.MicroDroplets.ListByName(context.TODO(), name, opt) + }) +} + +func (s *microDropletsService) paginate(lister func(*godo.ListOptions) ([]godo.MicroDroplet, *godo.Response, error)) (MicroDroplets, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + list, resp, err := lister(opt) + if err != nil { + return nil, nil, err + } + + si := make([]any, 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(MicroDroplets, len(si)) + for i := range si { + md := si[i].(godo.MicroDroplet) + list[i] = MicroDroplet{MicroDroplet: &md} + } + return list, nil +} + +func (s *microDropletsService) Get(id string) (*MicroDroplet, error) { + md, _, err := s.client.MicroDroplets.Get(context.TODO(), id) + if err != nil { + return nil, err + } + return &MicroDroplet{MicroDroplet: md}, nil +} + +func (s *microDropletsService) Create(req *godo.MicroDropletCreateRequest) (*MicroDroplet, error) { + md, _, err := s.client.MicroDroplets.Create(context.TODO(), req) + if err != nil { + return nil, err + } + return &MicroDroplet{MicroDroplet: md}, nil +} + +func (s *microDropletsService) Update(id string, req *godo.MicroDropletUpdateRequest) (*MicroDroplet, error) { + md, _, err := s.client.MicroDroplets.Update(context.TODO(), id, req) + if err != nil { + return nil, err + } + return &MicroDroplet{MicroDroplet: md}, nil +} + +func (s *microDropletsService) Delete(id string) error { + _, err := s.client.MicroDroplets.Delete(context.TODO(), id) + return err +} + +func (s *microDropletsService) ListSnapshots(id string) (MicroDropletSnapshots, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + list, resp, err := s.client.MicroDroplets.ListSnapshots(context.TODO(), id, opt) + if err != nil { + return nil, nil, err + } + + si := make([]any, 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(MicroDropletSnapshots, len(si)) + for i := range si { + snap := si[i].(godo.MicroDropletSnapshot) + list[i] = MicroDropletSnapshot{MicroDropletSnapshot: &snap} + } + return list, nil +} diff --git a/do/mocks/MicroDropletImagesService.go b/do/mocks/MicroDropletImagesService.go new file mode 100644 index 000000000..7032f44f6 --- /dev/null +++ b/do/mocks/MicroDropletImagesService.go @@ -0,0 +1,101 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: micro_droplet_images.go +// +// Generated by this command: +// +// mockgen -source micro_droplet_images.go -package=mocks MicroDropletImagesService +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + do "github.com/digitalocean/doctl/do" + godo "github.com/digitalocean/godo" + gomock "go.uber.org/mock/gomock" +) + +// MockMicroDropletImagesService is a mock of MicroDropletImagesService interface. +type MockMicroDropletImagesService struct { + ctrl *gomock.Controller + recorder *MockMicroDropletImagesServiceMockRecorder + isgomock struct{} +} + +// MockMicroDropletImagesServiceMockRecorder is the mock recorder for MockMicroDropletImagesService. +type MockMicroDropletImagesServiceMockRecorder struct { + mock *MockMicroDropletImagesService +} + +// NewMockMicroDropletImagesService creates a new mock instance. +func NewMockMicroDropletImagesService(ctrl *gomock.Controller) *MockMicroDropletImagesService { + mock := &MockMicroDropletImagesService{ctrl: ctrl} + mock.recorder = &MockMicroDropletImagesServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMicroDropletImagesService) EXPECT() *MockMicroDropletImagesServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockMicroDropletImagesService) Create(req *godo.MicroDropletImageCreateRequest) (*do.MicroDropletImage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", req) + ret0, _ := ret[0].(*do.MicroDropletImage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockMicroDropletImagesServiceMockRecorder) Create(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockMicroDropletImagesService)(nil).Create), req) +} + +// Delete mocks base method. +func (m *MockMicroDropletImagesService) Delete(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockMicroDropletImagesServiceMockRecorder) Delete(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockMicroDropletImagesService)(nil).Delete), id) +} + +// Get mocks base method. +func (m *MockMicroDropletImagesService) Get(id string) (*do.MicroDropletImage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", id) + ret0, _ := ret[0].(*do.MicroDropletImage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockMicroDropletImagesServiceMockRecorder) Get(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMicroDropletImagesService)(nil).Get), id) +} + +// List mocks base method. +func (m *MockMicroDropletImagesService) List() (do.MicroDropletImages, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List") + ret0, _ := ret[0].(do.MicroDropletImages) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockMicroDropletImagesServiceMockRecorder) List() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockMicroDropletImagesService)(nil).List)) +} diff --git a/do/mocks/MicroDropletsService.go b/do/mocks/MicroDropletsService.go new file mode 100644 index 000000000..86e4f17dc --- /dev/null +++ b/do/mocks/MicroDropletsService.go @@ -0,0 +1,161 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: microdroplets.go +// +// Generated by this command: +// +// mockgen -source microdroplets.go -package=mocks MicroDropletsService +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + do "github.com/digitalocean/doctl/do" + godo "github.com/digitalocean/godo" + gomock "go.uber.org/mock/gomock" +) + +// MockMicroDropletsService is a mock of MicroDropletsService interface. +type MockMicroDropletsService struct { + ctrl *gomock.Controller + recorder *MockMicroDropletsServiceMockRecorder + isgomock struct{} +} + +// MockMicroDropletsServiceMockRecorder is the mock recorder for MockMicroDropletsService. +type MockMicroDropletsServiceMockRecorder struct { + mock *MockMicroDropletsService +} + +// NewMockMicroDropletsService creates a new mock instance. +func NewMockMicroDropletsService(ctrl *gomock.Controller) *MockMicroDropletsService { + mock := &MockMicroDropletsService{ctrl: ctrl} + mock.recorder = &MockMicroDropletsServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMicroDropletsService) EXPECT() *MockMicroDropletsServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockMicroDropletsService) Create(req *godo.MicroDropletCreateRequest) (*do.MicroDroplet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", req) + ret0, _ := ret[0].(*do.MicroDroplet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockMicroDropletsServiceMockRecorder) Create(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockMicroDropletsService)(nil).Create), req) +} + +// Delete mocks base method. +func (m *MockMicroDropletsService) Delete(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockMicroDropletsServiceMockRecorder) Delete(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockMicroDropletsService)(nil).Delete), id) +} + +// Get mocks base method. +func (m *MockMicroDropletsService) Get(id string) (*do.MicroDroplet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", id) + ret0, _ := ret[0].(*do.MicroDroplet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockMicroDropletsServiceMockRecorder) Get(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMicroDropletsService)(nil).Get), id) +} + +// List mocks base method. +func (m *MockMicroDropletsService) List() (do.MicroDroplets, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List") + ret0, _ := ret[0].(do.MicroDroplets) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockMicroDropletsServiceMockRecorder) List() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockMicroDropletsService)(nil).List)) +} + +// ListByName mocks base method. +func (m *MockMicroDropletsService) ListByName(name string) (do.MicroDroplets, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByName", name) + ret0, _ := ret[0].(do.MicroDroplets) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByName indicates an expected call of ListByName. +func (mr *MockMicroDropletsServiceMockRecorder) ListByName(name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByName", reflect.TypeOf((*MockMicroDropletsService)(nil).ListByName), name) +} + +// ListByRegion mocks base method. +func (m *MockMicroDropletsService) ListByRegion(region string) (do.MicroDroplets, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByRegion", region) + ret0, _ := ret[0].(do.MicroDroplets) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListByRegion indicates an expected call of ListByRegion. +func (mr *MockMicroDropletsServiceMockRecorder) ListByRegion(region any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByRegion", reflect.TypeOf((*MockMicroDropletsService)(nil).ListByRegion), region) +} + +// ListSnapshots mocks base method. +func (m *MockMicroDropletsService) ListSnapshots(id string) (do.MicroDropletSnapshots, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSnapshots", id) + ret0, _ := ret[0].(do.MicroDropletSnapshots) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSnapshots indicates an expected call of ListSnapshots. +func (mr *MockMicroDropletsServiceMockRecorder) ListSnapshots(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSnapshots", reflect.TypeOf((*MockMicroDropletsService)(nil).ListSnapshots), id) +} + +// Update mocks base method. +func (m *MockMicroDropletsService) Update(id string, req *godo.MicroDropletUpdateRequest) (*do.MicroDroplet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", id, req) + ret0, _ := ret[0].(*do.MicroDroplet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockMicroDropletsServiceMockRecorder) Update(id, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockMicroDropletsService)(nil).Update), id, req) +} diff --git a/go.mod b/go.mod index 05e79b250..b78283851 100644 --- a/go.mod +++ b/go.mod @@ -135,3 +135,6 @@ require ( sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) + +// TODO(MDROP-21): drop once godo microdroplet support is tagged and released. +replace github.com/digitalocean/godo => ../godo diff --git a/go.sum b/go.sum index c0d8cc6fe..9a3d9e8aa 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/digitalocean/godo v1.198.0 h1:jylPVf1efxpOZnTOSc3K4a+Qse9pTl/AHu+GExthMNw= -github.com/digitalocean/godo v1.198.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc= diff --git a/integration/microdroplet_create_test.go b/integration/microdroplet_create_test.go new file mode 100644 index 000000000..5cdb4fa82 --- /dev/null +++ b/integration/microdroplet_create_test.go @@ -0,0 +1,115 @@ +package integration + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/microdroplet/create", 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/microdroplets/instances": + 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 := io.ReadAll(req.Body) + expect.NoError(err) + + var got map[string]any + expect.NoError(json.Unmarshal(reqBody, &got)) + expect.Equal("sammy-microdroplet", got["name"]) + expect.Equal("nyc1", got["region"]) + expect.Equal("md-1vcpu-512mb", got["size"]) + expect.Equal("do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", got["image"]) + + w.WriteHeader(http.StatusCreated) + w.Write([]byte(microDropletCreateResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("all required flags are passed", func() { + it("creates a microdroplet", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", "microdroplet", "create", "sammy-microdroplet", + "--region", "nyc1", + "--size", "md-1vcpu-512mb", + "--image", "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(microDropletCreateOutput), strings.TrimSpace(string(output))) + }) + }) + + when("missing required flags", func() { + it("returns an error", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", "https://www.example.com", + "compute", "microdroplet", "create", "sammy-microdroplet", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "missing required arguments") + }) + }) +}) + +const ( + microDropletCreateOutput = ` +ID Name Region State Size Networking Image Endpoint Created At +b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2 sammy-microdroplet nyc1 creating md-1vcpu-512mb public do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000 2026-07-16T10:00:00Z +` + microDropletCreateResponse = ` +{ + "micro_droplet": { + "id": "b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2", + "name": "sammy-microdroplet", + "region": "nyc1", + "state": "creating", + "size": "md-1vcpu-512mb", + "networking": "public", + "image": "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", + "created_at": "2026-07-16T10:00:00Z" + } +} +` +) diff --git a/integration/microdroplet_list_test.go b/integration/microdroplet_list_test.go new file mode 100644 index 000000000..b3de41d51 --- /dev/null +++ b/integration/microdroplet_list_test.go @@ -0,0 +1,110 @@ +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/microdroplet/list", 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/microdroplets/instances": + 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 + } + + if region := req.URL.Query().Get("region"); region == "nyc1" { + w.Write([]byte(microDropletListResponse)) + return + } + + w.Write([]byte(microDropletListResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("no flags are passed", func() { + it("lists all microdroplets", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", "microdroplet", "list", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(microDropletListOutput), strings.TrimSpace(string(output))) + }) + }) + + when("a region is provided", func() { + it("filters microdroplets by region", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", "microdroplet", "list", + "--region", "nyc1", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(microDropletListOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + microDropletListOutput = ` +ID Name Region State Size Networking Image Endpoint Created At +b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2 sammy-microdroplet nyc1 running md-1vcpu-512mb public do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000 https://sammy.microdroplets.digitalocean.app 2026-07-16T10:00:00Z +` + microDropletListResponse = ` +{ + "micro_droplets": [ + { + "id": "b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2", + "name": "sammy-microdroplet", + "region": "nyc1", + "state": "running", + "size": "md-1vcpu-512mb", + "networking": "public", + "image": "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", + "endpoint": "https://sammy.microdroplets.digitalocean.app", + "created_at": "2026-07-16T10:00:00Z" + } + ], + "links": {}, + "meta": {"total": 1} +} +` +) diff --git a/integration/microdroplet_pause_test.go b/integration/microdroplet_pause_test.go new file mode 100644 index 000000000..068d890c6 --- /dev/null +++ b/integration/microdroplet_pause_test.go @@ -0,0 +1,96 @@ +package integration + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/microdroplet/pause", 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/microdroplets/instances/b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPatch { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + reqBody, err := io.ReadAll(req.Body) + expect.NoError(err) + + var got map[string]any + expect.NoError(json.Unmarshal(reqBody, &got)) + expect.Equal("paused", got["state"]) + + w.Write([]byte(microDropletPauseResponse)) + 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 id is provided", func() { + it("pauses the microdroplet", func() { + cmd := exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", "microdroplet", "pause", + "b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(microDropletPauseOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + microDropletPauseOutput = ` +ID Name Region State Size Networking Image Endpoint Created At +b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2 sammy-microdroplet nyc1 paused md-1vcpu-512mb public do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000 https://sammy.microdroplets.digitalocean.app 2026-07-16T10:00:00Z +` + microDropletPauseResponse = ` +{ + "micro_droplet": { + "id": "b2a2f7a4-8d34-4c1c-9c66-3f2b7f8f38f2", + "name": "sammy-microdroplet", + "region": "nyc1", + "state": "paused", + "size": "md-1vcpu-512mb", + "networking": "public", + "image": "do:microdroplet-image:0f0f0f0f-0000-0000-0000-000000000000", + "endpoint": "https://sammy.microdroplets.digitalocean.app", + "created_at": "2026-07-16T10:00:00Z" + } +} +` +) diff --git a/scripts/regenmocks.sh b/scripts/regenmocks.sh index 5e97e3ed4..1621a6438 100755 --- a/scripts/regenmocks.sh +++ b/scripts/regenmocks.sh @@ -53,6 +53,8 @@ mockgen -source gradientai.go -package=mocks GradientAIService > mocks/GradientA mockgen -source inference.go -package=mocks InferenceService > mocks/InferenceService.go mockgen -source nfs.go -package=mocks NfsService > mocks/NfsService.go mockgen -source nfs_actions.go -package=mocks NfsActionsService > mocks/NfsActionsService.go +mockgen -source microdroplets.go -package=mocks MicroDropletsService > mocks/MicroDropletsService.go +mockgen -source micro_droplet_images.go -package=mocks MicroDropletImagesService > mocks/MicroDropletImagesService.go mockgen -source security.go -package=mocks SecurityService > mocks/SecurityService.go mockgen -source secrets.go -package=mocks SecretsService > mocks/SecretsService.go mockgen -source vector_databases.go -package=mocks VectorDBsService > mocks/VectorDBsService.go diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md index 5b8b65dc3..7ce103d9e 100644 --- a/vendor/github.com/digitalocean/godo/CHANGELOG.md +++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md @@ -1,12 +1,5 @@ # Change Log -## [1.198.0] - 2026-07-07 - -- #1043 - @johannaratliff - Support subnet_uuid in vpcnatgateway -- #1049 - @ritheek-pitti-do - gradientai: remove deprecated SaveAsPreset field -- #1041 - @sshirolkar - add support for custom metrics rpcs -- #1044 - @lprasanth-nadiminti - DBAAS-8142: Add GetDOSettings and UpdateDOSettings - ## [1.197.0] - 2026-06-25 - #1038 - @v-amanjain-afk - nfs access point apis diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index f32ae97a8..9d82e82ad 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -21,7 +21,7 @@ import ( ) const ( - libraryVersion = "1.198.0" + libraryVersion = "1.197.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -77,6 +77,8 @@ type Client struct { Keys KeysService Kubernetes KubernetesService LoadBalancers LoadBalancersService + MicroDroplets MicroDropletsService + MicroDropletImages MicroDropletImagesService Monitoring MonitoringService Security SecurityService Secrets SecretsService @@ -319,6 +321,8 @@ func NewClient(httpClient *http.Client) *Client { c.Keys = &KeysServiceOp{client: c} c.Kubernetes = &KubernetesServiceOp{client: c} c.LoadBalancers = &LoadBalancersServiceOp{client: c} + c.MicroDroplets = &MicroDropletsServiceOp{client: c} + c.MicroDropletImages = &MicroDropletImagesServiceOp{client: c} c.Monitoring = &MonitoringServiceOp{client: c} c.Security = &SecurityServiceOp{client: c} c.Secrets = &SecretsServiceOp{client: c} diff --git a/vendor/github.com/digitalocean/godo/micro_droplet_images.go b/vendor/github.com/digitalocean/godo/micro_droplet_images.go new file mode 100644 index 000000000..55f0a0e80 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/micro_droplet_images.go @@ -0,0 +1,168 @@ +package godo + +import ( + "context" + "fmt" + "net/http" +) + +const microDropletImageBasePath = "v2/microdroplets/images" + +// MicroDropletImageStatus represents the lifecycle status of a MicroDroplet image. +type MicroDropletImageStatus string + +// Possible states for a MicroDroplet image. +const ( + MicroDropletImageStatusUnknown = MicroDropletImageStatus("IMAGE_UNKNOWN") + MicroDropletImageStatusImporting = MicroDropletImageStatus("IMAGE_IMPORTING") + MicroDropletImageStatusAvailable = MicroDropletImageStatus("IMAGE_AVAILABLE") + MicroDropletImageStatusFailed = MicroDropletImageStatus("IMAGE_FAILED") + MicroDropletImageStatusDeleted = MicroDropletImageStatus("IMAGE_DELETED") +) + +// MicroDropletImagesService is an interface for interfacing with the +// MicroDroplet image endpoints of the DigitalOcean API. +// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/MicroDroplets +type MicroDropletImagesService interface { + List(ctx context.Context, opt *ListOptions) ([]MicroDropletImage, *Response, error) + Get(ctx context.Context, id string) (*MicroDropletImage, *Response, error) + Create(ctx context.Context, createRequest *MicroDropletImageCreateRequest) (*MicroDropletImage, *Response, error) + Delete(ctx context.Context, id string) (*Response, error) +} + +// MicroDropletImagesServiceOp handles communication with the MicroDroplet +// image related methods of the DigitalOcean API. +type MicroDropletImagesServiceOp struct { + client *Client +} + +var _ MicroDropletImagesService = &MicroDropletImagesServiceOp{} + +// MicroDropletImage represents an OCI image imported for use with MicroDroplets. +type MicroDropletImage struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Source string `json:"source,omitempty"` + Status MicroDropletImageStatus `json:"status,omitempty"` + Created string `json:"created_at,omitempty"` +} + +// MicroDropletImageCreateRequest represents a request to import a new +// MicroDroplet image from a public OCI ref or a DOCR ref. +type MicroDropletImageCreateRequest struct { + Name string `json:"name"` + Source string `json:"source"` +} + +// String returns a human-readable description of a MicroDropletImage. +func (i MicroDropletImage) String() string { + return Stringify(i) +} + +// URN returns the MicroDropletImage ID in a valid DO API URN form. +func (i MicroDropletImage) URN() string { + return ToURN("MicroDropletImage", i.ID) +} + +// String returns a human-readable description of a MicroDropletImageCreateRequest. +func (r MicroDropletImageCreateRequest) String() string { + return Stringify(r) +} + +type microDropletImageRoot struct { + Image *MicroDropletImage `json:"image"` +} + +type microDropletImagesRoot struct { + Images []MicroDropletImage `json:"images"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +// List lists all MicroDroplet images, with optional pagination. +func (s *MicroDropletImagesServiceOp) List(ctx context.Context, opt *ListOptions) ([]MicroDropletImage, *Response, error) { + path, err := addOptions(microDropletImageBasePath, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microDropletImagesRoot) + resp, err := s.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.Images, resp, nil +} + +// Get retrieves a MicroDroplet image by its ID. +func (s *MicroDropletImagesServiceOp) Get(ctx context.Context, id string) (*MicroDropletImage, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microDropletImageBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microDropletImageRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Image, resp, nil +} + +// Create imports a new MicroDroplet image from an OCI ref. Image import is +// asynchronous; callers should poll Get and inspect Status until it reports +// MicroDropletImageStatusAvailable or MicroDropletImageStatusFailed. +func (s *MicroDropletImagesServiceOp) Create(ctx context.Context, createRequest *MicroDropletImageCreateRequest) (*MicroDropletImage, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, microDropletImageBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(microDropletImageRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Image, resp, nil +} + +// Delete removes a MicroDroplet image by its ID. The DigitalOcean API returns +// a 204 on success and does not include a response body. +func (s *MicroDropletImagesServiceOp) Delete(ctx context.Context, id string) (*Response, error) { + if id == "" { + return nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microDropletImageBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/vendor/github.com/digitalocean/godo/micro_droplets.go b/vendor/github.com/digitalocean/godo/micro_droplets.go new file mode 100644 index 000000000..df99c3f8c --- /dev/null +++ b/vendor/github.com/digitalocean/godo/micro_droplets.go @@ -0,0 +1,359 @@ +package godo + +import ( + "context" + "fmt" + "net/http" +) + +const microDropletBasePath = "v2/microdroplets/instances" + +// MicroDropletState represents the lifecycle state of a MicroDroplet. +type MicroDropletState string + +// Possible lifecycle states for a MicroDroplet. +const ( + MicroDropletStateUnknown = MicroDropletState("unknown") + MicroDropletStateCreating = MicroDropletState("creating") + MicroDropletStateRunning = MicroDropletState("running") + MicroDropletStatePausing = MicroDropletState("pausing") + MicroDropletStatePaused = MicroDropletState("paused") + MicroDropletStateResuming = MicroDropletState("resuming") + MicroDropletStateTerminating = MicroDropletState("terminating") + MicroDropletStateTerminated = MicroDropletState("terminated") + MicroDropletStateFailed = MicroDropletState("failed") +) + +// MicroDropletNetworking represents the networking mode of a MicroDroplet. +type MicroDropletNetworking string + +// Possible networking modes for a MicroDroplet. +const ( + MicroDropletNetworkingUnknown = MicroDropletNetworking("unknown") + MicroDropletNetworkingPublic = MicroDropletNetworking("public") + MicroDropletNetworkingVPC = MicroDropletNetworking("vpc") +) + +// MicroDropletHTTPProtocol represents the HTTP protocol option for a MicroDroplet. +type MicroDropletHTTPProtocol string + +// Possible HTTP protocol values for a MicroDroplet. +const ( + MicroDropletHTTPProtocolHTTP = MicroDropletHTTPProtocol("http") + MicroDropletHTTPProtocolHTTPS = MicroDropletHTTPProtocol("https") + MicroDropletHTTPProtocolHTTP2 = MicroDropletHTTPProtocol("http2") +) + +// MicroDropletSnapshotStatus represents the status of a MicroDroplet snapshot. +type MicroDropletSnapshotStatus string + +// Possible states for a MicroDroplet snapshot. +const ( + MicroDropletSnapshotStatusUnknown = MicroDropletSnapshotStatus("SNAPSHOT_UNKNOWN") + MicroDropletSnapshotStatusCreating = MicroDropletSnapshotStatus("SNAPSHOT_CREATING") + MicroDropletSnapshotStatusAvailable = MicroDropletSnapshotStatus("SNAPSHOT_AVAILABLE") + MicroDropletSnapshotStatusFailed = MicroDropletSnapshotStatus("SNAPSHOT_FAILED") + MicroDropletSnapshotStatusDeleted = MicroDropletSnapshotStatus("SNAPSHOT_DELETED") +) + +// MicroDropletsService is an interface for interfacing with the MicroDroplet +// endpoints of the DigitalOcean API. +// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/MicroDroplets +type MicroDropletsService interface { + List(ctx context.Context, opt *ListOptions) ([]MicroDroplet, *Response, error) + ListByRegion(ctx context.Context, region string, opt *ListOptions) ([]MicroDroplet, *Response, error) + ListByName(ctx context.Context, name string, opt *ListOptions) ([]MicroDroplet, *Response, error) + Get(ctx context.Context, id string) (*MicroDroplet, *Response, error) + Create(ctx context.Context, createRequest *MicroDropletCreateRequest) (*MicroDroplet, *Response, error) + Update(ctx context.Context, id string, updateRequest *MicroDropletUpdateRequest) (*MicroDroplet, *Response, error) + Delete(ctx context.Context, id string) (*Response, error) + ListSnapshots(ctx context.Context, id string, opt *ListOptions) ([]MicroDropletSnapshot, *Response, error) +} + +// MicroDropletsServiceOp handles communication with the MicroDroplet related +// methods of the DigitalOcean API. +type MicroDropletsServiceOp struct { + client *Client +} + +var _ MicroDropletsService = &MicroDropletsServiceOp{} + +// MicroDroplet represents a DigitalOcean MicroDroplet. +type MicroDroplet struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + State MicroDropletState `json:"state,omitempty"` + Size string `json:"size,omitempty"` + Networking MicroDropletNetworking `json:"networking,omitempty"` + Image string `json:"image,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + AutoPause *AutoPauseConfig `json:"auto_pause,omitempty"` + AutoResume *bool `json:"auto_resume,omitempty"` + Created string `json:"created_at,omitempty"` +} + +// AutoPauseConfig configures MicroDroplet auto-pause behavior. IdleTimeout is +// a Go duration string (e.g. "5m", "30s") describing how long the MicroDroplet +// must be idle before it is paused. +type AutoPauseConfig struct { + Enabled *bool `json:"enabled,omitempty"` + IdleTimeout string `json:"idle_timeout,omitempty"` +} + +// MicroDropletSnapshot represents a snapshot of a MicroDroplet, taken +// automatically when the MicroDroplet is paused. +type MicroDropletSnapshot struct { + ID string `json:"id,omitempty"` + MicroDropletID string `json:"micro_droplet_id,omitempty"` + Status MicroDropletSnapshotStatus `json:"status,omitempty"` + Name string `json:"name,omitempty"` + MemoryBytes uint64 `json:"memory_bytes,omitempty"` + DiskBytes uint64 `json:"disk_bytes,omitempty"` + Created string `json:"created_at,omitempty"` +} + +// MicroDropletCreateRequest represents a request to create a MicroDroplet. +type MicroDropletCreateRequest struct { + Name string `json:"name"` + Region string `json:"region"` + Size string `json:"size"` + Image string `json:"image"` + Networking MicroDropletNetworking `json:"networking,omitempty"` + VPCUUID string `json:"vpc_uuid,omitempty"` + AutoPause *AutoPauseConfig `json:"auto_pause,omitempty"` + AutoResume *bool `json:"auto_resume,omitempty"` + HTTPPort uint32 `json:"http_port,omitempty"` + HTTPProtocol MicroDropletHTTPProtocol `json:"http_protocol,omitempty"` + Environment map[string]string `json:"environment,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// MicroDropletUpdateRequest represents a partial update to a MicroDroplet. +// +// The only currently supported mutation is a state transition. Set State to +// MicroDropletStatePaused to pause a running MicroDroplet, or to +// MicroDropletStateRunning to resume a paused MicroDroplet. +type MicroDropletUpdateRequest struct { + State MicroDropletState `json:"state,omitempty"` +} + +// String returns a human-readable description of a MicroDroplet. +func (m MicroDroplet) String() string { + return Stringify(m) +} + +// URN returns the MicroDroplet ID in a valid DO API URN form. +func (m MicroDroplet) URN() string { + return ToURN("MicroDroplet", m.ID) +} + +// String returns a human-readable description of a MicroDropletSnapshot. +func (s MicroDropletSnapshot) String() string { + return Stringify(s) +} + +// String returns a human-readable description of a MicroDropletCreateRequest. +func (r MicroDropletCreateRequest) String() string { + return Stringify(r) +} + +// String returns a human-readable description of a MicroDropletUpdateRequest. +func (r MicroDropletUpdateRequest) String() string { + return Stringify(r) +} + +type microDropletRoot struct { + MicroDroplet *MicroDroplet `json:"micro_droplet"` +} + +type microDropletsRoot struct { + MicroDroplets []MicroDroplet `json:"micro_droplets"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +type microDropletSnapshotsRoot struct { + Snapshots []MicroDropletSnapshot `json:"snapshots"` + Links *Links `json:"links"` + Meta *Meta `json:"meta"` +} + +// listMicroDropletOptions holds MicroDroplet-specific list filters that are +// not part of the shared ListOptions. +type listMicroDropletOptions struct { + Region string `url:"region,omitempty"` + Name string `url:"name,omitempty"` +} + +// List lists all MicroDroplets, with optional pagination. +func (s *MicroDropletsServiceOp) List(ctx context.Context, opt *ListOptions) ([]MicroDroplet, *Response, error) { + return s.list(ctx, opt, nil) +} + +// ListByRegion lists MicroDroplets filtered by region slug, with optional pagination. +func (s *MicroDropletsServiceOp) ListByRegion(ctx context.Context, region string, opt *ListOptions) ([]MicroDroplet, *Response, error) { + if region == "" { + return nil, nil, NewArgError("region", "cannot be empty") + } + return s.list(ctx, opt, &listMicroDropletOptions{Region: region}) +} + +// ListByName lists MicroDroplets filtered by exact name match, with optional pagination. +func (s *MicroDropletsServiceOp) ListByName(ctx context.Context, name string, opt *ListOptions) ([]MicroDroplet, *Response, error) { + if name == "" { + return nil, nil, NewArgError("name", "cannot be empty") + } + return s.list(ctx, opt, &listMicroDropletOptions{Name: name}) +} + +func (s *MicroDropletsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listMicroDropletOptions) ([]MicroDroplet, *Response, error) { + path := microDropletBasePath + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + path, err = addOptions(path, listOpt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microDropletsRoot) + resp, err := s.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.MicroDroplets, resp, nil +} + +// Get retrieves a MicroDroplet by its ID. +func (s *MicroDropletsServiceOp) Get(ctx context.Context, id string) (*MicroDroplet, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microDropletBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microDropletRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.MicroDroplet, resp, nil +} + +// Create provisions a new MicroDroplet with the provided configuration. +func (s *MicroDropletsServiceOp) Create(ctx context.Context, createRequest *MicroDropletCreateRequest) (*MicroDroplet, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, microDropletBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(microDropletRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.MicroDroplet, resp, nil +} + +// Update applies a partial update to a MicroDroplet. Today only the State +// field is honored: setting it to MicroDropletStatePaused pauses a running +// MicroDroplet, and setting it to MicroDropletStateRunning resumes a paused +// MicroDroplet. The returned MicroDroplet reflects the post-transition state. +func (s *MicroDropletsServiceOp) Update(ctx context.Context, id string, updateRequest *MicroDropletUpdateRequest) (*MicroDroplet, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + if updateRequest == nil { + return nil, nil, NewArgError("updateRequest", "cannot be nil") + } + + path := fmt.Sprintf("%s/%s", microDropletBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodPatch, path, updateRequest) + if err != nil { + return nil, nil, err + } + + root := new(microDropletRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.MicroDroplet, resp, nil +} + +// Delete removes a MicroDroplet by its ID. The DigitalOcean API returns a 204 +// on success and does not include a response body. +func (s *MicroDropletsServiceOp) Delete(ctx context.Context, id string) (*Response, error) { + if id == "" { + return nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s", microDropletBasePath, id) + + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// ListSnapshots lists snapshots that belong to a MicroDroplet. Snapshots are +// created automatically by DigitalOcean when a MicroDroplet is paused. +func (s *MicroDropletsServiceOp) ListSnapshots(ctx context.Context, id string, opt *ListOptions) ([]MicroDropletSnapshot, *Response, error) { + if id == "" { + return nil, nil, NewArgError("id", "cannot be empty") + } + + path := fmt.Sprintf("%s/%s/snapshots", microDropletBasePath, id) + path, err := addOptions(path, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(microDropletSnapshotsRoot) + resp, err := s.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.Snapshots, resp, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1bd3870b8..deb99761d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -64,7 +64,7 @@ github.com/creack/pty # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew -# github.com/digitalocean/godo v1.198.0 +# github.com/digitalocean/godo v1.198.0 => ../godo ## explicit; go 1.23.0 github.com/digitalocean/godo github.com/digitalocean/godo/metrics @@ -644,3 +644,4 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.3.0 ## explicit; go 1.12 sigs.k8s.io/yaml +# github.com/digitalocean/godo => ../godo