Skip to content

Commit 8e891c0

Browse files
committed
feat(understackctl): add deploy versions cmd for users
Added a sub-command to deploy for displaying the versions of all the environments in a deployment repo. This helps users see what version everything is at in a quick glance.
1 parent 8b94d6e commit 8e891c0

4 files changed

Lines changed: 168 additions & 3 deletions

File tree

docs/operator-guide/understackctl.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,21 @@ Walk all YAML files in the cluster directory and update UnderStack container ima
144144

145145
- `--no-digest` — Write the tag only, skip the registry digest lookup.
146146

147+
#### deploy versions
148+
149+
```bash
150+
understackctl deploy versions
151+
```
152+
153+
List every environment in the deployment repo along with the `understack_ref` and `deploy_ref` from its `deploy.yaml`. Environments without a ref set are shown as `unknown`.
154+
155+
```console
156+
$ understackctl deploy versions
157+
ENVIRONMENT UNDERSTACK DEPLOY
158+
cluster-a v0.4.16 HEAD
159+
cluster-b unknown unknown
160+
```
161+
147162
### device-type
148163

149164
Manage hardware device type definitions. Requires `UC_DEPLOY` to be set.

go/understackctl/cmd/deploy/deploy.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func NewCmdDeploy() *cobra.Command {
3737
cmd.AddCommand(newCmdDeployEnable())
3838
cmd.AddCommand(newCmdDeployDisable())
3939
cmd.AddCommand(newCmdDeployImageSet())
40+
cmd.AddCommand(newCmdDeployVersions())
4041

4142
return cmd
4243
}

go/understackctl/cmd/deploy/deploy_test.go

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ func TestEnabledComponentsConvertsToHyphens(t *testing.T) {
7272
components := enabledComponents(config)
7373

7474
expected := map[string]bool{
75-
"cert-manager": true,
76-
"external-secrets": true,
77-
"argo-workflows": true,
75+
"cert-manager": true,
76+
"external-secrets": true,
77+
"argo-workflows": true,
7878
}
7979

8080
if len(components) != len(expected) {
@@ -629,6 +629,83 @@ func TestDeployDisableRequiresTypeFlag(t *testing.T) {
629629
}
630630
}
631631

632+
func TestDeployVersions(t *testing.T) {
633+
tmpDir := t.TempDir()
634+
635+
writeCluster := func(name string, config map[string]any) {
636+
clusterDir := filepath.Join(tmpDir, name)
637+
if err := os.MkdirAll(clusterDir, 0755); err != nil {
638+
t.Fatalf("failed to create cluster dir: %v", err)
639+
}
640+
641+
data, err := yaml.Marshal(&config)
642+
if err != nil {
643+
t.Fatalf("failed to marshal config: %v", err)
644+
}
645+
646+
deployYaml := filepath.Join(clusterDir, "deploy.yaml")
647+
if err := os.WriteFile(deployYaml, data, 0644); err != nil {
648+
t.Fatalf("failed to write deploy.yaml: %v", err)
649+
}
650+
}
651+
652+
writeCluster("cluster-a", map[string]any{
653+
"understack_ref": "v0.4.16",
654+
"deploy_ref": "HEAD",
655+
})
656+
writeCluster("cluster-b", map[string]any{
657+
"site": map[string]any{"enabled": true},
658+
})
659+
660+
// Directories without a deploy.yaml and plain files should be skipped.
661+
if err := os.MkdirAll(filepath.Join(tmpDir, "not-a-cluster"), 0755); err != nil {
662+
t.Fatalf("failed to create dir: %v", err)
663+
}
664+
if err := os.WriteFile(filepath.Join(tmpDir, "README.md"), []byte("test"), 0644); err != nil {
665+
t.Fatalf("failed to write file: %v", err)
666+
}
667+
668+
var out strings.Builder
669+
if err := runDeployVersions(tmpDir, &out); err != nil {
670+
t.Fatalf("runDeployVersions failed: %v", err)
671+
}
672+
673+
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
674+
if len(lines) != 3 {
675+
t.Fatalf("expected header plus 2 environments, got %d lines: %q", len(lines), out.String())
676+
}
677+
678+
rows := map[string][]string{}
679+
for _, line := range lines[1:] {
680+
fields := strings.Fields(line)
681+
if len(fields) != 3 {
682+
t.Fatalf("expected 3 columns, got %q", line)
683+
}
684+
rows[fields[0]] = fields[1:]
685+
}
686+
687+
if refs, ok := rows["cluster-a"]; !ok || refs[0] != "v0.4.16" || refs[1] != "HEAD" {
688+
t.Errorf("unexpected refs for cluster-a: %v", rows["cluster-a"])
689+
}
690+
691+
if refs, ok := rows["cluster-b"]; !ok || refs[0] != "unknown" || refs[1] != "unknown" {
692+
t.Errorf("expected unknown refs for cluster-b, got: %v", rows["cluster-b"])
693+
}
694+
695+
if _, ok := rows["not-a-cluster"]; ok {
696+
t.Error("directories without deploy.yaml should be skipped")
697+
}
698+
}
699+
700+
func TestDeployVersionsNoEnvironments(t *testing.T) {
701+
tmpDir := t.TempDir()
702+
703+
var out strings.Builder
704+
if err := runDeployVersions(tmpDir, &out); err == nil {
705+
t.Fatal("expected error when no environments exist")
706+
}
707+
}
708+
632709
func TestDeployRenderDefaultsToDeployYAML(t *testing.T) {
633710
tmpDir := t.TempDir()
634711
clusterName := filepath.Join(tmpDir, "test-cluster")
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package deploy
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
"text/tabwriter"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func newCmdDeployVersions() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "versions",
16+
Short: "Show the UnderStack version deployed in each environment",
17+
Long: `List every environment in the deployment repo along with its understack_ref and deploy_ref from deploy.yaml.`,
18+
Args: cobra.NoArgs,
19+
RunE: func(cmd *cobra.Command, args []string) error {
20+
return runDeployVersions(".", cmd.OutOrStdout())
21+
},
22+
}
23+
24+
return cmd
25+
}
26+
27+
func runDeployVersions(repoDir string, out io.Writer) error {
28+
entries, err := os.ReadDir(repoDir)
29+
if err != nil {
30+
return fmt.Errorf("failed to read deployment repo: %w", err)
31+
}
32+
33+
// tabwriter defers write errors to Flush, which is returned below.
34+
w := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0)
35+
_, _ = fmt.Fprintln(w, "ENVIRONMENT\tUNDERSTACK\tDEPLOY")
36+
37+
found := false
38+
for _, entry := range entries {
39+
if !entry.IsDir() {
40+
continue
41+
}
42+
43+
clusterDir := filepath.Join(repoDir, entry.Name())
44+
if _, err := os.Stat(filepath.Join(clusterDir, "deploy.yaml")); err != nil {
45+
continue
46+
}
47+
48+
config, err := loadDeployConfig(clusterDir)
49+
if err != nil {
50+
return err
51+
}
52+
53+
understackRef, _ := config["understack_ref"].(string)
54+
if understackRef == "" {
55+
understackRef = "unknown"
56+
}
57+
58+
deployRef, _ := config["deploy_ref"].(string)
59+
if deployRef == "" {
60+
deployRef = "unknown"
61+
}
62+
63+
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", entry.Name(), understackRef, deployRef)
64+
found = true
65+
}
66+
67+
if !found {
68+
return fmt.Errorf("no environments with a deploy.yaml found, is this a deployment repo?")
69+
}
70+
71+
return w.Flush()
72+
}

0 commit comments

Comments
 (0)