Skip to content

Commit

Permalink
droplets: support listing GPU Droplets.
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewsomething committed Oct 25, 2024
1 parent 615db20 commit 55818e6
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 10 deletions.
3 changes: 3 additions & 0 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,4 +565,7 @@ const (

// ArgTokenValidationServer is the server used to validate an OAuth token
ArgTokenValidationServer = "token-validation-server"

// ArgGPUs specifies to list GPU Droplets
ArgGPUs = "gpus"
)
14 changes: 13 additions & 1 deletion commands/droplets.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ If you do not specify a region, the Droplet is created in the default region for
aliasOpt("ls"), displayerType(&displayers.Droplet{}))
AddStringFlag(cmdRunDropletList, doctl.ArgRegionSlug, "", "", "Retrieves a list of Droplets in a specified region")
AddStringFlag(cmdRunDropletList, doctl.ArgTagName, "", "", "Retrieves a list of Droplets with the specified tag name")
AddBoolFlag(cmdRunDropletList, doctl.ArgGPUs, "", false, "List GPU Droplets only. By default, only non-GPU Droplets are returned.")
cmdRunDropletList.Example = `The following example retrieves a list of all Droplets in the ` + "`" + `nyc1` + "`" + ` region: doctl compute droplet list --region nyc1`

cmdDropletNeighbors := CmdBuilder(cmd, RunDropletNeighbors, "neighbors <droplet-id>", "List a Droplet's neighbors on your account", `Lists your Droplets that are on the same physical hardware, including the following details:`+dropletDetails, Writer,
Expand Down Expand Up @@ -656,6 +657,15 @@ func RunDropletList(c *CmdConfig) error {
return err
}

gpus, err := c.Doit.GetBool(c.NS, doctl.ArgGPUs)
if err != nil {
return err
}

if gpus && tagName != "" {
return fmt.Errorf("The --gpus and --tag-name flags are mutually exclusive.")
}

matches := make([]glob.Glob, 0, len(c.Args))
for _, globStr := range c.Args {
g, err := glob.Compile(globStr)
Expand All @@ -669,7 +679,9 @@ func RunDropletList(c *CmdConfig) error {
var matchedList do.Droplets

var list do.Droplets
if tagName == "" {
if gpus {
list, err = ds.ListWithGPUs()
} else if tagName == "" {
list, err = ds.List()
} else {
list, err = ds.ListByTag(tagName)
Expand Down
19 changes: 19 additions & 0 deletions commands/droplets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,16 @@ func TestDropletsListByTag(t *testing.T) {
})
}

func TestDropletsListGPUs(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
tm.droplets.EXPECT().ListWithGPUs().Return(testDropletList, nil)

config.Doit.Set(config.NS, doctl.ArgGPUs, true)
err := RunDropletList(config)
assert.NoError(t, err)
})
}

func TestDropletsTag(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
trr := &godo.TagResourcesRequest{
Expand All @@ -415,6 +425,15 @@ func TestDropletsTag(t *testing.T) {
})
}

func TestDropletsListGPUsAndTagsExclusive(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
config.Doit.Set(config.NS, doctl.ArgGPUs, true)
config.Doit.Set(config.NS, doctl.ArgTagName, "my-tag")
err := RunDropletList(config)
assert.Error(t, err)
})
}

func TestDropletsTagMultiple(t *testing.T) {
withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
trr := &godo.TagResourcesRequest{
Expand Down
30 changes: 21 additions & 9 deletions do/droplets.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Kernels []Kernel
type DropletsService interface {
List() (Droplets, error)
ListByTag(string) (Droplets, error)
ListWithGPUs() (Droplets, error)
Get(int) (*Droplet, error)
Create(*godo.DropletCreateRequest, bool) (*Droplet, error)
CreateMultiple(*godo.DropletMultiCreateRequest) (Droplets, error)
Expand Down Expand Up @@ -93,18 +94,25 @@ func (ds *dropletsService) List() (Droplets, error) {
return si, resp, err
}

si, err := PaginateResp(f)
if err != nil {
return nil, err
}
return ds.list(f)
}

list := make(Droplets, len(si))
for i := range si {
a := si[i].(godo.Droplet)
list[i] = Droplet{Droplet: &a}
func (ds *dropletsService) ListWithGPUs() (Droplets, error) {
f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) {
list, resp, err := ds.client.Droplets.ListWithGPUs(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
}

return list, nil
return ds.list(f)
}

func (ds *dropletsService) ListByTag(tagName string) (Droplets, error) {
Expand All @@ -122,6 +130,10 @@ func (ds *dropletsService) ListByTag(tagName string) (Droplets, error) {
return si, resp, err
}

return ds.list(f)
}

func (ds *dropletsService) list(f Generator) (Droplets, error) {
si, err := PaginateResp(f)
if err != nil {
return nil, err
Expand Down
16 changes: 16 additions & 0 deletions do/mocks/DropletsService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions integration/droplet_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ var _ = suite("compute/droplet/list", func(t *testing.T, when spec.G, it spec.S)

q := req.URL.Query()
tag := q.Get("tag_name")
dtype := q.Get("type")

if tag != "" && dtype != "" {
w.Write([]byte(`{"id": "unprocessible_entity", "message":"mutually exclusive query parameters"}`))
w.WriteHeader(http.StatusUnprocessableEntity)
return
}

if tag == "some-tag" {
w.Write([]byte(`{}`))
return
Expand All @@ -48,6 +56,10 @@ var _ = suite("compute/droplet/list", func(t *testing.T, when spec.G, it spec.S)
return
}

if dtype == "gpus" {
w.Write([]byte(dropletListGPUsResponse))
}

w.Write([]byte(dropletListResponse))
default:
dump, err := httputil.DumpRequest(req, true)
Expand Down Expand Up @@ -76,6 +88,23 @@ var _ = suite("compute/droplet/list", func(t *testing.T, when spec.G, it spec.S)
})
})

when("the gpu flag is passed", func() {
it("lists gpu droplets", func() {
cmd := exec.Command(builtBinaryPath,
"-t", "some-magic-token",
"-u", server.URL,
"compute",
"droplet",
"list",
"--gpus",
)

output, err := cmd.CombinedOutput()
expect.NoError(err, fmt.Sprintf("received error output: %s", output))
expect.Equal(strings.TrimSpace(dropletGPUListOutput), strings.TrimSpace(string(output)))
})
})

when("a region is provided", func() {
it("filters the returned droplets by region", func() {
cmd := exec.Command(builtBinaryPath,
Expand Down Expand Up @@ -163,9 +192,30 @@ const (
}]
}`

dropletListGPUsResponse = `
{
"droplets": [{
"id": 1111,
"name": "gpu-droplet",
"image": {
"distribution": "Ubuntu",
"name": "gpu-h100x8-640gb-200"
},
"region": {
"slug": "tor1"
},
"status": "active"
}]
}`

dropletListOutput = `
ID Name Public IPv4 Private IPv4 Public IPv6 Memory VCPUs Disk Region Image VPC UUID Status Tags Features Volumes
1111 some-droplet-name 0 0 0 some-region-slug some-distro some-image-name active test,yes remotes some-volume-id
`

dropletGPUListOutput = `
ID Name Public IPv4 Private IPv4 Public IPv6 Memory VCPUs Disk Region Image VPC UUID Status Tags Features Volumes
1111 gpu-droplet 0 0 0 tor1 Ubuntu gpu-h100x8-640gb-200 active
`

dropletListRegionOutput = `
Expand Down

0 comments on commit 55818e6

Please sign in to comment.