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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ stackctl override branch set 42 3 feature/hotfix

# Quota overrides
stackctl override quota get 42
stackctl override quota set 42 --cpu 4 --memory 8Gi
stackctl override quota set 42 --cpu-request 200m --cpu-limit 500m --memory-request 256Mi --memory-limit 1Gi
stackctl override quota delete 42

# View merged values
Expand Down Expand Up @@ -289,6 +289,12 @@ stackctl stack list --cluster 1 -q | xargs stackctl bulk delete --yes
```bash
stackctl cluster list
stackctl cluster get 1

# Cluster-level shared Helm values (applied to all deploys on a cluster)
stackctl cluster shared-values list 1
stackctl cluster shared-values set 1 --name "local-dev-defaults" --file values.yaml
stackctl cluster shared-values set 1 --name "local-dev-defaults" --set persistence.storageClass=local-path --priority 10
stackctl cluster shared-values delete 1 5
```

### Git
Expand Down Expand Up @@ -373,7 +379,7 @@ cli/
bulk.go # bulk deploy/stop/clean/delete (names or IDs)
resolve.go # name/ID resolution helpers
git.go # git branches/validate
cluster.go # cluster list/get
cluster.go # cluster list/get + shared-values list/set/delete
completion.go # shell completion (bash/zsh/fish/powershell)
pkg/
client/ # HTTP client (auth, error handling)
Expand Down
237 changes: 237 additions & 0 deletions cli/cmd/cluster.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/omattsson/stackctl/cli/pkg/client"
"github.com/omattsson/stackctl/cli/pkg/output"
"github.com/omattsson/stackctl/cli/pkg/types"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

var clusterCmd = &cobra.Command{
Expand Down Expand Up @@ -165,8 +170,240 @@ Examples:
},
}

// --- Shared Values ---

var clusterSharedValuesCmd = &cobra.Command{
Use: "shared-values",
Short: "Manage cluster-level shared Helm values",
Long: "List, create, and delete shared Helm values that apply to all deployments on a cluster.",
}

var clusterSharedValuesListCmd = &cobra.Command{
Use: "list <cluster-id>",
Short: "List shared values for a cluster",
Long: `List all shared Helm values configured for a cluster.

Examples:
stackctl cluster shared-values list 1
stackctl cluster shared-values list 1 -o json`,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
id, err := parseID(args[0])
if err != nil {
return err
}

c, err := newClient()
if err != nil {
return err
}

svList, err := c.ListSharedValues(id)
if err != nil {
return err
}

if printer.Quiet {
for _, sv := range svList {
fmt.Fprintln(printer.Writer, sv.ID)
}
return nil
}

if len(svList) == 0 {
printer.PrintMessage("No shared values found for cluster %s", id)
return nil
}

switch printer.Format {
case output.FormatJSON:
return printer.PrintJSON(svList)
case output.FormatYAML:
return printer.PrintYAML(svList)
default:
headers := []string{"ID", "NAME", "PRIORITY", "HAS VALUES", "UPDATED AT"}
rows := make([][]string, len(svList))
for i, sv := range svList {
hasValues := "false"
if sv.Values != "" {
hasValues = "true"
}
rows[i] = []string{
sv.ID,
sv.Name,
strconv.Itoa(sv.Priority),
hasValues,
sv.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
return printer.PrintTable(headers, rows)
}
},
}

var clusterSharedValuesSetCmd = &cobra.Command{
Use: "set <cluster-id>",
Short: "Create or update shared values for a cluster",
Long: `Create or update shared Helm values for a cluster.

Values are provided via --file (JSON or YAML) and/or --set key=value flags,
following the same syntax as 'override set'.

Examples:
stackctl cluster shared-values set 1 --name "local-dev-defaults" --file values.yaml
stackctl cluster shared-values set 1 --name "local-dev-defaults" --set persistence.storageClass=local-path
stackctl cluster shared-values set 1 --name "local-dev-defaults" --file values.yaml --priority 10`,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
id, err := parseID(args[0])
if err != nil {
return err
}

name, _ := cmd.Flags().GetString("name")
file, _ := cmd.Flags().GetString("file")
setFlags, _ := cmd.Flags().GetStringSlice("set")
priority, _ := cmd.Flags().GetInt("priority")

if file == "" && len(setFlags) == 0 {
return fmt.Errorf("at least one of --file or --set is required")
}

values := map[string]interface{}{}

if file != "" {
for _, segment := range strings.Split(filepath.ToSlash(file), "/") {
if segment == ".." {
return fmt.Errorf("file path must not contain '..' segments")
}
}
file = filepath.Clean(file)
data, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("reading file %s: %w", file, err)
}
if err := json.Unmarshal(data, &values); err != nil {
if yamlErr := yaml.Unmarshal(data, &values); yamlErr != nil {
return fmt.Errorf("invalid JSON/YAML in file %s (json: %v): %w", file, err, yamlErr)
}
}
}

for _, kv := range setFlags {
parts := strings.SplitN(kv, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid --set format %q: expected key=value", kv)
}
setNestedValue(values, parts[0], parseScalarValue(parts[1]))
}

yamlBytes, err := yaml.Marshal(values)
if err != nil {
return fmt.Errorf("serializing values to YAML: %w", err)
}

c, err := newClient()
if err != nil {
return err
}

sv, err := c.SetSharedValues(id, &types.SetSharedValuesRequest{
Name: name,
Values: string(yamlBytes),
Priority: priority,
})
if err != nil {
return err
}

if printer.Quiet {
fmt.Fprintln(printer.Writer, sv.ID)
return nil
}

switch printer.Format {
case output.FormatJSON:
return printer.PrintJSON(sv)
case output.FormatYAML:
return printer.PrintYAML(sv)
default:
printer.PrintMessage("Set shared values %q for cluster %s", sv.Name, id)
return nil
}
},
}

var clusterSharedValuesDeleteCmd = &cobra.Command{
Use: "delete <cluster-id> <shared-values-id>",
Short: "Delete shared values from a cluster",
Long: `Delete shared Helm values from a cluster.

This is a destructive operation. You will be prompted for confirmation
unless --yes is specified.

Examples:
stackctl cluster shared-values delete 1 5
stackctl cluster shared-values delete 1 5 --yes`,
Args: cobra.ExactArgs(2),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
clusterID, err := parseID(args[0])
if err != nil {
return err
}
svID, err := parseID(args[1])
if err != nil {
return err
}

confirmed, err := confirmAction(cmd, fmt.Sprintf("This will delete shared values %s from cluster %s. Continue? (y/n): ", svID, clusterID))
if err != nil {
return err
}
if !confirmed {
printer.PrintMessage(msgAborted)
return nil
}

c, err := newClient()
if err != nil {
return err
}

if err := c.DeleteSharedValues(clusterID, svID); err != nil {
return err
}

if printer.Quiet {
fmt.Fprintln(printer.Writer, svID)
return nil
}

printer.PrintMessage("Deleted shared values %s from cluster %s", svID, clusterID)
return nil
},
}

func init() {
// shared-values set flags
clusterSharedValuesSetCmd.Flags().String("name", "", "Name for the shared values entry (required)")
clusterSharedValuesSetCmd.Flags().String("file", "", "JSON or YAML file with values")
clusterSharedValuesSetCmd.Flags().StringSlice("set", nil, "Set a value (key=value), repeatable")
clusterSharedValuesSetCmd.Flags().Int("priority", 0, "Merge priority (higher = applied later)")
_ = clusterSharedValuesSetCmd.MarkFlagRequired("name")

// shared-values delete flags
clusterSharedValuesDeleteCmd.Flags().BoolP("yes", "y", false, flagDescSkipConfirm)

// Wire up shared-values subcommands
clusterSharedValuesCmd.AddCommand(clusterSharedValuesListCmd)
clusterSharedValuesCmd.AddCommand(clusterSharedValuesSetCmd)
clusterSharedValuesCmd.AddCommand(clusterSharedValuesDeleteCmd)

clusterCmd.AddCommand(clusterListCmd)
clusterCmd.AddCommand(clusterGetCmd)
clusterCmd.AddCommand(clusterSharedValuesCmd)
rootCmd.AddCommand(clusterCmd)
}
Loading
Loading