Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/aiservices/aiservices.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package aiservices

import (
exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/cmd/aiservices/apikey"
"github.com/exoscale/cli/cmd/aiservices/deployment"
"github.com/exoscale/cli/cmd/aiservices/model"
"github.com/spf13/cobra"
Expand All @@ -18,4 +19,5 @@ func init() {
// Attach subcommand groups
DedicatedInferenceCmd.AddCommand(model.Cmd)
DedicatedInferenceCmd.AddCommand(deployment.Cmd)
DedicatedInferenceCmd.AddCommand(apikey.Cmd)
}
11 changes: 11 additions & 0 deletions cmd/aiservices/apikey/apikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package apikey

import (
"github.com/spf13/cobra"
)

// Cmd is the root command for apikey subcommands.
var Cmd = &cobra.Command{
Use: "api-key",
Short: "Manage AI API keys",
}
62 changes: 62 additions & 0 deletions cmd/aiservices/apikey/apikey_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package apikey

import (
"fmt"
"os"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/globalstate"
v3 "github.com/exoscale/egoscale/v3"
"github.com/spf13/cobra"
)

type AIAPIKeyCreateCmd struct {
exocmd.CliCommandSettings `cli-cmd:"-"`

_ bool `cli-cmd:"create"`

Name string `cli-flag:"name" cli-usage:"AI API key name"`
Scope string `cli-flag:"scope" cli-usage:"Scope: 'public' for all deployments, or a deployment UUID"`
}

func (c *AIAPIKeyCreateCmd) CmdAliases() []string { return exocmd.GCreateAlias }
func (c *AIAPIKeyCreateCmd) CmdShort() string { return "Create AI API key" }
func (c *AIAPIKeyCreateCmd) CmdLong() string {
return "This command creates an AI API key."
}
func (c *AIAPIKeyCreateCmd) CmdPreRun(cmd *cobra.Command, args []string) error {
return exocmd.CliCommandDefaultPreRun(c, cmd, args)
}
func (c *AIAPIKeyCreateCmd) CmdRun(_ *cobra.Command, _ []string) error {
ctx := exocmd.GContext
client := globalstate.EgoscaleV3Client

if c.Name == "" {
return fmt.Errorf("--name is required")
}
if c.Scope == "" {
return fmt.Errorf("--scope is required")
}

req := v3.CreateAIAPIKeyRequest{

Check failure on line 41 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

undefined: v3.CreateAIAPIKeyRequest
Name: c.Name,
Scope: c.Scope,
}

resp, err := client.CreateAIAPIKey(ctx, req)

Check failure on line 46 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

client.CreateAIAPIKey undefined (type *v3.Client has no field or method CreateAIAPIKey)
if err != nil {
return err
}

if !globalstate.Quiet {
fmt.Fprintf(os.Stderr, "Use 'reveal' command to get the API key value.\n\n")
fmt.Fprintf(os.Stdout, "ID: %s\n", resp.ID)

Check failure on line 53 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / govulncheck

resp.ID undefined (type *v3.AIAPIKeyWithValue has no field or method ID)

Check failure on line 53 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Run E2E (Testscript) Tests / Without API

resp.ID undefined (type *v3.AIAPIKeyWithValue has no field or method ID)

Check failure on line 53 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Run E2E (Testscript) Tests / With API

resp.ID undefined (type *v3.AIAPIKeyWithValue has no field or method ID)

Check failure on line 53 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / lint

resp.ID undefined (type *v3.AIAPIKeyWithValue has no field or method ID)

Check failure on line 53 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

resp.ID undefined (type *v3.AIAPIKeyWithValue has no field or method ID)

Check failure on line 53 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

resp.ID undefined (type *v3.AIAPIKeyWithValue has no field or method ID)
fmt.Fprintf(os.Stdout, "Name: %s\n", resp.Name)

Check failure on line 54 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / govulncheck

resp.Name undefined (type *v3.AIAPIKeyWithValue has no field or method Name)

Check failure on line 54 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Run E2E (Testscript) Tests / Without API

resp.Name undefined (type *v3.AIAPIKeyWithValue has no field or method Name)

Check failure on line 54 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Run E2E (Testscript) Tests / With API

resp.Name undefined (type *v3.AIAPIKeyWithValue has no field or method Name)

Check failure on line 54 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / lint

resp.Name undefined (type *v3.AIAPIKeyWithValue has no field or method Name)

Check failure on line 54 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

resp.Name undefined (type *v3.AIAPIKeyWithValue has no field or method Name)

Check failure on line 54 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

resp.Name undefined (type *v3.AIAPIKeyWithValue has no field or method Name)
fmt.Fprintf(os.Stdout, "Scope: %s\n", resp.Scope)

Check failure on line 55 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / govulncheck

resp.Scope undefined (type *v3.AIAPIKeyWithValue has no field or method Scope)

Check failure on line 55 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Run E2E (Testscript) Tests / Without API

resp.Scope undefined (type *v3.AIAPIKeyWithValue has no field or method Scope)

Check failure on line 55 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Run E2E (Testscript) Tests / With API

resp.Scope undefined (type *v3.AIAPIKeyWithValue has no field or method Scope)

Check failure on line 55 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / lint

resp.Scope undefined (type *v3.AIAPIKeyWithValue has no field or method Scope)

Check failure on line 55 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

resp.Scope undefined (type *v3.AIAPIKeyWithValue has no field or method Scope)

Check failure on line 55 in cmd/aiservices/apikey/apikey_create.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

resp.Scope undefined (type *v3.AIAPIKeyWithValue has no field or method Scope)
}
return nil
}

func init() {
cobra.CheckErr(exocmd.RegisterCLICommand(Cmd, &AIAPIKeyCreateCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings()}))
}
38 changes: 38 additions & 0 deletions cmd/aiservices/apikey/apikey_create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package apikey

import (
"context"
"testing"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/globalstate"
v3 "github.com/exoscale/egoscale/v3"
"github.com/exoscale/egoscale/v3/credentials"
)

func TestAIAPIKeyCreateValidationAndSuccess(t *testing.T) {
ts := newAPIKeyHelperServer(t)
defer ts.server.Close()

exocmd.GContext = context.Background()
globalstate.Quiet = true
creds := credentials.NewStaticCredentials("key", "secret")
client, err := v3.NewClient(creds)
if err != nil {
t.Fatalf("new client: %v", err)
}
globalstate.EgoscaleV3Client = client.WithEndpoint(v3.Endpoint(ts.server.URL))

// missing flags
c := &AIAPIKeyCreateCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings()}
if err := c.CmdRun(nil, nil); err == nil {
t.Fatalf("expected error for missing flags")
}

// success
c.Name = "test-key"
c.Scope = "public"
if err := c.CmdRun(nil, nil); err != nil {
t.Fatalf("create api key: %v", err)
}
}
72 changes: 72 additions & 0 deletions cmd/aiservices/apikey/apikey_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package apikey

import (
"fmt"
"os"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/globalstate"
"github.com/exoscale/cli/utils"
v3 "github.com/exoscale/egoscale/v3"
"github.com/spf13/cobra"
)

type AIAPIKeyDeleteCmd struct {
exocmd.CliCommandSettings `cli-cmd:"-"`

_ bool `cli-cmd:"delete"`

IDs []string `cli-arg:"#" cli-usage:"AI API key ID..."`
Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"`
}

func (c *AIAPIKeyDeleteCmd) CmdAliases() []string { return exocmd.GDeleteAlias }
func (c *AIAPIKeyDeleteCmd) CmdShort() string { return "Delete AI API key" }
func (c *AIAPIKeyDeleteCmd) CmdLong() string {
return "This command deletes AI API keys by ID."
}
func (c *AIAPIKeyDeleteCmd) CmdPreRun(cmd *cobra.Command, args []string) error {
return exocmd.CliCommandDefaultPreRun(c, cmd, args)
}
func (c *AIAPIKeyDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error {
ctx := exocmd.GContext
client := globalstate.EgoscaleV3Client

if len(c.IDs) == 0 {
return fmt.Errorf("at least one ID is required")
}

for _, idStr := range c.IDs {
id, err := v3.ParseUUID(idStr)
if err != nil {
if !c.Force {
return fmt.Errorf("invalid AI API key ID %q: %w", idStr, err)
}
fmt.Fprintf(os.Stderr, "warning: invalid AI API key ID %q\n", idStr)
continue
}

if !c.Force {
if !utils.AskQuestion(ctx, fmt.Sprintf("Are you sure you want to delete AI API key %q?", idStr)) {
continue
}
}

if _, err := client.DeleteAIAPIKey(ctx, id); err != nil {

Check failure on line 55 in cmd/aiservices/apikey/apikey_delete.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

client.DeleteAIAPIKey undefined (type *v3.Client has no field or method DeleteAIAPIKey)
if !c.Force {
return err
}
fmt.Fprintf(os.Stderr, "warning: failed to delete AI API key %q: %v\n", idStr, err)
continue
}
}

if !globalstate.Quiet {
fmt.Fprintln(os.Stdout, "AI API key(s) deleted.")
}
return nil
}

func init() {
cobra.CheckErr(exocmd.RegisterCLICommand(Cmd, &AIAPIKeyDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings()}))
}
38 changes: 38 additions & 0 deletions cmd/aiservices/apikey/apikey_delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package apikey

import (
"context"
"testing"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/globalstate"
v3 "github.com/exoscale/egoscale/v3"
"github.com/exoscale/egoscale/v3/credentials"
)

func TestAIAPIKeyDeleteValidationAndSuccess(t *testing.T) {
ts := newAPIKeyHelperServer(t)
defer ts.server.Close()

exocmd.GContext = context.Background()
globalstate.Quiet = true
creds := credentials.NewStaticCredentials("key", "secret")
client, err := v3.NewClient(creds)
if err != nil {
t.Fatalf("new client: %v", err)
}
globalstate.EgoscaleV3Client = client.WithEndpoint(v3.Endpoint(ts.server.URL))

// missing IDs
c := &AIAPIKeyDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings()}
if err := c.CmdRun(nil, nil); err == nil {
t.Fatalf("expected error for missing IDs")
}

// success with force
c.IDs = []string{"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
c.Force = true
if err := c.CmdRun(nil, nil); err != nil {
t.Fatalf("delete api key: %v", err)
}
}
73 changes: 73 additions & 0 deletions cmd/aiservices/apikey/apikey_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package apikey

import (
"fmt"
"time"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/globalstate"
"github.com/exoscale/cli/pkg/output"
v3 "github.com/exoscale/egoscale/v3"
"github.com/spf13/cobra"
)

type AIAPIKeyShowOutput struct {
ID v3.UUID `json:"id"`
Name string `json:"name"`
Scope string `json:"scope"`
CreatedAt string `json:"created_at" outputLabel:"Created At"`
UpdatedAt string `json:"updated_at" outputLabel:"Updated At"`
}

func (o *AIAPIKeyShowOutput) ToJSON() { output.JSON(o) }
func (o *AIAPIKeyShowOutput) ToText() { output.Text(o) }
func (o *AIAPIKeyShowOutput) ToTable() { output.Table(o) }

type AIAPIKeyGetCmd struct {
exocmd.CliCommandSettings `cli-cmd:"-"`

_ bool `cli-cmd:"show"`

ID string `cli-arg:"#" cli-usage:"AI API key ID"`
}

func (c *AIAPIKeyGetCmd) CmdAliases() []string { return exocmd.GShowAlias }
func (c *AIAPIKeyGetCmd) CmdShort() string { return "Show AI API key details" }
func (c *AIAPIKeyGetCmd) CmdLong() string {
return "This command shows details of an AI API key by ID."
}
func (c *AIAPIKeyGetCmd) CmdPreRun(cmd *cobra.Command, args []string) error {
return exocmd.CliCommandDefaultPreRun(c, cmd, args)
}
func (c *AIAPIKeyGetCmd) CmdRun(_ *cobra.Command, _ []string) error {
ctx := exocmd.GContext
client := globalstate.EgoscaleV3Client

if c.ID == "" {
return fmt.Errorf("ID is required")
}

id, err := v3.ParseUUID(c.ID)
if err != nil {
return fmt.Errorf("invalid AI API key ID: %w", err)
}

resp, err := client.GetAIAPIKey(ctx, id)

Check failure on line 55 in cmd/aiservices/apikey/apikey_get.go

View workflow job for this annotation

GitHub Actions / Build + Run Unit Tests

client.GetAIAPIKey undefined (type *v3.Client has no field or method GetAIAPIKey)
if err != nil {
return err
}

out := &AIAPIKeyShowOutput{
ID: resp.ID,
Name: resp.Name,
Scope: resp.Scope,
CreatedAt: resp.CreatedAT.Format(time.RFC3339),
UpdatedAt: resp.UpdatedAT.Format(time.RFC3339),
}

return c.OutputFunc(out, nil)
}

func init() {
cobra.CheckErr(exocmd.RegisterCLICommand(Cmd, &AIAPIKeyGetCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings()}))
}
54 changes: 54 additions & 0 deletions cmd/aiservices/apikey/apikey_get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package apikey

import (
"context"
"testing"
"time"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/globalstate"
v3 "github.com/exoscale/egoscale/v3"
"github.com/exoscale/egoscale/v3/credentials"
)

func TestAIAPIKeyGet(t *testing.T) {
ts := newAPIKeyHelperServer(t)
defer ts.server.Close()
now := time.Now()
ts.keys = []v3.AIAPIKey{
{ID: v3.UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), Name: "key1", Scope: "public", CreatedAT: now, UpdatedAT: now},
}

exocmd.GContext = context.Background()
globalstate.Quiet = true
creds := credentials.NewStaticCredentials("key", "secret")
client, err := v3.NewClient(creds)
if err != nil {
t.Fatalf("new client: %v", err)
}
globalstate.EgoscaleV3Client = client.WithEndpoint(v3.Endpoint(ts.server.URL))

c := &AIAPIKeyGetCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
if err := c.CmdRun(nil, nil); err != nil {
t.Fatalf("get api key: %v", err)
}
}

func TestAIAPIKeyGetMissingID(t *testing.T) {
ts := newAPIKeyHelperServer(t)
defer ts.server.Close()

exocmd.GContext = context.Background()
globalstate.Quiet = true
creds := credentials.NewStaticCredentials("key", "secret")
client, err := v3.NewClient(creds)
if err != nil {
t.Fatalf("new client: %v", err)
}
globalstate.EgoscaleV3Client = client.WithEndpoint(v3.Endpoint(ts.server.URL))

c := &AIAPIKeyGetCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings()}
if err := c.CmdRun(nil, nil); err == nil {
t.Fatalf("expected error for missing ID")
}
}
Loading
Loading