You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

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

Package Overview
Dependencies
Versions
174
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.8.0
to
v0.8.1
+40
docs/stackit_beta_server_backup_disable.md
## stackit beta server backup disable
Disables Server Backup service
### Synopsis
Disables Server Backup service.
```
stackit beta server backup disable [flags]
```
### Examples
```
Disable Server Backup functionality for your server.
$ stackit beta server backup disable --server-id=zzz
```
### Options
```
-h, --help Help for "stackit beta server backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup
## stackit beta server backup enable
Enables Server Backup service
### Synopsis
Enables Server Backup service.
```
stackit beta server backup enable [flags]
```
### Examples
```
Enable Server Backup functionality for your server
$ stackit beta server backup enable --server-id=zzz
```
### Options
```
-h, --help Help for "stackit beta server backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup
## stackit beta server backup schedule create
Creates a Server Backup Schedule
### Synopsis
Creates a Server Backup Schedule.
```
stackit beta server backup schedule create [flags]
```
### Examples
```
Create a Server Backup Schedule with name "myschedule" and backup name "mybackup"
$ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule
Create a Server Backup Schedule with name "myschedule", backup name "mybackup" and retention period of 5 days
$ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule --backup-retention-period=5
```
### Options
```
-b, --backup-name string Backup name
-d, --backup-retention-period int Backup retention period (in days) (default 14)
-n, --backup-schedule-name string Backup schedule name
-i, --backup-volume-ids string Backup volume ids, as comma separated UUID values.
-e, --enabled Is the server backup schedule enabled (default true)
-h, --help Help for "stackit beta server backup schedule create"
-r, --rrule string Backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule
## stackit beta server backup schedule delete
Deletes a Server Backup Schedule
### Synopsis
Deletes a Server Backup Schedule.
```
stackit beta server backup schedule delete SCHEDULE_ID [flags]
```
### Examples
```
Delete a Server Backup Schedule with ID "xxx" for server "zzz"
$ stackit beta server backup schedule delete xxx --server-id=zzz
```
### Options
```
-h, --help Help for "stackit beta server backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule
## stackit beta server backup schedule describe
Shows details of a Server Backup Schedule
### Synopsis
Shows details of a Server Backup Schedule.
```
stackit beta server backup schedule describe BACKUP_SCHEDULE_ID [flags]
```
### Examples
```
Get details of a Server Backup Schedule with id "my-schedule-id"
$ stackit beta server backup schedule describe my-schedule-id
Get details of a Server Backup Schedule with id "my-schedule-id" in JSON format
$ stackit beta server backup schedule describe my-schedule-id --output-format json
```
### Options
```
-h, --help Help for "stackit beta server backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule
## stackit beta server backup schedule list
Lists all server backup schedules
### Synopsis
Lists all server backup schedules.
```
stackit beta server backup schedule list [flags]
```
### Examples
```
List all backup schedules for a server with ID "xxx"
$ stackit beta server backup schedule list --server-id xxx
List all backup schedules for a server with ID "xxx" in JSON format
$ stackit beta server backup schedule list --server-id xxx --output-format json
```
### Options
```
-h, --help Help for "stackit beta server backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule
## stackit beta server backup schedule update
Updates a Server Backup Schedule
### Synopsis
Updates a Server Backup Schedule.
```
stackit beta server backup schedule update SCHEDULE_ID [flags]
```
### Examples
```
Update the retention period of the backup schedule "zzz" of server "xxx"
$ stackit beta server backup schedule update zzz --server-id=xxx --backup-retention-period=20
Update the backup name of the backup schedule "zzz" of server "xxx"
$ stackit beta server backup schedule update zzz --server-id=xxx --backup-name=newname
```
### Options
```
-b, --backup-name string Backup name
-d, --backup-retention-period int Backup retention period (in days) (default 14)
-n, --backup-schedule-name string Backup schedule name
-i, --backup-volume-ids string Backup volume ids, as comma separated UUID values.
-e, --enabled Is the server backup schedule enabled (default true)
-h, --help Help for "stackit beta server backup schedule update"
-r, --rrule string Backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule
## stackit beta server backup schedule
Provides functionality for Server Backup Schedule
### Synopsis
Provides functionality for Server Backup Schedule.
```
stackit beta server backup schedule [flags]
```
### Options
```
-h, --help Help for "stackit beta server backup 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup
* [stackit beta server backup schedule create](./stackit_beta_server_backup_schedule_create.md) - Creates a Server Backup Schedule
* [stackit beta server backup schedule delete](./stackit_beta_server_backup_schedule_delete.md) - Deletes a Server Backup Schedule
* [stackit beta server backup schedule describe](./stackit_beta_server_backup_schedule_describe.md) - Shows details of a Server Backup Schedule
* [stackit beta server backup schedule list](./stackit_beta_server_backup_schedule_list.md) - Lists all server backup schedules
* [stackit beta server backup schedule update](./stackit_beta_server_backup_schedule_update.md) - Updates a Server Backup Schedule
## stackit beta server backup
Provides functionality for Server Backup
### Synopsis
Provides functionality for Server Backup.
```
stackit beta server backup [flags]
```
### Options
```
-h, --help Help for "stackit beta server backup"
```
### 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
--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 Server
* [stackit beta server backup disable](./stackit_beta_server_backup_disable.md) - Disables Server Backup service
* [stackit beta server backup enable](./stackit_beta_server_backup_enable.md) - Enables Server Backup service
* [stackit beta server backup schedule](./stackit_beta_server_backup_schedule.md) - Provides functionality for Server Backup Schedule
## stackit beta server
Provides functionality for Server
### Synopsis
Provides functionality for Server.
```
stackit beta server [flags]
```
### Options
```
-h, --help Help for "stackit beta server"
```
### 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands
* [stackit beta server backup](./stackit_beta_server_backup.md) - Provides functionality for Server Backup
## stackit beta sqlserverflex database create
Creates a SQLServer Flex database
### Synopsis
Creates a SQLServer Flex database.
This operation cannot be triggered asynchronously (the "--async" flag will have no effect).
```
stackit beta sqlserverflex database create DATABASE_NAME [flags]
```
### Examples
```
Create a SQLServer Flex database with name "my-database" on instance with ID "xxx"
$ stackit beta sqlserverflex database create my-database --instance-id xxx --owner some-username
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex database create"
--instance-id string SQLServer Flex instance ID
--owner string Username of the owner user
```
### 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases
## stackit beta sqlserverflex database delete
Deletes a SQLServer Flex database
### Synopsis
Deletes a SQLServer Flex database.
This operation cannot be triggered asynchronously (the "--async" flag will have no effect).
```
stackit beta sqlserverflex database delete DATABASE_NAME [flags]
```
### Examples
```
Delete a SQLServer Flex database with name "my-database" of instance with ID "xxx"
$ stackit beta sqlserverflex database delete my-database --instance-id xxx
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex database delete"
--instance-id string SQLServer Flex instance 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases
## stackit beta sqlserverflex database
Provides functionality for SQLServer Flex databases
### Synopsis
Provides functionality for SQLServer Flex databases.
```
stackit beta sqlserverflex database [flags]
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex database"
```
### 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex
* [stackit beta sqlserverflex database create](./stackit_beta_sqlserverflex_database_create.md) - Creates a SQLServer Flex database
* [stackit beta sqlserverflex database delete](./stackit_beta_sqlserverflex_database_delete.md) - Deletes a SQLServer Flex database
## stackit beta sqlserverflex user create
Creates a SQLServer Flex user
### Synopsis
Creates a SQLServer Flex user for an instance.
The password is only visible upon creation and cannot be retrieved later.
Alternatively, you can reset the password and access the new one by running:
$ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID
Please refer to https://docs.stackit.cloud/stackit/en/creating-logins-and-users-in-sqlserver-flex-instances-210862358.html for additional information.
```
stackit beta sqlserverflex user create [flags]
```
### Examples
```
Create a SQLServer Flex user for instance with ID "xxx" and specify the username, role and database
$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles my-role --database my-database
Create a SQLServer Flex user for instance with ID "xxx", specifying multiple roles
$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2"
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex user create"
--instance-id string ID of the instance
--roles strings Roles of the user
--username string Username of the user
```
### 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users
## stackit beta sqlserverflex user delete
Deletes a SQLServer Flex user
### Synopsis
Deletes a SQLServer Flex user by ID. You can get the IDs of users for an instance by running:
$ stackit beta sqlserverflex user list --instance-id <INSTANCE_ID>
```
stackit beta sqlserverflex user delete USER_ID [flags]
```
### Examples
```
Delete a SQLServer Flex user with ID "xxx" for instance with ID "yyy"
$ stackit beta sqlserverflex user delete xxx --instance-id yyy
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex user delete"
--instance-id string Instance 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users
## stackit beta sqlserverflex user describe
Shows details of a SQLServer Flex user
### Synopsis
Shows details of a SQLServer Flex user.
The user password is only visible upon creation. You can reset it by running:
$ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID
```
stackit beta sqlserverflex user describe USER_ID [flags]
```
### Examples
```
Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy"
$ stackit beta sqlserverflex user describe xxx --instance-id yyy
Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy" in JSON format
$ stackit beta sqlserverflex user describe xxx --instance-id yyy --output-format json
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex user describe"
--instance-id string ID of the instance
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users
## stackit beta sqlserverflex user list
Lists all SQLServer Flex users of an instance
### Synopsis
Lists all SQLServer Flex users of an instance.
```
stackit beta sqlserverflex user list [flags]
```
### Examples
```
List all SQLServer Flex users of instance with ID "xxx"
$ stackit beta sqlserverflex user list --instance-id xxx
List all SQLServer Flex users of instance with ID "xxx" in JSON format
$ stackit beta sqlserverflex user list --instance-id xxx --output-format json
List up to 10 SQLServer Flex users of instance with ID "xxx"
$ stackit beta sqlserverflex user list --instance-id xxx --limit 10
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex user list"
--instance-id string Instance ID
--limit int Maximum number of entries to list
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users
## stackit beta sqlserverflex user reset-password
Resets the password of a SQLServer Flex user
### Synopsis
Resets the password of a SQLServer Flex user.
The new password is visible after resetting and cannot be retrieved later.
```
stackit beta sqlserverflex user reset-password USER_ID [flags]
```
### Examples
```
Reset the password of a SQLServer Flex user with ID "xxx" of instance with ID "yyy"
$ stackit beta sqlserverflex user reset-password xxx --instance-id yyy
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex user reset-password"
--instance-id string ID of the instance
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users
## stackit beta sqlserverflex user
Provides functionality for SQLServer Flex users
### Synopsis
Provides functionality for SQLServer Flex users.
```
stackit beta sqlserverflex user [flags]
```
### Options
```
-h, --help Help for "stackit beta sqlserverflex user"
```
### 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
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex
* [stackit beta sqlserverflex user create](./stackit_beta_sqlserverflex_user_create.md) - Creates a SQLServer Flex user
* [stackit beta sqlserverflex user delete](./stackit_beta_sqlserverflex_user_delete.md) - Deletes a SQLServer Flex user
* [stackit beta sqlserverflex user describe](./stackit_beta_sqlserverflex_user_describe.md) - Shows details of a SQLServer Flex user
* [stackit beta sqlserverflex user list](./stackit_beta_sqlserverflex_user_list.md) - Lists all SQLServer Flex users of an instance
* [stackit beta sqlserverflex user reset-password](./stackit_beta_sqlserverflex_user_reset-password.md) - Resets the password of a SQLServer Flex user
package backup
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/disable"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/enable"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/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: "backup",
Short: "Provides functionality for Server Backup",
Long: "Provides functionality for Server Backup.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(enable.NewCmd(p))
cmd.AddCommand(disable.NewCmd(p))
cmd.AddCommand(schedule.NewCmd(p))
}
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/serverbackup"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverbackup.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 *serverbackup.ApiDisableServiceRequest)) serverbackup.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 serverbackup.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/serverbackup/client"
serverbackupUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
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 Backup service",
Long: "Disables Server Backup service.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Disable Server Backup functionality for your server.`,
"$ stackit beta server backup disable --server-id=zzz"),
),
RunE: func(cmd *cobra.Command, args []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
}
canDisable, err := serverbackupUtils.CanDisableBackupService(ctx, apiClient, model.ProjectId, model.ServerId)
if err != nil {
return err
}
if !canDisable {
p.Info("Cannot disable backup service for server %s - existing backups or existing backup schedules found\n", model.ServerId)
return nil
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to disable the backup 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 backup service: %w", err)
}
p.Info("Disabled Server Backup 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 *serverbackup.APIClient) serverbackup.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/serverbackup"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverbackup.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 *serverbackup.ApiEnableServiceRequest)) serverbackup.ApiEnableServiceRequest {
request := testClient.EnableService(testCtx, testProjectId, testServerId).EnableServicePayload(serverbackup.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 serverbackup.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/serverbackup/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
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 Backup service",
Long: "Enables Server Backup service.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Enable Server Backup functionality for your server`,
"$ stackit beta server backup enable --server-id=zzz"),
),
RunE: func(cmd *cobra.Command, args []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 Backup 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 Backup: %w", err)
}
}
p.Info("Enabled backup 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 *serverbackup.APIClient) serverbackup.ApiEnableServiceRequest {
payload := serverbackup.EnableServicePayload{}
req := apiClient.EnableService(ctx, model.ProjectId, model.ServerId).EnableServicePayload(payload)
return req
}
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/serverbackup"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverbackup.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,
backupScheduleNameFlag: "example-backup-schedule-name",
enabledFlag: "true",
rruleFlag: defaultRrule,
backupNameFlag: "example-backup-name",
backupRetentionPeriodFlag: "14",
backupVolumeIdsFlag: defaultVolumeIds,
}
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,
BackupScheduleName: "example-backup-schedule-name",
Enabled: defaultEnabled,
Rrule: defaultRrule,
BackupName: "example-backup-name",
BackupRetentionPeriod: int64(14),
BackupVolumeIds: defaultVolumeIds,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverbackup.ApiCreateBackupScheduleRequest)) serverbackup.ApiCreateBackupScheduleRequest {
request := testClient.CreateBackupSchedule(testCtx, testProjectId, testServerId)
request = request.CreateBackupSchedulePayload(fixturePayload())
for _, mod := range mods {
mod(&request)
}
return request
}
func fixturePayload(mods ...func(payload *serverbackup.CreateBackupSchedulePayload)) serverbackup.CreateBackupSchedulePayload {
payload := serverbackup.CreateBackupSchedulePayload{
Name: utils.Ptr("example-backup-schedule-name"),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"),
BackupProperties: &serverbackup.BackupProperties{
Name: utils.Ptr("example-backup-name"),
RetentionPeriod: utils.Ptr(int64(14)),
VolumeIds: nil,
},
}
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, backupRetentionPeriodFlag)
}),
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 serverbackup.ApiCreateBackupScheduleRequest
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"
"strings"
"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/serverbackup/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
const (
backupScheduleNameFlag = "backup-schedule-name"
enabledFlag = "enabled"
rruleFlag = "rrule"
backupNameFlag = "backup-name"
backupVolumeIdsFlag = "backup-volume-ids"
backupRetentionPeriodFlag = "backup-retention-period"
serverIdFlag = "server-id"
defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"
defaultRetentionPeriod = 14
defaultEnabled = true
defaultVolumeIds = ""
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
BackupScheduleName string
Enabled bool
Rrule string
BackupName string
BackupRetentionPeriod int64
BackupVolumeIds string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a Server Backup Schedule",
Long: "Creates a Server Backup Schedule.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a Server Backup Schedule with name "myschedule" and backup name "mybackup"`,
`$ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule`),
examples.NewExample(
`Create a Server Backup Schedule with name "myschedule", backup name "mybackup" and retention period of 5 days`,
`$ stackit beta server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule --backup-retention-period=5`),
),
RunE: func(cmd *cobra.Command, args []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 Backup 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 Backup 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(backupScheduleNameFlag, "n", "", "Backup schedule name")
cmd.Flags().StringP(backupNameFlag, "b", "", "Backup name")
cmd.Flags().Int64P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)")
cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server backup schedule enabled")
cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "Backup RRULE (recurrence rule)")
cmd.Flags().StringP(backupVolumeIdsFlag, "i", defaultVolumeIds, "Backup volume ids, as comma separated UUID values.")
err := flags.MarkFlagsRequired(cmd, serverIdFlag, backupScheduleNameFlag, backupNameFlag)
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),
BackupRetentionPeriod: flags.FlagWithDefaultToInt64Value(p, cmd, backupRetentionPeriodFlag),
BackupScheduleName: flags.FlagToStringValue(p, cmd, backupScheduleNameFlag),
BackupName: flags.FlagToStringValue(p, cmd, backupNameFlag),
Rrule: flags.FlagWithDefaultToStringValue(p, cmd, rruleFlag),
Enabled: flags.FlagToBoolValue(p, cmd, enabledFlag),
BackupVolumeIds: flags.FlagToStringValue(p, cmd, backupVolumeIdsFlag),
}
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 *serverbackup.APIClient) (serverbackup.ApiCreateBackupScheduleRequest, error) {
req := apiClient.CreateBackupSchedule(ctx, model.ProjectId, model.ServerId)
backupProperties := serverbackup.BackupProperties{
Name: &model.BackupName,
RetentionPeriod: &model.BackupRetentionPeriod,
}
if model.BackupVolumeIds == "" {
backupProperties.VolumeIds = nil
} else {
ids := strings.Split(model.BackupVolumeIds, ",")
backupProperties.VolumeIds = &ids
}
req = req.CreateBackupSchedulePayload(serverbackup.CreateBackupSchedulePayload{
Enabled: &model.Enabled,
Name: &model.BackupScheduleName,
Rrule: &model.Rrule,
BackupProperties: &backupProperties,
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *serverbackup.BackupSchedule) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal server backup 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 backup schedule: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created server backup schedule for server %s. Backup 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/serverbackup"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverbackup.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testBackupScheduleId = "5"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testBackupScheduleId,
}
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: testBackupScheduleId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverbackup.ApiDeleteBackupScheduleRequest)) serverbackup.ApiDeleteBackupScheduleRequest {
request := testClient.DeleteBackupSchedule(testCtx, testProjectId, testServerId, testBackupScheduleId)
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 serverbackup.ApiDeleteBackupScheduleRequest
}{
{
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/serverbackup/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
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 Backup Schedule",
Long: "Deletes a Server Backup Schedule.",
Args: args.SingleArg(scheduleIdArg, nil),
Example: examples.Build(
examples.NewExample(
`Delete a Server Backup Schedule with ID "xxx" for server "zzz"`,
"$ stackit beta server backup 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 backup 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 Backup Schedule: %w", err)
}
p.Info("Deleted server backup 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 *serverbackup.APIClient) serverbackup.ApiDeleteBackupScheduleRequest {
req := apiClient.DeleteBackupSchedule(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/serverbackup"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverbackup.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testBackupScheduleId = "5"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testBackupScheduleId,
}
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,
BackupScheduleId: testBackupScheduleId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *serverbackup.ApiGetBackupScheduleRequest)) serverbackup.ApiGetBackupScheduleRequest {
request := testClient.GetBackupSchedule(testCtx, testProjectId, testServerId, testBackupScheduleId)
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 serverbackup.ApiGetBackupScheduleRequest
}{
{
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"
"strings"
"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/serverbackup/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
const (
backupScheduleIdArg = "BACKUP_SCHEDULE_ID"
serverIdFlag = "server-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
BackupScheduleId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", backupScheduleIdArg),
Short: "Shows details of a Server Backup Schedule",
Long: "Shows details of a Server Backup Schedule.",
Args: args.SingleArg(backupScheduleIdArg, nil),
Example: examples.Build(
examples.NewExample(
`Get details of a Server Backup Schedule with id "my-schedule-id"`,
"$ stackit beta server backup schedule describe my-schedule-id"),
examples.NewExample(
`Get details of a Server Backup Schedule with id "my-schedule-id" in JSON format`,
"$ stackit beta server backup 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 backup 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) {
backupScheduleId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
BackupScheduleId: backupScheduleId,
}
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 *serverbackup.APIClient) serverbackup.ApiGetBackupScheduleRequest {
req := apiClient.GetBackupSchedule(ctx, model.ProjectId, model.ServerId, model.BackupScheduleId)
return req
}
func outputResult(p *print.Printer, outputFormat string, schedule *serverbackup.BackupSchedule) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(schedule, "", " ")
if err != nil {
return fmt.Errorf("marshal server backup 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 backup 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()
if schedule.BackupProperties != nil {
table.AddRow("BACKUP NAME", *schedule.BackupProperties.Name)
table.AddSeparator()
table.AddRow("BACKUP RETENTION DAYS", *schedule.BackupProperties.RetentionPeriod)
table.AddSeparator()
ids := schedule.BackupProperties.VolumeIds
if ids == nil || len(*ids) == 0 {
table.AddRow("BACKUP VOLUME IDS", "")
} else {
table.AddRow("BACKUP VOLUME IDS", strings.Join(*ids, "\n"))
}
}
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/serverbackup"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverbackup.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 *serverbackup.ApiListBackupSchedulesRequest)) serverbackup.ApiListBackupSchedulesRequest {
request := testClient.ListBackupSchedules(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 serverbackup.ApiListBackupSchedulesRequest
}{
{
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"
"strings"
"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/serverbackup/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
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 backup schedules",
Long: "Lists all server backup schedules.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`List all backup schedules for a server with ID "xxx"`,
"$ stackit beta server backup schedule list --server-id xxx"),
examples.NewExample(
`List all backup schedules for a server with ID "xxx" in JSON format`,
"$ stackit beta server backup schedule list --server-id xxx --output-format json"),
),
RunE: func(cmd *cobra.Command, args []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 backup schedules: %w", err)
}
schedules := *resp.Items
if len(schedules) == 0 {
p.Info("No backup 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 *serverbackup.APIClient) serverbackup.ApiListBackupSchedulesRequest {
req := apiClient.ListBackupSchedules(ctx, model.ProjectId, model.ServerId)
return req
}
func outputResult(p *print.Printer, outputFormat string, schedules []serverbackup.BackupSchedule) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(schedules, "", " ")
if err != nil {
return fmt.Errorf("marshal Server Backup 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 Backup Schedules list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("SCHEDULE ID", "SCHEDULE NAME", "ENABLED", "RRULE", "BACKUP NAME", "BACKUP RETENTION DAYS", "BACKUP VOLUME IDS")
for i := range schedules {
s := schedules[i]
ids := ""
if s.BackupProperties.VolumeIds != nil && len(*s.BackupProperties.VolumeIds) != 0 {
ids = strings.Join(*s.BackupProperties.VolumeIds, ",")
}
table.AddRow(*s.Id, *s.Name, *s.Enabled, *s.Rrule, *s.BackupProperties.Name, *s.BackupProperties.RetentionPeriod, ids)
}
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/backup/schedule/create"
del "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/schedule/list"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup/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 Backup Schedule",
Long: "Provides functionality for Server Backup 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/serverbackup"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &serverbackup.APIClient{}
var testProjectId = uuid.NewString()
var testServerId = uuid.NewString()
var testBackupScheduleId = "5"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testBackupScheduleId,
}
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,
backupScheduleNameFlag: "example-backup-schedule-name",
enabledFlag: "true",
rruleFlag: defaultRrule,
backupNameFlag: "example-backup-name",
backupRetentionPeriodFlag: "14",
backupVolumeIdsFlag: defaultVolumeIds,
}
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,
},
BackupScheduleId: testBackupScheduleId,
ServerId: testServerId,
BackupScheduleName: utils.Ptr("example-backup-schedule-name"),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr(defaultRrule),
BackupName: utils.Ptr("example-backup-name"),
BackupRetentionPeriod: utils.Ptr(int64(14)),
BackupVolumeIds: utils.Ptr(defaultVolumeIds),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureBackupSchedule(mods ...func(schedule *serverbackup.BackupSchedule)) *serverbackup.BackupSchedule {
id, _ := strconv.ParseInt(testBackupScheduleId, 10, 64)
schedule := &serverbackup.BackupSchedule{
Name: utils.Ptr("example-backup-schedule-name"),
Id: utils.Ptr(id),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr(defaultRrule),
BackupProperties: &serverbackup.BackupProperties{
Name: utils.Ptr("example-backup-name"),
RetentionPeriod: utils.Ptr(int64(14)),
VolumeIds: nil,
},
}
for _, mod := range mods {
mod(schedule)
}
return schedule
}
func fixturePayload(mods ...func(payload *serverbackup.UpdateBackupSchedulePayload)) serverbackup.UpdateBackupSchedulePayload {
payload := serverbackup.UpdateBackupSchedulePayload{
Name: utils.Ptr("example-backup-schedule-name"),
Enabled: utils.Ptr(defaultEnabled),
Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"),
BackupProperties: &serverbackup.BackupProperties{
Name: utils.Ptr("example-backup-name"),
RetentionPeriod: utils.Ptr(int64(14)),
VolumeIds: nil,
},
}
for _, mod := range mods {
mod(&payload)
}
return payload
}
func fixtureRequest(mods ...func(request *serverbackup.ApiUpdateBackupScheduleRequest)) serverbackup.ApiUpdateBackupScheduleRequest {
request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testServerId, testBackupScheduleId)
request = request.UpdateBackupSchedulePayload(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: "backup 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 serverbackup.ApiUpdateBackupScheduleRequest
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, *fixtureBackupSchedule())
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"
"strings"
"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/serverbackup/client"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
const (
scheduleIdArg = "SCHEDULE_ID"
backupScheduleNameFlag = "backup-schedule-name"
enabledFlag = "enabled"
rruleFlag = "rrule"
backupNameFlag = "backup-name"
backupVolumeIdsFlag = "backup-volume-ids"
backupRetentionPeriodFlag = "backup-retention-period"
serverIdFlag = "server-id"
defaultRrule = "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"
defaultRetentionPeriod = 14
defaultEnabled = true
defaultVolumeIds = ""
)
type inputModel struct {
*globalflags.GlobalFlagModel
ServerId string
BackupScheduleId string
BackupScheduleName *string
Enabled *bool
Rrule *string
BackupName *string
BackupRetentionPeriod *int64
BackupVolumeIds *string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("update %s", scheduleIdArg),
Short: "Updates a Server Backup Schedule",
Long: "Updates a Server Backup Schedule.",
Example: examples.Build(
examples.NewExample(
`Update the retention period of the backup schedule "zzz" of server "xxx"`,
"$ stackit beta server backup schedule update zzz --server-id=xxx --backup-retention-period=20"),
examples.NewExample(
`Update the backup name of the backup schedule "zzz" of server "xxx"`,
"$ stackit beta server backup schedule update zzz --server-id=xxx --backup-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
}
currentBackupSchedule, err := apiClient.GetBackupScheduleExecute(ctx, model.ProjectId, model.ServerId, model.BackupScheduleId)
if err != nil {
p.Debug(print.ErrorLevel, "get current server backup schedule: %v", err)
return err
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to update Server Backup Schedule %q?", model.BackupScheduleId)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req, err := buildRequest(ctx, model, apiClient, *currentBackupSchedule)
if err != nil {
return err
}
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("update Server Backup 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(backupScheduleNameFlag, "n", "", "Backup schedule name")
cmd.Flags().StringP(backupNameFlag, "b", "", "Backup name")
cmd.Flags().Int64P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)")
cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server backup schedule enabled")
cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "Backup RRULE (recurrence rule)")
cmd.Flags().StringP(backupVolumeIdsFlag, "i", defaultVolumeIds, "Backup volume ids, as comma separated UUID values.")
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,
BackupScheduleId: scheduleId,
ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag),
BackupRetentionPeriod: flags.FlagToInt64Pointer(p, cmd, backupRetentionPeriodFlag),
BackupScheduleName: flags.FlagToStringPointer(p, cmd, backupScheduleNameFlag),
BackupName: flags.FlagToStringPointer(p, cmd, backupNameFlag),
Rrule: flags.FlagToStringPointer(p, cmd, rruleFlag),
Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag),
BackupVolumeIds: flags.FlagToStringPointer(p, cmd, backupVolumeIdsFlag),
}
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 *serverbackup.APIClient, old serverbackup.BackupSchedule) (serverbackup.ApiUpdateBackupScheduleRequest, error) {
req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, model.ServerId, model.BackupScheduleId)
if model.BackupName != nil {
old.BackupProperties.Name = model.BackupName
}
if model.BackupRetentionPeriod != nil {
old.BackupProperties.RetentionPeriod = model.BackupRetentionPeriod
}
if model.BackupVolumeIds != nil {
if *model.BackupVolumeIds == "" {
old.BackupProperties.VolumeIds = nil
} else {
ids := strings.Split(*model.BackupVolumeIds, ",")
old.BackupProperties.VolumeIds = &ids
}
}
if model.Enabled != nil {
old.Enabled = model.Enabled
}
if model.BackupScheduleName != nil {
old.Name = model.BackupScheduleName
}
if model.Rrule != nil {
old.Rrule = model.Rrule
}
req = req.UpdateBackupSchedulePayload(serverbackup.UpdateBackupSchedulePayload{
Enabled: old.Enabled,
Name: old.Name,
Rrule: old.Rrule,
BackupProperties: old.BackupProperties,
})
return req, nil
}
func outputResult(p *print.Printer, model *inputModel, resp *serverbackup.BackupSchedule) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal update server backup 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 backup schedule: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Info("Updated server backup schedule %d\n", *resp.Id)
return nil
}
}
package server
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/backup"
"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: "server",
Short: "Provides functionality for Server",
Long: "Provides functionality for Server.",
Args: args.NoArgs,
Run: utils.CmdHelp,
}
addSubcommands(cmd, p)
return cmd
}
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(backup.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/stackitcloud/stackit-sdk-go/services/sqlserverflex"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &sqlserverflex.APIClient{}
var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()
var testDatabaseName = "my-database"
var testOwner = "owner"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testDatabaseName,
}
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,
instanceIdFlag: testInstanceId,
ownerFlag: testOwner,
}
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,
},
DatabaseName: testDatabaseName,
InstanceId: testInstanceId,
Owner: testOwner,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateDatabaseRequest)) sqlserverflex.ApiCreateDatabaseRequest {
request := testClient.CreateDatabase(testCtx, testProjectId, testInstanceId)
payload := sqlserverflex.CreateDatabasePayload{
Name: &testDatabaseName,
Options: utils.Ptr(map[string]string{
"owner": testOwner,
}),
}
request = request.CreateDatabasePayload(payload)
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "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,
},
{
description: "instance id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
}),
isValid: false,
},
{
description: "instance id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = ""
}),
isValid: false,
},
{
description: "instance id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "owner missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, ownerFlag)
}),
isValid: false,
},
{
description: "database name 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)
}
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 sqlserverflex.ApiCreateDatabaseRequest
}{
{
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 create
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/sqlserverflex/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
"github.com/spf13/cobra"
)
const (
databaseNameArg = "DATABASE_NAME"
instanceIdFlag = "instance-id"
ownerFlag = "owner"
)
type inputModel struct {
*globalflags.GlobalFlagModel
DatabaseName string
InstanceId string
Owner string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("create %s", databaseNameArg),
Short: "Creates a SQLServer Flex database",
Long: fmt.Sprintf("%s\n%s",
"Creates a SQLServer Flex database.",
`This operation cannot be triggered asynchronously (the "--async" flag will have no effect).`,
),
Args: args.SingleArg(databaseNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Create a SQLServer Flex database with name "my-database" on instance with ID "xxx"`,
"$ stackit beta sqlserverflex database create my-database --instance-id xxx --owner some-username"),
),
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 create database %q? (This cannot be undone)", model.DatabaseName)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
s := spinner.New(p)
s.Start("Creating database")
resp, err := req.Execute()
if err != nil {
s.StopWithError()
return fmt.Errorf("create SQLServer Flex database: %w", err)
}
s.Stop()
return outputResult(p, model, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "SQLServer Flex instance ID")
cmd.Flags().String(ownerFlag, "", "Username of the owner user")
err := flags.MarkFlagsRequired(cmd, instanceIdFlag, ownerFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
databaseName := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
DatabaseName: databaseName,
InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag),
Owner: flags.FlagToStringValue(p, cmd, ownerFlag),
}
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 *sqlserverflex.APIClient) sqlserverflex.ApiCreateDatabaseRequest {
req := apiClient.CreateDatabase(ctx, model.ProjectId, model.InstanceId)
payload := sqlserverflex.CreateDatabasePayload{
Name: &model.DatabaseName,
Options: utils.Ptr(map[string]string{
"owner": model.Owner,
}),
}
req = req.CreateDatabasePayload(payload)
return req
}
func outputResult(p *print.Printer, model *inputModel, resp *sqlserverflex.CreateDatabaseResponse) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal SQLServer Flex database: %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 SQLServer Flex database: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created database %q\n", model.DatabaseName)
return nil
}
}
package database
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/delete"
"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: "database",
Short: "Provides functionality for SQLServer Flex databases",
Long: "Provides functionality for SQLServer Flex databases.",
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(delete.NewCmd(p))
}
package delete
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &sqlserverflex.APIClient{}
var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()
var testDatabaseName = "my-database"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testDatabaseName,
}
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,
instanceIdFlag: testInstanceId,
}
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,
},
DatabaseName: testDatabaseName,
InstanceId: testInstanceId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *sqlserverflex.ApiDeleteDatabaseRequest)) sqlserverflex.ApiDeleteDatabaseRequest {
request := testClient.DeleteDatabase(testCtx, testProjectId, testInstanceId, testDatabaseName)
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,
},
{
description: "instance id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
}),
isValid: false,
},
{
description: "instance id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = ""
}),
isValid: false,
},
{
description: "instance id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "database name 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)
}
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 sqlserverflex.ApiDeleteDatabaseRequest
}{
{
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/sqlserverflex/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
const (
databaseNameArg = "DATABASE_NAME"
instanceIdFlag = "instance-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
DatabaseName string
InstanceId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", databaseNameArg),
Short: "Deletes a SQLServer Flex database",
Long: fmt.Sprintf("%s\n%s",
"Deletes a SQLServer Flex database.",
`This operation cannot be triggered asynchronously (the "--async" flag will have no effect).`,
),
Args: args.SingleArg(databaseNameArg, nil),
Example: examples.Build(
examples.NewExample(
`Delete a SQLServer Flex database with name "my-database" of instance with ID "xxx"`,
"$ stackit beta sqlserverflex database delete my-database --instance-id xxx"),
),
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 database %q? (This cannot be undone)", model.DatabaseName)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
s := spinner.New(p)
s.Start("Deleting database")
err = req.Execute()
if err != nil {
s.StopWithError()
return fmt.Errorf("delete SQLServer Flex database: %w", err)
}
s.Stop()
p.Info("Deleted database %q\n", model.DatabaseName)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "SQLServer Flex instance ID")
err := flags.MarkFlagsRequired(cmd, instanceIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
databaseName := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
DatabaseName: databaseName,
InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag),
}
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 *sqlserverflex.APIClient) sqlserverflex.ApiDeleteDatabaseRequest {
req := apiClient.DeleteDatabase(ctx, model.ProjectId, model.InstanceId, model.DatabaseName)
return req
}
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/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &sqlserverflex.APIClient{}
var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
instanceIdFlag: testInstanceId,
usernameFlag: "johndoe",
rolesFlag: "read",
}
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,
},
InstanceId: testInstanceId,
Username: utils.Ptr("johndoe"),
Roles: utils.Ptr([]string{"read"}),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateUserRequest)) sqlserverflex.ApiCreateUserRequest {
request := testClient.CreateUser(testCtx, testProjectId, testInstanceId)
request = request.CreateUserPayload(sqlserverflex.CreateUserPayload{
Username: utils.Ptr("johndoe"),
Roles: utils.Ptr([]sqlserverflex.Role{"read"}),
})
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 username specified",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, usernameFlag)
}),
isValid: false,
},
{
description: "no roles specified",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, rolesFlag)
}),
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Roles = nil
}),
},
{
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: "instance id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
}),
isValid: false,
},
{
description: "instance id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = ""
}),
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)
}
configureFlags(cmd)
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 sqlserverflex.ApiCreateUserRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
},
{
description: "no username specified",
model: fixtureInputModel(func(model *inputModel) {
model.Username = nil
}),
expectedRequest: fixtureRequest().CreateUserPayload(sqlserverflex.CreateUserPayload{
Roles: utils.Ptr([]sqlserverflex.Role{"read"}),
}),
},
}
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 create
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
"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/sqlserverflex/client"
sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
const (
instanceIdFlag = "instance-id"
usernameFlag = "username"
rolesFlag = "roles"
)
type inputModel struct {
*globalflags.GlobalFlagModel
InstanceId string
Username *string
Roles *[]string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Creates a SQLServer Flex user",
Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s",
"Creates a SQLServer Flex user for an instance.",
"The password is only visible upon creation and cannot be retrieved later.",
"Alternatively, you can reset the password and access the new one by running:",
" $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID",
"Please refer to https://docs.stackit.cloud/stackit/en/creating-logins-and-users-in-sqlserver-flex-instances-210862358.html for additional information.",
),
Example: examples.Build(
examples.NewExample(
`Create a SQLServer Flex user for instance with ID "xxx" and specify the username, role and database`,
"$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles my-role --database my-database"),
examples.NewExample(
`Create a SQLServer Flex user for instance with ID "xxx", specifying multiple roles`,
`$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2"`),
),
Args: args.NoArgs,
RunE: func(cmd *cobra.Command, args []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
}
instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId)
if err != nil {
p.Debug(print.ErrorLevel, "get instance name: %v", err)
instanceLabel = model.InstanceId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("create SQLServer Flex user: %w", err)
}
user := resp.Item
return outputResult(p, model, instanceLabel, user)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance")
cmd.Flags().String(usernameFlag, "", "Username of the user")
cmd.Flags().StringSlice(rolesFlag, []string{}, "Roles of the user")
err := flags.MarkFlagsRequired(cmd, instanceIdFlag, usernameFlag)
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,
InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag),
Username: flags.FlagToStringPointer(p, cmd, usernameFlag),
Roles: flags.FlagToStringSlicePointer(p, cmd, rolesFlag),
}
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 *sqlserverflex.APIClient) sqlserverflex.ApiCreateUserRequest {
req := apiClient.CreateUser(ctx, model.ProjectId, model.InstanceId)
var roles []sqlserverflex.Role
if model.Roles != nil {
for _, r := range *model.Roles {
roles = append(roles, sqlserverflex.Role(r))
}
}
req = req.CreateUserPayload(sqlserverflex.CreateUserPayload{
Username: model.Username,
Roles: &roles,
})
return req
}
func outputResult(p *print.Printer, model *inputModel, instanceLabel string, user *sqlserverflex.User) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(user, "", " ")
if err != nil {
return fmt.Errorf("marshal SQLServer Flex user: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal SQLServer Flex user: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, *user.Id)
p.Outputf("Username: %s\n", *user.Username)
p.Outputf("Password: %s\n", *user.Password)
if user.Roles != nil && len(*user.Roles) != 0 {
p.Outputf("Roles: %v\n", *user.Roles)
}
if user.Host != nil && *user.Host != "" {
p.Outputf("Host: %s\n", *user.Host)
}
if user.Port != nil {
p.Outputf("Port: %d\n", *user.Port)
}
if user.Uri != nil && *user.Uri != "" {
p.Outputf("URI: %s\n", *user.Uri)
}
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/sqlserverflex"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &sqlserverflex.APIClient{}
var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()
var testUserId = "my-user-id"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testUserId,
}
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,
instanceIdFlag: testInstanceId,
}
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,
},
InstanceId: testInstanceId,
UserId: testUserId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *sqlserverflex.ApiDeleteUserRequest)) sqlserverflex.ApiDeleteUserRequest {
request := testClient.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId)
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: "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,
},
{
description: "instance id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
}),
isValid: false,
},
{
description: "instance id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = ""
}),
isValid: false,
},
{
description: "instance id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = "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
expectedRequest sqlserverflex.ApiDeleteUserRequest
}{
{
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/sqlserverflex/client"
sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
const (
userIdArg = "USER_ID"
instanceIdFlag = "instance-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
InstanceId string
UserId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("delete %s", userIdArg),
Short: "Deletes a SQLServer Flex user",
Long: fmt.Sprintf("%s\n%s",
"Deletes a SQLServer Flex user by ID. You can get the IDs of users for an instance by running:",
" $ stackit beta sqlserverflex user list --instance-id <INSTANCE_ID>",
),
Example: examples.Build(
examples.NewExample(
`Delete a SQLServer Flex user with ID "xxx" for instance with ID "yyy"`,
"$ stackit beta sqlserverflex user delete xxx --instance-id yyy"),
),
Args: args.SingleArg(userIdArg, 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
}
instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId)
if err != nil {
p.Debug(print.ErrorLevel, "get instance name: %v", err)
instanceLabel = model.InstanceId
}
userLabel, err := sqlserverflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId)
if err != nil {
p.Debug(print.ErrorLevel, "get user name: %v", err)
userLabel = model.UserId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel)
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 SQLServer Flex user: %w", err)
}
p.Info("Deleted user %q of instance %q\n", userLabel, instanceLabel)
return nil
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID")
err := flags.MarkFlagsRequired(cmd, instanceIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
userId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag),
UserId: userId,
}
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 *sqlserverflex.APIClient) sqlserverflex.ApiDeleteUserRequest {
req := apiClient.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId)
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/sqlserverflex"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &sqlserverflex.APIClient{}
var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()
var testUserId = "my-user-id"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testUserId,
}
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,
instanceIdFlag: testInstanceId,
}
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,
},
InstanceId: testInstanceId,
UserId: testUserId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *sqlserverflex.ApiGetUserRequest)) sqlserverflex.ApiGetUserRequest {
request := testClient.GetUser(testCtx, testProjectId, testInstanceId, testUserId)
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: "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,
},
{
description: "instance id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
}),
isValid: false,
},
{
description: "instance id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = ""
}),
isValid: false,
},
{
description: "instance id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = "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
expectedRequest sqlserverflex.ApiGetUserRequest
}{
{
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"
"strings"
"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/sqlserverflex/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
const (
userIdArg = "USER_ID"
instanceIdFlag = "instance-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
InstanceId string
UserId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("describe %s", userIdArg),
Short: "Shows details of a SQLServer Flex user",
Long: fmt.Sprintf("%s\n%s\n%s",
"Shows details of a SQLServer Flex user.",
`The user password is only visible upon creation. You can reset it by running:`,
" $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID",
),
Example: examples.Build(
examples.NewExample(
`Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy"`,
"$ stackit beta sqlserverflex user describe xxx --instance-id yyy"),
examples.NewExample(
`Get details of a SQLServer Flex user with ID "xxx" of instance with ID "yyy" in JSON format`,
"$ stackit beta sqlserverflex user describe xxx --instance-id yyy --output-format json"),
),
Args: args.SingleArg(userIdArg, 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
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("get SQLServer Flex user: %w", err)
}
return outputResult(p, model.OutputFormat, *resp.Item)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance")
err := flags.MarkFlagsRequired(cmd, instanceIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
userId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag),
UserId: userId,
}
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 *sqlserverflex.APIClient) sqlserverflex.ApiGetUserRequest {
req := apiClient.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId)
return req
}
func outputResult(p *print.Printer, outputFormat string, user sqlserverflex.InstanceResponseUser) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(user, "", " ")
if err != nil {
return fmt.Errorf("marshal SQLServer Flex user: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal SQLServer Flex user: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.AddRow("ID", *user.Id)
table.AddSeparator()
table.AddRow("USERNAME", *user.Username)
if user.Roles != nil && len(*user.Roles) != 0 {
table.AddSeparator()
table.AddRow("ROLES", strings.Join(*user.Roles, "\n"))
}
if user.Database != nil && *user.Database != "" {
table.AddSeparator()
table.AddRow("DATABASE", *user.Database)
}
if user.Host != nil && *user.Host != "" {
table.AddSeparator()
table.AddRow("HOST", *user.Host)
}
if user.Port != nil {
table.AddSeparator()
table.AddRow("PORT", *user.Port)
}
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/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &sqlserverflex.APIClient{}
var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
instanceIdFlag: testInstanceId,
limitFlag: "10",
}
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,
},
InstanceId: utils.Ptr(testInstanceId),
Limit: utils.Ptr(int64(10)),
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *sqlserverflex.ApiListUsersRequest)) sqlserverflex.ApiListUsersRequest {
request := testClient.ListUsers(testCtx, testProjectId, testInstanceId)
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: "instance id missing",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
}),
isValid: false,
},
{
description: "instance id invalid 1",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = ""
}),
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) {
cmd := &cobra.Command{}
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
configureFlags(cmd)
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 sqlserverflex.ApiListUsersRequest
}{
{
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/sqlserverflex/client"
sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
const (
instanceIdFlag = "instance-id"
limitFlag = "limit"
)
type inputModel struct {
*globalflags.GlobalFlagModel
InstanceId *string
Limit *int64
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "Lists all SQLServer Flex users of an instance",
Long: "Lists all SQLServer Flex users of an instance.",
Example: examples.Build(
examples.NewExample(
`List all SQLServer Flex users of instance with ID "xxx"`,
"$ stackit beta sqlserverflex user list --instance-id xxx"),
examples.NewExample(
`List all SQLServer Flex users of instance with ID "xxx" in JSON format`,
"$ stackit beta sqlserverflex user list --instance-id xxx --output-format json"),
examples.NewExample(
`List up to 10 SQLServer Flex users of instance with ID "xxx"`,
"$ stackit beta sqlserverflex user list --instance-id xxx --limit 10"),
),
Args: args.NoArgs,
RunE: func(cmd *cobra.Command, args []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("get SQLServer Flex users: %w", err)
}
if resp.Items == nil || len(*resp.Items) == 0 {
instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId)
if err != nil {
p.Debug(print.ErrorLevel, "get instance name: %v", err)
instanceLabel = *model.InstanceId
}
p.Info("No users found for instance %q\n", instanceLabel)
return nil
}
users := *resp.Items
// Truncate output
if model.Limit != nil && len(users) > int(*model.Limit) {
users = users[:*model.Limit]
}
return outputResult(p, model.OutputFormat, users)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID")
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
err := flags.MarkFlagsRequired(cmd, instanceIdFlag)
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,
InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag),
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 *sqlserverflex.APIClient) sqlserverflex.ApiListUsersRequest {
req := apiClient.ListUsers(ctx, model.ProjectId, *model.InstanceId)
return req
}
func outputResult(p *print.Printer, outputFormat string, users []sqlserverflex.InstanceListUser) error {
switch outputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(users, "", " ")
if err != nil {
return fmt.Errorf("marshal SQLServer Flex user list: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal SQLServer Flex user list: %w", err)
}
p.Outputln(string(details))
return nil
default:
table := tables.NewTable()
table.SetHeader("ID", "USERNAME")
for i := range users {
user := users[i]
table.AddRow(*user.Id, *user.Username)
}
err := table.Display(p)
if err != nil {
return fmt.Errorf("render table: %w", err)
}
return nil
}
}
package resetpassword
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/sqlserverflex"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &sqlserverflex.APIClient{}
var testProjectId = uuid.NewString()
var testInstanceId = uuid.NewString()
var testUserId = "my-user-id"
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testUserId,
}
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,
instanceIdFlag: testInstanceId,
}
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,
},
InstanceId: testInstanceId,
UserId: testUserId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *sqlserverflex.ApiResetUserRequest)) sqlserverflex.ApiResetUserRequest {
request := testClient.ResetUser(testCtx, testProjectId, testInstanceId, testUserId)
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: "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,
},
{
description: "instance id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
}),
isValid: false,
},
{
description: "instance id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = ""
}),
isValid: false,
},
{
description: "instance id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[instanceIdFlag] = "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
expectedRequest sqlserverflex.ApiResetUserRequest
}{
{
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 resetpassword
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/sqlserverflex/client"
sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"
)
const (
userIdArg = "USER_ID"
instanceIdFlag = "instance-id"
)
type inputModel struct {
*globalflags.GlobalFlagModel
InstanceId string
UserId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("reset-password %s", userIdArg),
Short: "Resets the password of a SQLServer Flex user",
Long: fmt.Sprintf("%s\n%s",
"Resets the password of a SQLServer Flex user.",
"The new password is visible after resetting and cannot be retrieved later.",
),
Example: examples.Build(
examples.NewExample(
`Reset the password of a SQLServer Flex user with ID "xxx" of instance with ID "yyy"`,
"$ stackit beta sqlserverflex user reset-password xxx --instance-id yyy"),
),
Args: args.SingleArg(userIdArg, 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
}
instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId)
if err != nil {
p.Debug(print.ErrorLevel, "get instance name: %v", err)
instanceLabel = model.InstanceId
}
userLabel, err := sqlserverflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId)
if err != nil {
p.Debug(print.ErrorLevel, "get user name: %v", err)
userLabel = model.UserId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
user, err := req.Execute()
if err != nil {
return fmt.Errorf("reset SQLServer Flex user password: %w", err)
}
return outputResult(p, model, userLabel, instanceLabel, user.Item)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance")
err := flags.MarkFlagsRequired(cmd, instanceIdFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
userId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag),
UserId: userId,
}
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 *sqlserverflex.APIClient) sqlserverflex.ApiResetUserRequest {
req := apiClient.ResetUser(ctx, model.ProjectId, model.InstanceId, model.UserId)
return req
}
func outputResult(p *print.Printer, model *inputModel, userLabel, instanceLabel string, user *sqlserverflex.User) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(user, "", " ")
if err != nil {
return fmt.Errorf("marshal SQLServer Flex reset password: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal SQLServer Flex reset password: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Reset password for user %q of instance %q\n\n", userLabel, instanceLabel)
p.Outputf("Username: %s\n", *user.Username)
p.Outputf("New password: %s\n", *user.Password)
if user.Uri != nil && *user.Uri != "" {
p.Outputf("New URI: %s\n", *user.Uri)
}
return nil
}
}
package user
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/delete"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/describe"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/list"
resetpassword "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/reset-password"
"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: "user",
Short: "Provides functionality for SQLServer Flex users",
Long: "Provides functionality for SQLServer Flex users.",
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(delete.NewCmd(p))
cmd.AddCommand(describe.NewCmd(p))
cmd.AddCommand(list.NewCmd(p))
cmd.AddCommand(resetpassword.NewCmd(p))
}
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/serverbackup"
)
func ConfigureClient(p *print.Printer) (*serverbackup.APIClient, error) {
var err error
var apiClient *serverbackup.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.ServerBackupCustomEndpointKey)
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 = serverbackup.NewAPIClient(cfgOptions...)
if err != nil {
p.Debug(print.ErrorLevel, "create new API client: %v", err)
return nil, &errors.AuthError{}
}
return apiClient, nil
}
package utils
import (
"context"
"fmt"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
var (
testProjectId = uuid.NewString()
testServerId = uuid.NewString()
)
type serverbackupClientMocked struct {
listBackupSchedulesFails bool
listBackupSchedulesResp *serverbackup.ListBackupSchedules200Response
listBackupsFails bool
listBackupsResp *serverbackup.ListBackups200Response
}
func (m *serverbackupClientMocked) ListBackupSchedulesExecute(_ context.Context, _, _ string) (*serverbackup.ListBackupSchedules200Response, error) {
if m.listBackupSchedulesFails {
return nil, fmt.Errorf("could not list backup schedules")
}
return m.listBackupSchedulesResp, nil
}
func (m *serverbackupClientMocked) ListBackupsExecute(_ context.Context, _, _ string) (*serverbackup.ListBackups200Response, error) {
if m.listBackupsFails {
return nil, fmt.Errorf("could not list backups")
}
return m.listBackupsResp, nil
}
func TestCanDisableBackupService(t *testing.T) {
tests := []struct {
description string
listBackupsFails bool
listBackupSchedulesFails bool
listBackups *serverbackup.ListBackups200Response
listBackupSchedules *serverbackup.ListBackupSchedules200Response
isValid bool // isValid ==> err == nil
expectedOutput bool // expectedCanDisable
}{
{
description: "base-ok-can-disable-backups-service-no-backups-no-backup-schedules",
listBackupsFails: false,
listBackupSchedulesFails: false,
listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}},
listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}},
isValid: true,
expectedOutput: true,
},
{
description: "not-ok-api-error-list-backups",
listBackupsFails: true,
listBackupSchedulesFails: false,
listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}},
listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}},
isValid: false,
expectedOutput: false,
},
{
description: "not-ok-api-error-list-backup-schedules",
listBackupsFails: true,
listBackupSchedulesFails: false,
listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}},
listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}},
isValid: false,
expectedOutput: false,
},
{
description: "not-ok-has-backups-cannot-disable",
listBackupsFails: false,
listBackupSchedulesFails: false,
listBackups: &serverbackup.ListBackups200Response{
Items: &[]serverbackup.Backup{
{
CreatedAt: utils.Ptr("test timestamp"),
ExpireAt: utils.Ptr("test timestamp"),
Id: utils.Ptr("5"),
LastRestoredAt: utils.Ptr("test timestamp"),
Name: utils.Ptr("test name"),
Size: utils.Ptr(int64(5)),
Status: utils.Ptr("test status"),
VolumeBackups: nil,
},
},
},
listBackupSchedules: &serverbackup.ListBackupSchedules200Response{Items: &[]serverbackup.BackupSchedule{}},
isValid: true,
expectedOutput: false,
},
{
description: "not-ok-has-backups-schedules-cannot-disable",
listBackupsFails: false,
listBackupSchedulesFails: false,
listBackups: &serverbackup.ListBackups200Response{Items: &[]serverbackup.Backup{}},
listBackupSchedules: &serverbackup.ListBackupSchedules200Response{
Items: &[]serverbackup.BackupSchedule{
{
BackupProperties: nil,
Enabled: utils.Ptr(false),
Id: utils.Ptr(int64(5)),
Name: utils.Ptr("some name"),
Rrule: utils.Ptr("some rrule"),
},
},
},
isValid: true,
expectedOutput: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &serverbackupClientMocked{
listBackupsFails: tt.listBackupsFails,
listBackupSchedulesFails: tt.listBackupSchedulesFails,
listBackupsResp: tt.listBackups,
listBackupSchedulesResp: tt.listBackupSchedules,
}
output, err := CanDisableBackupService(context.Background(), client, testProjectId, testServerId)
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 %t, got %t", tt.expectedOutput, output)
}
})
}
}
package utils
import (
"context"
"fmt"
"github.com/stackitcloud/stackit-sdk-go/services/serverbackup"
)
type ServerBackupClient interface {
ListBackupSchedulesExecute(ctx context.Context, projectId, serverId string) (*serverbackup.ListBackupSchedules200Response, error)
ListBackupsExecute(ctx context.Context, projectId, serverId string) (*serverbackup.ListBackups200Response, error)
}
func CanDisableBackupService(ctx context.Context, client ServerBackupClient, projectId, serverId string) (bool, error) {
schedules, err := client.ListBackupSchedulesExecute(ctx, projectId, serverId)
if err != nil {
return false, fmt.Errorf("list backup schedules: %w", err)
}
if *schedules.Items != nil && len(*schedules.Items) > 0 {
return false, nil
}
backups, err := client.ListBackupsExecute(ctx, projectId, serverId)
if err != nil {
return false, fmt.Errorf("list backups: %w", err)
}
if *backups.Items != nil && len(*backups.Items) > 0 {
return false, nil
}
// no backups and no backup schedules found for this server => can disable backup service
return true, nil
}
+2
-2

@@ -32,3 +32,3 @@ # STACKIT CLI release workflow.

fetch-depth: 0
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:

@@ -66,3 +66,3 @@ go-version-file: "go.mod"

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:

@@ -69,0 +69,0 @@ args: release --clean

@@ -16,5 +16,5 @@ name: Renovate

- name: Self-hosted Renovate
uses: renovatebot/github-action@v40.1.11
uses: renovatebot/github-action@v40.1.12
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN }}

@@ -0,1 +1,3 @@

version: 2
before:

@@ -146,3 +148,2 @@ hooks:

enabled: true
draft: true
base:

@@ -149,0 +150,0 @@ owner: microsoft

## stackit beta sqlserverflex instance create
Creates an SQLServer Flex instance
Creates a SQLServer Flex instance
### Synopsis
Creates an SQLServer Flex instance.
Creates a SQLServer Flex instance.

@@ -16,9 +16,9 @@ ```

```
Create an SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values
Create a SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values
$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4
Create an SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values
Create a SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values
$ stackit beta sqlserverflex instance create --name my-instance --flavor-id xxx
Create an SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values
Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values
$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24

@@ -25,0 +25,0 @@ ```

## stackit beta sqlserverflex instance delete
Deletes an SQLServer Flex instance
Deletes a SQLServer Flex instance
### Synopsis
Deletes an SQLServer Flex instance.
Deletes a SQLServer Flex instance.

@@ -16,3 +16,3 @@ ```

```
Delete an SQLServer Flex instance with ID "xxx"
Delete a SQLServer Flex instance with ID "xxx"
$ stackit beta sqlserverflex instance delete xxx

@@ -19,0 +19,0 @@ ```

## stackit beta sqlserverflex instance describe
Shows details of an SQLServer Flex instance
Shows details of a SQLServer Flex instance
### Synopsis
Shows details of an SQLServer Flex instance.
Shows details of a SQLServer Flex instance.

@@ -16,6 +16,6 @@ ```

```
Get details of an SQLServer Flex instance with ID "xxx"
Get details of a SQLServer Flex instance with ID "xxx"
$ stackit beta sqlserverflex instance describe xxx
Get details of an SQLServer Flex instance with ID "xxx" in JSON format
Get details of a SQLServer Flex instance with ID "xxx" in JSON format
$ stackit beta sqlserverflex instance describe xxx --output-format json

@@ -22,0 +22,0 @@ ```

## stackit beta sqlserverflex instance update
Updates an SQLServer Flex instance
Updates a SQLServer Flex instance
### Synopsis
Updates an SQLServer Flex instance.
Updates a SQLServer Flex instance.

@@ -16,6 +16,6 @@ ```

```
Update the name of an SQLServer Flex instance with ID "xxx"
Update the name of a SQLServer Flex instance with ID "xxx"
$ stackit beta sqlserverflex instance update xxx --name my-new-name
Update the backup schedule of an SQLServer Flex instance with ID "xxx"
Update the backup schedule of a SQLServer Flex instance with ID "xxx"
$ stackit beta sqlserverflex instance update xxx --backup-schedule "30 0 * * *"

@@ -22,0 +22,0 @@ ```

@@ -32,7 +32,7 @@ ## stackit beta sqlserverflex instance

* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex
* [stackit beta sqlserverflex instance create](./stackit_beta_sqlserverflex_instance_create.md) - Creates an SQLServer Flex instance
* [stackit beta sqlserverflex instance delete](./stackit_beta_sqlserverflex_instance_delete.md) - Deletes an SQLServer Flex instance
* [stackit beta sqlserverflex instance describe](./stackit_beta_sqlserverflex_instance_describe.md) - Shows details of an SQLServer Flex instance
* [stackit beta sqlserverflex instance create](./stackit_beta_sqlserverflex_instance_create.md) - Creates a SQLServer Flex instance
* [stackit beta sqlserverflex instance delete](./stackit_beta_sqlserverflex_instance_delete.md) - Deletes a SQLServer Flex instance
* [stackit beta sqlserverflex instance describe](./stackit_beta_sqlserverflex_instance_describe.md) - Shows details of a SQLServer Flex instance
* [stackit beta sqlserverflex instance list](./stackit_beta_sqlserverflex_instance_list.md) - Lists all SQLServer Flex instances
* [stackit beta sqlserverflex instance update](./stackit_beta_sqlserverflex_instance_update.md) - Updates an SQLServer Flex instance
* [stackit beta sqlserverflex instance update](./stackit_beta_sqlserverflex_instance_update.md) - Updates a SQLServer Flex instance

@@ -18,9 +18,12 @@ ## stackit beta sqlserverflex options

List SQL Server Flex flavors options
$ stackit sqlserverflex options --flavors
$ stackit beta sqlserverflex options --flavors
List SQL Server Flex available versions
$ stackit sqlserverflex options --versions
$ stackit beta sqlserverflex options --versions
List SQL Server Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit sqlserverflex options --flavors"
$ stackit sqlserverflex options --storages --flavor-id <FLAVOR_ID>
List SQL Server Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit beta sqlserverflex options --flavors"
$ stackit beta sqlserverflex options --storages --flavor-id <FLAVOR_ID>
List SQL Server Flex user roles and database compatibilities for a given instance. The IDs of existing instances can be obtained by running "$ stackit beta sqlserverflex instance list"
$ stackit beta sqlserverflex options --user-roles --db-compatibilities --instance-id <INSTANCE_ID>
```

@@ -31,7 +34,11 @@

```
--flavor-id string The flavor ID to show storages for. Only relevant when "--storages" is passed
--flavors Lists supported flavors
-h, --help Help for "stackit beta sqlserverflex options"
--storages Lists supported storages for a given flavor
--versions Lists supported versions
--db-collations Lists supported database collations for a given instance
--db-compatibilities Lists supported database compatibilities for a given instance
--flavor-id string The flavor ID to show storages for. Only relevant when "--storages" is passed
--flavors Lists supported flavors
-h, --help Help for "stackit beta sqlserverflex options"
--instance-id string The instance ID to show user roles, database collations and database compatibilities for. Only relevant when "--user-roles", "--db-collations" or "--db-compatibilities" is passed
--storages Lists supported storages for a given flavor
--user-roles Lists supported user roles for a given instance
--versions Lists supported versions
```

@@ -38,0 +45,0 @@

@@ -32,4 +32,6 @@ ## stackit beta sqlserverflex

* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands
* [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases
* [stackit beta sqlserverflex instance](./stackit_beta_sqlserverflex_instance.md) - Provides functionality for SQLServer Flex instances
* [stackit beta sqlserverflex options](./stackit_beta_sqlserverflex_options.md) - Lists SQL Server Flex options
* [stackit beta sqlserverflex user](./stackit_beta_sqlserverflex_user.md) - Provides functionality for SQLServer Flex users

@@ -43,3 +43,4 @@ ## stackit beta

* [stackit](./stackit.md) - Manage STACKIT resources using the command line
* [stackit beta server](./stackit_beta_server.md) - Provides functionality for Server
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex

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

--secrets-manager-custom-endpoint string Secrets Manager API base URL, used in calls to this API
--serverbackup-custom-endpoint string Server Backup API base URL, used in calls to this API
--service-account-custom-endpoint string Service Account API base URL, used in calls to this API

@@ -49,0 +50,0 @@ --session-time-limit string Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect)

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

--secrets-manager-custom-endpoint Secrets Manager API base URL. If unset, uses the default base URL
--serverbackup-custom-endpoint Server Backup base URL. If unset, uses the default base URL
--service-account-custom-endpoint SKE API base URL. If unset, uses the default base URL

@@ -49,0 +50,0 @@ --session-time-limit Maximum time before authentication is required again. If unset, defaults to 2h

@@ -19,6 +19,6 @@ ## stackit mongodbflex user describe

Get details of a MongoDB Flex user with ID "xxx" of instance with ID "yyy"
$ stackit mongodbflex user list xxx --instance-id yyy
$ stackit mongodbflex user describe xxx --instance-id yyy
Get details of a MongoDB Flex user with ID "xxx" of instance with ID "yyy" in JSON format
$ stackit mongodbflex user list xxx --instance-id yyy --output-format json
$ stackit mongodbflex user describe xxx --instance-id yyy --output-format json
```

@@ -25,0 +25,0 @@

@@ -19,6 +19,6 @@ ## stackit postgresflex user describe

Get details of a PostgreSQL Flex user with ID "xxx" of instance with ID "yyy"
$ stackit postgresflex user list xxx --instance-id yyy
$ stackit postgresflex user describe xxx --instance-id yyy
Get details of a PostgreSQL Flex user with ID "xxx" of instance with ID "yyy" in JSON format
$ stackit postgresflex user list xxx --instance-id yyy --output-format json
$ stackit postgresflex user describe xxx --instance-id yyy --output-format json
```

@@ -25,0 +25,0 @@

+14
-13

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

github.com/mattn/go-colorable v0.1.13
github.com/spf13/cobra v1.8.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5

@@ -24,13 +24,14 @@ github.com/spf13/viper v1.19.0

github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.14.0
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.8.0
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.15.0
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0
github.com/stackitcloud/stackit-sdk-go/services/ske v0.16.0
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.2.0
github.com/zalando/go-keyring v0.2.4
golang.org/x/mod v0.17.0
golang.org/x/oauth2 v0.20.0
golang.org/x/term v0.20.0
golang.org/x/text v0.15.0
github.com/zalando/go-keyring v0.2.5
golang.org/x/mod v0.18.0
golang.org/x/oauth2 v0.21.0
golang.org/x/term v0.21.0
golang.org/x/text v0.16.0
k8s.io/apimachinery v0.29.2

@@ -53,3 +54,3 @@ k8s.io/client-go v0.29.2

github.com/alessio/shellescape v1.4.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/danieljoos/wincred v1.2.1 // indirect

@@ -80,6 +81,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect

github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.15.0
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.13.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.16.0
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.15.0
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.10.0
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.15.0

@@ -90,3 +91,3 @@ github.com/stackitcloud/stackit-sdk-go/services/redis v0.15.0

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

@@ -93,0 +94,0 @@ gopkg.in/yaml.v2 v2.4.0 // indirect

+28
-26
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=

@@ -120,4 +120,4 @@ github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=

github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
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=

@@ -135,6 +135,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=

github.com/stackitcloud/stackit-sdk-go/services/dns v0.10.0/go.mod h1:MdZcRbs19s2NLeJmSLSoqTzm9IPIQhE1ZEMpo9gePq0=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0 h1:LAteZO46XmqTsmPw0QV8n8WiGM205pxrcqHqWznNmyY=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0/go.mod h1:wsO3+vXe1XiKLeCIctWAptaHQZ07Un7kmLTQ+drbj7w=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.15.0 h1:7gii3PZshOesHPCYlPycilXglk28imITIqjewySZwZ4=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.15.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.13.0 h1:W7tyIIIXgAilHpALRyrW3CrtQ2UAGZBjAG+P4tcK+QQ=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.13.0/go.mod h1:wsO3+vXe1XiKLeCIctWAptaHQZ07Un7kmLTQ+drbj7w=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.16.0 h1:Cz4zQnEax6L3Y9gL7jtETPmiTERB7WNQtIzZ1UWeNNk=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.16.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c=
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.15.0 h1:eYYyVUTS9Gjovg3z9+r6ctvsm1p1J4fHLa5QJbWHi0A=

@@ -144,8 +144,8 @@ github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.15.0/go.mod h1:kPetkX9hNm9HkRyiKQL/tlgdi8frZdMP8afg0mEvQ9s=

github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.14.0/go.mod h1:iFerEzGmkg6R13ldFUyHUWHm0ac9cS4ftTDLhP0k/dU=
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0 h1:rWgy4/eCIgyA2dUuc4a30pldmS6taQDwiLqoeZmyeP8=
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0/go.mod h1:dkVMJI88eJ3Xs0ZV15r4tUpgitUGJXcvrX3RL4Zq2bQ=
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.10.0 h1:tn1MD1nu+gYEbT3lslRI6BrapKwuvHv5Wi2Zw9uVPPc=
github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.10.0/go.mod h1:dkVMJI88eJ3Xs0ZV15r4tUpgitUGJXcvrX3RL4Zq2bQ=
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0 h1:zkhm0r0OZ5NbHJFrm+7B+h11QL0bNLC53nzXhqCaLWo=
github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0/go.mod h1:ZecMIf9oYj2DGZqWh93l97WdVaRdLl+tW5Fq3YKGwBM=
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.14.0 h1:PZAqXd8TVyTZo8qty4bM2sSoLlLG+Nc9tcpxbQhO+GY=
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.14.0/go.mod h1:SdrqGLCkilL6wl1+jcxmLtks2IocgIg+bsyeyYUIzR4=
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.15.0 h1:05wQYhO37Z4y8xAD+4OTYz6rYu6eJEmwMfCG4tjETEc=
github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.15.0/go.mod h1:SdrqGLCkilL6wl1+jcxmLtks2IocgIg+bsyeyYUIzR4=
github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.15.0 h1:Q7JxjVwb+9ugAX71AXdbfPL87HHmIIwb9LNahn6H/2o=

@@ -155,6 +155,8 @@ github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.15.0/go.mod h1:eSgnPBknTJh7t+jVKN+xzeAh+Cg1USOlH3QCyfvG20g=

github.com/stackitcloud/stackit-sdk-go/services/redis v0.15.0/go.mod h1:3LhiTR/DMbKR2HuleTzlFHltR1MT1KD0DeW46X6K2GE=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.8.0 h1:7AIvLkB7JZ5lYKtYLwI0rgJ0185hwQC1PFiUrjcinDM=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.8.0/go.mod h1:p16qz/pAW8b1gEhqMpIgJfutRPeDPqQLlbVGyCo3f8o=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0 h1:qCbvGqdG9saRB++UlhXt5ieCCDCITROqL5K2nm38efU=
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.9.0/go.mod h1:p16qz/pAW8b1gEhqMpIgJfutRPeDPqQLlbVGyCo3f8o=
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0 h1:pJBG455kmtbQFpCxcBfBK8wOuEnmsMv3h90LFcdj3q0=
github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.8.0/go.mod h1:LX0Mcyr7/QP77zf7e05fHCJO38RMuTxr7nEDUDZ3oPQ=
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0 h1:fYCBNvh4tqE+DXYDfbJEjC3n/I78zTZajdcPTPB/yig=
github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0/go.mod h1:ZYI3wj/NnhhWi25ugbdcniwnY/7mF6zN582c5HPe00o=
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0 h1:JB1O0E9+L50ZaO36uz7azurvUuB5JdX5s2ZXuIdb9t8=

@@ -181,4 +183,4 @@ github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0/go.mod h1:Ni9RBJvcaXRIrDIuQBpJcuQvCQSj27crQSyc+WM4p0c=

github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68=
github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=

@@ -195,4 +197,4 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=

golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

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

golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

@@ -215,10 +217,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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=

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

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

"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"

@@ -81,11 +83,3 @@ cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors"

p.Outputf("Created credentials for instance %q.\n\n", instanceLabel)
// The username field cannot be set by the user so we only display it if it's not returned empty
username := *resp.Credentials.Username
if username != "" {
p.Outputf("Username: %s\n", username)
}
p.Outputf("Password: %s\n", *resp.Credentials.Password)
return nil
return outputResult(p, model, instanceLabel, resp)
},

@@ -120,1 +114,32 @@ }

}
func outputResult(p *print.Printer, model *inputModel, instanceLabel string, resp *argus.CreateCredentialsResponse) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal Argus credentials: %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 Argus credentials: %w", err)
}
p.Outputln(string(details))
return nil
default:
p.Outputf("Created credentials for instance %q.\n\n", instanceLabel)
// The username field cannot be set by the user so we only display it if it's not returned empty
username := *resp.Credentials.Username
if username != "" {
p.Outputf("Username: %s\n", username)
}
p.Outputf("Password: %s\n", *resp.Credentials.Password)
return nil
}
}

@@ -26,2 +26,10 @@ package login

RunE: func(cmd *cobra.Command, args []string) error {
p.Warn(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n",
"Starting on July 9 2024, the new STACKIT Identity Provider (IDP) will be available.",
"On this date, we will release a new version of the STACKIT CLI that will use the new IDP for user authentication.",
"This also means that the user authentication on STACKIT CLI versions released before July 9 2024 is no longer guaranteed to work for all services.",
"Please make sure to update your STACKIT CLI to the latest version after July 9 2024 to ensure that you can continue to use all STACKIT services.",
"You can find more information regarding the new IDP at https://docs.stackit.cloud/stackit/en/release-notes-23101442.html#ReleaseNotes-2024-06-21-identity-provider",
))
err := auth.AuthorizeUser(p, false)

@@ -28,0 +36,0 @@ if err != nil {

@@ -6,2 +6,3 @@ package beta

"github.com/stackitcloud/stackit-cli/internal/cmd/beta/server"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex"

@@ -40,2 +41,3 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/args"

cmd.AddCommand(sqlserverflex.NewCmd(p))
cmd.AddCommand(server.NewCmd(p))
}

@@ -60,14 +60,14 @@ package create

Use: "create",
Short: "Creates an SQLServer Flex instance",
Long: "Creates an SQLServer Flex instance.",
Short: "Creates a SQLServer Flex instance",
Long: "Creates a SQLServer Flex instance.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create an SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values`,
`Create a SQLServer Flex instance with name "my-instance" and specify flavor by CPU and RAM. Other parameters are set to default values`,
`$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4`),
examples.NewExample(
`Create an SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values`,
`Create a SQLServer Flex instance with name "my-instance" and specify flavor by ID. Other parameters are set to default values`,
`$ stackit beta sqlserverflex instance create --name my-instance --flavor-id xxx`),
examples.NewExample(
`Create an SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values`,
`Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values`,
`$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24`),

@@ -96,3 +96,3 @@ ),

if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to create an SQLServer Flex instance for project %q?", projectLabel)
prompt := fmt.Sprintf("Are you sure you want to create a SQLServer Flex instance for project %q?", projectLabel)
err = p.PromptForConfirmation(prompt)

@@ -99,0 +99,0 @@ if err != nil {

@@ -34,8 +34,8 @@ package delete

Use: fmt.Sprintf("delete %s", instanceIdArg),
Short: "Deletes an SQLServer Flex instance",
Long: "Deletes an SQLServer Flex instance.",
Short: "Deletes a SQLServer Flex instance",
Long: "Deletes a SQLServer Flex instance.",
Args: args.SingleArg(instanceIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Delete an SQLServer Flex instance with ID "xxx"`,
`Delete a SQLServer Flex instance with ID "xxx"`,
"$ stackit beta sqlserverflex instance delete xxx"),

@@ -42,0 +42,0 @@ ),

@@ -35,11 +35,11 @@ package describe

Use: fmt.Sprintf("describe %s", instanceIdArg),
Short: "Shows details of an SQLServer Flex instance",
Long: "Shows details of an SQLServer Flex instance.",
Short: "Shows details of a SQLServer Flex instance",
Long: "Shows details of a SQLServer Flex instance.",
Args: args.SingleArg(instanceIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Get details of an SQLServer Flex instance with ID "xxx"`,
`Get details of a SQLServer Flex instance with ID "xxx"`,
"$ stackit beta sqlserverflex instance describe xxx"),
examples.NewExample(
`Get details of an SQLServer Flex instance with ID "xxx" in JSON format`,
`Get details of a SQLServer Flex instance with ID "xxx" in JSON format`,
"$ stackit beta sqlserverflex instance describe xxx --output-format json"),

@@ -46,0 +46,0 @@ ),

@@ -54,10 +54,10 @@ package update

Use: fmt.Sprintf("update %s", instanceIdArg),
Short: "Updates an SQLServer Flex instance",
Long: "Updates an SQLServer Flex instance.",
Short: "Updates a SQLServer Flex instance",
Long: "Updates a SQLServer Flex instance.",
Example: examples.Build(
examples.NewExample(
`Update the name of an SQLServer Flex instance with ID "xxx"`,
`Update the name of a SQLServer Flex instance with ID "xxx"`,
"$ stackit beta sqlserverflex instance update xxx --name my-new-name"),
examples.NewExample(
`Update the backup schedule of an SQLServer Flex instance with ID "xxx"`,
`Update the backup schedule of a SQLServer Flex instance with ID "xxx"`,
`$ stackit beta sqlserverflex instance update xxx --backup-schedule "30 0 * * *"`),

@@ -64,0 +64,0 @@ ),

@@ -13,2 +13,3 @@ package options

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex"

@@ -20,11 +21,18 @@ )

var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testInstanceId = uuid.NewString()
type sqlServerFlexClientMocked struct {
listFlavorsFails bool
listVersionsFails bool
listStoragesFails bool
listFlavorsFails bool
listVersionsFails bool
listStoragesFails bool
listUserRolesFails bool
listDBCollationsFails bool
listDBCompatibilitiesFails bool
listFlavorsCalled bool
listVersionsCalled bool
listStoragesCalled bool
listFlavorsCalled bool
listVersionsCalled bool
listStoragesCalled bool
listUserRolesCalled bool
listDBCollationsCalled bool
listDBCompatibilitiesCalled bool
}

@@ -66,8 +74,42 @@

func (c *sqlServerFlexClientMocked) ListRolesExecute(_ context.Context, _, _ string) (*sqlserverflex.ListRolesResponse, error) {
c.listUserRolesCalled = true
if c.listUserRolesFails {
return nil, fmt.Errorf("list roles failed")
}
return utils.Ptr(sqlserverflex.ListRolesResponse{
Roles: utils.Ptr([]string{}),
}), nil
}
func (c *sqlServerFlexClientMocked) ListCollationsExecute(_ context.Context, _, _ string) (*sqlserverflex.ListCollationsResponse, error) {
c.listDBCollationsCalled = true
if c.listDBCollationsFails {
return nil, fmt.Errorf("list collations failed")
}
return utils.Ptr(sqlserverflex.ListCollationsResponse{
Collations: utils.Ptr([]sqlserverflex.MssqlDatabaseCollation{}),
}), nil
}
func (c *sqlServerFlexClientMocked) ListCompatibilityExecute(_ context.Context, _, _ string) (*sqlserverflex.ListCompatibilityResponse, error) {
c.listDBCompatibilitiesCalled = true
if c.listDBCompatibilitiesFails {
return nil, fmt.Errorf("list compatibilities failed")
}
return utils.Ptr(sqlserverflex.ListCompatibilityResponse{
Compatibilities: utils.Ptr([]sqlserverflex.MssqlDatabaseCompatibility{}),
}), nil
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
flavorsFlag: "true",
versionsFlag: "true",
storagesFlag: "true",
flavorIdFlag: "2.4",
flavorsFlag: "true",
versionsFlag: "true",
storagesFlag: "true",
userRolesFlag: "true",
dbCollationsFlag: "true",
dbCompatibilitiesFlag: "true",
flavorIdFlag: "2.4",
instanceIdFlag: testInstanceId,
}

@@ -82,6 +124,9 @@ for _, mod := range mods {

model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{Verbosity: globalflags.VerbosityDefault},
Flavors: false,
Versions: false,
Storages: false,
GlobalFlagModel: &globalflags.GlobalFlagModel{Verbosity: globalflags.VerbosityDefault},
Flavors: false,
Versions: false,
Storages: false,
UserRoles: false,
DBCollations: false,
DBCompatibilities: false,
}

@@ -96,7 +141,11 @@ for _, mod := range mods {

model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{Verbosity: globalflags.VerbosityDefault},
Flavors: true,
Versions: true,
Storages: true,
FlavorId: utils.Ptr("2.4"),
GlobalFlagModel: &globalflags.GlobalFlagModel{Verbosity: globalflags.VerbosityDefault},
Flavors: true,
Versions: true,
Storages: true,
UserRoles: true,
DBCollations: true,
DBCompatibilities: true,
FlavorId: utils.Ptr("2.4"),
InstanceId: utils.Ptr(testInstanceId),
}

@@ -134,5 +183,5 @@ for _, mod := range mods {

isValid: true,
expectedModel: fixtureInputModelAllFalse(func(model *inputModel) {
model.Flavors = true
model.Versions = true
expectedModel: fixtureInputModelAllTrue(func(model *inputModel) {
model.Storages = false
model.FlavorId = nil
}),

@@ -145,2 +194,3 @@ },

delete(flagValues, versionsFlag)
delete(flagValues, userRolesFlag)
flagValues[storagesFlag] = "true"

@@ -150,5 +200,6 @@ flagValues[flavorIdFlag] = "2.4"

isValid: true,
expectedModel: fixtureInputModelAllFalse(func(model *inputModel) {
model.Storages = true
model.FlavorId = utils.Ptr("2.4")
expectedModel: fixtureInputModelAllTrue(func(model *inputModel) {
model.Flavors = false
model.Versions = false
model.UserRoles = false
}),

@@ -173,2 +224,43 @@ },

},
{
description: "user roles without instance id",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
delete(flagValues, dbCollationsFlag)
delete(flagValues, dbCompatibilitiesFlag)
}),
isValid: false,
},
{
description: "db collations without instance id",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
delete(flagValues, userRolesFlag)
delete(flagValues, dbCompatibilitiesFlag)
}),
isValid: false,
},
{
description: "db compatibilities without instance id",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, instanceIdFlag)
delete(flagValues, userRolesFlag)
delete(flagValues, dbCollationsFlag)
}),
isValid: false,
},
{
description: "instance id without user roles, db collations and db compatibilities",
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, userRolesFlag)
delete(flagValues, dbCollationsFlag)
delete(flagValues, dbCompatibilitiesFlag)
}),
isValid: true,
expectedModel: fixtureInputModelAllTrue(func(model *inputModel) {
model.UserRoles = false
model.DBCollations = false
model.DBCompatibilities = false
}),
},
}

@@ -224,27 +316,40 @@

tests := []struct {
description string
model *inputModel
isValid bool
listFlavorsFails bool
listVersionsFails bool
listStoragesFails bool
expectListFlavorsCalled bool
expectListVersionsCalled bool
expectListStoragesCalled bool
description string
model *inputModel
isValid bool
listFlavorsFails bool
listVersionsFails bool
listStoragesFails bool
listUserRolesFails bool
listDBCollationsFails bool
listDBCompatibilitiesFails bool
expectListFlavorsCalled bool
expectListVersionsCalled bool
expectListStoragesCalled bool
expectListUserRolesCalled bool
expectListDBCollationsCalled bool
expectListDBCompatibilitiesCalled bool
}{
{
description: "all values",
model: fixtureInputModelAllTrue(),
isValid: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: true,
description: "all values",
model: fixtureInputModelAllTrue(),
isValid: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: true,
expectListUserRolesCalled: true,
expectListDBCollationsCalled: true,
expectListDBCompatibilitiesCalled: true,
},
{
description: "no values",
model: fixtureInputModelAllFalse(),
isValid: true,
expectListFlavorsCalled: false,
expectListVersionsCalled: false,
expectListStoragesCalled: false,
description: "no values",
model: fixtureInputModelAllFalse(),
isValid: true,
expectListFlavorsCalled: false,
expectListVersionsCalled: false,
expectListStoragesCalled: false,
expectListUserRolesCalled: false,
expectListDBCollationsCalled: false,
expectListDBCompatibilitiesCalled: false,
},

@@ -273,28 +378,100 @@ {

{
description: "list flavors fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listFlavorsFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: false,
expectListStoragesCalled: false,
description: "only user roles",
model: fixtureInputModelAllFalse(func(model *inputModel) {
model.UserRoles = true
model.InstanceId = utils.Ptr(testInstanceId)
}),
isValid: true,
expectListUserRolesCalled: true,
},
{
description: "list versions fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listVersionsFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: false,
description: "only db collations",
model: fixtureInputModelAllFalse(func(model *inputModel) {
model.DBCollations = true
model.InstanceId = utils.Ptr(testInstanceId)
}),
isValid: true,
expectListDBCollationsCalled: true,
},
{
description: "list storages fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listStoragesFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: true,
description: "only db compatibilities",
model: fixtureInputModelAllFalse(func(model *inputModel) {
model.DBCompatibilities = true
model.InstanceId = utils.Ptr(testInstanceId)
}),
isValid: true,
expectListDBCompatibilitiesCalled: true,
},
{
description: "list flavors fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listFlavorsFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: false,
expectListStoragesCalled: false,
expectListUserRolesCalled: false,
expectListDBCollationsCalled: false,
expectListDBCompatibilitiesCalled: false,
},
{
description: "list versions fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listVersionsFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: false,
expectListUserRolesCalled: false,
expectListDBCollationsCalled: false,
expectListDBCompatibilitiesCalled: false,
},
{
description: "list storages fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listStoragesFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: true,
expectListUserRolesCalled: false,
expectListDBCollationsCalled: false,
expectListDBCompatibilitiesCalled: false,
},
{
description: "list user roles fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listUserRolesFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: true,
expectListUserRolesCalled: true,
expectListDBCollationsCalled: false,
expectListDBCompatibilitiesCalled: false,
},
{
description: "list db collations fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listDBCollationsFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: true,
expectListUserRolesCalled: true,
expectListDBCollationsCalled: true,
expectListDBCompatibilitiesCalled: false,
},
{
description: "list db compatibilities fails",
model: fixtureInputModelAllTrue(),
isValid: false,
listDBCompatibilitiesFails: true,
expectListFlavorsCalled: true,
expectListVersionsCalled: true,
expectListStoragesCalled: true,
expectListUserRolesCalled: true,
expectListDBCollationsCalled: true,
expectListDBCompatibilitiesCalled: true,
},
}

@@ -304,9 +481,12 @@

t.Run(tt.description, func(t *testing.T) {
p := &print.Printer{}
p := print.NewPrinter()
cmd := NewCmd(p)
p.Cmd = cmd
client := &sqlServerFlexClientMocked{
listFlavorsFails: tt.listFlavorsFails,
listVersionsFails: tt.listVersionsFails,
listStoragesFails: tt.listStoragesFails,
listFlavorsFails: tt.listFlavorsFails,
listVersionsFails: tt.listVersionsFails,
listStoragesFails: tt.listStoragesFails,
listUserRolesFails: tt.listUserRolesFails,
listDBCollationsFails: tt.listDBCollationsFails,
listDBCompatibilitiesFails: tt.listDBCompatibilitiesFails,
}

@@ -313,0 +493,0 @@

@@ -22,6 +22,11 @@ package options

const (
flavorsFlag = "flavors"
versionsFlag = "versions"
storagesFlag = "storages"
flavorIdFlag = "flavor-id"
flavorsFlag = "flavors"
versionsFlag = "versions"
storagesFlag = "storages"
userRolesFlag = "user-roles"
dbCollationsFlag = "db-collations"
dbCompatibilitiesFlag = "db-compatibilities"
flavorIdFlag = "flavor-id"
instanceIdFlag = "instance-id"
)

@@ -32,12 +37,20 @@

Flavors bool
Versions bool
Storages bool
FlavorId *string
Flavors bool
Versions bool
Storages bool
UserRoles bool
DBCollations bool
DBCompatibilities bool
FlavorId *string
InstanceId *string
}
type options struct {
Flavors *[]sqlserverflex.InstanceFlavorEntry `json:"flavors,omitempty"`
Versions *[]string `json:"versions,omitempty"`
Storages *flavorStorages `json:"flavorStorages,omitempty"`
Flavors *[]sqlserverflex.InstanceFlavorEntry `json:"flavors,omitempty"`
Versions *[]string `json:"versions,omitempty"`
Storages *flavorStorages `json:"flavorStorages,omitempty"`
UserRoles *instanceUserRoles `json:"userRoles,omitempty"`
DBCollations *instanceDBCollations `json:"dbCollations,omitempty"`
DBCompatibilities *instanceDBCompatibilities `json:"dbCompatibilities,omitempty"`
}

@@ -50,2 +63,17 @@

type instanceUserRoles struct {
InstanceId string `json:"instanceId"`
UserRoles []string `json:"userRoles"`
}
type instanceDBCollations struct {
InstanceId string `json:"instanceId"`
DBCollations []sqlserverflex.MssqlDatabaseCollation `json:"dbCollations"`
}
type instanceDBCompatibilities struct {
InstanceId string `json:"instanceId"`
DBCompatibilities []sqlserverflex.MssqlDatabaseCompatibility `json:"dbCompatibilities"`
}
func NewCmd(p *print.Printer) *cobra.Command {

@@ -60,9 +88,12 @@ cmd := &cobra.Command{

`List SQL Server Flex flavors options`,
"$ stackit sqlserverflex options --flavors"),
"$ stackit beta sqlserverflex options --flavors"),
examples.NewExample(
`List SQL Server Flex available versions`,
"$ stackit sqlserverflex options --versions"),
"$ stackit beta sqlserverflex options --versions"),
examples.NewExample(
`List SQL Server Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit sqlserverflex options --flavors"`,
"$ stackit sqlserverflex options --storages --flavor-id <FLAVOR_ID>"),
`List SQL Server Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit beta sqlserverflex options --flavors"`,
"$ stackit beta sqlserverflex options --storages --flavor-id <FLAVOR_ID>"),
examples.NewExample(
`List SQL Server Flex user roles and database compatibilities for a given instance. The IDs of existing instances can be obtained by running "$ stackit beta sqlserverflex instance list"`,
"$ stackit beta sqlserverflex options --user-roles --db-compatibilities --instance-id <INSTANCE_ID>"),
),

@@ -99,3 +130,7 @@ RunE: func(cmd *cobra.Command, args []string) error {

cmd.Flags().Bool(storagesFlag, false, "Lists supported storages for a given flavor")
cmd.Flags().Bool(userRolesFlag, false, "Lists supported user roles for a given instance")
cmd.Flags().Bool(dbCollationsFlag, false, "Lists supported database collations for a given instance")
cmd.Flags().Bool(dbCompatibilitiesFlag, false, "Lists supported database compatibilities for a given instance")
cmd.Flags().String(flavorIdFlag, "", `The flavor ID to show storages for. Only relevant when "--storages" is passed`)
cmd.Flags().String(instanceIdFlag, "", `The instance ID to show user roles, database collations and database compatibilities for. Only relevant when "--user-roles", "--db-collations" or "--db-compatibilities" is passed`)
}

@@ -105,8 +140,14 @@

globalFlags := globalflags.Parse(p, cmd)
flavors := flags.FlagToBoolValue(p, cmd, flavorsFlag)
versions := flags.FlagToBoolValue(p, cmd, versionsFlag)
storages := flags.FlagToBoolValue(p, cmd, storagesFlag)
userRoles := flags.FlagToBoolValue(p, cmd, userRolesFlag)
dbCollations := flags.FlagToBoolValue(p, cmd, dbCollationsFlag)
dbCompatibilities := flags.FlagToBoolValue(p, cmd, dbCompatibilitiesFlag)
flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag)
instanceId := flags.FlagToStringPointer(p, cmd, instanceIdFlag)
if !flavors && !versions && !storages {
if !flavors && !versions && !storages && !userRoles && !dbCollations && !dbCompatibilities {
return nil, fmt.Errorf("%s\n\n%s",

@@ -121,11 +162,22 @@ "please specify at least one category for which to list the available options.",

"You can get the available flavor IDs by running:",
" $ stackit sqlserverflex options --flavors")
" $ stackit beta sqlserverflex options --flavors")
}
if (userRoles || dbCollations || dbCompatibilities) && instanceId == nil {
return nil, fmt.Errorf("%s\n\n%s\n%s",
`please specify an instance ID to show user roles, database collations or database compatibilities for by setting the flag "--instance-id <INSTANCE_ID>".`,
"You can get the available instances and their IDs by running:",
" $ stackit beta sqlserverflex instance list")
}
model := inputModel{
GlobalFlagModel: globalFlags,
Flavors: flavors,
Versions: versions,
Storages: storages,
FlavorId: flags.FlagToStringPointer(p, cmd, flavorIdFlag),
GlobalFlagModel: globalFlags,
Flavors: flavors,
Versions: versions,
Storages: storages,
UserRoles: userRoles,
DBCollations: dbCollations,
DBCompatibilities: dbCompatibilities,
FlavorId: flavorId,
InstanceId: instanceId,
}

@@ -149,2 +201,5 @@

ListStoragesExecute(ctx context.Context, projectId, flavorId string) (*sqlserverflex.ListStoragesResponse, error)
ListRolesExecute(ctx context.Context, projectId string, instanceId string) (*sqlserverflex.ListRolesResponse, error)
ListCollationsExecute(ctx context.Context, projectId string, instanceId string) (*sqlserverflex.ListCollationsResponse, error)
ListCompatibilityExecute(ctx context.Context, projectId string, instanceId string) (*sqlserverflex.ListCompatibilityResponse, error)
}

@@ -156,2 +211,5 @@

var storages *sqlserverflex.ListStoragesResponse
var userRoles *sqlserverflex.ListRolesResponse
var dbCollations *sqlserverflex.ListCollationsResponse
var dbCompatibilities *sqlserverflex.ListCompatibilityResponse
var err error

@@ -177,7 +235,25 @@

}
if model.UserRoles {
userRoles, err = apiClient.ListRolesExecute(ctx, model.ProjectId, *model.InstanceId)
if err != nil {
return fmt.Errorf("get SQL Server Flex user roles: %w", err)
}
}
if model.DBCollations {
dbCollations, err = apiClient.ListCollationsExecute(ctx, model.ProjectId, *model.InstanceId)
if err != nil {
return fmt.Errorf("get SQL Server Flex DB collations: %w", err)
}
}
if model.DBCompatibilities {
dbCompatibilities, err = apiClient.ListCompatibilityExecute(ctx, model.ProjectId, *model.InstanceId)
if err != nil {
return fmt.Errorf("get SQL Server Flex DB compatibilities: %w", err)
}
}
return outputResult(p, model, flavors, versions, storages)
return outputResult(p, model, flavors, versions, storages, userRoles, dbCollations, dbCompatibilities)
}
func outputResult(p *print.Printer, model *inputModel, flavors *sqlserverflex.ListFlavorsResponse, versions *sqlserverflex.ListVersionsResponse, storages *sqlserverflex.ListStoragesResponse) error {
func outputResult(p *print.Printer, model *inputModel, flavors *sqlserverflex.ListFlavorsResponse, versions *sqlserverflex.ListVersionsResponse, storages *sqlserverflex.ListStoragesResponse, userRoles *sqlserverflex.ListRolesResponse, dbCollations *sqlserverflex.ListCollationsResponse, dbCompatibilities *sqlserverflex.ListCompatibilityResponse) error {
options := &options{}

@@ -196,2 +272,20 @@ if flavors != nil {

}
if userRoles != nil && model.InstanceId != nil {
options.UserRoles = &instanceUserRoles{
InstanceId: *model.InstanceId,
UserRoles: *userRoles.Roles,
}
}
if dbCollations != nil && model.InstanceId != nil {
options.DBCollations = &instanceDBCollations{
InstanceId: *model.InstanceId,
DBCollations: *dbCollations.Collations,
}
}
if dbCompatibilities != nil && model.InstanceId != nil {
options.DBCompatibilities = &instanceDBCompatibilities{
InstanceId: *model.InstanceId,
DBCompatibilities: *dbCompatibilities.Compatibilities,
}
}

@@ -230,2 +324,12 @@ switch model.OutputFormat {

}
if model.UserRoles {
content += renderUserRoles(options.UserRoles)
}
if model.DBCompatibilities {
content += renderDBCompatibilities(options.DBCompatibilities)
}
// Rendered at last because table is very long
if model.DBCollations {
content += renderDBCollations(options.DBCollations)
}

@@ -286,1 +390,43 @@ err := p.PagerDisplay(content)

}
func renderUserRoles(roles *instanceUserRoles) string {
if len(roles.UserRoles) == 0 {
return ""
}
table := tables.NewTable()
table.SetTitle("User Roles")
table.SetHeader("ROLE")
for i := range roles.UserRoles {
table.AddRow(roles.UserRoles[i])
}
return table.Render()
}
func renderDBCollations(dbCollations *instanceDBCollations) string {
if len(dbCollations.DBCollations) == 0 {
return ""
}
table := tables.NewTable()
table.SetTitle("DB Collations")
table.SetHeader("NAME", "DESCRIPTION")
for i := range dbCollations.DBCollations {
table.AddRow(*dbCollations.DBCollations[i].CollationName, *dbCollations.DBCollations[i].Description)
}
return table.Render()
}
func renderDBCompatibilities(dbCompatibilities *instanceDBCompatibilities) string {
if len(dbCompatibilities.DBCompatibilities) == 0 {
return ""
}
table := tables.NewTable()
table.SetTitle("DB Compatibilities")
table.SetHeader("COMPATIBILITY LEVEL", "DESCRIPTION")
for i := range dbCompatibilities.DBCompatibilities {
table.AddRow(*dbCompatibilities.DBCompatibilities[i].CompatibilityLevel, *dbCompatibilities.DBCompatibilities[i].Description)
}
return table.Render()
}
package sqlserverflex
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/instance"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/options"
"github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"

@@ -26,4 +28,6 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/print"

func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(database.NewCmd(p))
cmd.AddCommand(instance.NewCmd(p))
cmd.AddCommand(options.NewCmd(p))
cmd.AddCommand(user.NewCmd(p))
}

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

secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint"
serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint"
serviceAccountCustomEndpointFlag = "service-account-custom-endpoint"

@@ -143,2 +144,3 @@ skeCustomEndpointFlag = "ske-custom-endpoint"

cmd.Flags().String(serviceAccountCustomEndpointFlag, "", "Service Account API base URL, used in calls to this API")
cmd.Flags().String(serverBackupCustomEndpointFlag, "", "Server Backup API base URL, used in calls to this API")
cmd.Flags().String(skeCustomEndpointFlag, "", "SKE API base URL, used in calls to this API")

@@ -175,2 +177,4 @@ cmd.Flags().String(sqlServerFlexCustomEndpointFlag, "", "SQLServer Flex API base URL, used in calls to this API")

cobra.CheckErr(err)
err = viper.BindPFlag(config.ServerBackupCustomEndpointKey, cmd.Flags().Lookup(serverBackupCustomEndpointFlag))
cobra.CheckErr(err)
err = viper.BindPFlag(config.ServiceAccountCustomEndpointKey, cmd.Flags().Lookup(serviceAccountCustomEndpointFlag))

@@ -177,0 +181,0 @@ cobra.CheckErr(err)

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

serviceAccountCustomEndpointFlag: true,
serverBackupCustomEndpointFlag: true,
skeCustomEndpointFlag: true,

@@ -64,2 +65,3 @@ sqlServerFlexCustomEndpointFlag: true,

ServiceAccountCustomEndpoint: true,
ServerBackupCustomEndpoint: true,
SKECustomEndpoint: true,

@@ -111,2 +113,3 @@ SQLServerFlexCustomEndpoint: true,

model.ServiceAccountCustomEndpoint = false
model.ServerBackupCustomEndpoint = false
model.SKECustomEndpoint = false

@@ -196,2 +199,12 @@ model.SQLServerFlexCustomEndpoint = false

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

@@ -198,0 +211,0 @@ for _, tt := range tests {

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

serviceAccountCustomEndpointFlag = "service-account-custom-endpoint"
serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint"
skeCustomEndpointFlag = "ske-custom-endpoint"

@@ -66,2 +67,3 @@ sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint"

SecretsManagerCustomEndpoint bool
ServerBackupCustomEndpoint bool
ServiceAccountCustomEndpoint bool

@@ -153,2 +155,5 @@ SKECustomEndpoint bool

}
if model.ServerBackupCustomEndpoint {
viper.Set(config.ServerBackupCustomEndpointKey, "")
}
if model.SKECustomEndpoint {

@@ -194,2 +199,3 @@ viper.Set(config.SKECustomEndpointKey, "")

cmd.Flags().Bool(serviceAccountCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL")
cmd.Flags().Bool(serverBackupCustomEndpointFlag, false, "Server Backup base URL. If unset, uses the default base URL")
cmd.Flags().Bool(skeCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL")

@@ -222,2 +228,3 @@ cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL")

ServiceAccountCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceAccountCustomEndpointFlag),
ServerBackupCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverBackupCustomEndpointFlag),
SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag),

@@ -224,0 +231,0 @@ SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag),

@@ -48,6 +48,6 @@ package describe

`Get details of a MongoDB Flex user with ID "xxx" of instance with ID "yyy"`,
"$ stackit mongodbflex user list xxx --instance-id yyy"),
"$ stackit mongodbflex user describe xxx --instance-id yyy"),
examples.NewExample(
`Get details of a MongoDB Flex user with ID "xxx" of instance with ID "yyy" in JSON format`,
"$ stackit mongodbflex user list xxx --instance-id yyy --output-format json"),
"$ stackit mongodbflex user describe xxx --instance-id yyy --output-format json"),
),

@@ -54,0 +54,0 @@ Args: args.SingleArg(userIdArg, utils.ValidateUUID),

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

"github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"

@@ -64,2 +65,13 @@

// Check if the project is enabled before trying to create
enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId)
if err != nil {
return fmt.Errorf("check if Object Storage is enabled: %w", err)
}
if !enabled {
return &errors.ServiceDisabledError{
Service: "object-storage",
}
}
// Call API

@@ -66,0 +78,0 @@ req := buildRequest(ctx, model, apiClient)

@@ -47,6 +47,6 @@ package describe

`Get details of a PostgreSQL Flex user with ID "xxx" of instance with ID "yyy"`,
"$ stackit postgresflex user list xxx --instance-id yyy"),
"$ stackit postgresflex user describe xxx --instance-id yyy"),
examples.NewExample(
`Get details of a PostgreSQL Flex user with ID "xxx" of instance with ID "yyy" in JSON format`,
"$ stackit postgresflex user list xxx --instance-id yyy --output-format json"),
"$ stackit postgresflex user describe xxx --instance-id yyy --output-format json"),
),

@@ -53,0 +53,0 @@ Args: args.SingleArg(userIdArg, nil),

@@ -61,3 +61,3 @@ package create

}),
Members: &[]resourcemanager.ProjectMember{
Members: &[]resourcemanager.Member{
{

@@ -64,0 +64,0 @@ Role: utils.Ptr(ownerRole),

@@ -181,3 +181,3 @@ package create

Labels: model.Labels,
Members: &[]resourcemanager.ProjectMember{
Members: &[]resourcemanager.Member{
{

@@ -193,3 +193,3 @@ Role: utils.Ptr(ownerRole),

func outputResult(p *print.Printer, model *inputModel, resp *resourcemanager.ProjectResponse) error {
func outputResult(p *print.Printer, model *inputModel, resp *resourcemanager.Project) error {
switch model.OutputFormat {

@@ -196,0 +196,0 @@ case print.JSONOutputFormat:

@@ -121,3 +121,3 @@ package describe

func outputResult(p *print.Printer, outputFormat string, project *resourcemanager.ProjectResponseWithParents) error {
func outputResult(p *print.Printer, outputFormat string, project *resourcemanager.GetProjectResponse) error {
switch outputFormat {

@@ -124,0 +124,0 @@ case print.JSONOutputFormat:

@@ -454,4 +454,4 @@ package list

projects := make([]resourcemanager.ProjectResponse, numItemsToReturn)
mockedResp := resourcemanager.AllProjectsResponse{
projects := make([]resourcemanager.Project, numItemsToReturn)
mockedResp := resourcemanager.ListProjectsResponse{
Items: &projects,

@@ -458,0 +458,0 @@ }

@@ -185,3 +185,3 @@ package list

func fetchProjects(ctx context.Context, model *inputModel, apiClient resourceManagerClient) ([]resourcemanager.ProjectResponse, error) {
func fetchProjects(ctx context.Context, model *inputModel, apiClient resourceManagerClient) ([]resourcemanager.Project, error) {
if model.Limit != nil && *model.Limit < model.PageSize {

@@ -192,3 +192,3 @@ model.PageSize = *model.Limit

offset := 0
projects := []resourcemanager.ProjectResponse{}
projects := []resourcemanager.Project{}
for {

@@ -224,3 +224,3 @@ // Call API

func outputResult(p *print.Printer, outputFormat string, projects []resourcemanager.ProjectResponse) error {
func outputResult(p *print.Printer, outputFormat string, projects []resourcemanager.Project) error {
switch outputFormat {

@@ -227,0 +227,0 @@ case print.JSONOutputFormat:

@@ -90,3 +90,3 @@ package create

// Check if SKE is enabled for this project
// Check if the project is enabled before trying to create
enabled, err := skeUtils.ProjectEnabled(ctx, apiClient, model.ProjectId)

@@ -97,3 +97,5 @@ if err != nil {

if !enabled {
return fmt.Errorf("SKE isn't enabled for this project, please run 'stackit ske enable'")
return &errors.ServiceDisabledError{
Service: "ske",
}
}

@@ -100,0 +102,0 @@

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

ServiceAccountCustomEndpointKey = "service_account_custom_endpoint"
ServerBackupCustomEndpointKey = "serverbackup_custom_endpoint"
SKECustomEndpointKey = "ske_custom_endpoint"

@@ -80,2 +81,3 @@ SQLServerFlexCustomEndpointKey = "sqlserverflex_custom_endpoint"

ServiceAccountCustomEndpointKey,
ServerBackupCustomEndpointKey,
SKECustomEndpointKey,

@@ -150,2 +152,3 @@ SQLServerFlexCustomEndpointKey,

viper.SetDefault(ServiceAccountCustomEndpointKey, "")
viper.SetDefault(ServerBackupCustomEndpointKey, "")
viper.SetDefault(SKECustomEndpointKey, "")

@@ -152,0 +155,0 @@ viper.SetDefault(SQLServerFlexCustomEndpointKey, "")

@@ -136,2 +136,7 @@ package errors

$ %s --help`
SERVICE_DISABLED = `This service isn't enabled for the current project.
To enable it, run:
$ stackit %s enable`
)

@@ -368,1 +373,9 @@

}
type ServiceDisabledError struct {
Service string
}
func (e *ServiceDisabledError) Error() string {
return fmt.Sprintf(SERVICE_DISABLED, e.Service)
}

@@ -6,2 +6,3 @@ package projectname

"fmt"
"os"

@@ -45,3 +46,3 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/config"

// (So next time we can just pull it from there)
if !isProjectIdSetInFlags(p, cmd) {
if !(isProjectIdSetInFlags(p, cmd) || isProjectIdSetInEnvVar()) {
viper.Set(config.ProjectNameKey, projectName)

@@ -62,3 +63,4 @@ err = config.Write()

// - Project name in the config file is not empty
projectIdSet := isProjectIdSetInFlags(p, cmd)
projectIdSetInFlags := isProjectIdSetInFlags(p, cmd)
projectIdSetInEnv := isProjectIdSetInEnvVar()
projectName := viper.GetString(config.ProjectNameKey)

@@ -69,3 +71,3 @@ projectNameSet := false

}
return !projectIdSet && projectNameSet
return !projectIdSetInFlags && !projectIdSetInEnv && projectNameSet
}

@@ -84,1 +86,7 @@

}
func isProjectIdSetInEnvVar() bool {
// Reads the project Id from the environment variable PROJECT_ID
_, projectIdSetInEnv := os.LookupEnv("STACKIT_PROJECT_ID")
return projectIdSetInEnv
}

@@ -23,2 +23,9 @@ package utils

type MongoDBFlexClient interface {
ListVersionsExecute(ctx context.Context, projectId string) (*mongodbflex.ListVersionsResponse, error)
GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*mongodbflex.GetInstanceResponse, error)
GetUserExecute(ctx context.Context, projectId, instanceId, userId string) (*mongodbflex.GetUserResponse, error)
ListRestoreJobsExecute(ctx context.Context, projectId string, instanceId string) (*mongodbflex.ListRestoreJobsResponse, error)
}
func AvailableInstanceTypes() []string {

@@ -119,9 +126,2 @@ instanceTypes := make([]string, len(instanceTypeToReplicas))

type MongoDBFlexClient interface {
ListVersionsExecute(ctx context.Context, projectId string) (*mongodbflex.ListVersionsResponse, error)
GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*mongodbflex.GetInstanceResponse, error)
GetUserExecute(ctx context.Context, projectId, instanceId, userId string) (*mongodbflex.GetUserResponse, error)
ListRestoreJobsExecute(ctx context.Context, projectId string, instanceId string) (*mongodbflex.ListRestoreJobsResponse, error)
}
func GetLatestMongoDBVersion(ctx context.Context, apiClient MongoDBFlexClient, projectId string) (string, error) {

@@ -152,3 +152,3 @@ resp, err := apiClient.ListVersionsExecute(ctx, projectId)

if err != nil {
return "", fmt.Errorf("get MongoDBFlex instance: %w", err)
return "", fmt.Errorf("get MongoDB Flex instance: %w", err)
}

@@ -161,3 +161,3 @@ return *resp.Item.Name, nil

if err != nil {
return "", fmt.Errorf("get MongoDBFlex user: %w", err)
return "", fmt.Errorf("get MongoDB Flex user: %w", err)
}

@@ -164,0 +164,0 @@ return *resp.Item.Username, nil

@@ -15,2 +15,3 @@ package utils

"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/objectstorage"

@@ -31,2 +32,4 @@ )

type objectStorageClientMocked struct {
serviceDisabled bool
getServiceStatusFails bool
listCredentialsGroupsFails bool

@@ -37,2 +40,12 @@ listCredentialsGroupsResp *objectstorage.ListCredentialsGroupsResponse

func (m *objectStorageClientMocked) GetServiceStatusExecute(_ context.Context, _ string) (*objectstorage.ProjectStatus, error) {
if m.getServiceStatusFails {
return nil, fmt.Errorf("could not get service status")
}
if m.serviceDisabled {
return nil, &oapierror.GenericOpenAPIError{StatusCode: 404}
}
return &objectstorage.ProjectStatus{}, nil
}
func (m *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) {

@@ -49,2 +62,54 @@ if m.listCredentialsGroupsFails {

func TestProjectEnabled(t *testing.T) {
tests := []struct {
description string
serviceDisabled bool
getProjectFails bool
isValid bool
expectedOutput bool
}{
{
description: "project enabled",
isValid: true,
expectedOutput: true,
},
{
description: "project disabled (404)",
serviceDisabled: true,
isValid: true,
expectedOutput: false,
},
{
description: "get project fails",
getProjectFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &objectStorageClientMocked{
serviceDisabled: tt.serviceDisabled,
getServiceStatusFails: tt.getProjectFails,
}
output, err := ProjectEnabled(context.Background(), client, testProjectId)
if tt.isValid && err != nil {
fmt.Printf("failed on valid input: %v", err)
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 %t, got %t", tt.expectedOutput, output)
}
})
}
}
func TestGetCredentialsGroupName(t *testing.T) {

@@ -51,0 +116,0 @@ tests := []struct {

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

"fmt"
"net/http"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/objectstorage"

@@ -12,2 +14,3 @@ )

type ObjectStorageClient interface {
GetServiceStatusExecute(ctx context.Context, projectId string) (*objectstorage.ProjectStatus, error)
ListCredentialsGroupsExecute(ctx context.Context, projectId string) (*objectstorage.ListCredentialsGroupsResponse, error)

@@ -17,2 +20,17 @@ ListAccessKeys(ctx context.Context, projectId string) objectstorage.ApiListAccessKeysRequest

func ProjectEnabled(ctx context.Context, apiClient ObjectStorageClient, projectId string) (bool, error) {
_, err := apiClient.GetServiceStatusExecute(ctx, projectId)
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if !ok {
return false, err
}
if oapiErr.StatusCode == http.StatusNotFound {
return false, nil
}
return false, err
}
return true, nil
}
func GetCredentialsGroupName(ctx context.Context, apiClient ObjectStorageClient, projectId, credentialsGroupId string) (string, error) {

@@ -19,0 +37,0 @@ resp, err := apiClient.ListCredentialsGroupsExecute(ctx, projectId)

@@ -21,2 +21,8 @@ package utils

type PostgresFlexClient interface {
ListVersionsExecute(ctx context.Context, projectId string) (*postgresflex.ListVersionsResponse, error)
GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*postgresflex.InstanceResponse, error)
GetUserExecute(ctx context.Context, projectId, instanceId, userId string) (*postgresflex.GetUserResponse, error)
}
func AvailableInstanceTypes() []string {

@@ -117,8 +123,2 @@ instanceTypes := make([]string, len(instanceTypeToReplicas))

type PostgresFlexClient interface {
ListVersionsExecute(ctx context.Context, projectId string) (*postgresflex.ListVersionsResponse, error)
GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*postgresflex.InstanceResponse, error)
GetUserExecute(ctx context.Context, projectId, instanceId, userId string) (*postgresflex.GetUserResponse, error)
}
func GetLatestPostgreSQLVersion(ctx context.Context, apiClient PostgresFlexClient, projectId string) (string, error) {

@@ -125,0 +125,0 @@ resp, err := apiClient.ListVersionsExecute(ctx, projectId)

@@ -14,2 +14,3 @@ package utils

"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/ske"

@@ -27,2 +28,3 @@ )

type skeClientMocked struct {
serviceDisabled bool
getServiceStatusFails bool

@@ -40,2 +42,5 @@ getServiceStatusResp *ske.ProjectResponse

}
if m.serviceDisabled {
return nil, &oapierror.GenericOpenAPIError{StatusCode: 404}
}
return m.getServiceStatusResp, nil

@@ -61,2 +66,3 @@ }

description string
serviceDisabled bool
getProjectFails bool

@@ -74,2 +80,8 @@ getProjectResp *ske.ProjectResponse

{
description: "project disabled (404)",
serviceDisabled: true,
isValid: true,
expectedOutput: false,
},
{
description: "project disabled 1",

@@ -96,2 +108,3 @@ getProjectResp: &ske.ProjectResponse{State: ske.PROJECTSTATE_CREATING.Ptr()},

client := &skeClientMocked{
serviceDisabled: tt.serviceDisabled,
getServiceStatusFails: tt.getProjectFails,

@@ -98,0 +111,0 @@ getServiceStatusResp: tt.getProjectResp,

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

"fmt"
"net/http"
"os"

@@ -13,2 +14,3 @@ "path/filepath"

"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/ske"

@@ -42,3 +44,10 @@ "golang.org/x/mod/semver"

if err != nil {
return false, fmt.Errorf("get SKE status: %w", err)
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if !ok {
return false, err
}
if oapiErr.StatusCode == http.StatusNotFound {
return false, nil
}
return false, err
}

@@ -45,0 +54,0 @@ return *project.State == ske.PROJECTSTATE_CREATED, nil

@@ -18,2 +18,3 @@ package utils

testInstanceId = uuid.NewString()
testUserId = uuid.NewString()
)

@@ -425,1 +426,51 @@

}
func TestGetUserName(t *testing.T) {
tests := []struct {
description string
getUserFails bool
getUserResp *sqlserverflex.GetUserResponse
isValid bool
expectedOutput string
}{
{
description: "base",
getUserResp: &sqlserverflex.GetUserResponse{
Item: &sqlserverflex.InstanceResponseUser{
Username: utils.Ptr(testUserName),
},
},
isValid: true,
expectedOutput: testUserName,
},
{
description: "get user fails",
getUserFails: true,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
client := &sqlServerFlexClientMocked{
getUserFails: tt.getUserFails,
getUserResp: tt.getUserResp,
}
output, err := GetUserName(context.Background(), client, testProjectId, testInstanceId, testUserId)
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)
}
})
}
}

@@ -20,2 +20,3 @@ package utils

GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*sqlserverflex.GetInstanceResponse, error)
GetUserExecute(ctx context.Context, projectId, instanceId, userId string) (*sqlserverflex.GetUserResponse, error)
}

@@ -95,1 +96,9 @@

}
func GetUserName(ctx context.Context, apiClient SQLServerFlexClient, projectId, instanceId, userId string) (string, error) {
resp, err := apiClient.GetUserExecute(ctx, projectId, instanceId, userId)
if err != nil {
return "", fmt.Errorf("get SQLServer Flex user: %w", err)
}
return *resp.Item.Username, nil
}

@@ -39,2 +39,8 @@ package spinner

func (s *Spinner) StopWithError() {
s.done <- true
close(s.done)
s.printer.Info("\r%s ✗ \n", s.message)
}
func (s *Spinner) animate() {

@@ -41,0 +47,0 @@ i := 0

@@ -10,2 +10,5 @@ ROOT_DIR ?= $(shell git rev-parse --show-toplevel)

fmt:
@gofmt -s -w .
# Setup and tool initialization tasks

@@ -37,2 +40,2 @@ project-help:

@echo "Generating docs..."
@go run $(SCRIPTS_BASE)/generate.go
@go run $(SCRIPTS_BASE)/generate.go

@@ -1,11 +0,12 @@

[![Go Report Card](https://goreportcard.com/badge/github.com/stackitcloud/stackit-cli)](https://goreportcard.com/report/github.com/stackitcloud/stackit-cli) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/stackitcloud/stackit-cli) [![GitHub License](https://img.shields.io/github/license/stackitcloud/stackit-cli)](https://www.apache.org/licenses/LICENSE-2.0)
<div align="center">
<br>
<p align="center">
<img src=".github/images/stackit-logo.png" alt="STACKIT logo" width="50%"/>
</p>
<img src=".github/images/stackit-logo.png" alt="STACKIT logo" width="50%"/>
<br>
<br>
</div>
# STACKIT CLI (BETA)
[![Go Report Card](https://goreportcard.com/badge/github.com/stackitcloud/stackit-cli)](https://goreportcard.com/report/github.com/stackitcloud/stackit-cli) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/stackitcloud/stackit-cli) [![GitHub License](https://img.shields.io/github/license/stackitcloud/stackit-cli)](https://www.apache.org/licenses/LICENSE-2.0)
Welcome to the [STACKIT](https://www.stackit.de/en) CLI, a command-line interface for the STACKIT services.

@@ -76,2 +77,3 @@

| Secrets Manager | `secrets-manager` | :white_check_mark: |
| Server Backup Management | `beta server backup` | :white_check_mark: |
| Service Account | `service-account` | :white_check_mark: |

@@ -78,0 +80,0 @@ | SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) |