Skip to content

Commit

Permalink
Implement worker group update. (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit91 authored Jan 16, 2024
1 parent 9deb80f commit 9a4b7b0
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 25 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

[![Markdown Docs](https://img.shields.io/badge/markdown-docs-blue?link=https%3A%2F%2Fgithub.com%2Fmetal-stack-cloud%2Fcli%2Fdocs)](./docs/metal.md)

To work with this CLI, it is first necessary to create a metalstack.cloud api-token. This can be issued through the cloud console.
This is the official CLI for accessing the API of [metalstack.cloud](https://metalstack.cloud).

To work with this CLI, it is first necessary to create an api-token. This can be issued through the [cloud console](https://console.metalstack.cloud/token).

Once you got the token, you probably want to create a CLI context:

Expand Down
171 changes: 155 additions & 16 deletions cmd/api/v1/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v1

import (
"fmt"
"slices"
"time"

"connectrpc.com/connect"
Expand Down Expand Up @@ -54,7 +55,7 @@ func newClusterCmd(c *config.Config) *cobra.Command {
cmd.Flags().Int32("maintenance-minute", 0, "minute in which cluster maintenance is allowed to take place")
cmd.Flags().String("maintenance-timezone", time.Local.String(), "timezone used for the maintenance time window") // nolint
cmd.Flags().Duration("maintenance-duration", 2*time.Hour, "duration in which cluster maintenance is allowed to take place")
cmd.Flags().String("worker-name", "group-0", "the name of the initial worker group")
cmd.Flags().String("worker-group", "group-0", "the name of the initial worker group")
cmd.Flags().Uint32("worker-min", 1, "the minimum amount of worker nodes of the worker group")
cmd.Flags().Uint32("worker-max", 3, "the maximum amount of worker nodes of the worker group")
cmd.Flags().Uint32("worker-max-surge", 1, "the maximum amount of new worker nodes added to the worker group during a rolling update")
Expand Down Expand Up @@ -83,9 +84,18 @@ func newClusterCmd(c *config.Config) *cobra.Command {
cmd.Flags().Uint32("maintenance-minute", 0, "minute in which cluster maintenance is allowed to take place")
cmd.Flags().String("maintenance-timezone", time.Local.String(), "timezone used for the maintenance time window") // nolint
cmd.Flags().Duration("maintenance-duration", 2*time.Hour, "duration in which cluster maintenance is allowed to take place")
cmd.Flags().String("worker-group", "", "the name of the worker group to add, update or remove")
cmd.Flags().Uint32("worker-min", 1, "the minimum amount of worker nodes of the worker group")
cmd.Flags().Uint32("worker-max", 3, "the maximum amount of worker nodes of the worker group")
cmd.Flags().Uint32("worker-max-surge", 1, "the maximum amount of new worker nodes added to the worker group during a rolling update")
cmd.Flags().Uint32("worker-max-unavailable", 0, "the maximum amount of worker nodes removed from the worker group during a rolling update")
cmd.Flags().String("worker-type", "", "the worker type of the initial worker group")
cmd.Flags().Bool("remove-worker-group", false, "if set the selected worker group is being removed")

genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("kubernetes-version", c.Completion.KubernetesVersionAssetListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("worker-type", c.Completion.MachineTypeAssetListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("worker-group", c.Completion.ClusterWorkerGroupsCompletion))
},
UpdateRequestFromCLI: w.updateFromCLI,
}
Expand Down Expand Up @@ -173,14 +183,9 @@ func (c *cluster) createFromCLI() (*apiv1.ClusterServiceCreateRequest, error) {
}
}

if viper.IsSet("worker-name") ||
viper.IsSet("worker-min") ||
viper.IsSet("worker-max") ||
viper.IsSet("worker-max-surge") ||
viper.IsSet("worker-max-unavailable") ||
viper.IsSet("worker-type") {
if helpers.IsAnyViperFlagSet("worker-group", "worker-min", "worker-max", "worker-max-surge", "worker-max-unavailable", "worker-type") {
rq.Workers = append(rq.Workers, &apiv1.Worker{
Name: viper.GetString("worker-name"),
Name: viper.GetString("worker-group"),
MachineType: viper.GetString("worker-type"),
Minsize: viper.GetUint32("worker-min"),
Maxsize: viper.GetUint32("worker-max"),
Expand Down Expand Up @@ -267,14 +272,36 @@ func ClusterResponseToCreate(r *apiv1.Cluster) *apiv1.ClusterServiceCreateReques

func ClusterResponseToUpdate(r *apiv1.Cluster) *apiv1.ClusterServiceUpdateRequest {
return &apiv1.ClusterServiceUpdateRequest{
Uuid: r.Uuid,
Project: r.Project,
Kubernetes: r.Kubernetes,
// Workers: workers, // TODO
Uuid: r.Uuid,
Project: r.Project,
Kubernetes: r.Kubernetes,
Workers: clusterWorkersToWorkerUpdate(r.Workers),
Maintenance: r.Maintenance,
}
}

func clusterWorkersToWorkerUpdate(workers []*apiv1.Worker) []*apiv1.WorkerUpdate {
var res []*apiv1.WorkerUpdate
for _, worker := range workers {
worker := worker

res = append(res, clusterWorkerToWorkerUpdate(worker))
}

return res
}

func clusterWorkerToWorkerUpdate(worker *apiv1.Worker) *apiv1.WorkerUpdate {
return &apiv1.WorkerUpdate{
Name: worker.Name,
MachineType: pointer.Pointer(worker.MachineType),
Minsize: pointer.Pointer(worker.Minsize),
Maxsize: pointer.Pointer(worker.Maxsize),
Maxsurge: pointer.Pointer(worker.Maxsurge),
Maxunavailable: pointer.Pointer(worker.Maxunavailable),
}
}

func (c *cluster) Update(req *apiv1.ClusterServiceUpdateRequest) (*apiv1.Cluster, error) {
ctx, cancel := c.c.NewRequestContext()
defer cancel()
Expand All @@ -299,10 +326,8 @@ func (c *cluster) updateFromCLI(args []string) (*apiv1.ClusterServiceUpdateReque
}

rq := &apiv1.ClusterServiceUpdateRequest{
Uuid: uuid,
Project: c.c.GetProject(),
Kubernetes: &apiv1.KubernetesSpec{},
Maintenance: &apiv1.Maintenance{},
Uuid: uuid,
Project: cluster.Project,
}

if viper.IsSet("maintenance-hour") || viper.IsSet("maintenance-minute") || viper.IsSet("maintenance-duration") {
Expand All @@ -323,9 +348,123 @@ func (c *cluster) updateFromCLI(args []string) (*apiv1.ClusterServiceUpdateReque
}

if viper.IsSet("kubernetes-version") {
rq.Kubernetes = cluster.Kubernetes

rq.Kubernetes.Version = viper.GetString("kubernetes-version")
}

findWorkerGroup := func() (*apiv1.Worker, error) {
if viper.GetString("worker-group") == "" {
if len(cluster.Workers) != 1 {
return nil, fmt.Errorf("please specify the group to act on using the flag --worker-group")
}

return cluster.Workers[0], nil
}

for _, worker := range cluster.Workers {
worker := worker
if worker.Name == viper.GetString("worker-group") {
return worker, nil
}
}

return nil, nil
}

if helpers.IsAnyViperFlagSet("worker-group", "worker-min", "worker-max", "worker-max-surge", "worker-max-unavailable", "worker-type", "remove-worker-group") {
type operation string

const (
update operation = "Updating"
delete operation = "Deleting"
add operation = "Adding"
)

var (
newWorkers []*apiv1.WorkerUpdate
showPrompt = func(op operation, name string) error {
if viper.GetBool("skip-security-prompts") {
return nil
}

return genericcli.PromptCustom(&genericcli.PromptConfig{
Message: fmt.Sprintf("%s worker group %q, continue?", op, name),
ShowAnswers: true,
Out: c.c.PromptOut,
In: c.c.In,
})
}
)

selectedGroup, err := findWorkerGroup()
if err != nil {
return nil, err
}

if selectedGroup == nil {
if viper.IsSet("remove-worker-group") {
return nil, fmt.Errorf("cluster has no worker group with name %q", viper.GetString("worker-group"))
}

if err := showPrompt(add, viper.GetString("worker-group")); err != nil {
return nil, err
}

newWorkers = append(clusterWorkersToWorkerUpdate(cluster.Workers), &apiv1.WorkerUpdate{
Name: viper.GetString("worker-group"),
MachineType: pointer.PointerOrNil(viper.GetString("worker-type")),
Minsize: pointer.PointerOrNil(viper.GetUint32("worker-min")),
Maxsize: pointer.PointerOrNil(viper.GetUint32("worker-max")),
Maxsurge: pointer.PointerOrNil(viper.GetUint32("worker-max-surge")),
Maxunavailable: pointer.PointerOrNil(viper.GetUint32("worker-max-unavailable")),
})
} else {
if viper.IsSet("remove-worker-group") {
if err := showPrompt(delete, selectedGroup.Name); err != nil {
return nil, err
}

newWorkers = clusterWorkersToWorkerUpdate(cluster.Workers)
slices.DeleteFunc(newWorkers, func(w *apiv1.WorkerUpdate) bool {
return w.Name == selectedGroup.Name
})
} else {
if err := showPrompt(update, selectedGroup.Name); err != nil {
return nil, err
}

for _, worker := range cluster.Workers {
worker := worker

workerUpdate := clusterWorkerToWorkerUpdate(worker)

if worker.Name == selectedGroup.Name {
if viper.IsSet("worker-min") {
workerUpdate.Minsize = pointer.Pointer(viper.GetUint32("worker-min"))
}
if viper.IsSet("worker-max") {
workerUpdate.Maxsize = pointer.Pointer(viper.GetUint32("worker-max"))
}
if viper.IsSet("worker-max-surge") {
workerUpdate.Maxsurge = pointer.Pointer(viper.GetUint32("worker-max-surge"))
}
if viper.IsSet("worker-max-unavailable") {
workerUpdate.Maxunavailable = pointer.Pointer(viper.GetUint32("worker-max-unavailable"))
}
if viper.IsSet("worker-type") {
workerUpdate.MachineType = pointer.Pointer(viper.GetString("worker-type"))
}
}

newWorkers = append(newWorkers, workerUpdate)
}
}
}

rq.Workers = newWorkers
}

return rq, nil
}

Expand Down
13 changes: 11 additions & 2 deletions cmd/cluster_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"bytes"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -324,7 +325,7 @@ ID TENANT PROJECT NAME PARTIT
"--maintenance-hour", strconv.Itoa(int(want.Maintenance.TimeWindow.Begin.Hour)),
"--maintenance-minute", strconv.Itoa(int(want.Maintenance.TimeWindow.Begin.Minute)),
"--maintenance-timezone", want.Maintenance.TimeWindow.Begin.Timezone,
"--worker-name", want.Workers[0].Name,
"--worker-group", want.Workers[0].Name,
"--worker-min", strconv.Itoa(int(want.Workers[0].Minsize)),
"--worker-max", strconv.Itoa(int(want.Workers[0].Maxsize)),
"--worker-max-surge", strconv.Itoa(int(want.Workers[0].Maxsurge)),
Expand Down Expand Up @@ -358,10 +359,18 @@ ID TENANT PROJECT NAME PARTIT
"--maintenance-hour", strconv.Itoa(int(want.Maintenance.TimeWindow.Begin.Hour)),
"--maintenance-minute", strconv.Itoa(int(want.Maintenance.TimeWindow.Begin.Minute)),
"--maintenance-timezone", want.Maintenance.TimeWindow.Begin.Timezone,
"--worker-group", want.Workers[0].Name,
"--worker-min", strconv.Itoa(int(want.Workers[0].Minsize)),
"--worker-max", strconv.Itoa(int(want.Workers[0].Maxsize)),
"--worker-max-surge", strconv.Itoa(int(want.Workers[0].Maxsurge)),
"--worker-max-unavailable", strconv.Itoa(int(want.Workers[0].Maxunavailable)),
"--worker-type", want.Workers[0].MachineType,
}
AssertExhaustiveArgs(t, args, commonExcludedFileArgs()...)
exclude := append(commonExcludedFileArgs(), "remove-worker-group")
AssertExhaustiveArgs(t, args, exclude...)
return args
},
MockStdin: bytes.NewBufferString("y"),
ClientMocks: &apitests.ClientMockFns{
Apiv1Mocks: &apitests.Apiv1MockFns{
Cluster: func(m *mock.Mock) {
Expand Down
9 changes: 9 additions & 0 deletions cmd/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -40,6 +41,7 @@ type Test[R any] struct {

ClientMocks *apitests.ClientMockFns
FsMocks func(fs afero.Fs, want R)
MockStdin *bytes.Buffer

DisableMockClient bool // can switch off mock client creation

Expand Down Expand Up @@ -93,11 +95,18 @@ func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *config.Confi
c.FsMocks(fs, c.Want)
}

var in io.Reader
if c.MockStdin != nil {
in = bytes.NewReader(c.MockStdin.Bytes())
}

var (
out bytes.Buffer
config = &config.Config{
Fs: fs,
Out: &out,
In: in,
PromptOut: io.Discard,
Completion: &completion.Completion{},
Client: mock.Client(c.ClientMocks),
}
Expand Down
24 changes: 22 additions & 2 deletions cmd/completion/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,28 @@ func (c *Completion) ClusterListCompletion(cmd *cobra.Command, args []string, to
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, s := range resp.Msg.Clusters {
names = append(names, s.Uuid+"\t"+s.Name)
for _, c := range resp.Msg.Clusters {
c := c
names = append(names, c.Uuid+"\t"+c.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}

func (c *Completion) ClusterWorkerGroupsCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
req := &apiv1.ClusterServiceListRequest{
Project: c.Project,
}
resp, err := c.Client.Apiv1().Cluster().List(c.Ctx, connect.NewRequest(req))
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, c := range resp.Msg.Clusters {
c := c
for _, w := range c.Workers {
w := w
names = append(names, w.Name)
}
}
return names, cobra.ShellCompDirectiveNoFileComp
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const (

type Config struct {
Fs afero.Fs
In io.Reader
Out io.Writer
PromptOut io.Writer
Client client.Client
ListPrinter printers.Printer
DescribePrinter printers.Printer
Expand Down
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ func Execute() {
cfg := &config.Config{
Fs: afero.NewOsFs(),
Out: os.Stdout,
PromptOut: os.Stdout,
In: os.Stdin,
Completion: &completion.Completion{},
}

Expand Down
2 changes: 1 addition & 1 deletion docs/metal_cluster_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ metal cluster create [flags]
-p, --project string project of the cluster
--skip-security-prompts skips security prompt for bulk operations
--timestamps when used with --file (bulk operation): prints timestamps in-between the operations
--worker-group string the name of the initial worker group (default "group-0")
--worker-max uint32 the maximum amount of worker nodes of the worker group (default 3)
--worker-max-surge uint32 the maximum amount of new worker nodes added to the worker group during a rolling update (default 1)
--worker-max-unavailable uint32 the maximum amount of worker nodes removed from the worker group during a rolling update
--worker-min uint32 the minimum amount of worker nodes of the worker group (default 1)
--worker-name string the name of the initial worker group (default "group-0")
--worker-type string the worker type of the initial worker group
```

Expand Down
7 changes: 7 additions & 0 deletions docs/metal_cluster_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ metal cluster update [flags]
--maintenance-minute uint32 minute in which cluster maintenance is allowed to take place
--maintenance-timezone string timezone used for the maintenance time window (default "Local")
-p, --project string project of the cluster
--remove-worker-group if set the selected worker group is being removed
--skip-security-prompts skips security prompt for bulk operations
--timestamps when used with --file (bulk operation): prints timestamps in-between the operations
--worker-group string the name of the worker group to add, update or remove
--worker-max uint32 the maximum amount of worker nodes of the worker group (default 3)
--worker-max-surge uint32 the maximum amount of new worker nodes added to the worker group during a rolling update (default 1)
--worker-max-unavailable uint32 the maximum amount of worker nodes removed from the worker group during a rolling update
--worker-min uint32 the minimum amount of worker nodes of the worker group (default 1)
--worker-type string the worker type of the initial worker group
```

### Options inherited from parent commands
Expand Down
Loading

0 comments on commit 9a4b7b0

Please sign in to comment.