Skip to content
Draft
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
5 changes: 2 additions & 3 deletions cmd/upctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
)

func main() {
if err := core.BootstrapCLI(os.Args); err != nil {
os.Exit(1)
}
exitCode := core.Execute()
os.Exit(exitCode)
}
11 changes: 11 additions & 0 deletions internal/clierrors/command_failed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package clierrors

import "fmt"

type CommandFailedError struct {
FailedCount int
}

func (err CommandFailedError) Error() string {
return fmt.Sprintf("Command execution failed for %d resource(s)", err.FailedCount)
}
36 changes: 2 additions & 34 deletions internal/commands/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,16 @@ import (
"github.com/UpCloudLtd/upcloud-cli/internal/commands/root"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/router"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/server"
serverfirewall "github.com/UpCloudLtd/upcloud-cli/internal/commands/server/firewall"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/server/networkinterface"
serverstorage "github.com/UpCloudLtd/upcloud-cli/internal/commands/server/storage"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/storage"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/zone"
"github.com/UpCloudLtd/upcloud-cli/internal/config"

"github.com/spf13/cobra"
)

// BuildCommands is the main function that sets up the commands provided by upctl.
// BuildCommands is the main function that sets up the top-level commands provided by upctl.
func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
// Servers
serverCommand := commands.BuildCommand(server.BaseServerCommand(), rootCmd, conf)
commands.BuildCommand(server.ListCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.PlanListCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.ShowCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.StartCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.RestartCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.StopCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.CreateCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.ModifyCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.LoadCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.EjectCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(server.DeleteCommand(), serverCommand.Cobra(), conf)

// Server Network Interfaces
networkInterfaceCommand := commands.BuildCommand(networkinterface.BaseNetworkInterfaceCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(networkinterface.CreateCommand(), networkInterfaceCommand.Cobra(), conf)
commands.BuildCommand(networkinterface.ModifyCommand(), networkInterfaceCommand.Cobra(), conf)
commands.BuildCommand(networkinterface.DeleteCommand(), networkInterfaceCommand.Cobra(), conf)

// Server storage operations
serverStorageCommand := commands.BuildCommand(serverstorage.BaseServerStorageCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(serverstorage.AttachCommand(), serverStorageCommand.Cobra(), conf)
commands.BuildCommand(serverstorage.DetachCommand(), serverStorageCommand.Cobra(), conf)

// Server firewall operations
serverFirewallCommand := commands.BuildCommand(serverfirewall.BaseServerFirewallCommand(), serverCommand.Cobra(), conf)
commands.BuildCommand(serverfirewall.CreateCommand(), serverFirewallCommand.Cobra(), conf)
commands.BuildCommand(serverfirewall.DeleteCommand(), serverFirewallCommand.Cobra(), conf)
commands.BuildCommand(serverfirewall.ShowCommand(), serverFirewallCommand.Cobra(), conf)
commands.BuildCommand(server.BaseServerCommand(), rootCmd, conf)

// Storages
storageCommand := commands.BuildCommand(storage.BaseStorageCommand(), rootCmd, conf)
Expand Down
10 changes: 8 additions & 2 deletions internal/commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func New(name, usage string, examples ...string) *BaseCommand {
// Command is the base command type for all commands.
type Command interface {
InitCommand()
BuildSubCommands(*config.Config)
CobraCommand
}

Expand Down Expand Up @@ -115,6 +116,8 @@ func BuildCommand(child Command, parent *cobra.Command, config *config.Config) C
return commandRunE(child, service, config, args)
}

child.BuildSubCommands(config)

return child
}

Expand All @@ -139,12 +142,15 @@ func (s *BaseCommand) AddFlags(flags *pflag.FlagSet) {
})
}

// InitCommand can be overridden to handle flag registration.
// A hook to handle flag registration.
// InitCommand is a hook for handling flag registration and other initialization action.
// The config values are not available during this hook. Register a cobra hook to use them. You can set defaults though.
func (s *BaseCommand) InitCommand() {
}

// BuildSubCommands is a hook for handling possible sub-commands.
func (s *BaseCommand) BuildSubCommands(*config.Config) {
}

// Cobra returns the underlying *cobra.Command
func (s *BaseCommand) Cobra() *cobra.Command {
return s.cobra
Expand Down
3 changes: 3 additions & 0 deletions internal/commands/runcommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ var (
)

func commandRunE(command Command, service internal.AllServices, config *config.Config, args []string) error {
// Cobra validations were successful
command.Cobra().SilenceUsage = true

cmdLogger := logger.With("command", command.Cobra().CommandPath())
executor := NewExecutor(config, service, cmdLogger)
defer executor.Close()
Expand Down
16 changes: 16 additions & 0 deletions internal/commands/runcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ func (m *mockNone) InitCommand() {
panic("implement me")
}

func (m *mockNone) BuildSubCommands(*config.Config) {
panic("implement me")
}

func (m *mockNone) Cobra() *cobra.Command {
return m.Command
}
Expand All @@ -46,6 +50,10 @@ func (m *mockSingle) InitCommand() {
panic("implement me")
}

func (m *mockSingle) BuildSubCommands(*config.Config) {
panic("implement me")
}

func (m *mockSingle) Cobra() *cobra.Command {
return m.Command
}
Expand All @@ -68,6 +76,10 @@ func (m *mockMulti) InitCommand() {
panic("implement me")
}

func (m *mockMulti) BuildSubCommands(*config.Config) {
panic("implement me")
}

func (m *mockMulti) Cobra() *cobra.Command {
return m.Command
}
Expand Down Expand Up @@ -106,6 +118,10 @@ func (m *mockMultiResolver) InitCommand() {
panic("implement me")
}

func (m *mockMultiResolver) BuildSubCommands(*config.Config) {
panic("implement me")
}

func (m *mockMultiResolver) Cobra() *cobra.Command {
return m.Command
}
Expand Down
7 changes: 7 additions & 0 deletions internal/commands/server/firewall/server_firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package serverfirewall

import (
"github.com/UpCloudLtd/upcloud-cli/internal/commands"
"github.com/UpCloudLtd/upcloud-cli/internal/config"
)

// BaseServerFirewallCommand is the root command for all 'server firewall' commands
Expand All @@ -16,3 +17,9 @@ func BaseServerFirewallCommand() commands.Command {
type serverFirewallCommand struct {
*commands.BaseCommand
}

func (s *serverFirewallCommand) BuildSubCommands(cfg *config.Config) {
commands.BuildCommand(CreateCommand(), s.Cobra(), cfg)
commands.BuildCommand(DeleteCommand(), s.Cobra(), cfg)
commands.BuildCommand(ShowCommand(), s.Cobra(), cfg)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package networkinterface
import (
"github.com/UpCloudLtd/upcloud-cli/internal/commands"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/ipaddress"
"github.com/UpCloudLtd/upcloud-cli/internal/config"
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud/request"
)

Expand All @@ -17,6 +18,12 @@ type networkInterfaceCommand struct {
*commands.BaseCommand
}

func (n *networkInterfaceCommand) BuildSubCommands(cfg *config.Config) {
commands.BuildCommand(CreateCommand(), n.Cobra(), cfg)
commands.BuildCommand(ModifyCommand(), n.Cobra(), cfg)
commands.BuildCommand(DeleteCommand(), n.Cobra(), cfg)
}

func mapIPAddressesToRequest(ipStrings []string) ([]request.CreateNetworkInterfaceIPAddress, error) {
var ipAddresses []request.CreateNetworkInterfaceIPAddress
for _, ipAddrStr := range ipStrings {
Expand Down
22 changes: 22 additions & 0 deletions internal/commands/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"time"

"github.com/UpCloudLtd/upcloud-cli/internal/commands"
serverfirewall "github.com/UpCloudLtd/upcloud-cli/internal/commands/server/firewall"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/server/networkinterface"
serverstorage "github.com/UpCloudLtd/upcloud-cli/internal/commands/server/storage"
"github.com/UpCloudLtd/upcloud-cli/internal/config"
"github.com/UpCloudLtd/upcloud-cli/internal/ui"
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud/request"
"github.com/UpCloudLtd/upcloud-go-api/v4/upcloud/service"
Expand All @@ -31,6 +35,24 @@ type serverCommand struct {
*commands.BaseCommand
}

func (s *serverCommand) BuildSubCommands(cfg *config.Config) {
commands.BuildCommand(ListCommand(), s.Cobra(), cfg)
commands.BuildCommand(PlanListCommand(), s.Cobra(), cfg)
commands.BuildCommand(ShowCommand(), s.Cobra(), cfg)
commands.BuildCommand(StartCommand(), s.Cobra(), cfg)
commands.BuildCommand(RestartCommand(), s.Cobra(), cfg)
commands.BuildCommand(StopCommand(), s.Cobra(), cfg)
commands.BuildCommand(CreateCommand(), s.Cobra(), cfg)
commands.BuildCommand(ModifyCommand(), s.Cobra(), cfg)
commands.BuildCommand(LoadCommand(), s.Cobra(), cfg)
commands.BuildCommand(EjectCommand(), s.Cobra(), cfg)
commands.BuildCommand(DeleteCommand(), s.Cobra(), cfg)

commands.BuildCommand(networkinterface.BaseNetworkInterfaceCommand(), s.Cobra(), cfg)
commands.BuildCommand(serverstorage.BaseServerStorageCommand(), s.Cobra(), cfg)
commands.BuildCommand(serverfirewall.BaseServerFirewallCommand(), s.Cobra(), cfg)
}

// waitForServerState waits for server to reach given state and updates given logline with wait progress. Finally, logline is updated with given msg and either done state or timeout warning.
func waitForServerState(uuid, state string, service service.Server, logline *ui.LogEntry, msg string) {
logline.SetMessage(fmt.Sprintf("Waiting for server %s to be in %s state: polling", uuid, state))
Expand Down
6 changes: 6 additions & 0 deletions internal/commands/server/storage/server_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package serverstorage

import (
"github.com/UpCloudLtd/upcloud-cli/internal/commands"
"github.com/UpCloudLtd/upcloud-cli/internal/config"
)

const (
Expand All @@ -16,3 +17,8 @@ func BaseServerStorageCommand() commands.Command {
type serverStorageCommand struct {
*commands.BaseCommand
}

func (s *serverStorageCommand) BuildSubCommands(cfg *config.Config) {
commands.BuildCommand(AttachCommand(), s.Cobra(), cfg)
commands.BuildCommand(DetachCommand(), s.Cobra(), cfg)
}
29 changes: 26 additions & 3 deletions internal/core/core.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package core

import (
"errors"
"fmt"
"os"

"github.com/UpCloudLtd/upcloud-cli/internal/clierrors"
"github.com/UpCloudLtd/upcloud-cli/internal/commands"
"github.com/UpCloudLtd/upcloud-cli/internal/commands/all"
"github.com/UpCloudLtd/upcloud-cli/internal/config"
Expand Down Expand Up @@ -124,8 +126,29 @@ func BuildCLI() cobra.Command {
return rootCmd
}

// BootstrapCLI is the CLI entrypoint
func BootstrapCLI(args []string) error {
func min(a, b int) int {
if a < b {
return a
}
return b
}

// Execute is the application entrypoint. It returns the exit code that should be forwarded to the shell.
//
// Exit codes:
// 0-99: Number of failed executions
// 100-: Other, non-execution related, errors, e.g., flag validation failed
func Execute() int {
rootCmd := BuildCLI()
return rootCmd.Execute()
err := rootCmd.Execute()

if err != nil {
var commandFailed *clierrors.CommandFailedError
if errors.As(err, &commandFailed) {
return min(commandFailed.FailedCount, 99)
}
return 100
}

return 0
}
27 changes: 27 additions & 0 deletions internal/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,30 @@ func TestInputValidation(t *testing.T) {
})
}
}

func TestExecute(t *testing.T) {
realArgs := os.Args
defer func() { os.Args = realArgs }()

for _, test := range []struct {
name string
args []string
expected int
}{
{
name: "Successful command (upctl version)",
args: []string{"upctl", "version"},
expected: 0,
},
{
name: "Failing command (upctl server create)",
args: []string{"upctl", "server", "create"},
expected: 100,
},
} {
t.Run(test.name, func(t *testing.T) {
os.Args = test.args
assert.Equal(t, test.expected, Execute())
})
}
}
15 changes: 15 additions & 0 deletions internal/output/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"strings"

"github.com/UpCloudLtd/upcloud-cli/internal/clierrors"
"github.com/UpCloudLtd/upcloud-cli/internal/config"
)

Expand Down Expand Up @@ -74,5 +75,19 @@ func Render(writer io.Writer, cfg *config.Config, commandOutputs ...Output) (err
if _, err := writer.Write(output); err != nil {
return err
}

// Count failed outputs
var failedCount = 0
for _, commandOutput := range commandOutputs {
if _, ok := commandOutput.(Error); ok {
failedCount++
}
}

if failedCount > 0 {
return &clierrors.CommandFailedError{
FailedCount: failedCount,
}
}
return nil
}