diff --git a/.golangci.yml b/.golangci.yml index 6e62ffe0b7..ec14c0e00a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,6 +29,7 @@ linters: - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. [fast: false, auto-fix: false] - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] - exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions. [auto-fix] + - funcorder # Checks the order of functions, methods, and constructors. [fast] - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. [fast: true, auto-fix: false] - gochecksumtype # Run exhaustiveness checks on Go "sum types" [fast: false, auto-fix: false] - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] @@ -122,6 +123,7 @@ linters: linters: - unused - errcheck + - funcorder - path: internal/tabwriter/.*_test\.go linters: diff --git a/core/autocomplete.go b/core/autocomplete.go index cf805884df..d2ceecfd1f 100644 --- a/core/autocomplete.go +++ b/core/autocomplete.go @@ -57,13 +57,6 @@ type FlagSpec struct { EnumValues []string } -func (node *AutoCompleteNode) addFlags(flags []FlagSpec) { - for i := range flags { - flag := &flags[i] - node.Children[flag.Name] = NewAutoCompleteFlagNode(node, flag) - } -} - // newAutoCompleteResponse builds a new AutocompleteResponse func newAutoCompleteResponse(suggestions []string) *AutocompleteResponse { sort.Strings(suggestions) @@ -176,6 +169,13 @@ func (node *AutoCompleteNode) GetChildMatch(name string) (*AutoCompleteNode, boo return nil, false } +func (node *AutoCompleteNode) addFlags(flags []FlagSpec) { + for i := range flags { + flag := &flags[i] + node.Children[flag.Name] = NewAutoCompleteFlagNode(node, flag) + } +} + // isLeafCommand returns true only if n is a node with no child command (namespace, verb, resource) or a positional arg. // A leaf command can have 2 types of children: arguments or flags func (node *AutoCompleteNode) isLeafCommand() bool { diff --git a/core/command.go b/core/command.go index 786e01be0d..bf0c489d8f 100644 --- a/core/command.go +++ b/core/command.go @@ -2,9 +2,7 @@ package core import ( "context" - "fmt" "reflect" - "sort" "strings" "github.com/scaleway/scaleway-cli/v2/core/human" @@ -117,26 +115,6 @@ func (c *Command) Override(builder func(command *Command) *Command) { *c = *builder(c) } -func (c *Command) getPath() string { - if c.path != "" { - return c.path - } - path := []string(nil) - if c.Namespace != "" { - path = append(path, c.Namespace) - } - if c.Resource != "" { - path = append(path, c.Resource) - } - if c.Verb != "" { - path = append(path, c.Verb) - } - - c.path = strings.Join(path, indexCommandSeparator) - - return c.path -} - func (c *Command) GetCommandLine(binaryName string) string { return strings.Trim( binaryName+" "+strings.ReplaceAll(c.getPath(), indexCommandSeparator, " "), @@ -162,24 +140,6 @@ func (c *Command) GetUsage(binaryName string, commands *Commands) string { return strings.Join(parts, " ") } -// seeAlsosAsStr returns all See Alsos as a single string -func (c *Command) seeAlsosAsStr() string { - seeAlsos := make([]string, 0, len(c.SeeAlsos)) - - for _, cmdSeeAlso := range c.SeeAlsos { - short := " # " + cmdSeeAlso.Short - commandStr := " " + cmdSeeAlso.Command - - seeAlsoLines := []string{ - short, - commandStr, - } - seeAlsos = append(seeAlsos, strings.Join(seeAlsoLines, "\n")) - } - - return strings.Join(seeAlsos, "\n\n") -} - // AddInterceptors add one or multiple interceptors to a command. // These new interceptors will be added after the already present interceptors (if any). func (c *Command) AddInterceptors(interceptors ...CommandInterceptor) { @@ -204,145 +164,27 @@ func (c *Command) MatchAlias(alias alias.Alias) bool { return true } -// Commands represent a list of CLI commands, with a index to allow searching. -type Commands struct { - commands []*Command - commandIndex map[string]*Command -} - -func NewCommands(cmds ...*Command) *Commands { - c := &Commands{ - commands: make([]*Command, 0, len(cmds)), - commandIndex: make(map[string]*Command, len(cmds)), - } - - for _, cmd := range cmds { - c.Add(cmd) - } - - return c -} - -func NewCommandsMerge(cmdsList ...*Commands) *Commands { - cmdCount := 0 - for _, cmds := range cmdsList { - cmdCount += len(cmds.commands) - } - c := &Commands{ - commands: make([]*Command, 0, cmdCount), - commandIndex: make(map[string]*Command, cmdCount), - } - for _, cmds := range cmdsList { - for _, cmd := range cmds.commands { - c.Add(cmd) - } - } - - return c -} - -func (c *Commands) MustFind(path ...string) *Command { - cmd, exist := c.find(path...) - if exist { - return cmd - } - - panic(fmt.Errorf("command %v not found", strings.Join(path, " "))) -} - -func (c *Commands) Find(path ...string) *Command { - cmd, exist := c.find(path...) - if exist { - return cmd - } - - return nil -} - -func (c *Commands) Remove(namespace, verb string) { - for i := range c.commands { - if c.commands[i].Namespace == namespace && c.commands[i].Verb == verb { - c.commands = append(c.commands[:i], c.commands[i+1:]...) - - return - } - } -} - -func (c *Commands) RemoveResource(namespace, resource string) { - for i := range c.commands { - if c.commands[i].Namespace == namespace && c.commands[i].Resource == resource && - c.commands[i].Verb == "" { - c.commands = append(c.commands[:i], c.commands[i+1:]...) - - return - } - } -} - -func (c *Commands) Add(cmd *Command) { - c.commands = append(c.commands, cmd) - c.commandIndex[cmd.getPath()] = cmd -} - -func (c *Commands) Merge(cmds *Commands) { - for _, cmd := range cmds.commands { - c.Add(cmd) - } -} - -func (c *Commands) MergeAll(cmds ...*Commands) { - for _, command := range cmds { - c.Merge(command) +// Copy returns a copy of a command +func (c *Command) Copy() *Command { + newCommand := *c + newCommand.Aliases = append([]string(nil), c.Aliases...) + newCommand.Examples = make([]*Example, len(c.Examples)) + for i := range c.Examples { + e := *c.Examples[i] + newCommand.Examples[i] = &e } -} - -func (c *Commands) GetAll() []*Command { - return c.commands -} - -// find must take the command path, eg. find("instance","get","server") -func (c *Commands) find(path ...string) (*Command, bool) { - cmd, exist := c.commandIndex[strings.Join(path, indexCommandSeparator)] - if exist { - return cmd, true + newCommand.SeeAlsos = make([]*SeeAlso, len(c.SeeAlsos)) + for i := range c.SeeAlsos { + sa := *c.SeeAlsos[i] + newCommand.SeeAlsos[i] = &sa } - return nil, false -} - -// GetSortedCommand returns a slice of commands sorted alphabetically -func (c *Commands) GetSortedCommand() []*Command { - commands := make([]*Command, len(c.commands)) - copy(commands, c.commands) - sort.Slice(commands, func(i, j int) bool { - return commands[i].signature() < commands[j].signature() - }) - - return commands + return &newCommand } -func (c *Commands) HasSubCommands(cmd *Command) bool { - if cmd.Namespace != "" && cmd.Resource != "" && cmd.Verb != "" { - return false - } - if cmd.Namespace == "" && cmd.Resource == "" && cmd.Verb == "" { - return true - } - for _, command := range c.commands { - if command == cmd { - continue - } - if cmd.Resource == "" && cmd.Namespace == command.Namespace { - return true - } - if cmd.Verb == "" && cmd.Namespace == command.Namespace && - cmd.Resource == command.Resource { - return true - } - } - - return false +// get a signature to sort commands +func (c *Command) signature() string { + return c.Namespace + " " + c.Resource + " " + c.Verb + " " + c.Short } func (c *Command) getHumanMarshalerOpt() *human.MarshalOpt { @@ -353,98 +195,40 @@ func (c *Command) getHumanMarshalerOpt() *human.MarshalOpt { return nil } -// get a signature to sort commands -func (c *Command) signature() string { - return c.Namespace + " " + c.Resource + " " + c.Verb + " " + c.Short -} - -// AliasIsValidCommandChild returns true is alias is a valid child command of given command -// Useful for this case: -// isl => instance server list -// valid child of "instance" -// invalid child of "rdb instance" -func (c *Commands) AliasIsValidCommandChild(command *Command, alias alias.Alias) bool { - // if alias is of size one, it means it cannot be a child - if len(alias.Command) == 1 { - return true - } - - // if command is verb, it cannot have children - if command.Verb != "" { - return true - } +// seeAlsosAsStr returns all See Alsos as a single string +func (c *Command) seeAlsosAsStr() string { + seeAlsos := make([]string, 0, len(c.SeeAlsos)) - // if command is a resource, check command with alias' verb - if command.Resource != "" { - return c.Find(command.Namespace, command.Resource, alias.Command[1]) != nil - } + for _, cmdSeeAlso := range c.SeeAlsos { + short := " # " + cmdSeeAlso.Short + commandStr := " " + cmdSeeAlso.Command - // if command is a namespace, check for alias' verb or resource - if command.Namespace != "" { - if len(alias.Command) > 2 { - return c.Find(command.Namespace, alias.Command[1], alias.Command[2]) != nil + seeAlsoLines := []string{ + short, + commandStr, } - - return c.Find(command.Namespace, alias.Command[1]) != nil + seeAlsos = append(seeAlsos, strings.Join(seeAlsoLines, "\n")) } - return false + return strings.Join(seeAlsos, "\n\n") } -// addAliases add valid aliases to a command -func (c *Commands) addAliases(command *Command, aliases []alias.Alias) { - names := make([]string, 0, len(aliases)) - for i := range aliases { - if c.AliasIsValidCommandChild(command, aliases[i]) && command.MatchAlias(aliases[i]) { - names = append(names, aliases[i].Name) - } +func (c *Command) getPath() string { + if c.path != "" { + return c.path } - command.Aliases = append(command.Aliases, names...) -} - -// applyAliases add resource aliases to each commands -func (c *Commands) applyAliases(config *alias.Config) { - for _, command := range c.commands { - aliases := []alias.Alias(nil) - exists := false - switch { - case command.Verb != "": - aliases, exists = config.ResolveAliasesByFirstWord(command.Verb) - case command.Resource != "": - aliases, exists = config.ResolveAliasesByFirstWord(command.Resource) - case command.Namespace != "": - aliases, exists = config.ResolveAliasesByFirstWord(command.Namespace) - } - if exists { - c.addAliases(command, aliases) - } + path := []string(nil) + if c.Namespace != "" { + path = append(path, c.Namespace) } -} - -// Copy returns a copy of a command -func (c *Command) Copy() *Command { - newCommand := *c - newCommand.Aliases = append([]string(nil), c.Aliases...) - newCommand.Examples = make([]*Example, len(c.Examples)) - for i := range c.Examples { - e := *c.Examples[i] - newCommand.Examples[i] = &e + if c.Resource != "" { + path = append(path, c.Resource) } - newCommand.SeeAlsos = make([]*SeeAlso, len(c.SeeAlsos)) - for i := range c.SeeAlsos { - sa := *c.SeeAlsos[i] - newCommand.SeeAlsos[i] = &sa + if c.Verb != "" { + path = append(path, c.Verb) } - return &newCommand -} - -// Copy return a copy of all commands -func (c *Commands) Copy() *Commands { - newCommands := make([]*Command, len(c.commands)) - for i := range c.commands { - newCommands[i] = c.commands[i].Copy() - } + c.path = strings.Join(path, indexCommandSeparator) - return NewCommands(newCommands...) + return c.path } diff --git a/core/commands.go b/core/commands.go new file mode 100644 index 0000000000..d49fcafcc5 --- /dev/null +++ b/core/commands.go @@ -0,0 +1,223 @@ +package core + +import ( + "fmt" + "sort" + "strings" + + "github.com/scaleway/scaleway-cli/v2/internal/alias" +) + +// Commands represent a list of CLI commands, with a index to allow searching. +type Commands struct { + commands []*Command + commandIndex map[string]*Command +} + +func NewCommands(cmds ...*Command) *Commands { + c := &Commands{ + commands: make([]*Command, 0, len(cmds)), + commandIndex: make(map[string]*Command, len(cmds)), + } + + for _, cmd := range cmds { + c.Add(cmd) + } + + return c +} + +func NewCommandsMerge(cmdsList ...*Commands) *Commands { + cmdCount := 0 + for _, cmds := range cmdsList { + cmdCount += len(cmds.commands) + } + c := &Commands{ + commands: make([]*Command, 0, cmdCount), + commandIndex: make(map[string]*Command, cmdCount), + } + for _, cmds := range cmdsList { + for _, cmd := range cmds.commands { + c.Add(cmd) + } + } + + return c +} + +func (c *Commands) MustFind(path ...string) *Command { + cmd, exist := c.find(path...) + if exist { + return cmd + } + + panic(fmt.Errorf("command %v not found", strings.Join(path, " "))) +} + +func (c *Commands) Find(path ...string) *Command { + cmd, exist := c.find(path...) + if exist { + return cmd + } + + return nil +} + +func (c *Commands) Remove(namespace, verb string) { + for i := range c.commands { + if c.commands[i].Namespace == namespace && c.commands[i].Verb == verb { + c.commands = append(c.commands[:i], c.commands[i+1:]...) + + return + } + } +} + +func (c *Commands) RemoveResource(namespace, resource string) { + for i := range c.commands { + if c.commands[i].Namespace == namespace && c.commands[i].Resource == resource && + c.commands[i].Verb == "" { + c.commands = append(c.commands[:i], c.commands[i+1:]...) + + return + } + } +} + +func (c *Commands) Add(cmd *Command) { + c.commands = append(c.commands, cmd) + c.commandIndex[cmd.getPath()] = cmd +} + +func (c *Commands) Merge(cmds *Commands) { + for _, cmd := range cmds.commands { + c.Add(cmd) + } +} + +func (c *Commands) MergeAll(cmds ...*Commands) { + for _, command := range cmds { + c.Merge(command) + } +} + +func (c *Commands) GetAll() []*Command { + return c.commands +} + +// GetSortedCommand returns a slice of commands sorted alphabetically +func (c *Commands) GetSortedCommand() []*Command { + commands := make([]*Command, len(c.commands)) + copy(commands, c.commands) + sort.Slice(commands, func(i, j int) bool { + return commands[i].signature() < commands[j].signature() + }) + + return commands +} + +func (c *Commands) HasSubCommands(cmd *Command) bool { + if cmd.Namespace != "" && cmd.Resource != "" && cmd.Verb != "" { + return false + } + if cmd.Namespace == "" && cmd.Resource == "" && cmd.Verb == "" { + return true + } + for _, command := range c.commands { + if command == cmd { + continue + } + if cmd.Resource == "" && cmd.Namespace == command.Namespace { + return true + } + if cmd.Verb == "" && cmd.Namespace == command.Namespace && + cmd.Resource == command.Resource { + return true + } + } + + return false +} + +// AliasIsValidCommandChild returns true is alias is a valid child command of given command +// Useful for this case: +// isl => instance server list +// valid child of "instance" +// invalid child of "rdb instance" +func (c *Commands) AliasIsValidCommandChild(command *Command, alias alias.Alias) bool { + // if alias is of size one, it means it cannot be a child + if len(alias.Command) == 1 { + return true + } + + // if command is verb, it cannot have children + if command.Verb != "" { + return true + } + + // if command is a resource, check command with alias' verb + if command.Resource != "" { + return c.Find(command.Namespace, command.Resource, alias.Command[1]) != nil + } + + // if command is a namespace, check for alias' verb or resource + if command.Namespace != "" { + if len(alias.Command) > 2 { + return c.Find(command.Namespace, alias.Command[1], alias.Command[2]) != nil + } + + return c.Find(command.Namespace, alias.Command[1]) != nil + } + + return false +} + +// Copy return a copy of all commands +func (c *Commands) Copy() *Commands { + newCommands := make([]*Command, len(c.commands)) + for i := range c.commands { + newCommands[i] = c.commands[i].Copy() + } + + return NewCommands(newCommands...) +} + +// find must take the command path, eg. find("instance","get","server") +func (c *Commands) find(path ...string) (*Command, bool) { + cmd, exist := c.commandIndex[strings.Join(path, indexCommandSeparator)] + if exist { + return cmd, true + } + + return nil, false +} + +// addAliases add valid aliases to a command +func (c *Commands) addAliases(command *Command, aliases []alias.Alias) { + names := make([]string, 0, len(aliases)) + for i := range aliases { + if c.AliasIsValidCommandChild(command, aliases[i]) && command.MatchAlias(aliases[i]) { + names = append(names, aliases[i].Name) + } + } + command.Aliases = append(command.Aliases, names...) +} + +// applyAliases add resource aliases to each commands +func (c *Commands) applyAliases(config *alias.Config) { + for _, command := range c.commands { + aliases := []alias.Alias(nil) + exists := false + switch { + case command.Verb != "": + aliases, exists = config.ResolveAliasesByFirstWord(command.Verb) + case command.Resource != "": + aliases, exists = config.ResolveAliasesByFirstWord(command.Resource) + case command.Namespace != "": + aliases, exists = config.ResolveAliasesByFirstWord(command.Namespace) + } + if exists { + c.addAliases(command, aliases) + } + } +} diff --git a/core/printer.go b/core/printer.go index f9d4caa09f..5ab4edf9ec 100644 --- a/core/printer.go +++ b/core/printer.go @@ -47,6 +47,21 @@ type PrinterConfig struct { Stderr io.Writer } +type Printer struct { + printerType PrinterType + stdout io.Writer + stderr io.Writer + + // Enable pretty print on json output + jsonPretty bool + + // go template to use on template output + template *template.Template + + // Allow to select specifics column in a table with human printer + humanFields []string +} + // NewPrinter returns an initialized formatter corresponding to a given FormatterType. func NewPrinter(config *PrinterConfig) (*Printer, error) { printer := &Printer{ @@ -147,21 +162,6 @@ func setupWidePrinter(printer *Printer, opts string) { printer.printerType = PrinterTypeWide } -type Printer struct { - printerType PrinterType - stdout io.Writer - stderr io.Writer - - // Enable pretty print on json output - jsonPretty bool - - // go template to use on template output - template *template.Template - - // Allow to select specifics column in a table with human printer - humanFields []string -} - func (p *Printer) Print(data interface{}, opt *human.MarshalOpt) error { // No matter the printer type if data is a RawResult we should print it as is. if rawResult, isRawResult := data.(RawResult); isRawResult { diff --git a/core/shell.go b/core/shell.go index 969d2f36d3..fc8d45010f 100644 --- a/core/shell.go +++ b/core/shell.go @@ -24,6 +24,12 @@ type Completer struct { ctx context.Context } +func NewShellCompleter(ctx context.Context) *Completer { + return &Completer{ + ctx: ctx, + } +} + type ShellSuggestion struct { Text string Arg *ArgSpec @@ -262,12 +268,6 @@ func (c *Completer) Complete(d prompt.Document) []prompt.Suggest { return prompt.FilterHasPrefix(suggestions, currentArg, true) } -func NewShellCompleter(ctx context.Context) *Completer { - return &Completer{ - ctx: ctx, - } -} - // shellExecutor returns the function that will execute command entered in shell func shellExecutor(rootCmd *cobra.Command, printer *Printer, meta *Meta) func(s string) { return func(s string) { diff --git a/internal/alias/alias.go b/internal/alias/alias.go index 29f92d564f..74c53d1028 100644 --- a/internal/alias/alias.go +++ b/internal/alias/alias.go @@ -12,6 +12,14 @@ type Alias struct { args []string } +func (a *Alias) Args() []string { + if a.args == nil { + a.computeArgs() + } + + return a.args +} + func (a *Alias) computeArgs() { a.args = []string{} for _, cmd := range a.Command { @@ -21,11 +29,3 @@ func (a *Alias) computeArgs() { } } } - -func (a *Alias) Args() []string { - if a.args == nil { - a.computeArgs() - } - - return a.args -} diff --git a/internal/alias/config.go b/internal/alias/config.go index 2fa4c425c7..a91b62d580 100644 --- a/internal/alias/config.go +++ b/internal/alias/config.go @@ -64,20 +64,6 @@ func (c *Config) DeleteAlias(name string) bool { return exists } -func (c *Config) fillAliasByFirstWord() { - c.aliasesByFirstWord = make(map[string][]Alias, len(c.Aliases)) - for alias, cmd := range c.Aliases { - if len(cmd) == 0 { - continue - } - path := cmd[0] - c.aliasesByFirstWord[path] = append(c.aliasesByFirstWord[path], Alias{ - Name: alias, - Command: cmd, - }) - } -} - // ResolveAliasesByFirstWord return list of aliases that start with given first word // firstWord: instance // may return @@ -91,3 +77,17 @@ func (c *Config) ResolveAliasesByFirstWord(firstWord string) ([]Alias, bool) { return alias, ok } + +func (c *Config) fillAliasByFirstWord() { + c.aliasesByFirstWord = make(map[string][]Alias, len(c.Aliases)) + for alias, cmd := range c.Aliases { + if len(cmd) == 0 { + continue + } + path := cmd[0] + c.aliasesByFirstWord[path] = append(c.aliasesByFirstWord[path], Alias{ + Name: alias, + Command: cmd, + }) + } +} diff --git a/internal/args/args.go b/internal/args/args.go index 8ce03d71ff..f2cd763229 100644 --- a/internal/args/args.go +++ b/internal/args/args.go @@ -172,17 +172,6 @@ func (a RawArgs) Remove(argName string) RawArgs { }) } -func (a RawArgs) filter(test func(string) bool) RawArgs { - argsCopy := RawArgs{} - for _, arg := range a { - if test(arg) { - argsCopy = append(argsCopy, arg) - } - } - - return argsCopy -} - func (a RawArgs) GetSliceOrMapKeys(prefix string) []string { keys := []string(nil) for _, arg := range a { @@ -198,6 +187,17 @@ func (a RawArgs) GetSliceOrMapKeys(prefix string) []string { return keys } +func (a RawArgs) filter(test func(string) bool) RawArgs { + argsCopy := RawArgs{} + for _, arg := range a { + if test(arg) { + argsCopy = append(argsCopy, arg) + } + } + + return argsCopy +} + func splitArg(arg string) (name string, value string) { part := strings.SplitN(arg, "=", 2) if len(part) == 1 { diff --git a/internal/namespaces/instance/v1/custom_server_create_builder.go b/internal/namespaces/instance/v1/custom_server_create_builder.go index 51245dc4f2..bb15013f18 100644 --- a/internal/namespaces/instance/v1/custom_server_create_builder.go +++ b/internal/namespaces/instance/v1/custom_server_create_builder.go @@ -121,26 +121,6 @@ func (sb *ServerBuilder) AddAdminPasswordEncryptionSSHKeyID( return sb } -func (sb *ServerBuilder) isWindows() bool { - return commercialTypeIsWindowsServer(sb.createReq.CommercialType) -} - -func (sb *ServerBuilder) rootVolumeIsSBS() bool { - if sb.rootVolume == nil { - return true // Default to SBS if no volume type is requested. Local SSD is now only on explicit request. - } - - return sb.rootVolume.VolumeType == instance.VolumeVolumeTypeSbsVolume -} - -func (sb *ServerBuilder) marketplaceImageType() marketplace.LocalImageType { - if sb.rootVolumeIsSBS() { - return marketplace.LocalImageTypeInstanceSbs - } - - return marketplace.LocalImageTypeInstanceLocal -} - // AddImage handle a custom image argument. // image could be: // - A local image UUID. @@ -240,16 +220,6 @@ func (sb *ServerBuilder) AddIP(ip string) (*ServerBuilder, error) { return sb, nil } -func (sb *ServerBuilder) addIPID(ipID string) *ServerBuilder { - if sb.createReq.PublicIPs == nil { - sb.createReq.PublicIPs = new([]string) - } - - *sb.createReq.PublicIPs = append(*sb.createReq.PublicIPs, ipID) - - return sb -} - // AddVolumes build volume templates from arguments. // // More format details in buildVolumeTemplate function. @@ -522,6 +492,36 @@ func (sb *ServerBuilder) BuildPostCreationSetup() PostServerCreationSetupFunc { } } +func (sb *ServerBuilder) addIPID(ipID string) *ServerBuilder { + if sb.createReq.PublicIPs == nil { + sb.createReq.PublicIPs = new([]string) + } + + *sb.createReq.PublicIPs = append(*sb.createReq.PublicIPs, ipID) + + return sb +} + +func (sb *ServerBuilder) isWindows() bool { + return commercialTypeIsWindowsServer(sb.createReq.CommercialType) +} + +func (sb *ServerBuilder) rootVolumeIsSBS() bool { + if sb.rootVolume == nil { + return true // Default to SBS if no volume type is requested. Local SSD is now only on explicit request. + } + + return sb.rootVolume.VolumeType == instance.VolumeVolumeTypeSbsVolume +} + +func (sb *ServerBuilder) marketplaceImageType() marketplace.LocalImageType { + if sb.rootVolumeIsSBS() { + return marketplace.LocalImageTypeInstanceSbs + } + + return marketplace.LocalImageTypeInstanceLocal +} + type VolumeBuilder struct { Zone scw.Zone VolumeType instance.VolumeVolumeType @@ -599,59 +599,41 @@ func NewVolumeBuilder(zone scw.Zone, flagV string) (*VolumeBuilder, error) { } } -// buildSnapshotVolume builds the requested volume template to create a new volume from a snapshot -func (vb *VolumeBuilder) buildSnapshotVolume( - api *instance.API, - blockAPI *block.API, +// BuildVolumeServerTemplate builds the requested volume template to be used in a CreateServerRequest +func (vb *VolumeBuilder) BuildVolumeServerTemplate( + apiInstance *instance.API, + apiBlock *block.API, ) (*instance.VolumeServerTemplate, error) { - if vb.SnapshotID == nil { - return nil, errors.New("tried to build a volume from snapshot with an empty ID") - } - res, err := api.GetSnapshot(&instance.GetSnapshotRequest{ - Zone: vb.Zone, - SnapshotID: *vb.SnapshotID, - }) - if err != nil && !core.IsNotFoundError(err) { - return nil, fmt.Errorf("invalid snapshot %s: %w", *vb.SnapshotID, err) + if vb.SnapshotID != nil { + return vb.buildSnapshotVolume(apiInstance, apiBlock) } - if res != nil { - snapshotType := res.Snapshot.VolumeType - - if snapshotType != instance.VolumeVolumeTypeUnified && snapshotType != vb.VolumeType { - return nil, fmt.Errorf( - "snapshot of type %s not compatible with requested volume type %s", - snapshotType, - vb.VolumeType, - ) - } - - return &instance.VolumeServerTemplate{ - Name: &res.Snapshot.Name, - VolumeType: vb.VolumeType, - BaseSnapshot: &res.Snapshot.ID, - Size: &res.Snapshot.Size, - }, nil + if vb.VolumeID != nil { + return vb.buildImportedVolume(apiInstance, apiBlock) } - blockRes, err := blockAPI.GetSnapshot(&block.GetSnapshotRequest{ - Zone: vb.Zone, - SnapshotID: *vb.SnapshotID, - }) - if err != nil { - if core.IsNotFoundError(err) { - return nil, fmt.Errorf("snapshot %s does not exist", *vb.SnapshotID) - } + return vb.buildNewVolume() +} - return nil, err +// ExecutePostCreationSetup executes requests that are required after volume creation. +func (vb *VolumeBuilder) ExecutePostCreationSetup( + ctx context.Context, + apiBlock *block.API, + volumeID string, +) { + if vb.IOPS != nil { + _, err := apiBlock.UpdateVolume(&block.UpdateVolumeRequest{ + VolumeID: volumeID, + PerfIops: vb.IOPS, + Zone: vb.Zone, + }, + scw.WithContext(ctx), + ) + if err != nil { + core.ExtractLogger(ctx). + Warning(fmt.Sprintf("Failed to update volume %s IOPS: %s", volumeID, err.Error())) + } } - - return &instance.VolumeServerTemplate{ - Name: &blockRes.Name, - VolumeType: vb.VolumeType, - BaseSnapshot: &blockRes.ID, - Size: &blockRes.Size, - }, nil } // buildImportedVolume builds the requested volume template to import an existing volume @@ -723,39 +705,57 @@ func (vb *VolumeBuilder) buildNewVolume() (*instance.VolumeServerTemplate, error }, nil } -// BuildVolumeServerTemplate builds the requested volume template to be used in a CreateServerRequest -func (vb *VolumeBuilder) BuildVolumeServerTemplate( - apiInstance *instance.API, - apiBlock *block.API, +// buildSnapshotVolume builds the requested volume template to create a new volume from a snapshot +func (vb *VolumeBuilder) buildSnapshotVolume( + api *instance.API, + blockAPI *block.API, ) (*instance.VolumeServerTemplate, error) { - if vb.SnapshotID != nil { - return vb.buildSnapshotVolume(apiInstance, apiBlock) + if vb.SnapshotID == nil { + return nil, errors.New("tried to build a volume from snapshot with an empty ID") } - - if vb.VolumeID != nil { - return vb.buildImportedVolume(apiInstance, apiBlock) + res, err := api.GetSnapshot(&instance.GetSnapshotRequest{ + Zone: vb.Zone, + SnapshotID: *vb.SnapshotID, + }) + if err != nil && !core.IsNotFoundError(err) { + return nil, fmt.Errorf("invalid snapshot %s: %w", *vb.SnapshotID, err) } - return vb.buildNewVolume() -} + if res != nil { + snapshotType := res.Snapshot.VolumeType -// ExecutePostCreationSetup executes requests that are required after volume creation. -func (vb *VolumeBuilder) ExecutePostCreationSetup( - ctx context.Context, - apiBlock *block.API, - volumeID string, -) { - if vb.IOPS != nil { - _, err := apiBlock.UpdateVolume(&block.UpdateVolumeRequest{ - VolumeID: volumeID, - PerfIops: vb.IOPS, - Zone: vb.Zone, - }, - scw.WithContext(ctx), - ) - if err != nil { - core.ExtractLogger(ctx). - Warning(fmt.Sprintf("Failed to update volume %s IOPS: %s", volumeID, err.Error())) + if snapshotType != instance.VolumeVolumeTypeUnified && snapshotType != vb.VolumeType { + return nil, fmt.Errorf( + "snapshot of type %s not compatible with requested volume type %s", + snapshotType, + vb.VolumeType, + ) } + + return &instance.VolumeServerTemplate{ + Name: &res.Snapshot.Name, + VolumeType: vb.VolumeType, + BaseSnapshot: &res.Snapshot.ID, + Size: &res.Snapshot.Size, + }, nil + } + + blockRes, err := blockAPI.GetSnapshot(&block.GetSnapshotRequest{ + Zone: vb.Zone, + SnapshotID: *vb.SnapshotID, + }) + if err != nil { + if core.IsNotFoundError(err) { + return nil, fmt.Errorf("snapshot %s does not exist", *vb.SnapshotID) + } + + return nil, err } + + return &instance.VolumeServerTemplate{ + Name: &blockRes.Name, + VolumeType: vb.VolumeType, + BaseSnapshot: &blockRes.ID, + Size: &blockRes.Size, + }, nil } diff --git a/internal/tabwriter/tabwriter_test.go b/internal/tabwriter/tabwriter_test.go index 2f857b8ac5..2f2c0922ba 100644 --- a/internal/tabwriter/tabwriter_test.go +++ b/internal/tabwriter/tabwriter_test.go @@ -18,10 +18,6 @@ type buffer struct { a []byte } -func (b *buffer) init(n int) { b.a = make([]byte, 0, n) } - -func (b *buffer) clear() { b.a = b.a[0:0] } - func (b *buffer) Write(buf []byte) (written int, err error) { n := len(b.a) m := len(buf) @@ -39,6 +35,10 @@ func (b *buffer) Write(buf []byte) (written int, err error) { func (b *buffer) String() string { return string(b.a) } +func (b *buffer) init(n int) { b.a = make([]byte, 0, n) } + +func (b *buffer) clear() { b.a = b.a[0:0] } + func write(t *testing.T, testname string, w *tabwriter.Writer, src string) { t.Helper() written, err := io.WriteString(w, src)