From 5ce58e735565ef7820d5c799736cafb114b9c6e8 Mon Sep 17 00:00:00 2001 From: rarora Date: Wed, 18 Mar 2026 17:02:17 +0530 Subject: [PATCH 1/5] create-di --- args.go | 8 ++ commands/command_config.go | 74 +++++----- commands/commands_test.go | 93 +++++++------ commands/dedicated_inference.go | 128 +++++++++++++++++ commands/dedicated_inference_test.go | 154 +++++++++++++++++++++ commands/displayers/dedicated_inference.go | 73 ++++++++++ commands/doit.go | 1 + do/dedicated_inference.go | 64 +++++++++ do/mocks/DedicatedInferenceService.go | 58 ++++++++ 9 files changed, 573 insertions(+), 80 deletions(-) create mode 100644 commands/dedicated_inference.go create mode 100644 commands/dedicated_inference_test.go create mode 100644 commands/displayers/dedicated_inference.go create mode 100644 do/dedicated_inference.go create mode 100644 do/mocks/DedicatedInferenceService.go diff --git a/args.go b/args.go index 44b7c8dbd..9160110d8 100644 --- a/args.go +++ b/args.go @@ -846,4 +846,12 @@ const ( // ArgOpenAIKeyAPIKey is the API key for the OpenAI API Key ArgOpenAIKeyAPIKey = "api-key" + + // Dedicated Inference Args + + // ArgDedicatedInferenceSpec is the path to a dedicated inference spec file. + ArgDedicatedInferenceSpec = "spec" + + // ArgDedicatedInferenceHuggingFaceToken is the Hugging Face token (optional). + ArgDedicatedInferenceHuggingFaceToken = "hugging-face-token" ) diff --git a/commands/command_config.go b/commands/command_config.go index b46fb1507..de44d8e8c 100644 --- a/commands/command_config.go +++ b/commands/command_config.go @@ -52,41 +52,42 @@ type CmdConfig struct { ReservedIPv6s func() do.ReservedIPv6sService BYOIPPrefixes func() do.BYOIPPrefixsService - Droplets func() do.DropletsService - DropletActions func() do.DropletActionsService - DropletAutoscale func() do.DropletAutoscaleService - Domains func() do.DomainsService - VPCNATGateways func() do.VPCNATGatewaysService - Actions func() do.ActionsService - Account func() do.AccountService - Balance func() do.BalanceService - BillingHistory func() do.BillingHistoryService - Invoices func() do.InvoicesService - Tags func() do.TagsService - UptimeChecks func() do.UptimeChecksService - Volumes func() do.VolumesService - VolumeActions func() do.VolumeActionsService - Snapshots func() do.SnapshotsService - Certificates func() do.CertificatesService - Firewalls func() do.FirewallsService - CDNs func() do.CDNsService - Projects func() do.ProjectsService - Kubernetes func() do.KubernetesService - Databases func() do.DatabasesService - Registry func() do.RegistryService - Registries func() do.RegistriesService - VPCs func() do.VPCsService - OneClicks func() do.OneClickService - Apps func() do.AppsService - Monitoring func() do.MonitoringService - Serverless func() do.ServerlessService - OAuth func() do.OAuthService - PartnerAttachments func() do.PartnerAttachmentsService - SpacesKeys func() do.SpacesKeysService - GradientAI func() do.GradientAIService - Nfs func() do.NfsService - NfsActions func() do.NfsActionsService - Security func() do.SecurityService + Droplets func() do.DropletsService + DropletActions func() do.DropletActionsService + DropletAutoscale func() do.DropletAutoscaleService + Domains func() do.DomainsService + VPCNATGateways func() do.VPCNATGatewaysService + Actions func() do.ActionsService + Account func() do.AccountService + Balance func() do.BalanceService + BillingHistory func() do.BillingHistoryService + Invoices func() do.InvoicesService + Tags func() do.TagsService + UptimeChecks func() do.UptimeChecksService + Volumes func() do.VolumesService + VolumeActions func() do.VolumeActionsService + Snapshots func() do.SnapshotsService + Certificates func() do.CertificatesService + Firewalls func() do.FirewallsService + CDNs func() do.CDNsService + Projects func() do.ProjectsService + Kubernetes func() do.KubernetesService + Databases func() do.DatabasesService + Registry func() do.RegistryService + Registries func() do.RegistriesService + VPCs func() do.VPCsService + OneClicks func() do.OneClickService + Apps func() do.AppsService + Monitoring func() do.MonitoringService + Serverless func() do.ServerlessService + OAuth func() do.OAuthService + PartnerAttachments func() do.PartnerAttachmentsService + SpacesKeys func() do.SpacesKeysService + GradientAI func() do.GradientAIService + DedicatedInferences func() do.DedicatedInferenceService + Nfs func() do.NfsService + NfsActions func() do.NfsActionsService + Security func() do.SecurityService } // NewCmdConfig creates an instance of a CmdConfig. @@ -151,6 +152,9 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init } c.SpacesKeys = func() do.SpacesKeysService { return do.NewSpacesKeysService(godoClient) } c.GradientAI = func() do.GradientAIService { return do.NewGradientAIService(godoClient) } + c.DedicatedInferences = func() do.DedicatedInferenceService { + return do.NewDedicatedInferenceService(godoClient) + } c.Nfs = func() do.NfsService { return do.NewNfsService(godoClient) } c.NfsActions = func() do.NfsActionsService { return do.NewNfsActionsService(godoClient) } c.Security = func() do.SecurityService { return do.NewSecurityService(godoClient) } diff --git a/commands/commands_test.go b/commands/commands_test.go index 55d6a5481..53b2c9e3e 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -292,6 +292,7 @@ type tcMocks struct { partnerAttachments *domocks.MockPartnerAttachmentsService spacesKeys *domocks.MockSpacesKeysService gradientAI *domocks.MockGradientAIService + dedicatedInferences *domocks.MockDedicatedInferenceService nfs *domocks.MockNfsService nfsActions *domocks.MockNfsActionsService security *domocks.MockSecurityService @@ -350,6 +351,7 @@ func withTestClient(t *testing.T, tFn testFn) { partnerAttachments: domocks.NewMockPartnerAttachmentsService(ctrl), spacesKeys: domocks.NewMockSpacesKeysService(ctrl), gradientAI: domocks.NewMockGradientAIService(ctrl), + dedicatedInferences: domocks.NewMockDedicatedInferenceService(ctrl), nfs: domocks.NewMockNfsService(ctrl), nfsActions: domocks.NewMockNfsActionsService(ctrl), security: domocks.NewMockSecurityService(ctrl), @@ -374,51 +376,52 @@ func withTestClient(t *testing.T, tFn testFn) { componentBuilderFactory: tm.appBuilderFactory, - Keys: func() do.KeysService { return tm.keys }, - Sizes: func() do.SizesService { return tm.sizes }, - Regions: func() do.RegionsService { return tm.regions }, - Images: func() do.ImagesService { return tm.images }, - ImageActions: func() do.ImageActionsService { return tm.imageActions }, - ReservedIPs: func() do.ReservedIPsService { return tm.reservedIPs }, - ReservedIPActions: func() do.ReservedIPActionsService { return tm.reservedIPActions }, - ReservedIPv6s: func() do.ReservedIPv6sService { return tm.reservedIPv6s }, - BYOIPPrefixes: func() do.BYOIPPrefixsService { return tm.byoipPrefixes }, - Droplets: func() do.DropletsService { return tm.droplets }, - DropletActions: func() do.DropletActionsService { return tm.dropletActions }, - DropletAutoscale: func() do.DropletAutoscaleService { return tm.dropletAutoscale }, - Domains: func() do.DomainsService { return tm.domains }, - Actions: func() do.ActionsService { return tm.actions }, - Account: func() do.AccountService { return tm.account }, - Balance: func() do.BalanceService { return tm.balance }, - BillingHistory: func() do.BillingHistoryService { return tm.billingHistory }, - Invoices: func() do.InvoicesService { return tm.invoices }, - Tags: func() do.TagsService { return tm.tags }, - UptimeChecks: func() do.UptimeChecksService { return tm.uptimeChecks }, - Volumes: func() do.VolumesService { return tm.volumes }, - VolumeActions: func() do.VolumeActionsService { return tm.volumeActions }, - VPCNATGateways: func() do.VPCNATGatewaysService { return tm.vpcNatGateways }, - Snapshots: func() do.SnapshotsService { return tm.snapshots }, - Certificates: func() do.CertificatesService { return tm.certificates }, - LoadBalancers: func() do.LoadBalancersService { return tm.loadBalancers }, - Firewalls: func() do.FirewallsService { return tm.firewalls }, - CDNs: func() do.CDNsService { return tm.cdns }, - Projects: func() do.ProjectsService { return tm.projects }, - Kubernetes: func() do.KubernetesService { return tm.kubernetes }, - Databases: func() do.DatabasesService { return tm.databases }, - Registry: func() do.RegistryService { return tm.registry }, - Registries: func() do.RegistriesService { return tm.registries }, - VPCs: func() do.VPCsService { return tm.vpcs }, - OneClicks: func() do.OneClickService { return tm.oneClick }, - Apps: func() do.AppsService { return tm.apps }, - Monitoring: func() do.MonitoringService { return tm.monitoring }, - Serverless: func() do.ServerlessService { return tm.serverless }, - OAuth: func() do.OAuthService { return tm.oauth }, - PartnerAttachments: func() do.PartnerAttachmentsService { return tm.partnerAttachments }, - SpacesKeys: func() do.SpacesKeysService { return tm.spacesKeys }, - GradientAI: func() do.GradientAIService { return tm.gradientAI }, - Nfs: func() do.NfsService { return tm.nfs }, - NfsActions: func() do.NfsActionsService { return tm.nfsActions }, - Security: func() do.SecurityService { return tm.security }, + Keys: func() do.KeysService { return tm.keys }, + Sizes: func() do.SizesService { return tm.sizes }, + Regions: func() do.RegionsService { return tm.regions }, + Images: func() do.ImagesService { return tm.images }, + ImageActions: func() do.ImageActionsService { return tm.imageActions }, + ReservedIPs: func() do.ReservedIPsService { return tm.reservedIPs }, + ReservedIPActions: func() do.ReservedIPActionsService { return tm.reservedIPActions }, + ReservedIPv6s: func() do.ReservedIPv6sService { return tm.reservedIPv6s }, + BYOIPPrefixes: func() do.BYOIPPrefixsService { return tm.byoipPrefixes }, + Droplets: func() do.DropletsService { return tm.droplets }, + DropletActions: func() do.DropletActionsService { return tm.dropletActions }, + DropletAutoscale: func() do.DropletAutoscaleService { return tm.dropletAutoscale }, + Domains: func() do.DomainsService { return tm.domains }, + Actions: func() do.ActionsService { return tm.actions }, + Account: func() do.AccountService { return tm.account }, + Balance: func() do.BalanceService { return tm.balance }, + BillingHistory: func() do.BillingHistoryService { return tm.billingHistory }, + Invoices: func() do.InvoicesService { return tm.invoices }, + Tags: func() do.TagsService { return tm.tags }, + UptimeChecks: func() do.UptimeChecksService { return tm.uptimeChecks }, + Volumes: func() do.VolumesService { return tm.volumes }, + VolumeActions: func() do.VolumeActionsService { return tm.volumeActions }, + VPCNATGateways: func() do.VPCNATGatewaysService { return tm.vpcNatGateways }, + Snapshots: func() do.SnapshotsService { return tm.snapshots }, + Certificates: func() do.CertificatesService { return tm.certificates }, + LoadBalancers: func() do.LoadBalancersService { return tm.loadBalancers }, + Firewalls: func() do.FirewallsService { return tm.firewalls }, + CDNs: func() do.CDNsService { return tm.cdns }, + Projects: func() do.ProjectsService { return tm.projects }, + Kubernetes: func() do.KubernetesService { return tm.kubernetes }, + Databases: func() do.DatabasesService { return tm.databases }, + Registry: func() do.RegistryService { return tm.registry }, + Registries: func() do.RegistriesService { return tm.registries }, + VPCs: func() do.VPCsService { return tm.vpcs }, + OneClicks: func() do.OneClickService { return tm.oneClick }, + Apps: func() do.AppsService { return tm.apps }, + Monitoring: func() do.MonitoringService { return tm.monitoring }, + Serverless: func() do.ServerlessService { return tm.serverless }, + OAuth: func() do.OAuthService { return tm.oauth }, + PartnerAttachments: func() do.PartnerAttachmentsService { return tm.partnerAttachments }, + SpacesKeys: func() do.SpacesKeysService { return tm.spacesKeys }, + GradientAI: func() do.GradientAIService { return tm.gradientAI }, + DedicatedInferences: func() do.DedicatedInferenceService { return tm.dedicatedInferences }, + Nfs: func() do.NfsService { return tm.nfs }, + NfsActions: func() do.NfsActionsService { return tm.nfsActions }, + Security: func() do.SecurityService { return tm.security }, } tFn(config, tm) diff --git a/commands/dedicated_inference.go b/commands/dedicated_inference.go new file mode 100644 index 000000000..931ec61e5 --- /dev/null +++ b/commands/dedicated_inference.go @@ -0,0 +1,128 @@ +/* +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 ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/commands/displayers" + "github.com/digitalocean/doctl/do" + "github.com/digitalocean/godo" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" +) + +// DedicatedInferenceCmd creates the dedicated-inference command and its subcommands. +func DedicatedInferenceCmd() *Command { + cmd := &Command{ + Command: &cobra.Command{ + Use: "dedicated-inference", + Aliases: []string{"di", "dedicated-inferences"}, + Short: "Display commands for managing dedicated inference endpoints", + Long: "The subcommands of `doctl dedicated-inference` manage your dedicated inference endpoints.", + GroupID: manageResourcesGroup, + }, + } + + cmdCreate := CmdBuilder( + cmd, + RunDedicatedInferenceCreate, + "create", + "Create a dedicated inference endpoint", + `Creates a dedicated inference endpoint on your account using a spec file in JSON or YAML format. +Use the `+"`"+`--spec`+"`"+` flag to provide the path to the spec file. +Optionally provide a Hugging Face access token using `+"`"+`--hugging-face-token`+"`"+`.`, + Writer, + aliasOpt("c"), + displayerType(&displayers.DedicatedInference{}), + ) + AddStringFlag(cmdCreate, doctl.ArgDedicatedInferenceSpec, "", "", `Path to a dedicated inference spec in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) + AddStringFlag(cmdCreate, doctl.ArgDedicatedInferenceHuggingFaceToken, "", "", "Hugging Face token for accessing gated models (optional)") + cmdCreate.Example = `The following example creates a dedicated inference endpoint using a spec file: doctl dedicated-inference create --spec spec.yaml --hugging-face-token "hf_mytoken"` + + return cmd +} + +// readDedicatedInferenceSpec reads and parses a dedicated inference spec from a file path or stdin. +func readDedicatedInferenceSpec(stdin io.Reader, path string) (*godo.DedicatedInferenceSpecRequest, error) { + var specReader io.Reader + if path == "-" && stdin != nil { + specReader = stdin + } else { + f, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("opening spec: %s does not exist", path) + } + return nil, fmt.Errorf("opening spec: %w", err) + } + defer f.Close() + specReader = f + } + + byt, err := io.ReadAll(specReader) + if err != nil { + return nil, fmt.Errorf("reading spec: %w", err) + } + + jsonSpec, err := yaml.YAMLToJSON(byt) + if err != nil { + return nil, fmt.Errorf("parsing spec: %w", err) + } + + dec := json.NewDecoder(bytes.NewReader(jsonSpec)) + dec.DisallowUnknownFields() + + var spec godo.DedicatedInferenceSpecRequest + if err := dec.Decode(&spec); err != nil { + return nil, fmt.Errorf("parsing spec: %w", err) + } + + return &spec, nil +} + +// RunDedicatedInferenceCreate creates a new dedicated inference endpoint. +func RunDedicatedInferenceCreate(c *CmdConfig) error { + specPath, err := c.Doit.GetString(c.NS, doctl.ArgDedicatedInferenceSpec) + if err != nil { + return err + } + + spec, err := readDedicatedInferenceSpec(os.Stdin, specPath) + if err != nil { + return err + } + + req := &godo.DedicatedInferenceCreateRequest{ + Spec: spec, + } + + hfToken, _ := c.Doit.GetString(c.NS, doctl.ArgDedicatedInferenceHuggingFaceToken) + if hfToken != "" { + req.Secrets = &godo.DedicatedInferenceSecrets{ + HuggingFaceToken: hfToken, + } + } + + endpoint, _, err := c.DedicatedInferences().Create(req) + if err != nil { + return err + } + return c.Display(&displayers.DedicatedInference{DedicatedInferences: do.DedicatedInferences{*endpoint}}) +} diff --git a/commands/dedicated_inference_test.go b/commands/dedicated_inference_test.go new file mode 100644 index 000000000..84851644b --- /dev/null +++ b/commands/dedicated_inference_test.go @@ -0,0 +1,154 @@ +package commands + +import ( + "os" + "testing" + + "github.com/digitalocean/godo" + + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/do" + "github.com/stretchr/testify/assert" +) + +// Test data +var ( + testDedicatedInferenceSpecRequest = &godo.DedicatedInferenceSpecRequest{ + Version: 0, + Name: "test-dedicated-inference", + Region: "nyc2", + VPC: &godo.DedicatedInferenceVPCRequest{ + UUID: "00000000-0000-4000-8000-000000000001", + }, + EnablePublicEndpoint: true, + ModelDeployments: []*godo.DedicatedInferenceModelRequest{ + { + ModelSlug: "mistral/mistral-7b-instruct-v3", + ModelProvider: "hugging_face", + Accelerators: []*godo.DedicatedInferenceAcceleratorRequest{ + { + Scale: 2, + Type: "prefill", + AcceleratorSlug: "gpu-mi300x1-192gb", + }, + { + Scale: 4, + Type: "decode", + AcceleratorSlug: "gpu-mi300x1-192gb", + }, + }, + }, + }, + } + + testDedicatedInference = do.DedicatedInference{ + DedicatedInference: &godo.DedicatedInference{ + ID: "00000000-0000-4000-8000-000000000000", + Name: "test-dedicated-inference", + Status: "PROVISIONING", + Region: "nyc2", + VPCUUID: "00000000-0000-4000-8000-000000000001", + }, + } + + testDedicatedInferenceToken = &do.DedicatedInferenceToken{ + DedicatedInferenceToken: &godo.DedicatedInferenceToken{ + ID: "tok-1", + Name: "default", + Value: "secret-token-value", + }, + } +) + +func TestDedicatedInferenceCommand(t *testing.T) { + cmd := DedicatedInferenceCmd() + assert.NotNil(t, cmd) + assert.Equal(t, "dedicated-inference", cmd.Name()) + + // Verify create is a subcommand + found := false + for _, c := range cmd.Commands() { + if c.Name() == "create" { + found = true + break + } + } + assert.True(t, found, "Expected create subcommand") +} + +func TestRunDedicatedInferenceCreate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + // Write a temp spec file + specJSON := `{ + "version": 0, + "name": "test-dedicated-inference", + "region": "nyc2", + "vpc": {"uuid": "00000000-0000-4000-8000-000000000001"}, + "enable_public_endpoint": true, + "model_deployments": [ + { + "model_slug": "mistral/mistral-7b-instruct-v3", + "model_provider": "hugging_face", + "accelerators": [ + {"scale": 2, "type": "prefill", "accelerator_slug": "gpu-mi300x1-192gb"}, + {"scale": 4, "type": "decode", "accelerator_slug": "gpu-mi300x1-192gb"} + ] + } + ] + }` + tmpFile := t.TempDir() + "/spec.json" + err := os.WriteFile(tmpFile, []byte(specJSON), 0644) + assert.NoError(t, err) + + config.Doit.Set(config.NS, doctl.ArgDedicatedInferenceSpec, tmpFile) + + expectedReq := &godo.DedicatedInferenceCreateRequest{ + Spec: testDedicatedInferenceSpecRequest, + } + + tm.dedicatedInferences.EXPECT().Create(expectedReq).Return(&testDedicatedInference, testDedicatedInferenceToken, nil) + + err = RunDedicatedInferenceCreate(config) + assert.NoError(t, err) + }) +} + +func TestRunDedicatedInferenceCreate_WithHuggingFaceToken(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + specJSON := `{ + "version": 0, + "name": "test-dedicated-inference", + "region": "nyc2", + "vpc": {"uuid": "00000000-0000-4000-8000-000000000001"}, + "enable_public_endpoint": true, + "model_deployments": [ + { + "model_slug": "mistral/mistral-7b-instruct-v3", + "model_provider": "hugging_face", + "accelerators": [ + {"scale": 2, "type": "prefill", "accelerator_slug": "gpu-mi300x1-192gb"}, + {"scale": 4, "type": "decode", "accelerator_slug": "gpu-mi300x1-192gb"} + ] + } + ] + }` + tmpFile := t.TempDir() + "/spec.json" + err := os.WriteFile(tmpFile, []byte(specJSON), 0644) + assert.NoError(t, err) + + config.Doit.Set(config.NS, doctl.ArgDedicatedInferenceSpec, tmpFile) + config.Doit.Set(config.NS, doctl.ArgDedicatedInferenceHuggingFaceToken, "hf_test_token") + + expectedReq := &godo.DedicatedInferenceCreateRequest{ + Spec: testDedicatedInferenceSpecRequest, + Secrets: &godo.DedicatedInferenceSecrets{ + HuggingFaceToken: "hf_test_token", + }, + } + + tm.dedicatedInferences.EXPECT().Create(expectedReq).Return(&testDedicatedInference, testDedicatedInferenceToken, nil) + + err = RunDedicatedInferenceCreate(config) + assert.NoError(t, err) + }) +} diff --git a/commands/displayers/dedicated_inference.go b/commands/displayers/dedicated_inference.go new file mode 100644 index 000000000..4eda812ac --- /dev/null +++ b/commands/displayers/dedicated_inference.go @@ -0,0 +1,73 @@ +package displayers + +import ( + "io" + + "github.com/digitalocean/doctl/do" +) + +// DedicatedInference wraps a slice of dedicated inference endpoints for display. +type DedicatedInference struct { + DedicatedInferences do.DedicatedInferences +} + +var _ Displayable = &DedicatedInference{} + +func (d *DedicatedInference) JSON(out io.Writer) error { + return writeJSON(d.DedicatedInferences, out) +} + +func (d *DedicatedInference) Cols() []string { + return []string{ + "ID", + "Name", + "Region", + "Status", + "VPCUUID", + "PublicEndpoint", + "PrivateEndpoint", + "CreatedAt", + "UpdatedAt", + } +} + +func (d *DedicatedInference) ColMap() map[string]string { + return map[string]string{ + "ID": "ID", + "Name": "Name", + "Region": "Region", + "Status": "Status", + "VPCUUID": "VPC UUID", + "PublicEndpoint": "Public Endpoint", + "PrivateEndpoint": "Private Endpoint", + "CreatedAt": "Created At", + "UpdatedAt": "Updated At", + } +} + +func (d *DedicatedInference) KV() []map[string]any { + if d == nil || d.DedicatedInferences == nil { + return []map[string]any{} + } + out := make([]map[string]any, 0, len(d.DedicatedInferences)) + for _, di := range d.DedicatedInferences { + publicEndpoint := "" + privateEndpoint := "" + if di.Endpoints != nil { + publicEndpoint = di.Endpoints.PublicEndpointFQDN + privateEndpoint = di.Endpoints.PrivateEndpointFQDN + } + out = append(out, map[string]any{ + "ID": di.ID, + "Name": di.Name, + "Region": di.Region, + "Status": di.Status, + "VPCUUID": di.VPCUUID, + "PublicEndpoint": publicEndpoint, + "PrivateEndpoint": privateEndpoint, + "CreatedAt": di.CreatedAt, + "UpdatedAt": di.UpdatedAt, + }) + } + return out +} diff --git a/commands/doit.go b/commands/doit.go index 6645e4e93..927fc2a44 100644 --- a/commands/doit.go +++ b/commands/doit.go @@ -192,6 +192,7 @@ func addCommands() { DoitCmd.AddCommand(Serverless()) DoitCmd.AddCommand(Spaces()) DoitCmd.AddCommand(GradientAI()) + DoitCmd.AddCommand(DedicatedInferenceCmd()) DoitCmd.AddCommand(Nfs()) DoitCmd.AddCommand(Security()) } diff --git a/do/dedicated_inference.go b/do/dedicated_inference.go new file mode 100644 index 000000000..f306926ed --- /dev/null +++ b/do/dedicated_inference.go @@ -0,0 +1,64 @@ +/* +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" +) + +// DedicatedInference wraps a godo.DedicatedInference. +type DedicatedInference struct { + *godo.DedicatedInference +} + +// DedicatedInferences is a slice of DedicatedInference. +type DedicatedInferences []DedicatedInference + +// DedicatedInferenceToken wraps a godo.DedicatedInferenceToken. +type DedicatedInferenceToken struct { + *godo.DedicatedInferenceToken +} + +// DedicatedInferenceService is an interface for interacting with DigitalOcean's Dedicated Inference API. +type DedicatedInferenceService interface { + Create(req *godo.DedicatedInferenceCreateRequest) (*DedicatedInference, *DedicatedInferenceToken, error) +} + +var _ DedicatedInferenceService = &dedicatedInferenceService{} + +type dedicatedInferenceService struct { + client *godo.Client +} + +// NewDedicatedInferenceService builds an instance of DedicatedInferenceService. +func NewDedicatedInferenceService(client *godo.Client) DedicatedInferenceService { + return &dedicatedInferenceService{ + client: client, + } +} + +// Create creates a new dedicated inference endpoint. +func (s *dedicatedInferenceService) Create(req *godo.DedicatedInferenceCreateRequest) (*DedicatedInference, *DedicatedInferenceToken, error) { + d, t, _, err := s.client.DedicatedInference.Create(context.TODO(), req) + if err != nil { + return nil, nil, err + } + var token *DedicatedInferenceToken + if t != nil { + token = &DedicatedInferenceToken{DedicatedInferenceToken: t} + } + return &DedicatedInference{DedicatedInference: d}, token, nil +} diff --git a/do/mocks/DedicatedInferenceService.go b/do/mocks/DedicatedInferenceService.go new file mode 100644 index 000000000..346e6e11e --- /dev/null +++ b/do/mocks/DedicatedInferenceService.go @@ -0,0 +1,58 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: dedicated_inference.go +// +// Generated by this command: +// +// mockgen -source dedicated_inference.go -package=mocks DedicatedInferenceService +// + +// 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" +) + +// MockDedicatedInferenceService is a mock of DedicatedInferenceService interface. +type MockDedicatedInferenceService struct { + ctrl *gomock.Controller + recorder *MockDedicatedInferenceServiceMockRecorder + isgomock struct{} +} + +// MockDedicatedInferenceServiceMockRecorder is the mock recorder for MockDedicatedInferenceService. +type MockDedicatedInferenceServiceMockRecorder struct { + mock *MockDedicatedInferenceService +} + +// NewMockDedicatedInferenceService creates a new mock instance. +func NewMockDedicatedInferenceService(ctrl *gomock.Controller) *MockDedicatedInferenceService { + mock := &MockDedicatedInferenceService{ctrl: ctrl} + mock.recorder = &MockDedicatedInferenceServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDedicatedInferenceService) EXPECT() *MockDedicatedInferenceServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockDedicatedInferenceService) Create(req *godo.DedicatedInferenceCreateRequest) (*do.DedicatedInference, *do.DedicatedInferenceToken, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", req) + ret0, _ := ret[0].(*do.DedicatedInference) + ret1, _ := ret[1].(*do.DedicatedInferenceToken) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Create indicates an expected call of Create. +func (mr *MockDedicatedInferenceServiceMockRecorder) Create(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDedicatedInferenceService)(nil).Create), req) +} From e348be8f8ebf314b33b6ac024c55397b40a9d993 Mon Sep 17 00:00:00 2001 From: rarora Date: Fri, 20 Mar 2026 18:46:55 +0530 Subject: [PATCH 2/5] delete_di --- commands/dedicated_inference.go | 31 +++++++++++++++++++++++++++ commands/dedicated_inference_test.go | 20 +++++++++++++++++ do/dedicated_inference.go | 7 ++++++ do/mocks/DedicatedInferenceService.go | 14 ++++++++++++ 4 files changed, 72 insertions(+) diff --git a/commands/dedicated_inference.go b/commands/dedicated_inference.go index bf4152640..f26bc9b12 100644 --- a/commands/dedicated_inference.go +++ b/commands/dedicated_inference.go @@ -70,6 +70,18 @@ For more information, see https://docs.digitalocean.com/reference/api/digitaloce ) cmdGet.Example = `The following example retrieves a dedicated inference endpoint: doctl dedicated-inference get 12345678-1234-1234-1234-123456789012` + cmdDelete := CmdBuilder( + cmd, + RunDedicatedInferenceDelete, + "delete ", + "Delete a dedicated inference endpoint", + `Deletes a dedicated inference endpoint by its ID. All associated resources will be destroyed.`, + Writer, + aliasOpt("d", "rm"), + ) + AddBoolFlag(cmdDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the dedicated inference endpoint without a confirmation prompt") + cmdDelete.Example = `The following example deletes a dedicated inference endpoint: doctl dedicated-inference delete 12345678-1234-1234-1234-123456789012` + return cmd } @@ -154,3 +166,22 @@ func RunDedicatedInferenceGet(c *CmdConfig) error { } return c.Display(&displayers.DedicatedInference{DedicatedInferences: do.DedicatedInferences{*endpoint}}) } + +// RunDedicatedInferenceDelete deletes a dedicated inference endpoint by ID. +func RunDedicatedInferenceDelete(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("dedicated inference endpoint", 1) == nil { + id := c.Args[0] + return c.DedicatedInferences().Delete(id) + } + + return errOperationAborted +} diff --git a/commands/dedicated_inference_test.go b/commands/dedicated_inference_test.go index d62c105c0..8aa05dff6 100644 --- a/commands/dedicated_inference_test.go +++ b/commands/dedicated_inference_test.go @@ -72,6 +72,7 @@ func TestDedicatedInferenceCommand(t *testing.T) { } assert.True(t, subcommands["create"], "Expected create subcommand") assert.True(t, subcommands["get"], "Expected get subcommand") + assert.True(t, subcommands["delete"], "Expected delete subcommand") } func TestRunDedicatedInferenceCreate(t *testing.T) { @@ -168,3 +169,22 @@ func TestRunDedicatedInferenceGet_MissingID(t *testing.T) { assert.Error(t, err) }) } + +func TestRunDedicatedInferenceDelete(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.dedicatedInferences.EXPECT().Delete("00000000-0000-4000-8000-000000000000").Return(nil) + + config.Args = append(config.Args, "00000000-0000-4000-8000-000000000000") + config.Doit.Set(config.NS, doctl.ArgForce, true) + + err := RunDedicatedInferenceDelete(config) + assert.NoError(t, err) + }) +} + +func TestRunDedicatedInferenceDelete_MissingID(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + err := RunDedicatedInferenceDelete(config) + assert.Error(t, err) + }) +} diff --git a/do/dedicated_inference.go b/do/dedicated_inference.go index 95b11056b..8c2f45525 100644 --- a/do/dedicated_inference.go +++ b/do/dedicated_inference.go @@ -36,6 +36,7 @@ type DedicatedInferenceToken struct { type DedicatedInferenceService interface { Create(req *godo.DedicatedInferenceCreateRequest) (*DedicatedInference, *DedicatedInferenceToken, error) Get(id string) (*DedicatedInference, error) + Delete(id string) error } var _ DedicatedInferenceService = &dedicatedInferenceService{} @@ -72,3 +73,9 @@ func (s *dedicatedInferenceService) Get(id string) (*DedicatedInference, error) } return &DedicatedInference{DedicatedInference: d}, nil } + +// Delete deletes a dedicated inference endpoint by ID. +func (s *dedicatedInferenceService) Delete(id string) error { + _, err := s.client.DedicatedInference.Delete(context.TODO(), id) + return err +} diff --git a/do/mocks/DedicatedInferenceService.go b/do/mocks/DedicatedInferenceService.go index fa7891552..70254784a 100644 --- a/do/mocks/DedicatedInferenceService.go +++ b/do/mocks/DedicatedInferenceService.go @@ -57,6 +57,20 @@ func (mr *MockDedicatedInferenceServiceMockRecorder) Create(req any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDedicatedInferenceService)(nil).Create), req) } +// Delete mocks base method. +func (m *MockDedicatedInferenceService) 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 *MockDedicatedInferenceServiceMockRecorder) Delete(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDedicatedInferenceService)(nil).Delete), id) +} + // Get mocks base method. func (m *MockDedicatedInferenceService) Get(id string) (*do.DedicatedInference, error) { m.ctrl.T.Helper() From 99d9569938b0180c55ed94b0a89e823ddfa77864 Mon Sep 17 00:00:00 2001 From: rarora Date: Fri, 20 Mar 2026 20:14:37 +0530 Subject: [PATCH 3/5] LIST_ACCELORATORS --- args.go | 3 ++ commands/dedicated_inference.go | 32 ++++++++++++ commands/dedicated_inference_test.go | 61 ++++++++++++++++++++++ commands/displayers/dedicated_inference.go | 48 +++++++++++++++++ do/dedicated_inference.go | 37 +++++++++++++ do/mocks/DedicatedInferenceService.go | 15 ++++++ 6 files changed, 196 insertions(+) diff --git a/args.go b/args.go index 9160110d8..43c56edc8 100644 --- a/args.go +++ b/args.go @@ -854,4 +854,7 @@ const ( // ArgDedicatedInferenceHuggingFaceToken is the Hugging Face token (optional). ArgDedicatedInferenceHuggingFaceToken = "hugging-face-token" + + // ArgDedicatedInferenceAcceleratorSlug filters accelerators by slug (optional). + ArgDedicatedInferenceAcceleratorSlug = "slug" ) diff --git a/commands/dedicated_inference.go b/commands/dedicated_inference.go index f26bc9b12..d02b7d474 100644 --- a/commands/dedicated_inference.go +++ b/commands/dedicated_inference.go @@ -82,6 +82,22 @@ For more information, see https://docs.digitalocean.com/reference/api/digitaloce AddBoolFlag(cmdDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the dedicated inference endpoint without a confirmation prompt") cmdDelete.Example = `The following example deletes a dedicated inference endpoint: doctl dedicated-inference delete 12345678-1234-1234-1234-123456789012` + cmdListAccelerators := CmdBuilder( + cmd, + RunDedicatedInferenceListAccelerators, + "list-accelerators ", + "List accelerators for a dedicated inference endpoint", + `Lists the accelerators provisioned for a dedicated inference endpoint, including their IDs, names, slugs, and statuses. +Optionally use `+"`"+`--slug`+"`"+` to filter by accelerator slug.`, + Writer, + aliasOpt("la"), + displayerType(&displayers.DedicatedInferenceAccelerator{}), + ) + AddStringFlag(cmdListAccelerators, doctl.ArgDedicatedInferenceAcceleratorSlug, "", "", "Filter accelerators by slug (optional)") + cmdListAccelerators.Example = `The following example lists accelerators for a dedicated inference endpoint: doctl dedicated-inference list-accelerators 12345678-1234-1234-1234-123456789012 + +The following example filters by slug: doctl dedicated-inference list-accelerators 12345678-1234-1234-1234-123456789012 --slug gpu-mi300x1-192gb` + return cmd } @@ -167,6 +183,22 @@ func RunDedicatedInferenceGet(c *CmdConfig) error { return c.Display(&displayers.DedicatedInference{DedicatedInferences: do.DedicatedInferences{*endpoint}}) } +// RunDedicatedInferenceListAccelerators lists accelerators for a dedicated inference endpoint. +func RunDedicatedInferenceListAccelerators(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + diID := c.Args[0] + + slug, _ := c.Doit.GetString(c.NS, doctl.ArgDedicatedInferenceAcceleratorSlug) + + accelerators, err := c.DedicatedInferences().ListAccelerators(diID, slug) + if err != nil { + return err + } + return c.Display(&displayers.DedicatedInferenceAccelerator{DedicatedInferenceAcceleratorInfos: accelerators}) +} + // RunDedicatedInferenceDelete deletes a dedicated inference endpoint by ID. func RunDedicatedInferenceDelete(c *CmdConfig) error { if len(c.Args) < 1 { diff --git a/commands/dedicated_inference_test.go b/commands/dedicated_inference_test.go index 8aa05dff6..24b3a2b43 100644 --- a/commands/dedicated_inference_test.go +++ b/commands/dedicated_inference_test.go @@ -73,6 +73,7 @@ func TestDedicatedInferenceCommand(t *testing.T) { assert.True(t, subcommands["create"], "Expected create subcommand") assert.True(t, subcommands["get"], "Expected get subcommand") assert.True(t, subcommands["delete"], "Expected delete subcommand") + assert.True(t, subcommands["list-accelerators"], "Expected list-accelerators subcommand") } func TestRunDedicatedInferenceCreate(t *testing.T) { @@ -188,3 +189,63 @@ func TestRunDedicatedInferenceDelete_MissingID(t *testing.T) { assert.Error(t, err) }) } + +func TestRunDedicatedInferenceListAccelerators(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + testAccelerators := do.DedicatedInferenceAcceleratorInfos{ + { + DedicatedInferenceAcceleratorInfo: &godo.DedicatedInferenceAcceleratorInfo{ + ID: "accel-1", + Name: "gpu-mi300x1-192gb", + Slug: "gpu-mi300x1-192gb", + Status: "active", + }, + }, + { + DedicatedInferenceAcceleratorInfo: &godo.DedicatedInferenceAcceleratorInfo{ + ID: "accel-2", + Name: "gpu-mi300x1-192gb", + Slug: "gpu-mi300x1-192gb", + Status: "active", + }, + }, + } + + tm.dedicatedInferences.EXPECT().ListAccelerators("00000000-0000-4000-8000-000000000000", "").Return(testAccelerators, nil) + + config.Args = append(config.Args, "00000000-0000-4000-8000-000000000000") + + err := RunDedicatedInferenceListAccelerators(config) + assert.NoError(t, err) + }) +} + +func TestRunDedicatedInferenceListAccelerators_WithSlug(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + testAccelerators := do.DedicatedInferenceAcceleratorInfos{ + { + DedicatedInferenceAcceleratorInfo: &godo.DedicatedInferenceAcceleratorInfo{ + ID: "accel-1", + Name: "mi300x1-ghfpsf", + Slug: "gpu-mi300x1-192gb", + Status: "ACTIVE", + }, + }, + } + + tm.dedicatedInferences.EXPECT().ListAccelerators("00000000-0000-4000-8000-000000000000", "gpu-mi300x1-192gb").Return(testAccelerators, nil) + + config.Args = append(config.Args, "00000000-0000-4000-8000-000000000000") + config.Doit.Set(config.NS, doctl.ArgDedicatedInferenceAcceleratorSlug, "gpu-mi300x1-192gb") + + err := RunDedicatedInferenceListAccelerators(config) + assert.NoError(t, err) + }) +} + +func TestRunDedicatedInferenceListAccelerators_MissingID(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + err := RunDedicatedInferenceListAccelerators(config) + assert.Error(t, err) + }) +} diff --git a/commands/displayers/dedicated_inference.go b/commands/displayers/dedicated_inference.go index 4eda812ac..01c59b855 100644 --- a/commands/displayers/dedicated_inference.go +++ b/commands/displayers/dedicated_inference.go @@ -71,3 +71,51 @@ func (d *DedicatedInference) KV() []map[string]any { } return out } + +// DedicatedInferenceAccelerator wraps a slice of accelerator info for display. +type DedicatedInferenceAccelerator struct { + DedicatedInferenceAcceleratorInfos do.DedicatedInferenceAcceleratorInfos +} + +var _ Displayable = &DedicatedInferenceAccelerator{} + +func (d *DedicatedInferenceAccelerator) JSON(out io.Writer) error { + return writeJSON(d.DedicatedInferenceAcceleratorInfos, out) +} + +func (d *DedicatedInferenceAccelerator) Cols() []string { + return []string{ + "ID", + "Name", + "Slug", + "Status", + "CreatedAt", + } +} + +func (d *DedicatedInferenceAccelerator) ColMap() map[string]string { + return map[string]string{ + "ID": "ID", + "Name": "Name", + "Slug": "Slug", + "Status": "Status", + "CreatedAt": "Created At", + } +} + +func (d *DedicatedInferenceAccelerator) KV() []map[string]any { + if d == nil || d.DedicatedInferenceAcceleratorInfos == nil { + return []map[string]any{} + } + out := make([]map[string]any, 0, len(d.DedicatedInferenceAcceleratorInfos)) + for _, a := range d.DedicatedInferenceAcceleratorInfos { + out = append(out, map[string]any{ + "ID": a.ID, + "Name": a.Name, + "Slug": a.Slug, + "Status": a.Status, + "CreatedAt": a.CreatedAt, + }) + } + return out +} diff --git a/do/dedicated_inference.go b/do/dedicated_inference.go index 8c2f45525..102601092 100644 --- a/do/dedicated_inference.go +++ b/do/dedicated_inference.go @@ -32,11 +32,20 @@ type DedicatedInferenceToken struct { *godo.DedicatedInferenceToken } +// DedicatedInferenceAcceleratorInfo wraps a godo.DedicatedInferenceAcceleratorInfo. +type DedicatedInferenceAcceleratorInfo struct { + *godo.DedicatedInferenceAcceleratorInfo +} + +// DedicatedInferenceAcceleratorInfos is a slice of DedicatedInferenceAcceleratorInfo. +type DedicatedInferenceAcceleratorInfos []DedicatedInferenceAcceleratorInfo + // DedicatedInferenceService is an interface for interacting with DigitalOcean's Dedicated Inference API. type DedicatedInferenceService interface { Create(req *godo.DedicatedInferenceCreateRequest) (*DedicatedInference, *DedicatedInferenceToken, error) Get(id string) (*DedicatedInference, error) Delete(id string) error + ListAccelerators(diID string, slug string) (DedicatedInferenceAcceleratorInfos, error) } var _ DedicatedInferenceService = &dedicatedInferenceService{} @@ -79,3 +88,31 @@ func (s *dedicatedInferenceService) Delete(id string) error { _, err := s.client.DedicatedInference.Delete(context.TODO(), id) return err } + +// ListAccelerators lists accelerators for a dedicated inference endpoint. +func (s *dedicatedInferenceService) ListAccelerators(diID string, slug string) (DedicatedInferenceAcceleratorInfos, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + list, resp, err := s.client.DedicatedInference.ListAccelerators(context.TODO(), diID, &godo.DedicatedInferenceListAcceleratorsOptions{Slug: slug, ListOptions: *opt}) + if err != nil { + return nil, nil, err + } + + items := make([]any, len(list)) + for i := range list { + items[i] = list[i] + } + return items, resp, nil + } + + si, err := PaginateResp(f) + if err != nil { + return nil, err + } + + list := make(DedicatedInferenceAcceleratorInfos, len(si)) + for i := range si { + a := si[i].(godo.DedicatedInferenceAcceleratorInfo) + list[i] = DedicatedInferenceAcceleratorInfo{DedicatedInferenceAcceleratorInfo: &a} + } + return list, nil +} diff --git a/do/mocks/DedicatedInferenceService.go b/do/mocks/DedicatedInferenceService.go index 70254784a..948e3cd60 100644 --- a/do/mocks/DedicatedInferenceService.go +++ b/do/mocks/DedicatedInferenceService.go @@ -71,6 +71,21 @@ func (mr *MockDedicatedInferenceServiceMockRecorder) Delete(id any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockDedicatedInferenceService)(nil).Delete), id) } +// ListAccelerators mocks base method. +func (m *MockDedicatedInferenceService) ListAccelerators(diID string, slug string) (do.DedicatedInferenceAcceleratorInfos, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAccelerators", diID, slug) + ret0, _ := ret[0].(do.DedicatedInferenceAcceleratorInfos) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAccelerators indicates an expected call of ListAccelerators. +func (mr *MockDedicatedInferenceServiceMockRecorder) ListAccelerators(diID any, slug any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccelerators", reflect.TypeOf((*MockDedicatedInferenceService)(nil).ListAccelerators), diID, slug) +} + // Get mocks base method. func (m *MockDedicatedInferenceService) Get(id string) (*do.DedicatedInference, error) { m.ctrl.T.Helper() From 84ded2588f78cce619369ca4c2ad4435c8ce6ab2 Mon Sep 17 00:00:00 2001 From: rarora Date: Mon, 23 Mar 2026 14:26:23 +0530 Subject: [PATCH 4/5] intergration-tests-added-list-accelerators --- ...icated_inference_list_accelerators_test.go | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 integration/dedicated_inference_list_accelerators_test.go diff --git a/integration/dedicated_inference_list_accelerators_test.go b/integration/dedicated_inference_list_accelerators_test.go new file mode 100644 index 000000000..b958fa994 --- /dev/null +++ b/integration/dedicated_inference_list_accelerators_test.go @@ -0,0 +1,235 @@ +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("dedicated-inference/list-accelerators", 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/dedicated-inferences/00000000-0000-4000-8000-000000000000/accelerators": + 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 + } + + // Check for slug filter query param + slugFilter := req.URL.Query().Get("slug") + if slugFilter == "gpu-mi300x1-192gb" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(dedicatedInferenceListAcceleratorsFilteredResponse)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(dedicatedInferenceListAcceleratorsResponse)) + case "/v2/dedicated-inferences/99999999-9999-4999-8999-999999999999/accelerators": + 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.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"id":"not_found","message":"The resource you requested could not be found."}`)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("valid dedicated inference ID is provided", func() { + it("lists the accelerators", func() { + aliases := []string{"list-accelerators", "la"} + + for _, alias := range aliases { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + alias, + "00000000-0000-4000-8000-000000000000", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output for alias %q: %s", alias, output)) + expect.Equal(strings.TrimSpace(dedicatedInferenceListAcceleratorsOutput), strings.TrimSpace(string(output))) + } + }) + }) + + when("dedicated inference ID is missing", func() { + it("returns an error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "list-accelerators", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "missing") + }) + }) + + when("dedicated inference does not exist", func() { + it("returns a not found error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "list-accelerators", + "99999999-9999-4999-8999-999999999999", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "404") + }) + }) + + when("slug filter is provided", func() { + it("lists only matching accelerators", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "list-accelerators", + "00000000-0000-4000-8000-000000000000", + "--slug", "gpu-mi300x1-192gb", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dedicatedInferenceListAcceleratorsFilteredOutput), strings.TrimSpace(string(output))) + }) + }) + + when("passing a format flag", func() { + it("displays only those columns", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "list-accelerators", + "00000000-0000-4000-8000-000000000000", + "--format", "ID,Slug,Status", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dedicatedInferenceListAcceleratorsFormatOutput), strings.TrimSpace(string(output))) + }) + }) + + when("using the di alias", func() { + it("lists the accelerators", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "di", + "list-accelerators", + "00000000-0000-4000-8000-000000000000", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dedicatedInferenceListAcceleratorsOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + dedicatedInferenceListAcceleratorsOutput = ` +ID Name Slug Status Created At +acc-001 prefill-gpu-1 gpu-mi300x1-192gb ACTIVE 2023-01-01 00:00:00 +0000 UTC +acc-002 decode-gpu-1 gpu-h100x1-80gb ACTIVE 2023-01-01 00:00:00 +0000 UTC +` + dedicatedInferenceListAcceleratorsFilteredOutput = ` +ID Name Slug Status Created At +acc-001 prefill-gpu-1 gpu-mi300x1-192gb ACTIVE 2023-01-01 00:00:00 +0000 UTC +` + dedicatedInferenceListAcceleratorsFormatOutput = ` +ID Slug Status +acc-001 gpu-mi300x1-192gb ACTIVE +acc-002 gpu-h100x1-80gb ACTIVE +` + + dedicatedInferenceListAcceleratorsResponse = ` +{ + "accelerators": [ + { + "id": "acc-001", + "name": "prefill-gpu-1", + "slug": "gpu-mi300x1-192gb", + "status": "ACTIVE", + "created_at": "2023-01-01T00:00:00Z" + }, + { + "id": "acc-002", + "name": "decode-gpu-1", + "slug": "gpu-h100x1-80gb", + "status": "ACTIVE", + "created_at": "2023-01-01T00:00:00Z" + } + ], + "links": {}, + "meta": { + "total": 2 + } +} +` + dedicatedInferenceListAcceleratorsFilteredResponse = ` +{ + "accelerators": [ + { + "id": "acc-001", + "name": "prefill-gpu-1", + "slug": "gpu-mi300x1-192gb", + "status": "ACTIVE", + "created_at": "2023-01-01T00:00:00Z" + } + ], + "links": {}, + "meta": { + "total": 1 + } +} +` +) From 0a83caf460740780718dd16f0c8d183a0eda0e62 Mon Sep 17 00:00:00 2001 From: rarora Date: Mon, 23 Mar 2026 17:43:51 +0530 Subject: [PATCH 5/5] update-dedicated-inference --- commands/dedicated_inference.go | 51 ++++ commands/dedicated_inference_test.go | 88 ++++++ do/dedicated_inference.go | 10 + do/mocks/DedicatedInferenceService.go | 15 + .../dedicated_inference_update_test.go | 259 ++++++++++++++++++ 5 files changed, 423 insertions(+) create mode 100644 integration/dedicated_inference_update_test.go diff --git a/commands/dedicated_inference.go b/commands/dedicated_inference.go index d02b7d474..bbdab1c57 100644 --- a/commands/dedicated_inference.go +++ b/commands/dedicated_inference.go @@ -82,6 +82,22 @@ For more information, see https://docs.digitalocean.com/reference/api/digitaloce AddBoolFlag(cmdDelete, doctl.ArgForce, doctl.ArgShortForce, false, "Delete the dedicated inference endpoint without a confirmation prompt") cmdDelete.Example = `The following example deletes a dedicated inference endpoint: doctl dedicated-inference delete 12345678-1234-1234-1234-123456789012` + cmdUpdate := CmdBuilder( + cmd, + RunDedicatedInferenceUpdate, + "update ", + "Update a dedicated inference endpoint", + `Updates a dedicated inference endpoint using a spec file in JSON or YAML format. +Use the `+"`"+`--spec`+"`"+` flag to provide the path to the updated spec file. +Optionally provide a Hugging Face access token using `+"`"+`--hugging-face-token`+"`"+`.`, + Writer, + aliasOpt("u"), + displayerType(&displayers.DedicatedInference{}), + ) + AddStringFlag(cmdUpdate, doctl.ArgDedicatedInferenceSpec, "", "", `Path to a dedicated inference spec in JSON or YAML format. Set to "-" to read from stdin.`, requiredOpt()) + AddStringFlag(cmdUpdate, doctl.ArgDedicatedInferenceHuggingFaceToken, "", "", "Hugging Face token for accessing gated models (optional)") + cmdUpdate.Example = `The following example updates a dedicated inference endpoint using a spec file: doctl dedicated-inference update 12345678-1234-1234-1234-123456789012 --spec spec.yaml` + cmdListAccelerators := CmdBuilder( cmd, RunDedicatedInferenceListAccelerators, @@ -183,6 +199,41 @@ func RunDedicatedInferenceGet(c *CmdConfig) error { return c.Display(&displayers.DedicatedInference{DedicatedInferences: do.DedicatedInferences{*endpoint}}) } +// RunDedicatedInferenceUpdate updates a dedicated inference endpoint. +func RunDedicatedInferenceUpdate(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + id := c.Args[0] + + specPath, err := c.Doit.GetString(c.NS, doctl.ArgDedicatedInferenceSpec) + if err != nil { + return err + } + + spec, err := readDedicatedInferenceSpec(os.Stdin, specPath) + if err != nil { + return err + } + + req := &godo.DedicatedInferenceUpdateRequest{ + Spec: spec, + } + + hfToken, _ := c.Doit.GetString(c.NS, doctl.ArgDedicatedInferenceHuggingFaceToken) + if hfToken != "" { + req.Secrets = &godo.DedicatedInferenceSecrets{ + HuggingFaceToken: hfToken, + } + } + + endpoint, err := c.DedicatedInferences().Update(id, req) + if err != nil { + return err + } + return c.Display(&displayers.DedicatedInference{DedicatedInferences: do.DedicatedInferences{*endpoint}}) +} + // RunDedicatedInferenceListAccelerators lists accelerators for a dedicated inference endpoint. func RunDedicatedInferenceListAccelerators(c *CmdConfig) error { if len(c.Args) < 1 { diff --git a/commands/dedicated_inference_test.go b/commands/dedicated_inference_test.go index 24b3a2b43..bf966a10c 100644 --- a/commands/dedicated_inference_test.go +++ b/commands/dedicated_inference_test.go @@ -72,6 +72,7 @@ func TestDedicatedInferenceCommand(t *testing.T) { } assert.True(t, subcommands["create"], "Expected create subcommand") assert.True(t, subcommands["get"], "Expected get subcommand") + assert.True(t, subcommands["update"], "Expected update subcommand") assert.True(t, subcommands["delete"], "Expected delete subcommand") assert.True(t, subcommands["list-accelerators"], "Expected list-accelerators subcommand") } @@ -190,6 +191,93 @@ func TestRunDedicatedInferenceDelete_MissingID(t *testing.T) { }) } +func TestRunDedicatedInferenceUpdate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + specJSON := `{ + "version": 0, + "name": "test-dedicated-inference", + "region": "nyc2", + "vpc": {"uuid": "00000000-0000-4000-8000-000000000001"}, + "enable_public_endpoint": true, + "model_deployments": [ + { + "model_slug": "mistral/mistral-7b-instruct-v3", + "model_provider": "hugging_face", + "accelerators": [ + {"scale": 2, "type": "prefill", "accelerator_slug": "gpu-mi300x1-192gb"}, + {"scale": 4, "type": "decode", "accelerator_slug": "gpu-mi300x1-192gb"} + ] + } + ] + }` + tmpFile := t.TempDir() + "/spec.json" + err := os.WriteFile(tmpFile, []byte(specJSON), 0644) + assert.NoError(t, err) + + config.Args = append(config.Args, "00000000-0000-4000-8000-000000000000") + config.Doit.Set(config.NS, doctl.ArgDedicatedInferenceSpec, tmpFile) + + expectedReq := &godo.DedicatedInferenceUpdateRequest{ + Spec: testDedicatedInferenceSpecRequest, + } + + updatedDI := testDedicatedInference + tm.dedicatedInferences.EXPECT().Update("00000000-0000-4000-8000-000000000000", expectedReq).Return(&updatedDI, nil) + + err = RunDedicatedInferenceUpdate(config) + assert.NoError(t, err) + }) +} + +func TestRunDedicatedInferenceUpdate_WithHuggingFaceToken(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + specJSON := `{ + "version": 0, + "name": "test-dedicated-inference", + "region": "nyc2", + "vpc": {"uuid": "00000000-0000-4000-8000-000000000001"}, + "enable_public_endpoint": true, + "model_deployments": [ + { + "model_slug": "mistral/mistral-7b-instruct-v3", + "model_provider": "hugging_face", + "accelerators": [ + {"scale": 2, "type": "prefill", "accelerator_slug": "gpu-mi300x1-192gb"}, + {"scale": 4, "type": "decode", "accelerator_slug": "gpu-mi300x1-192gb"} + ] + } + ] + }` + tmpFile := t.TempDir() + "/spec.json" + err := os.WriteFile(tmpFile, []byte(specJSON), 0644) + assert.NoError(t, err) + + config.Args = append(config.Args, "00000000-0000-4000-8000-000000000000") + config.Doit.Set(config.NS, doctl.ArgDedicatedInferenceSpec, tmpFile) + config.Doit.Set(config.NS, doctl.ArgDedicatedInferenceHuggingFaceToken, "hf_test_token") + + expectedReq := &godo.DedicatedInferenceUpdateRequest{ + Spec: testDedicatedInferenceSpecRequest, + Secrets: &godo.DedicatedInferenceSecrets{ + HuggingFaceToken: "hf_test_token", + }, + } + + updatedDI := testDedicatedInference + tm.dedicatedInferences.EXPECT().Update("00000000-0000-4000-8000-000000000000", expectedReq).Return(&updatedDI, nil) + + err = RunDedicatedInferenceUpdate(config) + assert.NoError(t, err) + }) +} + +func TestRunDedicatedInferenceUpdate_MissingID(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + err := RunDedicatedInferenceUpdate(config) + assert.Error(t, err) + }) +} + func TestRunDedicatedInferenceListAccelerators(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { testAccelerators := do.DedicatedInferenceAcceleratorInfos{ diff --git a/do/dedicated_inference.go b/do/dedicated_inference.go index 102601092..92c106f8f 100644 --- a/do/dedicated_inference.go +++ b/do/dedicated_inference.go @@ -44,6 +44,7 @@ type DedicatedInferenceAcceleratorInfos []DedicatedInferenceAcceleratorInfo type DedicatedInferenceService interface { Create(req *godo.DedicatedInferenceCreateRequest) (*DedicatedInference, *DedicatedInferenceToken, error) Get(id string) (*DedicatedInference, error) + Update(id string, req *godo.DedicatedInferenceUpdateRequest) (*DedicatedInference, error) Delete(id string) error ListAccelerators(diID string, slug string) (DedicatedInferenceAcceleratorInfos, error) } @@ -83,6 +84,15 @@ func (s *dedicatedInferenceService) Get(id string) (*DedicatedInference, error) return &DedicatedInference{DedicatedInference: d}, nil } +// Update updates a dedicated inference endpoint by ID. +func (s *dedicatedInferenceService) Update(id string, req *godo.DedicatedInferenceUpdateRequest) (*DedicatedInference, error) { + d, _, err := s.client.DedicatedInference.Update(context.TODO(), id, req) + if err != nil { + return nil, err + } + return &DedicatedInference{DedicatedInference: d}, nil +} + // Delete deletes a dedicated inference endpoint by ID. func (s *dedicatedInferenceService) Delete(id string) error { _, err := s.client.DedicatedInference.Delete(context.TODO(), id) diff --git a/do/mocks/DedicatedInferenceService.go b/do/mocks/DedicatedInferenceService.go index 948e3cd60..df5432eea 100644 --- a/do/mocks/DedicatedInferenceService.go +++ b/do/mocks/DedicatedInferenceService.go @@ -100,3 +100,18 @@ func (mr *MockDedicatedInferenceServiceMockRecorder) Get(id any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDedicatedInferenceService)(nil).Get), id) } + +// Update mocks base method. +func (m *MockDedicatedInferenceService) Update(id string, req *godo.DedicatedInferenceUpdateRequest) (*do.DedicatedInference, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", id, req) + ret0, _ := ret[0].(*do.DedicatedInference) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockDedicatedInferenceServiceMockRecorder) Update(id any, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockDedicatedInferenceService)(nil).Update), id, req) +} diff --git a/integration/dedicated_inference_update_test.go b/integration/dedicated_inference_update_test.go new file mode 100644 index 000000000..5c6d308c6 --- /dev/null +++ b/integration/dedicated_inference_update_test.go @@ -0,0 +1,259 @@ +package integration + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("dedicated-inference/update", 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/dedicated-inferences/00000000-0000-4000-8000-000000000000": + 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 + } + + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatal("failed to read request body") + } + + var payload map[string]interface{} + err = json.Unmarshal(body, &payload) + if err != nil { + t.Fatalf("failed to parse request body: %s", err) + } + + // Verify the spec is present in the request + if _, ok := payload["spec"]; !ok { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"id":"bad_request","message":"spec is required"}`)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(dedicatedInferenceUpdateResponse)) + case "/v2/dedicated-inferences/99999999-9999-4999-8999-999999999999": + 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 + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"id":"not_found","message":"The resource you requested could not be found."}`)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("valid ID and spec are provided", func() { + it("updates the dedicated inference endpoint", func() { + specFile := createDedicatedInferenceUpdateSpecFile(t) + + aliases := []string{"update", "u"} + + for _, alias := range aliases { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + alias, + "00000000-0000-4000-8000-000000000000", + "--spec", specFile, + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output for alias %q: %s", alias, output)) + expect.Equal(strings.TrimSpace(dedicatedInferenceUpdateOutput), strings.TrimSpace(string(output))) + } + }) + }) + + when("dedicated inference ID is missing", func() { + it("returns an error", func() { + specFile := createDedicatedInferenceUpdateSpecFile(t) + + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "update", + "--spec", specFile, + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "missing") + }) + }) + + when("spec flag is missing", func() { + it("returns an error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "update", + "00000000-0000-4000-8000-000000000000", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "spec") + }) + }) + + when("dedicated inference does not exist", func() { + it("returns a not found error", func() { + specFile := createDedicatedInferenceUpdateSpecFile(t) + + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "update", + "99999999-9999-4999-8999-999999999999", + "--spec", specFile, + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "404") + }) + }) + + when("using the di alias", func() { + it("updates the dedicated inference endpoint", func() { + specFile := createDedicatedInferenceUpdateSpecFile(t) + + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "di", + "update", + "00000000-0000-4000-8000-000000000000", + "--spec", specFile, + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dedicatedInferenceUpdateOutput), strings.TrimSpace(string(output))) + }) + }) + + when("passing a format flag", func() { + it("displays only those columns", func() { + specFile := createDedicatedInferenceUpdateSpecFile(t) + + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "dedicated-inference", + "update", + "00000000-0000-4000-8000-000000000000", + "--spec", specFile, + "--format", "ID,Name,Status", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(dedicatedInferenceUpdateFormatOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +func createDedicatedInferenceUpdateSpecFile(t *testing.T) string { + t.Helper() + specJSON := `{ + "version": 0, + "name": "updated-dedicated-inference", + "region": "nyc2", + "vpc": {"uuid": "00000000-0000-4000-8000-000000000001"}, + "enable_public_endpoint": true, + "model_deployments": [ + { + "model_slug": "mistral/mistral-7b-instruct-v3", + "model_provider": "hugging_face", + "accelerators": [ + {"scale": 3, "type": "prefill", "accelerator_slug": "gpu-mi300x1-192gb"}, + {"scale": 6, "type": "decode", "accelerator_slug": "gpu-mi300x1-192gb"} + ] + } + ] + }` + tmpFile := t.TempDir() + "/update-spec.json" + err := os.WriteFile(tmpFile, []byte(specJSON), 0644) + if err != nil { + t.Fatalf("failed to write spec file: %s", err) + } + return tmpFile +} + +const ( + dedicatedInferenceUpdateOutput = ` +ID Name Region Status VPC UUID Public Endpoint Private Endpoint Created At Updated At +00000000-0000-4000-8000-000000000000 updated-dedicated-inference nyc2 UPDATING 00000000-0000-4000-8000-000000000001 public.dedicated-inference.example.com private.dedicated-inference.example.com 2023-01-01 00:00:00 +0000 UTC 2023-01-02 00:00:00 +0000 UTC +` + dedicatedInferenceUpdateFormatOutput = ` +ID Name Status +00000000-0000-4000-8000-000000000000 updated-dedicated-inference UPDATING +` + + dedicatedInferenceUpdateResponse = ` +{ + "dedicated_inference": { + "id": "00000000-0000-4000-8000-000000000000", + "name": "updated-dedicated-inference", + "region": "nyc2", + "status": "UPDATING", + "vpc_uuid": "00000000-0000-4000-8000-000000000001", + "endpoints": { + "public_endpoint_fqdn": "public.dedicated-inference.example.com", + "private_endpoint_fqdn": "private.dedicated-inference.example.com" + }, + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-02T00:00:00Z" + } +} +` +)