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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions core/cobra_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -84,16 +85,16 @@ func testGetCommands() *core.Commands {
ArgsType: reflect.TypeOf(args.RawArgs{}),
AllowAnonymousClient: true,
Run: func(_ context.Context, argsI any) (i any, e error) {
res := ""
rawArgs := *argsI.(*args.RawArgs)
var builder strings.Builder
for i, arg := range rawArgs {
res += arg
builder.WriteString(arg)
if i != len(rawArgs)-1 {
res += " "
builder.WriteString(" ")
}
}

return res, nil
return builder.String(), nil
},
},
&core.Command{
Expand Down
1 change: 0 additions & 1 deletion internal/gotty/resize_windows.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//go:build windows
// +build windows

package gotty

Expand Down
14 changes: 9 additions & 5 deletions internal/interactive/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"os"
"strings"

tea "github.com/charmbracelet/bubbletea"
)
Expand Down Expand Up @@ -52,19 +53,22 @@ func (m *ListPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

func (m *ListPrompt) View() string {
s := m.Prompt + "\n\n"
var builder strings.Builder
builder.WriteString(m.Prompt)
builder.WriteString("\n\n")

for i, choice := range m.Choices {
if m.cursor == i {
s += fmt.Sprintf("> %s\n", choice)
builder.WriteString(fmt.Sprintf("> %s\n", choice))
} else {
s += choice + "\n"
builder.WriteString(choice)
builder.WriteString("\n")
}
}

s += "\nPress enter or space for select.\n"
builder.WriteString("\nPress enter or space for select.\n")

return s
return builder.String()
}

// Execute start the prompt and return the selected index
Expand Down
7 changes: 1 addition & 6 deletions internal/interactive/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,5 @@ func RemoveIndent(str string) string {
}

func makeStr(char string, length int) string {
str := ""
for range length {
str += char
}

return str
return strings.Repeat(char, length)
}
4 changes: 3 additions & 1 deletion internal/namespaces/autocomplete/autocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,15 +455,17 @@ func TrimText(str string) string {
for i, line := range lines {
if !foundFirstNonEmptyLine {
if len(line) > 0 {
var builder strings.Builder
for _, c := range line {
if c == ' ' || c == '\t' {
strToRemove += string(c)
builder.WriteRune(c)

continue
}

break
}
strToRemove = builder.String()
foundFirstNonEmptyLine = true
}
}
Expand Down
8 changes: 5 additions & 3 deletions internal/namespaces/init/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"path"
"regexp"
"strings"
"testing"

"github.com/scaleway/scaleway-cli/v2/core"
Expand All @@ -26,12 +27,13 @@ func checkConfig(
}

func appendArgs(prefix string, args map[string]string) string {
cmd := prefix
var builder strings.Builder
builder.WriteString(prefix)
for k, v := range args {
cmd += fmt.Sprintf(" %s=%s", k, v)
builder.WriteString(fmt.Sprintf(" %s=%s", k, v))
}

return cmd
return builder.String()
}

func beforeFuncSaveConfig(config *scw.Config) core.BeforeFunc {
Expand Down
17 changes: 11 additions & 6 deletions internal/namespaces/instance/v1/custom_security_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,18 @@ func securityGroupDeleteBuilder(c *core.Command) *core.Command {
return nil, newError
}

// Create detail message.
hint := "Attach all these instances to another security-group before deleting this one:"
for _, s := range sg.SecurityGroup.Servers {
hint += "\nscw instance server update " + s.ID + " security-group.id=$NEW_SECURITY_GROUP_ID"
}
// Create detail message.
var hintBuilder strings.Builder
hintBuilder.WriteString(
"Attach all these instances to another security-group before deleting this one:",
)
for _, s := range sg.SecurityGroup.Servers {
hintBuilder.WriteString("\nscw instance server update ")
hintBuilder.WriteString(s.ID)
hintBuilder.WriteString(" security-group.id=$NEW_SECURITY_GROUP_ID")
}

newError.Hint = hint
newError.Hint = hintBuilder.String()

return nil, newError
}
Expand Down
3 changes: 3 additions & 0 deletions internal/namespaces/rdb/v1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func GetCommands() *core.Commands {
cmds.MustFind("rdb", "instance", "update").Override(instanceUpdateBuilder)
cmds.MustFind("rdb", "instance", "upgrade").Override(instanceUpgradeBuilder)

// Make database create idempotent
cmds.MustFind("rdb", "database", "create").Override(databaseCreateBuilder)

cmds.MustFind("rdb", "user", "create").Override(userCreateBuilder)
cmds.MustFind("rdb", "user", "list").Override(userListBuilder)
cmds.MustFind("rdb", "user", "update").Override(userUpdateBuilder)
Expand Down
33 changes: 33 additions & 0 deletions internal/namespaces/rdb/v1/custom_database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package rdb

import (
"context"

"github.com/scaleway/scaleway-cli/v2/core"
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

func databaseCreateBuilder(c *core.Command) *core.Command {
c.Interceptor = func(ctx context.Context, argsI any, runner core.CommandRunner) (any, error) {
req := argsI.(*rdb.CreateDatabaseRequest)
api := rdb.NewAPI(core.ExtractClient(ctx))
if req != nil && req.Name != "" {
name := req.Name
list, err := api.ListDatabases(&rdb.ListDatabasesRequest{
Region: req.Region,
InstanceID: req.InstanceID,
Name: &name,
}, scw.WithAllPages())
if err == nil && list.TotalCount > 0 {
// Return the existing database to maintain compatibility with workflows
// that expect database object (not just SuccessResult)
return list.Databases[0], nil
}
}

return runner(ctx, argsI)
}

return c
}
143 changes: 143 additions & 0 deletions internal/namespaces/rdb/v1/custom_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,152 @@ func instanceGetBuilder(c *core.Command) *core.Command {
return c
}

// instanceUpgradeInterceptor checks if upgrade is needed and returns success if no change
func instanceUpgradeInterceptor(ctx context.Context, argsI any, runner core.CommandRunner) (any, error) {
req := argsI.(*rdbSDK.UpgradeInstanceRequest)

client := core.ExtractClient(ctx)
api := rdbSDK.NewAPI(client)

current, err := api.GetInstance(&rdbSDK.GetInstanceRequest{
Region: req.Region,
InstanceID: req.InstanceID,
})
if err != nil {
return nil, err
}

changeRequested := checkUpgradeChanges(req, current)

if !changeRequested {
return &core.SuccessResult{Message: "Nothing to do!"}, nil
}

return runner(ctx, argsI)
}

// checkUpgradeChanges determines if an upgrade request would change the instance
func checkUpgradeChanges(req *rdbSDK.UpgradeInstanceRequest, current *rdbSDK.Instance) bool {
rv := reflect.ValueOf(req).Elem()

// NodeType
if f := rv.FieldByName("NodeType"); f.IsValid() {
if checkNodeTypeChange(f, current.NodeType) {
return true
}
}

// EnableHa
if f := rv.FieldByName("EnableHa"); f.IsValid() {
if checkEnableHaChange(f, current.IsHaCluster) {
return true
}
}

// VolumeType
if f := rv.FieldByName("VolumeType"); f.IsValid() && current.Volume != nil {
if checkVolumeTypeChange(f, current.Volume.Type) {
return true
}
}

// VolumeSize
if current.Volume != nil {
if f := rv.FieldByName("VolumeSize"); f.IsValid() {
if checkVolumeSizeChange(f, current.Volume.Size) {
return true
}
}
}

// Engine upgrade
if checkEngineUpgrade(rv) {
return true
}

return false
}

func checkNodeTypeChange(f reflect.Value, currentNodeType string) bool {
switch v := f.Interface().(type) {
case string:
return v != "" && !strings.EqualFold(currentNodeType, v)
case *string:
return v != nil && !strings.EqualFold(currentNodeType, *v)
}
return false
}

func checkEnableHaChange(f reflect.Value, isHaCluster bool) bool {
switch v := f.Interface().(type) {
case bool:
return v && !isHaCluster
case *bool:
return v != nil && *v && !isHaCluster
}
return false
}

func checkVolumeTypeChange(f reflect.Value, currentType rdbSDK.VolumeType) bool {
desired := ""
switch v := f.Interface().(type) {
case string:
desired = v
case *string:
if v != nil {
desired = *v
}
default:
desired = fmt.Sprint(f.Interface())
}
return desired != "" && fmt.Sprint(currentType) != desired
}

func checkVolumeSizeChange(f reflect.Value, currentSize scw.Size) bool {
switch v := f.Interface().(type) {
case uint64:
return v > uint64(currentSize)
case *uint64:
return v != nil && *v > uint64(currentSize)
}
return false
}

func checkEngineUpgrade(rv reflect.Value) bool {
if f := rv.FieldByName("UpgradableVersionID"); f.IsValid() {
switch v := f.Interface().(type) {
case string:
if v != "" {
return true
}
case *string:
if v != nil && *v != "" {
return true
}
}
}
if f := rv.FieldByName("MajorUpgradeWorkflow"); f.IsValid() && !f.IsZero() {
wf := f
if wf.Kind() == reflect.Ptr && !wf.IsNil() {
wf = wf.Elem()
}
if uv := wf.FieldByName("UpgradableVersionID"); uv.IsValid() {
switch v := uv.Interface().(type) {
case string:
return v != ""
case *string:
return v != nil && *v != ""
}
}
}
return false
}

func instanceUpgradeBuilder(c *core.Command) *core.Command {
c.ArgSpecs.GetByName("node-type").AutoCompleteFunc = autoCompleteNodeType

c.Interceptor = instanceUpgradeInterceptor

c.WaitFunc = func(ctx context.Context, _, respI any) (any, error) {
api := rdbSDK.NewAPI(core.ExtractClient(ctx))

Expand Down
17 changes: 1 addition & 16 deletions internal/namespaces/rdb/v1/custom_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,7 @@ func generateURL(ctx context.Context, argsI any) (any, error) {

// Finally we add the database if it was given
if args.Db != "" {
databases, err := api.ListDatabases(&rdb.ListDatabasesRequest{
Region: args.Region,
InstanceID: args.InstanceID,
Name: &args.Db,
}, scw.WithContext(ctx), scw.WithAllPages())
if err != nil {
return nil, fmt.Errorf("failed to list databases for instance %q", args.InstanceID)
}
if databases.TotalCount != 1 {
return nil, fmt.Errorf(
"expected 1 database with the name %q, got %d",
args.Db,
databases.TotalCount,
)
}
u = u.JoinPath(databases.Databases[0].Name)
u = u.JoinPath(args.Db)
}

return u.String(), nil
Expand Down
18 changes: 18 additions & 0 deletions internal/namespaces/rdb/v1/custom_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ func userCreateBuilder(c *core.Command) *core.Command {
customRequest := argsI.(*rdbCreateUserRequestCustom)
createUserRequest := customRequest.CreateUserRequest

// Idempotency: if user already exists, return the existing user
if createUserRequest != nil && createUserRequest.Name != "" {
name := createUserRequest.Name
users, err := api.ListUsers(&rdb.ListUsersRequest{
Region: createUserRequest.Region,
InstanceID: createUserRequest.InstanceID,
Name: &name,
}, scw.WithAllPages())
if err == nil && users.TotalCount > 0 {
// Return existing user object to maintain compatibility
result := rdbCreateUserResponseCustom{
User: users.Users[0],
Password: "", // no password for existing user
}
return result, nil
}
}

var err error
if customRequest.GeneratePassword && customRequest.Password == "" {
createUserRequest.Password, err = passwordgenerator.GeneratePassword(21, 1, 1, 1, 1)
Expand Down
Loading
Loading