Skip to content
Merged
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: 5 additions & 0 deletions .changes/next-release/api-change-vks-ckjydblt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "api-change",
"category": "vks",
"description": "update-nodegroup: drop deprecated --labels/--taints (use update-nodegroup-metadata); replace --auto-scale-min/max and --upgrade-strategy/max-surge/max-unavailable with struct flags --auto-scale and --upgrade-config"
}
5 changes: 5 additions & 0 deletions .changes/next-release/feature-vks-bgen7jri.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "vks",
"description": "Add struct-valued flags (shorthand or JSON) to create-nodegroup: --tags, --secondary-subnets, --auto-scale, --placement-group, --upgrade-config"
}
35 changes: 35 additions & 0 deletions docs/commands/vks/create-nodegroup.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ grn vks create-nodegroup
[--subnet-id <value>]
[--labels <value>]
[--taints <value>]
[--tags <value>]
[--secondary-subnets <value>]
[--auto-scale <value>]
[--placement-group <value>]
[--upgrade-config <value>]
[--enable-encryption-volume]
[--dry-run]
```
Expand Down Expand Up @@ -68,6 +73,21 @@ grn vks create-nodegroup
`--taints` (optional)
: Comma-separated node taints in `key=value:effect` format (e.g. `dedicated=gpu:NoSchedule`).

`--tags` (optional)
: Comma-separated `key=value` pairs to attach as node group tags (e.g. `team=platform,cost-center=42`).

`--secondary-subnets` (optional)
: Comma-separated list of secondary subnet IDs for the node group.

`--auto-scale` (optional)
: Auto-scale configuration. Shorthand `minSize=2,maxSize=10` or JSON `{"minSize":2,"maxSize":10}`.

`--placement-group` (optional)
: Placement group configuration. Shorthand `type=NEW,placementGroupName=pg-1` or JSON. `type` is `NEW` or `EXISTING`; use `placementGroupName` (with `NEW`) or `placementGroupId` (with `EXISTING`).

`--upgrade-config` (optional, default `maxSurge=1,maxUnavailable=0,strategy=SURGE`)
: Upgrade configuration. Shorthand `maxSurge=1,maxUnavailable=0,strategy=SURGE` or JSON `{"maxSurge":1,"maxUnavailable":0,"strategy":"SURGE"}`.

`--enable-encryption-volume` (optional)
: Enable encryption for the node boot volumes.

Expand Down Expand Up @@ -105,6 +125,21 @@ grn vks create-nodegroup \
--enable-encryption-volume
```

Create an auto-scaling node group with a custom upgrade config (shorthand and JSON are both accepted):

```bash
grn vks create-nodegroup \
--cluster-id cls-abc12345-6789-def0-1234-abcdef012345 \
--name auto-ng \
--os ubuntu \
--flavor-id flv-4c8g \
--disk-type SSD \
--ssh-key-id key-abc12345-0000-0000-0000-000000000001 \
--auto-scale minSize=2,maxSize=10 \
--upgrade-config '{"maxSurge":2,"maxUnavailable":1,"strategy":"SURGE"}' \
--tags team=platform,env=prod
```

Validate parameters without creating:

```bash
Expand Down
52 changes: 21 additions & 31 deletions docs/commands/vks/update-nodegroup.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Description

Update a node group's node count, security groups, labels, taints, auto-scaling configuration, and upgrade strategy.
Update a node group's node count, security groups, auto-scaling configuration, and upgrade configuration.

To update labels, tags, or taints, use `grn vks update-nodegroup-metadata` — those fields are deprecated on `update-nodegroup`.

Use `--dry-run` to preview the update payload without executing it.

Expand All @@ -14,13 +16,8 @@ grn vks update-nodegroup
--nodegroup-id <value>
[--num-nodes <value>]
[--security-groups <value>]
[--labels <value>]
[--taints <value>]
[--auto-scale-min <value>]
[--auto-scale-max <value>]
[--upgrade-strategy <value>]
[--upgrade-max-surge <value>]
[--upgrade-max-unavailable <value>]
[--auto-scale <value>]
[--upgrade-config <value>]
[--dry-run]
```

Expand All @@ -38,26 +35,11 @@ grn vks update-nodegroup
`--security-groups` (optional)
: Comma-separated list of security group IDs to replace the current set.

`--labels` (optional)
: Comma-separated `key=value` pairs to set as Kubernetes node labels (replaces existing labels).

`--taints` (optional)
: Comma-separated node taints in `key=value:effect` format (replaces existing taints).

`--auto-scale-min` (optional)
: Minimum number of nodes for the auto-scaler.

`--auto-scale-max` (optional)
: Maximum number of nodes for the auto-scaler.

`--upgrade-strategy` (optional)
: Node upgrade strategy. Accepted value: `SURGE`.

`--upgrade-max-surge` (optional)
: Maximum number of extra nodes to create during a surge upgrade.
`--auto-scale` (optional)
: Auto-scale configuration. Shorthand `minSize=2,maxSize=10` or JSON `{"minSize":2,"maxSize":10}`.

`--upgrade-max-unavailable` (optional)
: Maximum number of nodes that may be unavailable during an upgrade.
`--upgrade-config` (optional)
: Upgrade configuration. Shorthand `maxSurge=1,maxUnavailable=0,strategy=SURGE` or JSON `{"maxSurge":1,"maxUnavailable":0,"strategy":"SURGE"}`.

`--dry-run` (optional)
: Print the update payload without sending the request.
Expand All @@ -73,20 +55,28 @@ grn vks update-nodegroup \
--num-nodes 5
```

Set auto-scaling limits:
Set auto-scaling limits (shorthand or JSON):

```bash
grn vks update-nodegroup \
--cluster-id cls-abc12345-6789-def0-1234-abcdef012345 \
--nodegroup-id ng-abc12345-6789-def0-1234-abcdef012345 \
--auto-scale-min 2 \
--auto-scale-max 10
--auto-scale minSize=2,maxSize=10
```

Update labels and taints:
Set the upgrade configuration:

```bash
grn vks update-nodegroup \
--cluster-id cls-abc12345-6789-def0-1234-abcdef012345 \
--nodegroup-id ng-abc12345-6789-def0-1234-abcdef012345 \
--upgrade-config '{"maxSurge":2,"maxUnavailable":1,"strategy":"SURGE"}'
```

To update labels, tags, or taints, use `update-nodegroup-metadata` (those fields are deprecated on `update-nodegroup`):

```bash
grn vks update-nodegroup-metadata \
--cluster-id cls-abc12345-6789-def0-1234-abcdef012345 \
--nodegroup-id ng-abc12345-6789-def0-1234-abcdef012345 \
--labels env=prod,tier=app \
Expand Down
38 changes: 38 additions & 0 deletions go/cmd/vks/create_nodegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"regexp"

"github.com/spf13/cobra"
"github.com/vngcloud/greennode-cli/internal/cli"
"github.com/vngcloud/greennode-cli/internal/validator"
)

Expand All @@ -31,6 +32,11 @@ func init() {
f.String("labels", "", "Node labels as key=value pairs (comma-separated)")
f.String("taints", "", "Node taints as key=value:effect (comma-separated)")
f.Bool("enable-encryption-volume", false, "Enable volume encryption")
f.String("tags", "", "Node group tags as key=value pairs (comma-separated)")
f.String("secondary-subnets", "", "Secondary subnet IDs (comma-separated)")
f.String("auto-scale", "", "Auto-scale config (shorthand minSize=2,maxSize=10 or JSON)")
f.String("placement-group", "", "Placement group config (shorthand type=NEW,placementGroupName=pg or JSON)")
f.String("upgrade-config", "", "Upgrade config (shorthand maxSurge=1,maxUnavailable=0,strategy=SURGE or JSON); default SURGE 1/0")
f.Bool("dry-run", false, "Validate parameters without creating")

for _, name := range []string{"cluster-id", "name", "flavor-id", "disk-type", "ssh-key-id"} {
Expand All @@ -54,6 +60,11 @@ func runCreateNodegroup(cmd *cobra.Command, args []string) error {
taintsStr, _ := cmd.Flags().GetString("taints")
enableEncryption, _ := cmd.Flags().GetBool("enable-encryption-volume")
dryRun, _ := cmd.Flags().GetBool("dry-run")
tagsStr, _ := cmd.Flags().GetString("tags")
secondarySubnets, _ := cmd.Flags().GetString("secondary-subnets")
autoScaleStr, _ := cmd.Flags().GetString("auto-scale")
placementGroupStr, _ := cmd.Flags().GetString("placement-group")
upgradeConfigStr, _ := cmd.Flags().GetString("upgrade-config")

if err := validator.ValidateID(clusterID, "cluster-id"); err != nil {
return err
Expand Down Expand Up @@ -94,6 +105,33 @@ func runCreateNodegroup(cmd *cobra.Command, args []string) error {
if taintsStr != "" {
body["taints"] = parseTaints(taintsStr)
}
if tagsStr != "" {
body["tags"] = parseLabels(tagsStr)
}
if secondarySubnets != "" {
body["secondarySubnets"] = parseCommaSeparated(secondarySubnets)
}
if autoScaleStr != "" {
asc, err := cli.ParseStructFlag(autoScaleStr, "minSize", "maxSize")
if err != nil {
return fmt.Errorf("--auto-scale: %w", err)
}
body["autoScaleConfig"] = asc
}
if placementGroupStr != "" {
pg, err := cli.ParseStructFlag(placementGroupStr)
if err != nil {
return fmt.Errorf("--placement-group: %w", err)
}
body["placementGroupConfigDto"] = pg
}
if upgradeConfigStr != "" {
uc, err := cli.ParseStructFlag(upgradeConfigStr, "maxSurge", "maxUnavailable")
if err != nil {
return fmt.Errorf("--upgrade-config: %w", err)
}
body["upgradeConfig"] = uc
}

if dryRun {
return validateCreateNodegroup(name, diskSize, numNodes)
Expand Down
58 changes: 16 additions & 42 deletions go/cmd/vks/update_nodegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/spf13/cobra"
"github.com/vngcloud/greennode-cli/internal/cli"
"github.com/vngcloud/greennode-cli/internal/validator"
)

Expand All @@ -20,13 +21,8 @@ func init() {
f.String("nodegroup-id", "", "Node group ID (required)")
f.String("num-nodes", "", "New number of nodes")
f.String("security-groups", "", "Security group IDs (comma-separated)")
f.String("labels", "", "Node labels as key=value pairs (comma-separated)")
f.String("taints", "", "Node taints as key=value:effect (comma-separated)")
f.String("auto-scale-min", "", "Auto-scale minimum nodes")
f.String("auto-scale-max", "", "Auto-scale maximum nodes")
f.String("upgrade-strategy", "", "Upgrade strategy (SURGE)")
f.String("upgrade-max-surge", "", "Max surge during upgrade")
f.String("upgrade-max-unavailable", "", "Max unavailable during upgrade")
f.String("auto-scale", "", "Auto-scale config (shorthand minSize=2,maxSize=10 or JSON)")
f.String("upgrade-config", "", "Upgrade config (shorthand maxSurge=1,maxUnavailable=0,strategy=SURGE or JSON)")
f.Bool("dry-run", false, "Preview update without executing")

updateNodegroupCmd.MarkFlagRequired("cluster-id")
Expand All @@ -38,13 +34,8 @@ func runUpdateNodegroup(cmd *cobra.Command, args []string) error {
nodegroupID, _ := cmd.Flags().GetString("nodegroup-id")
numNodes, _ := cmd.Flags().GetString("num-nodes")
securityGroups, _ := cmd.Flags().GetString("security-groups")
labelsStr, _ := cmd.Flags().GetString("labels")
taintsStr, _ := cmd.Flags().GetString("taints")
autoScaleMin, _ := cmd.Flags().GetString("auto-scale-min")
autoScaleMax, _ := cmd.Flags().GetString("auto-scale-max")
upgradeStrategy, _ := cmd.Flags().GetString("upgrade-strategy")
upgradeMaxSurge, _ := cmd.Flags().GetString("upgrade-max-surge")
upgradeMaxUnavail, _ := cmd.Flags().GetString("upgrade-max-unavailable")
autoScaleStr, _ := cmd.Flags().GetString("auto-scale")
upgradeConfigStr, _ := cmd.Flags().GetString("upgrade-config")
dryRun, _ := cmd.Flags().GetBool("dry-run")

if err := validator.ValidateID(clusterID, "cluster-id"); err != nil {
Expand All @@ -62,40 +53,23 @@ func runUpdateNodegroup(cmd *cobra.Command, args []string) error {
if securityGroups != "" {
body["securityGroups"] = parseCommaSeparated(securityGroups)
}
if labelsStr != "" {
body["labels"] = parseLabels(labelsStr)
}
if taintsStr != "" {
body["taints"] = parseTaints(taintsStr)
}

if autoScaleMin != "" || autoScaleMax != "" {
autoScaleConfig := map[string]interface{}{}
if autoScaleMin != "" {
autoScaleConfig["minSize"] = toInt(autoScaleMin)
}
if autoScaleMax != "" {
autoScaleConfig["maxSize"] = toInt(autoScaleMax)
if autoScaleStr != "" {
asc, err := cli.ParseStructFlag(autoScaleStr, "minSize", "maxSize")
if err != nil {
return fmt.Errorf("--auto-scale: %w", err)
}
body["autoScaleConfig"] = autoScaleConfig
body["autoScaleConfig"] = asc
}

if upgradeStrategy != "" || upgradeMaxSurge != "" || upgradeMaxUnavail != "" {
upgradeConfig := map[string]interface{}{}
if upgradeStrategy != "" {
upgradeConfig["strategy"] = upgradeStrategy
}
if upgradeMaxSurge != "" {
upgradeConfig["maxSurge"] = toInt(upgradeMaxSurge)
}
if upgradeMaxUnavail != "" {
upgradeConfig["maxUnavailable"] = toInt(upgradeMaxUnavail)
if upgradeConfigStr != "" {
uc, err := cli.ParseStructFlag(upgradeConfigStr, "maxSurge", "maxUnavailable")
if err != nil {
return fmt.Errorf("--upgrade-config: %w", err)
}
body["upgradeConfig"] = upgradeConfig
body["upgradeConfig"] = uc
}

if len(body) == 0 {
return fmt.Errorf("nothing to update: provide at least one of --num-nodes, --security-groups, --labels, --taints, --auto-scale-min/max, or --upgrade-strategy/max-surge/max-unavailable")
return fmt.Errorf("nothing to update: provide at least one of --num-nodes, --security-groups, --auto-scale, or --upgrade-config (use 'update-nodegroup-metadata' for labels/tags/taints)")
}

if dryRun {
Expand Down
50 changes: 50 additions & 0 deletions go/internal/cli/parse.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cli

import (
"encoding/json"
"fmt"
"strconv"
"strings"
)

Expand All @@ -22,6 +24,54 @@ func ParseCommaSeparated(s string) []string {
return result
}

// ParseStructFlag parses a struct-valued CLI flag accepting either JSON
// ({"minSize":2,"maxSize":10}) or AWS-style shorthand (minSize=2,maxSize=10).
// Keys listed in intFields are coerced to int in the shorthand form; all other
// values stay strings. JSON is passed through as decoded. An empty/blank value
// returns (nil, nil). Returns an error on malformed JSON, a shorthand entry
// without '=', or a non-integer value for an int field.
func ParseStructFlag(value string, intFields ...string) (map[string]interface{}, error) {
v := strings.TrimSpace(value)
if v == "" {
return nil, nil
}
if strings.HasPrefix(v, "{") {
var m map[string]interface{}
if err := json.Unmarshal([]byte(v), &m); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
return m, nil
}

ints := map[string]bool{}
for _, f := range intFields {
ints[f] = true
}
out := map[string]interface{}{}
for _, pair := range strings.Split(v, ",") {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
idx := strings.Index(pair, "=")
if idx < 0 {
return nil, fmt.Errorf("invalid shorthand %q: expected key=value", pair)
}
key := strings.TrimSpace(pair[:idx])
val := strings.TrimSpace(pair[idx+1:])
if ints[key] {
n, err := strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("%s must be an integer, got %q", key, val)
}
out[key] = n
} else {
out[key] = val
}
}
return out, nil
}

// BuildEventsQuery builds query params for events endpoints, including only
// flags the user explicitly set. `changed` maps flag name -> was it set.
// VKS pagination is 0-based, so page is passed through verbatim.
Expand Down
Loading
Loading