diff --git a/.github/workflows/nightly_tests.yaml b/.github/workflows/nightly_tests.yaml index 159a7ffa..18b4622c 100644 --- a/.github/workflows/nightly_tests.yaml +++ b/.github/workflows/nightly_tests.yaml @@ -80,9 +80,4 @@ jobs: - name: "Run All Tests" run: | make test - - - name: "Run Coverage" - run: | - make test-cover - make view-cover diff --git a/Makefile b/Makefile index 7e3dc234..3e636a0e 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ GIT_COMMIT = $(shell git rev-parse HEAD 2> /dev/null || echo unknown) CLC_VERSION ?= v0.0.0-CUSTOMBUILD MAIN_CMD_HELP ?= Hazelcast CLC LDFLAGS = -s -w -X 'github.com/hazelcast/hazelcast-commandline-client/clc/cmd.MainCommandShortHelp=$(MAIN_CMD_HELP)' -X 'github.com/hazelcast/hazelcast-commandline-client/internal.GitCommit=$(GIT_COMMIT)' -X 'github.com/hazelcast/hazelcast-commandline-client/internal.Version=$(CLC_VERSION)' -X 'github.com/hazelcast/hazelcast-go-client/internal.ClientType=CLC' -X 'github.com/hazelcast/hazelcast-go-client/internal.ClientVersion=$(CLC_VERSION)' -TEST_FLAGS ?= -v -count 1 -timeout 30m -race +TEST_FLAGS ?= -count 1 -timeout 30m -race COVERAGE_OUT = coverage.out PACKAGES = $(shell go list ./... | grep -v internal/it | tr '\n' ',') BINARY_NAME ?= clc diff --git a/base/commands/config/config_add.go b/base/commands/config/config_add.go index df96f15d..21a9b883 100644 --- a/base/commands/config/config_add.go +++ b/base/commands/config/config_add.go @@ -66,6 +66,12 @@ func (cm AddCmd) Exec(_ context.Context, ec plug.ExecContext) error { if ec.Interactive() || ec.Props().GetBool(clc.PropertyVerbose) { I2(fmt.Fprintf(ec.Stdout(), "Created configuration at: %s\n", filepath.Join(dir, cfgPath))) } + mopt := config.ConvertKeyValuesToMap(opts) + // ignoring the JSON path for now + _, _, err = config.CreateJSON(target, mopt) + if err != nil { + ec.Logger().Warn("Failed creating the JSON configuration: %s", err.Error()) + } return nil } diff --git a/base/commands/config/config_import.go b/base/commands/config/config_import.go index 5f648a1d..a6047a99 100644 --- a/base/commands/config/config_import.go +++ b/base/commands/config/config_import.go @@ -19,20 +19,16 @@ func (cm ImportCmd) Init(cc plug.InitContext) error { short := "Imports configuration from an arbitrary source" long := `Imports configuration from an arbitrary source -Currently importing only Viridian connection configuration is supported. +Currently importing Viridian connection configuration is supported only. 1. On Viridian console, visit: - Dashboard -> Connect Client -> Quick connection guide -> Python + Dashboard -> Connect Client -> CLI -2. Copy the text in box 1 and pass it as the second parameter. +2. Copy the URL in box 2 and pass it as the second parameter. Make sure the text is quoted before running: - clc config import my-config "curl https://api.viridian.hazelcast.com ... default.zip" - -Alternatively, you can use an already downloaded Python client sample: - - clc config import my-config /home/me/Downloads/hazelcast-cloud-python-sample....zip + clc config import my-config "https://api.viridian.hazelcast.com/client_samples/download/..." ` cc.SetCommandHelp(long, short) diff --git a/base/commands/config/config_it_test.go b/base/commands/config/config_it_test.go index fa16e118..7a591775 100644 --- a/base/commands/config/config_it_test.go +++ b/base/commands/config/config_it_test.go @@ -32,7 +32,7 @@ func TestConfig(t *testing.T) { func importTest(t *testing.T) { tcx := it.TestContext{T: t} - const configURL = "https://rcd-download.s3.us-east-2.amazonaws.com/hazelcast-cloud-python-sample-client-pr-FOR_TESTING-default.zip" + const configURL = "https://rcd-download.s3.us-east-2.amazonaws.com/hazelcast-cloud-clc-sample-client-pr-FOR_TESTING-default.zip" tcx.Tester(func(tcx it.TestContext) { name := it.NewUniqueObjectName("cfg") ctx := context.Background() diff --git a/base/commands/demo/demo_it_test.go b/base/commands/demo/demo_it_test.go index 91a78a0b..3212d07d 100644 --- a/base/commands/demo/demo_it_test.go +++ b/base/commands/demo/demo_it_test.go @@ -29,6 +29,7 @@ func TestGenerateData(t *testing.T) { } func generateData_WikipediaTest(t *testing.T) { + it.MarkFlaky(t, "https://github.com/hazelcast/hazelcast-commandline-client/issues/350") it.MapTester(t, func(tcx it.TestContext, m *hz.Map) { t := tcx.T ctx := context.Background() diff --git a/base/commands/project/project_create_it_test.go b/base/commands/project/project_create_it_test.go index 850237cc..53a24096 100644 --- a/base/commands/project/project_create_it_test.go +++ b/base/commands/project/project_create_it_test.go @@ -17,25 +17,22 @@ import ( ) func TestCreateCommand(t *testing.T) { - // TODO: create a temp home and copy the template into it - testDir := filepath.Join(check.MustValue(filepath.Abs("testdata"))) - home := filepath.Join(testDir, "home") tcx := it.TestContext{T: t} tcx.Tester(func(tcx it.TestContext) { - it.WithEnv(paths.EnvCLCHome, home, func() { - tempDir := check.MustValue(os.MkdirTemp("", "clc-")) - outDir := filepath.Join(tempDir, "my-project") - fixture := filepath.Join(testDir, "fixture", "simple") - defer func() { - // ignoring the error here - _ = os.RemoveAll(outDir) - }() - ctx := context.Background() - // logging to stderr in order to avoid creating the logs directory - cmd := []string{"project", "create", "simple", "-o", outDir, "--log.path", "stderr", "another_key=foo", "key1=bar"} - check.Must(tcx.CLC().Execute(ctx, cmd...)) - check.Must(compareDirectories(fixture, outDir)) - }) + testHomeDir := "testdata/home" + check.Must(paths.CopyDir(testHomeDir, tcx.HomePath())) + tempDir := check.MustValue(os.MkdirTemp("", "clc-")) + outDir := filepath.Join(tempDir, "my-project") + fixtureDir := "testdata/fixture/simple" + defer func() { + // ignoring the error here + _ = os.RemoveAll(outDir) + }() + ctx := context.Background() + // logging to stderr in order to avoid creating the logs directory + cmd := []string{"project", "create", "simple", "-o", outDir, "--log.path", "stderr", "another_key=foo", "key1=bar"} + check.Must(tcx.CLC().Execute(ctx, cmd...)) + check.Must(compareDirectories(fixtureDir, outDir)) }) } diff --git a/base/commands/project/utils.go b/base/commands/project/utils.go index 49183e78..076f81bb 100644 --- a/base/commands/project/utils.go +++ b/base/commands/project/utils.go @@ -11,7 +11,7 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/transport" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -103,11 +103,11 @@ func parseYAML(prefix string, yamlFile []byte, result map[string]string) error { case string: (result)[fullKey] = val default: - if _, isMap := val.(map[any]any); !isMap { + if _, isMap := val.(map[string]any); !isMap { (result)[fullKey] = fmt.Sprintf("%v", val) } } - if subMap, isMap := v.(map[any]any); isMap { + if subMap, isMap := v.(map[string]any); isMap { err = parseYAML(fullKey, marshalYAML(subMap), result) if err != nil { return err @@ -124,7 +124,7 @@ func joinKeys(prefix, key string) string { return prefix + "." + key } -func marshalYAML(m map[any]any) []byte { +func marshalYAML(m map[string]any) []byte { d, _ := yaml.Marshal(m) return d } diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 546167b7..2fb231e3 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -137,18 +137,20 @@ func waitClusterState(ctx context.Context, ec plug.ExecContext, api *viridian.AP func tryImportConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName string) (configPath string, err error) { cpv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Importing configuration") - zipPath, stop, err := api.DownloadConfig(ctx, clusterID, "python") + cfgPath, ok, err := importCLCConfig(ctx, ec, api, clusterID, cfgName) if err != nil { - return nil, err + ec.Logger().Error(err) + } else if ok { + return cfgPath, err } - defer stop() - cfgPath, err := config.CreateFromZip(ctx, ec, cfgName, zipPath) + ec.Logger().Debugf("could not download CLC configuration, trying the Python configuration.") + cfgPath, ok, err = importPythonConfig(ctx, ec, api, clusterID, cfgName) if err != nil { return nil, err } cfgDir, _ := filepath.Split(cfgPath) // import the Java/.Net certificates - zipPath, stop, err = api.DownloadConfig(ctx, clusterID, "java") + zipPath, stop, err := api.DownloadConfig(ctx, clusterID, "java") if err != nil { return nil, err } @@ -173,6 +175,28 @@ func tryImportConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API return cp, nil } +func importCLCConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName string) (configPath string, ok bool, err error) { + return importConfig(ctx, ec, api, clusterID, cfgName, "clc", config.CreateFromZip) +} + +func importPythonConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName string) (configPath string, ok bool, err error) { + return importConfig(ctx, ec, api, clusterID, cfgName, "python", config.CreateFromZipLegacy) +} + +func importConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName, language string, f func(ctx context.Context, ec plug.ExecContext, target, path string) (string, bool, error)) (configPath string, ok bool, err error) { + zipPath, stop, err := api.DownloadConfig(ctx, clusterID, language) + if err != nil { + return "", false, err + } + defer stop() + cfgPath, ok, err := f(ctx, ec, cfgName, zipPath) + if err != nil { + return "", false, err + } + return cfgPath, ok, nil + +} + // importFileFromZip extracts files matching selectPaths to targetDir // Note that this function assumes a Viridian sample zip file. func importFileFromZip(ctx context.Context, ec plug.ExecContext, selectPaths *types.Set[string], zipPath, targetDir string) (imported *types.Set[string], err error) { diff --git a/base/commands/viridian/download_logs_it_test.go b/base/commands/viridian/download_logs_it_test.go index 7720caec..32576650 100644 --- a/base/commands/viridian/download_logs_it_test.go +++ b/base/commands/viridian/download_logs_it_test.go @@ -1,44 +1,3 @@ //go:build std || viridian package viridian_test - -import ( - "context" - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/it" -) - -func downloadLogs_NonInteractiveTest(t *testing.T) { - viridianTester(t, func(ctx context.Context, tcx it.TestContext) { - dir := check.MustValue(os.MkdirTemp("", "log")) - defer func() { check.Must(os.RemoveAll(dir)) }() - c := createOrGetClusterWithState(ctx, tcx, "RUNNING") - tcx.WithReset(func() { - tcx.CLCExecute(ctx, "viridian", "download-logs", c.ID, "--output-dir", dir) - tcx.AssertStderrContains("OK") - require.FileExists(t, paths.Join(dir, "node-1.log")) - }) - }) -} - -func downloadLogs_InteractiveTest(t *testing.T) { - viridianTester(t, func(ctx context.Context, tcx it.TestContext) { - dir := check.MustValue(os.MkdirTemp("", "log")) - defer func() { check.Must(os.RemoveAll(dir)) }() - t.Logf("Downloading to directory: %s", dir) - tcx.WithShell(ctx, func(tcx it.TestContext) { - tcx.WithReset(func() { - c := createOrGetClusterWithState(ctx, tcx, "RUNNING") - tcx.WriteStdinf("\\viridian download-logs %s -o %s\n", c.Name, dir) - tcx.AssertStderrContains("OK") - require.FileExists(t, paths.Join(dir, "node-1.log")) - }) - }) - }) -} diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index 07e2cddf..d8bab0ea 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -5,6 +5,7 @@ package viridian_test import ( "context" "fmt" + "os" "testing" "time" @@ -59,7 +60,7 @@ func TestViridian(t *testing.T) { {"resumeCluster_NonInteractive", resumeCluster_NonInteractiveTest}, {"stopCluster_Interactive", stopCluster_InteractiveTest}, {"stopCluster_NonInteractive", stopCluster_NonInteractiveTest}, - {"streamLogs_nonInteractive", streamLogs_nonInteractiveTest}, + {"streamLogs_NonInteractive", streamLogs_NonInteractiveTest}, } for _, tc := range testCases { t.Run(tc.name, tc.f) @@ -73,12 +74,13 @@ func loginWithParams_NonInteractiveTest(t *testing.T) { } tcx.Tester(func(tcx it.TestContext) { ctx := context.Background() - tcx.CLCExecute(ctx, "viridian", "login", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) + tcx.CLCExecute(ctx, "viridian", "login", "--api-base", "dev2", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) tcx.AssertStdoutContains("Viridian token was fetched and saved.") }) } func loginWithParams_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") tcx := it.TestContext{ T: t, UseViridian: true, @@ -87,7 +89,7 @@ func loginWithParams_InteractiveTest(t *testing.T) { ctx := context.Background() tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { - tcx.WriteStdinf("\\viridian login --api-key %s --api-secret %s\n", it.ViridianAPIKey(), it.ViridianAPISecret()) + tcx.WriteStdinf("\\viridian login --api-base dev2 --api-key %s --api-secret %s\n", it.ViridianAPIKey(), it.ViridianAPISecret()) tcx.AssertStdoutContains("Viridian token was fetched and saved.") }) }) @@ -122,6 +124,7 @@ func listClusters_NonInteractiveTest(t *testing.T) { } func listClusters_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -146,12 +149,12 @@ func createCluster_NonInteractiveTest(t *testing.T) { cs := check.MustValue(tcx.Viridian.ListClusters(ctx)) cid := cs[0].ID tcx.AssertStdoutDollar(fmt.Sprintf("$%s$%s$", cid, clusterName)) - check.Must(waitState(ctx, tcx, cid, "RUNNING")) require.True(t, paths.Exists(paths.ResolveConfigDir(clusterName))) }) } func createCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { ensureNoClusterRunning(ctx, tcx) @@ -180,6 +183,7 @@ func stopCluster_NonInteractiveTest(t *testing.T) { } func stopCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -202,6 +206,7 @@ func resumeCluster_NonInteractiveTest(t *testing.T) { } func resumeCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -226,6 +231,7 @@ func getCluster_NonInteractiveTest(t *testing.T) { } func getCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -252,7 +258,7 @@ func deleteCluster_NonInteractiveTest(t *testing.T) { } func deleteCluster_InteractiveTest(t *testing.T) { - t.Skip() + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -270,6 +276,37 @@ func deleteCluster_InteractiveTest(t *testing.T) { }) } +func downloadLogs_NonInteractiveTest(t *testing.T) { + t.Skipf("skipping this test until the reason of failure is determined") + viridianTester(t, func(ctx context.Context, tcx it.TestContext) { + dir := check.MustValue(os.MkdirTemp("", "log")) + defer func() { check.Must(os.RemoveAll(dir)) }() + c := createOrGetClusterWithState(ctx, tcx, "RUNNING") + tcx.WithReset(func() { + tcx.CLCExecute(ctx, "viridian", "download-logs", c.ID, "--output-dir", dir) + tcx.AssertStderrContains("OK") + require.FileExists(t, paths.Join(dir, "node-1.log")) + }) + }) +} + +func downloadLogs_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") + viridianTester(t, func(ctx context.Context, tcx it.TestContext) { + dir := check.MustValue(os.MkdirTemp("", "log")) + defer func() { check.Must(os.RemoveAll(dir)) }() + t.Logf("Downloading to directory: %s", dir) + tcx.WithShell(ctx, func(tcx it.TestContext) { + tcx.WithReset(func() { + c := createOrGetClusterWithState(ctx, tcx, "RUNNING") + tcx.WriteStdinf("\\viridian download-logs %s -o %s\n", c.Name, dir) + tcx.AssertStderrContains("OK") + require.FileExists(t, paths.Join(dir, "node-1.log")) + }) + }) + }) +} + func viridianTester(t *testing.T, f func(ctx context.Context, tcx it.TestContext)) { tcx := it.TestContext{ T: t, @@ -277,7 +314,7 @@ func viridianTester(t *testing.T, f func(ctx context.Context, tcx it.TestContext } tcx.Tester(func(tcx it.TestContext) { ctx := context.Background() - tcx.CLCExecute(ctx, "viridian", "login", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) + tcx.CLCExecute(ctx, "viridian", "--api-base", "dev2", "login", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) tcx.AssertStdoutContains("Viridian token was fetched and saved.") tcx.WithReset(func() { f(ctx, tcx) diff --git a/base/commands/viridian/viridian_it_unix_test.go b/base/commands/viridian/viridian_it_unix_test.go index 51796f5c..94dfacff 100644 --- a/base/commands/viridian/viridian_it_unix_test.go +++ b/base/commands/viridian/viridian_it_unix_test.go @@ -11,7 +11,8 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/it" ) -func streamLogs_nonInteractiveTest(t *testing.T) { +func streamLogs_NonInteractiveTest(t *testing.T) { + t.Skipf("skipping this test until the reason of failure is determined") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") go func() { diff --git a/base/commands/viridian/viridian_it_windows_test.go b/base/commands/viridian/viridian_it_windows_test.go index e47021b3..e77da8c9 100644 --- a/base/commands/viridian/viridian_it_windows_test.go +++ b/base/commands/viridian/viridian_it_windows_test.go @@ -6,6 +6,6 @@ import ( "testing" ) -func streamLogs_nonInteractiveTest(t *testing.T) { +func streamLogs_NonInteractiveTest(t *testing.T) { t.Skipf("This test doesn't run on Windows") } diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index 35e17337..14f7ef15 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -8,6 +8,7 @@ import ( "io" "os" "os/signal" + "strings" "time" "github.com/fatih/color" @@ -265,7 +266,7 @@ func (ec *ExecContext) WrapResult(f func() error) error { func (ec *ExecContext) PrintlnUnnecessary(text string) { if !ec.Quiet() { - I2(fmt.Fprintln(ec.Stdout(), text)) + I2(fmt.Fprintln(ec.Stdout(), colorizeText(text))) } } @@ -286,6 +287,16 @@ func (ec *ExecContext) ensurePrinter() error { return nil } +func colorizeText(text string) string { + if strings.HasPrefix(text, "OK ") { + return fmt.Sprintf(" %s %s", color.GreenString("OK"), text[3:]) + } + if strings.HasPrefix(text, "FAIL ") { + return fmt.Sprintf(" %s %s", color.RedString("FAIL"), text[5:]) + } + return text +} + func makeErrorStringFromHTTPResponse(text string) string { m := map[string]any{} if err := json.Unmarshal([]byte(text), &m); err != nil { @@ -314,13 +325,18 @@ func (s *simpleSpinner) Start() { _ = s.sp.Start() } +func (s *simpleSpinner) Stop() { + // ignoring the error here + _ = s.sp.Stop() +} + func (s *simpleSpinner) SetText(text string) { s.text = text if text == "" { s.sp.Prefix("") return } - s.sp.Prefix(text + cancelMsg) + s.sp.Prefix(" " + text + cancelMsg) } func (s *simpleSpinner) SetProgress(progress float32) { diff --git a/clc/config/config.go b/clc/config/config.go index 6f7fde6e..9d3ab954 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -11,10 +11,11 @@ import ( "strings" "time" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-go-client" "golang.org/x/exp/slices" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/log" @@ -45,6 +46,37 @@ func CreateJSON(path string, opts map[string]any) (dir, cfgPath string, err erro }) } +func ConvertKeyValuesToMap(kvs clc.KeyValues[string, string]) map[string]any { + m := map[string]any{} + for _, kv := range kvs { + mp := m + ps := strings.Split(kv.Key, ".") + var i int + var p string + for i, p = range ps { + if i >= len(ps)-1 { + // this is the leaf + break + } + v, ok := mp[p] + if ok { + // found the sub, set the map pointer + mp = v.(map[string]any) + } else { + // sub doesn't exist, create it + mm := map[string]any{} + mp[p] = mm + // set the map pointer + mp = mm + } + } + if p != "" { + mp[p] = kv.Value + } + } + return m +} + func createFile(path string, f func(string) (string, []byte, error)) (dir, cfgPath string, err error) { dir, cfgPath, err = DirAndFile(path) if err != nil { diff --git a/clc/config/config_test.go b/clc/config/config_test.go index ba2476b5..45b4e4f4 100644 --- a/clc/config/config_test.go +++ b/clc/config/config_test.go @@ -9,12 +9,13 @@ import ( "path/filepath" "testing" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-go-client" hzlogger "github.com/hazelcast/hazelcast-go-client/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/clc/logger" @@ -277,6 +278,25 @@ ssl: } } +func TestConvertKeyValuesToMap(t *testing.T) { + kvs := clc.KeyValues[string, string]{ + {Key: "cluster.name", Value: "de-foobar"}, + {Key: "ssl.ca-path", Value: "ca.pem"}, + {Key: "cluster.discovery-token", Value: "tok123"}, + } + m := config.ConvertKeyValuesToMap(kvs) + target := map[string]any{ + "cluster": map[string]any{ + "name": "de-foobar", + "discovery-token": "tok123", + }, + "ssl": map[string]any{ + "ca-path": "ca.pem", + }, + } + assert.Equal(t, target, m) +} + func userHostName() string { u := MustValue(user.Current()) host := MustValue(os.Hostname()) diff --git a/clc/config/import.go b/clc/config/import.go index b60c0047..5e201cad 100644 --- a/clc/config/import.go +++ b/clc/config/import.go @@ -20,17 +20,8 @@ import ( func ImportSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, error) { target = strings.TrimSpace(target) src = strings.TrimSpace(src) - // first assume the passed string is a CURL command line, and try to import it. - path, ok, err := tryImportViridianCurlSource(ctx, ec, target, src) - if err != nil { - return "", err - } - // import is successful - if ok { - return path, nil - } - // import is not successful, check whether this an HTTP source - path, ok, err = tryImportHTTPSource(ctx, ec, target, src) + // check whether this an HTTP source + path, ok, err := tryImportHTTPSource(ctx, ec, target, src) if err != nil { return "", err } @@ -49,21 +40,6 @@ func ImportSource(ctx context.Context, ec plug.ExecContext, target, src string) return path, nil } -// tryImportViridianCurlSource returns true if importing from a Viridian CURL command line is successful -func tryImportViridianCurlSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, bool, error) { - const reCurlSource = `curl (?P[^\s]+)\s+` - re, err := regexp.Compile(reCurlSource) - if err != nil { - return "", false, err - } - grps := re.FindStringSubmatch(src) - if len(grps) < 2 { - return "", false, nil - } - url := grps[1] - return tryImportHTTPSource(ctx, ec, target, url) -} - func tryImportHTTPSource(ctx context.Context, ec plug.ExecContext, target, url string) (string, bool, error) { if !strings.HasPrefix(url, "https://") && !strings.HasSuffix(url, "http://") { return "", false, nil @@ -72,40 +48,26 @@ func tryImportHTTPSource(ctx context.Context, ec plug.ExecContext, target, url s if err != nil { return "", false, err } - ec.Logger().Info("Downloaded sample to: %s", path) - path, err = CreateFromZip(ctx, ec, target, path) - if err != nil { - return "", false, err - } - return path, true, nil - + ec.Logger().Info("Downloaded the configuration at: %s", path) + return tryImportViridianZipSource(ctx, ec, target, path) } // tryImportViridianZipSource returns true if importing from a Viridian Go sample zip file is successful func tryImportViridianZipSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, bool, error) { - const reSource = `hazelcast-cloud-(?P[a-z]+)-sample-client-(?P[a-zA-Z0-9_-]+)-default\.zip` - re, err := regexp.Compile(reSource) - if err != nil { - return "", false, err - } - grps := re.FindStringSubmatch(src) - if len(grps) != 3 { - return "", false, nil - } - language := grps[1] - if language != "go" { - return "", false, fmt.Errorf("%s is not usable as a configuration source, use Go sample", src) + path, ok, err := CreateFromZip(ctx, ec, target, src) + if ok { + return path, true, nil } - path, err := CreateFromZip(ctx, ec, target, src) + path, ok, err = CreateFromZipLegacy(ctx, ec, target, src) if err != nil { - return "", false, err + return "", ok, err } - return path, true, nil + return path, ok, nil } func download(ctx context.Context, ec plug.ExecContext, url string) (string, error) { p, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Downloading the sample") + sp.SetText("Downloading the configuration") f, err := os.CreateTemp("", "clc-download-*") if err != nil { return "", err @@ -125,7 +87,53 @@ func download(ctx context.Context, ec plug.ExecContext, url string) (string, err return p.(string), nil } -func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string) (string, error) { +func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string) (string, bool, error) { + // TODO: refactor this function so it is not dependent on ec + p, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText("Extracting configuration files") + reader, err := zip.OpenReader(path) + if err != nil { + return nil, err + } + defer reader.Close() + // check whether this is the new config zip + var newConfig bool + var files []*zip.File + for _, rf := range reader.File { + if strings.HasSuffix(rf.Name, "/config.json") { + newConfig = true + } + if !rf.FileInfo().IsDir() { + files = append(files, rf) + } + } + if !newConfig { + return false, nil + } + // this is the new config zip, just extract to target + outDir, cfgFileName, err := DirAndFile(target) + if err != nil { + return nil, err + } + if err = os.MkdirAll(outDir, 0700); err != nil { + return nil, err + } + if err = copyFiles(ec, files, outDir); err != nil { + return nil, err + } + return paths.Join(outDir, cfgFileName), nil + }) + if err != nil { + return "", false, err + } + stop() + if p == false { + return "", false, nil + } + return p.(string), true, nil +} + +func CreateFromZipLegacy(ctx context.Context, ec plug.ExecContext, target, path string) (string, bool, error) { // TODO: refactor this function so it is not dependent on ec p, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Extracting configuration files") @@ -172,10 +180,10 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string return paths.Join(outDir, cfgPath), nil }) if err != nil { - return "", err + return "", false, err } stop() - return p.(string), nil + return p.(string), true, nil } func makeViridianOpts(clusterName, token, password, apiBaseURL string) clc.KeyValues[string, string] { diff --git a/clc/config/provider.go b/clc/config/provider.go index c6b5397c..6c02d583 100644 --- a/clc/config/provider.go +++ b/clc/config/provider.go @@ -9,7 +9,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/spf13/pflag" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" @@ -66,7 +66,7 @@ func (p *FileProvider) load(path string) error { if err != nil { return fmt.Errorf("reading configuration: %w", err) } - m := map[any]any{} + m := map[string]any{} if err := yaml.Unmarshal(b, m); err != nil { return fmt.Errorf("loading configuration: %w", err) } @@ -185,20 +185,15 @@ func (p *FileProvider) clientConfig() (hazelcast.Config, bool) { return hazelcast.Config{}, false } -func (p *FileProvider) traverseMap(root string, m map[any]any) { - for k, v := range m { - // skip if the key is not a string - ks, ok := k.(string) - if !ok { - continue - } +func (p *FileProvider) traverseMap(root string, m map[string]any) { + for ks, v := range m { var r string if root == "" { r = ks } else { r = strings.Join([]string{root, ks}, ".") } - if mm, ok := v.(map[any]any); ok { + if mm, ok := v.(map[string]any); ok { p.traverseMap(r, mm) continue } diff --git a/clc/config/wizard/provider.go b/clc/config/wizard/provider.go index 16eccb4c..f685c5c2 100644 --- a/clc/config/wizard/provider.go +++ b/clc/config/wizard/provider.go @@ -58,11 +58,11 @@ func maybeUnwrapStdout(ec plug.ExecContext) any { } func (p *Provider) ClientConfig(ctx context.Context, ec plug.ExecContext) (hazelcast.Config, error) { - if terminal.IsPipe(maybeUnwrapStdout(ec)) { - return hazelcast.Config{}, fmt.Errorf(`no configuration was provided and cannot display the configuration wizard; use the --config flag`) - } cfg, err := p.fp.Load().ClientConfig(ctx, ec) if err != nil { + if terminal.IsPipe(maybeUnwrapStdout(ec)) { + return hazelcast.Config{}, fmt.Errorf(`no configuration was provided and cannot display the configuration wizard; use the --config flag`) + } // ask the config to the user name, err := p.runWizard(ctx, ec) if err != nil { diff --git a/clc/paths/paths.go b/clc/paths/paths.go index 9685b567..ccfec662 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -2,6 +2,8 @@ package paths import ( "fmt" + "io" + "io/fs" "os" "path/filepath" "strings" @@ -160,6 +162,43 @@ func FindAll(cd string, fn FilterFn) ([]string, error) { return cs, nil } +// CopyDir copies directory src into target directory. +// src/dir/file is copied as target/dir/file +func CopyDir(src, target string) error { + l := len(src) + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) (errOut error) { + if err != nil { + return err + } + part := path[l:] + dest := filepath.Join(target, part) + if d.IsDir() { + if err := os.MkdirAll(dest, 0700); err != nil { + return err + } + return nil + } + in, err := os.Open(path) + if err != nil { + return err + } + // ignoring the error here + defer in.Close() + out, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer func() { + errOut = err + return + }() + if _, err := io.Copy(out, in); err != nil { + return err + } + return nil + }) +} + func nearbyConfigPath() string { // check whether there is config.yaml in the current directory wd, err := os.Getwd() diff --git a/clc/ux/stage/stage.go b/clc/ux/stage/stage.go new file mode 100644 index 00000000..9129b4b5 --- /dev/null +++ b/clc/ux/stage/stage.go @@ -0,0 +1,117 @@ +package stage + +import ( + "context" + "fmt" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" +) + +type Statuser interface { + SetProgress(progress float32) + SetRemainingDuration(dur time.Duration) +} + +type basicStatuser struct { + text string + textFmtWithRemaining string + indexText string + sp clc.Spinner +} + +func (s *basicStatuser) SetProgress(progress float32) { + s.sp.SetProgress(progress) +} + +func (s *basicStatuser) SetRemainingDuration(dur time.Duration) { + text := s.text + if dur > 0 { + text = fmt.Sprintf(s.textFmtWithRemaining, dur) + } + s.sp.SetText(s.indexText + " " + text) +} + +type Stage struct { + ProgressMsg string + SuccessMsg string + FailureMsg string + Func func(status Statuser) error +} + +type Provider internal.Iterator[Stage] + +type Counter interface { + StageCount() int +} + +type FixedProvider struct { + stages []Stage + offset int + current Stage + err error +} + +func NewFixedProvider(stages ...Stage) *FixedProvider { + return &FixedProvider{stages: stages} +} + +func (sp *FixedProvider) Next() bool { + if sp.offset >= len(sp.stages) { + return false + } + sp.current = sp.stages[sp.offset] + sp.offset++ + return true +} + +func (sp *FixedProvider) Value() Stage { + return sp.current +} + +func (sp *FixedProvider) Err() error { + return sp.err +} + +func (sp *FixedProvider) StageCount() int { + return len(sp.stages) +} + +func Execute(ctx context.Context, ec plug.ExecContext, sp Provider) error { + ss := &basicStatuser{} + var index int + var stageCount int + if sc, ok := sp.(Counter); ok { + stageCount = sc.StageCount() + } + for sp.Next() { + if sp.Err() != nil { + return sp.Err() + } + stg := sp.Value() + index++ + ss.text = stg.ProgressMsg + ss.textFmtWithRemaining = stg.ProgressMsg + " (%s left)" + if stageCount > 0 { + d := str.SpacePaddedIntFormat(stageCount) + ss.indexText = fmt.Sprintf("["+d+"/%d]", index, stageCount) + } else { + ss.indexText = "" + } + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, spinner clc.Spinner) (any, error) { + ss.sp = spinner + ss.SetRemainingDuration(0) + return nil, stg.Func(ss) + }) + if err != nil { + ec.PrintlnUnnecessary(fmt.Sprintf("FAIL %s: %s", stg.FailureMsg, err.Error())) + return err + } + stop() + ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg)) + } + return nil +} diff --git a/clc/ux/stage/stage_test.go b/clc/ux/stage/stage_test.go new file mode 100644 index 00000000..837608f9 --- /dev/null +++ b/clc/ux/stage/stage_test.go @@ -0,0 +1,97 @@ +package stage_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" + "github.com/hazelcast/hazelcast-commandline-client/internal/it" +) + +func TestExecute(t *testing.T) { + stages := []stage.Stage{ + { + ProgressMsg: "Progressing 1", + SuccessMsg: "Success 1", + FailureMsg: "Failure 1", + Func: func(status stage.Statuser) error { + time.Sleep(1 * time.Millisecond) + return nil + }, + }, + { + ProgressMsg: "Progressing 2", + SuccessMsg: "Success 2", + FailureMsg: "Failure 2", + Func: func(status stage.Statuser) error { + for i := 0; i < 5; i++ { + status.SetProgress(float32(i+1) / float32(5)) + } + time.Sleep(1 * time.Millisecond) + return nil + }, + }, + { + ProgressMsg: "Progressing 3", + SuccessMsg: "Success 3", + FailureMsg: "Failure 3", + Func: func(status stage.Statuser) error { + for i := 0; i < 5; i++ { + status.SetRemainingDuration(5*time.Second - time.Duration(i+1)*time.Second) + } + time.Sleep(1 * time.Millisecond) + return nil + }, + }, + } + ec := it.NewExecuteContext(nil) + err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...)) + assert.NoError(t, err) + texts := []string{ + "[1/3] Progressing 1", + "[2/3] Progressing 2", + "[3/3] Progressing 3", + "[3/3] Progressing 3 (4s left)", + "[3/3] Progressing 3 (3s left)", + "[3/3] Progressing 3 (2s left)", + "[3/3] Progressing 3 (1s left)", + "[3/3] Progressing 3", + } + assert.Equal(t, texts, ec.Spinner.Texts) + progresses := []float32{0.2, 0.4, 0.6, 0.8, 1} + assert.Equal(t, progresses, ec.Spinner.Progresses) + text := "OK [1/3] Success 1.\nOK [2/3] Success 2.\nOK [3/3] Success 3.\n" + assert.Equal(t, text, ec.StdoutText()) +} + +func TestExecute_WithFailure(t *testing.T) { + stages := []stage.Stage{ + { + ProgressMsg: "Progressing 1", + SuccessMsg: "Success 1", + FailureMsg: "Failure 1", + Func: func(status stage.Statuser) error { + return fmt.Errorf("some error") + }, + }, + { + ProgressMsg: "Progressing 2", + SuccessMsg: "Success 2", + FailureMsg: "Failure 2", + Func: func(status stage.Statuser) error { + return nil + }, + }, + } + ec := it.NewExecuteContext(nil) + err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...)) + assert.Error(t, err) + texts := []string{"[1/2] Progressing 1"} + assert.Equal(t, texts, ec.Spinner.Texts) + text := "FAIL Failure 1: some error\n" + assert.Equal(t, text, ec.StdoutText()) +} diff --git a/docs/antora.yml b/docs/antora.yml index b1b2ca05..28d2f66e 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -17,6 +17,6 @@ asciidoc: page-toclevels: 3@ # Required Go version for build go-version: 1.19 - page-latest-supported-mc: '5.3.2-snapshot' + page-latest-supported-mc: '5.4-snapshot' nav: - modules/ROOT/nav.adoc \ No newline at end of file diff --git a/docs/check-links-playbook.yml b/docs/check-links-playbook.yml index cc88fe9d..18e49ca1 100644 --- a/docs/check-links-playbook.yml +++ b/docs/check-links-playbook.yml @@ -32,7 +32,7 @@ asciidoc: # Separate anchor link names by dashes idseparator: '-' page-survey: https://www.surveymonkey.co.uk/r/NYGJNF9 - hazelcast-cloud: Hazelcast Viridian + hazelcast-cloud: Viridian Cloud extensions: - ./tabs-block.js - asciidoctor-kroki diff --git a/docs/modules/ROOT/pages/clc-job.adoc b/docs/modules/ROOT/pages/clc-job.adoc index c0923bc1..b40bcdf0 100644 --- a/docs/modules/ROOT/pages/clc-job.adoc +++ b/docs/modules/ROOT/pages/clc-job.adoc @@ -25,7 +25,7 @@ clc job [command] [options] Creates a Jet job using the provided Jar file. -This command requires a {hazelcast-cloud} or Hazelcast cluster of version 5.3.0 or above. +This command requires a Hazelcast {hazelcast-cloud} or Hazelcast Platform cluster of version 5.3.0 or above. Usage: @@ -250,7 +250,7 @@ Parameters: == clc job export-snapshot -Exports a snapshot from a Jet job. This feature requires a {hazelcast-cloud} or Hazelcast Enterprise cluster. +Exports a snapshot from a Jet job. This feature requires a Hazelcast {hazelcast-cloud} or Hazelcast Enterprise cluster. Usage: diff --git a/docs/modules/ROOT/pages/clc-multimap.adoc b/docs/modules/ROOT/pages/clc-multimap.adoc index 6ef3407b..9122d0f1 100644 --- a/docs/modules/ROOT/pages/clc-multimap.adoc +++ b/docs/modules/ROOT/pages/clc-multimap.adoc @@ -25,7 +25,7 @@ clc multi-map [command] [flags] == clc multi-map put -Put a value in the given MultiMap +Put a value in the given MultiMap. Usage: @@ -120,7 +120,7 @@ Parameters: == clc multi-map remove -Remove values from the given multi-map. +Remove values from the given MultiMap. Usage: diff --git a/docs/modules/ROOT/pages/clc-project.adoc b/docs/modules/ROOT/pages/clc-project.adoc index 5d1bc48e..6936b930 100644 --- a/docs/modules/ROOT/pages/clc-project.adoc +++ b/docs/modules/ROOT/pages/clc-project.adoc @@ -16,7 +16,7 @@ clc project [command] [flags] == clc project create -Creates project from the given template. +Creates a project from the given template. Usage: diff --git a/docs/modules/ROOT/pages/clc-queue.adoc b/docs/modules/ROOT/pages/clc-queue.adoc index 9a8df97a..e134e664 100644 --- a/docs/modules/ROOT/pages/clc-queue.adoc +++ b/docs/modules/ROOT/pages/clc-queue.adoc @@ -19,7 +19,7 @@ clc queue [command] [flags] == clc queue clear -Delete all entries of a Queue. +Delete all entries of a queue. Usage: @@ -49,7 +49,7 @@ clc queue clear --name my-queue == clc queue offer -Add a value to the given Queue. +Add a value to the given queue. Usage: @@ -88,7 +88,7 @@ clc queue offer --value-type f32 19.94 19.92 --name my-queue == clc queue poll -Remove the given number of elements from the given Queue. +Remove the given number of elements from the given queue. Usage: @@ -134,7 +134,7 @@ clc queue poll --count 2 --name my-queue 5 == clc queue size -Return the size of the given Queue. +Return the size of the given queue. Usage: diff --git a/docs/modules/ROOT/pages/clc-set.adoc b/docs/modules/ROOT/pages/clc-set.adoc index 6fc19d24..cc803f66 100644 --- a/docs/modules/ROOT/pages/clc-set.adoc +++ b/docs/modules/ROOT/pages/clc-set.adoc @@ -20,7 +20,7 @@ clc set [command] [flags] == clc set clear -Deletes all entries of a Set. +Deletes all entries of a set. Usage: @@ -56,7 +56,7 @@ clc set clear --name my-set == clc set add -Adds values to the given Set. +Adds values to the given set. Usage: @@ -123,7 +123,7 @@ clc set get-all --name my-set == clc set remove -Removes values from the given Set. +Removes values from the given set. Usage: @@ -159,7 +159,7 @@ clc set remove 1 2 3 4 --name my-set == clc set size -Returns the size of the given Set. +Returns the size of the given set. Usage: @@ -190,7 +190,7 @@ clc set size --name my-set == clc set destroy -Destroys a Set. This command will delete the Set and the data in it will not be available anymore. +Destroys a set. This command will delete the set and the data in it will not be available anymore. Usage: diff --git a/docs/modules/ROOT/pages/clc-snapshot.adoc b/docs/modules/ROOT/pages/clc-snapshot.adoc index d7cd049e..e76bb742 100644 --- a/docs/modules/ROOT/pages/clc-snapshot.adoc +++ b/docs/modules/ROOT/pages/clc-snapshot.adoc @@ -203,7 +203,7 @@ Parameters: == clc job export-snapshot -Exports a snapshot from a Jet job. Note that this feature requires Viridian or Hazelcast Enterprise. +Exports a snapshot from a Jet job. This feature requires Hazelcast {hazelcast-cloud} or Hazelcast Enterprise. Usage: diff --git a/docs/modules/ROOT/pages/clc-topic.adoc b/docs/modules/ROOT/pages/clc-topic.adoc index 3db5062f..36b9a6be 100644 --- a/docs/modules/ROOT/pages/clc-topic.adoc +++ b/docs/modules/ROOT/pages/clc-topic.adoc @@ -17,7 +17,7 @@ clc topic [command] [flags] == clc topic publish -Publish new messages for a Topic. +Publish new messages for a topic. Usage: @@ -57,7 +57,7 @@ clc topic publish -v string string1 string2 --name topic1 == clc topic subscribe -Subscribe to a Topic for new messages. +Subscribe to a topic for new messages. Usage: @@ -88,9 +88,7 @@ clc topic subscribe --name topic1 == clc topic destroy -Destroy a Topic - -This command will delete the Topic and the data in it will not be available anymore. +This command will delete the topic and the data in it will not be available anymore. Usage: diff --git a/docs/modules/ROOT/pages/clc-viridian.adoc b/docs/modules/ROOT/pages/clc-viridian.adoc index b1bf6383..a8c7def9 100644 --- a/docs/modules/ROOT/pages/clc-viridian.adoc +++ b/docs/modules/ROOT/pages/clc-viridian.adoc @@ -1,6 +1,6 @@ = clc viridian -This command group provides commands for doing various {hazelcast-cloud} operations, such as creating and managing clusters. +This command group provides commands for doing various operations on Hazelcast {hazelcast-cloud}, such as creating and managing clusters. All commands except `viridian login` require the generation of a token using an API key and secret, which you can retrieve from the {hazelcast-cloud} console. Running `viridian login` prompts you for this information, and creates and saves the token. @@ -233,7 +233,7 @@ Parameters: == clc viridian delete-cluster -Deletes the given {hazelcast-cloud} cluster. Note that, all data in the cluster is deleted irreversibly. +Deletes the given {hazelcast-cloud} cluster. All data in the cluster is deleted irreversibly. Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. @@ -333,7 +333,7 @@ Parameters: == clc viridian import-config -Imports connection configuration of the given {hazelcast-cloud} cluster. +Imports the connection configuration of the given {hazelcast-cloud} cluster. Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. diff --git a/docs/modules/ROOT/pages/connect-to-viridian.adoc b/docs/modules/ROOT/pages/connect-to-viridian.adoc index c7042a45..09ae1e94 100644 --- a/docs/modules/ROOT/pages/connect-to-viridian.adoc +++ b/docs/modules/ROOT/pages/connect-to-viridian.adoc @@ -1,5 +1,5 @@ -== Connecting to {hazelcast-cloud} with Hazelcast CLC -:description: To use the Hazelcast CLC with Hazelcast {hazelcast-cloud}, you need to authenticate with {hazelcast-cloud} using your API secret and key. You can then import the configuration of the {hazelcast-cloud} cluster that you want to connect to. No additional configuration is required. +== Connecting to Hazelcast {hazelcast-cloud} with Hazelcast CLC +:description: To use the Hazelcast CLC with Hazelcast {hazelcast-cloud}, you need to authenticate with {hazelcast-cloud} using your API secret and key. You can then import the configuration of the cluster that you want to connect to. No additional configuration is required. :page-product: cloud @@ -18,7 +18,7 @@ You need the following: To allow the Hazelcast CLC to do cluster operations, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + ```bash clc viridian login @@ -54,6 +54,6 @@ clc viridian import-config $CLUSTER-NAME --name dev clc -c dev ``` -CLC will start in the interactive mode, and you should see a command prompt. You're ready to start managing xref:clc-viridian.adoc[{hazelcast-cloud} clusters]. +The Hazelcast CLC will start in interactive mode, and you should see a command prompt. You're ready to start managing xref:clc-viridian.adoc[{hazelcast-cloud} clusters]. NOTE: The Hazelcast CLC connects to the cluster on demand, that is when you issue a command that requires the connection, such as running a SQL query. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/environment-variables.adoc b/docs/modules/ROOT/pages/environment-variables.adoc index 1e93bb01..24fefaeb 100644 --- a/docs/modules/ROOT/pages/environment-variables.adoc +++ b/docs/modules/ROOT/pages/environment-variables.adoc @@ -8,7 +8,7 @@ |Environment Variable|Description|Default |CLC_CLIENT_LABELS -|Sets the client labels. The labels must be separated by commas. This value is used in {hazelcast-cloud} and Management Center. See xref:{page-latest-supported-mc}@management-center:clusters:clients.adoc[the Management Center documentation] for more information. +|Sets the client labels. The labels must be separated by commas. This value is used in {hazelcast-cloud} and Management Center. See xref:{page-latest-supported-mc}@management-center:clusters:clients.adoc[Management Center documentation] for more information. |`CLC,USER@HOST-TIMESTAMP` |CLC_CLIENT_NAME diff --git a/docs/modules/ROOT/pages/get-started.adoc b/docs/modules/ROOT/pages/get-started.adoc index c8b9321f..c18f4222 100644 --- a/docs/modules/ROOT/pages/get-started.adoc +++ b/docs/modules/ROOT/pages/get-started.adoc @@ -1,5 +1,5 @@ -= Get Started With the Hazelcast CLC -:description: In this tutorial, you'll learn how to use Hazelcast CLC commands to authenticate with Hazelcast {hazelcast-cloud} and create two production clusters. You'll connect to and switch between clusters from the command line. Finally, you'll perform some basic operations on a cluster from both the command line and by running a script to automate the same actions. += Get Started with the Hazelcast CLC +:description: In this tutorial, you'll learn how to use Hazelcast CLC commands to authenticate with Hazelcast {hazelcast-cloud} and create two production clusters. You'll connect to and switch between the clusters from the command line. Finally, you'll perform some basic operations on a cluster from both the command line and by running a script to automate the same actions. {description} @@ -8,13 +8,14 @@ You need the following: - xref:install-clc.adoc[Hazelcast CLC] installed on your local machine. +- xref:cloud:ROOT:create-account.adoc[{hazelcast-cloud} account] - xref:cloud:ROOT:developer.adoc[{hazelcast-cloud} API key and secret] == Step 1. Authenticating with {hazelcast-cloud} To allow the Hazelcast CLC to to interact with {hazelcast-cloud} clusters, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + [source,shell] ---- @@ -23,7 +24,7 @@ clc viridian login . When prompted, enter your API key and secret. If both are correct, the token is retrieved and saved. -== Step 2. Create Two {hazelcast-cloud} Clusters +== Step 2. Create Two Clusters on {hazelcast-cloud} In this step, you'll create two production clusters called Test1 and Test2 from the command line, and run some commands to check their status. @@ -51,7 +52,7 @@ clc viridian create-cluster --name Test2 clc viridian list-clusters ---- + -The details of all clusters linked to your Hazelcast {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. +The details of all clusters linked to your {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. [[step-2-prod-configure]] diff --git a/docs/modules/ROOT/pages/install-clc.adoc b/docs/modules/ROOT/pages/install-clc.adoc index 5cb1e31c..fffa57b6 100644 --- a/docs/modules/ROOT/pages/install-clc.adoc +++ b/docs/modules/ROOT/pages/install-clc.adoc @@ -204,7 +204,7 @@ Windows:: . Right-click on it and select *Uninstall*. . Press kbd:[Yes] on the uninstallation dialog. -Release Packagae:: +Release Package:: + Delete the `hazelcast-commandline-client` directory. ==== diff --git a/docs/modules/ROOT/pages/jet-job-management.adoc b/docs/modules/ROOT/pages/jet-job-management.adoc index 439cf072..cad6dc09 100644 --- a/docs/modules/ROOT/pages/jet-job-management.adoc +++ b/docs/modules/ROOT/pages/jet-job-management.adoc @@ -1,5 +1,5 @@ = Get Started With Hazelcast Jet Job Management -:description: In this tutorial, you'll learn the basics of how to manage stream-processing pipelines using the Hazelcast CLC with Hazelcast {hazelcast-cloud}. You'll see how to connect to a {hazelcast-cloud} cluster and submit a sample Jet job that generates a stream of numbers. You'll also learn how to use simple commands to monitor and cancel jobs. +:description: In this tutorial, you'll learn the basics of how to manage stream processing pipelines using the Hazelcast CLC with Hazelcast {hazelcast-cloud}. You'll see how to connect to a cluster on {hazelcast-cloud}, and submit a sample Jet job that generates a stream of numbers. You'll also learn how to use simple commands to monitor and cancel jobs. {description} @@ -8,7 +8,7 @@ You need the following: - xref:install-clc.adoc[Hazelcast CLC] installed on your local machine -- One running Hazelcast {hazelcast-cloud} cluster. You can create a xref:managing-viridian-clusters.adoc#creating-a-cluster-on-viridian[{hazelcast-cloud} development cluster] using the Hazelcast CLC. +- One running {hazelcast-cloud} cluster. You can create a xref:managing-viridian-clusters.adoc#creating-a-cluster-on-viridian[development cluster] using the Hazelcast CLC. - xref:cloud:ROOT:developer.adoc[{hazelcast-cloud} API key and secret] - JRE 8 or above. - Gradle 8 or above. @@ -18,7 +18,7 @@ You need the following: To allow the Hazelcast CLC to perform cluster operations, including submitting Jet jobs, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + [source,shell] ---- @@ -37,7 +37,7 @@ Next, check that the Hazelcast CLC can access your cluster by running the follow clc viridian list-clusters ---- -The details of all clusters linked to your Hazelcast {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. +The details of all clusters linked to your {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. [[step-3-dev-configure]] == Step 3. Connect to Your Cluster @@ -182,7 +182,7 @@ clc -c dev job cancel simple-pipeline In this tutorial, you learned how to do the following: -* Connect to a Hazelcast cluster. +* Connect to a cluster on {hazelcast-cloud}. * Build and submit a Hazelcast Jet job to create a data pipeline. * Manage the lifecycle of a Jet job using list and cancel commands. diff --git a/docs/modules/ROOT/pages/managing-viridian-clusters.adoc b/docs/modules/ROOT/pages/managing-viridian-clusters.adoc index 0cb2dc3d..7840e3af 100644 --- a/docs/modules/ROOT/pages/managing-viridian-clusters.adoc +++ b/docs/modules/ROOT/pages/managing-viridian-clusters.adoc @@ -1,6 +1,6 @@ -= Managing Viridian Clusters Using the Hazelcast CLC += Managing Clusters on Hazelcast Viridian Cloud Using the Hazelcast CLC -:description: In this tutorial, you'll learn the basics of managing {hazelcast-cloud} clusters using the Hazelcast CLC. You'll see how to create, list, and delete clusters, and how to download their logs. You'll also learn how to perform pause/resume operations on {hazelcast-cloud} clusters using the Hazelcast CLC. +:description: In this tutorial, you'll learn the basics of managing clusters on Hazelcast {hazelcast-cloud} using the Hazelcast CLC. You'll see how to create, list, and delete clusters, and how to download their logs. You'll also learn how to perform pause/resume operations on the clusters using the Hazelcast CLC. {description} @@ -17,7 +17,7 @@ You need the following: To allow the Hazelcast CLC to perform cluster operations, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + [source, bash] ---- diff --git a/docs/modules/ROOT/pages/overview.adoc b/docs/modules/ROOT/pages/overview.adoc index a2dcb0e6..73b36cf9 100644 --- a/docs/modules/ROOT/pages/overview.adoc +++ b/docs/modules/ROOT/pages/overview.adoc @@ -1,10 +1,10 @@ = Hazelcast Command-Line Client (CLC) :url-github-clc: https://github.com/hazelcast/hazelcast-cloud-cli/blob/master/README.md -:description: You can use the Hazelcast Command Line Client (CLC) to connect to and interact with clusters on {hazelcast-cloud} and Hazelcast Platform direct from the command line or through scripts. +:description: You can use the Hazelcast Command Line Client (CLC) to connect to and interact with clusters on Hazelcast {hazelcast-cloud} and Hazelcast Platform direct from the command line or through scripts. {description} -The Hazelcast CLC is a single binary with no dependencies. Within minutes of installation, you can start to perform common tasks on {hazelcast-cloud} and Hazelcast clusters. +The Hazelcast CLC is a single binary with no dependencies. Within minutes of installation, you can start to perform common tasks on clusters. == Install @@ -24,7 +24,7 @@ xref:clc-viridian.adoc[Create and manage] {hazelcast-cloud} clusters, and the cu === Create Data Pipelines -xref:clc-job.adoc[Create and manage] data pipelines using the Hazelcast CLC. Check out xref:hazelcast:pipelines:overview.adoc[Platform documentation] for more information about data pipelines. +xref:clc-job.adoc[Create and manage] data pipelines using the Hazelcast CLC. Check out the xref:hazelcast:pipelines:overview.adoc[Platform documentation] for more information about data pipelines. === Access Data for Debugging diff --git a/docs/modules/ROOT/pages/release-notes-5.2.0.adoc b/docs/modules/ROOT/pages/release-notes-5.2.0.adoc index fe69b700..3f47a2a2 100644 --- a/docs/modules/ROOT/pages/release-notes-5.2.0.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.2.0.adoc @@ -2,13 +2,13 @@ == New Features -* CLC can now read data serialized using Compact Serialization and Portable automatically. +* Hazelcast CLC can now automatically read data serialized using compact and portable serialization. * Added the ability to select a configuration from a list or import a {hazelcast-cloud} configuration when a configuration is not provided in the shell mode. -* Added the `--quite` (shorthand `-q`) flag which suppresses unnecessary outputs. CLC outputs can be sometimes noisy, such as success message logs; you can use this flag for a more quiet output. +* Added the `--quite` (shorthand `-q`) flag which suppresses unnecessary outputs. Hazelcast CLC outputs can be sometimes noisy, such as success message logs; you can use this flag for a more quiet output. * Added the `CLC_CLIENT_NAME` environment variable which allows overriding the default client name. * Added the `CLC_CLIENT_LABELS` environment variable which allows overriding the default client labels with a comma separated list of labels. -* Added support for link:https://hazelcast.com/products/viridian/[{hazelcast-cloud} Serverless]. +* Added support for link:https://hazelcast.com/products/viridian/[{hazelcast-cloud} Standard]. * Added the following commands: @@ -42,7 +42,7 @@ * Removed `map get-all`, `map put` and `map put-all` commands. * Added the `map set` command. * Auto-completion is disabled in interactive mode. -* {hazelcast-cloud} Serverless is the default cloud platform. +* {hazelcast-cloud} Standard is the default cloud platform. * The shell connects to the cluster on demand. == Known issues diff --git a/docs/modules/ROOT/pages/release-notes-5.2.1.adoc b/docs/modules/ROOT/pages/release-notes-5.2.1.adoc index 347e1bbe..e22fe2e9 100644 --- a/docs/modules/ROOT/pages/release-notes-5.2.1.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.2.1.adoc @@ -2,9 +2,9 @@ == Changes -* Corrected the --quite flag to --quiet. -* Uses the experimental NY readline library on Windows by default. That fixes arrow key related issues but disables syntax highlight for SQL -* Use stderr for unnecessary output. +* Corrects the `--quite` flag to `--quiet`. +* Uses the experimental NY readline library on Windows by default. This update fixes arrow key related issues but disables syntax highlighting for SQL. +* Uses stderr for unnecessary output. == Improvements * More consistent success messages when a list doesn't have any items. @@ -12,5 +12,5 @@ == Fixes * Fixed a race in shell command. -* Fixed a bug that would cause a panic if the SQL command is interrupted.* -* Powershell completion is fixed +* Fixed a bug that would cause a panic if the SQL command was interrupted. +* Powershell completion is fixed. diff --git a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc index 240a856a..7847b744 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc @@ -2,7 +2,7 @@ == New Features -* CLC can now submit Jet jobs and manage job snapshots. +* Hazelcast CLC can now submit Jet jobs and manage job snapshots. * Added the following `job` commands: ** `submit`: Creates a job from the given jar file. ** `cancel`: Cancels a job. @@ -11,6 +11,6 @@ ** `resume`: Resumes a suspended job. ** `restart`: Restarts a job. ** `export-snapshot`: Exports a snapshot for a job. This feature requires a {hazelcast-cloud} or Hazelcast Enterprise cluster. -* Added the following `snapshot` commandS: +* Added the following `snapshot` commands: ** `list`: Lists the snapshots of a job. ** `delete`: Deletes a snapshot. diff --git a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc index 230690ce..4008a7bb 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc @@ -2,7 +2,7 @@ == New Features -* {hazelcast-cloud} support. The following commands were added: +* Support for Hazelcast {hazelcast-cloud}. The following commands were added: ** `viridian login` ** `viridian create-cluster` ** `viridian delete-cluster` diff --git a/docs/modules/ROOT/pages/release-notes-5.3.0.adoc b/docs/modules/ROOT/pages/release-notes-5.3.0.adoc index 5e768834..a3ea8283 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.0.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.0.adoc @@ -1,7 +1,7 @@ = 5.3.0 Release Notes == New Features -* {hazelcast-cloud} support. The following commands were added: +* Support for Hazelcast {hazelcast-cloud}. The following commands were added: ** `viridian login` ** `viridian create-cluster` ** `viridian delete-cluster` @@ -16,7 +16,7 @@ ** `viridian download-custom-class` ** `viridian list-custom-classes` ** `viridian upload-custom-class` -* CLC can now submit Jet jobs and manage job snapshots. +* Hazelcast CLC can now submit Jet jobs and manage job snapshots. * Added the following `job` commands: ** `submit`: Creates a job from the given jar file. ** `cancel`: Cancels a job. diff --git a/docs/modules/ROOT/pages/release-notes-5.3.1.adoc b/docs/modules/ROOT/pages/release-notes-5.3.1.adoc index c14a2edb..53de998b 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.1.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.1.adoc @@ -2,7 +2,7 @@ == New Features -* Added the `--timeout` flag which causes CLC to exit with status code 2 if an operation cannot be completed in the given time. +* Added the `--timeout` flag which causes Hazelcast CLC to exit with status code 2 if an operation cannot be completed in the given time. == Improvements diff --git a/go.mod b/go.mod index 8e1dc3e4..f26031a6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/alecthomas/chroma v0.10.0 github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230809052932-73bc747e32b9 + github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230818105346-0389b1c5849c github.com/mattn/go-runewidth v0.0.14 github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d github.com/spf13/cobra v1.7.0 @@ -81,7 +81,6 @@ require ( golang.org/x/text v0.11.0 // indirect golang.org/x/tools v0.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( @@ -94,5 +93,5 @@ require ( github.com/go-git/go-git/v5 v5.8.1 github.com/mattn/go-colorable v0.1.12 github.com/nyaosorg/go-readline-ny v0.9.1 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 5ff34d84..fa82f07d 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230809052932-73bc747e32b9 h1:V1jVTVLL6BXU+yv2zWrfhhTcQ5oXDH+Q06umd2Z0HB8= -github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230809052932-73bc747e32b9/go.mod h1:PJ38lqXJ18S0YpkrRznPDlUH8GnnMAQCx3jpQtBPZ6Q= +github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230818105346-0389b1c5849c h1:V3Hhid/bU2mrR51+FG/FfmwyAEmq8gEFNRxfY/UerEw= +github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230818105346-0389b1c5849c/go.mod h1:PJ38lqXJ18S0YpkrRznPDlUH8GnnMAQCx3jpQtBPZ6Q= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -331,8 +331,6 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/it/context.go b/internal/it/context.go index 1d41133d..0313b91e 100644 --- a/internal/it/context.go +++ b/internal/it/context.go @@ -73,28 +73,31 @@ func (c CommandContext) SetTopLevel(b bool) { } type ExecContext struct { - lg *Logger - stdout *bytes.Buffer - stderr *bytes.Buffer - stdin *bytes.Buffer - args []string - props *plug.Properties - Rows []output.Row + lg *Logger + stdout *bytes.Buffer + stderr *bytes.Buffer + stdin *bytes.Buffer + args []string + props *plug.Properties + Rows []output.Row + Spinner *Spinner } func NewExecuteContext(args []string) *ExecContext { return &ExecContext{ - lg: NewLogger(), - stdout: &bytes.Buffer{}, - stderr: &bytes.Buffer{}, - stdin: &bytes.Buffer{}, - args: args, - props: plug.NewProperties(), + lg: NewLogger(), + stdout: &bytes.Buffer{}, + stderr: &bytes.Buffer{}, + stdin: &bytes.Buffer{}, + args: args, + props: plug.NewProperties(), + Spinner: NewSpinner(), } } -func (ec *ExecContext) ExecuteBlocking(context.Context, func(context.Context, clc.Spinner) (any, error)) (any, context.CancelFunc, error) { - //TODO implement me - panic("implement me") +func (ec *ExecContext) ExecuteBlocking(ctx context.Context, f func(context.Context, clc.Spinner) (any, error)) (any, context.CancelFunc, error) { + v, err := f(ctx, ec.Spinner) + stop := func() {} + return v, stop, err } func (ec *ExecContext) Props() plug.ReadOnlyProperties { @@ -187,3 +190,25 @@ func (ec *ExecContext) PrintlnUnnecessary(text string) { func (ec *ExecContext) WrapResult(f func() error) error { return f() } + +type Spinner struct { + Texts []string + Progresses []float32 +} + +func NewSpinner() *Spinner { + return &Spinner{} +} + +func (s *Spinner) Reset() { + s.Texts = nil + s.Progresses = nil +} + +func (s *Spinner) SetText(text string) { + s.Texts = append(s.Texts, text) +} + +func (s *Spinner) SetProgress(progress float32) { + s.Progresses = append(s.Progresses, progress) +} diff --git a/internal/it/test_context.go b/internal/it/test_context.go index 6e02f7c9..cdb2121b 100644 --- a/internal/it/test_context.go +++ b/internal/it/test_context.go @@ -30,7 +30,7 @@ import ( "time" hz "github.com/hazelcast/hazelcast-go-client" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" diff --git a/internal/it/util.go b/internal/it/util.go index 14746c49..5827ff30 100644 --- a/internal/it/util.go +++ b/internal/it/util.go @@ -34,7 +34,7 @@ import ( hz "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-go-client/logger" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/check" diff --git a/internal/iterator.go b/internal/iterator.go new file mode 100644 index 00000000..c6acae3f --- /dev/null +++ b/internal/iterator.go @@ -0,0 +1,17 @@ +package internal + +// Iterator is a generic iterator interface. +// Non thread safe. +type Iterator[T any] interface { + // Next returns false if the iterator is exhausted. + // Otherwise advances the iterator and returns true. + Next() bool + // Value returns the current value in the iterator. + // Next should always be called before Value is called. + // Otherwise may panic. + Value() T + // Err contains the error after advancing the iterator. + // If it is nil, it is safe to call Next. + // Otherwise Next should not be called. + Err() error +} diff --git a/internal/str/str.go b/internal/str/str.go index 77332356..f2613f4e 100644 --- a/internal/str/str.go +++ b/internal/str/str.go @@ -2,6 +2,7 @@ package str import ( "fmt" + "strconv" "strings" ) @@ -37,3 +38,12 @@ func MaybeShorten(s string, l int) string { } return fmt.Sprintf("%s...", s[:l]) } + +// SpacePaddedIntFormat returns the fmt string that can fit the given integer. +// The padding uses spaces. +func SpacePaddedIntFormat(maxValue int) string { + if maxValue < 0 { + panic("SpacePaddedIntFormat: cannot be negative") + } + return fmt.Sprintf("%%%dd", len(strconv.Itoa(maxValue))) +} diff --git a/internal/str/str_test.go b/internal/str/str_test.go index ab2c0b58..b7ca05a3 100644 --- a/internal/str/str_test.go +++ b/internal/str/str_test.go @@ -1,6 +1,7 @@ package str_test import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -86,3 +87,24 @@ func TestSplitByComma(t *testing.T) { }) } } + +func TestSpacePaddedIntFormat(t *testing.T) { + testCases := []struct { + num int + out string + }{ + {num: 0, out: "%1d"}, + {num: 9, out: "%1d"}, + {num: 10, out: "%2d"}, + {num: 99, out: "%2d"}, + {num: 100, out: "%3d"}, + {num: 9999, out: "%4d"}, + } + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("pad %d", tc.num), func(t *testing.T) { + s := str.SpacePaddedIntFormat(tc.num) + assert.Equal(t, tc.out, s) + }) + } +}