From 34b107e062221ef902e7d5799f39c8cc86208351 Mon Sep 17 00:00:00 2001 From: Syeda Haya Fatima Date: Mon, 7 Sep 2026 20:30:39 +0200 Subject: [PATCH 1/5] Added functionality for model experiments on Stackit CLI --- go.mod | 1 + go.sum | 2 + .../instance/create/create.go | 155 ++++++++++ .../instance/create/create_test.go | 178 +++++++++++ .../instance/delete/delete.go | 136 +++++++++ .../instance/delete/delete_test.go | 179 +++++++++++ .../cmd/modelexperiments/instance/get/get.go | 124 ++++++++ .../modelexperiments/instance/get/get_test.go | 181 +++++++++++ .../cmd/modelexperiments/instance/instance.go | 35 +++ .../modelexperiments/instance/list/list.go | 116 ++++++++ .../instance/list/list_test.go | 176 +++++++++++ .../modelexperiments/instance/patch/patch.go | 207 +++++++++++++ .../instance/patch/patch_test.go | 281 ++++++++++++++++++ .../cmd/modelexperiments/modelexperiments.go | 29 ++ .../modelexperiments/token/create/create.go | 106 +++++++ .../token/create/create_test.go | 100 +++++++ .../modelexperiments/token/delete/delete.go | 81 +++++ .../token/delete/delete_test.go | 63 ++++ .../cmd/modelexperiments/token/get/get.go | 85 ++++++ .../modelexperiments/token/get/get_test.go | 73 +++++ .../cmd/modelexperiments/token/list/list.go | 75 +++++ .../modelexperiments/token/list/list_test.go | 74 +++++ .../cmd/modelexperiments/token/patch/patch.go | 107 +++++++ .../token/patch/patch_test.go | 95 ++++++ internal/cmd/modelexperiments/token/token.go | 19 ++ internal/cmd/root.go | 2 + internal/pkg/config/config.go | 30 +- .../modelexperiments/client/client.go | 14 + .../services/modelexperiments/utils/utils.go | 36 +++ .../modelexperiments/utils/utils_test.go | 96 ++++++ 30 files changed, 2842 insertions(+), 14 deletions(-) create mode 100644 internal/cmd/modelexperiments/instance/create/create.go create mode 100644 internal/cmd/modelexperiments/instance/create/create_test.go create mode 100644 internal/cmd/modelexperiments/instance/delete/delete.go create mode 100644 internal/cmd/modelexperiments/instance/delete/delete_test.go create mode 100644 internal/cmd/modelexperiments/instance/get/get.go create mode 100644 internal/cmd/modelexperiments/instance/get/get_test.go create mode 100644 internal/cmd/modelexperiments/instance/instance.go create mode 100644 internal/cmd/modelexperiments/instance/list/list.go create mode 100644 internal/cmd/modelexperiments/instance/list/list_test.go create mode 100644 internal/cmd/modelexperiments/instance/patch/patch.go create mode 100644 internal/cmd/modelexperiments/instance/patch/patch_test.go create mode 100644 internal/cmd/modelexperiments/modelexperiments.go create mode 100644 internal/cmd/modelexperiments/token/create/create.go create mode 100644 internal/cmd/modelexperiments/token/create/create_test.go create mode 100644 internal/cmd/modelexperiments/token/delete/delete.go create mode 100644 internal/cmd/modelexperiments/token/delete/delete_test.go create mode 100644 internal/cmd/modelexperiments/token/get/get.go create mode 100644 internal/cmd/modelexperiments/token/get/get_test.go create mode 100644 internal/cmd/modelexperiments/token/list/list.go create mode 100644 internal/cmd/modelexperiments/token/list/list_test.go create mode 100644 internal/cmd/modelexperiments/token/patch/patch.go create mode 100644 internal/cmd/modelexperiments/token/patch/patch_test.go create mode 100644 internal/cmd/modelexperiments/token/token.go create mode 100644 internal/pkg/services/modelexperiments/client/client.go create mode 100644 internal/pkg/services/modelexperiments/utils/utils.go create mode 100644 internal/pkg/services/modelexperiments/utils/utils_test.go diff --git a/go.mod b/go.mod index 916453c27..db0195702 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/iaas v1.14.1 github.com/stackitcloud/stackit-sdk-go/services/intake v0.11.1 github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.1 + github.com/stackitcloud/stackit-sdk-go/services/modelexperiments v0.3.0 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.1 github.com/stackitcloud/stackit-sdk-go/services/opensearch v1.3.0 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.13.0 diff --git a/go.sum b/go.sum index 854729b5f..9c6ef14e2 100644 --- a/go.sum +++ b/go.sum @@ -620,6 +620,8 @@ github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.1 h1:sWvLJF6/7Nh3hONp github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.1/go.mod h1:tvRejL8w5KpGBbLFPQ+dXOJURgZ3OMbZmwxlKQrGMuA= github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.3.0 h1:0JuNaVbyuIUEX5Cn88XoC5FVGU92cP1w3ZLAvCuuohQ= github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.3.0/go.mod h1:joa89Y1dyn0j22FstRcIKfW2ada3FDxNfttxSvq27uY= +github.com/stackitcloud/stackit-sdk-go/services/modelexperiments v0.3.0 h1:g8AX62NqDLJy2dNPPTzfeaBoaR2K+EIwxF8sRiAESL0= +github.com/stackitcloud/stackit-sdk-go/services/modelexperiments v0.3.0/go.mod h1:TW2PYG0kSrfAos3yY8wUxDem0J9ZYSXulnbzBZ4BTaE= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.1 h1:lLR6Ouu3H21jjyIcAZZc4f2SOBWgHTBEHbn/urtY+9M= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.1/go.mod h1:0hHEPiOEMAA23EzEl42Rm3FlyKIzkW+LWLvDkuFTZ+Q= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.9.1 h1:9n1BrPj6gAuKnyJ1OmsN+MEl2DBg9KJHgUfkijN84Rs= diff --git a/internal/cmd/modelexperiments/instance/create/create.go b/internal/cmd/modelexperiments/instance/create/create.go new file mode 100644 index 000000000..9829833fc --- /dev/null +++ b/internal/cmd/modelexperiments/instance/create/create.go @@ -0,0 +1,155 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + nameFlag = "name" + descriptionFlag = "description" + labelFlag = "label" + retentionFlag = "deleted-experiment-retention" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + Name string + Description *string + Labels *map[string]string + Retention *string + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates an AI Model Experiments instance", + Long: "Creates an AI Model Experiments (MLflow) instance in your STACKIT project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create an AI Model Experiments instance with name "my-tracking"`, + `$ stackit ai-model-experiments instance create --name my-tracking`), + examples.NewExample( + `Create an instance with a description and labels`, + `$ stackit ai-model-experiments instance create --name my-tracking --description "team tracking server" --label env=prod`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create an AI Model Experiments instance for project %q?", projectLabel) + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + + req := buildCreateInstanceRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create AI Model Experiments instance: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(nameFlag, "n", "", "Instance name") + cmd.Flags().String(descriptionFlag, "", "Instance description") + cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`) + cmd.Flags().String(retentionFlag, "", `Retention period for deleted experiments before permanent purge, e.g. "30d" (min 1d, max 90d)`) + err := flags.MarkFlagsRequired(cmd, nameFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + labels, err := cmd.Flags().GetStringToString(labelFlag) + if err != nil { + return nil, fmt.Errorf("parse %q flag: %w", labelFlag, err) + } + var labelsPtr *map[string]string + if len(labels) > 0 { + labelsPtr = &labels + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: labelsPtr, + Retention: flags.FlagToStringPointer(p, cmd, retentionFlag), + Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildCreateInstanceRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiCreateInstanceRequest { + req := apiClient.DefaultAPI.CreateInstance(ctx, model.ProjectId, model.Region) + + payload := modelexperiments.CreateInstancePayload{ + Name: model.Name, + } + if model.Description != nil { + payload.Description = model.Description + } + if model.Labels != nil { + payload.Labels = model.Labels + } + if model.Retention != nil { + payload.DeletedExperimentRetention = model.Retention + } + + return req.CreateInstancePayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *modelexperiments.CreateInstanceResponse) error { + if resp == nil { + return fmt.Errorf("response instance is nil") + } + + return p.OutputResult(outputFormat, resp.Instance, func() error { + p.Outputf("Creating AI Model Experiments instance for project %q. Instance ID: %s\n", projectLabel, resp.Instance.Id) + return nil + }) +} diff --git a/internal/cmd/modelexperiments/instance/create/create_test.go b/internal/cmd/modelexperiments/instance/create/create_test.go new file mode 100644 index 000000000..2bc86ef03 --- /dev/null +++ b/internal/cmd/modelexperiments/instance/create/create_test.go @@ -0,0 +1,178 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &modelexperiments.APIClient{ + DefaultAPI: modelexperiments.DefaultAPIServiceMock{}, +} +var testProjectId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + nameFlag: "example", + globalflags.RegionFlag: "eu01", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu01", + Verbosity: globalflags.VerbosityDefault, + }, + Name: "example", + Region: "eu01", + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *modelexperiments.ApiCreateInstanceRequest)) modelexperiments.ApiCreateInstanceRequest { + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, "eu01") + request = request.CreateInstancePayload(modelexperiments.CreateInstancePayload{ + Name: "example", + }) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "name missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + { + description: "with description and retention", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[descriptionFlag] = "team tracking server" + flagValues[retentionFlag] = "30d" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("team tracking server") + model.Retention = utils.Ptr("30d") + }), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildCreateInstanceRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest modelexperiments.ApiCreateInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "with description", + model: fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("team tracking server") + }), + expectedRequest: fixtureRequest(func(request *modelexperiments.ApiCreateInstanceRequest) { + payload := modelexperiments.CreateInstancePayload{ + Name: "example", + Description: utils.Ptr("team tracking server"), + } + *request = request.CreateInstancePayload(payload) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildCreateInstanceRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + name string + resp *modelexperiments.CreateInstanceResponse + wantErr bool + }{ + { + name: "nil response", + resp: nil, + wantErr: true, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, "", "test-project", tt.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/modelexperiments/instance/delete/delete.go b/internal/cmd/modelexperiments/instance/delete/delete.go new file mode 100644 index 000000000..4136d4984 --- /dev/null +++ b/internal/cmd/modelexperiments/instance/delete/delete.go @@ -0,0 +1,136 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", instanceIdArg), + Short: "Deletes an AI Model Experiments instance", + Long: "Deletes an AI Model Experiments instance from a STACKIT project.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete an AI Model Experiments instance with ID "xxx"`, + `$ stackit ai-model-experiments instance delete xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + prompt := fmt.Sprintf( + "Are you sure you want to delete AI Model Experiments instance %q?", + model.InstanceId, + ) + + if err := params.Printer.PromptForConfirmation(prompt); err != nil { + return err + } + + req := buildDeleteInstanceRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("delete AI Model Experiments instance: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + _ = flags.MarkFlagsRequired(cmd, globalflags.RegionFlag) +} + +func parseInput( + p *print.Printer, + cmd *cobra.Command, + inputArgs []string, +) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: inputArgs[0], + Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), + } + + p.DebugInputModel(model) + + return &model, nil +} + +func buildDeleteInstanceRequest( + ctx context.Context, + model *inputModel, + apiClient *modelexperiments.APIClient, +) modelexperiments.ApiDeleteInstanceRequest { + return apiClient.DefaultAPI.DeleteInstance( + ctx, + model.ProjectId, + model.Region, + model.InstanceId, + ) +} + +func outputResult( + p *print.Printer, + outputFormat string, + resp *modelexperiments.DeleteInstanceResponse, +) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp.Instance, func() error { + p.Outputf( + "Deleted AI Model Experiments instance.\n", + ) + return nil + }) +} diff --git a/internal/cmd/modelexperiments/instance/delete/delete_test.go b/internal/cmd/modelexperiments/instance/delete/delete_test.go new file mode 100644 index 000000000..fe21ee06c --- /dev/null +++ b/internal/cmd/modelexperiments/instance/delete/delete_test.go @@ -0,0 +1,179 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + +var testClient = &modelexperiments.APIClient{ + DefaultAPI: modelexperiments.DefaultAPIServiceMock{}, +} + +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues( + mods ...func(*map[string]string), +) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + globalflags.RegionFlag: "eu01", + } + + for _, mod := range mods { + mod(&flagValues) + } + + return flagValues +} + +func fixtureInputModel( + mods ...func(*inputModel), +) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu01", + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + Region: "eu01", + } + + for _, mod := range mods { + mod(model) + } + + return model +} + +func fixtureRequest( + mods ...func(*modelexperiments.ApiDeleteInstanceRequest), +) modelexperiments.ApiDeleteInstanceRequest { + request := testClient.DefaultAPI.DeleteInstance( + testCtx, + testProjectId, + "eu01", + testInstanceId, + ) + + for _, mod := range mods { + mod(&request) + } + + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues *map[string]string) { + delete(*flagValues, projectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput( + t, + NewCmd, + parseInput, + tt.expectedModel, + tt.argValues, + tt.flagValues, + tt.isValid, + ) + }) + } +} + +func TestBuildDeleteInstanceRequest(t *testing.T) { + request := buildDeleteInstanceRequest( + testCtx, + fixtureInputModel(), + testClient, + ) + + expectedRequest := fixtureRequest() + + diff := cmp.Diff( + request, + expectedRequest, + cmp.AllowUnexported(expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + name string + resp *modelexperiments.DeleteInstanceResponse + wantErr bool + }{ + { + name: "nil response", + resp: nil, + wantErr: true, + }, + { + name: "response", + resp: &modelexperiments.DeleteInstanceResponse{}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult( + params.Printer, + "", + tt.resp, + ); (err != nil) != tt.wantErr { + t.Errorf( + "outputResult() error = %v, wantErr %v", + err, + tt.wantErr, + ) + } + }) + } +} diff --git a/internal/cmd/modelexperiments/instance/get/get.go b/internal/cmd/modelexperiments/instance/get/get.go new file mode 100644 index 000000000..3bd5a0ff6 --- /dev/null +++ b/internal/cmd/modelexperiments/instance/get/get.go @@ -0,0 +1,124 @@ +package get + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("get %s", instanceIdArg), + Short: "Gets an AI Model Experiments instance", + Long: "Gets an AI Model Experiments instance in a STACKIT project.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get an AI Model Experiments instance with ID "xxx"`, + `$ stackit ai-model-experiments instance get xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildGetInstanceRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get AI Model Experiments instance: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + _ = flags.MarkFlagsRequired(cmd, globalflags.RegionFlag) +} + +func parseInput( + p *print.Printer, + cmd *cobra.Command, + inputArgs []string, +) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: inputArgs[0], + Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), + } + + p.DebugInputModel(model) + + return &model, nil +} + +func buildGetInstanceRequest( + ctx context.Context, + model *inputModel, + apiClient *modelexperiments.APIClient, +) modelexperiments.ApiGetInstanceRequest { + return apiClient.DefaultAPI.GetInstance( + ctx, + model.ProjectId, + model.Region, + model.InstanceId, + ) +} + +func outputResult( + p *print.Printer, + outputFormat string, + resp *modelexperiments.GetInstanceResponse, +) error { + if resp == nil { + return fmt.Errorf("response instance is nil") + } + + return p.OutputResult(outputFormat, resp.Instance, func() error { + return nil + }) +} diff --git a/internal/cmd/modelexperiments/instance/get/get_test.go b/internal/cmd/modelexperiments/instance/get/get_test.go new file mode 100644 index 000000000..a15dcc374 --- /dev/null +++ b/internal/cmd/modelexperiments/instance/get/get_test.go @@ -0,0 +1,181 @@ +package get + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + +var testClient = &modelexperiments.APIClient{ + DefaultAPI: modelexperiments.DefaultAPIServiceMock{}, +} + +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues( + mods ...func(*map[string]string), +) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + globalflags.RegionFlag: "eu01", + } + + for _, mod := range mods { + mod(&flagValues) + } + + return flagValues +} + +func fixtureInputModel( + mods ...func(*inputModel), +) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu01", + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + Region: "eu01", + } + + for _, mod := range mods { + mod(model) + } + + return model +} + +func fixtureRequest( + mods ...func(*modelexperiments.ApiGetInstanceRequest), +) modelexperiments.ApiGetInstanceRequest { + request := testClient.DefaultAPI.GetInstance( + testCtx, + testProjectId, + "eu01", + testInstanceId, + ) + + for _, mod := range mods { + mod(&request) + } + + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues *map[string]string) { + delete(*flagValues, projectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput( + t, + NewCmd, + parseInput, + tt.expectedModel, + tt.argValues, + tt.flagValues, + tt.isValid, + ) + }) + } +} + +func TestBuildGetInstanceRequest(t *testing.T) { + request := buildGetInstanceRequest( + testCtx, + fixtureInputModel(), + testClient, + ) + + expectedRequest := fixtureRequest() + + diff := cmp.Diff( + request, + expectedRequest, + cmp.AllowUnexported(expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + name string + resp *modelexperiments.GetInstanceResponse + wantErr bool + }{ + { + name: "nil response", + resp: nil, + wantErr: true, + }, + { + name: "empty instance", + resp: &modelexperiments.GetInstanceResponse{ + Instance: modelexperiments.Instance{}, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult( + params.Printer, + "", + tt.resp, + ); (err != nil) != tt.wantErr { + t.Errorf( + "outputResult() error = %v, wantErr %v", + err, + tt.wantErr, + ) + } + }) + } +} diff --git a/internal/cmd/modelexperiments/instance/instance.go b/internal/cmd/modelexperiments/instance/instance.go new file mode 100644 index 000000000..1cf6073c3 --- /dev/null +++ b/internal/cmd/modelexperiments/instance/instance.go @@ -0,0 +1,35 @@ +package instance + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/get" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/patch" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "instance", + Short: "Provides functionality for AI Model Experiments instances", + Long: "Provides functionality for AI Model Experiments instances.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + + addSubcommands(cmd, params) + + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(get.NewCmd(params)) + cmd.AddCommand(patch.NewCmd(params)) +} diff --git a/internal/cmd/modelexperiments/instance/list/list.go b/internal/cmd/modelexperiments/instance/list/list.go new file mode 100644 index 000000000..0ba444e0e --- /dev/null +++ b/internal/cmd/modelexperiments/instance/list/list.go @@ -0,0 +1,116 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists AI Model Experiments instances", + Long: "Lists AI Model Experiments instances in a STACKIT project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List AI Model Experiments instances in a project`, + `$ stackit ai-model-experiments instance list --region eu01`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildListInstancesRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list AI Model Experiments instances: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + _ = flags.MarkFlagsRequired(cmd, globalflags.RegionFlag) +} + +func parseInput( + p *print.Printer, + cmd *cobra.Command, + _ []string, +) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), + } + + p.DebugInputModel(model) + + return &model, nil +} + +func buildListInstancesRequest( + ctx context.Context, + model *inputModel, + apiClient *modelexperiments.APIClient, +) modelexperiments.ApiListInstancesRequest { + return apiClient.DefaultAPI.ListInstances( + ctx, + model.ProjectId, + model.Region, + ) +} + +func outputResult( + p *print.Printer, + outputFormat string, + resp *modelexperiments.ListInstancesResponse, +) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp.Instances, func() error { + return nil + }) +} diff --git a/internal/cmd/modelexperiments/instance/list/list_test.go b/internal/cmd/modelexperiments/instance/list/list_test.go new file mode 100644 index 000000000..ec288bad0 --- /dev/null +++ b/internal/cmd/modelexperiments/instance/list/list_test.go @@ -0,0 +1,176 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + +var testClient = &modelexperiments.APIClient{ + DefaultAPI: modelexperiments.DefaultAPIServiceMock{}, +} + +var testProjectId = uuid.NewString() + +func fixtureFlagValues( + mods ...func(*map[string]string), +) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + globalflags.RegionFlag: "eu01", + } + + for _, mod := range mods { + mod(&flagValues) + } + + return flagValues +} + +func fixtureInputModel( + mods ...func(*inputModel), +) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu01", + Verbosity: globalflags.VerbosityDefault, + }, + Region: "eu01", + } + + for _, mod := range mods { + mod(model) + } + + return model +} + +func fixtureRequest( + mods ...func(*modelexperiments.ApiListInstancesRequest), +) modelexperiments.ApiListInstancesRequest { + request := testClient.DefaultAPI.ListInstances( + testCtx, + testProjectId, + "eu01", + ) + + for _, mod := range mods { + mod(&request) + } + + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues *map[string]string) { + delete(*flagValues, projectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput( + t, + NewCmd, + parseInput, + tt.expectedModel, + tt.argValues, + tt.flagValues, + tt.isValid, + ) + }) + } +} + +func TestBuildListInstancesRequest(t *testing.T) { + request := buildListInstancesRequest( + testCtx, + fixtureInputModel(), + testClient, + ) + + expectedRequest := fixtureRequest() + + diff := cmp.Diff( + request, + expectedRequest, + cmp.AllowUnexported(expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + name string + resp *modelexperiments.ListInstancesResponse + wantErr bool + }{ + { + name: "nil response", + resp: nil, + wantErr: true, + }, + { + name: "instances", + resp: &modelexperiments.ListInstancesResponse{ + Instances: []modelexperiments.Instance{}, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult( + params.Printer, + "", + tt.resp, + ); (err != nil) != tt.wantErr { + t.Errorf( + "outputResult() error = %v, wantErr %v", + err, + tt.wantErr, + ) + } + }) + } +} diff --git a/internal/cmd/modelexperiments/instance/patch/patch.go b/internal/cmd/modelexperiments/instance/patch/patch.go new file mode 100644 index 000000000..3d7082bfe --- /dev/null +++ b/internal/cmd/modelexperiments/instance/patch/patch.go @@ -0,0 +1,207 @@ +package patch + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + instanceIdArg = "INSTANCE_ID" + + nameFlag = "name" + descriptionFlag = "description" + labelFlag = "label" + retentionFlag = "deleted-experiment-retention" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + InstanceId string + + Name *string + Description *string + Labels *map[string]*string + Retention *string + + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("patch %s", instanceIdArg), + Short: "Updates an AI Model Experiments instance", + Long: "Partially updates an AI Model Experiments instance in a STACKIT project.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update the name of an AI Model Experiments instance with ID "xxx"`, + `$ stackit ai-model-experiments instance patch xxx --name my-new-name`, + ), + examples.NewExample( + `Update the description of an AI Model Experiments instance`, + `$ stackit ai-model-experiments instance patch xxx --description "team tracking server"`, + ), + examples.NewExample( + `Update labels on an AI Model Experiments instance`, + `$ stackit ai-model-experiments instance patch xxx --label env=prod`, + ), + examples.NewExample( + `Update multiple fields of an AI Model Experiments instance`, + `$ stackit ai-model-experiments instance patch xxx --name my-new-name --description "team tracking server" --label env=prod --deleted-experiment-retention 30d`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildPatchInstanceRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update AI Model Experiments instance: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(nameFlag, "n", "", "Instance name") + cmd.Flags().String(descriptionFlag, "", "Instance description") + cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`) + cmd.Flags().String(retentionFlag, "", `Retention period for deleted experiments before permanent purge, e.g. "30d" (min 1d, max 90d)`) +} + +func parseInput( + p *print.Printer, + cmd *cobra.Command, + inputArgs []string, +) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + labels, err := cmd.Flags().GetStringToString(labelFlag) + if err != nil { + return nil, fmt.Errorf("parse %q flag: %w", labelFlag, err) + } + + var labelsPtr *map[string]*string + + if len(labels) > 0 { + convertedLabels := make(map[string]*string, len(labels)) + + for key, value := range labels { + valueCopy := value + convertedLabels[key] = &valueCopy + } + + labelsPtr = &convertedLabels + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + + InstanceId: inputArgs[0], + + Name: flags.FlagToStringPointer(p, cmd, nameFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: labelsPtr, + Retention: flags.FlagToStringPointer(p, cmd, retentionFlag), + + Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), + } + + if model.Name == nil && + model.Description == nil && + model.Labels == nil && + model.Retention == nil { + return nil, fmt.Errorf("at least one update flag must be provided") + } + + p.DebugInputModel(model) + + return &model, nil +} + +func buildPatchInstanceRequest( + ctx context.Context, + model *inputModel, + apiClient *modelexperiments.APIClient, +) modelexperiments.ApiPartialUpdateInstanceRequest { + req := apiClient.DefaultAPI.PartialUpdateInstance( + ctx, + model.ProjectId, + model.Region, + model.InstanceId, + ) + + payload := modelexperiments.PartialUpdateInstancePayload{} + + if model.Name != nil { + payload.Name = model.Name + } + + if model.Description != nil { + payload.Description = model.Description + } + + if model.Labels != nil { + payload.Labels = model.Labels + } + + if model.Retention != nil { + payload.DeletedExperimentRetention = model.Retention + } + + return req.PartialUpdateInstancePayload(payload) +} + +func outputResult( + p *print.Printer, + outputFormat string, + resp *modelexperiments.PartialUpdateInstanceResponse, +) error { + if resp == nil { + return fmt.Errorf("response instance is nil") + } + + return p.OutputResult(outputFormat, resp.Instance, func() error { + p.Outputf( + "Updated AI Model Experiments instance. Instance ID: %s\n", + resp.Instance.Id, + ) + return nil + }) +} diff --git a/internal/cmd/modelexperiments/instance/patch/patch_test.go b/internal/cmd/modelexperiments/instance/patch/patch_test.go new file mode 100644 index 000000000..70407abee --- /dev/null +++ b/internal/cmd/modelexperiments/instance/patch/patch_test.go @@ -0,0 +1,281 @@ +package patch + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + +var testClient = &modelexperiments.APIClient{ + DefaultAPI: modelexperiments.DefaultAPIServiceMock{}, +} + +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues( + mods ...func(*map[string]string), +) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + globalflags.RegionFlag: "eu01", + } + + for _, mod := range mods { + mod(&flagValues) + } + + return flagValues +} + +func fixtureInputModel( + mods ...func(*inputModel), +) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: "eu01", + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + Region: "eu01", + } + + for _, mod := range mods { + mod(model) + } + + return model +} + +func fixtureRequest( + mods ...func(*modelexperiments.ApiPartialUpdateInstanceRequest), +) modelexperiments.ApiPartialUpdateInstanceRequest { + request := testClient.DefaultAPI.PartialUpdateInstance( + testCtx, + testProjectId, + "eu01", + testInstanceId, + ) + + payload := modelexperiments.PartialUpdateInstancePayload{ + Name: utils.Ptr("example"), + } + + request = request.PartialUpdateInstancePayload(payload) + + for _, mod := range mods { + mod(&request) + } + + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "name", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues *map[string]string) { + (*flagValues)[nameFlag] = "example" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Name = utils.Ptr("example") + }), + }, + { + description: "description", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues *map[string]string) { + (*flagValues)[descriptionFlag] = "team tracking server" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("team tracking server") + }), + }, + { + description: "retention", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues *map[string]string) { + (*flagValues)[retentionFlag] = "30d" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Retention = utils.Ptr("30d") + }), + }, + { + description: "no update flags", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues *map[string]string) { + delete(*flagValues, projectIdFlag) + (*flagValues)[nameFlag] = "example" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput( + t, + NewCmd, + parseInput, + tt.expectedModel, + tt.argValues, + tt.flagValues, + tt.isValid, + ) + }) + } +} + +func TestBuildPatchInstanceRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest modelexperiments.ApiPartialUpdateInstanceRequest + }{ + { + description: "name", + model: fixtureInputModel(func(model *inputModel) { + model.Name = utils.Ptr("example") + }), + expectedRequest: fixtureRequest(), + }, + { + description: "description", + model: fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("team tracking server") + }), + expectedRequest: func() modelexperiments.ApiPartialUpdateInstanceRequest { + request := testClient.DefaultAPI.PartialUpdateInstance( + testCtx, + testProjectId, + "eu01", + testInstanceId, + ) + + request = request.PartialUpdateInstancePayload( + modelexperiments.PartialUpdateInstancePayload{ + Description: utils.Ptr("team tracking server"), + }, + ) + + return request + }(), + }, + { + description: "retention", + model: fixtureInputModel(func(model *inputModel) { + model.Retention = utils.Ptr("30d") + }), + expectedRequest: func() modelexperiments.ApiPartialUpdateInstanceRequest { + request := testClient.DefaultAPI.PartialUpdateInstance( + testCtx, + testProjectId, + "eu01", + testInstanceId, + ) + + request = request.PartialUpdateInstancePayload( + modelexperiments.PartialUpdateInstancePayload{ + DeletedExperimentRetention: utils.Ptr("30d"), + }, + ) + + return request + }(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildPatchInstanceRequest( + testCtx, + tt.model, + testClient, + ) + + diff := cmp.Diff( + request, + tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + name string + resp *modelexperiments.PartialUpdateInstanceResponse + wantErr bool + }{ + { + name: "nil response", + resp: nil, + wantErr: true, + }, + { + name: "empty instance", + resp: &modelexperiments.PartialUpdateInstanceResponse{ + Instance: modelexperiments.Instance{}, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult( + params.Printer, + "", + tt.resp, + ); (err != nil) != tt.wantErr { + t.Errorf( + "outputResult() error = %v, wantErr %v", + err, + tt.wantErr, + ) + } + }) + } +} diff --git a/internal/cmd/modelexperiments/modelexperiments.go b/internal/cmd/modelexperiments/modelexperiments.go new file mode 100644 index 000000000..ae6804e09 --- /dev/null +++ b/internal/cmd/modelexperiments/modelexperiments.go @@ -0,0 +1,29 @@ +package modelexperiments + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "ai-model-experiments", + Short: "Provides functionality for AI Model Experiments", + Long: "Provides functionality for AI Model Experiments.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + + addSubcommands(cmd, params) + + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(instance.NewCmd(params)) + cmd.AddCommand(token.NewCmd(params)) +} diff --git a/internal/cmd/modelexperiments/token/create/create.go b/internal/cmd/modelexperiments/token/create/create.go new file mode 100644 index 000000000..ac6e5e1e9 --- /dev/null +++ b/internal/cmd/modelexperiments/token/create/create.go @@ -0,0 +1,106 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + instanceIDFlag = "instance-id" + regionFlag = "region" + nameFlag = "name" + descriptionFlag = "description" + labelFlag = "label" + ttlDurationFlag = "ttl-duration" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceID string + Region string + Name string + Description *string + Labels *map[string]string + TTLDuration *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{Use: "create", Short: "Creates an instance token", Long: "Creates an auth token for an AI Model Experiments instance.", Args: args.NoArgs, Example: examples.Build(examples.NewExample("Create an auth token", "$ stackit ai-model-experiments token create --instance-id xxx --name my-token")), RunE: func(cmd *cobra.Command, _ []string) error { + model, err := parseInput(params.Printer, cmd, nil) + if err != nil { + return err + } + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + resp, err := buildRequest(context.Background(), model, apiClient).Execute() + if err != nil { + return fmt.Errorf("create instance token: %w", err) + } + return outputResult(params.Printer, model.OutputFormat, resp) + }} + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") + cmd.Flags().String(nameFlag, "", "Token name") + cmd.Flags().String(descriptionFlag, "", "Token description") + cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`) + cmd.Flags().String(ttlDurationFlag, "", "Token time to live duration") + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag, nameFlag) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + labels, err := cmd.Flags().GetStringToString(labelFlag) + if err != nil { + return nil, fmt.Errorf("parse %q flag: %w", labelFlag, err) + } + var labelsPtr *map[string]string + if len(labels) > 0 { + labelsPtr = &labels + } + model := &inputModel{GlobalFlagModel: globalFlags, InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag), Name: flags.FlagToStringValue(p, cmd, nameFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr, TTLDuration: flags.FlagToStringPointer(p, cmd, ttlDurationFlag)} + if model.Name == "" { + return nil, fmt.Errorf("%s flag is required", nameFlag) + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiCreateInstanceTokenRequest { + payload := modelexperiments.CreateInstanceTokenPayload{Name: model.Name, Description: model.Description, Labels: model.Labels, TtlDuration: model.TTLDuration} + return apiClient.DefaultAPI.CreateInstanceToken(ctx, model.ProjectId, model.Region, model.InstanceID).CreateInstanceTokenPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.CreateInstanceTokenResponse) error { + if resp == nil || resp.Token.Name == "" { + return fmt.Errorf("response token is nil") + } + return p.OutputResult(outputFormat, resp.Token, func() error { + p.Outputf( + "Created instance token. ID: %s\nToken: %s\n", + resp.Token.Id, + resp.Token.Content, + ) + return nil + }) +} diff --git a/internal/cmd/modelexperiments/token/create/create_test.go b/internal/cmd/modelexperiments/token/create/create_test.go new file mode 100644 index 000000000..cedf3b09e --- /dev/null +++ b/internal/cmd/modelexperiments/token/create/create_test.go @@ -0,0 +1,100 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIDFlag = globalflags.ProjectIdFlag + +type testContextKey struct{} + +var testContext = context.WithValue(context.Background(), testContextKey{}, "token-create") +var testClient = &modelexperiments.APIClient{DefaultAPI: modelexperiments.DefaultAPIServiceMock{}} +var testProjectID = uuid.NewString() +var testInstanceID = uuid.NewString() + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01", nameFlag: "example"} + for _, mod := range mods { + mod(values) + } + return values +} + +func fixtureInputModel(mods ...func(*inputModel)) *inputModel { + model := &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, InstanceID: testInstanceID, Region: "eu01", Name: "example"} + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(*modelexperiments.ApiCreateInstanceTokenRequest)) modelexperiments.ApiCreateInstanceTokenRequest { + request := testClient.DefaultAPI.CreateInstanceToken(testContext, testProjectID, "eu01", testInstanceID).CreateInstanceTokenPayload(modelexperiments.CreateInstanceTokenPayload{Name: "example"}) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + name string + flags map[string]string + valid bool + want *inputModel + }{ + {name: "base", flags: fixtureFlagValues(), valid: true, want: fixtureInputModel()}, + {name: "optional fields", flags: fixtureFlagValues(func(values map[string]string) { + values[descriptionFlag] = "service token" + values[labelFlag] = "env=prod" + values[ttlDurationFlag] = "5h" + }), valid: true, want: fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("service token") + model.Labels = &map[string]string{"env": "prod"} + model.TTLDuration = utils.Ptr("5h") + })}, + {name: "name missing", flags: fixtureFlagValues(func(values map[string]string) { delete(values, nameFlag) }), valid: false}, + {name: "project missing", flags: fixtureFlagValues(func(values map[string]string) { delete(values, projectIDFlag) }), valid: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { testutils.TestParseInput(t, NewCmd, parseInput, tt.want, nil, tt.flags, tt.valid) }) + } +} + +func TestBuildRequest(t *testing.T) { + model := fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("service token") + model.Labels = &map[string]string{"env": "prod"} + model.TTLDuration = utils.Ptr("5h") + }) + want := fixtureRequest(func(request *modelexperiments.ApiCreateInstanceTokenRequest) { + *request = request.CreateInstanceTokenPayload(modelexperiments.CreateInstanceTokenPayload{Name: "example", Description: utils.Ptr("service token"), Labels: &map[string]string{"env": "prod"}, TtlDuration: utils.Ptr("5h")}) + }) + got := buildRequest(testContext, model, testClient) + if diff := cmp.Diff(got, want, cmp.AllowUnexported(want), cmpopts.EquateComparable(testContext)); diff != "" { + t.Fatalf("request mismatch (-got +want):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, "", nil); err == nil { + t.Fatal("expected nil response error") + } + if err := outputResult(params.Printer, "", &modelexperiments.CreateInstanceTokenResponse{}); err == nil { + t.Fatal("expected nil token error") + } +} diff --git a/internal/cmd/modelexperiments/token/delete/delete.go b/internal/cmd/modelexperiments/token/delete/delete.go new file mode 100644 index 000000000..902ff5081 --- /dev/null +++ b/internal/cmd/modelexperiments/token/delete/delete.go @@ -0,0 +1,81 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + tokenIDArg = "TOKEN_ID" + instanceIDFlag = "instance-id" + regionFlag = "region" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + TokenID string + InstanceID string + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", tokenIDArg), + Short: "Deletes an instance token", + Long: "Deletes an auth token from an AI Model Experiments instance.", + Args: args.SingleArg(tokenIDArg, utils.ValidateUUID), + Example: examples.Build(examples.NewExample(`Delete an auth token with ID "xxx"`, `$ stackit ai-model-experiments token delete xxx --instance-id yyy`)), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return err + } + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + if err := params.Printer.PromptForConfirmation(fmt.Sprintf("Are you sure you want to delete instance token %q?", model.TokenID)); err != nil { + return err + } + if _, err := buildRequest(context.Background(), model, apiClient).Execute(); err != nil { + return fmt.Errorf("delete instance token: %w", err) + } + params.Printer.Outputf("Deleted instance token %q\n", model.TokenID) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag)} + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiDeleteInstanceTokenRequest { + return apiClient.DefaultAPI.DeleteInstanceToken(ctx, model.ProjectId, model.Region, model.TokenID, model.InstanceID) +} diff --git a/internal/cmd/modelexperiments/token/delete/delete_test.go b/internal/cmd/modelexperiments/token/delete/delete_test.go new file mode 100644 index 000000000..94c013fe0 --- /dev/null +++ b/internal/cmd/modelexperiments/token/delete/delete_test.go @@ -0,0 +1,63 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIDFlag = globalflags.ProjectIdFlag + +type testContextKey struct{} + +var testContext = context.WithValue(context.Background(), testContextKey{}, "token-delete") +var testClient = &modelexperiments.APIClient{DefaultAPI: modelexperiments.DefaultAPIServiceMock{}} +var testProjectID = uuid.NewString() +var testInstanceID = uuid.NewString() +var testTokenID = uuid.NewString() + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + for _, mod := range mods { + mod(values) + } + return values +} +func fixtureInputModel() *inputModel { + return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID, Region: "eu01"} +} +func fixtureRequest() modelexperiments.ApiDeleteInstanceTokenRequest { + return testClient.DefaultAPI.DeleteInstanceToken(testContext, testProjectID, "eu01", testTokenID, testInstanceID) +} + +func TestParseInput(t *testing.T) { + tests := []struct { + name string + flags map[string]string + valid bool + }{{name: "base", flags: fixtureFlagValues(), valid: true}, {name: "project missing", flags: fixtureFlagValues(func(values map[string]string) { delete(values, projectIDFlag) }), valid: false}} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var want *inputModel + if tt.valid { + want = fixtureInputModel() + } + testutils.TestParseInput(t, NewCmd, parseInput, want, []string{testTokenID}, tt.flags, tt.valid) + }) + } +} +func TestBuildRequest(t *testing.T) { + got := buildRequest(testContext, fixtureInputModel(), testClient) + want := fixtureRequest() + if diff := cmp.Diff(got, want, cmp.AllowUnexported(want), cmpopts.EquateComparable(testContext)); diff != "" { + t.Fatalf("request mismatch (-got +want):\n%s", diff) + } +} diff --git a/internal/cmd/modelexperiments/token/get/get.go b/internal/cmd/modelexperiments/token/get/get.go new file mode 100644 index 000000000..5f8db6d6d --- /dev/null +++ b/internal/cmd/modelexperiments/token/get/get.go @@ -0,0 +1,85 @@ +package get + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + tokenIDArg = "TOKEN_ID" + instanceIDFlag = "instance-id" + regionFlag = "region" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + TokenID string + InstanceID string + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("get %s", tokenIDArg), + Short: "Gets an instance token", + Long: "Gets an auth token for an AI Model Experiments instance.", + Args: args.SingleArg(tokenIDArg, utils.ValidateUUID), + Example: examples.Build(examples.NewExample(`Get an auth token with ID "xxx"`, `$ stackit ai-model-experiments token get xxx --instance-id yyy`)), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return err + } + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + resp, err := buildRequest(context.Background(), model, apiClient).Execute() + if err != nil { + return fmt.Errorf("get instance token: %w", err) + } + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag)} + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiGetInstanceTokenRequest { + return apiClient.DefaultAPI.GetInstanceToken(ctx, model.ProjectId, model.Region, model.TokenID, model.InstanceID) +} + +func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.GetInstanceTokenResponse) error { + if resp == nil || resp.Token.Name == "" { + return fmt.Errorf("response token is nil") + } + return p.OutputResult(outputFormat, resp.Token, func() error { return nil }) +} diff --git a/internal/cmd/modelexperiments/token/get/get_test.go b/internal/cmd/modelexperiments/token/get/get_test.go new file mode 100644 index 000000000..ec88ea99d --- /dev/null +++ b/internal/cmd/modelexperiments/token/get/get_test.go @@ -0,0 +1,73 @@ +package get + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIDFlag = globalflags.ProjectIdFlag + +type testContextKey struct{} + +var testContext = context.WithValue(context.Background(), testContextKey{}, "token-get") +var testClient = &modelexperiments.APIClient{DefaultAPI: modelexperiments.DefaultAPIServiceMock{}} +var testProjectID = uuid.NewString() +var testInstanceID = uuid.NewString() +var testTokenID = uuid.NewString() + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + for _, mod := range mods { + mod(values) + } + return values +} +func fixtureInputModel() *inputModel { + return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID, Region: "eu01"} +} +func fixtureRequest() modelexperiments.ApiGetInstanceTokenRequest { + return testClient.DefaultAPI.GetInstanceToken(testContext, testProjectID, "eu01", testTokenID, testInstanceID) +} + +func TestParseInput(t *testing.T) { + tests := []struct { + name string + flags map[string]string + valid bool + }{{name: "base", flags: fixtureFlagValues(), valid: true}, {name: "project missing", flags: fixtureFlagValues(func(values map[string]string) { delete(values, projectIDFlag) }), valid: false}} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var want *inputModel + if tt.valid { + want = fixtureInputModel() + } + testutils.TestParseInput(t, NewCmd, parseInput, want, []string{testTokenID}, tt.flags, tt.valid) + }) + } +} +func TestBuildRequest(t *testing.T) { + got := buildRequest(testContext, fixtureInputModel(), testClient) + want := fixtureRequest() + if diff := cmp.Diff(got, want, cmp.AllowUnexported(want), cmpopts.EquateComparable(testContext)); diff != "" { + t.Fatalf("request mismatch (-got +want):\n%s", diff) + } +} +func TestOutputResult(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, "", nil); err == nil { + t.Fatal("expected nil response error") + } + if err := outputResult(params.Printer, "", &modelexperiments.GetInstanceTokenResponse{}); err == nil { + t.Fatal("expected nil token error") + } +} diff --git a/internal/cmd/modelexperiments/token/list/list.go b/internal/cmd/modelexperiments/token/list/list.go new file mode 100644 index 000000000..cf3da805f --- /dev/null +++ b/internal/cmd/modelexperiments/token/list/list.go @@ -0,0 +1,75 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + instanceIDFlag = "instance-id" + regionFlag = "region" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceID string + Region string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{Use: "list", Short: "Lists instance tokens", Long: "Lists all auth tokens for an AI Model Experiments instance.", Args: args.NoArgs, Example: examples.Build(examples.NewExample("List all tokens for an instance", "$ stackit ai-model-experiments token list --instance-id xxx")), RunE: func(cmd *cobra.Command, _ []string) error { + model, err := parseInput(params.Printer, cmd, nil) + if err != nil { + return err + } + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + resp, err := buildRequest(context.Background(), model, apiClient).Execute() + if err != nil { + return fmt.Errorf("list instance tokens: %w", err) + } + return outputResult(params.Printer, model.OutputFormat, resp) + }} + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + model := &inputModel{GlobalFlagModel: globalFlags, InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag)} + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiListInstanceTokensRequest { + return apiClient.DefaultAPI.ListInstanceTokens(ctx, model.ProjectId, model.Region, model.InstanceID) +} + +func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.ListInstanceTokensResponse) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + return p.OutputResult(outputFormat, resp.Tokens, func() error { return nil }) +} diff --git a/internal/cmd/modelexperiments/token/list/list_test.go b/internal/cmd/modelexperiments/token/list/list_test.go new file mode 100644 index 000000000..fbbd6352b --- /dev/null +++ b/internal/cmd/modelexperiments/token/list/list_test.go @@ -0,0 +1,74 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIDFlag = globalflags.ProjectIdFlag + +type testContextKey struct{} + +var testContext = context.WithValue(context.Background(), testContextKey{}, "token-list") +var testClient = &modelexperiments.APIClient{DefaultAPI: modelexperiments.DefaultAPIServiceMock{}} +var testProjectID = uuid.NewString() +var testInstanceID = uuid.NewString() + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + for _, mod := range mods { + mod(values) + } + return values +} +func fixtureInputModel() *inputModel { + return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, InstanceID: testInstanceID, Region: "eu01"} +} +func fixtureRequest() modelexperiments.ApiListInstanceTokensRequest { + return testClient.DefaultAPI.ListInstanceTokens(testContext, testProjectID, "eu01", testInstanceID) +} + +func TestParseInput(t *testing.T) { + tests := []struct { + name string + flags map[string]string + valid bool + }{{name: "base", flags: fixtureFlagValues(), valid: true}, {name: "project missing", flags: fixtureFlagValues(func(values map[string]string) { delete(values, projectIDFlag) }), valid: false}} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var want *inputModel + if tt.valid { + want = fixtureInputModel() + } + testutils.TestParseInput(t, NewCmd, parseInput, want, nil, tt.flags, tt.valid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + got := buildRequest(testContext, fixtureInputModel(), testClient) + want := fixtureRequest() + if diff := cmp.Diff(got, want, cmp.AllowUnexported(want), cmpopts.EquateComparable(testContext)); diff != "" { + t.Fatalf("request mismatch (-got +want):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, "", nil); err == nil { + t.Fatal("expected nil response error") + } + if err := outputResult(params.Printer, "", &modelexperiments.ListInstanceTokensResponse{}); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/internal/cmd/modelexperiments/token/patch/patch.go b/internal/cmd/modelexperiments/token/patch/patch.go new file mode 100644 index 000000000..4e3c91e00 --- /dev/null +++ b/internal/cmd/modelexperiments/token/patch/patch.go @@ -0,0 +1,107 @@ +package patch + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +const ( + tokenIDArg = "TOKEN_ID" + instanceIDFlag = "instance-id" + regionFlag = "region" + nameFlag = "name" + descriptionFlag = "description" + labelFlag = "label" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + TokenID string + InstanceID string + Region string + Name *string + Description *string + Labels *map[string]string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("patch %s", tokenIDArg), Short: "Updates an instance token", Long: "Partially updates an auth token for an AI Model Experiments instance.", Args: args.SingleArg(tokenIDArg, utils.ValidateUUID), + Example: examples.Build(examples.NewExample(`Update an auth token with ID "xxx"`, `$ stackit ai-model-experiments token patch xxx --instance-id yyy --name updated-token`)), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return err + } + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + resp, err := buildRequest(context.Background(), model, apiClient).Execute() + if err != nil { + return fmt.Errorf("update instance token: %w", err) + } + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") + cmd.Flags().String(nameFlag, "", "Token name") + cmd.Flags().String(descriptionFlag, "", "Token description") + cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`) + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + labels, err := cmd.Flags().GetStringToString(labelFlag) + if err != nil { + return nil, fmt.Errorf("parse %q flag: %w", labelFlag, err) + } + var labelsPtr *map[string]string + if len(labels) > 0 { + labelsPtr = &labels + } + model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag), Name: flags.FlagToStringPointer(p, cmd, nameFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr} + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiPartialUpdateInstanceTokenRequest { + labels := map[string]*string(nil) + if model.Labels != nil { + labels = make(map[string]*string, len(*model.Labels)) + for key, value := range *model.Labels { + labels[key] = &value + } + } + payload := modelexperiments.PartialUpdateInstanceTokenPayload{Name: model.Name, Description: model.Description, Labels: &labels} + return apiClient.DefaultAPI.PartialUpdateInstanceToken(ctx, model.ProjectId, model.Region, model.TokenID, model.InstanceID).PartialUpdateInstanceTokenPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.PartialUpdateInstanceTokenResponse) error { + if resp == nil || resp.Token.Name == "" { + return fmt.Errorf("response token is nil") + } + return p.OutputResult(outputFormat, resp.Token, func() error { return nil }) +} diff --git a/internal/cmd/modelexperiments/token/patch/patch_test.go b/internal/cmd/modelexperiments/token/patch/patch_test.go new file mode 100644 index 000000000..638932358 --- /dev/null +++ b/internal/cmd/modelexperiments/token/patch/patch_test.go @@ -0,0 +1,95 @@ +package patch + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIDFlag = globalflags.ProjectIdFlag + +type testContextKey struct{} + +var testContext = context.WithValue(context.Background(), testContextKey{}, "token-patch") +var testClient = &modelexperiments.APIClient{DefaultAPI: modelexperiments.DefaultAPIServiceMock{}} +var testProjectID = uuid.NewString() +var testInstanceID = uuid.NewString() +var testTokenID = uuid.NewString() + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + for _, mod := range mods { + mod(values) + } + return values +} +func fixtureInputModel(mods ...func(*inputModel)) *inputModel { + model := &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID, Region: "eu01"} + for _, mod := range mods { + mod(model) + } + return model +} +func fixtureRequest(payload modelexperiments.PartialUpdateInstanceTokenPayload) modelexperiments.ApiPartialUpdateInstanceTokenRequest { + return testClient.DefaultAPI.PartialUpdateInstanceToken(testContext, testProjectID, "eu01", testTokenID, testInstanceID).PartialUpdateInstanceTokenPayload(payload) +} + +func TestParseInput(t *testing.T) { + tests := []struct { + name string + flags map[string]string + valid bool + want *inputModel + }{ + {name: "base", flags: fixtureFlagValues(), valid: true, want: fixtureInputModel()}, + {name: "optional fields", flags: fixtureFlagValues(func(values map[string]string) { + values[nameFlag] = "updated" + values[descriptionFlag] = "updated token" + values[labelFlag] = "env=prod" + }), valid: true, want: fixtureInputModel(func(model *inputModel) { + model.Name = utils.Ptr("updated") + model.Description = utils.Ptr("updated token") + model.Labels = &map[string]string{"env": "prod"} + })}, + {name: "project missing", flags: fixtureFlagValues(func(values map[string]string) { delete(values, projectIDFlag) }), valid: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.want, []string{testTokenID}, tt.flags, tt.valid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + model := fixtureInputModel(func(model *inputModel) { + model.Name = utils.Ptr("updated") + model.Description = utils.Ptr("updated token") + model.Labels = &map[string]string{"env": "prod"} + }) + got := buildRequest(testContext, model, testClient) + labels := map[string]*string{"env": utils.Ptr("prod")} + want := fixtureRequest(modelexperiments.PartialUpdateInstanceTokenPayload{Name: utils.Ptr("updated"), Description: utils.Ptr("updated token"), Labels: &labels}) + if diff := cmp.Diff(got, want, cmp.AllowUnexported(want), cmpopts.EquateComparable(testContext)); diff != "" { + t.Fatalf("request mismatch (-got +want):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, "", nil); err == nil { + t.Fatal("expected nil response error") + } + if err := outputResult(params.Printer, "", &modelexperiments.PartialUpdateInstanceTokenResponse{}); err == nil { + t.Fatal("expected nil token error") + } +} diff --git a/internal/cmd/modelexperiments/token/token.go b/internal/cmd/modelexperiments/token/token.go new file mode 100644 index 000000000..67d8bced1 --- /dev/null +++ b/internal/cmd/modelexperiments/token/token.go @@ -0,0 +1,19 @@ +package token + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/get" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/patch" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{Use: "token", Short: "Provides functionality for AI Model Experiments instance tokens", Long: "Provides functionality for AI Model Experiments instance tokens.", Args: args.NoArgs, Run: utils.CmdHelp} + cmd.AddCommand(list.NewCmd(params), create.NewCmd(params), delete.NewCmd(params), get.NewCmd(params), patch.NewCmd(params)) + return cmd +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index bbb05aca3..1a432477c 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -22,6 +22,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/logme" "github.com/stackitcloud/stackit-cli/internal/cmd/logs" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex" "github.com/stackitcloud/stackit-cli/internal/cmd/network" networkArea "github.com/stackitcloud/stackit-cli/internal/cmd/network-area" @@ -180,6 +181,7 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(logme.NewCmd(params)) cmd.AddCommand(logs.NewCmd(params)) cmd.AddCommand(mariadb.NewCmd(params)) + cmd.AddCommand(modelexperiments.NewCmd(params)) cmd.AddCommand(mongodbflex.NewCmd(params)) cmd.AddCommand(objectstorage.NewCmd(params)) cmd.AddCommand(observability.NewCmd(params)) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 2365ed8b4..0c2663c47 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -23,20 +23,21 @@ const ( IdentityProviderCustomClientIdKey = "identity_provider_custom_client_id" AllowedUrlDomainKey = "allowed_url_domain" - AuthorizationCustomEndpointKey = "authorization_custom_endpoint" - AlbCustomEndpoint = "alb_custom _endpoint" - AlbWafCustomEndpointKey = "alb_waf_custom_endpoint" - DNSCustomEndpointKey = "dns_custom_endpoint" - EdgeCustomEndpointKey = "edge_custom_endpoint" - LoadBalancerCustomEndpointKey = "load_balancer_custom_endpoint" - LogMeCustomEndpointKey = "logme_custom_endpoint" - MariaDBCustomEndpointKey = "mariadb_custom_endpoint" - MongoDBFlexCustomEndpointKey = "mongodbflex_custom_endpoint" - ObjectStorageCustomEndpointKey = "object_storage_custom_endpoint" - ObservabilityCustomEndpointKey = "observability_custom_endpoint" - OpenSearchCustomEndpointKey = "opensearch_custom_endpoint" - PostgresFlexCustomEndpointKey = "postgresflex_custom_endpoint" - RabbitMQCustomEndpointKey = "rabbitmq_custom_endpoint" + AuthorizationCustomEndpointKey = "authorization_custom_endpoint" + AlbCustomEndpoint = "alb_custom _endpoint" + AlbWafCustomEndpointKey = "alb_waf_custom_endpoint" + DNSCustomEndpointKey = "dns_custom_endpoint" + EdgeCustomEndpointKey = "edge_custom_endpoint" + LoadBalancerCustomEndpointKey = "load_balancer_custom_endpoint" + LogMeCustomEndpointKey = "logme_custom_endpoint" + MariaDBCustomEndpointKey = "mariadb_custom_endpoint" + MongoDBFlexCustomEndpointKey = "mongodbflex_custom_endpoint" + ObjectStorageCustomEndpointKey = "object_storage_custom_endpoint" + ObservabilityCustomEndpointKey = "observability_custom_endpoint" + OpenSearchCustomEndpointKey = "opensearch_custom_endpoint" + PostgresFlexCustomEndpointKey = "postgresflex_custom_endpoint" + RabbitMQCustomEndpointKey = "rabbitmq_custom_endpoint" + AIModelExperimentsCustomEndpointKey = "aimodelexperiments_custom_endpoint" // Deprecated: Will be removed after 2027-08-31. RedisCustomEndpointKey = "redis_custom_endpoint" ResourceManagerEndpointKey = "resource_manager_custom_endpoint" @@ -114,6 +115,7 @@ var ConfigKeys = []string{ OpenSearchCustomEndpointKey, PostgresFlexCustomEndpointKey, RabbitMQCustomEndpointKey, + AIModelExperimentsCustomEndpointKey, RedisCustomEndpointKey, ResourceManagerEndpointKey, RunCommandCustomEndpointKey, diff --git a/internal/pkg/services/modelexperiments/client/client.go b/internal/pkg/services/modelexperiments/client/client.go new file mode 100644 index 000000000..9ab23f22d --- /dev/null +++ b/internal/pkg/services/modelexperiments/client/client.go @@ -0,0 +1,14 @@ +package client + +import ( + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/spf13/viper" + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*modelexperiments.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), true, genericclient.CreateApiClient[*modelexperiments.APIClient](modelexperiments.NewAPIClient)) +} diff --git a/internal/pkg/services/modelexperiments/utils/utils.go b/internal/pkg/services/modelexperiments/utils/utils.go new file mode 100644 index 000000000..ac0abfb71 --- /dev/null +++ b/internal/pkg/services/modelexperiments/utils/utils.go @@ -0,0 +1,36 @@ +package utils + +import ( + "context" + "errors" + "fmt" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" +) + +var ErrResponseNil = errors.New("response is nil") + +func GetInstanceName(ctx context.Context, apiClient modelexperiments.DefaultAPI, projectID, regionID, instanceID string) (string, error) { + resp, err := apiClient.GetInstance(ctx, projectID, regionID, instanceID).Execute() + if err != nil { + return "", fmt.Errorf("get AI Model Experiments instance: %w", err) + } + if resp == nil { + return "", ErrResponseNil + } + if resp.Instance.Name == "" { + return "", ErrResponseNil + } + return resp.Instance.Name, nil +} + +func GetTokenName(ctx context.Context, apiClient modelexperiments.DefaultAPI, projectID, regionID, instanceID, tokenID string) (string, error) { + resp, err := apiClient.GetInstanceToken(ctx, projectID, regionID, tokenID, instanceID).Execute() + if err != nil { + return "", fmt.Errorf("get AI Model Experiments token: %w", err) + } + if resp == nil || resp.Token.Name == "" { + return "", ErrResponseNil + } + return resp.Token.Name, nil +} diff --git a/internal/pkg/services/modelexperiments/utils/utils_test.go b/internal/pkg/services/modelexperiments/utils/utils_test.go new file mode 100644 index 000000000..3d364de8f --- /dev/null +++ b/internal/pkg/services/modelexperiments/utils/utils_test.go @@ -0,0 +1,96 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + "github.com/google/uuid" + + modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" + + cliutils "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var ( + testProjectID = uuid.NewString() + testRegion = "eu01" + testInstanceID = uuid.NewString() + testTokenID = uuid.NewString() +) + +type mockSettings struct { + instanceFails bool + instanceResp *modelexperiments.GetInstanceResponse + tokenFails bool + tokenResp *modelexperiments.GetInstanceTokenResponse +} + +func newAPIMock(settings mockSettings) modelexperiments.DefaultAPI { + return &modelexperiments.DefaultAPIServiceMock{ + GetInstanceExecuteMock: cliutils.Ptr(func(modelexperiments.ApiGetInstanceRequest) (*modelexperiments.GetInstanceResponse, error) { + if settings.instanceFails { + return nil, fmt.Errorf("could not get instance") + } + return settings.instanceResp, nil + }), + GetInstanceTokenExecuteMock: cliutils.Ptr(func(modelexperiments.ApiGetInstanceTokenRequest) (*modelexperiments.GetInstanceTokenResponse, error) { + if settings.tokenFails { + return nil, fmt.Errorf("could not get token") + } + return settings.tokenResp, nil + }), + } +} + +func TestGetInstanceName(t *testing.T) { + name := "instance" + tests := []struct { + name string + settings mockSettings + want string + wantErr bool + }{ + {name: "success", settings: mockSettings{instanceResp: &modelexperiments.GetInstanceResponse{Instance: modelexperiments.Instance{Name: name}}}, want: name}, + {name: "request error", settings: mockSettings{instanceFails: true}, wantErr: true}, + {name: "nil response", settings: mockSettings{}, wantErr: true}, + {name: "nil instance", settings: mockSettings{instanceResp: &modelexperiments.GetInstanceResponse{}}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetInstanceName(context.Background(), newAPIMock(tt.settings), testProjectID, testRegion, testInstanceID) + if (err != nil) != tt.wantErr { + t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && got != tt.want { + t.Fatalf("name = %q, want %q", got, tt.want) + } + }) + } +} + +func TestGetTokenName(t *testing.T) { + name := "token" + tests := []struct { + name string + settings mockSettings + want string + wantErr bool + }{ + {name: "success", settings: mockSettings{tokenResp: &modelexperiments.GetInstanceTokenResponse{Token: modelexperiments.TokenMetadata{Name: name}}}, want: name}, + {name: "request error", settings: mockSettings{tokenFails: true}, wantErr: true}, + {name: "nil response", settings: mockSettings{}, wantErr: true}, + {name: "zero token", settings: mockSettings{tokenResp: &modelexperiments.GetInstanceTokenResponse{}}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetTokenName(context.Background(), newAPIMock(tt.settings), testProjectID, testRegion, testInstanceID, testTokenID) + if (err != nil) != tt.wantErr { + t.Fatalf("error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && got != tt.want { + t.Fatalf("name = %q, want %q", got, tt.want) + } + }) + } +} From 2ca23eea9f4e3ef3f72a678aa8094a41fb66c2c0 Mon Sep 17 00:00:00 2001 From: Syeda Haya Fatima Date: Tue, 8 Sep 2026 10:02:23 +0200 Subject: [PATCH 2/5] Generated docs, fixed formatting and lint passing --- docs/stackit.md | 1 + docs/stackit_ai-model-experiments.md | 35 ++++++++++++ docs/stackit_ai-model-experiments_instance.md | 38 +++++++++++++ ...it_ai-model-experiments_instance_create.md | 47 ++++++++++++++++ ...it_ai-model-experiments_instance_delete.md | 40 ++++++++++++++ ...ackit_ai-model-experiments_instance_get.md | 40 ++++++++++++++ ...ckit_ai-model-experiments_instance_list.md | 40 ++++++++++++++ ...kit_ai-model-experiments_instance_patch.md | 53 +++++++++++++++++++ docs/stackit_ai-model-experiments_token.md | 38 +++++++++++++ ...ackit_ai-model-experiments_token_create.md | 45 ++++++++++++++++ ...ackit_ai-model-experiments_token_delete.md | 41 ++++++++++++++ .../stackit_ai-model-experiments_token_get.md | 41 ++++++++++++++ ...stackit_ai-model-experiments_token_list.md | 41 ++++++++++++++ ...tackit_ai-model-experiments_token_patch.md | 44 +++++++++++++++ .../cmd/modelexperiments/instance/instance.go | 1 + .../cmd/modelexperiments/modelexperiments.go | 1 + .../modelexperiments/token/create/create.go | 1 + .../modelexperiments/token/delete/delete.go | 1 + .../cmd/modelexperiments/token/get/get.go | 1 + .../cmd/modelexperiments/token/list/list.go | 1 + .../cmd/modelexperiments/token/patch/patch.go | 1 + internal/cmd/modelexperiments/token/token.go | 1 + 22 files changed, 552 insertions(+) create mode 100644 docs/stackit_ai-model-experiments.md create mode 100644 docs/stackit_ai-model-experiments_instance.md create mode 100644 docs/stackit_ai-model-experiments_instance_create.md create mode 100644 docs/stackit_ai-model-experiments_instance_delete.md create mode 100644 docs/stackit_ai-model-experiments_instance_get.md create mode 100644 docs/stackit_ai-model-experiments_instance_list.md create mode 100644 docs/stackit_ai-model-experiments_instance_patch.md create mode 100644 docs/stackit_ai-model-experiments_token.md create mode 100644 docs/stackit_ai-model-experiments_token_create.md create mode 100644 docs/stackit_ai-model-experiments_token_delete.md create mode 100644 docs/stackit_ai-model-experiments_token_get.md create mode 100644 docs/stackit_ai-model-experiments_token_list.md create mode 100644 docs/stackit_ai-model-experiments_token_patch.md diff --git a/docs/stackit.md b/docs/stackit.md index 5f6b1107a..bcd1f9436 100644 --- a/docs/stackit.md +++ b/docs/stackit.md @@ -27,6 +27,7 @@ stackit [flags] ### SEE ALSO * [stackit affinity-group](./stackit_affinity-group.md) - Manage server affinity groups +* [stackit ai-model-experiments](./stackit_ai-model-experiments.md) - Provides functionality for AI Model Experiments * [stackit auth](./stackit_auth.md) - Authenticates the STACKIT CLI * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands * [stackit config](./stackit_config.md) - Provides functionality for CLI configuration options diff --git a/docs/stackit_ai-model-experiments.md b/docs/stackit_ai-model-experiments.md new file mode 100644 index 000000000..95bc8397f --- /dev/null +++ b/docs/stackit_ai-model-experiments.md @@ -0,0 +1,35 @@ +## stackit ai-model-experiments + +Provides functionality for AI Model Experiments + +### Synopsis + +Provides functionality for AI Model Experiments. + +``` +stackit ai-model-experiments [flags] +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit ai-model-experiments instance](./stackit_ai-model-experiments_instance.md) - Provides functionality for AI Model Experiments instances +* [stackit ai-model-experiments token](./stackit_ai-model-experiments_token.md) - Provides functionality for AI Model Experiments instance tokens + diff --git a/docs/stackit_ai-model-experiments_instance.md b/docs/stackit_ai-model-experiments_instance.md new file mode 100644 index 000000000..413a32ae5 --- /dev/null +++ b/docs/stackit_ai-model-experiments_instance.md @@ -0,0 +1,38 @@ +## stackit ai-model-experiments instance + +Provides functionality for AI Model Experiments instances + +### Synopsis + +Provides functionality for AI Model Experiments instances. + +``` +stackit ai-model-experiments instance [flags] +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments instance" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments](./stackit_ai-model-experiments.md) - Provides functionality for AI Model Experiments +* [stackit ai-model-experiments instance create](./stackit_ai-model-experiments_instance_create.md) - Creates an AI Model Experiments instance +* [stackit ai-model-experiments instance delete](./stackit_ai-model-experiments_instance_delete.md) - Deletes an AI Model Experiments instance +* [stackit ai-model-experiments instance get](./stackit_ai-model-experiments_instance_get.md) - Gets an AI Model Experiments instance +* [stackit ai-model-experiments instance list](./stackit_ai-model-experiments_instance_list.md) - Lists AI Model Experiments instances +* [stackit ai-model-experiments instance patch](./stackit_ai-model-experiments_instance_patch.md) - Updates an AI Model Experiments instance + diff --git a/docs/stackit_ai-model-experiments_instance_create.md b/docs/stackit_ai-model-experiments_instance_create.md new file mode 100644 index 000000000..5e32f4e22 --- /dev/null +++ b/docs/stackit_ai-model-experiments_instance_create.md @@ -0,0 +1,47 @@ +## stackit ai-model-experiments instance create + +Creates an AI Model Experiments instance + +### Synopsis + +Creates an AI Model Experiments (MLflow) instance in your STACKIT project. + +``` +stackit ai-model-experiments instance create [flags] +``` + +### Examples + +``` + Create an AI Model Experiments instance with name "my-tracking" + $ stackit ai-model-experiments instance create --name my-tracking + + Create an instance with a description and labels + $ stackit ai-model-experiments instance create --name my-tracking --description "team tracking server" --label env=prod +``` + +### Options + +``` + --deleted-experiment-retention string Retention period for deleted experiments before permanent purge, e.g. "30d" (min 1d, max 90d) + --description string Instance description + -h, --help Help for "stackit ai-model-experiments instance create" + --label stringToString Labels as key-value pairs, e.g. "--label env=prod" (default []) + -n, --name string Instance name +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments instance](./stackit_ai-model-experiments_instance.md) - Provides functionality for AI Model Experiments instances + diff --git a/docs/stackit_ai-model-experiments_instance_delete.md b/docs/stackit_ai-model-experiments_instance_delete.md new file mode 100644 index 000000000..bda6dab16 --- /dev/null +++ b/docs/stackit_ai-model-experiments_instance_delete.md @@ -0,0 +1,40 @@ +## stackit ai-model-experiments instance delete + +Deletes an AI Model Experiments instance + +### Synopsis + +Deletes an AI Model Experiments instance from a STACKIT project. + +``` +stackit ai-model-experiments instance delete INSTANCE_ID [flags] +``` + +### Examples + +``` + Delete an AI Model Experiments instance with ID "xxx" + $ stackit ai-model-experiments instance delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments instance delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments instance](./stackit_ai-model-experiments_instance.md) - Provides functionality for AI Model Experiments instances + diff --git a/docs/stackit_ai-model-experiments_instance_get.md b/docs/stackit_ai-model-experiments_instance_get.md new file mode 100644 index 000000000..5ec18194d --- /dev/null +++ b/docs/stackit_ai-model-experiments_instance_get.md @@ -0,0 +1,40 @@ +## stackit ai-model-experiments instance get + +Gets an AI Model Experiments instance + +### Synopsis + +Gets an AI Model Experiments instance in a STACKIT project. + +``` +stackit ai-model-experiments instance get INSTANCE_ID [flags] +``` + +### Examples + +``` + Get an AI Model Experiments instance with ID "xxx" + $ stackit ai-model-experiments instance get xxx +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments instance get" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments instance](./stackit_ai-model-experiments_instance.md) - Provides functionality for AI Model Experiments instances + diff --git a/docs/stackit_ai-model-experiments_instance_list.md b/docs/stackit_ai-model-experiments_instance_list.md new file mode 100644 index 000000000..406482f54 --- /dev/null +++ b/docs/stackit_ai-model-experiments_instance_list.md @@ -0,0 +1,40 @@ +## stackit ai-model-experiments instance list + +Lists AI Model Experiments instances + +### Synopsis + +Lists AI Model Experiments instances in a STACKIT project. + +``` +stackit ai-model-experiments instance list [flags] +``` + +### Examples + +``` + List AI Model Experiments instances in a project + $ stackit ai-model-experiments instance list --region eu01 +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments instance list" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments instance](./stackit_ai-model-experiments_instance.md) - Provides functionality for AI Model Experiments instances + diff --git a/docs/stackit_ai-model-experiments_instance_patch.md b/docs/stackit_ai-model-experiments_instance_patch.md new file mode 100644 index 000000000..8ab0b25ea --- /dev/null +++ b/docs/stackit_ai-model-experiments_instance_patch.md @@ -0,0 +1,53 @@ +## stackit ai-model-experiments instance patch + +Updates an AI Model Experiments instance + +### Synopsis + +Partially updates an AI Model Experiments instance in a STACKIT project. + +``` +stackit ai-model-experiments instance patch INSTANCE_ID [flags] +``` + +### Examples + +``` + Update the name of an AI Model Experiments instance with ID "xxx" + $ stackit ai-model-experiments instance patch xxx --name my-new-name + + Update the description of an AI Model Experiments instance + $ stackit ai-model-experiments instance patch xxx --description "team tracking server" + + Update labels on an AI Model Experiments instance + $ stackit ai-model-experiments instance patch xxx --label env=prod + + Update multiple fields of an AI Model Experiments instance + $ stackit ai-model-experiments instance patch xxx --name my-new-name --description "team tracking server" --label env=prod --deleted-experiment-retention 30d +``` + +### Options + +``` + --deleted-experiment-retention string Retention period for deleted experiments before permanent purge, e.g. "30d" (min 1d, max 90d) + --description string Instance description + -h, --help Help for "stackit ai-model-experiments instance patch" + --label stringToString Labels as key-value pairs, e.g. "--label env=prod" (default []) + -n, --name string Instance name +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments instance](./stackit_ai-model-experiments_instance.md) - Provides functionality for AI Model Experiments instances + diff --git a/docs/stackit_ai-model-experiments_token.md b/docs/stackit_ai-model-experiments_token.md new file mode 100644 index 000000000..d79f5eea2 --- /dev/null +++ b/docs/stackit_ai-model-experiments_token.md @@ -0,0 +1,38 @@ +## stackit ai-model-experiments token + +Provides functionality for AI Model Experiments instance tokens + +### Synopsis + +Provides functionality for AI Model Experiments instance tokens. + +``` +stackit ai-model-experiments token [flags] +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments token" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments](./stackit_ai-model-experiments.md) - Provides functionality for AI Model Experiments +* [stackit ai-model-experiments token create](./stackit_ai-model-experiments_token_create.md) - Creates an instance token +* [stackit ai-model-experiments token delete](./stackit_ai-model-experiments_token_delete.md) - Deletes an instance token +* [stackit ai-model-experiments token get](./stackit_ai-model-experiments_token_get.md) - Gets an instance token +* [stackit ai-model-experiments token list](./stackit_ai-model-experiments_token_list.md) - Lists instance tokens +* [stackit ai-model-experiments token patch](./stackit_ai-model-experiments_token_patch.md) - Updates an instance token + diff --git a/docs/stackit_ai-model-experiments_token_create.md b/docs/stackit_ai-model-experiments_token_create.md new file mode 100644 index 000000000..a4608e7f9 --- /dev/null +++ b/docs/stackit_ai-model-experiments_token_create.md @@ -0,0 +1,45 @@ +## stackit ai-model-experiments token create + +Creates an instance token + +### Synopsis + +Creates an auth token for an AI Model Experiments instance. + +``` +stackit ai-model-experiments token create [flags] +``` + +### Examples + +``` + Create an auth token + $ stackit ai-model-experiments token create --instance-id xxx --name my-token +``` + +### Options + +``` + --description string Token description + -h, --help Help for "stackit ai-model-experiments token create" + --instance-id string ID of the instance + --label stringToString Labels as key-value pairs, e.g. "--label env=prod" (default []) + --name string Token name + --ttl-duration string Token time to live duration +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments token](./stackit_ai-model-experiments_token.md) - Provides functionality for AI Model Experiments instance tokens + diff --git a/docs/stackit_ai-model-experiments_token_delete.md b/docs/stackit_ai-model-experiments_token_delete.md new file mode 100644 index 000000000..862e16c35 --- /dev/null +++ b/docs/stackit_ai-model-experiments_token_delete.md @@ -0,0 +1,41 @@ +## stackit ai-model-experiments token delete + +Deletes an instance token + +### Synopsis + +Deletes an auth token from an AI Model Experiments instance. + +``` +stackit ai-model-experiments token delete TOKEN_ID [flags] +``` + +### Examples + +``` + Delete an auth token with ID "xxx" + $ stackit ai-model-experiments token delete xxx --instance-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments token delete" + --instance-id string ID of the instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments token](./stackit_ai-model-experiments_token.md) - Provides functionality for AI Model Experiments instance tokens + diff --git a/docs/stackit_ai-model-experiments_token_get.md b/docs/stackit_ai-model-experiments_token_get.md new file mode 100644 index 000000000..aa28ce08b --- /dev/null +++ b/docs/stackit_ai-model-experiments_token_get.md @@ -0,0 +1,41 @@ +## stackit ai-model-experiments token get + +Gets an instance token + +### Synopsis + +Gets an auth token for an AI Model Experiments instance. + +``` +stackit ai-model-experiments token get TOKEN_ID [flags] +``` + +### Examples + +``` + Get an auth token with ID "xxx" + $ stackit ai-model-experiments token get xxx --instance-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments token get" + --instance-id string ID of the instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments token](./stackit_ai-model-experiments_token.md) - Provides functionality for AI Model Experiments instance tokens + diff --git a/docs/stackit_ai-model-experiments_token_list.md b/docs/stackit_ai-model-experiments_token_list.md new file mode 100644 index 000000000..f51bd24a7 --- /dev/null +++ b/docs/stackit_ai-model-experiments_token_list.md @@ -0,0 +1,41 @@ +## stackit ai-model-experiments token list + +Lists instance tokens + +### Synopsis + +Lists all auth tokens for an AI Model Experiments instance. + +``` +stackit ai-model-experiments token list [flags] +``` + +### Examples + +``` + List all tokens for an instance + $ stackit ai-model-experiments token list --instance-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit ai-model-experiments token list" + --instance-id string ID of the instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments token](./stackit_ai-model-experiments_token.md) - Provides functionality for AI Model Experiments instance tokens + diff --git a/docs/stackit_ai-model-experiments_token_patch.md b/docs/stackit_ai-model-experiments_token_patch.md new file mode 100644 index 000000000..390c9414c --- /dev/null +++ b/docs/stackit_ai-model-experiments_token_patch.md @@ -0,0 +1,44 @@ +## stackit ai-model-experiments token patch + +Updates an instance token + +### Synopsis + +Partially updates an auth token for an AI Model Experiments instance. + +``` +stackit ai-model-experiments token patch TOKEN_ID [flags] +``` + +### Examples + +``` + Update an auth token with ID "xxx" + $ stackit ai-model-experiments token patch xxx --instance-id yyy --name updated-token +``` + +### Options + +``` + --description string Token description + -h, --help Help for "stackit ai-model-experiments token patch" + --instance-id string ID of the instance + --label stringToString Labels as key-value pairs, e.g. "--label env=prod" (default []) + --name string Token name +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ai-model-experiments token](./stackit_ai-model-experiments_token.md) - Provides functionality for AI Model Experiments instance tokens + diff --git a/internal/cmd/modelexperiments/instance/instance.go b/internal/cmd/modelexperiments/instance/instance.go index 1cf6073c3..de3a56c00 100644 --- a/internal/cmd/modelexperiments/instance/instance.go +++ b/internal/cmd/modelexperiments/instance/instance.go @@ -2,6 +2,7 @@ package instance import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/create" "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance/get" diff --git a/internal/cmd/modelexperiments/modelexperiments.go b/internal/cmd/modelexperiments/modelexperiments.go index ae6804e09..ff4b96518 100644 --- a/internal/cmd/modelexperiments/modelexperiments.go +++ b/internal/cmd/modelexperiments/modelexperiments.go @@ -2,6 +2,7 @@ package modelexperiments import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token" "github.com/stackitcloud/stackit-cli/internal/pkg/args" diff --git a/internal/cmd/modelexperiments/token/create/create.go b/internal/cmd/modelexperiments/token/create/create.go index ac6e5e1e9..05e51794a 100644 --- a/internal/cmd/modelexperiments/token/create/create.go +++ b/internal/cmd/modelexperiments/token/create/create.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" diff --git a/internal/cmd/modelexperiments/token/delete/delete.go b/internal/cmd/modelexperiments/token/delete/delete.go index 902ff5081..0c3c2245f 100644 --- a/internal/cmd/modelexperiments/token/delete/delete.go +++ b/internal/cmd/modelexperiments/token/delete/delete.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" diff --git a/internal/cmd/modelexperiments/token/get/get.go b/internal/cmd/modelexperiments/token/get/get.go index 5f8db6d6d..56e5155f9 100644 --- a/internal/cmd/modelexperiments/token/get/get.go +++ b/internal/cmd/modelexperiments/token/get/get.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" diff --git a/internal/cmd/modelexperiments/token/list/list.go b/internal/cmd/modelexperiments/token/list/list.go index cf3da805f..84ad1a506 100644 --- a/internal/cmd/modelexperiments/token/list/list.go +++ b/internal/cmd/modelexperiments/token/list/list.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" diff --git a/internal/cmd/modelexperiments/token/patch/patch.go b/internal/cmd/modelexperiments/token/patch/patch.go index 4e3c91e00..38d63153e 100644 --- a/internal/cmd/modelexperiments/token/patch/patch.go +++ b/internal/cmd/modelexperiments/token/patch/patch.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" diff --git a/internal/cmd/modelexperiments/token/token.go b/internal/cmd/modelexperiments/token/token.go index 67d8bced1..3e796e95b 100644 --- a/internal/cmd/modelexperiments/token/token.go +++ b/internal/cmd/modelexperiments/token/token.go @@ -2,6 +2,7 @@ package token import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/create" "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/modelexperiments/token/get" From dcc849902c966d8710c3d8c634ceb92b8de171e1 Mon Sep 17 00:00:00 2001 From: Syeda Haya Fatima Date: Tue, 8 Sep 2026 12:00:48 +0200 Subject: [PATCH 3/5] Fixed region parsing --- internal/pkg/services/modelexperiments/client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/services/modelexperiments/client/client.go b/internal/pkg/services/modelexperiments/client/client.go index 9ab23f22d..a6b12827c 100644 --- a/internal/pkg/services/modelexperiments/client/client.go +++ b/internal/pkg/services/modelexperiments/client/client.go @@ -10,5 +10,5 @@ import ( ) func ConfigureClient(p *print.Printer, cliVersion string) (*modelexperiments.APIClient, error) { - return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), true, genericclient.CreateApiClient[*modelexperiments.APIClient](modelexperiments.NewAPIClient)) + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AIModelExperimentsCustomEndpointKey), false, genericclient.CreateApiClient[*modelexperiments.APIClient](modelexperiments.NewAPIClient)) } From 45d864011dd615edd80aa7ffc8ea66b36562730a Mon Sep 17 00:00:00 2001 From: Syeda Haya Fatima Date: Tue, 8 Sep 2026 17:26:24 +0200 Subject: [PATCH 4/5] Fixed region and output response, STACKITMLO-1718 --- .../instance/create/create.go | 4 +-- .../instance/create/create_test.go | 3 +-- .../instance/delete/delete.go | 6 +---- .../instance/delete/delete_test.go | 1 - .../cmd/modelexperiments/instance/get/get.go | 17 +++++++----- .../modelexperiments/instance/get/get_test.go | 1 - .../modelexperiments/instance/list/list.go | 26 ++++++++++++++----- .../instance/list/list_test.go | 1 - .../modelexperiments/instance/patch/patch.go | 6 +---- .../instance/patch/patch_test.go | 1 - .../cmd/modelexperiments/token/get/get.go | 5 +++- .../cmd/modelexperiments/token/list/list.go | 15 ++++++++++- .../cmd/modelexperiments/token/patch/patch.go | 5 +++- 13 files changed, 57 insertions(+), 34 deletions(-) diff --git a/internal/cmd/modelexperiments/instance/create/create.go b/internal/cmd/modelexperiments/instance/create/create.go index 9829833fc..173bdea48 100644 --- a/internal/cmd/modelexperiments/instance/create/create.go +++ b/internal/cmd/modelexperiments/instance/create/create.go @@ -33,7 +33,6 @@ type inputModel struct { Description *string Labels *map[string]string Retention *string - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -117,7 +116,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr, Retention: flags.FlagToStringPointer(p, cmd, retentionFlag), - Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), } p.DebugInputModel(model) @@ -125,7 +123,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildCreateInstanceRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiCreateInstanceRequest { - req := apiClient.DefaultAPI.CreateInstance(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.CreateInstance(ctx, model.ProjectId, model.GlobalFlagModel.Region) payload := modelexperiments.CreateInstancePayload{ Name: model.Name, diff --git a/internal/cmd/modelexperiments/instance/create/create_test.go b/internal/cmd/modelexperiments/instance/create/create_test.go index 2bc86ef03..ba90c9eb2 100644 --- a/internal/cmd/modelexperiments/instance/create/create_test.go +++ b/internal/cmd/modelexperiments/instance/create/create_test.go @@ -45,8 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: "eu01", Verbosity: globalflags.VerbosityDefault, }, - Name: "example", - Region: "eu01", + Name: "example", } for _, mod := range mods { mod(model) diff --git a/internal/cmd/modelexperiments/instance/delete/delete.go b/internal/cmd/modelexperiments/instance/delete/delete.go index 4136d4984..69ca7fa04 100644 --- a/internal/cmd/modelexperiments/instance/delete/delete.go +++ b/internal/cmd/modelexperiments/instance/delete/delete.go @@ -9,7 +9,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" - "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" @@ -26,7 +25,6 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -80,7 +78,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - _ = flags.MarkFlagsRequired(cmd, globalflags.RegionFlag) } func parseInput( @@ -97,7 +94,6 @@ func parseInput( model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: inputArgs[0], - Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), } p.DebugInputModel(model) @@ -113,7 +109,7 @@ func buildDeleteInstanceRequest( return apiClient.DefaultAPI.DeleteInstance( ctx, model.ProjectId, - model.Region, + model.GlobalFlagModel.Region, model.InstanceId, ) } diff --git a/internal/cmd/modelexperiments/instance/delete/delete_test.go b/internal/cmd/modelexperiments/instance/delete/delete_test.go index fe21ee06c..544845108 100644 --- a/internal/cmd/modelexperiments/instance/delete/delete_test.go +++ b/internal/cmd/modelexperiments/instance/delete/delete_test.go @@ -53,7 +53,6 @@ func fixtureInputModel( Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - Region: "eu01", } for _, mod := range mods { diff --git a/internal/cmd/modelexperiments/instance/get/get.go b/internal/cmd/modelexperiments/instance/get/get.go index 3bd5a0ff6..69f613962 100644 --- a/internal/cmd/modelexperiments/instance/get/get.go +++ b/internal/cmd/modelexperiments/instance/get/get.go @@ -9,10 +9,10 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" - "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -26,7 +26,6 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -71,7 +70,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - _ = flags.MarkFlagsRequired(cmd, globalflags.RegionFlag) } func parseInput( @@ -88,7 +86,6 @@ func parseInput( model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: inputArgs[0], - Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), } p.DebugInputModel(model) @@ -104,7 +101,7 @@ func buildGetInstanceRequest( return apiClient.DefaultAPI.GetInstance( ctx, model.ProjectId, - model.Region, + model.GlobalFlagModel.Region, model.InstanceId, ) } @@ -119,6 +116,14 @@ func outputResult( } return p.OutputResult(outputFormat, resp.Instance, func() error { - return nil + table := tables.NewTable() + table.SetHeader("ID", "NAME", "REGION", "STATUS") + table.AddRow( + resp.Instance.Id, + resp.Instance.Name, + resp.Instance.Region, + resp.Instance.State, + ) + return table.Display(p) }) } diff --git a/internal/cmd/modelexperiments/instance/get/get_test.go b/internal/cmd/modelexperiments/instance/get/get_test.go index a15dcc374..1fdf6c814 100644 --- a/internal/cmd/modelexperiments/instance/get/get_test.go +++ b/internal/cmd/modelexperiments/instance/get/get_test.go @@ -53,7 +53,6 @@ func fixtureInputModel( Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - Region: "eu01", } for _, mod := range mods { diff --git a/internal/cmd/modelexperiments/instance/list/list.go b/internal/cmd/modelexperiments/instance/list/list.go index 0ba444e0e..840973fc4 100644 --- a/internal/cmd/modelexperiments/instance/list/list.go +++ b/internal/cmd/modelexperiments/instance/list/list.go @@ -9,10 +9,10 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" - "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/types" modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" @@ -20,7 +20,6 @@ import ( type inputModel struct { *globalflags.GlobalFlagModel - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -65,7 +64,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - _ = flags.MarkFlagsRequired(cmd, globalflags.RegionFlag) } func parseInput( @@ -81,7 +79,6 @@ func parseInput( model := inputModel{ GlobalFlagModel: globalFlags, - Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), } p.DebugInputModel(model) @@ -97,7 +94,7 @@ func buildListInstancesRequest( return apiClient.DefaultAPI.ListInstances( ctx, model.ProjectId, - model.Region, + model.GlobalFlagModel.Region, ) } @@ -111,6 +108,23 @@ func outputResult( } return p.OutputResult(outputFormat, resp.Instances, func() error { - return nil + if len(resp.Instances) == 0 { + p.Outputf("No instances found\n") + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "REGION", "STATUS") + + for _, instance := range resp.Instances { + table.AddRow( + instance.Id, + instance.Name, + instance.Region, + instance.State, + ) + } + + return table.Display(p) }) } diff --git a/internal/cmd/modelexperiments/instance/list/list_test.go b/internal/cmd/modelexperiments/instance/list/list_test.go index ec288bad0..0c536fd6a 100644 --- a/internal/cmd/modelexperiments/instance/list/list_test.go +++ b/internal/cmd/modelexperiments/instance/list/list_test.go @@ -51,7 +51,6 @@ func fixtureInputModel( Region: "eu01", Verbosity: globalflags.VerbosityDefault, }, - Region: "eu01", } for _, mod := range mods { diff --git a/internal/cmd/modelexperiments/instance/patch/patch.go b/internal/cmd/modelexperiments/instance/patch/patch.go index 3d7082bfe..1d1c77f94 100644 --- a/internal/cmd/modelexperiments/instance/patch/patch.go +++ b/internal/cmd/modelexperiments/instance/patch/patch.go @@ -37,8 +37,6 @@ type inputModel struct { Description *string Labels *map[string]*string Retention *string - - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -139,8 +137,6 @@ func parseInput( Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr, Retention: flags.FlagToStringPointer(p, cmd, retentionFlag), - - Region: flags.FlagToStringValue(p, cmd, globalflags.RegionFlag), } if model.Name == nil && @@ -163,7 +159,7 @@ func buildPatchInstanceRequest( req := apiClient.DefaultAPI.PartialUpdateInstance( ctx, model.ProjectId, - model.Region, + model.GlobalFlagModel.Region, model.InstanceId, ) diff --git a/internal/cmd/modelexperiments/instance/patch/patch_test.go b/internal/cmd/modelexperiments/instance/patch/patch_test.go index 70407abee..f1efb15dd 100644 --- a/internal/cmd/modelexperiments/instance/patch/patch_test.go +++ b/internal/cmd/modelexperiments/instance/patch/patch_test.go @@ -54,7 +54,6 @@ func fixtureInputModel( Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - Region: "eu01", } for _, mod := range mods { diff --git a/internal/cmd/modelexperiments/token/get/get.go b/internal/cmd/modelexperiments/token/get/get.go index 56e5155f9..11010c898 100644 --- a/internal/cmd/modelexperiments/token/get/get.go +++ b/internal/cmd/modelexperiments/token/get/get.go @@ -82,5 +82,8 @@ func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments. if resp == nil || resp.Token.Name == "" { return fmt.Errorf("response token is nil") } - return p.OutputResult(outputFormat, resp.Token, func() error { return nil }) + return p.OutputResult(outputFormat, resp.Token, func() error { + p.Outputf("Instance token %q (ID: %s)\n", resp.Token.Name, resp.Token.Id) + return nil + }) } diff --git a/internal/cmd/modelexperiments/token/list/list.go b/internal/cmd/modelexperiments/token/list/list.go index 84ad1a506..78a3b7a28 100644 --- a/internal/cmd/modelexperiments/token/list/list.go +++ b/internal/cmd/modelexperiments/token/list/list.go @@ -13,6 +13,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/modelexperiments/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/types" modelexperiments "github.com/stackitcloud/stackit-sdk-go/services/modelexperiments/v1api" @@ -72,5 +73,17 @@ func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments. if resp == nil { return fmt.Errorf("response is nil") } - return p.OutputResult(outputFormat, resp.Tokens, func() error { return nil }) + return p.OutputResult(outputFormat, resp.Tokens, func() error { + if len(resp.Tokens) == 0 { + p.Outputf("No instance tokens found\n") + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "DESCRIPTION") + for _, token := range resp.Tokens { + table.AddRow(token.Id, token.Name, token.Description) + } + return table.Display(p) + }) } diff --git a/internal/cmd/modelexperiments/token/patch/patch.go b/internal/cmd/modelexperiments/token/patch/patch.go index 38d63153e..a33cdb520 100644 --- a/internal/cmd/modelexperiments/token/patch/patch.go +++ b/internal/cmd/modelexperiments/token/patch/patch.go @@ -104,5 +104,8 @@ func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments. if resp == nil || resp.Token.Name == "" { return fmt.Errorf("response token is nil") } - return p.OutputResult(outputFormat, resp.Token, func() error { return nil }) + return p.OutputResult(outputFormat, resp.Token, func() error { + p.Outputf("Updated instance token %q (ID: %s)\n", resp.Token.Name, resp.Token.Id) + return nil + }) } From b146f0323f587607927adfef6789cc9006000256 Mon Sep 17 00:00:00 2001 From: Syeda Haya Fatima Date: Tue, 8 Sep 2026 22:30:45 +0200 Subject: [PATCH 5/5] Fix: model experiment list retrieval and response parsing, STACKITMLO-1718 --- internal/cmd/modelexperiments/instance/create/create.go | 2 +- internal/cmd/modelexperiments/instance/delete/delete.go | 7 +------ internal/cmd/modelexperiments/instance/get/get.go | 7 +------ internal/cmd/modelexperiments/instance/list/list.go | 7 +------ internal/cmd/modelexperiments/instance/patch/patch.go | 2 +- internal/cmd/modelexperiments/token/create/create.go | 8 +++----- internal/cmd/modelexperiments/token/create/create_test.go | 4 ++-- internal/cmd/modelexperiments/token/delete/delete.go | 6 ++---- internal/cmd/modelexperiments/token/delete/delete_test.go | 4 ++-- internal/cmd/modelexperiments/token/get/get.go | 6 ++---- internal/cmd/modelexperiments/token/get/get_test.go | 4 ++-- internal/cmd/modelexperiments/token/list/list.go | 6 ++---- internal/cmd/modelexperiments/token/list/list_test.go | 4 ++-- internal/cmd/modelexperiments/token/patch/patch.go | 8 +++----- internal/cmd/modelexperiments/token/patch/patch_test.go | 4 ++-- 15 files changed, 27 insertions(+), 52 deletions(-) diff --git a/internal/cmd/modelexperiments/instance/create/create.go b/internal/cmd/modelexperiments/instance/create/create.go index 173bdea48..d7d72d4f7 100644 --- a/internal/cmd/modelexperiments/instance/create/create.go +++ b/internal/cmd/modelexperiments/instance/create/create.go @@ -123,7 +123,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildCreateInstanceRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiCreateInstanceRequest { - req := apiClient.DefaultAPI.CreateInstance(ctx, model.ProjectId, model.GlobalFlagModel.Region) + req := apiClient.DefaultAPI.CreateInstance(ctx, model.ProjectId, model.Region) payload := modelexperiments.CreateInstancePayload{ Name: model.Name, diff --git a/internal/cmd/modelexperiments/instance/delete/delete.go b/internal/cmd/modelexperiments/instance/delete/delete.go index 69ca7fa04..33f92a286 100644 --- a/internal/cmd/modelexperiments/instance/delete/delete.go +++ b/internal/cmd/modelexperiments/instance/delete/delete.go @@ -72,14 +72,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { }, } - configureFlags(cmd) - return cmd } -func configureFlags(cmd *cobra.Command) { -} - func parseInput( p *print.Printer, cmd *cobra.Command, @@ -109,7 +104,7 @@ func buildDeleteInstanceRequest( return apiClient.DefaultAPI.DeleteInstance( ctx, model.ProjectId, - model.GlobalFlagModel.Region, + model.Region, model.InstanceId, ) } diff --git a/internal/cmd/modelexperiments/instance/get/get.go b/internal/cmd/modelexperiments/instance/get/get.go index 69f613962..b156a8d9b 100644 --- a/internal/cmd/modelexperiments/instance/get/get.go +++ b/internal/cmd/modelexperiments/instance/get/get.go @@ -64,14 +64,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { }, } - configureFlags(cmd) - return cmd } -func configureFlags(cmd *cobra.Command) { -} - func parseInput( p *print.Printer, cmd *cobra.Command, @@ -101,7 +96,7 @@ func buildGetInstanceRequest( return apiClient.DefaultAPI.GetInstance( ctx, model.ProjectId, - model.GlobalFlagModel.Region, + model.Region, model.InstanceId, ) } diff --git a/internal/cmd/modelexperiments/instance/list/list.go b/internal/cmd/modelexperiments/instance/list/list.go index 840973fc4..d1ee5b6a7 100644 --- a/internal/cmd/modelexperiments/instance/list/list.go +++ b/internal/cmd/modelexperiments/instance/list/list.go @@ -58,14 +58,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { }, } - configureFlags(cmd) - return cmd } -func configureFlags(cmd *cobra.Command) { -} - func parseInput( p *print.Printer, cmd *cobra.Command, @@ -94,7 +89,7 @@ func buildListInstancesRequest( return apiClient.DefaultAPI.ListInstances( ctx, model.ProjectId, - model.GlobalFlagModel.Region, + model.Region, ) } diff --git a/internal/cmd/modelexperiments/instance/patch/patch.go b/internal/cmd/modelexperiments/instance/patch/patch.go index 1d1c77f94..1b4e080f7 100644 --- a/internal/cmd/modelexperiments/instance/patch/patch.go +++ b/internal/cmd/modelexperiments/instance/patch/patch.go @@ -159,7 +159,7 @@ func buildPatchInstanceRequest( req := apiClient.DefaultAPI.PartialUpdateInstance( ctx, model.ProjectId, - model.GlobalFlagModel.Region, + model.Region, model.InstanceId, ) diff --git a/internal/cmd/modelexperiments/token/create/create.go b/internal/cmd/modelexperiments/token/create/create.go index 05e51794a..b9fa0f2a6 100644 --- a/internal/cmd/modelexperiments/token/create/create.go +++ b/internal/cmd/modelexperiments/token/create/create.go @@ -20,7 +20,6 @@ import ( const ( instanceIDFlag = "instance-id" - regionFlag = "region" nameFlag = "name" descriptionFlag = "description" labelFlag = "label" @@ -30,7 +29,6 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel InstanceID string - Region string Name string Description *string Labels *map[string]string @@ -63,7 +61,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(descriptionFlag, "", "Token description") cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`) cmd.Flags().String(ttlDurationFlag, "", "Token time to live duration") - _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag, nameFlag) + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, nameFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { @@ -79,7 +77,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, if len(labels) > 0 { labelsPtr = &labels } - model := &inputModel{GlobalFlagModel: globalFlags, InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag), Name: flags.FlagToStringValue(p, cmd, nameFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr, TTLDuration: flags.FlagToStringPointer(p, cmd, ttlDurationFlag)} + model := &inputModel{GlobalFlagModel: globalFlags, InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Name: flags.FlagToStringValue(p, cmd, nameFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr, TTLDuration: flags.FlagToStringPointer(p, cmd, ttlDurationFlag)} if model.Name == "" { return nil, fmt.Errorf("%s flag is required", nameFlag) } @@ -89,7 +87,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperiments.APIClient) modelexperiments.ApiCreateInstanceTokenRequest { payload := modelexperiments.CreateInstanceTokenPayload{Name: model.Name, Description: model.Description, Labels: model.Labels, TtlDuration: model.TTLDuration} - return apiClient.DefaultAPI.CreateInstanceToken(ctx, model.ProjectId, model.Region, model.InstanceID).CreateInstanceTokenPayload(payload) + return apiClient.DefaultAPI.CreateInstanceToken(ctx, model.ProjectId, model.GlobalFlagModel.Region, model.InstanceID).CreateInstanceTokenPayload(payload) } func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.CreateInstanceTokenResponse) error { diff --git a/internal/cmd/modelexperiments/token/create/create_test.go b/internal/cmd/modelexperiments/token/create/create_test.go index cedf3b09e..ad8fc2429 100644 --- a/internal/cmd/modelexperiments/token/create/create_test.go +++ b/internal/cmd/modelexperiments/token/create/create_test.go @@ -26,7 +26,7 @@ var testProjectID = uuid.NewString() var testInstanceID = uuid.NewString() func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { - values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01", nameFlag: "example"} + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, globalflags.RegionFlag: "eu01", nameFlag: "example"} for _, mod := range mods { mod(values) } @@ -34,7 +34,7 @@ func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { } func fixtureInputModel(mods ...func(*inputModel)) *inputModel { - model := &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, InstanceID: testInstanceID, Region: "eu01", Name: "example"} + model := &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, InstanceID: testInstanceID, Name: "example"} for _, mod := range mods { mod(model) } diff --git a/internal/cmd/modelexperiments/token/delete/delete.go b/internal/cmd/modelexperiments/token/delete/delete.go index 0c3c2245f..77106410f 100644 --- a/internal/cmd/modelexperiments/token/delete/delete.go +++ b/internal/cmd/modelexperiments/token/delete/delete.go @@ -22,14 +22,12 @@ import ( const ( tokenIDArg = "TOKEN_ID" instanceIDFlag = "instance-id" - regionFlag = "region" ) type inputModel struct { *globalflags.GlobalFlagModel TokenID string InstanceID string - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -64,7 +62,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") - _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -72,7 +70,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} } - model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag)} + model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag)} p.DebugInputModel(model) return model, nil } diff --git a/internal/cmd/modelexperiments/token/delete/delete_test.go b/internal/cmd/modelexperiments/token/delete/delete_test.go index 94c013fe0..5909815ac 100644 --- a/internal/cmd/modelexperiments/token/delete/delete_test.go +++ b/internal/cmd/modelexperiments/token/delete/delete_test.go @@ -25,14 +25,14 @@ var testInstanceID = uuid.NewString() var testTokenID = uuid.NewString() func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { - values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, globalflags.RegionFlag: "eu01"} for _, mod := range mods { mod(values) } return values } func fixtureInputModel() *inputModel { - return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID, Region: "eu01"} + return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID} } func fixtureRequest() modelexperiments.ApiDeleteInstanceTokenRequest { return testClient.DefaultAPI.DeleteInstanceToken(testContext, testProjectID, "eu01", testTokenID, testInstanceID) diff --git a/internal/cmd/modelexperiments/token/get/get.go b/internal/cmd/modelexperiments/token/get/get.go index 11010c898..d0f4163e0 100644 --- a/internal/cmd/modelexperiments/token/get/get.go +++ b/internal/cmd/modelexperiments/token/get/get.go @@ -22,14 +22,12 @@ import ( const ( tokenIDArg = "TOKEN_ID" instanceIDFlag = "instance-id" - regionFlag = "region" ) type inputModel struct { *globalflags.GlobalFlagModel TokenID string InstanceID string - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -61,7 +59,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") - _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -69,7 +67,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} } - model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag)} + model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag)} p.DebugInputModel(model) return model, nil } diff --git a/internal/cmd/modelexperiments/token/get/get_test.go b/internal/cmd/modelexperiments/token/get/get_test.go index ec88ea99d..4a7ab60a5 100644 --- a/internal/cmd/modelexperiments/token/get/get_test.go +++ b/internal/cmd/modelexperiments/token/get/get_test.go @@ -26,14 +26,14 @@ var testInstanceID = uuid.NewString() var testTokenID = uuid.NewString() func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { - values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, globalflags.RegionFlag: "eu01"} for _, mod := range mods { mod(values) } return values } func fixtureInputModel() *inputModel { - return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID, Region: "eu01"} + return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID} } func fixtureRequest() modelexperiments.ApiGetInstanceTokenRequest { return testClient.DefaultAPI.GetInstanceToken(testContext, testProjectID, "eu01", testTokenID, testInstanceID) diff --git a/internal/cmd/modelexperiments/token/list/list.go b/internal/cmd/modelexperiments/token/list/list.go index 78a3b7a28..194af8f4d 100644 --- a/internal/cmd/modelexperiments/token/list/list.go +++ b/internal/cmd/modelexperiments/token/list/list.go @@ -21,13 +21,11 @@ import ( const ( instanceIDFlag = "instance-id" - regionFlag = "region" ) type inputModel struct { *globalflags.GlobalFlagModel InstanceID string - Region string } func NewCmd(params *types.CmdParams) *cobra.Command { @@ -52,7 +50,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), instanceIDFlag, "ID of the instance") - _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { @@ -60,7 +58,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} } - model := &inputModel{GlobalFlagModel: globalFlags, InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag)} + model := &inputModel{GlobalFlagModel: globalFlags, InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag)} p.DebugInputModel(model) return model, nil } diff --git a/internal/cmd/modelexperiments/token/list/list_test.go b/internal/cmd/modelexperiments/token/list/list_test.go index fbbd6352b..9d7218969 100644 --- a/internal/cmd/modelexperiments/token/list/list_test.go +++ b/internal/cmd/modelexperiments/token/list/list_test.go @@ -25,14 +25,14 @@ var testProjectID = uuid.NewString() var testInstanceID = uuid.NewString() func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { - values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, globalflags.RegionFlag: "eu01"} for _, mod := range mods { mod(values) } return values } func fixtureInputModel() *inputModel { - return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, InstanceID: testInstanceID, Region: "eu01"} + return &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, InstanceID: testInstanceID} } func fixtureRequest() modelexperiments.ApiListInstanceTokensRequest { return testClient.DefaultAPI.ListInstanceTokens(testContext, testProjectID, "eu01", testInstanceID) diff --git a/internal/cmd/modelexperiments/token/patch/patch.go b/internal/cmd/modelexperiments/token/patch/patch.go index a33cdb520..904b28f36 100644 --- a/internal/cmd/modelexperiments/token/patch/patch.go +++ b/internal/cmd/modelexperiments/token/patch/patch.go @@ -22,7 +22,6 @@ import ( const ( tokenIDArg = "TOKEN_ID" instanceIDFlag = "instance-id" - regionFlag = "region" nameFlag = "name" descriptionFlag = "description" labelFlag = "label" @@ -32,7 +31,6 @@ type inputModel struct { *globalflags.GlobalFlagModel TokenID string InstanceID string - Region string Name *string Description *string Labels *map[string]string @@ -67,7 +65,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(nameFlag, "", "Token name") cmd.Flags().String(descriptionFlag, "", "Token description") cmd.Flags().StringToString(labelFlag, nil, `Labels as key-value pairs, e.g. "--label env=prod"`) - _ = flags.MarkFlagsRequired(cmd, instanceIDFlag, regionFlag) + _ = flags.MarkFlagsRequired(cmd, instanceIDFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -83,7 +81,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu if len(labels) > 0 { labelsPtr = &labels } - model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Region: flags.FlagToStringValue(p, cmd, regionFlag), Name: flags.FlagToStringPointer(p, cmd, nameFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr} + model := &inputModel{GlobalFlagModel: globalFlags, TokenID: inputArgs[0], InstanceID: flags.FlagToStringValue(p, cmd, instanceIDFlag), Name: flags.FlagToStringPointer(p, cmd, nameFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Labels: labelsPtr} p.DebugInputModel(model) return model, nil } @@ -97,7 +95,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *modelexperi } } payload := modelexperiments.PartialUpdateInstanceTokenPayload{Name: model.Name, Description: model.Description, Labels: &labels} - return apiClient.DefaultAPI.PartialUpdateInstanceToken(ctx, model.ProjectId, model.Region, model.TokenID, model.InstanceID).PartialUpdateInstanceTokenPayload(payload) + return apiClient.DefaultAPI.PartialUpdateInstanceToken(ctx, model.ProjectId, model.GlobalFlagModel.Region, model.TokenID, model.InstanceID).PartialUpdateInstanceTokenPayload(payload) } func outputResult(p *print.Printer, outputFormat string, resp *modelexperiments.PartialUpdateInstanceTokenResponse) error { diff --git a/internal/cmd/modelexperiments/token/patch/patch_test.go b/internal/cmd/modelexperiments/token/patch/patch_test.go index 638932358..c846bb9b3 100644 --- a/internal/cmd/modelexperiments/token/patch/patch_test.go +++ b/internal/cmd/modelexperiments/token/patch/patch_test.go @@ -27,14 +27,14 @@ var testInstanceID = uuid.NewString() var testTokenID = uuid.NewString() func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { - values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, regionFlag: "eu01"} + values := map[string]string{projectIDFlag: testProjectID, instanceIDFlag: testInstanceID, globalflags.RegionFlag: "eu01"} for _, mod := range mods { mod(values) } return values } func fixtureInputModel(mods ...func(*inputModel)) *inputModel { - model := &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID, Region: "eu01"} + model := &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectID, Region: "eu01", Verbosity: globalflags.VerbosityDefault}, TokenID: testTokenID, InstanceID: testInstanceID} for _, mod := range mods { mod(model) }