Skip to content

Commit 3f66900

Browse files
authored
Adding S3 TUI builder (#563)
Fixes #532
1 parent dad1d54 commit 3f66900

7 files changed

Lines changed: 651 additions & 2 deletions

File tree

cli/tools/context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func newContextCmd() simplecobra.Commander {
2626
newDeleteContext(),
2727
newContext(),
2828
newSetContext(),
29+
newS3Cmd(),
2930
},
3031
RunFunc: func(ctx context.Context, cd *simplecobra.Commandeer, r *domain.RootCommand, args []string) error {
3132
return cd.CobraCommand.Help()

cli/tools/org_preferences.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ func newGetOrgPreferenceCmd() simplecobra.Commander {
8686
Long: "get current org preferences",
8787
WithCFunc: func(cmd *cobra.Command, r *domain.RootCommand) {},
8888
RunFunc: func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *domain.RootCommand, args []string) error {
89-
9089
pref, err := rootCmd.GrafanaSvc().GetOrgPreferences()
9190
if err != nil {
9291
log.Fatal(err.Error())

cli/tools/s3.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/bep/simplecobra"
9+
"github.com/esnet/gdg/cli/domain"
10+
"github.com/esnet/gdg/internal/adapter/storage"
11+
"github.com/esnet/gdg/internal/config"
12+
"github.com/jedib0t/go-pretty/v6/table"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func newS3Cmd() simplecobra.Commander {
17+
return &domain.SimpleCommand{
18+
NameP: "s3",
19+
Short: "Manage custom S3-compatible storage engine configurations",
20+
Long: "Create, list, and delete custom S3-compatible storage engine configs. For AWS S3, GCS, or Azure, the wizard prints the relevant documentation URL instead.",
21+
WithCFunc: func(cmd *cobra.Command, r *domain.RootCommand) {
22+
cmd.Aliases = []string{"storage"}
23+
},
24+
CommandsList: []simplecobra.Commander{
25+
newS3NewCmd(),
26+
newS3ListCmd(),
27+
newS3DeleteCmd(),
28+
},
29+
RunFunc: func(ctx context.Context, cd *simplecobra.Commandeer, r *domain.RootCommand, args []string) error {
30+
return cd.CobraCommand.Help()
31+
},
32+
}
33+
}
34+
35+
func newS3NewCmd() simplecobra.Commander {
36+
return &domain.SimpleCommand{
37+
NameP: "new",
38+
Short: "Launch the TUI wizard to create a new cloud storage engine config",
39+
Long: "Interactively configure a custom S3-compatible storage engine (Minio, Ceph, etc.). For AWS S3, GCS, or Azure, prints the relevant auth documentation URL.",
40+
RunFunc: func(ctx context.Context, cd *simplecobra.Commandeer, r *domain.RootCommand, args []string) error {
41+
config.NewCustomS3Config(r.ConfigSvc())
42+
return nil
43+
},
44+
}
45+
}
46+
47+
func newS3ListCmd() simplecobra.Commander {
48+
return &domain.SimpleCommand{
49+
NameP: "list",
50+
Short: "List all configured storage engines",
51+
RunFunc: func(ctx context.Context, cd *simplecobra.Commandeer, r *domain.RootCommand, args []string) error {
52+
engines := config.ListS3Configs(r.ConfigSvc())
53+
if len(engines) == 0 {
54+
fmt.Println("No storage engines configured.")
55+
return nil
56+
}
57+
58+
// Build a reverse index: storage label → context names that reference it
59+
assignedTo := make(map[string][]string)
60+
for ctxName, ctx := range r.ConfigSvc().GetContexts() {
61+
if ctx.Storage != "" {
62+
assignedTo[ctx.Storage] = append(assignedTo[ctx.Storage], ctxName)
63+
}
64+
}
65+
66+
r.TableObj.AppendHeader(table.Row{
67+
"label", "cloud_type", "endpoint", "bucket", "region", "prefix", "init_bucket", "ssl", "assigned_contexts",
68+
})
69+
70+
for label, cfg := range engines {
71+
assigned := "-"
72+
if refs := assignedTo[label]; len(refs) > 0 {
73+
assigned = strings.Join(refs, ", ")
74+
}
75+
r.TableObj.AppendRow(table.Row{
76+
label,
77+
cfg[storage.CloudType],
78+
cfg[storage.Endpoint],
79+
cfg[storage.BucketName],
80+
cfg[storage.Region],
81+
cfg[storage.Prefix],
82+
cfg[storage.InitBucket],
83+
cfg["ssl_enabled"],
84+
assigned,
85+
})
86+
}
87+
88+
r.Render(cd.CobraCommand, engines)
89+
return nil
90+
},
91+
}
92+
}
93+
94+
func newS3DeleteCmd() simplecobra.Commander {
95+
return &domain.SimpleCommand{
96+
NameP: "delete",
97+
Short: "delete <label>",
98+
Long: "Delete a named storage engine config and its credentials file from the secure location.",
99+
InitCFunc: func(cd *simplecobra.Commandeer, r *domain.RootCommand) error {
100+
cd.CobraCommand.Aliases = []string{"del"}
101+
return nil
102+
},
103+
RunFunc: func(ctx context.Context, cd *simplecobra.Commandeer, r *domain.RootCommand, args []string) error {
104+
if len(args) < 1 {
105+
return cd.CobraCommand.Help()
106+
}
107+
config.DeleteS3Config(r.ConfigSvc(), args[0])
108+
return nil
109+
},
110+
}
111+
}

cli/tools/s3_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package tools_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/esnet/gdg/cli"
8+
"github.com/esnet/gdg/cli/domain"
9+
"github.com/esnet/gdg/internal/ports/mocks"
10+
"github.com/esnet/gdg/pkg/test_tooling"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
// TestS3ListShowsEngines exercises "gdg tools contexts s3 list" against the
15+
// testing.yml config (which defines a "test" storage engine) and verifies that
16+
// the table output contains the expected column headers and engine data.
17+
func TestS3ListShowsEngines(t *testing.T) {
18+
rootSvc := cli.NewRootService()
19+
execMe := func(_ *mocks.GrafanaService, optionMockSvc func() domain.RootOption) error {
20+
return cli.Execute(rootSvc, []string{"tools", "contexts", "s3", "list"}, optionMockSvc())
21+
}
22+
outStr, closeReader := test_tooling.SetupAndExecuteMockingServices(t, execMe)
23+
defer closeReader()
24+
25+
lower := strings.ToLower(outStr)
26+
27+
// Column headers (go-pretty StyleLight uppercases them, so compare lowercase)
28+
assert.Contains(t, lower, "label")
29+
assert.Contains(t, lower, "cloud_type")
30+
assert.Contains(t, lower, "endpoint")
31+
assert.Contains(t, lower, "bucket")
32+
33+
// Data from testing.yml storage_engine.test
34+
assert.Contains(t, outStr, "test")
35+
assert.Contains(t, outStr, "http://localhost:9000")
36+
}
37+
38+
// TestS3ListViaStorageAlias verifies that the "storage" alias on the s3 command
39+
// and the "ctx" alias on the contexts command both resolve correctly.
40+
func TestS3ListViaStorageAlias(t *testing.T) {
41+
rootSvc := cli.NewRootService()
42+
execMe := func(_ *mocks.GrafanaService, optionMockSvc func() domain.RootOption) error {
43+
return cli.Execute(rootSvc, []string{"tools", "ctx", "storage", "list"}, optionMockSvc())
44+
}
45+
outStr, closeReader := test_tooling.SetupAndExecuteMockingServices(t, execMe)
46+
defer closeReader()
47+
48+
// Aliases should produce the same table as the canonical command
49+
assert.Contains(t, strings.ToLower(outStr), "label")
50+
assert.Contains(t, outStr, "http://localhost:9000")
51+
}
52+
53+
// TestS3ParentShowsHelp verifies that running "gdg tools contexts s3" with no
54+
// subcommand prints the help listing all three child commands.
55+
func TestS3ParentShowsHelp(t *testing.T) {
56+
rootSvc := cli.NewRootService()
57+
execMe := func(_ *mocks.GrafanaService, optionMockSvc func() domain.RootOption) error {
58+
return cli.Execute(rootSvc, []string{"tools", "contexts", "s3"}, optionMockSvc())
59+
}
60+
outStr, closeReader := test_tooling.SetupAndExecuteMockingServices(t, execMe)
61+
defer closeReader()
62+
63+
lower := strings.ToLower(outStr)
64+
assert.Contains(t, lower, "new")
65+
assert.Contains(t, lower, "list")
66+
assert.Contains(t, lower, "delete")
67+
}
68+
69+
// TestS3DeleteNoArgsShowsHelp verifies that running "gdg tools contexts s3 delete"
70+
// without a label argument prints the command's usage help rather than returning an error.
71+
func TestS3DeleteNoArgsShowsHelp(t *testing.T) {
72+
rootSvc := cli.NewRootService()
73+
execMe := func(_ *mocks.GrafanaService, optionMockSvc func() domain.RootOption) error {
74+
return cli.Execute(rootSvc, []string{"tools", "contexts", "s3", "delete"}, optionMockSvc())
75+
}
76+
outStr, closeReader := test_tooling.SetupAndExecuteMockingServices(t, execMe)
77+
defer closeReader()
78+
79+
lower := strings.ToLower(outStr)
80+
// Cobra help output always includes "usage" and the command name
81+
assert.Contains(t, lower, "usage")
82+
assert.Contains(t, lower, "delete")
83+
}

0 commit comments

Comments
 (0)