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/enhancement-core-9wovvlf2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "core",
"description": "cli.ParseStructFlag gains bool-field coercion (ParseStructFlagTyped) so struct flags can carry boolean values in shorthand"
}
5 changes: 5 additions & 0 deletions .changes/next-release/feature-vks-twemaoo1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "vks",
"description": "create-cluster gains the remaining optional CreateClusterDto fields: --secondary-subnets, --list-subnet-ids, --node-netmask-size, --service-endpoint, --az-strategy, and struct-valued --auto-upgrade-config/--auto-healing-config (shorthand or JSON, matching create-nodegroup). --subnet-id is now optional (per spec; pass --subnet-id or --list-subnet-ids or neither, the server validates)"
}
34 changes: 31 additions & 3 deletions docs/commands/vks/create-cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ grn vks create-cluster
--k8s-version <value>
--network-type <value>
--vpc-id <value>
--subnet-id <value>
[--subnet-id <value>]
[--cidr <value>]
[--description <value>]
[--private-cluster <enabled|disabled>]
[--release-channel <value>]
[--load-balancer-plugin <enabled|disabled>]
[--block-store-csi-plugin <enabled|disabled>]
[--service-endpoint <enabled|disabled>]
[--az-strategy <value>]
[--secondary-subnets <value>]
[--list-subnet-ids <value>]
[--node-netmask-size <value>]
[--auto-upgrade-config <value>]
[--auto-healing-config <value>]
[--dry-run]
```

Expand All @@ -42,8 +49,8 @@ grn vks create-cluster
`--vpc-id` (required)
: VPC ID where the cluster will be provisioned.

`--subnet-id` (required)
: Subnet ID for the cluster control plane.
`--subnet-id` (optional)
: Subnet ID for the cluster control plane. Optional per the API — provide either `--subnet-id` or `--list-subnet-ids` (or neither); the server validates.

`--cidr` (optional)
: Pod CIDR block. Required when `--network-type` is `TIGERA` or `CILIUM_OVERLAY` (e.g. `10.96.0.0/12`).
Expand All @@ -63,6 +70,27 @@ grn vks create-cluster
`--block-store-csi-plugin` (optional, default `enabled`)
: Block store CSI plugin state. Accepted values: `enabled`, `disabled`.

`--service-endpoint` (optional, default `disabled`)
: Service endpoint state. Accepted values: `enabled`, `disabled`.

`--az-strategy` (optional, default `SINGLE`)
: Availability-zone strategy for the cluster.

`--secondary-subnets` (optional)
: Comma-separated list of secondary subnet IDs.

`--list-subnet-ids` (optional)
: Comma-separated list of subnet IDs for the cluster.

`--node-netmask-size` (optional)
: Node netmask size (integer). Only sent when explicitly provided.

`--auto-upgrade-config` (optional)
: Auto-upgrade schedule. Shorthand `time=03:00,weekdays=Mon` or JSON `{"weekdays":"Mon,Wed,Fri","time":"03:00"}`. Use JSON when `weekdays` has multiple days (shorthand splits on commas).

`--auto-healing-config` (optional)
: Auto-healing config. Shorthand `enableAutoHealing=true,maxUnhealthy=20%,unhealthyRange=[2-5],timeoutUnhealthy=10` or JSON. `enableAutoHealing` is a boolean, `timeoutUnhealthy` an integer.

`--dry-run` (optional)
: Validate all parameters and print a report without sending the create request.

Expand Down
54 changes: 49 additions & 5 deletions go/cmd/vks/create_cluster.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"
)

var createClusterCmd = &cobra.Command{
Expand All @@ -23,9 +24,9 @@ func init() {
f.String("k8s-version", "", "Kubernetes version (required)")
f.String("network-type", "", "Network type: TIGERA, CILIUM_OVERLAY, CILIUM_NATIVE_ROUTING (required)")
f.String("vpc-id", "", "VPC ID (required)")
f.String("subnet-id", "", "Subnet ID (required)")
f.String("subnet-id", "", "Subnet ID")

for _, name := range []string{"name", "k8s-version", "network-type", "vpc-id", "subnet-id"} {
for _, name := range []string{"name", "k8s-version", "network-type", "vpc-id"} {
createClusterCmd.MarkFlagRequired(name)
}

Expand All @@ -36,6 +37,13 @@ func init() {
f.String("release-channel", "STABLE", "Release channel (RAPID, STABLE)")
f.String("load-balancer-plugin", "enabled", "Load balancer plugin (enabled, disabled)")
f.String("block-store-csi-plugin", "enabled", "Block store CSI plugin (enabled, disabled)")
f.String("service-endpoint", "disabled", "Service endpoint (enabled, disabled)")
f.String("az-strategy", "SINGLE", "Availability zone strategy")
f.String("secondary-subnets", "", "Secondary subnet IDs (comma-separated)")
f.String("list-subnet-ids", "", "Subnet IDs for the cluster (comma-separated)")
f.Int("node-netmask-size", 0, "Node netmask size")
f.String("auto-upgrade-config", "", "Auto-upgrade config (shorthand time=03:00,weekdays=Mon or JSON; use JSON for multiple weekdays)")
f.String("auto-healing-config", "", "Auto-healing config (shorthand enableAutoHealing=true,maxUnhealthy=20%,unhealthyRange=[2-5],timeoutUnhealthy=10 or JSON)")
f.Bool("dry-run", false, "Validate parameters without creating the cluster")
}

Expand All @@ -48,12 +56,18 @@ func runCreateCluster(cmd *cobra.Command, args []string) error {
cidr, _ := cmd.Flags().GetString("cidr")
description, _ := cmd.Flags().GetString("description")
releaseChannel, _ := cmd.Flags().GetString("release-channel")
azStrategy, _ := cmd.Flags().GetString("az-strategy")
secondarySubnets, _ := cmd.Flags().GetString("secondary-subnets")
listSubnetIDs, _ := cmd.Flags().GetString("list-subnet-ids")
autoUpgradeStr, _ := cmd.Flags().GetString("auto-upgrade-config")
autoHealingStr, _ := cmd.Flags().GetString("auto-healing-config")
dryRun, _ := cmd.Flags().GetBool("dry-run")

// Parse enabled/disabled toggle flags.
privateClusterVal, _ := cmd.Flags().GetString("private-cluster")
lbPluginVal, _ := cmd.Flags().GetString("load-balancer-plugin")
csiPluginVal, _ := cmd.Flags().GetString("block-store-csi-plugin")
serviceEndpointVal, _ := cmd.Flags().GetString("service-endpoint")
enablePrivateCluster, err := parseToggle("private-cluster", privateClusterVal)
if err != nil {
return err
Expand All @@ -66,6 +80,10 @@ func runCreateCluster(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
enabledServiceEndpoint, err := parseToggle("service-endpoint", serviceEndpointVal)
if err != nil {
return err
}

// Build cluster body. Node groups are created separately via
// 'grn vks create-nodegroup'.
Expand All @@ -74,21 +92,47 @@ func runCreateCluster(cmd *cobra.Command, args []string) error {
"version": k8sVersion,
"networkType": networkType,
"vpcId": vpcID,
"subnetId": subnetID,
"enablePrivateCluster": enablePrivateCluster,
"releaseChannel": releaseChannel,
"enabledBlockStoreCsiPlugin": enabledCSIPlugin,
"enabledLoadBalancerPlugin": enabledLBPlugin,
"enabledServiceEndpoint": false,
"azStrategy": "SINGLE",
"enabledServiceEndpoint": enabledServiceEndpoint,
"azStrategy": azStrategy,
}

if subnetID != "" {
body["subnetId"] = subnetID
}
if cidr != "" {
body["cidr"] = cidr
}
if description != "" {
body["description"] = description
}
if secondarySubnets != "" {
body["secondarySubnets"] = parseCommaSeparated(secondarySubnets)
}
if listSubnetIDs != "" {
body["listSubnetIds"] = parseCommaSeparated(listSubnetIDs)
}
if cmd.Flags().Changed("node-netmask-size") {
nodeNetmaskSize, _ := cmd.Flags().GetInt("node-netmask-size")
body["nodeNetmaskSize"] = nodeNetmaskSize
}
if autoUpgradeStr != "" {
uc, err := cli.ParseStructFlag(autoUpgradeStr)
if err != nil {
return fmt.Errorf("--auto-upgrade-config: %w", err)
}
body["autoUpgradeConfig"] = uc
}
if autoHealingStr != "" {
hc, err := cli.ParseStructFlagTyped(autoHealingStr, []string{"timeoutUnhealthy"}, []string{"enableAutoHealing"})
if err != nil {
return fmt.Errorf("--auto-healing-config: %w", err)
}
body["autoHealingConfig"] = hc
}

if dryRun {
return validateCreateCluster(name, networkType, cidr)
Expand Down
23 changes: 21 additions & 2 deletions go/internal/cli/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ func ParseCommaSeparated(s string) []string {
// 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) {
return ParseStructFlagTyped(value, intFields, nil)
}

// ParseStructFlagTyped is ParseStructFlag with explicit int and bool field
// lists. In shorthand form, keys in intFields are coerced to int and keys in
// boolFields to bool ("true"/"false"); all other values stay strings. JSON is
// passed through as decoded.
func ParseStructFlagTyped(value string, intFields, boolFields []string) (map[string]interface{}, error) {
v := strings.TrimSpace(value)
if v == "" {
return nil, nil
Expand All @@ -47,6 +55,10 @@ func ParseStructFlag(value string, intFields ...string) (map[string]interface{},
for _, f := range intFields {
ints[f] = true
}
bools := map[string]bool{}
for _, f := range boolFields {
bools[f] = true
}
out := map[string]interface{}{}
for _, pair := range strings.Split(v, ",") {
pair = strings.TrimSpace(pair)
Expand All @@ -59,13 +71,20 @@ func ParseStructFlag(value string, intFields ...string) (map[string]interface{},
}
key := strings.TrimSpace(pair[:idx])
val := strings.TrimSpace(pair[idx+1:])
if ints[key] {
switch {
case 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 {
case bools[key]:
b, err := strconv.ParseBool(val)
if err != nil {
return nil, fmt.Errorf("%s must be a boolean (true/false), got %q", key, val)
}
out[key] = b
default:
out[key] = val
}
}
Expand Down
23 changes: 23 additions & 0 deletions go/internal/cli/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,26 @@ func TestParseStructFlagErrors(t *testing.T) {
t.Error("non-integer int field should error")
}
}

func TestParseStructFlagTypedBool(t *testing.T) {
got, err := ParseStructFlagTyped("enableAutoHealing=true,timeoutUnhealthy=10,maxUnhealthy=20%",
[]string{"timeoutUnhealthy"}, []string{"enableAutoHealing"})
if err != nil {
t.Fatalf("err: %v", err)
}
if got["enableAutoHealing"] != true {
t.Errorf("enableAutoHealing = %#v, want bool true", got["enableAutoHealing"])
}
if got["timeoutUnhealthy"] != 10 {
t.Errorf("timeoutUnhealthy = %#v, want int 10", got["timeoutUnhealthy"])
}
if got["maxUnhealthy"] != "20%" {
t.Errorf("maxUnhealthy = %#v, want string 20%%", got["maxUnhealthy"])
}
}

func TestParseStructFlagTypedBadBool(t *testing.T) {
if _, err := ParseStructFlagTyped("enableAutoHealing=yes", nil, []string{"enableAutoHealing"}); err == nil {
t.Error("invalid bool should error")
}
}
Loading