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-e1jsnbim.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "api-change",
"category": "vks",
"description": "Rename 'set-auto-upgrade-config' to 'config-auto-upgrade'; the old name still works as a deprecated alias"
}
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-configure-guu8cc3u.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "bugfix",
"category": "configure",
"description": "Mask credential values in 'configure set' output so client_id/client_secret are no longer echoed in plaintext to stdout (consistent with 'configure list'); non-sensitive values are still shown"
}
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-vks-tlhtc992.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "bugfix",
"category": "vks",
"description": "grn vks wait now aborts immediately on a permanent error (HTTP 403/401/400, or 404 for an active waiter) instead of polling until timeout; transient errors (5xx, network) still retry"
}
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-core-mk5mzv01.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "core",
"description": "Add --dry-run to destructive commands that lacked it (vServer server stop/reboot/delete, volume/vpc/subnet/secgroup/secgroup-rule delete, and vks delete-auto-upgrade-config) and unify the preview + confirmation flow across delete commands via shared helpers"
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# set-auto-upgrade-config
# config-auto-upgrade

## Description

Configure auto-upgrade schedule for a cluster. Sets the days and time when automatic Kubernetes version upgrades will be performed.

> Formerly `set-auto-upgrade-config`. That name still works as a deprecated alias.

## Synopsis

```
grn vks set-auto-upgrade-config
grn vks config-auto-upgrade
--cluster-id <value>
--weekdays <value>
--time <value>
Expand All @@ -29,7 +31,7 @@ grn vks set-auto-upgrade-config
Set auto-upgrade to run on weekdays at 3 AM:

```bash
grn vks set-auto-upgrade-config \
grn vks config-auto-upgrade \
--cluster-id k8s-xxxxx \
--weekdays Mon,Tue,Wed,Thu,Fri \
--time 03:00
Expand All @@ -38,7 +40,7 @@ grn vks set-auto-upgrade-config \
Set auto-upgrade to run on weekends at midnight:

```bash
grn vks set-auto-upgrade-config \
grn vks config-auto-upgrade \
--cluster-id k8s-xxxxx \
--weekdays Sat,Sun \
--time 00:00
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/vks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ grn vks <command> [options]

| Command | Description |
|---------|-------------|
| [set-auto-upgrade-config](set-auto-upgrade-config.md) | Configure auto-upgrade schedule for a cluster |
| [config-auto-upgrade](config-auto-upgrade.md) | Configure auto-upgrade schedule for a cluster (alias: set-auto-upgrade-config) |
| [delete-auto-upgrade-config](delete-auto-upgrade-config.md) | Delete auto-upgrade config for a cluster |

### Auto-Healing
Expand Down
14 changes: 13 additions & 1 deletion go/cmd/configure/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,17 @@ func runSet(cmd *cobra.Command, args []string) {
os.Exit(1)
}

fmt.Printf("Set '%s' to '%s' for profile '%s'.\n", key, value, profile)
fmt.Printf("Set '%s' to '%s' for profile '%s'.\n", key, displaySetValue(key, value), profile)
}

// displaySetValue masks credential values so `configure set` never echoes a
// secret in plaintext (matching how `configure list` masks them). Non-sensitive
// values are shown as-is.
func displaySetValue(key, value string) string {
switch key {
case "client_id", "client_secret":
return config.MaskCredential(value)
default:
return value
}
}
18 changes: 18 additions & 0 deletions go/cmd/configure/set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ import (
"github.com/spf13/cobra"
)

// displaySetValue must mask credentials so `set` never echoes a secret, while
// leaving non-sensitive values readable.
func TestDisplaySetValueMasksSecrets(t *testing.T) {
secret := "super-secret-value-z789"
for _, key := range []string{"client_id", "client_secret"} {
got := displaySetValue(key, secret)
if strings.Contains(got, "super-secret") {
t.Errorf("%s: value not masked, got %q", key, got)
}
if !strings.HasSuffix(got, "z789") {
t.Errorf("%s: expected masked value keeping last 4 chars, got %q", key, got)
}
}
if got := displaySetValue("region", "HCM-3"); got != "HCM-3" {
t.Errorf("region should not be masked, got %q", got)
}
}

// newConfigureTestCmd wires a root command with the persistent `profile` flag
// (registered on rootCmd in production) so the configure subcommands resolve it
// exactly as they do at runtime.
Expand Down
33 changes: 20 additions & 13 deletions go/cmd/vks/auto_upgrade.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package vks

import (
"bufio"
"fmt"
"os"
"strings"

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

var setAutoUpgradeConfigCmd = &cobra.Command{
Use: "set-auto-upgrade-config",
Use: "config-auto-upgrade",
Short: "Configure auto-upgrade schedule for a cluster",
RunE: runSetAutoUpgradeConfig,
// set-auto-upgrade-config is the former name, kept for backward compatibility.
Aliases: []string{"set-auto-upgrade-config"},
RunE: runSetAutoUpgradeConfig,
}

var deleteAutoUpgradeConfigCmd = &cobra.Command{
Expand All @@ -23,7 +24,7 @@ var deleteAutoUpgradeConfigCmd = &cobra.Command{
}

func init() {
// set-auto-upgrade-config flags
// config-auto-upgrade flags
f := setAutoUpgradeConfigCmd.Flags()
f.String("cluster-id", "", "Cluster ID (required)")
f.String("weekdays", "", "Days of the week, e.g. Mon,Wed,Fri (required)")
Expand All @@ -35,6 +36,7 @@ func init() {
// delete-auto-upgrade-config flags
g := deleteAutoUpgradeConfigCmd.Flags()
g.String("cluster-id", "", "Cluster ID (required)")
g.Bool("dry-run", false, "Preview what will be deleted without executing")
g.Bool("force", false, "Skip confirmation prompt")
deleteAutoUpgradeConfigCmd.MarkFlagRequired("cluster-id")
}
Expand Down Expand Up @@ -71,20 +73,25 @@ func runSetAutoUpgradeConfig(cmd *cobra.Command, args []string) error {

func runDeleteAutoUpgradeConfig(cmd *cobra.Command, args []string) error {
clusterID, _ := cmd.Flags().GetString("cluster-id")
dryRun, _ := cmd.Flags().GetBool("dry-run")
force, _ := cmd.Flags().GetBool("force")

if err := validator.ValidateID(clusterID, "cluster-id"); err != nil {
return err
}

if !force {
fmt.Print("Are you sure you want to delete the auto-upgrade config? (yes/no): ")
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(response)) != "yes" {
fmt.Println("Delete cancelled.")
return nil
}
fmt.Println("The following will be deleted:")
fmt.Printf(" Auto-upgrade config for cluster: %s\n", clusterID)
fmt.Println("\nThis action is irreversible.")

if dryRun {
cli.DryRunNotice("delete")
return nil
}

if !cli.Confirm(force, "Are you sure you want to delete the auto-upgrade config?") {
fmt.Println("Aborted.")
return nil
}

apiClient, err := createClient(cmd)
Expand Down
17 changes: 5 additions & 12 deletions go/cmd/vks/delete_cluster.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package vks

import (
"bufio"
"fmt"
"os"
"strings"

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

Expand Down Expand Up @@ -57,19 +56,13 @@ func runDeleteCluster(cmd *cobra.Command, args []string) error {
printClusterPreview(cluster, nodegroups)

if dryRun {
fmt.Println("Run without --dry-run to delete.")
cli.DryRunNotice("delete")
return nil
}

// Confirm unless --force
if !force {
fmt.Print("\nAre you sure you want to delete this cluster? (yes/no): ")
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(response)) != "yes" {
fmt.Println("Delete cancelled.")
return nil
}
if !cli.Confirm(force, "Are you sure you want to delete this cluster?") {
fmt.Println("Aborted.")
return nil
}

result, err := apiClient.Delete(fmt.Sprintf("/v1/clusters/%s", clusterID), nil)
Expand Down
16 changes: 5 additions & 11 deletions go/cmd/vks/delete_nodegroup.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package vks

import (
"bufio"
"fmt"
"os"
"strings"

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

Expand Down Expand Up @@ -66,18 +65,13 @@ func runDeleteNodegroup(cmd *cobra.Command, args []string) error {
fmt.Println("This action is irreversible.")

if dryRun {
fmt.Println("Run without --dry-run to delete.")
cli.DryRunNotice("delete")
return nil
}

if !force {
fmt.Print("\nAre you sure you want to delete this node group? (yes/no): ")
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(response)) != "yes" {
fmt.Println("Delete cancelled.")
return nil
}
if !cli.Confirm(force, "Are you sure you want to delete this node group?") {
fmt.Println("Aborted.")
return nil
}

params := map[string]string{}
Expand Down
67 changes: 52 additions & 15 deletions go/cmd/vks/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,64 @@ func statusOf(result interface{}) string {
return s
}

// evaluateActive decides one poll for an "*-active" waiter.
func evaluateActive(result interface{}, err error) (done bool, failed bool, status string) {
// isPermanentAPIError reports whether err is an HTTP client error that will not
// resolve by polling again (bad request, unauthenticated, forbidden). 404 is
// intentionally excluded here — each waiter interprets it for itself.
func isPermanentAPIError(err error) bool {
var apiErr *client.APIError
if errors.As(err, &apiErr) {
switch apiErr.StatusCode {
case 400, 401, 403:
return true
}
}
return false
}

// evaluateActive decides one poll for an "*-active" waiter. A fatal error aborts
// the wait immediately (permanent client errors / resource not found); other
// errors are treated as transient and polling continues.
func evaluateActive(result interface{}, err error) (done bool, failed bool, status string, fatal error) {
if err != nil {
return false, false, ""
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
return false, false, "", err
}
if isPermanentAPIError(err) {
return false, false, "", err
}
return false, false, "", nil
}
status = statusOf(result)
switch status {
case "ACTIVE":
return true, false, status
return true, false, status, nil
case "ERROR", "FAILED":
return false, true, status
return false, true, status, nil
default:
return false, false, status
return false, false, status, nil
}
}

// evaluateDeleted decides one poll for a "*-deleted" waiter.
func evaluateDeleted(result interface{}, err error) (done bool, failed bool, status string) {
// evaluateDeleted decides one poll for a "*-deleted" waiter. A 404 means the
// resource is gone (success); permanent client errors abort; other errors are
// transient.
func evaluateDeleted(result interface{}, err error) (done bool, failed bool, status string, fatal error) {
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
return true, false, "DELETED"
return true, false, "DELETED", nil
}
if isPermanentAPIError(err) {
return false, false, "", err
}
return false, false, ""
return false, false, "", nil
}
status = statusOf(result)
if status == "ACTIVE" {
return false, true, status
return false, true, status, nil
}
return false, false, status
return false, false, status, nil
}

// waitCmd is the parent for all `grn vks wait <condition>` subcommands.
Expand All @@ -60,8 +88,9 @@ var waitCmd = &cobra.Command{
Long: "Poll until a cluster or node group reaches the requested state (active or deleted).",
}

// evaluator decides, for one poll, whether the waiter is done or has failed.
type evaluator func(result interface{}, err error) (done bool, failed bool, status string)
// evaluator decides, for one poll, whether the waiter is done, has failed, or
// hit a fatal error that should abort polling immediately.
type evaluator func(result interface{}, err error) (done bool, failed bool, status string, fatal error)

// runWaiter polls describe() every delay seconds up to maxAttempts times,
// driving the waiter via eval. Progress goes to stderr; on success it prints
Expand All @@ -70,7 +99,15 @@ type evaluator func(result interface{}, err error) (done bool, failed bool, stat
func runWaiter(label, successMsg string, describe func() (interface{}, error), eval evaluator, delay, maxAttempts int) error {
for attempt := 1; attempt <= maxAttempts; attempt++ {
result, err := describe()
done, failed, status := eval(result, err)
done, failed, status, fatal := eval(result, err)

// A fatal error (e.g. 403 Forbidden) will never resolve by polling —
// abort immediately instead of retrying until timeout.
if fatal != nil {
fmt.Fprintln(os.Stderr)
fmt.Fprintf(os.Stderr, "Error waiting for %s: %v\n", label, fatal)
os.Exit(255)
}

shown := status
if shown == "" {
Expand Down
Loading
Loading