🚀 Big News:Socket Has Acquired Secure Annex.Learn More
Socket
Book a DemoSign in
Socket

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli

Package Overview
Dependencies
Versions
178
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli - go Package Compare versions

Comparing version
v0.22.0
to
v0.23.0
+43
docs/stackit_beta_server_machine-type_describe.md
## stackit beta server machine-type describe
Shows details of a server machine type
### Synopsis
Shows details of a server machine type.
```
stackit beta server machine-type describe MACHINE_TYPE [flags]
```
### Examples
```
Show details of a server machine type with name "xxx"
$ stackit beta server machine-type describe xxx
Show details of a server machine type with name "xxx" in JSON format
$ stackit beta server machine-type describe xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta server machine-type describe"
```
### 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 beta server machine-type](./stackit_beta_server_machine-type.md) - Provides functionality for server machine types available inside a project
## stackit beta server machine-type list
Get list of all machine types available in a project
### Synopsis
Get list of all machine types available in a project.
```
stackit beta server machine-type list [flags]
```
### Examples
```
Get list of all machine types
$ stackit beta server machine-type list
Get list of all machine types in JSON format
$ stackit beta server machine-type list --output-format json
List the first 10 machine types
$ stackit beta server machine-type list --limit=10
```
### Options
```
-h, --help Help for "stackit beta server machine-type list"
--limit int Limit the output to the first n elements
```
### 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 beta server machine-type](./stackit_beta_server_machine-type.md) - Provides functionality for server machine types available inside a project
## stackit beta server machine-type
Provides functionality for server machine types available inside a project
### Synopsis
Provides functionality for server machine types available inside a project.
```
stackit beta server machine-type [flags]
```
### Options
```
-h, --help Help for "stackit beta server machine-type"
```
### 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 beta server](./stackit_beta_server.md) - Provides functionality for servers
* [stackit beta server machine-type describe](./stackit_beta_server_machine-type_describe.md) - Shows details of a server machine type
* [stackit beta server machine-type list](./stackit_beta_server_machine-type_list.md) - Get list of all machine types available in a project
## stackit beta server os-update create
Creates a Server os-update.
### Synopsis
Creates a Server os-update. Operation always is async.
```
stackit beta server os-update create [flags]
```
### Examples
```
Create a Server os-update with name "myupdate"
$ stackit beta server os-update create --server-id xxx --name=myupdate
Create a Server os-update with name "myupdate" and maintenance window for 13 o'clock.
$ stackit beta server os-update create --server-id xxx --name=mybupdate --maintenance-window=13
```
### Options
```
-h, --help Help for "stackit beta server os-update create"
-m, --maintenance-window int Maintenance window (in hours, 1-24) (default 23)
-s, --server-id string Server ID
```
### 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 beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates
## stackit beta server os-update describe
Shows details of a Server os-update
### Synopsis
Shows details of a Server os-update.
```
stackit beta server os-update describe UPDATE_ID [flags]
```
### Examples
```
Get details of a Server os-update with id "my-os-update-id"
$ stackit beta server os-update describe my-os-update-id
Get details of a Server os-update with id "my-os-update-id" in JSON format
$ stackit beta server os-update describe my-os-update-id --output-format json
```
### Options
```
-h, --help Help for "stackit beta server os-update describe"
-s, --server-id string Server ID
```
### 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 beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates
## stackit beta server os-update disable
Disables server os-update service
### Synopsis
Disables server os-update service.
```
stackit beta server os-update disable [flags]
```
### Examples
```
Disable os-update functionality for your server.
$ stackit beta server os-update disable --server-id=zzz
```
### Options
```
-h, --help Help for "stackit beta server os-update disable"
-s, --server-id string Server ID
```
### 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 beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates
## stackit beta server os-update enable
Enables Server os-update service
### Synopsis
Enables Server os-update service.
```
stackit beta server os-update enable [flags]
```
### Examples
```
Enable os-update functionality for your server
$ stackit beta server os-update enable --server-id=zzz
```
### Options
```
-h, --help Help for "stackit beta server os-update enable"
-s, --server-id string Server ID
```
### 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 beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates
## stackit beta server os-update list
Lists all server os-updates
### Synopsis
Lists all server os-updates.
```
stackit beta server os-update list [flags]
```
### Examples
```
List all os-updates for a server with ID "xxx"
$ stackit beta server os-update list --server-id xxx
List all os-updates for a server with ID "xxx" in JSON format
$ stackit beta server os-update list --server-id xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta server os-update list"
--limit int Maximum number of entries to list
-s, --server-id string Server ID
```
### 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 beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates
## stackit beta server os-update schedule create
Creates a Server os-update Schedule
### Synopsis
Creates a Server os-update Schedule.
```
stackit beta server os-update schedule create [flags]
```
### Examples
```
Create a Server os-update Schedule with name "myschedule"
$ stackit beta server os-update schedule create --server-id xxx --name=myschedule
Create a Server os-update Schedule with name "myschedule" and maintenance window for 14 o'clock
$ stackit beta server os-update schedule create --server-id xxx --name=myschedule --maintenance-window=14
```
### Options
```
-e, --enabled Is the server os-update schedule enabled (default true)
-h, --help Help for "stackit beta server os-update schedule create"
-d, --maintenance-window int os-update maintenance window (in hours, 1-24) (default 23)
-n, --name string os-update schedule name
-r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1")
-s, --server-id string Server ID
```
### 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 beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule
## stackit beta server os-update schedule delete
Deletes a Server os-update Schedule
### Synopsis
Deletes a Server os-update Schedule.
```
stackit beta server os-update schedule delete SCHEDULE_ID [flags]
```
### Examples
```
Delete a Server os-update Schedule with ID "xxx" for server "zzz"
$ stackit beta server os-update schedule delete xxx --server-id=zzz
```
### Options
```
-h, --help Help for "stackit beta server os-update schedule delete"
-s, --server-id string Server ID
```
### 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 beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule
## stackit beta server os-update schedule describe
Shows details of a Server os-update Schedule
### Synopsis
Shows details of a Server os-update Schedule.
```
stackit beta server os-update schedule describe SCHEDULE_ID [flags]
```
### Examples
```
Get details of a Server os-update Schedule with id "my-schedule-id"
$ stackit beta server os-update schedule describe my-schedule-id
Get details of a Server os-update Schedule with id "my-schedule-id" in JSON format
$ stackit beta server os-update schedule describe my-schedule-id --output-format json
```
### Options
```
-h, --help Help for "stackit beta server os-update schedule describe"
-s, --server-id string Server ID
```
### 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 beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule
## stackit beta server os-update schedule list
Lists all server os-update schedules
### Synopsis
Lists all server os-update schedules.
```
stackit beta server os-update schedule list [flags]
```
### Examples
```
List all os-update schedules for a server with ID "xxx"
$ stackit beta server os-update schedule list --server-id xxx
List all os-update schedules for a server with ID "xxx" in JSON format
$ stackit beta server os-update schedule list --server-id xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta server os-update schedule list"
--limit int Maximum number of entries to list
-s, --server-id string Server ID
```
### 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 beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule
## stackit beta server os-update schedule update
Updates a Server os-update Schedule
### Synopsis
Updates a Server os-update Schedule.
```
stackit beta server os-update schedule update SCHEDULE_ID [flags]
```
### Examples
```
Update the name of the os-update schedule "zzz" of server "xxx"
$ stackit beta server os-update schedule update zzz --server-id=xxx --name=newname
```
### Options
```
-e, --enabled Is the server os-update schedule enabled (default true)
-h, --help Help for "stackit beta server os-update schedule update"
-d, --maintenance-window int Maintenance window (in hours, 1-24) (default 23)
-n, --name string os-update schedule name
-r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1")
-s, --server-id string Server ID
```
### 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 beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule
## stackit beta server os-update schedule
Provides functionality for Server os-update Schedule
### Synopsis
Provides functionality for Server os-update Schedule.
```
stackit beta server os-update schedule [flags]
```
### Options
```
-h, --help Help for "stackit beta server os-update schedule"
```
### 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 beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates
* [stackit beta server os-update schedule create](./stackit_beta_server_os-update_schedule_create.md) - Creates a Server os-update Schedule
* [stackit beta server os-update schedule delete](./stackit_beta_server_os-update_schedule_delete.md) - Deletes a Server os-update Schedule
* [stackit beta server os-update schedule describe](./stackit_beta_server_os-update_schedule_describe.md) - Shows details of a Server os-update Schedule
* [stackit beta server os-update schedule list](./stackit_beta_server_os-update_schedule_list.md) - Lists all server os-update schedules
* [stackit beta server os-update schedule update](./stackit_beta_server_os-update_schedule_update.md) - Updates a Server os-update Schedule
## stackit beta server os-update
Provides functionality for managed server updates
### Synopsis
Provides functionality for managed server updates.
```
stackit beta server os-update [flags]
```
### Options
```
-h, --help Help for "stackit beta server os-update"
```
### 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 beta server](./stackit_beta_server.md) - Provides functionality for servers
* [stackit beta server os-update create](./stackit_beta_server_os-update_create.md) - Creates a Server os-update.
* [stackit beta server os-update describe](./stackit_beta_server_os-update_describe.md) - Shows details of a Server os-update
* [stackit beta server os-update disable](./stackit_beta_server_os-update_disable.md) - Disables server os-update service
* [stackit beta server os-update enable](./stackit_beta_server_os-update_enable.md) - Enables Server os-update service
* [stackit beta server os-update list](./stackit_beta_server_os-update_list.md) - Lists all server os-updates
* [stackit beta server os-update schedule](./stackit_beta_server_os-update_schedule.md) - Provides functionality for Server os-update Schedule
package describe
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testProjectId = uuid.NewString()
var testMachineType = "t1.1"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testMachineType,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
MachineType: testMachineType,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiGetMachineTypeRequest)) iaas.ApiGetMachineTypeRequest {
request := testClient.GetMachineType(testCtx, testProjectId, testMachineType)
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: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "missing machine type arg",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest iaas.ApiGetMachineTypeRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package describe
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/spf13/cobra"
)
const (
machineTypeArg = "MACHINE_TYPE"
)
type inputModel struct {
*globalflags.GlobalFlagModel
MachineType string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", machineTypeArg),
Short: "Shows details of a server machine type",
Long: "Shows details of a server machine type.",
Args: args.SingleArg(machineTypeArg, nil),
Example: examples.Build(
examples.NewExample(
`Show details of a server machine type with name "xxx"`,
"$ stackit beta server machine-type describe xxx",
),
examples.NewExample(
`Show details of a server machine type with name "xxx" in JSON format`,
"$ stackit beta server machine-type describe xxx --output-format json",
),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read server machine type: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
return cmd
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
machineType := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
MachineType: machineType,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetMachineTypeRequest {
return apiClient.GetMachineType(ctx, model.ProjectId, model.MachineType)
}
func outputResult(p *print.Printer, outputFormat string, machineType *iaas.MachineType) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(machineType, "", " ")
if err != nil {
return fmt.Errorf("marshal server machine type: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(machineType, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server machine type: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("NAME", utils.PtrString(machineType.Name))
table.AddSeparator()
table.AddRow("VCPUS", utils.PtrString(machineType.Vcpus))
table.AddSeparator()
table.AddRow("RAM (in MB)", utils.PtrString(machineType.Ram))
table.AddSeparator()
table.AddRow("DISK SIZE (in GB)", utils.PtrString(machineType.Disk))
table.AddSeparator()
table.AddRow("DESCRIPTION", utils.PtrString(machineType.Description))
table.AddSeparator()
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package list
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &iaas.APIClient{}
var testProjectId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
ProjectId: testProjectId,
},
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *iaas.ApiListMachineTypesRequest)) iaas.ApiListMachineTypesRequest {
request := testClient.ListMachineTypes(testCtx, testProjectId)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "no flag values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "limit invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "limit invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest iaas.ApiListMachineTypesRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/iaas/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
}
const (
limitFlag = "limit"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Get list of all machine types available in a project",
Long: "Get list of all machine types available in a project.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Get list of all machine types`,
"$ stackit beta server machine-type list",
),
examples.NewExample(
`Get list of all machine types in JSON format`,
"$ stackit beta server machine-type list --output-format json",
),
examples.NewExample(
`List the first 10 machine types`,
`$ stackit beta server machine-type list --limit=10`,
),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read machine-types: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
projectLabel, err := projectname.GetProjectName(ctx, p, cmd)
if err != nil {
p.Debug(print.ErrorLevel, "get project name: %v", err)
projectLabel = model.ProjectId
}
p.Info("No machine-types found for project %q\n", projectLabel)
return nil
}
// limit output
if model.Limit != nil && len(*resp.Items) > int(*model.Limit) {
*resp.Items = (*resp.Items)[:*model.Limit]
}
return outputResult(p, model, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements")
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
if limit != nil && *limit < 1 {
return nil, &errors.FlagValidationError{
Flag: limitFlag,
Details: "must be greater than 0",
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListMachineTypesRequest {
return apiClient.ListMachineTypes(ctx, model.ProjectId)
}
func outputResult(p *print.Printer, model *inputModel, machineTypes *iaas.MachineTypeListResponse) error {
outputFormat := model.OutputFormat
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(machineTypes, "", " ")
if err != nil {
return fmt.Errorf("marshal machineTypes: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(machineTypes, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal machineTypes: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetTitle("Machine-Types")
table.SetHeader("NAME", "DESCRIPTION")
for _, machineType := range *machineTypes.GetItems() {
table.AddRow(*machineType.Name, utils.PtrString(machineType.Description))
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package machinetype
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/machine-type/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/machine-type/list"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "machine-type",
Short: "Provides functionality for server machine types available inside a project",
Long: "Provides functionality for server machine types available inside a project.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
}
package create
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
serverIdFlag: testServerId,
maintenanceWindowFlag: "13",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
MaintenanceWindow: int64(13),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateRequest)) serverupdate.ApiCreateUpdateRequest {
request := testClient.CreateUpdate(testCtx, testProjectId, testServerId)
request = request.CreateUpdatePayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *serverupdate.CreateUpdatePayload)) serverupdate.CreateUpdatePayload {
payload := serverupdate.CreateUpdatePayload{
MaintenanceWindow: utils.Ptr(int64(13)),
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
aclValues []string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "with defaults",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, maintenanceWindowFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.MaintenanceWindow = 23
}),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiCreateUpdateRequest
isValid bool
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"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/serverosupdate/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
serverIdFlag = "server-id"
maintenanceWindowFlag = "maintenance-window"
defaultMaintenanceWindow = 23
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
MaintenanceWindow int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a Server os-update.",
Long: "Creates a Server os-update. Operation always is async.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a Server os-update with name "myupdate"`,
`$ stackit beta server os-update create --server-id xxx --name=myupdate`),
examples.NewExample(
`Create a Server os-update with name "myupdate" and maintenance window for 13 o'clock.`,
`$ stackit beta server os-update create --server-id xxx --name=mybupdate --maintenance-window=13`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a os-update for server %s?", model.ServerId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create Server os-update: %w", err)
}
return outputResult(p, model, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
cmd.Flags().Int64P(maintenanceWindowFlag, "m", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)")
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
MaintenanceWindow: flags.FlagWithDefaultToInt64Value(p, cmd, maintenanceWindowFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) (serverupdate.ApiCreateUpdateRequest, error) {
req := apiClient.CreateUpdate(ctx, model.ProjectId, model.ServerId)
payload := serverupdate.CreateUpdatePayload{
MaintenanceWindow: &model.MaintenanceWindow,
}
req = req.CreateUpdatePayload(payload)
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *serverupdate.Update) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal server os-update: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server os-update: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Triggered creation of server os-update for server %s. Update ID: %d\n", model.ServerId, *resp.Id)
return nil
}
}
package describe
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testUpdateId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testUpdateId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
serverIdFlag: testServerId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
UpdateId: testUpdateId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiGetUpdateRequest)) serverupdate.ApiGetUpdateRequest {
request := testClient.GetUpdate(testCtx, testProjectId, testServerId, testUpdateId)
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: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
isValid bool
expectedRequest serverupdate.ApiGetUpdateRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package describe
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/serverosupdate/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
updateIdArg = "UPDATE_ID"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
UpdateId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", updateIdArg),
Short: "Shows details of a Server os-update",
Long: "Shows details of a Server os-update.",
Args: args.SingleArg(updateIdArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of a Server os-update with id "my-os-update-id"`,
"$ stackit beta server os-update describe my-os-update-id"),
examples.NewExample(
`Get details of a Server os-update with id "my-os-update-id" in JSON format`,
"$ stackit beta server os-update describe my-os-update-id --output-format json"),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read server os-update: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
updateId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
UpdateId: updateId,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiGetUpdateRequest {
req := apiClient.GetUpdate(ctx, model.ProjectId, model.ServerId, model.UpdateId)
return req
}
func outputResult(p *print.Printer, outputFormat string, update *serverupdate.Update) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(update, "", " ")
if err != nil {
return fmt.Errorf("marshal server update: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(update, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server update: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("ID", *update.Id)
table.AddSeparator()
table.AddRow("STATUS", *update.Status)
table.AddSeparator()
if update.InstalledUpdates != nil {
table.AddRow("INSTALLED UPDATES", *update.InstalledUpdates)
} else {
table.AddRow("INSTALLED UPDATES", "...")
}
table.AddSeparator()
if update.FailedUpdates != nil {
table.AddRow("FAILED UPDATES", *update.FailedUpdates)
} else {
table.AddRow("FAILED UPDATES", "...")
}
table.AddRow("START DATE", *update.StartDate)
table.AddSeparator()
if update.EndDate != nil {
table.AddRow("END DATE", *update.EndDate)
} else {
table.AddRow("END DATE", "...")
}
table.AddSeparator()
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package disable
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiDisableServiceRequest)) serverupdate.ApiDisableServiceRequest {
request := testClient.DisableService(testCtx, testProjectId, testServerId)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.ServerId = ""
}),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
p := print.NewPrinter()
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiDisableServiceRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package disable
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/serverosupdate/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "disable",
Short: "Disables server os-update service",
Long: "Disables server os-update service.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Disable os-update functionality for your server.`,
"$ stackit beta server os-update disable --server-id=zzz"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to disable the os-update service for server %s?", model.ServerId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("disable server os-update service: %w", err)
}
p.Info("Disabled Server os-update service for server %s\n", model.ServerId)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiDisableServiceRequest {
req := apiClient.DisableService(ctx, model.ProjectId, model.ServerId)
return req
}
package enable
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiEnableServiceRequest)) serverupdate.ApiEnableServiceRequest {
request := testClient.EnableService(testCtx, testProjectId, testServerId).EnableServicePayload(serverupdate.EnableServicePayload{})
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.ServerId = ""
}),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
p := print.NewPrinter()
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiEnableServiceRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package enable
import (
"context"
"fmt"
"strings"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/serverosupdate/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "enable",
Short: "Enables Server os-update service",
Long: "Enables Server os-update service.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Enable os-update functionality for your server`,
"$ stackit beta server os-update enable --server-id=zzz"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to enable the server os-update service for server %s?", model.ServerId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
if !strings.Contains(err.Error(), "Tried to activate already active service") {
return fmt.Errorf("enable server os-update: %w", err)
}
}
p.Info("Enabled os-update service for server %s\n", model.ServerId)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiEnableServiceRequest {
payload := serverupdate.EnableServicePayload{}
req := apiClient.EnableService(ctx, model.ProjectId, model.ServerId).EnableServicePayload(payload)
return req
}
package list
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
serverIdFlag: testServerId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Limit: utils.Ptr(int64(10)),
ServerId: testServerId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdatesRequest)) serverupdate.ApiListUpdatesRequest {
request := testClient.ListUpdates(testCtx, testProjectId, testServerId)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "limit invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "limit invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiListUpdatesRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/serverosupdate/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
limitFlag = "limit"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all server os-updates",
Long: "Lists all server os-updates.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all os-updates for a server with ID "xxx"`,
"$ stackit beta server os-update list --server-id xxx"),
examples.NewExample(
`List all os-updates for a server with ID "xxx" in JSON format`,
"$ stackit beta server os-update list --server-id xxx --output-format json"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list server os-update: %w", err)
}
updates := *resp.Items
if len(updates) == 0 {
p.Info("No os-updates found for server %s\n", model.ServerId)
return nil
}
// Truncate output
if model.Limit != nil && len(updates) > int(*model.Limit) {
updates = updates[:*model.Limit]
}
return outputResult(p, model.OutputFormat, updates)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
if limit != nil && *limit < 1 {
return nil, &errors.FlagValidationError{
Flag: limitFlag,
Details: "must be greater than 0",
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
Limit: limit,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiListUpdatesRequest {
req := apiClient.ListUpdates(ctx, model.ProjectId, model.ServerId)
return req
}
func outputResult(p *print.Printer, outputFormat string, updates []serverupdate.Update) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(updates, "", " ")
if err != nil {
return fmt.Errorf("marshal server os-update list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(updates, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server os-update list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "STATUS", "INSTALLED UPDATES", "FAILED UPDATES", "START DATE", "END DATE")
for i := range updates {
s := updates[i]
endDate := "..."
if s.EndDate != nil {
endDate = *s.EndDate
}
installed := "..."
if s.InstalledUpdates != nil {
installed = strconv.FormatInt(*s.InstalledUpdates, 10)
}
failed := "..."
if s.FailedUpdates != nil {
failed = strconv.FormatInt(*s.FailedUpdates, 10)
}
table.AddRow(*s.Id, *s.Status, installed, failed, *s.StartDate, endDate)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package osupdate
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/disable"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/enable"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "os-update",
Short: "Provides functionality for managed server updates",
Long: "Provides functionality for managed server updates.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(create.NewCmd(p))
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
cmd.AddCommand(enable.NewCmd(p))
cmd.AddCommand(disable.NewCmd(p))
cmd.AddCommand(schedule.NewCmd(p))
}
package create
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
serverIdFlag: testServerId,
nameFlag: "example-schedule-name",
enabledFlag: "true",
rruleFlag: defaultRrule,
maintenanceWindowFlag: "23",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
ScheduleName: "example-schedule-name",
Enabled: defaultEnabled,
Rrule: defaultRrule,
MaintenanceWindow: int64(23),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateScheduleRequest)) serverupdate.ApiCreateUpdateScheduleRequest {
request := testClient.CreateUpdateSchedule(testCtx, testProjectId, testServerId)
request = request.CreateUpdateSchedulePayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *serverupdate.CreateUpdateSchedulePayload)) serverupdate.CreateUpdateSchedulePayload {
payload := serverupdate.CreateUpdateSchedulePayload{
Name: utils.Ptr("example-schedule-name"),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"),
MaintenanceWindow: utils.Ptr(int64(23)),
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
aclValues []string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "with defaults",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, maintenanceWindowFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiCreateUpdateScheduleRequest
isValid bool
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"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/serverosupdate/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
nameFlag = "name"
enabledFlag = "enabled"
rruleFlag = "rrule"
maintenanceWindowFlag = "maintenance-window"
serverIdFlag = "server-id"
defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"
defaultMaintenanceWindow = 23
defaultEnabled = true
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
ScheduleName string
Enabled bool
Rrule string
MaintenanceWindow int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a Server os-update Schedule",
Long: "Creates a Server os-update Schedule.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a Server os-update Schedule with name "myschedule"`,
`$ stackit beta server os-update schedule create --server-id xxx --name=myschedule`),
examples.NewExample(
`Create a Server os-update Schedule with name "myschedule" and maintenance window for 14 o'clock`,
`$ stackit beta server os-update schedule create --server-id xxx --name=myschedule --maintenance-window=14`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a os-update Schedule for server %s?", model.ServerId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create Server os-update Schedule: %w", err)
}
return outputResult(p, model, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
cmd.Flags().StringP(nameFlag, "n", "", "os-update schedule name")
cmd.Flags().Int64P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "os-update maintenance window (in hours, 1-24)")
cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server os-update schedule enabled")
cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "os-update RRULE (recurrence rule)")
err := flags.MarkFlagsRequired(cmd, serverIdFlag, nameFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
MaintenanceWindow: flags.FlagWithDefaultToInt64Value(p, cmd, maintenanceWindowFlag),
ScheduleName: flags.FlagToStringValue(p, cmd, nameFlag),
Rrule: flags.FlagWithDefaultToStringValue(p, cmd, rruleFlag),
Enabled: flags.FlagToBoolValue(p, cmd, enabledFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) (serverupdate.ApiCreateUpdateScheduleRequest, error) {
req := apiClient.CreateUpdateSchedule(ctx, model.ProjectId, model.ServerId)
req = req.CreateUpdateSchedulePayload(serverupdate.CreateUpdateSchedulePayload{
Enabled: &model.Enabled,
Name: &model.ScheduleName,
Rrule: &model.Rrule,
MaintenanceWindow: &model.MaintenanceWindow,
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *serverupdate.UpdateSchedule) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal server os-update schedule: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server os-update schedule: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created server os-update schedule for server %s. os-update Schedule ID: %d\n", model.ServerId, *resp.Id)
return nil
}
}
package delete
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testUpdateScheduleId = "5"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testUpdateScheduleId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
serverIdFlag: testServerId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
ScheduleId: testUpdateScheduleId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiDeleteUpdateScheduleRequest)) serverupdate.ApiDeleteUpdateScheduleRequest {
request := testClient.DeleteUpdateSchedule(testCtx, testProjectId, testServerId, testUpdateScheduleId)
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: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiDeleteUpdateScheduleRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package delete
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/serverosupdate/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
scheduleIdArg = "SCHEDULE_ID"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ScheduleId string
ServerId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", scheduleIdArg),
Short: "Deletes a Server os-update Schedule",
Long: "Deletes a Server os-update Schedule.",
Args: args.SingleArg(scheduleIdArg, nil),
Example: examples.Build(
examples.NewExample(
`Delete a Server os-update Schedule with ID "xxx" for server "zzz"`,
"$ stackit beta server os-update schedule delete xxx --server-id=zzz"),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete server os-update schedule %q? (This cannot be undone)", model.ScheduleId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
err = req.Execute()
if err != nil {
return fmt.Errorf("delete Server os-update Schedule: %w", err)
}
p.Info("Deleted server os-update schedule %q\n", model.ScheduleId)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
scheduleId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ScheduleId: scheduleId,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiDeleteUpdateScheduleRequest {
req := apiClient.DeleteUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId)
return req
}
package describe
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testScheduleId = "5"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testScheduleId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
serverIdFlag: testServerId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ServerId: testServerId,
ScheduleId: testScheduleId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiGetUpdateScheduleRequest)) serverupdate.ApiGetUpdateScheduleRequest {
request := testClient.GetUpdateSchedule(testCtx, testProjectId, testServerId, testScheduleId)
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: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
isValid bool
expectedRequest serverupdate.ApiGetUpdateScheduleRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package describe
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/serverosupdate/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
scheduleIdArg = "SCHEDULE_ID"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
ScheduleId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", scheduleIdArg),
Short: "Shows details of a Server os-update Schedule",
Long: "Shows details of a Server os-update Schedule.",
Args: args.SingleArg(scheduleIdArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of a Server os-update Schedule with id "my-schedule-id"`,
"$ stackit beta server os-update schedule describe my-schedule-id"),
examples.NewExample(
`Get details of a Server os-update Schedule with id "my-schedule-id" in JSON format`,
"$ stackit beta server os-update schedule describe my-schedule-id --output-format json"),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("read server os-update schedule: %w", err)
}
return outputResult(p, model.OutputFormat, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
scheduleId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
ScheduleId: scheduleId,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiGetUpdateScheduleRequest {
req := apiClient.GetUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId)
return req
}
func outputResult(p *print.Printer, outputFormat string, schedule *serverupdate.UpdateSchedule) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(schedule, "", " ")
if err != nil {
return fmt.Errorf("marshal server os-update schedule: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(schedule, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal server os-update schedule: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("SCHEDULE ID", *schedule.Id)
table.AddSeparator()
table.AddRow("SCHEDULE NAME", *schedule.Name)
table.AddSeparator()
table.AddRow("ENABLED", *schedule.Enabled)
table.AddSeparator()
table.AddRow("RRULE", *schedule.Rrule)
table.AddSeparator()
table.AddRow("MAINTENANCE WINDOW", *schedule.MaintenanceWindow)
table.AddSeparator()
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package list
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
limitFlag: "10",
serverIdFlag: testServerId,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Limit: utils.Ptr(int64(10)),
ServerId: testServerId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdateSchedulesRequest)) serverupdate.ApiListUpdateSchedulesRequest {
request := testClient.ListUpdateSchedules(testCtx, testProjectId, testServerId)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
flagValues: map[string]string{},
isValid: false,
},
{
description: "project id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "limit invalid",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "invalid"
}),
isValid: false,
},
{
description: "limit invalid 2",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[limitFlag] = "0"
}),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiListUpdateSchedulesRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(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)
}
})
}
}
package list
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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/serverosupdate/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
limitFlag = "limit"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all server os-update schedules",
Long: "Lists all server os-update schedules.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all os-update schedules for a server with ID "xxx"`,
"$ stackit beta server os-update schedule list --server-id xxx"),
examples.NewExample(
`List all os-update schedules for a server with ID "xxx" in JSON format`,
"$ stackit beta server os-update schedule list --server-id xxx --output-format json"),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list server os-update schedules: %w", err)
}
schedules := *resp.Items
if len(schedules) == 0 {
p.Info("No os-update schedules found for server %s\n", model.ServerId)
return nil
}
// Truncate output
if model.Limit != nil && len(schedules) > int(*model.Limit) {
schedules = schedules[:*model.Limit]
}
return outputResult(p, model.OutputFormat, schedules)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
if limit != nil && *limit < 1 {
return nil, &errors.FlagValidationError{
Flag: limitFlag,
Details: "must be greater than 0",
}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
Limit: limit,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiListUpdateSchedulesRequest {
req := apiClient.ListUpdateSchedules(ctx, model.ProjectId, model.ServerId)
return req
}
func outputResult(p *print.Printer, outputFormat string, schedules []serverupdate.UpdateSchedule) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(schedules, "", " ")
if err != nil {
return fmt.Errorf("marshal Server os-update Schedules list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(schedules, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal Server os-update Schedules list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("SCHEDULE ID", "SCHEDULE NAME", "ENABLED", "RRULE", "MAINTENANCE WINDOW")
for i := range schedules {
s := schedules[i]
table.AddRow(*s.Id, *s.Name, *s.Enabled, *s.Rrule, *s.MaintenanceWindow)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package schedule
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/create"
del "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update/schedule/update"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "schedule",
Short: "Provides functionality for Server os-update Schedule",
Long: "Provides functionality for Server os-update Schedule.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(list.NewCmd(p))
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(create.NewCmd(p))
cmd.AddCommand(del.NewCmd(p))
cmd.AddCommand(update.NewCmd(p))
}
package update
import (
"context"
"strconv"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverupdate.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testScheduleId = "5"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testScheduleId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
serverIdFlag: testServerId,
nameFlag: "example-schedule-name",
enabledFlag: "true",
rruleFlag: defaultRrule,
maintenanceWindowFlag: "23",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
ScheduleId: testScheduleId,
ServerId: testServerId,
ScheduleName: utils.Ptr("example-schedule-name"),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr(defaultRrule),
MaintenanceWindow: utils.Ptr(int64(23)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureUpdateSchedule(mods ...func(schedule *serverupdate.UpdateSchedule)) *serverupdate.UpdateSchedule {
id, _ := strconv.ParseInt(testScheduleId, 10, 64)
schedule := &serverupdate.UpdateSchedule{
Name: utils.Ptr("example-schedule-name"),
Id: utils.Ptr(id),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr(defaultRrule),
MaintenanceWindow: utils.Ptr(int64(23)),
}
for _, mod := range mods {
mod(schedule)
}
return schedule
}
func fixturePayload(mods ...func(payload *serverupdate.UpdateUpdateSchedulePayload)) serverupdate.UpdateUpdateSchedulePayload {
payload := serverupdate.UpdateUpdateSchedulePayload{
Name: utils.Ptr("example-schedule-name"),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"),
MaintenanceWindow: utils.Ptr(int64(23)),
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func fixtureRequest(mods ...func(request *serverupdate.ApiUpdateUpdateScheduleRequest)) serverupdate.ApiUpdateUpdateScheduleRequest {
request := testClient.UpdateUpdateSchedule(testCtx, testProjectId, testServerId, testScheduleId)
request = request.UpdateUpdateSchedulePayload(fixturePayload())
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: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "schedule id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
err = cmd.ValidateFlagGroups()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flag groups: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest serverupdate.ApiUpdateUpdateScheduleRequest
isValid bool
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
isValid: true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request, err := buildRequest(testCtx, tt.model, testClient, *fixtureUpdateSchedule())
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error building request: %v", err)
}
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package update
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"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/serverosupdate/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
const (
scheduleIdArg = "SCHEDULE_ID"
nameFlag = "name"
enabledFlag = "enabled"
rruleFlag = "rrule"
maintenanceWindowFlag = "maintenance-window"
serverIdFlag = "server-id"
defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"
defaultMaintenanceWindow = 23
defaultEnabled = true
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
ScheduleId string
ScheduleName *string
Enabled *bool
Rrule *string
MaintenanceWindow *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("update %s", scheduleIdArg),
Short: "Updates a Server os-update Schedule",
Long: "Updates a Server os-update Schedule.",
Example: examples.Build(
examples.NewExample(
`Update the name of the os-update schedule "zzz" of server "xxx"`,
"$ stackit beta server os-update schedule update zzz --server-id=xxx --name=newname"),
),
Args: args.SingleArg(scheduleIdArg, nil),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
currentSchedule, err := apiClient.GetUpdateScheduleExecute(ctx, model.ProjectId, model.ServerId, model.ScheduleId)
if err != nil {
p.Debug(print.ErrorLevel, "get current server os-update schedule: %v", err)
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update Server os-update Schedule %q?", model.ScheduleId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient, *currentSchedule)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("update Server os-update Schedule: %w", err)
}
return outputResult(p, model, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID")
cmd.Flags().StringP(nameFlag, "n", "", "os-update schedule name")
cmd.Flags().Int64P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)")
cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server os-update schedule enabled")
cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "os-update RRULE (recurrence rule)")
err := flags.MarkFlagsRequired(cmd, serverIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
scheduleId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &cliErr.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ScheduleId: scheduleId,
ScheduleName: flags.FlagToStringPointer(p, cmd, nameFlag),
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
MaintenanceWindow: flags.FlagToInt64Pointer(p, cmd, maintenanceWindowFlag),
Rrule: flags.FlagToStringPointer(p, cmd, rruleFlag),
Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag),
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient, old serverupdate.UpdateSchedule) (serverupdate.ApiUpdateUpdateScheduleRequest, error) {
req := apiClient.UpdateUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId)
if model.MaintenanceWindow != nil {
old.MaintenanceWindow = model.MaintenanceWindow
}
if model.Enabled != nil {
old.Enabled = model.Enabled
}
if model.ScheduleName != nil {
old.Name = model.ScheduleName
}
if model.Rrule != nil {
old.Rrule = model.Rrule
}
req = req.UpdateUpdateSchedulePayload(serverupdate.UpdateUpdateSchedulePayload{
Enabled: old.Enabled,
Name: old.Name,
Rrule: old.Rrule,
MaintenanceWindow: old.MaintenanceWindow,
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *serverupdate.UpdateSchedule) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal update server os-update schedule: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal update server os-update schedule: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Info("Updated server os-update schedule %d\n", *resp.Id)
return nil
}
}
package client
import (
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/spf13/viper"
sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/services/serverupdate"
)
func ConfigureClient(p *print.Printer) (*serverupdate.APIClient, error) {
var err error
var apiClient *serverupdate.APIClient
var cfgOptions []sdkConfig.ConfigurationOption
authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser)
if err != nil {
p.Debug(print.ErrorLevel, "configure authentication: %v", err)
return nil, &errors.AuthError{}
}
cfgOptions = append(cfgOptions, authCfgOption)
customEndpoint := viper.GetString(config.ServerOsUpdateCustomEndpointKey)
if customEndpoint != "" {
cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint))
} else {
cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion("eu01"))
}
if p.IsVerbosityDebug() {
cfgOptions = append(cfgOptions,
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)),
)
}
apiClient, err = serverupdate.NewAPIClient(cfgOptions...)
if err != nil {
p.Debug(print.ErrorLevel, "create new API client: %v", err)
return nil, &errors.AuthError{}
}
return apiClient, nil
}
#!/bin/bash
# Add replace directives to local files to go.work
set -eo pipefail
while getopts "s:" option; do
case "${option}" in
s)
SDK_DIR=${OPTARG}
;;
*)
echo "call: $0 [-s sdk-dir] <apis*>"
exit 0
;;
esac
done
shift $((OPTIND-1))
if [ -z "$SDK_DIR" ]; then
SDK_DIR=../stackit-sdk-generator/sdk-repo-updated
echo "No SDK_DIR set, using $SDK_DIR"
fi
if [ ! -f go.work ]; then
go work init
go work use .
else
echo "go.work already exists"
fi
if [ $# -gt 0 ];then
# modules passed via commandline
for service in $*; do
if [ ! -d $SDK_DIR/services/$service ]; then
echo "service directory $SDK_DIR/services/$service does not exist"
exit 1
fi
echo "replacing selected service $service"
if [ "$service" = "core" ]; then
go work edit -replace github.com/stackitcloud/stackit-sdk-go/core=$SDK_DIR/core
else
go work edit -replace github.com/stackitcloud/stackit-sdk-go/services/$service=$SDK_DIR/services/$service
fi
done
else
# replace all modules
echo "replacing all services"
go work edit -replace github.com/stackitcloud/stackit-sdk-go/core=$SDK_DIR/core
for n in $(find ${SDK_DIR}/services -name go.mod);do
service=$(dirname $n)
service=${service#${SDK_DIR}/services/}
go work edit -replace github.com/stackitcloud/stackit-sdk-go/services/$service=$(dirname $n)
done
fi
go work edit -fmt
go work sync
+4
-0

@@ -11,1 +11,5 @@ # Binaries

.DS_Store
# Go workspace file
go.work
go.work.sum

@@ -42,3 +42,5 @@ ## stackit beta server

* [stackit beta server log](./stackit_beta_server_log.md) - Gets server console log
* [stackit beta server machine-type](./stackit_beta_server_machine-type.md) - Provides functionality for server machine types available inside a project
* [stackit beta server network-interface](./stackit_beta_server_network-interface.md) - Allows attaching/detaching network interfaces to servers
* [stackit beta server os-update](./stackit_beta_server_os-update.md) - Provides functionality for managed server updates
* [stackit beta server public-ip](./stackit_beta_server_public-ip.md) - Allows attaching/detaching public IPs to servers

@@ -45,0 +47,0 @@ * [stackit beta server reboot](./stackit_beta_server_reboot.md) - Reboots a server

@@ -52,2 +52,3 @@ ## stackit config set

--secrets-manager-custom-endpoint string Secrets Manager API base URL, used in calls to this API
--server-osupdate-custom-endpoint string Server Update Management API base URL, used in calls to this API
--serverbackup-custom-endpoint string Server Backup API base URL, used in calls to this API

@@ -54,0 +55,0 @@ --service-account-custom-endpoint string Service Account API base URL, used in calls to this API

@@ -53,2 +53,3 @@ ## stackit config unset

--secrets-manager-custom-endpoint Secrets Manager API base URL. If unset, uses the default base URL
--server-osupdate-custom-endpoint Server Update Management base URL. If unset, uses the default base URL
--serverbackup-custom-endpoint Server Backup base URL. If unset, uses the default base URL

@@ -55,0 +56,0 @@ --service-account-custom-endpoint Service Account API base URL. If unset, uses the default base URL

+10
-9

@@ -7,3 +7,3 @@ module github.com/stackitcloud/stackit-cli

github.com/fatih/color v1.18.0
github.com/goccy/go-yaml v1.15.15
github.com/goccy/go-yaml v1.15.17
github.com/golang-jwt/jwt/v5 v5.2.1

@@ -14,6 +14,6 @@ github.com/google/go-cmp v0.6.0

github.com/jedib0t/go-pretty/v6 v6.6.5
github.com/lmittmann/tint v1.0.6
github.com/lmittmann/tint v1.0.7
github.com/mattn/go-colorable v0.1.14
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/pflag v1.0.6
github.com/spf13/viper v1.19.0

@@ -27,6 +27,7 @@ github.com/stackitcloud/stackit-sdk-go/core v0.15.1

github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.17.0
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.1
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.12.0
github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.1
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.10.1
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.5.0
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.3.0
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0

@@ -37,6 +38,6 @@ github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.4.0

github.com/zalando/go-keyring v0.2.6
golang.org/x/mod v0.22.0
golang.org/x/oauth2 v0.25.0
golang.org/x/term v0.28.0
golang.org/x/text v0.21.0
golang.org/x/mod v0.23.0
golang.org/x/oauth2 v0.26.0
golang.org/x/term v0.29.0
golang.org/x/text v0.22.0
k8s.io/apimachinery v0.31.3

@@ -95,3 +96,3 @@ k8s.io/client-go v0.31.3

golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

@@ -98,0 +99,0 @@ gopkg.in/yaml.v2 v2.4.0 // indirect

+20
-17

@@ -29,4 +29,4 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=

github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/goccy/go-yaml v1.15.15 h1:5turdzAlutS2Q7/QR/9R99Z1K0J00qDb4T0pHJcZ5ew=
github.com/goccy/go-yaml v1.15.15/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.15.17 h1:dK4FbbTTEOZTLH/NW3/xBqg0JdC14YKVmYwS9GT3H60=
github.com/goccy/go-yaml v1.15.17/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=

@@ -72,4 +72,4 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=

github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc=
github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=

@@ -118,4 +118,5 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=

github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=

@@ -151,4 +152,4 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=

github.com/stackitcloud/stackit-sdk-go/services/redis v0.20.1/go.mod h1:YJdkyuY7aK/clfE3lQDz7O369JLPcg0FO4yfCIPNUNE=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.1 h1:bICGCqRsGEzqidVCgQIH3hxB+SX1vJapZgrSP5nhvBo=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.11.1/go.mod h1:9Om4A5FI/wXZE/8zu5wF8eRBb70VddyPfnj/nlYXHX0=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.12.0 h1:uYWg/wovldLquCsHsdJUWyUF3Z1Yz4rR64WPfcfudmg=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.12.0/go.mod h1:hLc1YDjcwnkkQntTMQ5pk1ZQyX6cuBZoaEe4lr8sppI=
github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.1 h1:qAKT20siGhkIIg4gY0JBPD7TU+I/6UieYcivGU7hVKc=

@@ -160,2 +161,4 @@ github.com/stackitcloud/stackit-sdk-go/services/runcommand v0.2.1/go.mod h1:LgCIIj7jA2lWX4DI3bxUYD+m0TbWCr1VgAyBYNJeghc=

github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.5.0/go.mod h1:thrCIDBjEHAcihjWUOMJ5mbYVhOWfS/bLTBJ+yl5W4g=
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.3.0 h1:x39lier26DANhGoo3Ko9Ta7ZeXwhW97fD6zPFX6xvNc=
github.com/stackitcloud/stackit-sdk-go/services/serverupdate v0.3.0/go.mod h1:m/lZ3XH3wdKkkFb6vHxi0MhhMdRL2uAuCATeYChh+s0=
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0 h1:yf9BxAZEG2hdaekWxaNt2BOX/4qmGkl0d268ggR+tCU=

@@ -197,4 +200,4 @@ github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0/go.mod h1:Wpqj80yGruCNYGr2yxqhRaLLj4gPSkhJqZyWRXUh/QU=

golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

@@ -206,4 +209,4 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

@@ -216,10 +219,10 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=

@@ -226,0 +229,0 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=

@@ -16,2 +16,3 @@ package list

"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"

@@ -81,3 +82,8 @@ "github.com/stackitcloud/stackit-sdk-go/services/iaas"

if resp.Items == nil || len(*resp.Items) == 0 {
p.Info("No network interfaces found for network %d\n", model.NetworkId)
networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, *model.NetworkId)
if err != nil {
p.Debug(print.ErrorLevel, "get network name: %v", err)
networkLabel = *model.NetworkId
}
p.Info("No network interfaces found for network %q\n", networkLabel)
return nil

@@ -84,0 +90,0 @@ }

@@ -13,3 +13,5 @@ package server

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/log"
machinetype "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/machine-type"
networkinterface "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/network-interface"
osUpdate "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/os-update"
publicip "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/public-ip"

@@ -66,2 +68,4 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/reboot"

cmd.AddCommand(unrescue.NewCmd(p))
cmd.AddCommand(osUpdate.NewCmd(p))
cmd.AddCommand(machinetype.NewCmd(p))
}

@@ -7,2 +7,3 @@ package describe

"fmt"
"strings"

@@ -18,2 +19,3 @@ "github.com/goccy/go-yaml"

"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"

@@ -126,9 +128,18 @@

table.AddSeparator()
table.AddRow("DESCRIPTION", *performanceClass.Description)
table.AddRow("DESCRIPTION", utils.PtrString(performanceClass.Description))
table.AddSeparator()
table.AddRow("IOPS", *performanceClass.Iops)
table.AddRow("IOPS", utils.PtrString(performanceClass.Iops))
table.AddSeparator()
table.AddRow("THROUGHPUT", *performanceClass.Throughput)
table.AddRow("THROUGHPUT", utils.PtrString(performanceClass.Throughput))
table.AddSeparator()
if performanceClass.Labels != nil && len(*performanceClass.Labels) > 0 {
labels := []string{}
for key, value := range *performanceClass.Labels {
labels = append(labels, fmt.Sprintf("%s: %s", key, value))
}
table.AddRow("LABELS", strings.Join(labels, "\n"))
table.AddSeparator()
}
err := table.Display(p)

@@ -135,0 +146,0 @@ if err != nil {

@@ -40,2 +40,3 @@ package set

serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint"
serverOsUpdateCustomEndpointFlag = "server-osupdate-custom-endpoint"
runCommandCustomEndpointFlag = "runcommand-custom-endpoint"

@@ -155,2 +156,3 @@ serviceAccountCustomEndpointFlag = "service-account-custom-endpoint"

cmd.Flags().String(serverBackupCustomEndpointFlag, "", "Server Backup API base URL, used in calls to this API")
cmd.Flags().String(serverOsUpdateCustomEndpointFlag, "", "Server Update Management API base URL, used in calls to this API")
cmd.Flags().String(runCommandCustomEndpointFlag, "", "Run Command API base URL, used in calls to this API")

@@ -201,2 +203,4 @@ cmd.Flags().String(skeCustomEndpointFlag, "", "SKE API base URL, used in calls to this API")

cobra.CheckErr(err)
err = viper.BindPFlag(config.ServerOsUpdateCustomEndpointKey, cmd.Flags().Lookup(serverOsUpdateCustomEndpointFlag))
cobra.CheckErr(err)
err = viper.BindPFlag(config.RunCommandCustomEndpointKey, cmd.Flags().Lookup(runCommandCustomEndpointFlag))

@@ -203,0 +207,0 @@ cobra.CheckErr(err)

@@ -38,2 +38,3 @@ package unset

serverBackupCustomEndpointFlag: true,
serverOsUpdateCustomEndpointFlag: true,
runCommandCustomEndpointFlag: true,

@@ -77,2 +78,3 @@ skeCustomEndpointFlag: true,

ServerBackupCustomEndpoint: true,
ServerOsUpdateCustomEndpoint: true,
RunCommandCustomEndpoint: true,

@@ -132,2 +134,3 @@ SKECustomEndpoint: true,

model.ServerBackupCustomEndpoint = false
model.ServerOsUpdateCustomEndpoint = false
model.RunCommandCustomEndpoint = false

@@ -261,2 +264,12 @@ model.SKECustomEndpoint = false

{
description: "serverosupdate custom endpoint empty",
flagValues: fixtureFlagValues(func(flagValues map[string]bool) {
flagValues[serverOsUpdateCustomEndpointFlag] = false
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.ServerOsUpdateCustomEndpoint = false
}),
},
{
description: "runcommand custom endpoint empty",

@@ -263,0 +276,0 @@ flagValues: fixtureFlagValues(func(flagValues map[string]bool) {

@@ -46,2 +46,3 @@ package unset

serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint"
serverOsUpdateCustomEndpointFlag = "server-osupdate-custom-endpoint"
runCommandCustomEndpointFlag = "runcommand-custom-endpoint"

@@ -81,2 +82,3 @@ skeCustomEndpointFlag = "ske-custom-endpoint"

ServerBackupCustomEndpoint bool
ServerOsUpdateCustomEndpoint bool
RunCommandCustomEndpoint bool

@@ -191,2 +193,5 @@ ServiceAccountCustomEndpoint bool

}
if model.ServerOsUpdateCustomEndpoint {
viper.Set(config.ServerOsUpdateCustomEndpointKey, "")
}
if model.RunCommandCustomEndpoint {

@@ -248,2 +253,3 @@ viper.Set(config.RunCommandCustomEndpointKey, "")

cmd.Flags().Bool(serverBackupCustomEndpointFlag, false, "Server Backup base URL. If unset, uses the default base URL")
cmd.Flags().Bool(serverOsUpdateCustomEndpointFlag, false, "Server Update Management base URL. If unset, uses the default base URL")
cmd.Flags().Bool(runCommandCustomEndpointFlag, false, "Server Command base URL. If unset, uses the default base URL")

@@ -286,2 +292,3 @@ cmd.Flags().Bool(skeCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL")

ServerBackupCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverBackupCustomEndpointFlag),
ServerOsUpdateCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverOsUpdateCustomEndpointFlag),
RunCommandCustomEndpoint: flags.FlagToBoolValue(p, cmd, runCommandCustomEndpointFlag),

@@ -288,0 +295,0 @@ SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag),

@@ -5,2 +5,4 @@ package create

"context"
"fmt"
"strings"
"testing"

@@ -27,2 +29,8 @@

var recordTxtOver255Char = []string{
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo",
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo",
"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar",
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {

@@ -81,3 +89,3 @@ flagValues := map[string]string{

func TestParseInput(t *testing.T) {
tests := []struct {
var tests = []struct {
description string

@@ -242,4 +250,23 @@ flagValues map[string]string

},
{
description: "TXT record with > 255 characters",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[typeFlag] = txtType
flagValues[recordFlag] = strings.Join(recordTxtOver255Char, "")
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
var content string
for idx, val := range recordTxtOver255Char {
content += fmt.Sprintf("%q", val)
if idx != len(recordTxtOver255Char)-1 {
content += " "
}
}
model.Records = []string{content}
model.Type = txtType
}),
},
}
for _, tt := range tests {

@@ -246,0 +273,0 @@ t.Run(tt.description, func(t *testing.T) {

@@ -9,2 +9,3 @@ package create

"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"

@@ -20,4 +21,2 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/errors"

"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/dns"

@@ -36,2 +35,3 @@ "github.com/stackitcloud/stackit-sdk-go/services/dns/wait"

defaultType = "A"
txtType = "TXT"
)

@@ -143,2 +143,16 @@

if model.Type == txtType {
for idx := range model.Records {
// Based on RFC 1035 section 2.3.4, TXT Records are limited to 255 Characters
// Longer strings need to be split into multiple records
if len(model.Records[idx]) > 255 {
var err error
model.Records[idx], err = dnsUtils.FormatTxtRecord(model.Records[idx])
if err != nil {
return nil, err
}
}
}
}
if p.IsVerbosityDebug() {

@@ -145,0 +159,0 @@ modelStr, err := print.BuildDebugStrFromInputModel(model)

@@ -27,2 +27,9 @@ package update

var (
text255Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
text256Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob"
result256Characters = "\"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo\" \"b\""
text4050Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
)
func fixtureArgValues(mods ...func(argValues []string)) []string {

@@ -82,6 +89,7 @@ argValues := []string{

})
req := &request
for _, mod := range mods {
mod(&request)
mod(req)
}
return request
return *req
}

@@ -311,2 +319,67 @@

func TestParseTxtRecord(t *testing.T) {
tests := []struct {
description string
records *[]string
expectedResult *[]string
isValid bool
shouldErr bool
}{
{
description: "empty",
records: nil,
expectedResult: nil,
isValid: true,
},
{
description: "base",
records: &[]string{"foobar"},
expectedResult: &[]string{"foobar"},
isValid: true,
},
{
description: "input has length of 255 characters and should not split",
records: &[]string{text255Characters},
expectedResult: &[]string{text255Characters},
isValid: true,
},
{
description: "input has length 256 characters and should split",
records: &[]string{text256Characters},
expectedResult: &[]string{result256Characters},
isValid: true,
},
{
description: "input has length 4050 characters and should fail",
records: &[]string{text4050Characters},
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := parseTxtRecord(tt.records)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("should not fail but got error: %v", err)
return
}
if err == nil && !tt.isValid {
t.Fatalf("should fail but got none")
return
}
if !tt.isValid {
t.Fatalf("should fail but got none")
return
}
diff := cmp.Diff(tt.expectedResult, tt.records)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {

@@ -313,0 +386,0 @@ tests := []struct {

@@ -31,2 +31,3 @@ package update

ttlFlag = "ttl"
txtType = "TXT"
)

@@ -42,2 +43,3 @@

TTL *int64
Type *string
}

@@ -81,2 +83,15 @@

typeLabel, err := dnsUtils.GetRecordSetType(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId)
if err != nil {
p.Debug(print.ErrorLevel, "get record set type: %v", err)
}
model.Type = typeLabel
if utils.PtrString(model.Type) == txtType {
err = parseTxtRecord(model.Records)
if err != nil {
return err
}
}
if !model.AssumeYes {

@@ -171,2 +186,23 @@ prompt := fmt.Sprintf("Are you sure you want to update record set %s of zone %s?", recordSetLabel, zoneLabel)

func parseTxtRecord(records *[]string) error {
if records == nil {
return nil
}
if len(*records) == 0 {
return nil
}
for idx := range *records {
var err error
// Based on RFC 1035 section 2.3.4, TXT Records are limited to 255 Characters.
// Longer strings need to be split into multiple records
(*records)[idx], err = dnsUtils.FormatTxtRecord((*records)[idx])
if err != nil {
return err
}
}
return nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiPartialUpdateRecordSetRequest {

@@ -173,0 +209,0 @@ var records *[]dns.RecordPayload = nil

@@ -42,2 +42,3 @@ package config

ServerBackupCustomEndpointKey = "serverbackup_custom_endpoint"
ServerOsUpdateCustomEndpointKey = "serverosupdate_custom_endpoint"
RunCommandCustomEndpointKey = "runcommand_custom_endpoint"

@@ -100,2 +101,3 @@ SKECustomEndpointKey = "ske_custom_endpoint"

ServerBackupCustomEndpointKey,
ServerOsUpdateCustomEndpointKey,
RunCommandCustomEndpointKey,

@@ -183,2 +185,3 @@ SKECustomEndpointKey,

viper.SetDefault(ServerBackupCustomEndpointKey, "")
viper.SetDefault(ServerOsUpdateCustomEndpointKey, "")
viper.SetDefault(RunCommandCustomEndpointKey, "")

@@ -185,0 +188,0 @@ viper.SetDefault(SKECustomEndpointKey, "")

@@ -8,5 +8,4 @@ package utils

"github.com/google/uuid"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/dns"

@@ -19,2 +18,7 @@ )

testRecordSetId = uuid.NewString()
text255Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
text256Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob"
result256Characters = "\"foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo\" \"b\""
text4050Characters = "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoofoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo"
)

@@ -25,2 +29,3 @@

testRecordSetName = "record-set"
testRecordSetType = "A"
)

@@ -148,1 +153,116 @@

}
func TestGetRecordSetType(t *testing.T) {
tests := []struct {
description string
getRecordSetFails bool
getRecordSetResp *dns.RecordSetResponse
isValid bool
expectedOutput string
}{
{
description: "base",
getRecordSetResp: &dns.RecordSetResponse{
Rrset: &dns.RecordSet{
Name: utils.Ptr(testRecordSetType),
},
},
isValid: true,
expectedOutput: testRecordSetType,
},
{
description: "get record set fails",
getRecordSetFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &dnsClientMocked{
getRecordSetFails: tt.getRecordSetFails,
getRecordSetResp: tt.getRecordSetResp,
}
output, err := GetRecordSetName(context.Background(), client, testProjectId, testZoneId, testRecordSetId)
if tt.isValid && err != nil {
t.Errorf("failed on valid input")
}
if !tt.isValid && err == nil {
t.Errorf("did not fail on invalid input")
}
if !tt.isValid {
return
}
if output != tt.expectedOutput {
t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output)
}
})
}
}
func TestFormatTxtRecord(t *testing.T) {
tests := []struct {
description string
input string
expected string
isValid bool
}{
{
description: "base",
input: "foobar",
expected: "foobar",
isValid: true,
},
{
description: "empty",
input: "",
expected: "",
isValid: true,
},
{
description: "255 characters",
input: text255Characters,
expected: text255Characters,
isValid: true,
},
{
description: "256 characters",
input: text256Characters,
expected: result256Characters,
isValid: true,
},
{
description: "> 4049 characters should throw error",
input: text4050Characters,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
result, err := FormatTxtRecord(tt.input)
if err != nil {
if !tt.isValid {
return
}
t.Errorf("failed on valid input, got %v", err)
return
}
if err == nil && !tt.isValid {
t.Errorf("did not fail on invalid input")
return
}
if !tt.isValid {
t.Errorf("did not fail on invalid input")
return
}
if result != tt.expected {
t.Errorf("expected result to be %s, got %s", tt.expected, result)
}
})
}
}

@@ -6,3 +6,5 @@ package utils

"fmt"
"math"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/dns"

@@ -31,1 +33,35 @@ )

}
func GetRecordSetType(ctx context.Context, apiClient DNSClient, projectId, zoneId, recordSetId string) (*string, error) {
resp, err := apiClient.GetRecordSetExecute(ctx, projectId, zoneId, recordSetId)
if err != nil {
return utils.Ptr(""), fmt.Errorf("get DNS recordset: %w", err)
}
return resp.Rrset.Type, nil
}
func FormatTxtRecord(input string) (string, error) {
length := float64(len(input))
if length <= 255 {
return input, nil
}
// Max length with quotes and white spaces is 4096. Without the quotes and white spaces the max length is 4049
if length > 4049 {
return "", fmt.Errorf("max input length is 4049. The length of the input is %v", length)
}
result := ""
chunks := int(math.Ceil(length / 255))
for i := range chunks {
skip := 255 * i
if i == chunks-1 {
// Append the left record content
result += fmt.Sprintf("%q", input[0+skip:])
} else {
// Add 255 characters of the record data quoted to the result
result += fmt.Sprintf("%q ", input[0+skip:255+skip])
}
}
return result, nil
}