From 65ea360cc0568cf7636a420e2d8fe008d29cc768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 28 Jul 2023 15:46:51 +0300 Subject: [PATCH 01/79] Simple refactor to be able to change the main command help and name (#310) Simple refactor to be able to change the main command help and name --- Makefile | 5 +++-- base/commands/viridian/tokens.go | 2 ++ clc/cmd/clc.go | 5 +++-- cmd/clc/main.go | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 79841aeca..715dc65c3 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,8 @@ GIT_COMMIT = $(shell git rev-parse HEAD 2> /dev/null || echo unknown) CLC_VERSION ?= v0.0.0-CUSTOMBUILD -LDFLAGS = "-s -w -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)'" +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 50m COVERAGE_OUT = coverage.out PACKAGES = $(shell go list ./... | grep -v internal/it | tr '\n' ',') @@ -14,7 +15,7 @@ RELEASE_FILE ?= release.txt TARGZ ?= true build: - CGO_ENABLED=0 go build -tags base,std,hazelcastinternal,hazelcastinternaltest -ldflags $(LDFLAGS) -o build/$(BINARY_NAME) ./cmd/clc + CGO_ENABLED=0 go build -tags base,std,hazelcastinternal,hazelcastinternaltest -ldflags "$(LDFLAGS)" -o build/$(BINARY_NAME) ./cmd/clc test: go test -tags base,std,hazelcastinternal,hazelcastinternaltest -p 1 $(TEST_FLAGS) ./... diff --git a/base/commands/viridian/tokens.go b/base/commands/viridian/tokens.go index 81ee3dce1..622a8a641 100644 --- a/base/commands/viridian/tokens.go +++ b/base/commands/viridian/tokens.go @@ -1,3 +1,5 @@ +//go:build std || viridian + package viridian import ( diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index 8def61c53..e6e11cc84 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -26,6 +26,7 @@ import ( ) var ( + MainCommandShortHelp = "Hazelcast CLC" // client is currently global in order to have a single client. // This is bad. // TODO: make the client unique without making it global. @@ -62,8 +63,8 @@ type Main struct { func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLevel string, sio clc.IO) (*Main, error) { rc := &cobra.Command{ Use: arg0, - Short: "Hazelcast CLC", - Long: "Hazelcast CLC", + Short: MainCommandShortHelp, + Long: MainCommandShortHelp, Args: cobra.NoArgs, CompletionOptions: cobra.CompletionOptions{DisableDescriptions: true}, SilenceErrors: true, diff --git a/cmd/clc/main.go b/cmd/clc/main.go index ea06ac459..1c8ae5030 100644 --- a/cmd/clc/main.go +++ b/cmd/clc/main.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "github.com/spf13/cobra" @@ -37,7 +38,8 @@ func main() { if err != nil { bye(err) } - m, err := cmd.NewMain("clc", cfgPath, cp, logPath, logLevel, clc.StdIO()) + _, name := filepath.Split(os.Args[0]) + m, err := cmd.NewMain(name, cfgPath, cp, logPath, logLevel, clc.StdIO()) if err != nil { bye(err) } From a85cf45673c840f98d3569d1a58b2619c58dd2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 28 Jul 2023 15:48:58 +0300 Subject: [PATCH 02/79] Use Python code sample instead of Go (#312) Use Python code sample instead of Go --- README.md | 20 --------- base/commands/config/config_import.go | 6 +-- base/commands/config/config_it_test.go | 2 +- clc/config/import.go | 19 +++++---- docs/modules/ROOT/pages/clc-config.adoc | 56 ------------------------- internal/types/types.go | 7 ++++ internal/viridian/api.go | 10 ++--- internal/viridian/cluster_log.go | 2 +- 8 files changed, 27 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 90a560740..f0615e9ab 100644 --- a/README.md +++ b/README.md @@ -178,26 +178,6 @@ The following keyboard shortcuts are available in the interactive-mode: | Ctrl + L | Clear the screen | | Ctrl + C | Cancel running command | -## Connecting to Viridian Serverless - -1. If you don't have a running Viridian Serverless cluster, follow the steps in [Step 1. Start a Viridian Serverless Development Cluster](https://docs.hazelcast.com/cloud/get-started#step-1-start-a-viridian-serverless-development-cluster) to create a cluster. - Both development and production clusters will work very well. -2. Download the Go client sample for your cluster from the Viridian Console. The sample is typically a Zip file with the following name format: "hazelcast-cloud-go-sample-client-CLUSTER-ID-default.zip". For instance: `hazelcast-cloud-csharp-sample-client-pr-3814-default.zip` -3. Import the configuration with CLC with `my-cluster` as the name: - ``` - $ clc config import my-cluster ~/hazelcast-cloud-go-sample-client-pr-3814-default.zip - ``` -4. Check that the configuration is known to CLC: - ``` - $ clc config list - default - my-cluster - ``` -5. In order to use this configuration, use `-c CONFIG_NAME` flag whenever you run CLC: - ``` - $ clc -c my-cluster map set my-key my-value - ``` - ## Generating auto-completion CLC supports auto-completion in the non-interactive mode for the following shells: diff --git a/base/commands/config/config_import.go b/base/commands/config/config_import.go index 8e25cf4a4..5f648a1dd 100644 --- a/base/commands/config/config_import.go +++ b/base/commands/config/config_import.go @@ -23,16 +23,16 @@ Currently importing only Viridian connection configuration is supported. 1. On Viridian console, visit: - Dashboard -> Connect Client -> Quick connection guide -> Go + Dashboard -> Connect Client -> Quick connection guide -> Python 2. Copy the text in box 1 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 Go client sample: +Alternatively, you can use an already downloaded Python client sample: - clc config import my-config /home/me/Downloads/hazelcast-cloud-go-sample....zip + clc config import my-config /home/me/Downloads/hazelcast-cloud-python-sample....zip ` cc.SetCommandHelp(long, short) diff --git a/base/commands/config/config_it_test.go b/base/commands/config/config_it_test.go index 7a1fbd812..fa16e1188 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-go-sample-client-pr-FOR_TESTING-default.zip" + const configURL = "https://rcd-download.s3.us-east-2.amazonaws.com/hazelcast-cloud-python-sample-client-pr-FOR_TESTING-default.zip" tcx.Tester(func(tcx it.TestContext) { name := it.NewUniqueObjectName("cfg") ctx := context.Background() diff --git a/clc/config/import.go b/clc/config/import.go index 7ed653f07..bca9298fa 100644 --- a/clc/config/import.go +++ b/clc/config/import.go @@ -135,9 +135,9 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string defer reader.Close() var goPaths []string var pemFiles []*zip.File - // find .go and .pem paths + // find .py and .pem paths for _, rf := range reader.File { - if strings.HasSuffix(rf.Name, ".go") { + if strings.HasSuffix(rf.Name, ".py") { goPaths = append(goPaths, rf.Name) continue } @@ -151,7 +151,7 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string // find the configuration bits token, clusterName, pw, cfgFound := extractConfigFields(reader, goPaths) if !cfgFound { - return nil, errors.New("go file with configuration not found") + return nil, errors.New("python file with configuration not found") } opts := makeViridianOpts(clusterName, token, pw) outDir, cfgPath, err := Create(target, opts) @@ -162,7 +162,7 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string if err := copyFiles(ec, pemFiles, outDir); err != nil { return nil, err } - return cfgPath, nil + return paths.Join(outDir, cfgPath), nil }) if err != nil { return "", err @@ -182,8 +182,8 @@ func makeViridianOpts(clusterName, token, password string) clc.KeyValues[string, } } -func extractConfigFields(reader *zip.ReadCloser, goPaths []string) (token, clusterName, pw string, cfgFound bool) { - for _, p := range goPaths { +func extractConfigFields(reader *zip.ReadCloser, pyPaths []string) (token, clusterName, pw string, cfgFound bool) { + for _, p := range pyPaths { rc, err := reader.Open(p) if err != nil { continue @@ -233,22 +233,23 @@ func copyFiles(ec plug.ExecContext, files []*zip.File, outDir string) error { func extractClusterName(text string) string { // extract from config.Cluster.Name = "pr-3814" - const re = `config.Cluster.Name\s+=\s+"([^"]+)"` + const re = `cluster_name="([^"]+)"` return extractSimpleString(re, text) } func extractViridianToken(text string) string { // extract from: config.Cluster.Cloud.Token = "EWEKHVOOQOjMN5mXB8OngRF4YG5aOm6N2LUEOlhdC7SWpY54hm" - const re = `config.Cluster.Cloud.Token\s+=\s+"([^"]+)"` + const re = `cloud_discovery_token="([^"]+)"` return extractSimpleString(re, text) } func extractKeyPassword(text string) string { // extract from: err = config.Cluster.Network.SSL.AddClientCertAndEncryptedKeyPath(certFile, keyFile, "12ee6ff601a") - const re = `config.Cluster.Network.SSL.AddClientCertAndEncryptedKeyPath\(certFile,\s+keyFile,\s+"([^"]+)"` + const re = `ssl_password="([^"]+)"` return extractSimpleString(re, text) } + func extractSimpleString(pattern, text string) string { re, err := regexp.Compile(pattern) if err != nil { diff --git a/docs/modules/ROOT/pages/clc-config.adoc b/docs/modules/ROOT/pages/clc-config.adoc index b1ea4ae82..c8fe781bb 100644 --- a/docs/modules/ROOT/pages/clc-config.adoc +++ b/docs/modules/ROOT/pages/clc-config.adoc @@ -12,7 +12,6 @@ clc config [command] [flags] == Commands * <> -* <> * <> == clc config add @@ -131,61 +130,6 @@ Example output: clc config add dev cluster.name=dev cluster.address=localhost:5701 ---- -== clc config import - -Imports configuration from an arbitrary source. - -Currently importing only Viridian connection configuration is supported. - -1. On Viridian console, visit: -+ -Dashboard -> Connect Client -> Quick connection guide -> Go - -2. Copy the text in box 1 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 Go client sample: -+ - clc config import my-config /home/me/Downloads/hazelcast-cloud-go-sample....zip - -Usage: - -[source,bash] ----- -clc config import [configuration-name] [source] [flags] ----- - -Parameters: - -[cols="1m,1a,2a,1a"] -|=== -|Parameter|Required|Description|Default - -|`configuration-name` -|Required -|Name of the configuration or its path. If a name is used, the configuration is saved to `$CLC_HOME/configs/NAME` -|N/A - -|`source` -|Required -|The following configuration sources are supported: - -* Zip file for the Viridian Go client, e.g., `hazelcast-cloud-go-sample-client-pr-3814-default.zip`. Use this if you have downloaded the sample yourself. -* cURL command line for the Viridian Go client sample, such as: `curl https://api.viridian.hazelcast.com/client_samples/download/XXX -o hazelcast-cloud-go-sample-client-pr-3814-default.zip`. Use this command to allow the Hazelcast CLC to download the sample. Do not forget to wrap the line with quotes. - -|N/A - -|=== - -Example output: - -[source,bash] ----- -clc config import production ~/Downloads/hazelcast-cloud-go-sample-client-pr-3814-default.zip ----- - == clc config list Lists the known configurations. diff --git a/internal/types/types.go b/internal/types/types.go index 85f3f866b..9859f18ba 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -4,3 +4,10 @@ type Tuple2[A, B any] struct { First A Second B } + +func MakeTuple2[A, B any](first A, second B) Tuple2[A, B] { + return Tuple2[A, B]{ + First: first, + Second: second, + } +} diff --git a/internal/viridian/api.go b/internal/viridian/api.go index 5c2834279..3d52eea32 100644 --- a/internal/viridian/api.go +++ b/internal/viridian/api.go @@ -106,9 +106,6 @@ func (a API) UploadCustomClasses(ctx context.Context, p func(progress float32), } return nil, nil }) - if err != nil { - return err - } if err != nil { return fmt.Errorf("uploading custom class: %w", err) } @@ -173,8 +170,11 @@ func (a API) DownloadConfig(ctx context.Context, clusterID string) (path string, if err != nil { return types.Tuple2[string, func()]{}, err } - return types.Tuple2[string, func()]{path, stop}, nil + return types.MakeTuple2(path, stop), nil }) + if err != nil { + return "", nil, err + } return r.First, r.Second, nil } @@ -425,7 +425,7 @@ func download(ctx context.Context, url, token string) (downloadPath string, stop } func makeConfigURL(clusterID string) string { - return makeUrl(fmt.Sprintf("/client_samples/%s/go?source_identifier=default", clusterID)) + return makeUrl(fmt.Sprintf("/client_samples/%s/python?source_identifier=default", clusterID)) } func APIClass() string { diff --git a/internal/viridian/cluster_log.go b/internal/viridian/cluster_log.go index e7131f445..4d6fdd2ec 100644 --- a/internal/viridian/cluster_log.go +++ b/internal/viridian/cluster_log.go @@ -26,7 +26,7 @@ func (a API) DownloadClusterLogs(ctx context.Context, destDir string, idOrName s return types.Tuple2[string, func()]{path, stop}, nil }) if err != nil { - return err + return fmt.Errorf("downloading cluster logs: %w", err) } defer r.Second() zipFile, err := os.Open(r.First) From 1e6d7494151fbe098a17f0a85cbe71bad01060ed Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Mon, 31 Jul 2023 13:43:40 +0300 Subject: [PATCH 03/79] Add CLC_CONFIG env variable [CLC-198] (#313) --- clc/const.go | 1 + clc/paths/paths.go | 11 ++++++++++- docs/modules/ROOT/pages/environment-variables.adoc | 11 ++++------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/clc/const.go b/clc/const.go index 2be2521c1..1ad527710 100644 --- a/clc/const.go +++ b/clc/const.go @@ -31,6 +31,7 @@ const ( GroupJetID = "jet" GroupJetTitle = "Jet" EnvMaxCols = "CLC_MAX_COLS" + EnvConfig = "CLC_CONFIG" EnvSkipServerVersionCheck = "CLC_SKIP_SERVER_VERSION_CHECK" FlagAutoYes = "yes" ) diff --git a/clc/paths/paths.go b/clc/paths/paths.go index 7885d133e..27a83b1c8 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -6,6 +6,8 @@ import ( "path/filepath" "strings" "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc" ) const ( @@ -55,13 +57,20 @@ func DefaultConfigPath() string { if p := nearbyConfigPath(); p != "" { return p } - p := filepath.Join(ResolveConfigDir("default"), DefaultConfig) + p := filepath.Join(ResolveConfigDir(configName()), DefaultConfig) if Exists(p) { return p } return "" } +func configName() string { + if cfg := os.Getenv(clc.EnvConfig); cfg != "" { + return cfg + } + return "default" +} + func DefaultLogPath(now time.Time) string { fn := fmt.Sprintf("%s.log", now.Format("2006-01-02")) return filepath.Join(Logs(), fn) diff --git a/docs/modules/ROOT/pages/environment-variables.adoc b/docs/modules/ROOT/pages/environment-variables.adoc index be4493373..05fc1be07 100644 --- a/docs/modules/ROOT/pages/environment-variables.adoc +++ b/docs/modules/ROOT/pages/environment-variables.adoc @@ -26,15 +26,11 @@ |CLC_SKIP_SERVER_VERSION_CHECK |Some Hazelcast CLC features require the cluster to be of a certain version. If set to `1`, this variable disables the version check performed by the CLC. |`0` (false) -//// -|CLC_VIRIDIAN_API_KEY -|Sets the {hazelcast-cloud} API key. -| -|CLC_VIRIDIAN_API_SECRET -|Sets the {hazelcast-cloud} API secret. +|CLC_CONFIG +|Sets the configuration CLC will use if `--config` flag is not specified. | -//// + |=== The following environment variables are experimental and may be removed in a future version. @@ -45,6 +41,7 @@ The following environment variables are experimental and may be removed in a fut |CLC_EXPERIMENTAL_FORMATTER |Sets a formatter for the highlighting of SQL syntax, either: `terminal`, `terminal8`, `terminal16`, `terminal256`, or `terminal16m`. Only with the `gohxs` readline implementation. +| |CLC_EXPERIMENTAL_READLINE |Sets the readline implementation, either `gohxs` or `ny`. From d60fabe266b366e14f457808fa519d6214d145c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 31 Jul 2023 16:18:48 +0300 Subject: [PATCH 04/79] Config files should have the .yaml extensions (#314) --- clc/paths/paths.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clc/paths/paths.go b/clc/paths/paths.go index 27a83b1c8..bf9a76b78 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -96,7 +96,7 @@ func ResolveConfigPath(path string) string { if path == "" { return path } - if filepath.Ext(path) == "" { + if filepath.Ext(path) != ".yaml" { path = filepath.Join(Configs(), path, DefaultConfig) } return path From c4f68cdd38d27966270d588086489d8381bd1ab6 Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Thu, 3 Aug 2023 08:50:18 +0300 Subject: [PATCH 05/79] [CLC-227]: Revert refresh token flow and implement access token flow instead (#316) --- base/commands/viridian/common.go | 68 +++++++- base/commands/viridian/common_test.go | 8 +- base/commands/viridian/tokens.go | 185 --------------------- base/commands/viridian/viridian_login.go | 15 +- clc/secrets/secrets.go | 78 +++------ docs/modules/ROOT/nav.adoc | 1 + internal/viridian/api.go | 91 ++++------ internal/viridian/cluster.go | 28 ++-- internal/viridian/cluster_log.go | 8 +- internal/viridian/custom_class_download.go | 6 +- internal/viridian/custom_class_upload.go | 6 +- internal/viridian/login.go | 11 +- 12 files changed, 167 insertions(+), 338 deletions(-) delete mode 100644 base/commands/viridian/tokens.go diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 7b8530731..5e76d205b 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -7,9 +7,14 @@ import ( "errors" "fmt" "net/http" + "os" + "path/filepath" + "sort" "strings" "time" + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) @@ -20,15 +25,72 @@ const ( ) var ( - ErrClusterFailed = errors.New("cluster failed") + ErrClusterFailed = errors.New("cluster failed") + ErrLoadingSecrets = errors.New("could not load Viridian secrets, did you login?") ) +func findToken(apiKey string) (string, error) { + ac := viridian.APIClass() + if apiKey == "" { + apiKey = os.Getenv(viridian.EnvAPIKey) + } + if apiKey != "" { + return fmt.Sprintf("%s-%s", ac, apiKey), nil + } + tokenPaths, err := findAll(secretPrefix) + if err != nil { + return "", fmt.Errorf("cannot access the secrets, did you login?: %w", err) + } + // sort tokens, so findToken returns the same token everytime. + sort.Slice(tokenPaths, func(i, j int) bool { + return tokenPaths[i] < tokenPaths[j] + }) + var tp string + for _, p := range tokenPaths { + if strings.HasPrefix(p, ac) { + tp = p + break + } + } + if tp == "" { + return "", fmt.Errorf("no secrets found, did you login?") + } + return tp, nil +} + +func findAll(prefix string) ([]string, error) { + return paths.FindAll(paths.Join(paths.Secrets(), prefix), func(basePath string, entry os.DirEntry) (ok bool) { + return !entry.IsDir() && filepath.Ext(entry.Name()) == filepath.Ext(secrets.TokenFileFormat) + }) +} + +func findKeyAndSecret(tokenPath string) (string, string, error) { + apiKey := strings.TrimPrefix(strings.TrimSuffix(tokenPath, filepath.Ext(tokenPath)), fmt.Sprintf("%s-", viridian.APIClass())) + fn := fmt.Sprintf(secrets.SecretFileFormat, viridian.APIClass(), apiKey) + secret, err := secrets.Read(secretPrefix, fn) + if err != nil { + return "", "", err + } + return apiKey, string(secret), nil +} + func getAPI(ec plug.ExecContext) (*viridian.API, error) { - t, err := FindTokens(ec) + tp, err := findToken(ec.Props().GetString(propAPIKey)) if err != nil { return nil, err } - return viridian.NewAPI(secretPrefix, t.Key, t.AccessToken, t.RefreshToken, t.ExpiresIn), nil + ec.Logger().Info("Using Viridian secret at: %s", tp) + token, err := secrets.Read(secretPrefix, tp) + if err != nil { + ec.Logger().Error(err) + return nil, ErrLoadingSecrets + } + key, secret, err := findKeyAndSecret(tp) + if err != nil { + ec.Logger().Error(err) + return nil, ErrLoadingSecrets + } + return viridian.NewAPI(secretPrefix, key, secret, string(token)), nil } func waitClusterState(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterIDOrName, state string) error { diff --git a/base/commands/viridian/common_test.go b/base/commands/viridian/common_test.go index 998d98fea..26f8f92f5 100644 --- a/base/commands/viridian/common_test.go +++ b/base/commands/viridian/common_test.go @@ -22,19 +22,19 @@ func TestFindToken(t *testing.T) { it.WithEnv(paths.EnvCLCHome, home.Path(), func() { it.WithEnv(viridian.EnvAPIKey, "", func() { // should return an error if there are no secrets - _, err := findAccessTokenPath("") + _, err := findToken("") require.Error(t, err) // fixture check.Must(secrets.Write(prefix, "api-APIKEY1.access", []byte("token-APIKEY1"))) check.Must(secrets.Write(prefix, "api-APIKEY2.access", []byte("token-APIKEY2"))) check.Must(secrets.Write(prefix, "cls-CLSKEY1.access", []byte("token-CLSKEY1"))) // check the token filename for the first API key is returned if the API key was not specified - require.Equal(t, "api-APIKEY1.access", check.MustValue(findAccessTokenPath(""))) + require.Equal(t, "api-APIKEY1.access", check.MustValue(findToken(""))) // check the token filename for the given API key is returned - require.Equal(t, "api-APIKEY2.access", check.MustValue(findAccessTokenPath("APIKEY2"))) + require.Equal(t, "api-APIKEY2.access", check.MustValue(findToken("APIKEY2.access"))) // check the token filename for the given API class is returned it.WithEnv(viridian.EnvAPI, "cls", func() { - require.Equal(t, "cls-CLSKEY1.access", check.MustValue(findAccessTokenPath("CLSKEY1"))) + require.Equal(t, "cls-CLSKEY1.access", check.MustValue(findToken("CLSKEY1.access"))) }) }) }) diff --git a/base/commands/viridian/tokens.go b/base/commands/viridian/tokens.go deleted file mode 100644 index 622a8a641..000000000 --- a/base/commands/viridian/tokens.go +++ /dev/null @@ -1,185 +0,0 @@ -//go:build std || viridian - -package viridian - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - "time" - - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" - "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" -) - -type Tokens struct { - Key string - AccessToken string - RefreshToken string - ExpiresIn int -} - -func FindTokens(ec plug.ExecContext) (Tokens, error) { - apiKey := ec.Props().GetString(propAPIKey) - accessTokenFilePath, err := findAccessTokenPath(apiKey) - if err != nil { - return Tokens{}, err - } - if apiKey == "" { - apiKey = findAPIKey(accessTokenFilePath) - } - ec.Logger().Info("Using Viridian secret at: %s", accessTokenFilePath) - err = refreshTokenIfExpired(secretPrefix, accessTokenFilePath) - if err != nil { - return Tokens{}, err - } - token, err := readToken(secretPrefix, apiKey, secrets.AccessTokenFileFormat) - if err != nil { - ec.Logger().Error(err) - return Tokens{}, fmt.Errorf("could not load Viridian secrets, did you login?") - } - refToken, err := readToken(secretPrefix, apiKey, secrets.RefreshTokenFileFormat) - if err != nil { - ec.Logger().Error(err) - return Tokens{}, fmt.Errorf("could not load Viridian secrets, did you login?") - } - _, expiresIn, err := expiryValues(secretPrefix, fmt.Sprintf(secrets.ExpiresInFileFormat, viridian.APIClass(), apiKey)) - if err != nil { - ec.Logger().Error(err) - return Tokens{}, fmt.Errorf("could not load Viridian secrets, did you login?") - } - return Tokens{ - Key: apiKey, - AccessToken: token, - RefreshToken: refToken, - ExpiresIn: expiresIn, - }, nil -} - -func findAccessTokenPath(apiKey string) (string, error) { - var path string - if apiKey != "" { - path = fmt.Sprintf(secrets.AccessTokenFileFormat, viridian.APIClass(), apiKey) - } else if os.Getenv(viridian.EnvAPIKey) != "" { - path = fmt.Sprintf(secrets.AccessTokenFileFormat, viridian.APIClass(), os.Getenv(viridian.EnvAPIKey)) - } else { - tokenPaths, err := findAllAccessTokenFiles(secretPrefix) - if err != nil { - return "", fmt.Errorf("cannot access the secrets, did you login?: %w", err) - } - // sort tokens, so it returns the same token everytime. - sort.Slice(tokenPaths, func(i, j int) bool { - return tokenPaths[i] < tokenPaths[j] - }) - for _, p := range tokenPaths { - if strings.HasPrefix(p, viridian.APIClass()) { - path = p - break - } - } - if path == "" { - return "", fmt.Errorf("no secrets found, did you login?") - } - } - return path, nil -} - -func findAllAccessTokenFiles(prefix string) ([]string, error) { - return paths.FindAll(paths.Join(paths.Secrets(), prefix), func(basePath string, entry os.DirEntry) (ok bool) { - return !entry.IsDir() && filepath.Ext(entry.Name()) == filepath.Ext(secrets.AccessTokenFileFormat) - }) -} - -func readToken(prefix, apiKey, fileFormat string) (string, error) { - b, err := secrets.Read(prefix, fmt.Sprintf(fileFormat, viridian.APIClass(), apiKey)) - return string(b), err -} - -func refreshTokenIfExpired(secretPrefix, accessTokenFileName string) error { - apiKey := findAPIKey(accessTokenFileName) - expiryFileName := fmt.Sprintf(secrets.ExpiresInFileFormat, viridian.APIClass(), apiKey) - expired, err := isTokenExpired(secretPrefix, expiryFileName) - if err != nil { - return err - } - if expired { - refreshTokenFileName := fmt.Sprintf(secrets.RefreshTokenFileFormat, viridian.APIClass(), apiKey) - refreshToken, err := secrets.Read(secretPrefix, refreshTokenFileName) - if err != nil { - return err - } - _, e, err := expiryValues(secretPrefix, expiryFileName) - if err != nil { - return err - } - api := viridian.API{Key: apiKey, SecretPrefix: secretPrefix, RefreshToken: string(refreshToken), ExpiresIn: e} - r, err := api.RefreshAccessToken(context.Background()) - if err != nil { - return err - } - // save to .access file - if err = secrets.Write(secretPrefix, accessTokenFileName, []byte(r.AccessToken)); err != nil { - return err - } - // save to .refresh file - if err = secrets.Write(secretPrefix, refreshTokenFileName, []byte(r.RefreshToken)); err != nil { - return err - } - // save to .expiry file - d, err := expiryData(e) - if err != nil { - return err - } - if err := os.WriteFile(paths.ResolveSecretPath(secretPrefix, expiryFileName), d, 0600); err != nil { - return fmt.Errorf("writing the expires in to file: %w", err) - } - } - return nil -} - -func findAPIKey(accessTokenFileName string) string { - pre := fmt.Sprintf("%s-", viridian.APIClass()) - ext := fmt.Sprintf("%s", filepath.Ext(secrets.AccessTokenFileFormat)) - return strings.TrimPrefix(strings.TrimSuffix(accessTokenFileName, ext), pre) -} - -func isTokenExpired(secretPrefix, expiryFileName string) (bool, error) { - t, _, err := expiryValues(secretPrefix, expiryFileName) - if err != nil { - return false, err - } - if time.Unix(t, 0).After(time.Now()) { - return false, nil - } - return true, nil -} - -func expiryValues(secretPrefix, expiryFileName string) (int64, int, error) { - expiry, err := os.ReadFile(paths.Join(paths.Secrets(), secretPrefix, expiryFileName)) - if err != nil { - return 0, 0, err - } - ex := strings.Split(string(expiry), "-") - expiryDuration, err := strconv.Atoi(ex[1]) - if err != nil { - return 0, 0, err - } - creationTime, err := strconv.ParseInt(ex[0], 10, 64) - if err != nil { - return 0, 0, err - } - return creationTime, expiryDuration, nil -} - -func expiryData(expiresIn int) ([]byte, error) { - secrets.CalculateExpiry(expiresIn) - ts := strconv.FormatInt(time.Now().Unix(), 10) - ex := strconv.Itoa(expiresIn) - return []byte(fmt.Sprintf("%s-%s", ts, ex)), nil -} diff --git a/base/commands/viridian/viridian_login.go b/base/commands/viridian/viridian_login.go index dd91afe0c..f3599f7f4 100644 --- a/base/commands/viridian/viridian_login.go +++ b/base/commands/viridian/viridian_login.go @@ -46,11 +46,12 @@ func (cm LoginCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - api, err := cm.retrieveTokens(ctx, ec, key, secret) + token, err := cm.retrieveToken(ctx, ec, key, secret) if err != nil { return err } - if err = secrets.Save(secretPrefix, key, viridian.APIClass(), api.Token, api.RefreshToken, api.ExpiresIn); err != nil { + + if err = secrets.Save(ctx, viridian.APIClass(), secretPrefix, key, secret, token); err != nil { return err } ec.PrintlnUnnecessary("") @@ -58,20 +59,20 @@ func (cm LoginCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return nil } -func (cm LoginCmd) retrieveTokens(ctx context.Context, ec plug.ExecContext, key, secret string) (viridian.API, error) { +func (cm LoginCmd) retrieveToken(ctx context.Context, ec plug.ExecContext, key, secret string) (string, error) { ti, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Logging in") - api, err := viridian.Login(ctx, key, secret) + api, err := viridian.Login(ctx, secretPrefix, key, secret) if err != nil { return nil, err } - return api, err + return api.Token, err }) if err != nil { - return viridian.API{}, handleErrorResponse(ec, err) + return "", handleErrorResponse(ec, err) } stop() - return ti.(viridian.API), nil + return ti.(string), nil } func apiKeySecret(ec plug.ExecContext) (key, secret string, err error) { diff --git a/clc/secrets/secrets.go b/clc/secrets/secrets.go index a1697b9e3..fb0ce1462 100644 --- a/clc/secrets/secrets.go +++ b/clc/secrets/secrets.go @@ -1,22 +1,38 @@ package secrets import ( + "context" "encoding/base64" "fmt" "os" "path/filepath" - "strconv" - "time" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" ) const ( - AccessTokenFileFormat = "%s-%s.access" - RefreshTokenFileFormat = "%s-%s.refresh" - ExpiresInFileFormat = "%s-%s.expiry" + TokenFileFormat = "%s-%s.access" + SecretFileFormat = "%s-%s.secret" ) +func Save(ctx context.Context, apiClass, secretPrefix, key, secret, token string) error { + tokenFile := fmt.Sprintf(TokenFileFormat, apiClass, key) + secretFile := fmt.Sprintf(SecretFileFormat, apiClass, key) + if ctx.Err() != nil { + return ctx.Err() + } + if err := os.MkdirAll(paths.Secrets(), 0700); err != nil { + return fmt.Errorf("creating secrets directory: %w", err) + } + if err := Write(secretPrefix, tokenFile, []byte(token)); err != nil { + return err + } + if err := Write(secretPrefix, secretFile, []byte(secret)); err != nil { + return err + } + return nil +} + func Write(prefix, name string, token []byte) (err error) { path := paths.ResolveSecretPath(prefix, name) dir := filepath.Dir(path) @@ -44,52 +60,8 @@ func Read(prefix, name string) ([]byte, error) { return b[:n], nil } -func Save(secretPrefix, key, apiClass, token, refreshToken string, expiresIn int) error { - if err := os.MkdirAll(paths.Secrets(), 0700); err != nil { - return fmt.Errorf("creating secrets directory: %w", err) - } - if err := saveToken(secretPrefix, apiClass, key, token); err != nil { - return err - } - if err := saveRefreshToken(secretPrefix, apiClass, key, refreshToken); err != nil { - return err - } - if err := saveExpiry(secretPrefix, apiClass, key, expiresIn); err != nil { - return err - } - return nil -} - -func saveToken(secretPrefix, apiClass, key, token string) error { - fn := fmt.Sprintf(AccessTokenFileFormat, apiClass, key) - if err := Write(secretPrefix, fn, []byte(token)); err != nil { - return err - } - return nil -} - -func saveRefreshToken(secretPrefix, apiClass, key, refreshToken string) error { - fn := fmt.Sprintf(RefreshTokenFileFormat, apiClass, key) - if err := Write(secretPrefix, fn, []byte(refreshToken)); err != nil { - return err - } - return nil -} - -func saveExpiry(secretPrefix, apiClass, key string, expiresIn int) error { - fn := fmt.Sprintf(fmt.Sprintf(ExpiresInFileFormat, apiClass, key)) - path := paths.ResolveSecretPath(secretPrefix, fn) - ts := strconv.FormatInt(CalculateExpiry(expiresIn), 10) - ex := strconv.Itoa(expiresIn) - // We have to save to this file in (expireTime + expireDuration)-expireDuration format, - // Because Viridian refresh token endpoint does not return expiryDuration - // On Viridian expiryDuration is related to api key - if err := os.WriteFile(path, []byte(fmt.Sprintf("%s-%s", ts, ex)), 0600); err != nil { - return fmt.Errorf("writing the expires in to file: %w", err) - } - return nil -} - -func CalculateExpiry(expiresIn int) int64 { - return time.Now().Add(time.Duration(expiresIn) * time.Second).Unix() +func FindAll(prefix string) ([]string, error) { + return paths.FindAll(paths.Join(paths.Secrets(), prefix), func(basePath string, entry os.DirEntry) (ok bool) { + return !entry.IsDir() && filepath.Ext(entry.Name()) == filepath.Ext(TokenFileFormat) + }) } diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 0947d144b..3abf4619f 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -26,6 +26,7 @@ ** xref:clc-set.adoc[] ** xref:clc-queue.adoc[] ** xref:clc-topic.adoc[] +** xref:clc-multimap.adoc[] ** xref:clc-script.adoc[] ** xref:clc-sql.adoc[] ** xref:clc-snapshot.adoc[] diff --git a/internal/viridian/api.go b/internal/viridian/api.go index 3d52eea32..cdd30345b 100644 --- a/internal/viridian/api.go +++ b/internal/viridian/api.go @@ -28,50 +28,26 @@ type Wrapper[T any] struct { Content T } +//TODO: We need to separate API and secrets into two different structs + type API struct { - Key string SecretPrefix string Token string - RefreshToken string - ExpiresIn int + Key string + Secret string } -func NewAPI(secretPrefix, key, token, refreshToken string, expiresIn int) *API { +func NewAPI(secretPrefix, key, secret, token string) *API { return &API{ SecretPrefix: secretPrefix, Key: key, + Secret: secret, Token: token, - RefreshToken: refreshToken, - ExpiresIn: expiresIn, } } -type refreshTokenRequest struct { - RefreshToken string `json:"refreshToken"` -} - -type RefreshTokenResponse struct { - AccessToken string `json:"accessToken"` - RefreshToken string `json:"refreshToken"` -} - -func (a *API) RefreshAccessToken(ctx context.Context) (RefreshTokenResponse, error) { - r := refreshTokenRequest{RefreshToken: a.RefreshToken} - resp, err := doPost[refreshTokenRequest, RefreshTokenResponse](ctx, "/customers/api/token/refresh", "", r) - if err != nil { - return RefreshTokenResponse{}, fmt.Errorf("refreshing token: %w", err) - } - a.Token = resp.AccessToken - a.RefreshToken = resp.RefreshToken - err = secrets.Save(a.SecretPrefix, a.Key, APIClass(), a.Token, a.RefreshToken, a.ExpiresIn) - if err != nil { - return RefreshTokenResponse{}, err - } - return resp, nil -} - -func (a API) ListAvailableK8sClusters(ctx context.Context) ([]K8sCluster, error) { - c, err := WithRetry(ctx, a, func() ([]K8sCluster, error) { +func (a *API) ListAvailableK8sClusters(ctx context.Context) ([]K8sCluster, error) { + c, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) ([]K8sCluster, error) { return doGet[[]K8sCluster](ctx, "/kubernetes_clusters/available", a.Token) }) if err != nil { @@ -80,12 +56,12 @@ func (a API) ListAvailableK8sClusters(ctx context.Context) ([]K8sCluster, error) return c, nil } -func (a API) ListCustomClasses(ctx context.Context, cluster string) ([]CustomClass, error) { +func (a *API) ListCustomClasses(ctx context.Context, cluster string) ([]CustomClass, error) { c, err := a.FindCluster(ctx, cluster) if err != nil { return nil, err } - csw, err := WithRetry(ctx, a, func() ([]CustomClass, error) { + csw, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) ([]CustomClass, error) { return doGet[[]CustomClass](ctx, fmt.Sprintf("/cluster/%s/custom_classes", c.ID), a.Token) }) if err != nil { @@ -94,25 +70,28 @@ func (a API) ListCustomClasses(ctx context.Context, cluster string) ([]CustomCla return csw, nil } -func (a API) UploadCustomClasses(ctx context.Context, p func(progress float32), cluster, filePath string) error { +func (a *API) UploadCustomClasses(ctx context.Context, p func(progress float32), cluster, filePath string) error { c, err := a.FindCluster(ctx, cluster) if err != nil { return err } - _, err = WithRetry(ctx, a, func() (any, error) { - err = doCustomClassUpload(ctx, p, fmt.Sprintf("/cluster/%s/custom_classes", c.ID), filePath, a) + _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { + err = doCustomClassUpload(ctx, p, fmt.Sprintf("/cluster/%s/custom_classes", c.ID), filePath, a.Token) if err != nil { return nil, err } return nil, nil }) + if err != nil { + return err + } if err != nil { return fmt.Errorf("uploading custom class: %w", err) } return nil } -func (a API) DownloadCustomClass(ctx context.Context, p func(progress float32), targetInfo TargetInfo, cluster, artifact string) error { +func (a *API) DownloadCustomClass(ctx context.Context, p func(progress float32), targetInfo TargetInfo, cluster, artifact string) error { c, err := a.FindCluster(ctx, cluster) if err != nil { return err @@ -125,8 +104,8 @@ func (a API) DownloadCustomClass(ctx context.Context, p func(progress float32), return fmt.Errorf("no custom class artifact found with name or ID %s in cluster %s", artifact, c.ID) } url := fmt.Sprintf("/cluster/%s/custom_classes/%d", c.ID, artifactID) - _, err = WithRetry(ctx, a, func() (any, error) { - err = doCustomClassDownload(ctx, p, targetInfo, url, artifactName, a) + _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { + err = doCustomClassDownload(ctx, p, targetInfo, url, artifactName, a.Token) if err != nil { return nil, err } @@ -138,7 +117,7 @@ func (a API) DownloadCustomClass(ctx context.Context, p func(progress float32), return nil } -func (a API) DeleteCustomClass(ctx context.Context, cluster string, artifact string) error { +func (a *API) DeleteCustomClass(ctx context.Context, cluster string, artifact string) error { c, err := a.FindCluster(ctx, cluster) if err != nil { return err @@ -150,7 +129,7 @@ func (a API) DeleteCustomClass(ctx context.Context, cluster string, artifact str if artifactID == 0 { return fmt.Errorf("no custom class artifact found with name or ID %s in cluster %s", artifact, c.ID) } - _, err = WithRetry(ctx, a, func() (any, error) { + _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { err = doDelete(ctx, fmt.Sprintf("/cluster/%s/custom_classes/%d", c.ID, artifactID), a.Token) if err != nil { return nil, err @@ -163,22 +142,19 @@ func (a API) DeleteCustomClass(ctx context.Context, cluster string, artifact str return nil } -func (a API) DownloadConfig(ctx context.Context, clusterID string) (path string, stop func(), err error) { +func (a *API) DownloadConfig(ctx context.Context, clusterID string) (path string, stop func(), err error) { url := makeConfigURL(clusterID) - r, err := WithRetry(ctx, a, func() (types.Tuple2[string, func()], error) { + r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (types.Tuple2[string, func()], error) { path, stop, err = download(ctx, url, a.Token) if err != nil { return types.Tuple2[string, func()]{}, err } return types.MakeTuple2(path, stop), nil }) - if err != nil { - return "", nil, err - } return r.First, r.Second, nil } -func (a API) FindCluster(ctx context.Context, idOrName string) (Cluster, error) { +func (a *API) FindCluster(ctx context.Context, idOrName string) (Cluster, error) { clusters, err := a.ListClusters(ctx) if err != nil { return Cluster{}, err @@ -191,7 +167,7 @@ func (a API) FindCluster(ctx context.Context, idOrName string) (Cluster, error) return Cluster{}, fmt.Errorf("no such cluster found: %s", idOrName) } -func (a API) FindClusterType(ctx context.Context, name string) (ClusterType, error) { +func (a *API) FindClusterType(ctx context.Context, name string) (ClusterType, error) { cts, err := a.ListClusterTypes(ctx) if err != nil { return ClusterType{}, err @@ -204,13 +180,13 @@ func (a API) FindClusterType(ctx context.Context, name string) (ClusterType, err return ClusterType{}, fmt.Errorf("no such cluster type found: %s", name) } -func (a API) StreamLogs(ctx context.Context, idOrName string, out io.Writer) error { +func (a *API) StreamLogs(ctx context.Context, idOrName string, out io.Writer) error { c, err := a.FindCluster(ctx, idOrName) if err != nil { return err } path := fmt.Sprintf("/cluster/%s/logstream", c.ID) - r, err := WithRetry(ctx, a, func() (io.ReadCloser, error) { + r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (io.ReadCloser, error) { return doGetRaw(ctx, path, a.Token) }) if err != nil { @@ -221,7 +197,7 @@ func (a API) StreamLogs(ctx context.Context, idOrName string, out io.Writer) err return err } -func (a API) findArtifactIDAndName(ctx context.Context, clusterName, artifact string) (int64, string, error) { +func (a *API) findArtifactIDAndName(ctx context.Context, clusterName, artifact string) (int64, string, error) { customClasses, err := a.ListCustomClasses(ctx, clusterName) if err != nil { return 0, "", err @@ -252,15 +228,18 @@ func makeUrl(path string) string { return APIBaseURL() + path } -func WithRetry[Res any](ctx context.Context, api API, f func() (Res, error)) (Res, error) { - r, err := f() +func RetryOnAuthFail[Res any](ctx context.Context, api *API, f func(ctx context.Context, token string) (Res, error)) (Res, error) { + r, err := f(ctx, api.Token) var e HTTPClientError if errors.As(err, &e) && e.Code() == http.StatusUnauthorized { - _, err = api.RefreshAccessToken(ctx) + *api, err = Login(ctx, api.SecretPrefix, api.Key, api.Secret) if err != nil { return r, err } - r, err = f() + if err = secrets.Save(ctx, APIClass(), api.SecretPrefix, api.Key, api.Secret, api.Token); err != nil { + return r, err + } + r, err = f(ctx, api.Token) if err != nil { return r, err } diff --git a/internal/viridian/cluster.go b/internal/viridian/cluster.go index 0b3a20107..3746682ce 100644 --- a/internal/viridian/cluster.go +++ b/internal/viridian/cluster.go @@ -23,7 +23,7 @@ type createClusterRequest struct { type createClusterResponse Cluster -func (a API) CreateCluster(ctx context.Context, name string, clusterType string, k8sClusterID int, hzVersion string) (Cluster, error) { +func (a *API) CreateCluster(ctx context.Context, name string, clusterType string, k8sClusterID int, hzVersion string) (Cluster, error) { if name == "" { name = clusterName() } @@ -42,7 +42,7 @@ func (a API) CreateCluster(ctx context.Context, name string, clusterType string, ClusterTypeID: clusterTypeID, PlanName: planName, } - cluster, err := WithRetry(ctx, a, func() (Cluster, error) { + cluster, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Cluster, error) { c, err := doPost[createClusterRequest, createClusterResponse](ctx, "/cluster", a.Token, c) return Cluster(c), err }) @@ -63,12 +63,12 @@ func clusterName() string { return fmt.Sprintf("%s-%s-%.4d", base, date, num) } -func (a API) StopCluster(ctx context.Context, idOrName string) error { +func (a *API) StopCluster(ctx context.Context, idOrName string) error { c, err := a.FindCluster(ctx, idOrName) if err != nil { return err } - ok, err := WithRetry(ctx, a, func() (bool, error) { + ok, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (bool, error) { return doPost[[]byte, bool](ctx, fmt.Sprintf("/cluster/%s/stop", c.ID), a.Token, nil) }) if err != nil { @@ -80,8 +80,8 @@ func (a API) StopCluster(ctx context.Context, idOrName string) error { return nil } -func (a API) ListClusters(ctx context.Context) ([]Cluster, error) { - csw, err := WithRetry(ctx, a, func() (Wrapper[[]Cluster], error) { +func (a *API) ListClusters(ctx context.Context) ([]Cluster, error) { + csw, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Wrapper[[]Cluster], error) { return doGet[Wrapper[[]Cluster]](ctx, "/cluster", a.Token) }) if err != nil { @@ -90,12 +90,12 @@ func (a API) ListClusters(ctx context.Context) ([]Cluster, error) { return csw.Content, nil } -func (a API) ResumeCluster(ctx context.Context, idOrName string) error { +func (a *API) ResumeCluster(ctx context.Context, idOrName string) error { c, err := a.FindCluster(ctx, idOrName) if err != nil { return err } - ok, err := WithRetry(ctx, a, func() (bool, error) { + ok, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (bool, error) { return doPost[[]byte, bool](ctx, fmt.Sprintf("/cluster/%s/resume", c.ID), a.Token, nil) }) if err != nil { @@ -107,12 +107,12 @@ func (a API) ResumeCluster(ctx context.Context, idOrName string) error { return nil } -func (a API) DeleteCluster(ctx context.Context, idOrName string) error { +func (a *API) DeleteCluster(ctx context.Context, idOrName string) error { c, err := a.FindCluster(ctx, idOrName) if err != nil { return err } - _, err = WithRetry(ctx, a, func() (any, error) { + _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { err = doDelete(ctx, fmt.Sprintf("/cluster/%s", c.ID), a.Token) if err != nil { return nil, err @@ -125,12 +125,12 @@ func (a API) DeleteCluster(ctx context.Context, idOrName string) error { return nil } -func (a API) GetCluster(ctx context.Context, idOrName string) (Cluster, error) { +func (a *API) GetCluster(ctx context.Context, idOrName string) (Cluster, error) { cluster, err := a.FindCluster(ctx, idOrName) if err != nil { return Cluster{}, err } - c, err := WithRetry(ctx, a, func() (Cluster, error) { + c, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Cluster, error) { return doGet[Cluster](ctx, fmt.Sprintf("/cluster/%s", cluster.ID), a.Token) }) if err != nil { @@ -139,8 +139,8 @@ func (a API) GetCluster(ctx context.Context, idOrName string) (Cluster, error) { return c, nil } -func (a API) ListClusterTypes(ctx context.Context) ([]ClusterType, error) { - csw, err := WithRetry(ctx, a, func() (Wrapper[[]ClusterType], error) { +func (a *API) ListClusterTypes(ctx context.Context) ([]ClusterType, error) { + csw, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Wrapper[[]ClusterType], error) { return doGet[Wrapper[[]ClusterType]](ctx, "/cluster_types", a.Token) }) if err != nil { diff --git a/internal/viridian/cluster_log.go b/internal/viridian/cluster_log.go index 4d6fdd2ec..79d2e0d52 100644 --- a/internal/viridian/cluster_log.go +++ b/internal/viridian/cluster_log.go @@ -12,21 +12,21 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) -func (a API) DownloadClusterLogs(ctx context.Context, destDir string, idOrName string) error { +func (a *API) DownloadClusterLogs(ctx context.Context, destDir string, idOrName string) error { c, err := a.FindCluster(ctx, idOrName) if err != nil { return err } - r, err := WithRetry(ctx, a, func() (types.Tuple2[string, func()], error) { + r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (types.Tuple2[string, func()], error) { u := makeUrl(fmt.Sprintf("/cluster/%s/logs", c.ID)) path, stop, err := download(ctx, makeUrl(u), a.Token) if err != nil { return types.Tuple2[string, func()]{}, err } - return types.Tuple2[string, func()]{path, stop}, nil + return types.MakeTuple2(path, stop), nil }) if err != nil { - return fmt.Errorf("downloading cluster logs: %w", err) + return err } defer r.Second() zipFile, err := os.Open(r.First) diff --git a/internal/viridian/custom_class_download.go b/internal/viridian/custom_class_download.go index 789e93e76..50185989d 100644 --- a/internal/viridian/custom_class_download.go +++ b/internal/viridian/custom_class_download.go @@ -29,7 +29,7 @@ func (pp *DownloadProgressPrinter) Print() { pp.SetterFunc(p) } -func doCustomClassDownload(ctx context.Context, progressSetter func(progress float32), t TargetInfo, url, className string, api API) error { +func doCustomClassDownload(ctx context.Context, progressSetter func(progress float32), t TargetInfo, url, className, token string) error { fn, err := t.fileToBeCreated(className) if err != nil { return err @@ -44,8 +44,8 @@ func doCustomClassDownload(ctx context.Context, progressSetter func(progress flo return fmt.Errorf("creating request: %w", err) } req.Header.Set("Content-Type", "application/json") - if api.Token != "" { - req.Header.Set("Authorization", "Bearer "+api.Token) + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) } req = req.WithContext(ctx) res, err := http.DefaultClient.Do(req) diff --git a/internal/viridian/custom_class_upload.go b/internal/viridian/custom_class_upload.go index b4a74e8b7..211a65dd0 100644 --- a/internal/viridian/custom_class_upload.go +++ b/internal/viridian/custom_class_upload.go @@ -36,7 +36,7 @@ func (pr *UploadProgressReader) Print() { } } -func doCustomClassUpload(ctx context.Context, progressSetter func(progress float32), url, path string, api API) error { +func doCustomClassUpload(ctx context.Context, progressSetter func(progress float32), url, path, token string) error { reqBody := &bytes.Buffer{} w := multipart.NewWriter(reqBody) p, err := w.CreateFormFile("customClassesFile", filepath.Base(path)) @@ -58,8 +58,8 @@ func doCustomClassUpload(ctx context.Context, progressSetter func(progress float if err != nil { return err } - if api.Token != "" { - req.Header.Set("Authorization", "Bearer "+api.Token) + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) } req.Header.Set("Content-Type", w.FormDataContentType()) req = req.WithContext(ctx) diff --git a/internal/viridian/login.go b/internal/viridian/login.go index 200ced6a8..46a0b5b6b 100644 --- a/internal/viridian/login.go +++ b/internal/viridian/login.go @@ -11,12 +11,10 @@ type loginRequest struct { } type loginResponse struct { - Token string `json:"token"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` + Token string `json:"token"` } -func Login(ctx context.Context, key, secret string) (API, error) { +func Login(ctx context.Context, secretPrefix, key, secret string) (API, error) { var api API if key == "" { return api, errors.New("api key cannot be blank") @@ -32,8 +30,9 @@ func Login(ctx context.Context, key, secret string) (API, error) { if err != nil { return api, err } + api.Key = key + api.Secret = secret api.Token = resp.Token - api.ExpiresIn = resp.ExpiresIn - api.RefreshToken = resp.RefreshToken + api.SecretPrefix = secretPrefix return api, nil } From bf74f32b81ca682835d63e1da9ca24a0402c8ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 3 Aug 2023 17:51:22 +0300 Subject: [PATCH 06/79] Updated project creator (#320) Updated project creator --- base/commands/project/const.go | 1 + base/commands/project/project_create.go | 75 ++++---- .../project/project_create_it_test.go | 60 ++---- .../project/testdata/fixture/simple/.dotfile2 | 1 + .../project/testdata/fixture/simple/README.md | 3 + .../testdata/fixture/simple/dir1/another.file | 1 + .../project/testdata/fixture/simple/my.py | 7 + .../testdata/home/templates/simple/.dotfile1 | 1 + .../home/templates/simple/.dotfile2.keep | 1 + .../testdata/home/templates/simple/README.md | 3 + .../home/templates/simple/defaults.yaml | 2 + .../home/templates/simple/dir1/another.file | 1 + .../home/templates/simple/my.py.template | 7 + .../simple-streaming-pipeline/Makefile | 4 - .../simple-streaming-pipeline/README.md | 32 ---- .../simple-streaming-pipeline/build.gradle | 29 --- .../simple-streaming-pipeline/settings.gradle | 2 - .../main/java/com/example/SimplePipeline.java | 37 ---- base/commands/project/utils.go | 43 +++-- docs/modules/ROOT/pages/clc-project.adoc | 47 +++-- internal/str/camel.go | 84 --------- internal/str/camel_test.go | 173 ------------------ 22 files changed, 137 insertions(+), 477 deletions(-) create mode 100644 base/commands/project/testdata/fixture/simple/.dotfile2 create mode 100644 base/commands/project/testdata/fixture/simple/README.md create mode 100644 base/commands/project/testdata/fixture/simple/dir1/another.file create mode 100644 base/commands/project/testdata/fixture/simple/my.py create mode 100644 base/commands/project/testdata/home/templates/simple/.dotfile1 create mode 100644 base/commands/project/testdata/home/templates/simple/.dotfile2.keep create mode 100644 base/commands/project/testdata/home/templates/simple/README.md create mode 100644 base/commands/project/testdata/home/templates/simple/defaults.yaml create mode 100644 base/commands/project/testdata/home/templates/simple/dir1/another.file create mode 100644 base/commands/project/testdata/home/templates/simple/my.py.template delete mode 100644 base/commands/project/testdata/simple-streaming-pipeline/Makefile delete mode 100644 base/commands/project/testdata/simple-streaming-pipeline/README.md delete mode 100644 base/commands/project/testdata/simple-streaming-pipeline/build.gradle delete mode 100644 base/commands/project/testdata/simple-streaming-pipeline/settings.gradle delete mode 100644 base/commands/project/testdata/simple-streaming-pipeline/src/main/java/com/example/SimplePipeline.java delete mode 100644 internal/str/camel.go delete mode 100644 internal/str/camel_test.go diff --git a/base/commands/project/const.go b/base/commands/project/const.go index 8c7460c54..02a4994f0 100644 --- a/base/commands/project/const.go +++ b/base/commands/project/const.go @@ -10,4 +10,5 @@ const ( hzTemplatesOrganization = "https://github.com/hazelcast-templates" defaultsFileName = "defaults.yaml" envTemplateSource = "CLC_EXPERIMENTAL_TEMPLATE_SOURCE" + flagOutputDir = "output-dir" ) diff --git a/base/commands/project/project_create.go b/base/commands/project/project_create.go index b993344f4..e41297ec5 100644 --- a/base/commands/project/project_create.go +++ b/base/commands/project/project_create.go @@ -9,25 +9,28 @@ import ( "math" "os" "path/filepath" - "reflect" "regexp" "strings" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/mk" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -var regexpValidKey = regexp.MustCompile(`^[[:alnum:]]+$`) +var regexpValidKey = regexp.MustCompile(`^[a-z0-9_]+$`) type CreateCmd struct{} func (pc CreateCmd) Init(cc plug.InitContext) error { - cc.SetPositionalArgCount(2, math.MaxInt) - cc.SetCommandUsage("create [template-name] [output-dir] [placeholder-values] [flags]") - short := "(Beta) Create project from the given template" - long := fmt.Sprintf(`(Beta) Create project from the given template. + cc.SetPositionalArgCount(1, math.MaxInt) + cc.SetCommandUsage("create [template-name] [placeholder-values] [flags]") + cc.AddStringFlag(flagOutputDir, "o", "", false, "the directory to create the project at") + short := "Create project from the given template (BETA)" + long := fmt.Sprintf(`Create project from the given template. + +This command is in BETA, it may change in future versions. Templates are located in %s. You can override it by using CLC_EXPERIMENTAL_TEMPLATE_SOURCE environment variable. @@ -44,39 +47,39 @@ Rules while creating your own templates: Properties are read from the following resources in order: - 1. defaults.yaml (keys cannot contain punctuation) + 1. defaults.yaml (keys should be in lowercase letters, digits or underscore) 2. config.yaml - 3. User passed key-values in the "KEY=VALUE" format. The keys can only contain letters and numbers. + 3. User passed key-values in the "KEY=VALUE" format. The keys can only contain lowercase letters, digits or underscore. You can use the placeholders in "defaults.yaml" and the following configuration item placeholders: - * ClusterName - * ClusterAddress - * ClusterUser - * ClusterPassword - * ClusterDiscoveryToken - * SslEnabled - * SslServer - * SslSkipVerify - * SslCaPath - * SslKeyPath - * SslKeyPassword - * LogPath - * LogLevel + * cluster_name + * cluster_address + * cluster_user + * cluster_password + * cluster_discovery_token + * ssl_enabled + * ssl_server + * ssl_skip_verify + * ssl_ca_path + * ssl_key_path + * ssl_key_password + * log_path + * log_level Example (Linux and MacOS): $ clc project create \ simple-streaming-pipeline\ - my-project\ - MyKey1=MyValue1 MyKey2=MyValue2 + --output-dir my-project\ + my_key1=my_value1 my_key2=my_value2 Example (Windows): > clc project create^ simple-streaming-pipeline^ - my-project^ - MyKey1=MyValue1 MyKey2=MyValue2 + --output-dir my-project^ + my_key1=my_value1 my_key2=my_value2 `, hzTemplatesOrganization) cc.SetCommandHelp(long, short) return nil @@ -84,7 +87,10 @@ Example (Windows): func (pc CreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { templateName := ec.Args()[0] - outputDir := ec.Args()[1] + outputDir := ec.Props().GetString(flagOutputDir) + if outputDir == "" { + outputDir = templateName + } templatesDir := paths.Templates() templateExists := paths.Exists(filepath.Join(templatesDir, templateName)) if !templateExists { @@ -110,12 +116,12 @@ func (pc CreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { func createProject(ec plug.ExecContext, outputDir, templateName string) error { sourceDir := paths.ResolveTemplatePath(templateName) - vars, err := loadVars(ec, sourceDir) + vs, err := loadValues(ec, sourceDir) if err != nil { return err } ec.Logger().Debug(func() string { - return fmt.Sprintf("available placeholders: %+v", reflect.ValueOf(vars).MapKeys()) + return fmt.Sprintf("available placeholders: %+v", mk.KeysOf(vs)) }) err = filepath.WalkDir(sourceDir, func(path string, entry fs.DirEntry, err error) error { if err != nil { @@ -137,7 +143,7 @@ func createProject(ec plug.ExecContext, outputDir, templateName string) error { return nil } if hasTemplateExt(entry) { - err = applyTemplateAndCopyToTarget(vars, path, target) + err = applyTemplateAndCopyToTarget(vs, path, target) if err != nil { return err } @@ -158,17 +164,16 @@ func createProject(ec plug.ExecContext, outputDir, templateName string) error { return nil } -func loadVars(ec plug.ExecContext, sourceDir string) (map[string]string, error) { - vars, err := loadFromDefaults(sourceDir) +func loadValues(ec plug.ExecContext, sourceDir string) (map[string]string, error) { + vs, err := loadFromDefaults(sourceDir) if err != nil { return nil, err } - loadFromProps(ec, vars) - err = updatePropsWithUserInput(ec, vars) - if err != nil { + loadFromProps(ec, vs) + if err = updatePropsWithUserValues(ec, vs); err != nil { return nil, err } - return vars, nil + return vs, nil } func isSkip(d fs.DirEntry) bool { diff --git a/base/commands/project/project_create_it_test.go b/base/commands/project/project_create_it_test.go index c444a080f..0d0cd44d6 100644 --- a/base/commands/project/project_create_it_test.go +++ b/base/commands/project/project_create_it_test.go @@ -10,53 +10,33 @@ import ( "path/filepath" "testing" + _ "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/it" - "github.com/hazelcast/hazelcast-commandline-client/internal/it/skip" ) func TestCreateCommand(t *testing.T) { - // We are skipping this test on Windows, because git-go does not allow to configure core.autocrlf option - skip.If(t, "os = windows") - os.Setenv(envTemplateSource, "https://github.com/kutluhanmetin") - testCases := []struct { - inputTemplateName string - inputOutputDir string - inputArgs []string - testProjectDir string - }{ - { - inputTemplateName: "simple-streaming-pipeline-template", - inputOutputDir: "my-simple-streaming-pipeline", - inputArgs: []string{"rootProjectName=simple-streaming-pipeline"}, - testProjectDir: "testdata/simple-streaming-pipeline", - }, - } - for _, tc := range testCases { - t.Run(tc.inputTemplateName, func(t *testing.T) { - tcx := it.TestContext{T: t} - tcx.Tester(func(tcx it.TestContext) { - // create project in temp dir - // we cannot initialize it in test case because CLC_HOME is set to a temp dir in tcx.Tester func - tc.inputOutputDir = filepath.Join(paths.Home(), tc.inputOutputDir) - defer teardown(tc.inputOutputDir) - ctx := context.Background() - tcx.WithReset(func() { - cmd := []string{"project", "create", tc.inputTemplateName, tc.inputOutputDir} - cmd = append(cmd, tc.inputArgs...) - check.Must(tcx.CLC().Execute(ctx, cmd...)) - }) - tcx.WithReset(func() { - check.Must(compareDirectories(tc.inputOutputDir, tc.testProjectDir)) - }) - }) + // 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)) }) - } -} - -func teardown(dir string) { - os.RemoveAll(dir) + }) } func compareDirectories(dir1, dir2 string) error { diff --git a/base/commands/project/testdata/fixture/simple/.dotfile2 b/base/commands/project/testdata/fixture/simple/.dotfile2 new file mode 100644 index 000000000..7b4d68d70 --- /dev/null +++ b/base/commands/project/testdata/fixture/simple/.dotfile2 @@ -0,0 +1 @@ +empty \ No newline at end of file diff --git a/base/commands/project/testdata/fixture/simple/README.md b/base/commands/project/testdata/fixture/simple/README.md new file mode 100644 index 000000000..53c1211ff --- /dev/null +++ b/base/commands/project/testdata/fixture/simple/README.md @@ -0,0 +1,3 @@ +# My Template + +Just some static file \ No newline at end of file diff --git a/base/commands/project/testdata/fixture/simple/dir1/another.file b/base/commands/project/testdata/fixture/simple/dir1/another.file new file mode 100644 index 000000000..e06fbc5e0 --- /dev/null +++ b/base/commands/project/testdata/fixture/simple/dir1/another.file @@ -0,0 +1 @@ +Yet another file \ No newline at end of file diff --git a/base/commands/project/testdata/fixture/simple/my.py b/base/commands/project/testdata/fixture/simple/my.py new file mode 100644 index 000000000..96a6f9a79 --- /dev/null +++ b/base/commands/project/testdata/fixture/simple/my.py @@ -0,0 +1,7 @@ +def main(): + print("key1: bar") + print("another_key: foo") + print("cluster address: localhost:10000") + +if __name__ == "__main__": + main() diff --git a/base/commands/project/testdata/home/templates/simple/.dotfile1 b/base/commands/project/testdata/home/templates/simple/.dotfile1 new file mode 100644 index 000000000..c6cac6926 --- /dev/null +++ b/base/commands/project/testdata/home/templates/simple/.dotfile1 @@ -0,0 +1 @@ +empty diff --git a/base/commands/project/testdata/home/templates/simple/.dotfile2.keep b/base/commands/project/testdata/home/templates/simple/.dotfile2.keep new file mode 100644 index 000000000..7b4d68d70 --- /dev/null +++ b/base/commands/project/testdata/home/templates/simple/.dotfile2.keep @@ -0,0 +1 @@ +empty \ No newline at end of file diff --git a/base/commands/project/testdata/home/templates/simple/README.md b/base/commands/project/testdata/home/templates/simple/README.md new file mode 100644 index 000000000..53c1211ff --- /dev/null +++ b/base/commands/project/testdata/home/templates/simple/README.md @@ -0,0 +1,3 @@ +# My Template + +Just some static file \ No newline at end of file diff --git a/base/commands/project/testdata/home/templates/simple/defaults.yaml b/base/commands/project/testdata/home/templates/simple/defaults.yaml new file mode 100644 index 000000000..f5402e3de --- /dev/null +++ b/base/commands/project/testdata/home/templates/simple/defaults.yaml @@ -0,0 +1,2 @@ +key1: value1 +another_key: another_value \ No newline at end of file diff --git a/base/commands/project/testdata/home/templates/simple/dir1/another.file b/base/commands/project/testdata/home/templates/simple/dir1/another.file new file mode 100644 index 000000000..e06fbc5e0 --- /dev/null +++ b/base/commands/project/testdata/home/templates/simple/dir1/another.file @@ -0,0 +1 @@ +Yet another file \ No newline at end of file diff --git a/base/commands/project/testdata/home/templates/simple/my.py.template b/base/commands/project/testdata/home/templates/simple/my.py.template new file mode 100644 index 000000000..8afcf872b --- /dev/null +++ b/base/commands/project/testdata/home/templates/simple/my.py.template @@ -0,0 +1,7 @@ +def main(): + print("key1: {{ .key1 }}") + print("another_key: {{ .another_key }}") + print("cluster address: {{ .cluster_address }}") + +if __name__ == "__main__": + main() diff --git a/base/commands/project/testdata/simple-streaming-pipeline/Makefile b/base/commands/project/testdata/simple-streaming-pipeline/Makefile deleted file mode 100644 index 8fc2f95d7..000000000 --- a/base/commands/project/testdata/simple-streaming-pipeline/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -.PHONY: build - -build: - ./gradlew shadowJar diff --git a/base/commands/project/testdata/simple-streaming-pipeline/README.md b/base/commands/project/testdata/simple-streaming-pipeline/README.md deleted file mode 100644 index 768837177..000000000 --- a/base/commands/project/testdata/simple-streaming-pipeline/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Simple Streaming Pipeline - -Requirements: - -1. JRE 8 -2. Gradle 8 - -## Usage - -### Compile the project - -``` -gradle shadowJar -``` - -### Submit the Jet Job - -``` -clc job submit ./build/libs/simple-streaming-pipeline-1.0-SNAPSHOT-all.jar -``` - -### Observe the Map Updates - -``` -clc map -n my-map entry-set -``` - -### Clean Up - -``` -clc job cancel ./build/libs/simple-streaming-pipeline-1.0-SNAPSHOT-all.jar -``` \ No newline at end of file diff --git a/base/commands/project/testdata/simple-streaming-pipeline/build.gradle b/base/commands/project/testdata/simple-streaming-pipeline/build.gradle deleted file mode 100644 index bdde224a4..000000000 --- a/base/commands/project/testdata/simple-streaming-pipeline/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -plugins { - id 'java' - id 'com.github.johnrengelman.shadow' version '7.1.2' -} - -group = 'org.example' -version = '1.0-SNAPSHOT' - -repositories { - mavenCentral() -} - -dependencies { - compileOnly 'com.hazelcast:hazelcast:5.3.0-BETA-2' - implementation 'org.hashids:hashids:1.0.3' - testImplementation platform('org.junit:junit-bom:5.9.1') - testImplementation 'org.junit.jupiter:junit-jupiter' -} - -test { - useJUnitPlatform() -} - -jar.manifest.attributes 'Main-Class': 'com.example.SimplePipeline' - -compileJava { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' -} diff --git a/base/commands/project/testdata/simple-streaming-pipeline/settings.gradle b/base/commands/project/testdata/simple-streaming-pipeline/settings.gradle deleted file mode 100644 index e4941e670..000000000 --- a/base/commands/project/testdata/simple-streaming-pipeline/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'simple-streaming-pipeline' - diff --git a/base/commands/project/testdata/simple-streaming-pipeline/src/main/java/com/example/SimplePipeline.java b/base/commands/project/testdata/simple-streaming-pipeline/src/main/java/com/example/SimplePipeline.java deleted file mode 100644 index cbc867a30..000000000 --- a/base/commands/project/testdata/simple-streaming-pipeline/src/main/java/com/example/SimplePipeline.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example; - -import com.hazelcast.core.Hazelcast; -import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.jet.config.JetConfig; -import com.hazelcast.jet.config.JobConfig; -import com.hazelcast.jet.pipeline.Pipeline; -import com.hazelcast.jet.pipeline.Sinks; -import com.hazelcast.jet.pipeline.test.TestSources; -import org.hashids.Hashids; - -import java.util.AbstractMap; - -public class SimplePipeline { - public static void main(String[] args) { - final String mapName = (args.length > 0)? args[0] : "my-map"; - // Create a pipeline that creates one item per second - // and writes it to a map with the name given as an argument or my-map. - Pipeline pipeline = Pipeline.create(); - pipeline.readFrom(TestSources.itemStream(1)) - .withoutTimestamps() - .map(e -> new AbstractMap.SimpleEntry<>( - e.sequence(), - new Hashids().encode(e.sequence())) - ) - .writeTo(Sinks.map(mapName, - // Map key is the sequence number - AbstractMap.SimpleEntry::getKey, - // Map value is the hash ID of the sequence - AbstractMap.SimpleEntry::getValue)); - HazelcastInstance hz = Hazelcast.bootstrappedInstance(); - JobConfig config = new JobConfig(); - // optionally set the name of the job - config.setName("simple-pipeline"); - hz.getJet().newJob(pipeline, config); - } -} diff --git a/base/commands/project/utils.go b/base/commands/project/utils.go index 164f52f91..565d4d7e0 100644 --- a/base/commands/project/utils.go +++ b/base/commands/project/utils.go @@ -31,22 +31,13 @@ func loadFromDefaults(templateDir string) (map[string]string, error) { if err = parseYAML("", b, props); err != nil { return nil, err } - props = camelizeMapKeys(props) - if err != nil { - return nil, err + if key, ok := validateValueMap(props); !ok { + return nil, fmt.Errorf("invalid property: %s (keys can only contain lowercase letters, numbers or underscore", key) } return props, nil } -func camelizeMapKeys(m map[string]string) map[string]string { - r := make(map[string]string) - for k, v := range m { - r[str.ToCamel(k)] = v - } - return r -} - -func updatePropsWithUserInput(ec plug.ExecContext, props map[string]string) error { +func updatePropsWithUserValues(ec plug.ExecContext, props map[string]string) error { for _, arg := range ec.Args() { k, v := str.ParseKeyValue(arg) if k == "" { @@ -65,20 +56,38 @@ func updatePropsWithUserInput(ec plug.ExecContext, props map[string]string) erro func loadFromProps(ec plug.ExecContext, p map[string]string) { m := ec.Props().All() - m = maybeCamelizeMapKeys(m) + m = convertMapKeyToSnakeCase(m) for k, v := range m { - p[k] = fmt.Sprintf("%v", v) + p[k] = fmt.Sprint(v) } } -func maybeCamelizeMapKeys(m map[string]any) map[string]any { - r := make(map[string]any) +func convertMapKeyToSnakeCase[T any](m map[string]T) map[string]T { + r := make(map[string]T, len(m)) for k, v := range m { - r[str.ToCamel(k)] = v + r[convertToSnakeCase(k)] = v } return r } +func convertToSnakeCase(s string) string { + // this function assumes s contains only letters, numbers, dot and dash + // this is not an efficient implementation + // but sufficient for our needs. + s = strings.ToLower(s) + s = strings.ReplaceAll(s, ".", "_") + return strings.ReplaceAll(s, "-", "_") +} + +func validateValueMap[T any](m map[string]T) (invalid string, ok bool) { + for k, _ := range m { + if !regexpValidKey.MatchString(k) { + return k, false + } + } + return "", true +} + func parseYAML(prefix string, yamlFile []byte, result map[string]string) error { var parsedData map[string]any err := yaml.Unmarshal(yamlFile, &parsedData) diff --git a/docs/modules/ROOT/pages/clc-project.adoc b/docs/modules/ROOT/pages/clc-project.adoc index 7aaf2edc4..8d1bd4363 100644 --- a/docs/modules/ROOT/pages/clc-project.adoc +++ b/docs/modules/ROOT/pages/clc-project.adoc @@ -21,7 +21,7 @@ Usage: [source,bash] ---- -clc project create [template-name] [output-dir] [placeholder-values] [flags] +clc project create [template-name] [placeholder-values] [flags] ---- Parameters: @@ -30,10 +30,10 @@ Parameters: |=== |Parameter|Required|Description|Default -|`output-dir` -|Required +|`--output-dir`, `-o` +|Optional |Output directory for the project to be created. -| +|Template name |`template-name` |Required @@ -42,9 +42,8 @@ Parameters: |`placeholder-values` |Optional -|Template placeholder values can be specified as key-value pairs. You can use letters and numbers in keys. Example: `key`=`value` +|Template placeholder values can be specified as key-value pairs. You can use lowercase letters, numbers and the underscore character in keys. Example: `my_key1`=`value` -WARNING: keys cannot contain punctuation | |=== @@ -65,23 +64,23 @@ Templates are located in https://github.com/hazelcast-templates. You can overrid 1. `defaults.yaml` (keys cannot contain punctuation) 2. `config.yaml` -3. User passed key-values in the "KEY=VALUE" format. The keys can only contain letters and numbers. +3. User passed key-values in the "KEY=VALUE" format. The keys can only contain lowercase letters, numbers and the underscore character. You can use the placeholders in "defaults.yaml" and the following configuration item placeholders: -* ClusterName -* ClusterAddress -* ClusterUser -* ClusterPassword -* ClusterDiscoveryToken -* SslEnabled -* SslServer -* SslSkipVerify -* SslCaPath -* SslKeyPath -* SslKeyPassword -* LogPath -* LogLevel +* cluster_name +* cluster_address +* cluster_user +* cluster_password +* cluster_discovery_token +* ssl_enabled +* ssl_server +* ssl_skip_verify +* ssl_ca_path +* ssl_key_path +* ssl_key_password +* log_path +* log_level Example (Linux and MacOS): @@ -89,8 +88,8 @@ Example (Linux and MacOS): ---- clc project create \ simple-streaming-pipeline\ -my-project\ -MyKey1=MyValue1 MyKey2=MyValue2 +--output-dir my-project\ +my_key1=my_value1 my_key2=my_value2 ---- Example (Windows): @@ -99,6 +98,6 @@ Example (Windows): ---- clc project create^ simple-streaming-pipeline^ -my-project^ -MyKey1=MyValue1 MyKey2=MyValue2 +--output-dir my-project^ +my_key1=my_value1 my_key2=my_value2 ---- \ No newline at end of file diff --git a/internal/str/camel.go b/internal/str/camel.go deleted file mode 100644 index 3458bffd2..000000000 --- a/internal/str/camel.go +++ /dev/null @@ -1,84 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 Ian Coleman - * Copyright (c) 2018 Ma_124, - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, Subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or Substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package str - -import ( - "strings" -) - -// Converts a string to CamelCase -func toCamelInitCase(s string, initCase bool) string { - s = strings.TrimSpace(s) - if s == "" { - return s - } - if a, ok := uppercaseAcronym[s]; ok { - s = a - } - - n := strings.Builder{} - n.Grow(len(s)) - capNext := initCase - for i, v := range []byte(s) { - vIsCap := v >= 'A' && v <= 'Z' - vIsLow := v >= 'a' && v <= 'z' - if capNext { - if vIsLow { - v += 'A' - v -= 'a' - } - } else if i == 0 { - if vIsCap { - v += 'a' - v -= 'A' - } - } - if vIsCap || vIsLow { - n.WriteByte(v) - capNext = false - } else if vIsNum := v >= '0' && v <= '9'; vIsNum { - n.WriteByte(v) - capNext = true - } else { - capNext = v == '_' || v == ' ' || v == '-' || v == '.' - } - } - return n.String() -} - -// ToCamel converts a string to CamelCase -func ToCamel(s string) string { - return toCamelInitCase(s, true) -} - -// ToLowerCamel converts a string to lowerCamelCase -func ToLowerCamel(s string) string { - return toCamelInitCase(s, false) -} - -var uppercaseAcronym = map[string]string{ - "ID": "id", -} diff --git a/internal/str/camel_test.go b/internal/str/camel_test.go deleted file mode 100644 index 962465db2..000000000 --- a/internal/str/camel_test.go +++ /dev/null @@ -1,173 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 Ian Coleman - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, Subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or Substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package str - -import ( - "testing" -) - -func toCamel(tb testing.TB) { - cases := [][]string{ - {"test_case", "TestCase"}, - {"test.case", "TestCase"}, - {"test", "Test"}, - {"TestCase", "TestCase"}, - {" test case ", "TestCase"}, - {"", ""}, - {"many_many_words", "ManyManyWords"}, - {"AnyKind of_string", "AnyKindOfString"}, - {"odd-fix", "OddFix"}, - {"numbers2And55with000", "Numbers2And55With000"}, - {"ID", "Id"}, - } - for _, i := range cases { - in := i[0] - out := i[1] - result := ToCamel(in) - if result != out { - tb.Errorf("%q (%q != %q)", in, result, out) - } - } -} - -func TestToCamel(t *testing.T) { - toCamel(t) -} - -func BenchmarkToCamel(b *testing.B) { - benchmarkCamelTest(b, toCamel) -} - -func toLowerCamel(tb testing.TB) { - cases := [][]string{ - {"foo-bar", "fooBar"}, - {"TestCase", "testCase"}, - {"", ""}, - {"AnyKind of_string", "anyKindOfString"}, - {"AnyKind.of-string", "anyKindOfString"}, - {"ID", "id"}, - {"some string", "someString"}, - {" some string", "someString"}, - } - for _, i := range cases { - in := i[0] - out := i[1] - result := ToLowerCamel(in) - if result != out { - tb.Errorf("%q (%q != %q)", in, result, out) - } - } -} - -func TestToLowerCamel(t *testing.T) { - toLowerCamel(t) -} - -func TestCustomAcronymsToCamel(t *testing.T) { - tests := []struct { - name string - acronymKey string - acronymValue string - expected string - }{ - { - name: "API Custom Acronym", - acronymKey: "API", - acronymValue: "api", - expected: "Api", - }, - { - name: "ABCDACME Custom Acroynm", - acronymKey: "ABCDACME", - acronymValue: "AbcdAcme", - expected: "AbcdAcme", - }, - { - name: "PostgreSQL Custom Acronym", - acronymKey: "PostgreSQL", - acronymValue: "PostgreSQL", - expected: "PostgreSQL", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ConfigureAcronym(test.acronymKey, test.acronymValue) - if result := ToCamel(test.acronymKey); result != test.expected { - t.Errorf("expected custom acronym result %s, got %s", test.expected, result) - } - }) - } -} - -func TestCustomAcronymsToLowerCamel(t *testing.T) { - tests := []struct { - name string - acronymKey string - acronymValue string - expected string - }{ - { - name: "API Custom Acronym", - acronymKey: "API", - acronymValue: "api", - expected: "api", - }, - { - name: "ABCDACME Custom Acroynm", - acronymKey: "ABCDACME", - acronymValue: "AbcdAcme", - expected: "abcdAcme", - }, - { - name: "PostgreSQL Custom Acronym", - acronymKey: "PostgreSQL", - acronymValue: "PostgreSQL", - expected: "postgreSQL", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ConfigureAcronym(test.acronymKey, test.acronymValue) - if result := ToLowerCamel(test.acronymKey); result != test.expected { - t.Errorf("expected custom acronym result %s, got %s", test.expected, result) - } - }) - } -} - -func BenchmarkToLowerCamel(b *testing.B) { - benchmarkCamelTest(b, toLowerCamel) -} - -func benchmarkCamelTest(b *testing.B, fn func(testing.TB)) { - for n := 0; n < b.N; n++ { - fn(b) - } -} - -// ConfigureAcronym allows you to add additional words which will be considered acronyms -func ConfigureAcronym(key, val string) { - uppercaseAcronym[key] = val -} From c347012e9e8d15e00d542e8ff756683e71f2b09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 3 Aug 2023 18:57:07 +0300 Subject: [PATCH 07/79] Fix arg0 (#321) --- clc/cmd/clc.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index e6e11cc84..2140ae0e9 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -58,6 +58,7 @@ type Main struct { props *plug.Properties cc *CommandContext cp config.Provider + arg0 string } func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLevel string, sio clc.IO) (*Main, error) { @@ -79,6 +80,7 @@ func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLeve stdin: sio.Stdin, props: plug.NewProperties(), cp: cfgProvider, + arg0: arg0, } if logPath == "" { logPath = cfgProvider.GetString(clc.PropertyLogPath) @@ -152,7 +154,7 @@ func (m *Main) Execute(ctx context.Context, args ...string) error { if err != nil { return err } - if cm.Use == "clc" { + if cm.Use == m.arg0 { // check whether help or completion is requested useShell := true for i, arg := range cmdArgs { From 9d720d91557edfb2324e19178d3d15f880660a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 3 Aug 2023 19:22:47 +0300 Subject: [PATCH 08/79] Updates (#322) --- docs/antora.yml | 8 ++++---- extras/windows/installer/hazelcast-clc-installer.iss | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 9acb04bad..b1b2ca053 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -2,21 +2,21 @@ name: clc title: Hazelcast CLC start_page: overview.adoc # Version in the URL -version: '5.3.2-snapshot' +version: '5.3.3-snapshot' # Version in the version selector (we display only the latest major.minor version) -display_version: '5.3.2-SNAPSHOT' +display_version: '5.3.3-SNAPSHOT' # Displays a banner to inform users that this is a prerelease version prerelease: true asciidoc: attributes: # The full major.minor.patch version, which is used as a variable in the docs for things like download links - full-version: '5.3.2-SNAPSHOT' + full-version: '5.3.3-SNAPSHOT' # Allows us to use UI macros. See https://docs.asciidoctor.org/asciidoc/latest/macros/ui-macros/ experimental: true snapshot: true page-toclevels: 3@ # Required Go version for build go-version: 1.19 - page-latest-supported-mc: '5.3.1-snapshot' + page-latest-supported-mc: '5.3.2-snapshot' nav: - modules/ROOT/nav.adoc \ No newline at end of file diff --git a/extras/windows/installer/hazelcast-clc-installer.iss b/extras/windows/installer/hazelcast-clc-installer.iss index df4d53c2e..b2eaffbb2 100644 --- a/extras/windows/installer/hazelcast-clc-installer.iss +++ b/extras/windows/installer/hazelcast-clc-installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Hazelcast CLC" -#define MyAppVersion "{#GetEnv('CLC_VERSION')}" +#define MyAppVersion "v5.3.3-SNAPSHOT}" #define MyAppPublisher "Hazelcast, Inc." #define MyAppURL "https://www.hazelcast.com/" #define MyAppExeName "clc.exe" From 5d2cf7f6d1844e2c44d5757b3b9f8d4a354e4b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 3 Aug 2023 19:55:39 +0300 Subject: [PATCH 09/79] Added the release notes for v5.3.2 (#323) --- docs/modules/ROOT/nav.adoc | 1 + docs/modules/ROOT/pages/release-notes-5.3.2.adoc | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/modules/ROOT/pages/release-notes-5.3.2.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 3abf4619f..54ed355d1 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -37,6 +37,7 @@ * xref:keyboard-shortcuts.adoc[] .Release Notes +* xref:release-notes-5.3.2.adoc[5.3.2] * xref:release-notes-5.3.1.adoc[5.3.1] * xref:release-notes-5.3.0.adoc[5.3.0] * xref:release-notes-5.3.0-BETA-2.adoc[5.3.0-BETA-2] diff --git a/docs/modules/ROOT/pages/release-notes-5.3.2.adoc b/docs/modules/ROOT/pages/release-notes-5.3.2.adoc new file mode 100644 index 000000000..dbc48d90b --- /dev/null +++ b/docs/modules/ROOT/pages/release-notes-5.3.2.adoc @@ -0,0 +1,14 @@ += 5.3.2 Release Notes + +== New Features + +* Added the `project create` command that can create a Hazelcast project from a template. Note that this features is currently BETA. +* Added the `script` command that runs an SQL or CLC script from a local file or HTTP resource. +* Added the following data structure commands: `atomic-long`, `list`, `multi-map`, `queue` and `topic`. +* Added more `map` commands: `values`, `load-all`, `lock`, `try-lock` and `unlock`. Note that lock related commands are only available in the interactive mode. +* Added the `CLC_CONFIG` environment variable which sets the default CLC configuration. If unset, the default is `default`. + +== Fixes + +* link:https://github.com/hazelcast/hazelcast-commandline-client/pull/290[#290] Refactored `job` commands to use coordinator for SQL jobs. +* link:https://github.com/hazelcast/hazelcast-commandline-client/pull/288[#288] Fix streaming timeout. From 4e20ca8277887fe547e322832ff93a46a9a01ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 4 Aug 2023 13:15:52 +0300 Subject: [PATCH 10/79] Fixed Windows files (#324) --- extras/windows/installer/hazelcast-clc-installer.iss | 2 +- make.cmd | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/extras/windows/installer/hazelcast-clc-installer.iss b/extras/windows/installer/hazelcast-clc-installer.iss index b2eaffbb2..2e0a6b283 100644 --- a/extras/windows/installer/hazelcast-clc-installer.iss +++ b/extras/windows/installer/hazelcast-clc-installer.iss @@ -1,5 +1,5 @@ #define MyAppName "Hazelcast CLC" -#define MyAppVersion "v5.3.3-SNAPSHOT}" +#define MyAppVersion "v5.3.3-SNAPSHOT" #define MyAppPublisher "Hazelcast, Inc." #define MyAppURL "https://www.hazelcast.com/" #define MyAppExeName "clc.exe" diff --git a/make.cmd b/make.cmd index 7cb85612c..566409293 100644 --- a/make.cmd +++ b/make.cmd @@ -4,9 +4,9 @@ setlocal FOR /F "tokens=* USEBACKQ" %%F IN (`git rev-list --tags --max-count=1`) DO ( set GIT_COMMIT=%%F ) -if not defined CLC_VERSION set CLC_VERSION=UNKNOWN +if not defined CLC_VERSION set CLC_VERSION=v0.0.0-CUSTOMBUILD echo CLC_VERSION: %CLC_VERSION% -set ldflags="-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%'" +set ldflags="-s -w -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%'" REM default target is build if "%1" == "" ( @@ -25,9 +25,8 @@ if errorlevel 1 ( goto :end :build - go-winres make --in windows/winres.json --product-version=%CLC_VERSION% --file-version=%CLC_VERSION% go-winres make --in extras/windows/winres.json --product-version=%CLC_VERSION% --file-version=%CLC_VERSION% --out cmd\clc\rsrc - go build -tags base,hazelcastinternal,hazelcastinternaltest -ldflags %ldflags% -o build\clc.exe ./cmd/clc + go build -tags base,std,hazelcastinternal,hazelcastinternaltest -ldflags %ldflags% -o build\clc.exe ./cmd/clc if errorlevel 1 ( echo Build failed set target_failed=1 @@ -46,7 +45,7 @@ goto :end goto :end :test - go test -tags base,hazelcastinternal,hazelcastinternaltest -p 1 -v -count 1 ./... + go test -tags base,std,hazelcastinternal,hazelcastinternaltest -p 1 -v -count 1 ./... if errorlevel 1 ( echo Test failed set target_failed=1 From 2082f958ec38fa20954e0415895bfa69fcbde9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 4 Aug 2023 15:50:53 +0300 Subject: [PATCH 11/79] Fix loading access token when the API key is given explicitly (#325) Fixes getting the access token path when --api-key or CLC_VIRIDIAN_API_KEY env var is used --- base/commands/viridian/common.go | 8 ++++---- base/commands/viridian/common_test.go | 8 ++++---- clc/secrets/secrets.go | 6 ------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 5e76d205b..3a1f7d094 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -29,19 +29,19 @@ var ( ErrLoadingSecrets = errors.New("could not load Viridian secrets, did you login?") ) -func findToken(apiKey string) (string, error) { +func findTokenPath(apiKey string) (string, error) { ac := viridian.APIClass() if apiKey == "" { apiKey = os.Getenv(viridian.EnvAPIKey) } if apiKey != "" { - return fmt.Sprintf("%s-%s", ac, apiKey), nil + return fmt.Sprintf(secrets.TokenFileFormat, ac, apiKey), nil } tokenPaths, err := findAll(secretPrefix) if err != nil { return "", fmt.Errorf("cannot access the secrets, did you login?: %w", err) } - // sort tokens, so findToken returns the same token everytime. + // sort tokens, so findTokenPath returns the same token everytime. sort.Slice(tokenPaths, func(i, j int) bool { return tokenPaths[i] < tokenPaths[j] }) @@ -75,7 +75,7 @@ func findKeyAndSecret(tokenPath string) (string, string, error) { } func getAPI(ec plug.ExecContext) (*viridian.API, error) { - tp, err := findToken(ec.Props().GetString(propAPIKey)) + tp, err := findTokenPath(ec.Props().GetString(propAPIKey)) if err != nil { return nil, err } diff --git a/base/commands/viridian/common_test.go b/base/commands/viridian/common_test.go index 26f8f92f5..b581cb33b 100644 --- a/base/commands/viridian/common_test.go +++ b/base/commands/viridian/common_test.go @@ -22,19 +22,19 @@ func TestFindToken(t *testing.T) { it.WithEnv(paths.EnvCLCHome, home.Path(), func() { it.WithEnv(viridian.EnvAPIKey, "", func() { // should return an error if there are no secrets - _, err := findToken("") + _, err := findTokenPath("") require.Error(t, err) // fixture check.Must(secrets.Write(prefix, "api-APIKEY1.access", []byte("token-APIKEY1"))) check.Must(secrets.Write(prefix, "api-APIKEY2.access", []byte("token-APIKEY2"))) check.Must(secrets.Write(prefix, "cls-CLSKEY1.access", []byte("token-CLSKEY1"))) // check the token filename for the first API key is returned if the API key was not specified - require.Equal(t, "api-APIKEY1.access", check.MustValue(findToken(""))) + require.Equal(t, "api-APIKEY1.access", check.MustValue(findTokenPath(""))) // check the token filename for the given API key is returned - require.Equal(t, "api-APIKEY2.access", check.MustValue(findToken("APIKEY2.access"))) + require.Equal(t, "api-APIKEY2.access", check.MustValue(findTokenPath("APIKEY2"))) // check the token filename for the given API class is returned it.WithEnv(viridian.EnvAPI, "cls", func() { - require.Equal(t, "cls-CLSKEY1.access", check.MustValue(findToken("CLSKEY1.access"))) + require.Equal(t, "cls-CLSKEY1.access", check.MustValue(findTokenPath("CLSKEY1"))) }) }) }) diff --git a/clc/secrets/secrets.go b/clc/secrets/secrets.go index fb0ce1462..2b7d2e01e 100644 --- a/clc/secrets/secrets.go +++ b/clc/secrets/secrets.go @@ -59,9 +59,3 @@ func Read(prefix, name string) ([]byte, error) { } return b[:n], nil } - -func FindAll(prefix string) ([]string, error) { - return paths.FindAll(paths.Join(paths.Secrets(), prefix), func(basePath string, entry os.DirEntry) (ok bool) { - return !entry.IsDir() && filepath.Ext(entry.Name()) == filepath.Ext(TokenFileFormat) - }) -} From dc8435b03ec4004e47e00e44674d08a20dbd3335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 10 Aug 2023 00:28:25 +0300 Subject: [PATCH 12/79] Removed the global client (#315) Removed the global client --- clc/cmd/clc.go | 50 ++++++++++++++++-------------------- clc/cmd/exec_context.go | 12 ++++----- internal/it/expect/expect.go | 5 +--- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index 2140ae0e9..b1279ee6d 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "strings" + "sync" "sync/atomic" "github.com/hazelcast/hazelcast-go-client" @@ -27,24 +28,8 @@ import ( var ( MainCommandShortHelp = "Hazelcast CLC" - // client is currently global in order to have a single client. - // This is bad. - // TODO: make the client unique without making it global. - clientInternal atomic.Pointer[hazelcast.ClientInternal] ) -func getClientInternal() *hazelcast.ClientInternal { - return clientInternal.Load() -} - -func setClientInternal(ci *hazelcast.ClientInternal) { - clientInternal.Store(ci) -} - -func ServerVersionOf(ci *hazelcast.ClientInternal) string { - return ci.ConnectionManager().RandomConnection().ServerVersion() -} - type Main struct { root *cobra.Command cmds map[string]*cobra.Command @@ -59,6 +44,8 @@ type Main struct { cc *CommandContext cp config.Provider arg0 string + ciMu *sync.Mutex + ci *atomic.Pointer[hazelcast.ClientInternal] } func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLevel string, sio clc.IO) (*Main, error) { @@ -81,6 +68,8 @@ func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLeve props: plug.NewProperties(), cp: cfgProvider, arg0: arg0, + ciMu: &sync.Mutex{}, + ci: &atomic.Pointer[hazelcast.ClientInternal]{}, } if logPath == "" { logPath = cfgProvider.GetString(clc.PropertyLogPath) @@ -327,12 +316,7 @@ func (m *Main) createCommands() error { Stderr: m.stderr, Stdout: m.stdout, } - ec, err := NewExecContext(m.lg, sio, m.props, func(ctx context.Context, cfg hazelcast.Config) (*hazelcast.ClientInternal, error) { - if err := m.ensureClient(ctx, cfg); err != nil { - return nil, err - } - return clientInternal.Load(), nil - }, m.isInteractive) + ec, err := NewExecContext(m.lg, sio, m.props, m.isInteractive) if err != nil { return err } @@ -389,13 +373,19 @@ func (m *Main) createCommands() error { } func (m *Main) ensureClient(ctx context.Context, cfg hazelcast.Config) error { - if getClientInternal() == nil { - client, err := hazelcast.StartNewClientWithConfig(ctx, cfg) - if err != nil { - return err - } - setClientInternal(hazelcast.NewClientInternal(client)) + if m.ci.Load() != nil { + return nil + } + m.ciMu.Lock() + defer m.ciMu.Unlock() + if m.ci.Load() != nil { + return nil + } + c, err := hazelcast.StartNewClientWithConfig(ctx, cfg) + if err != nil { + return err } + m.ci.Store(hazelcast.NewClientInternal(c)) return nil } @@ -410,6 +400,10 @@ func (m *Main) setConfigProps(props *plug.Properties, key string, value any) { } } +func (m *Main) clientInternal() *hazelcast.ClientInternal { + return m.ci.Load() +} + func convertFlagValue(fs *pflag.FlagSet, name string, v pflag.Value) any { switch v.Type() { case "string": diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index ab60b7609..528fece6c 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -38,7 +38,6 @@ type ExecContext struct { stdin io.Reader args []string props *plug.Properties - clientFn ClientFn isInteractive bool cmd *cobra.Command main *Main @@ -47,14 +46,13 @@ type ExecContext struct { cp config.Provider } -func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, clientFn ClientFn, interactive bool) (*ExecContext, error) { +func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, interactive bool) (*ExecContext, error) { return &ExecContext{ lg: lg, stdout: sio.Stdout, stderr: sio.Stderr, stdin: sio.Stdin, props: props, - clientFn: clientFn, isInteractive: interactive, spinnerWait: 1 * time.Second, }, nil @@ -105,7 +103,7 @@ func (ec *ExecContext) Props() plug.ReadOnlyProperties { } func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInternal, error) { - ci := getClientInternal() + ci := ec.main.clientInternal() if ci != nil { return ci, nil } @@ -115,14 +113,16 @@ func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInt } civ, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Connecting to the cluster") - return ec.clientFn(ctx, cfg) + if err := ec.main.ensureClient(ctx, cfg); err != nil { + return nil, err + } + return ec.main.clientInternal(), nil }) if err != nil { return nil, err } stop() ci = civ.(*hazelcast.ClientInternal) - setClientInternal(ci) verbose := ec.Props().GetBool(clc.PropertyVerbose) if verbose || ec.Interactive() { cn := ci.ClusterService().FailoverService().Current().ClusterName diff --git a/internal/it/expect/expect.go b/internal/it/expect/expect.go index 0168ac3c4..b28f07a8f 100644 --- a/internal/it/expect/expect.go +++ b/internal/it/expect/expect.go @@ -73,10 +73,7 @@ func (e *Expect) Match(m Matcher, options ...Option) bool { var done atomic.Bool go func() { for !done.Load() { - e.mu.RLock() - ok := m.Match(e.String()) - e.mu.RUnlock() - if ok { + if m.Match(e.String()) { ch <- struct{}{} return } From 1e1c873f3f007009774d6152c085aefaa8a22e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 10 Aug 2023 11:27:15 +0300 Subject: [PATCH 13/79] Fix test race (#331) Fixed a test race --- .github/workflows/test-all-386.yaml | 2 +- Makefile | 2 +- base/commands/shell_it_test.go | 6 +----- base/commands/topic/topic_it_test.go | 18 ++++++++++++++++-- make.cmd | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-all-386.yaml b/.github/workflows/test-all-386.yaml index 7c6b93ff1..19e940936 100644 --- a/.github/workflows/test-all-386.yaml +++ b/.github/workflows/test-all-386.yaml @@ -57,4 +57,4 @@ jobs: - name: "Run All Tests" run: | - GOARCH=386 make test + GOARCH=386 make test TEST_FLAGS="-v -count 1 -timeout 30m" diff --git a/Makefile b/Makefile index 715dc65c3..7e3dc2345 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 50m +TEST_FLAGS ?= -v -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/shell_it_test.go b/base/commands/shell_it_test.go index 157e91bfc..9ca3c1680 100644 --- a/base/commands/shell_it_test.go +++ b/base/commands/shell_it_test.go @@ -9,7 +9,6 @@ import ( _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/object" "github.com/hazelcast/hazelcast-commandline-client/internal/it" - "github.com/hazelcast/hazelcast-commandline-client/internal/it/skip" ) func TestShell(t *testing.T) { @@ -61,10 +60,7 @@ func shellErrorsTest(t *testing.T) { } func shellNoDoubleErrorTest(t *testing.T) { - // this test times out on Windows on CI, - // but passes on Windows on local. - // so skipping for now... --YT - skip.If(t, "os = windows") + it.MarkFlaky(t, "https://github.com/hazelcast/hazelcast-commandline-client/issues/332") tcx := it.TestContext{T: t} tcx.Tester(func(tcx it.TestContext) { ctx := context.Background() diff --git a/base/commands/topic/topic_it_test.go b/base/commands/topic/topic_it_test.go index d777de87a..5fa06c84c 100644 --- a/base/commands/topic/topic_it_test.go +++ b/base/commands/topic/topic_it_test.go @@ -4,6 +4,7 @@ package topic_test import ( "context" + "sync" "testing" "time" @@ -39,14 +40,27 @@ func publish_NonInteractiveTest(t *testing.T) { ctx := context.Background() tcx.WithReset(func() { var values []string + valuesMu := &sync.Mutex{} sid := check.MustValue(tp.AddMessageListener(ctx, func(event *hz.MessagePublished) { + valuesMu.Lock() values = append(values, event.Value.(string)) + valuesMu.Unlock() })) defer func() { check.Must(tp.RemoveListener(ctx, sid)) }() check.Must(tcx.CLC().Execute(ctx, "topic", "-n", tp.Name(), "publish", "value1")) check.Must(tcx.CLC().Execute(ctx, "topic", "-n", tp.Name(), "publish", "value2")) - require.Eventually(t, func() bool { return slices.Contains(values, "value1") }, 5*time.Second, 100*time.Millisecond) - require.Eventually(t, func() bool { return slices.Contains(values, "value2") }, 5*time.Second, 100*time.Millisecond) + require.Eventually(t, func() bool { + valuesMu.Lock() + ok := slices.Contains(values, "value1") + valuesMu.Unlock() + return ok + }, 5*time.Second, 100*time.Millisecond) + require.Eventually(t, func() bool { + valuesMu.Lock() + ok := slices.Contains(values, "value2") + valuesMu.Unlock() + return ok + }, 5*time.Second, 100*time.Millisecond) }) }) } diff --git a/make.cmd b/make.cmd index 566409293..5cfbf025c 100644 --- a/make.cmd +++ b/make.cmd @@ -45,7 +45,7 @@ goto :end goto :end :test - go test -tags base,std,hazelcastinternal,hazelcastinternaltest -p 1 -v -count 1 ./... + go test -tags base,std,hazelcastinternal,hazelcastinternaltest -p 1 -v -count 1 -timeout 30m -race ./... if errorlevel 1 ( echo Test failed set target_failed=1 From 84a264ce61c6460f623ad8312197905ba3119525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 10 Aug 2023 11:27:36 +0300 Subject: [PATCH 14/79] Fix shell default output (#333) Fixed arg0 dependent default output format in the shell mode --- base/commands/shell_it_test.go | 22 +++++++++++++++++++ base/commands/sql/sql.go | 10 +++++++-- .../testdata/default_output_format.txt | 5 +++++ clc/cmd/clc.go | 4 ++++ clc/cmd/exec_context.go | 4 ++++ internal/it/test_context.go | 2 +- 6 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 base/commands/testdata/default_output_format.txt diff --git a/base/commands/shell_it_test.go b/base/commands/shell_it_test.go index 9ca3c1680..d69aff6e0 100644 --- a/base/commands/shell_it_test.go +++ b/base/commands/shell_it_test.go @@ -8,6 +8,8 @@ import ( "testing" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/object" + _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/sql" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/it" ) @@ -16,6 +18,7 @@ func TestShell(t *testing.T) { name string f func(t *testing.T) }{ + {name: "DefaultOutputFormat", f: shellDefaultOutputFormatTest}, {name: "ShellErrors", f: shellErrorsTest}, {name: "ShellNoDoubleError", f: shellNoDoubleErrorTest}, {name: "ShellHelp", f: shellHelpTest}, @@ -87,3 +90,22 @@ func shellHelpTest(t *testing.T) { }) }) } + +func shellDefaultOutputFormatTest(t *testing.T) { + tcx := it.TestContext{T: t} + tcx.Tester(func(tcx it.TestContext) { + ctx := context.Background() + it.WithEnv(clc.EnvMaxCols, "16", func() { + tcx.WithShell(ctx, func(tcx it.TestContext) { + tcx.WithReset(func() { + tcx.WriteStdinString("create mapping t(__key varchar, this varchar) type imap options ('keyFormat' = 'varchar', 'valueFormat' = 'varchar');\n") + tcx.WriteStdinString("\\map -n t set foo bar\n") + }) + tcx.WithReset(func() { + tcx.WriteStdinString("select * from t;\n") + tcx.AssertStdoutDollarWithPath("testdata/default_output_format.txt") + }) + }) + }) + }) +} diff --git a/base/commands/sql/sql.go b/base/commands/sql/sql.go index b96510b93..8181530ea 100644 --- a/base/commands/sql/sql.go +++ b/base/commands/sql/sql.go @@ -21,12 +21,18 @@ const ( minServerVersion = "5.0.0" ) +type arg0er interface { + Arg0() string +} + type SQLCommand struct{} func (cm *SQLCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { // set the default format to table in the interactive mode - if ec.CommandName() == "clc shell" && len(ec.Args()) == 0 { - props.Set(clc.PropertyFormat, base.PrinterTable) + if ecc, ok := ec.(arg0er); ok { + if ec.CommandName() == ecc.Arg0()+" shell" && len(ec.Args()) == 0 { + props.Set(clc.PropertyFormat, base.PrinterTable) + } } return nil } diff --git a/base/commands/testdata/default_output_format.txt b/base/commands/testdata/default_output_format.txt new file mode 100644 index 000000000..6a58ff9ee --- /dev/null +++ b/base/commands/testdata/default_output_format.txt @@ -0,0 +1,5 @@ +$---------------$ +$ __key | this $ +$---------------$ +$ foo | bar $ +$---------------$ \ No newline at end of file diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index b1279ee6d..210f64d23 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -194,6 +194,10 @@ func (m *Main) Exit() error { return nil } +func (m *Main) Arg0() string { + return m.arg0 +} + func (m *Main) createLogger(path, level string) error { weight, err := logger.WeightForLevel(level) if err != nil { diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index 528fece6c..35e173370 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -98,6 +98,10 @@ func (ec *ExecContext) Args() []string { return ec.args } +func (ec *ExecContext) Arg0() string { + return ec.main.Arg0() +} + func (ec *ExecContext) Props() plug.ReadOnlyProperties { return ec.props } diff --git a/internal/it/test_context.go b/internal/it/test_context.go index 6acc46406..60ed570ff 100644 --- a/internal/it/test_context.go +++ b/internal/it/test_context.go @@ -320,7 +320,7 @@ func (tcx TestContext) createMain() (*cmd.Main, error) { if err != nil { panic(err) } - return cmd.NewMain("clc", tcx.ConfigPath, fp, tcx.LogPath, tcx.LogLevel, tcx.IO()) + return cmd.NewMain("clctest", tcx.ConfigPath, fp, tcx.LogPath, tcx.LogLevel, tcx.IO()) } func WithEnv(name, value string, f func()) { From 0a64579e64964914478c8467d5d00a165c2d6db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 10 Aug 2023 16:31:37 +0300 Subject: [PATCH 15/79] API Base URL config (#330) * Added cluster.api-base config * Added support for api-base for Viridian commands * viridian get-cluster reorg --- base/commands/project/file.go | 5 +- .../project/project_create_it_test.go | 2 +- base/commands/viridian/common.go | 30 ++++--- base/commands/viridian/const.go | 1 + .../commands/viridian/viridian_cluster_get.go | 43 +++++----- base/commands/viridian/viridian_it_test.go | 7 +- base/commands/viridian/viridian_login.go | 34 ++++++-- clc/config/config.go | 8 +- clc/config/config_test.go | 2 + clc/config/import.go | 29 ++++--- clc/const.go | 1 + clc/paths/paths.go | 5 +- clc/secrets/secrets.go | 14 +--- go.mod | 2 +- go.sum | 4 +- internal/it/test_context.go | 21 ++--- internal/viridian/api.go | 78 ++++++++++++------- internal/viridian/cluster.go | 28 +++++-- internal/viridian/cluster_log.go | 4 +- internal/viridian/custom_class_download.go | 2 +- internal/viridian/custom_class_upload.go | 3 +- internal/viridian/login.go | 29 +++---- 22 files changed, 207 insertions(+), 145 deletions(-) diff --git a/base/commands/project/file.go b/base/commands/project/file.go index fd7785eed..07f0ef35e 100644 --- a/base/commands/project/file.go +++ b/base/commands/project/file.go @@ -11,7 +11,8 @@ import ( ) func applyTemplateAndCopyToTarget(vars map[string]string, source, dest string) error { - f, err := os.Create(paths.SplitExt(dest)) + base, _ := paths.SplitExt(dest) + f, err := os.Create(base) if err != nil { return err } @@ -33,7 +34,7 @@ func copyToTarget(source, dest string, removeExt bool) error { } defer sf.Close() if removeExt { - dest = paths.SplitExt(dest) + dest, _ = paths.SplitExt(dest) } df, err := os.Create(dest) if err != nil { diff --git a/base/commands/project/project_create_it_test.go b/base/commands/project/project_create_it_test.go index 0d0cd44d6..850237ccd 100644 --- a/base/commands/project/project_create_it_test.go +++ b/base/commands/project/project_create_it_test.go @@ -67,7 +67,7 @@ func getDirectoryHashes(dir string) (map[string][16]byte, error) { return err } if filepath.Ext(relativePath) == keepExt || filepath.Ext(relativePath) == templateExt { - relativePath = paths.SplitExt(relativePath) + relativePath, _ = paths.SplitExt(relativePath) } hashes[relativePath] = fileHash } diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 3a1f7d094..0f5204076 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -35,7 +35,7 @@ func findTokenPath(apiKey string) (string, error) { apiKey = os.Getenv(viridian.EnvAPIKey) } if apiKey != "" { - return fmt.Sprintf(secrets.TokenFileFormat, ac, apiKey), nil + return fmt.Sprintf(viridian.FmtTokenFileName, ac, apiKey), nil } tokenPaths, err := findAll(secretPrefix) if err != nil { @@ -60,18 +60,25 @@ func findTokenPath(apiKey string) (string, error) { func findAll(prefix string) ([]string, error) { return paths.FindAll(paths.Join(paths.Secrets(), prefix), func(basePath string, entry os.DirEntry) (ok bool) { - return !entry.IsDir() && filepath.Ext(entry.Name()) == filepath.Ext(secrets.TokenFileFormat) + return !entry.IsDir() && filepath.Ext(entry.Name()) == filepath.Ext(viridian.FmtTokenFileName) }) } -func findKeyAndSecret(tokenPath string) (string, string, error) { - apiKey := strings.TrimPrefix(strings.TrimSuffix(tokenPath, filepath.Ext(tokenPath)), fmt.Sprintf("%s-", viridian.APIClass())) - fn := fmt.Sprintf(secrets.SecretFileFormat, viridian.APIClass(), apiKey) - secret, err := secrets.Read(secretPrefix, fn) +func findKeyAndSecret(tokenPath string) (key, secret, apiBase string, err error) { + key, _ = paths.SplitExt(tokenPath) + key = strings.TrimPrefix(key, viridian.APIClass()+"-") + fn := fmt.Sprintf(fmtSecretFileName, viridian.APIClass(), key) + b, err := secrets.Read(secretPrefix, fn) if err != nil { - return "", "", err + return "", "", "", err } - return apiKey, string(secret), nil + ss := string(b) + // secret and API base + ls := strings.SplitN(ss, "\n", 2) + if len(ls) == 1 { + return key, ls[0], "", nil + } + return key, ls[0], ls[1], nil } func getAPI(ec plug.ExecContext) (*viridian.API, error) { @@ -85,12 +92,15 @@ func getAPI(ec plug.ExecContext) (*viridian.API, error) { ec.Logger().Error(err) return nil, ErrLoadingSecrets } - key, secret, err := findKeyAndSecret(tp) + key, secret, base, err := findKeyAndSecret(tp) if err != nil { ec.Logger().Error(err) return nil, ErrLoadingSecrets } - return viridian.NewAPI(secretPrefix, key, secret, string(token)), nil + if base == "" { + base = viridian.APIBaseURL() + } + return viridian.NewAPI(secretPrefix, key, secret, string(token), base), nil } func waitClusterState(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterIDOrName, state string) error { diff --git a/base/commands/viridian/const.go b/base/commands/viridian/const.go index 57d9fa31c..7372f8510 100644 --- a/base/commands/viridian/const.go +++ b/base/commands/viridian/const.go @@ -7,4 +7,5 @@ const ( flagClusterType = "cluster-type" flagOutputDir = "output-dir" flagHazelcastVersion = "hazelcast-version" + fmtSecretFileName = "%s-%s.secret" ) diff --git a/base/commands/viridian/viridian_cluster_get.go b/base/commands/viridian/viridian_cluster_get.go index 908476e0d..18e6e2f6e 100644 --- a/base/commands/viridian/viridian_cluster_get.go +++ b/base/commands/viridian/viridian_cluster_get.go @@ -69,39 +69,40 @@ func (cm ClusterGetCmd) Exec(ctx context.Context, ec plug.ExecContext) error { Type: serialization.TypeString, Value: c.HazelcastVersion, }, - output.Column{ - Name: "Hot Backup Enabled", - Type: serialization.TypeString, - Value: boolToYesNo(c.HotBackupEnabled), - }, - output.Column{ - Name: "Hot Restart Enabled", - Type: serialization.TypeString, - Value: boolToYesNo(c.HotRestartEnabled), - }, - output.Column{ - Name: "IP Whitelist Enabled", - Type: serialization.TypeString, - Value: boolToYesNo(c.IPWhitelistEnabled), - }, - output.Column{ - Name: "Creation Time", - Type: serialization.TypeJavaLocalDateTime, - Value: time.UnixMilli(c.CreationTime), - }, } if ec.Props().GetBool(clc.PropertyVerbose) { row = append(row, + output.Column{ + Name: "Creation Time", + Type: serialization.TypeJavaLocalDateTime, + Value: time.UnixMilli(c.CreationTime), + }, output.Column{ Name: "Start Time", Type: serialization.TypeJavaLocalDateTime, Value: time.UnixMilli(c.StartTime), }, + output.Column{ + Name: "Hot Backup Enabled", + Type: serialization.TypeString, + Value: boolToYesNo(c.HotBackupEnabled), + }, + output.Column{ + Name: "Hot Restart Enabled", + Type: serialization.TypeString, + Value: boolToYesNo(c.HotRestartEnabled), + }, + output.Column{ + Name: "IP Whitelist Enabled", + Type: serialization.TypeString, + Value: boolToYesNo(c.IPWhitelistEnabled), + }, output.Column{ Name: "Regions", Type: serialization.TypeStringArray, Value: regionTitleSlice(c.Regions), - }) + }, + ) } return ec.AddOutputRows(ctx, row) } diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index a22cb7f91..07e2cddf8 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/require" - _ "github.com/hazelcast/hazelcast-commandline-client/base/commands" + _ "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/it" @@ -143,7 +143,6 @@ func createCluster_NonInteractiveTest(t *testing.T) { ensureNoClusterRunning(ctx, tcx) clusterName := it.UniqueClusterName() tcx.CLCExecute(ctx, "viridian", "create-cluster", "--verbose", "--name", clusterName) - //fields := tcx.AssertStdoutHasRowWithFields("ID", "Name") cs := check.MustValue(tcx.Viridian.ListClusters(ctx)) cid := cs[0].ID tcx.AssertStdoutDollar(fmt.Sprintf("$%s$%s$", cid, clusterName)) @@ -218,9 +217,9 @@ func resumeCluster_InteractiveTest(t *testing.T) { func getCluster_NonInteractiveTest(t *testing.T) { viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "") - tcx.CLCExecute(ctx, "viridian", "get-cluster", c.ID, "--verbose") + tcx.CLCExecute(ctx, "viridian", "get-cluster", c.ID, "--verbose", "-f", "json") tcx.AssertStderrContains("OK") - fields := tcx.AssertStdoutHasRowWithFields("ID", "Name", "State", "Version") + fields := tcx.AssertJSONStdoutHasRowWithFields("ID", "Name", "State", "Hazelcast Version", "Creation Time", "Start Time", "Hot Backup Enabled", "Hot Restart Enabled", "IP Whitelist Enabled", "Regions") require.Equal(t, c.ID, fields["ID"]) require.Equal(t, c.Name, fields["Name"]) }) diff --git a/base/commands/viridian/viridian_login.go b/base/commands/viridian/viridian_login.go index f3599f7f4..8b371a81c 100644 --- a/base/commands/viridian/viridian_login.go +++ b/base/commands/viridian/viridian_login.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" @@ -19,6 +20,7 @@ import ( const ( propAPIKey = "api-key" propAPISecret = "api-secret" + propAPIBase = "api-base" secretPrefix = "viridian" ) @@ -37,21 +39,28 @@ Alternatively, you can use the following environment variables: cc.SetCommandHelp(long, short) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(propAPISecret, "", "", false, "Viridian API Secret") + cc.AddStringFlag(propAPIBase, "", "", false, "Viridian API Base") cc.SetPositionalArgCount(0, 0) return nil } func (cm LoginCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - key, secret, err := apiKeySecret(ec) + key, secret, err := getAPIKeySecret(ec) if err != nil { return err } - token, err := cm.retrieveToken(ctx, ec, key, secret) + ab := getAPIBase(ec) + token, err := cm.retrieveToken(ctx, ec, key, secret, ab) if err != nil { return err } - - if err = secrets.Save(ctx, viridian.APIClass(), secretPrefix, key, secret, token); err != nil { + secret += "\n" + ab + sk := fmt.Sprintf(fmtSecretFileName, viridian.APIClass(), key) + if err = secrets.Save(ctx, secretPrefix, sk, secret); err != nil { + return err + } + tk := fmt.Sprintf(viridian.FmtTokenFileName, viridian.APIClass(), key) + if err = secrets.Save(ctx, secretPrefix, tk, token); err != nil { return err } ec.PrintlnUnnecessary("") @@ -59,10 +68,10 @@ func (cm LoginCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return nil } -func (cm LoginCmd) retrieveToken(ctx context.Context, ec plug.ExecContext, key, secret string) (string, error) { +func (cm LoginCmd) retrieveToken(ctx context.Context, ec plug.ExecContext, key, secret, apiBase string) (string, error) { ti, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Logging in") - api, err := viridian.Login(ctx, secretPrefix, key, secret) + api, err := viridian.Login(ctx, secretPrefix, key, secret, apiBase) if err != nil { return nil, err } @@ -75,7 +84,18 @@ func (cm LoginCmd) retrieveToken(ctx context.Context, ec plug.ExecContext, key, return ti.(string), nil } -func apiKeySecret(ec plug.ExecContext) (key, secret string, err error) { +func getAPIBase(ec plug.ExecContext) string { + ab := ec.Props().GetString(propAPIBase) + if ab == "" { + return viridian.APIBaseURL() + } + if strings.HasPrefix(ab, "https://") || strings.HasPrefix(ab, "http://") { + return ab + } + return fmt.Sprintf("https://api.%s.viridian.hazelcast.cloud", ab) +} + +func getAPIKeySecret(ec plug.ExecContext) (key, secret string, err error) { pr := prompt.New(ec.Stdin(), ec.Stdout()) key = ec.Props().GetString(propAPIKey) if key == "" { diff --git a/clc/config/config.go b/clc/config/config.go index 7d30c3a2f..f1af444d0 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -18,7 +18,6 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/str" - "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) const ( @@ -112,11 +111,10 @@ func MakeHzConfig(props plug.ReadOnlyProperties, lg log.Logger) (hazelcast.Confi } } } - apiBase := props.GetString(clc.PropertyExperimentalAPIBase) + apiBase := props.GetString(clc.PropertyClusterAPIBase) if apiBase != "" { - if err := os.Setenv(viridian.EnvAPIBaseURL, apiBase); err != nil { - lg.Error(fmt.Errorf("setting environment variable: %s: %w", viridian.EnvAPIBaseURL, err)) - } + lg.Debugf("Viridan API Base: %s", apiBase) + cfg.Cluster.Cloud.ExperimentalAPIBaseURL = apiBase } cfg.Labels = makeClientLabels() cfg.ClientName = makeClientName() diff --git a/clc/config/config_test.go b/clc/config/config_test.go index b0fe73437..1a30a5e5f 100644 --- a/clc/config/config_test.go +++ b/clc/config/config_test.go @@ -43,6 +43,7 @@ func TestMakeConfiguration_Viridian(t *testing.T) { props := plug.NewProperties() props.Set(clc.PropertyClusterDiscoveryToken, "TOKEN") props.Set(clc.PropertyClusterName, "pr-3066") + props.Set(clc.PropertyClusterAPIBase, "https://api.dev2.viridian.cloud") /* // TODO: need to figure out how to specify these config options --YT props.Set(clc.PropertySSLCertPath, "my-cert.pem") @@ -63,6 +64,7 @@ func TestMakeConfiguration_Viridian(t *testing.T) { target.Cluster.Name = "pr-3066" target.Cluster.Cloud.Enabled = true target.Cluster.Cloud.Token = "TOKEN" + target.Cluster.Cloud.ExperimentalAPIBaseURL = "https://api.dev2.viridian.cloud" target.Cluster.Network.SSL.Enabled = true target.Cluster.Network.SSL.SetTLSConfig(&tls.Config{ServerName: "hazelcast.cloud"}) target.Stats.Enabled = true diff --git a/clc/config/import.go b/clc/config/import.go index bca9298fa..3af2072ff 100644 --- a/clc/config/import.go +++ b/clc/config/import.go @@ -133,12 +133,12 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string return nil, err } defer reader.Close() - var goPaths []string + var pyPaths []string var pemFiles []*zip.File // find .py and .pem paths for _, rf := range reader.File { if strings.HasSuffix(rf.Name, ".py") { - goPaths = append(goPaths, rf.Name) + pyPaths = append(pyPaths, rf.Name) continue } // copy only pem files @@ -149,11 +149,11 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string } var cfgFound bool // find the configuration bits - token, clusterName, pw, cfgFound := extractConfigFields(reader, goPaths) + token, clusterName, pw, apiBase, cfgFound := extractConfigFields(reader, pyPaths) if !cfgFound { return nil, errors.New("python file with configuration not found") } - opts := makeViridianOpts(clusterName, token, pw) + opts := makeViridianOpts(clusterName, token, pw, apiBase) outDir, cfgPath, err := Create(target, opts) if err != nil { return nil, err @@ -171,10 +171,11 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string return p.(string), nil } -func makeViridianOpts(clusterName, token, password string) clc.KeyValues[string, string] { +func makeViridianOpts(clusterName, token, password, apiBaseURL string) clc.KeyValues[string, string] { return clc.KeyValues[string, string]{ {Key: "cluster.name", Value: clusterName}, {Key: "cluster.discovery-token", Value: token}, + {Key: "cluster.api-base", Value: apiBaseURL}, {Key: "ssl.ca-path", Value: "ca.pem"}, {Key: "ssl.cert-path", Value: "cert.pem"}, {Key: "ssl.key-path", Value: "key.pem"}, @@ -182,7 +183,7 @@ func makeViridianOpts(clusterName, token, password string) clc.KeyValues[string, } } -func extractConfigFields(reader *zip.ReadCloser, pyPaths []string) (token, clusterName, pw string, cfgFound bool) { +func extractConfigFields(reader *zip.ReadCloser, pyPaths []string) (token, clusterName, pw, apiBase string, cfgFound bool) { for _, p := range pyPaths { rc, err := reader.Open(p) if err != nil { @@ -204,6 +205,11 @@ func extractConfigFields(reader *zip.ReadCloser, pyPaths []string) (token, clust } pw = extractKeyPassword(text) // it's OK if password is not found + apiBase = extractClusterAPIBaseURL(text) + if apiBase != "" { + apiBase = "https://" + apiBase + } + // it's OK if apiBase is not found cfgFound = true break } @@ -232,20 +238,25 @@ func copyFiles(ec plug.ExecContext, files []*zip.File, outDir string) error { } func extractClusterName(text string) string { - // extract from config.Cluster.Name = "pr-3814" + // extract from cluster_name="XXXX" const re = `cluster_name="([^"]+)"` return extractSimpleString(re, text) +} +func extractClusterAPIBaseURL(text string) string { + // extract from HazelcastCloudDiscovery._CLOUD_URL_BASE = "XXXX" + const re = `HazelcastCloudDiscovery._CLOUD_URL_BASE\s*=\s*"([^"]+)"` + return extractSimpleString(re, text) } func extractViridianToken(text string) string { - // extract from: config.Cluster.Cloud.Token = "EWEKHVOOQOjMN5mXB8OngRF4YG5aOm6N2LUEOlhdC7SWpY54hm" + // extract from: cloud_discovery_token="XXXX", const re = `cloud_discovery_token="([^"]+)"` return extractSimpleString(re, text) } func extractKeyPassword(text string) string { - // extract from: err = config.Cluster.Network.SSL.AddClientCertAndEncryptedKeyPath(certFile, keyFile, "12ee6ff601a") + // extract from: ssl_password="XXXX", const re = `ssl_password="([^"]+)"` return extractSimpleString(re, text) } diff --git a/clc/const.go b/clc/const.go index 1ad527710..149f74152 100644 --- a/clc/const.go +++ b/clc/const.go @@ -8,6 +8,7 @@ const ( PropertyClusterDiscoveryToken = "cluster.discovery-token" PropertyClusterUser = "cluster.user" PropertyClusterPassword = "cluster.password" + PropertyClusterAPIBase = "cluster.api-base" PropertyFormat = "format" PropertyVerbose = "verbose" PropertyQuiet = "quiet" diff --git a/clc/paths/paths.go b/clc/paths/paths.go index bf9a76b78..2fd0c3de2 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -183,6 +183,7 @@ func nearbyConfigPath() string { return "" } -func SplitExt(dest string) string { - return strings.TrimSuffix(dest, filepath.Ext(dest)) +func SplitExt(dest string) (base, ext string) { + ext = filepath.Ext(dest) + return dest[:len(dest)-len(ext)], ext } diff --git a/clc/secrets/secrets.go b/clc/secrets/secrets.go index 2b7d2e01e..fa9549f37 100644 --- a/clc/secrets/secrets.go +++ b/clc/secrets/secrets.go @@ -10,24 +10,14 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/paths" ) -const ( - TokenFileFormat = "%s-%s.access" - SecretFileFormat = "%s-%s.secret" -) - -func Save(ctx context.Context, apiClass, secretPrefix, key, secret, token string) error { - tokenFile := fmt.Sprintf(TokenFileFormat, apiClass, key) - secretFile := fmt.Sprintf(SecretFileFormat, apiClass, key) +func Save(ctx context.Context, secretPrefix, key, token string) error { if ctx.Err() != nil { return ctx.Err() } if err := os.MkdirAll(paths.Secrets(), 0700); err != nil { return fmt.Errorf("creating secrets directory: %w", err) } - if err := Write(secretPrefix, tokenFile, []byte(token)); err != nil { - return err - } - if err := Write(secretPrefix, secretFile, []byte(secret)); err != nil { + if err := Write(secretPrefix, key, []byte(token)); err != nil { return err } return nil diff --git a/go.mod b/go.mod index b319e4b1e..f7df6434c 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.20230525070313-5912aa215603 + github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230809052932-73bc747e32b9 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 diff --git a/go.sum b/go.sum index 29adaefe2..73d9c185d 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er 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.20230525070313-5912aa215603 h1:JhoQe+QsOt1lIuNXJjAgGAD0xCwfvSDiaT+U8Pwkmz4= -github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230525070313-5912aa215603/go.mod h1:PJ38lqXJ18S0YpkrRznPDlUH8GnnMAQCx3jpQtBPZ6Q= +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/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= diff --git a/internal/it/test_context.go b/internal/it/test_context.go index 60ed570ff..6e02f7c9f 100644 --- a/internal/it/test_context.go +++ b/internal/it/test_context.go @@ -19,6 +19,7 @@ package it import ( "bytes" "context" + "encoding/json" "fmt" "io" "os" @@ -257,18 +258,20 @@ func (tcx TestContext) AssertStdoutDollar(text string) { } } -func (tcx TestContext) AssertStdoutHasRowWithFields(fields ...string) map[string]string { +func (tcx TestContext) AssertJSONStdoutHasRowWithFields(fields ...string) map[string]any { stdout := tcx.ExpectStdout.String() - out := strings.Fields(stdout) - if len(fields) != len(out) { - tcx.T.Log("STDOUT:", stdout) - tcx.T.Fatalf("stdout does not have the same fields as %v", fields) + var m map[string]any + check.Must(json.Unmarshal([]byte(stdout), &m)) + tcx.T.Log("STDOUT:", stdout) + if len(fields) != len(m) { + tcx.T.Fatalf("stdout does not have the same number fields as %v", fields) } - fm := map[string]string{} - for i, f := range fields { - fm[f] = out[i] + for _, k := range fields { + if _, ok := m[k]; !ok { + tcx.T.Fatalf("stdout does not have the same fields as %v", fields) + } } - return fm + return m } func (tcx TestContext) AssertStdoutDollarWithPath(path string) { diff --git a/internal/viridian/api.go b/internal/viridian/api.go index cdd30345b..69499c672 100644 --- a/internal/viridian/api.go +++ b/internal/viridian/api.go @@ -22,6 +22,7 @@ const ( EnvAPISecret = "CLC_VIRIDIAN_API_SECRET" EnvAPI = "CLC_EXPERIMENTAL_VIRIDIAN_API" DefaultAPIBaseURL = "https://api.viridian.hazelcast.com" + FmtTokenFileName = "%s-%s.access" ) type Wrapper[T any] struct { @@ -35,20 +36,24 @@ type API struct { Token string Key string Secret string + APIBaseURL string } -func NewAPI(secretPrefix, key, secret, token string) *API { +func NewAPI(secretPrefix, key, secret, token, apiBase string) *API { + apiBase = strings.TrimRight(apiBase, "/") return &API{ SecretPrefix: secretPrefix, Key: key, Secret: secret, Token: token, + APIBaseURL: apiBase, } } func (a *API) ListAvailableK8sClusters(ctx context.Context) ([]K8sCluster, error) { c, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) ([]K8sCluster, error) { - return doGet[[]K8sCluster](ctx, "/kubernetes_clusters/available", a.Token) + u := a.makeURL("/kubernetes_clusters/available") + return doGet[[]K8sCluster](ctx, u, a.Token) }) if err != nil { return nil, fmt.Errorf("listing available Kubernetes clusters: %w", err) @@ -62,7 +67,8 @@ func (a *API) ListCustomClasses(ctx context.Context, cluster string) ([]CustomCl return nil, err } csw, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) ([]CustomClass, error) { - return doGet[[]CustomClass](ctx, fmt.Sprintf("/cluster/%s/custom_classes", c.ID), a.Token) + u := a.makeURL("/cluster/%s/custom_classes", c.ID) + return doGet[[]CustomClass](ctx, u, a.Token) }) if err != nil { return nil, fmt.Errorf("listing custom classes: %w", err) @@ -76,7 +82,8 @@ func (a *API) UploadCustomClasses(ctx context.Context, p func(progress float32), return err } _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { - err = doCustomClassUpload(ctx, p, fmt.Sprintf("/cluster/%s/custom_classes", c.ID), filePath, a.Token) + u := a.makeURL("/cluster/%s/custom_classes", c.ID) + err = doCustomClassUpload(ctx, p, u, filePath, a.Token) if err != nil { return nil, err } @@ -103,7 +110,7 @@ func (a *API) DownloadCustomClass(ctx context.Context, p func(progress float32), if artifactID == 0 { return fmt.Errorf("no custom class artifact found with name or ID %s in cluster %s", artifact, c.ID) } - url := fmt.Sprintf("/cluster/%s/custom_classes/%d", c.ID, artifactID) + url := a.makeURL("/cluster/%s/custom_classes/%d", c.ID, artifactID) _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { err = doCustomClassDownload(ctx, p, targetInfo, url, artifactName, a.Token) if err != nil { @@ -130,7 +137,8 @@ func (a *API) DeleteCustomClass(ctx context.Context, cluster string, artifact st return fmt.Errorf("no custom class artifact found with name or ID %s in cluster %s", artifact, c.ID) } _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { - err = doDelete(ctx, fmt.Sprintf("/cluster/%s/custom_classes/%d", c.ID, artifactID), a.Token) + u := a.makeURL("/cluster/%s/custom_classes/%d", c.ID, artifactID) + err = doDelete(ctx, u, a.Token) if err != nil { return nil, err } @@ -143,7 +151,7 @@ func (a *API) DeleteCustomClass(ctx context.Context, cluster string, artifact st } func (a *API) DownloadConfig(ctx context.Context, clusterID string) (path string, stop func(), err error) { - url := makeConfigURL(clusterID) + url := makeConfigURL(a.APIBaseURL, clusterID) r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (types.Tuple2[string, func()], error) { path, stop, err = download(ctx, url, a.Token) if err != nil { @@ -185,9 +193,9 @@ func (a *API) StreamLogs(ctx context.Context, idOrName string, out io.Writer) er if err != nil { return err } - path := fmt.Sprintf("/cluster/%s/logstream", c.ID) + url := a.makeURL("/cluster/%s/logstream", c.ID) r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (io.ReadCloser, error) { - return doGetRaw(ctx, path, a.Token) + return doGetRaw(ctx, url, a.Token) }) if err != nil { return err @@ -214,6 +222,26 @@ func (a *API) findArtifactIDAndName(ctx context.Context, clusterName, artifact s return artifactID, artifactName, nil } +func (a *API) login(ctx context.Context) error { + if a.Key == "" { + return errors.New("api key cannot be blank") + } + if a.Secret == "" { + return errors.New("api secret cannot be blank") + } + c := loginRequest{ + APIKey: a.Key, + APISecret: a.Secret, + } + u := a.makeURL("/customers/api/login") + resp, err := doPost[loginRequest, loginResponse](ctx, u, "", c) + if err != nil { + return err + } + a.Token = resp.Token + return nil +} + func APIBaseURL() string { u := os.Getenv(EnvAPIBaseURL) if u != "" { @@ -222,21 +250,15 @@ func APIBaseURL() string { return DefaultAPIBaseURL } -func makeUrl(path string) string { - path = strings.TrimLeft(path, "/") - path = "/" + path - return APIBaseURL() + path -} - func RetryOnAuthFail[Res any](ctx context.Context, api *API, f func(ctx context.Context, token string) (Res, error)) (Res, error) { r, err := f(ctx, api.Token) var e HTTPClientError if errors.As(err, &e) && e.Code() == http.StatusUnauthorized { - *api, err = Login(ctx, api.SecretPrefix, api.Key, api.Secret) - if err != nil { + if err := api.login(ctx); err != nil { return r, err } - if err = secrets.Save(ctx, APIClass(), api.SecretPrefix, api.Key, api.Secret, api.Token); err != nil { + tk := fmt.Sprintf(FmtTokenFileName, APIClass(), api.Key) + if err = secrets.Save(ctx, api.SecretPrefix, tk, api.Token); err != nil { return r, err } r, err = f(ctx, api.Token) @@ -248,8 +270,8 @@ func RetryOnAuthFail[Res any](ctx context.Context, api *API, f func(ctx context. return r, err } -func doGet[Res any](ctx context.Context, path, token string) (res Res, err error) { - req, err := http.NewRequest(http.MethodGet, makeUrl(path), nil) +func doGet[Res any](ctx context.Context, url, token string) (res Res, err error) { + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return res, fmt.Errorf("creating request: %w", err) } @@ -276,8 +298,8 @@ func doGet[Res any](ctx context.Context, path, token string) (res Res, err error return res, NewHTTPClientError(rawRes.StatusCode, rb) } -func doGetRaw(ctx context.Context, path, token string) (res io.ReadCloser, err error) { - req, err := http.NewRequest(http.MethodGet, makeUrl(path), nil) +func doGetRaw(ctx context.Context, url, token string) (res io.ReadCloser, err error) { + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return res, fmt.Errorf("creating request: %w", err) } @@ -301,12 +323,12 @@ func doGetRaw(ctx context.Context, path, token string) (res io.ReadCloser, err e return res, NewHTTPClientError(rawRes.StatusCode, rb) } -func doPost[Req, Res any](ctx context.Context, path, token string, request Req) (res Res, err error) { +func doPost[Req, Res any](ctx context.Context, url, token string, request Req) (res Res, err error) { m, err := json.Marshal(request) if err != nil { return res, fmt.Errorf("creating payload: %w", err) } - b, err := doPostBytes(ctx, makeUrl(path), token, m) + b, err := doPostBytes(ctx, url, token, m) if err != nil { return res, err } @@ -342,8 +364,8 @@ func doPostBytes(ctx context.Context, url, token string, body []byte) ([]byte, e return nil, NewHTTPClientError(res.StatusCode, rb) } -func doDelete(ctx context.Context, path, token string) error { - req, err := http.NewRequest(http.MethodDelete, makeUrl(path), nil) +func doDelete(ctx context.Context, url, token string) error { + req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { return fmt.Errorf("creating request: %w", err) } @@ -403,8 +425,8 @@ func download(ctx context.Context, url, token string) (downloadPath string, stop return "", nil, fmt.Errorf("%d: %s", rawRes.StatusCode, string(rb)) } -func makeConfigURL(clusterID string) string { - return makeUrl(fmt.Sprintf("/client_samples/%s/python?source_identifier=default", clusterID)) +func makeConfigURL(base, clusterID string) string { + return fmt.Sprintf("%s/client_samples/%s/python?source_identifier=default", base, clusterID) } func APIClass() string { diff --git a/internal/viridian/cluster.go b/internal/viridian/cluster.go index 3746682ce..bf33c5de0 100644 --- a/internal/viridian/cluster.go +++ b/internal/viridian/cluster.go @@ -43,7 +43,8 @@ func (a *API) CreateCluster(ctx context.Context, name string, clusterType string PlanName: planName, } cluster, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Cluster, error) { - c, err := doPost[createClusterRequest, createClusterResponse](ctx, "/cluster", a.Token, c) + u := a.makeURL("/cluster") + c, err := doPost[createClusterRequest, createClusterResponse](ctx, u, a.Token, c) return Cluster(c), err }) if err != nil { @@ -69,7 +70,8 @@ func (a *API) StopCluster(ctx context.Context, idOrName string) error { return err } ok, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (bool, error) { - return doPost[[]byte, bool](ctx, fmt.Sprintf("/cluster/%s/stop", c.ID), a.Token, nil) + u := a.makeURL("/cluster/%s/stop", c.ID) + return doPost[[]byte, bool](ctx, u, a.Token, nil) }) if err != nil { return fmt.Errorf("stopping cluster: %w", err) @@ -82,7 +84,8 @@ func (a *API) StopCluster(ctx context.Context, idOrName string) error { func (a *API) ListClusters(ctx context.Context) ([]Cluster, error) { csw, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Wrapper[[]Cluster], error) { - return doGet[Wrapper[[]Cluster]](ctx, "/cluster", a.Token) + u := a.makeURL("/cluster") + return doGet[Wrapper[[]Cluster]](ctx, u, a.Token) }) if err != nil { return nil, fmt.Errorf("listing clusters: %w", err) @@ -96,7 +99,8 @@ func (a *API) ResumeCluster(ctx context.Context, idOrName string) error { return err } ok, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (bool, error) { - return doPost[[]byte, bool](ctx, fmt.Sprintf("/cluster/%s/resume", c.ID), a.Token, nil) + u := a.makeURL("/cluster/%s/resume", c.ID) + return doPost[[]byte, bool](ctx, u, a.Token, nil) }) if err != nil { return fmt.Errorf("resuming cluster: %w", err) @@ -113,7 +117,8 @@ func (a *API) DeleteCluster(ctx context.Context, idOrName string) error { return err } _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { - err = doDelete(ctx, fmt.Sprintf("/cluster/%s", c.ID), a.Token) + u := a.makeURL("/cluster/%s", c.ID) + err = doDelete(ctx, u, a.Token) if err != nil { return nil, err } @@ -131,7 +136,8 @@ func (a *API) GetCluster(ctx context.Context, idOrName string) (Cluster, error) return Cluster{}, err } c, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Cluster, error) { - return doGet[Cluster](ctx, fmt.Sprintf("/cluster/%s", cluster.ID), a.Token) + u := a.makeURL("/cluster/%s", cluster.ID) + return doGet[Cluster](ctx, u, a.Token) }) if err != nil { return Cluster{}, fmt.Errorf("retrieving cluster: %w", err) @@ -141,10 +147,18 @@ func (a *API) GetCluster(ctx context.Context, idOrName string) (Cluster, error) func (a *API) ListClusterTypes(ctx context.Context) ([]ClusterType, error) { csw, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Wrapper[[]ClusterType], error) { - return doGet[Wrapper[[]ClusterType]](ctx, "/cluster_types", a.Token) + u := a.makeURL("/cluster_types") + return doGet[Wrapper[[]ClusterType]](ctx, u, a.Token) }) if err != nil { return nil, fmt.Errorf("listing cluster types: %w", err) } return csw.Content, nil } + +func (a *API) makeURL(format string, args ...any) string { + var sb strings.Builder + sb.WriteString(a.APIBaseURL) + sb.WriteString(fmt.Sprintf(format, args...)) + return sb.String() +} diff --git a/internal/viridian/cluster_log.go b/internal/viridian/cluster_log.go index 79d2e0d52..cedeee18c 100644 --- a/internal/viridian/cluster_log.go +++ b/internal/viridian/cluster_log.go @@ -18,8 +18,8 @@ func (a *API) DownloadClusterLogs(ctx context.Context, destDir string, idOrName return err } r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (types.Tuple2[string, func()], error) { - u := makeUrl(fmt.Sprintf("/cluster/%s/logs", c.ID)) - path, stop, err := download(ctx, makeUrl(u), a.Token) + u := a.makeURL("/cluster/%s/logs", c.ID) + path, stop, err := download(ctx, u, a.Token) if err != nil { return types.Tuple2[string, func()]{}, err } diff --git a/internal/viridian/custom_class_download.go b/internal/viridian/custom_class_download.go index 50185989d..cc4d5807e 100644 --- a/internal/viridian/custom_class_download.go +++ b/internal/viridian/custom_class_download.go @@ -39,7 +39,7 @@ func doCustomClassDownload(ctx context.Context, progressSetter func(progress flo return err } defer f.Close() - req, err := http.NewRequest(http.MethodGet, makeUrl(url), nil) + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return fmt.Errorf("creating request: %w", err) } diff --git a/internal/viridian/custom_class_upload.go b/internal/viridian/custom_class_upload.go index 211a65dd0..267b409c4 100644 --- a/internal/viridian/custom_class_upload.go +++ b/internal/viridian/custom_class_upload.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "io" - "mime/multipart" "net/http" "os" @@ -54,7 +53,7 @@ func doCustomClassUpload(ctx context.Context, progressSetter func(progress float } w.Close() pr := &UploadProgressReader{Reader: reqBody, Total: size, SetterFunc: progressSetter} - req, err := http.NewRequest(http.MethodPost, makeUrl(url), pr) + req, err := http.NewRequest(http.MethodPost, url, pr) if err != nil { return err } diff --git a/internal/viridian/login.go b/internal/viridian/login.go index 46a0b5b6b..ddfaee32a 100644 --- a/internal/viridian/login.go +++ b/internal/viridian/login.go @@ -2,7 +2,6 @@ package viridian import ( "context" - "errors" ) type loginRequest struct { @@ -14,25 +13,15 @@ type loginResponse struct { Token string `json:"token"` } -func Login(ctx context.Context, secretPrefix, key, secret string) (API, error) { - var api API - if key == "" { - return api, errors.New("api key cannot be blank") +func Login(ctx context.Context, secretPrefix, key, secret, apiBase string) (API, error) { + a := API{ + SecretPrefix: secretPrefix, + Key: key, + Secret: secret, + APIBaseURL: apiBase, } - if secret == "" { - return api, errors.New("api secret cannot be blank") + if err := a.login(ctx); err != nil { + return a, err } - c := loginRequest{ - APIKey: key, - APISecret: secret, - } - resp, err := doPost[loginRequest, loginResponse](ctx, "/customers/api/login", "", c) - if err != nil { - return api, err - } - api.Key = key - api.Secret = secret - api.Token = resp.Token - api.SecretPrefix = secretPrefix - return api, nil + return a, nil } From fd8bf7c85dd1b49bdfd026fc868a6daa35474053 Mon Sep 17 00:00:00 2001 From: angelasimms <106963942+angelasimms@users.noreply.github.com> Date: Thu, 10 Aug 2023 14:36:43 +0100 Subject: [PATCH 16/79] DOCS-608 Reintroduce Viridian Content and Update Product Name (#319) * DOCS-608 Reintroduce Viridian Content and Update Product Name * DOCS-608 Update Getting Started * DOCS-608 Review comments * DOCS-608 Typos and use cluster name rather than ID * DOCS-608 Hide config wizard * DOCS-608 Correct numbering in tutorial --- README.md | 4 +- docs/antora-playbook-local.yml | 2 +- docs/antora-playbook.yml | 2 +- docs/modules/ROOT/nav.adoc | 14 +- docs/modules/ROOT/pages/clc-commands.adoc | 2 +- docs/modules/ROOT/pages/clc-config.adoc | 16 +- docs/modules/ROOT/pages/clc-home.adoc | 4 +- docs/modules/ROOT/pages/clc-job.adoc | 4 +- docs/modules/ROOT/pages/clc-viridian.adoc | 62 ++--- docs/modules/ROOT/pages/config-wizard.adoc | 17 +- docs/modules/ROOT/pages/configuration.adoc | 11 +- .../ROOT/pages/connect-to-viridian.adoc | 2 +- .../ROOT/pages/environment-variables.adoc | 8 + docs/modules/ROOT/pages/get-started.adoc | 224 ++++++++---------- .../ROOT/pages/jet-job-management.adoc | 2 +- .../pages/managing-viridian-clusters.adoc | 6 +- docs/modules/ROOT/pages/overview.adoc | 6 +- .../ROOT/pages/release-notes-5.2.0.adoc | 7 +- .../pages/release-notes-5.3.0-BETA-1.adoc | 2 +- .../pages/release-notes-5.3.0-BETA-2.adoc | 5 +- .../ROOT/pages/release-notes-5.3.0.adoc | 8 +- .../ROOT/pages/release-notes-5.3.1.adoc | 2 +- 22 files changed, 196 insertions(+), 214 deletions(-) diff --git a/README.md b/README.md index f0615e9ab..1c85d3a02 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ All paths in the configuration are relative to the parent directory of the confi * cluster * name: Name of the cluster. By default `dev`. * address: Address of a member in the cluster. By default `localhost:5701`. - * discovery-token: Viridian Serverless discovery token. + * discovery-token: {hazelcast-cloud} Serverless discovery token. * ssl * ca-path: TLS CA certificate path. @@ -93,7 +93,7 @@ All paths in the configuration are relative to the parent directory of the confi * path: Path to the log file, or `stderr`. By default, the logs are written to `$CLC_HOME/logs` with the current date as the name. * level: Log level, one of: `debug`, `info`, `warn`, `error`. The default is `info`. -Here's a sample Viridian Serverless configuration: +Here's a sample {hazelcast-cloud} Serverless configuration: ``` cluster: name: "pr-3814" diff --git a/docs/antora-playbook-local.yml b/docs/antora-playbook-local.yml index d2d85cdf7..a14396bfe 100644 --- a/docs/antora-playbook-local.yml +++ b/docs/antora-playbook-local.yml @@ -27,7 +27,7 @@ asciidoc: idseparator: '-' # Variables used in the docs page-survey: https://www.surveymonkey.co.uk/r/NYGJNF9 - hazelcast-cloud: Viridian + hazelcast-cloud: Viridian Cloud extensions: - ./tabs-block.js - asciidoctor-kroki diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml index dd6b79b08..0a211f75e 100644 --- a/docs/antora-playbook.yml +++ b/docs/antora-playbook.yml @@ -27,7 +27,7 @@ asciidoc: idseparator: '-' # Variables used in the docs page-survey: https://www.surveymonkey.co.uk/r/NYGJNF9 - hazelcast-cloud: Viridian + hazelcast-cloud: Viridian Cloud extensions: - ./tabs-block.js - asciidoctor-kroki diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 54ed355d1..873edaa70 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -1,17 +1,17 @@ .Get Started * xref:overview.adoc[What is Hazelcast CLC?] -//* xref:get-started.adoc[Get Started] -//* Tutorials -//** xref:managing-viridian-clusters.adoc[Manage Viridian Clusters] -//** xref:jet-job-management.adoc[Manage Jet Jobs] +* xref:get-started.adoc[Get Started] +* Tutorials +** xref:managing-viridian-clusters.adoc[Manage {hazelcast-cloud} Clusters] +** xref:jet-job-management.adoc[Manage Jet Jobs] .Manage * xref:install-clc.adoc[Install] * xref:configuration.adoc[Configure] -//** xref:connect-to-viridian.adoc[Connect to Viridian] +** xref:connect-to-viridian.adoc[Connect to {hazelcast-cloud}] ** xref:connect-to-platform.adoc[Connect to Platform] -// ** xref:config-wizard.adoc[CLC Configuration Wizard ] +//** xref:config-wizard.adoc[CLC Configuration Wizard ] * xref:upgrade-clc.adoc[Upgrade] .Reference @@ -31,7 +31,7 @@ ** xref:clc-sql.adoc[] ** xref:clc-snapshot.adoc[] ** xref:clc-version.adoc[] -//** xref:clc-viridian.adoc[] +** xref:clc-viridian.adoc[] ** xref:clc-project.adoc[] * xref:environment-variables.adoc[] * xref:keyboard-shortcuts.adoc[] diff --git a/docs/modules/ROOT/pages/clc-commands.adoc b/docs/modules/ROOT/pages/clc-commands.adoc index ef6bbac1b..3d88cca8b 100644 --- a/docs/modules/ROOT/pages/clc-commands.adoc +++ b/docs/modules/ROOT/pages/clc-commands.adoc @@ -53,7 +53,7 @@ include::partial$global-parameters.adoc[] |Manage Jet job snapshots. |xref:clc-viridian.adoc[clc viridian] -|Viridian related operations. +|{hazelcast-cloud} related operations. |xref:clc-object.adoc[clc object] |Commands for generic distributed data structures. diff --git a/docs/modules/ROOT/pages/clc-config.adoc b/docs/modules/ROOT/pages/clc-config.adoc index c8fe781bb..34287b25b 100644 --- a/docs/modules/ROOT/pages/clc-config.adoc +++ b/docs/modules/ROOT/pages/clc-config.adoc @@ -56,37 +56,37 @@ Configuration keys: |cluster.discovery-token |Any string | -|Viridian only +|{hazelcast-cloud} only |ssl.enabled |true / false |false -|Viridian or Hazelcast Enterprise +|{hazelcast-cloud} or Hazelcast Enterprise |ssl.server |DOMAIN_NAME (must match SSL certificate) | -|Viridian (automatically set) or Hazelcast Enterprise +|{hazelcast-cloud} (automatically set) or Hazelcast Enterprise |ssl.skip-verify |true / false |false -|Viridian or Hazelcast Enterprise. Disables SSL host name and client-side verification. Never set to true in production. +|{hazelcast-cloud} or Hazelcast Enterprise. Disables SSL host name and client-side verification. Never set to true in production. |ssl.ca-path |An absolute or relative path | -|Viridian or Hazelcast Enterprise. Sets the path to the CA certificate. +|{hazelcast-cloud} or Hazelcast Enterprise. Sets the path to the CA certificate. |ssl.key-path |An absolute or relative path | -|Viridian or Hazelcast Enterprise. Sets the path to the certificate key. +|{hazelcast-cloud} or Hazelcast Enterprise. Sets the path to the certificate key. |ssl.key-password -|Any sring +|Any string | -|Viridian or Hazelcast Enterprise. Sets the passwor the certificate key. +|{hazelcast-cloud} or Hazelcast Enterprise. Sets the password for the certificate key. |log.path |An absolute or relative path or `stderr` to log to stderr. diff --git a/docs/modules/ROOT/pages/clc-home.adoc b/docs/modules/ROOT/pages/clc-home.adoc index ec38bbb63..a59c64d48 100644 --- a/docs/modules/ROOT/pages/clc-home.adoc +++ b/docs/modules/ROOT/pages/clc-home.adoc @@ -28,6 +28,6 @@ Example output: [source,bash] ---- -clc home configs prod -/home/me/.local/share/clc/configs/prod +clc home configs pr-3814 +/home/me/.local/share/clc/configs/pr-3814 ---- diff --git a/docs/modules/ROOT/pages/clc-job.adoc b/docs/modules/ROOT/pages/clc-job.adoc index b2c31283a..c0923bc1d 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 cluster of 5.3.0-BETA-2 or better. +This command requires a {hazelcast-cloud} or Hazelcast 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 Enterprise cluster. +Exports a snapshot from a Jet job. This feature requires a {hazelcast-cloud} or Hazelcast Enterprise cluster. Usage: diff --git a/docs/modules/ROOT/pages/clc-viridian.adoc b/docs/modules/ROOT/pages/clc-viridian.adoc index 7f0b321de..b1bf63837 100644 --- a/docs/modules/ROOT/pages/clc-viridian.adoc +++ b/docs/modules/ROOT/pages/clc-viridian.adoc @@ -1,8 +1,8 @@ = clc viridian -This command group provides commands for doing various Viridian operations, such as creating and managing clusters. +This command group provides commands for doing various {hazelcast-cloud} operations, such as creating and managing clusters. -All commands except `viridian login` requires a token to be generated using an API key and secret which can be retrieved from Viridian Console. Running `viridian login` prompts this information, and creates and saves the token. +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. Usage: @@ -31,7 +31,7 @@ clc viridian [command] [options] == clc viridian login -Logs in to Viridian using the given API key and API secret and retrieves a token. +Logs in to {hazelcast-cloud} using the given API key and API secret and retrieves a token. The token is cached to be used by other commands. If not specified, the key and the secret will be asked in a prompt. @@ -67,9 +67,9 @@ Parameters: == clc viridian list-cluster-types -Lists available cluster types that can be used while creating a Viridian cluster. +Lists available cluster types that can be used while creating a {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate with the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -93,9 +93,9 @@ Parameters: == clc viridian create-cluster -Creates a Viridian cluster. +Creates a {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -129,9 +129,9 @@ Parameters: == clc viridian get-cluster -Gets the information about the given Viridian cluster. +Gets the information about the given {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -155,9 +155,9 @@ Parameters: == clc viridian list-clusters -Lists all Viridian clusters for the logged in API key. +Lists all {hazelcast-cloud} clusters for the logged in API key. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -181,9 +181,9 @@ Parameters: == clc viridian stop-cluster -Stops the given Viridian cluster. +Stops the given {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -207,9 +207,9 @@ Parameters: == clc viridian resume-cluster -Resumes the given Viridian cluster. +Resumes the given {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -233,9 +233,9 @@ Parameters: == clc viridian delete-cluster -Deletes the given Viridian cluster. Note that, all data in the cluster is deleted irreversibly. +Deletes the given {hazelcast-cloud} cluster. Note that, all data in the cluster is deleted irreversibly. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -264,9 +264,9 @@ Parameters: == clc viridian download-logs -Downloads the logs of the given Viridian cluster. +Downloads the logs of the given {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -295,9 +295,9 @@ Parameters: == clc viridian stream-logs -Outputs the logs of the given Viridian cluster as a stream. +Outputs the logs of the given {hazelcast-cloud} cluster as a stream. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. The log format may be one of: @@ -333,9 +333,9 @@ Parameters: == clc viridian import-config -Imports connection configuration of the given Viridian cluster. +Imports connection configuration of the given {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -359,9 +359,9 @@ Parameters: == clc viridian list-custom-classes -Lists all custom classes in the Viridian cluster. +Lists all custom classes in the {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -385,9 +385,9 @@ Parameters: == clc viridian upload-custom-class -Uploads a custom class to the Viridian cluster. +Uploads a custom class to the {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -398,9 +398,9 @@ clc viridian upload-custom-class [cluster-name/cluster-ID] [file-name] [flags] == clc viridian download-custom-class -Downloads a custom class from the Viridian cluster. +Downloads a custom class from the {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: @@ -424,9 +424,9 @@ Parameters: == clc viridian delete-custom-class -Deletes a custom class from the Viridian cluster. +Deletes a custom class from the {hazelcast-cloud} cluster. -Make sure you authenticate to the Viridian API using `viridian login` before running this command. +Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. Usage: diff --git a/docs/modules/ROOT/pages/config-wizard.adoc b/docs/modules/ROOT/pages/config-wizard.adoc index f035c9fbd..83b8b79a5 100644 --- a/docs/modules/ROOT/pages/config-wizard.adoc +++ b/docs/modules/ROOT/pages/config-wizard.adoc @@ -1,4 +1,4 @@ -//== CLC Configuration Wizard +== CLC Configuration Wizard :description: Hazelcast CLC provides a configuration wizard in interactive mode, which allows you to easily manage multiple cluster configurations. The configuration wizard runs when a client connection needs and asks for a single cluster configuration. @@ -6,11 +6,11 @@ {description} -If you don't have any configuration yet, it will show a prompt screen to enter a configuration source with target name. If you have configurations -in the Hazelcast CLC home directory, it will list them and ask for you to select one to create client connection. +If you try to run an operation that requires a client connection before you have added any configuration, you are prompted to enter a configuration source with the target name. -TIP: If you have a configuration named `default`, a configuration YAML exists next to executable or in the current working directory, -CLC directly uses it instead of running configuration wizard. +If you have already added one or more configurations to the Hazelcast CLC home directory, you are prompted to select one to create a client connection. + +TIP: If you have a configuration named `default`, a configuration YAML exists next to the executable or in the current working directory. CLC uses this instead of running configuration wizard. == Before you Begin @@ -40,7 +40,7 @@ input screen for you to import a new configuration. + ```bash There is no configuration detected. You can register a new configuration to conn -You can connect to Viridian cluster using curl request from `Dashboard -> Connec +You can connect to {hazelcast-cloud} cluster using curl request from `Dashboard -> Connec Paste the link to source field below and it will download the config and TLS fil Configuration Name: default @@ -60,14 +60,13 @@ Configuration Name: default . Copy the Go Client sample url from {hazelcast-cloud} console. -. Paste the Go client sample CURL request link to `Source: ` in this screen. +. Paste the Go client sample CURL request link to `Source:` in this screen. + ```bash Source: curl https://api.viridian.hazelcast.com/client_samples/download/... ``` + -. Press `[ Submit ]` and it will automatically connect to your Viridian cluster. Notice that if you didn't specify the -configuration name or leave it as default, it will automatically connect to this client unless you specify it with `--config` flag. +. Press `[ Submit ]` and it will automatically connect to your Viridian Cloud cluster. Notice that if you didn't specify the configuration name or leave it as default, it will automatically connect to this client unless you specify it with `--config` flag. == Multiple Configurations diff --git a/docs/modules/ROOT/pages/configuration.adoc b/docs/modules/ROOT/pages/configuration.adoc index 87b792216..3601d5fdc 100644 --- a/docs/modules/ROOT/pages/configuration.adoc +++ b/docs/modules/ROOT/pages/configuration.adoc @@ -10,8 +10,7 @@ This file can exist anywhere in the file system, and can be used with the `--con clc -c test/config.yaml ---- -//TIP: If you don't add any configuration before and try to run an operation that requires client connection, CLC will prompt a configuration wizard to import Viridian config easily. For details, see xref:config-wizard.adoc[CLC Configuration Wizard ]. - +//TIP: If you try to run an operation that requires a client connection before you have added any configuration, CLC will prompt the configuration wizard to import a {hazelcast-cloud} configuration. For details, see xref:config-wizard.adoc[CLC Configuration Wizard]. If there is a `config.yaml` in the same directory with the CLC binary and the configuration was not explicitly set, CLC tries to load that configuration file: [source, bash] @@ -36,10 +35,10 @@ Known configurations can be directly specified by their names, instead of the fu # List configurations $ clc config list default -prod +pr-3066 # Start CLC shell with configuration named pr-3066 -$ clc -c prod +$ clc -c pr-3066 ---- If no configuration is specified, the `default` configuration is used. @@ -54,7 +53,7 @@ include::partial$global-parameters.adoc[] == Related Resources -//- xref:connect-to-viridian.adoc[]. +- xref:connect-to-viridian.adoc[Connecting to {hazelcast-cloud} with Hazelcast CLC]. -- xref:connect-to-platform.adoc[]. +- xref:connect-to-platform.adoc[Connecting to Hazelcast Platform with Hazelcast CLC]. diff --git a/docs/modules/ROOT/pages/connect-to-viridian.adoc b/docs/modules/ROOT/pages/connect-to-viridian.adoc index 2bbee04a7..c7042a458 100644 --- a/docs/modules/ROOT/pages/connect-to-viridian.adoc +++ b/docs/modules/ROOT/pages/connect-to-viridian.adoc @@ -29,7 +29,7 @@ clc viridian login [[list-accessible-clusters]] == List Accessible Clusters -Run the following command to list the clusters linked to your Hazelcast {hazelcast-cloud} account: +Run the following command to list the clusters linked to your {hazelcast-cloud} account: ```bash clc viridian list-clusters diff --git a/docs/modules/ROOT/pages/environment-variables.adoc b/docs/modules/ROOT/pages/environment-variables.adoc index 05fc1be07..1e93bb01c 100644 --- a/docs/modules/ROOT/pages/environment-variables.adoc +++ b/docs/modules/ROOT/pages/environment-variables.adoc @@ -27,6 +27,14 @@ |Some Hazelcast CLC features require the cluster to be of a certain version. If set to `1`, this variable disables the version check performed by the CLC. |`0` (false) +|CLC_VIRIDIAN_API_KEY +|Sets the {hazelcast-cloud} API key. +| + +|CLC_VIRIDIAN_API_SECRET +|Sets the {hazelcast-cloud} API secret. +| + |CLC_CONFIG |Sets the configuration CLC will use if `--config` flag is not specified. | diff --git a/docs/modules/ROOT/pages/get-started.adoc b/docs/modules/ROOT/pages/get-started.adoc index 769f32ce5..fe62f7052 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 the basics of how to configure the Hazelcast CLC, start it, and connect it to a Hazelcast {hazelcast-cloud} Serverless development cluster and a Serverless production cluster. You'll also see how to switch between clusters, using the Hazelcast CLC. Finally, you'll perform some basic operations on a cluster from the command line and then by running a script to automate the same actions. +: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. {description} @@ -7,183 +7,151 @@ You need the following: -- The xref:install-clc.adoc[Hazelcast CLC] installed on your local machine -- Two xref:cloud:ROOT:create-serverless-cluster.adoc[Hazelcast {hazelcast-cloud} Serverless clusters]. For this tutorial, you'll use one development and one production cluster, to show how you might use the CLC for basic administration and deployment tasks. +- xref:install-clc.adoc[Hazelcast CLC] installed on your local machine. +- xref:cloud:ROOT:developer.adoc[{hazelcast-cloud} API key and secret] -TIP: You can download Go client samples from Hazelcast {hazelcast-cloud} console. For details, see xref:cloud:ROOT:connect-to-cluster.adoc[]. +== Step 1. Authenticating with {hazelcast-cloud} -[[step-1-dev-configure]] -== Step 1. Add Configuration for the Development Cluster +To allow the Hazelcast CLC to to interact with {hazelcast-cloud} clusters, you must generate a {hazelcast-cloud} token. -Make sure that your Viridian development cluster is running before you start. +. Execute the following command to retrieve the {hazelcast-cloud} token. ++ +[source,shell] +---- +clc viridian login +---- -The Hazelcast CLC can use a Go client sample for Viridian to discover the configuration for your Viridian cluster. +. When prompted, enter your API key and secret. If both are correct, the token is retrieved and saved. -. Download the Go client sample for your development cluster from the Viridian console. -Once the sample is downloaded, you can use the `clc config import` command to import the configuration. +== Step 2. Create Two {hazelcast-cloud} Clusters -. From the command line, execute the following command, replacing `PATH-TO-SAMPLE.zip` with the path to the downloaded sample: +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. +. Execute the following command to create your first production cluster. + -[source, bash] +[source,shell] ---- -clc config import dev PATH-TO-SAMPLE.zip -clc -c dev +clc viridian create-cluster --name Test1 ---- - + -. You can use the `clc config list` command to confirm that the configuration was imported. The output will look something like this: +Wait while the cluster is created and the cluster configuration is imported into the Hazelcast CLC. + +. Create your second production cluster in the same way. + + -[source, bash] +[source,shell] ---- -clc config list -default -dev +clc viridian create-cluster --name Test2 ---- +. Try running the following command to check your clusters have been created: + -. The CLC only connects to the cluster when necessary. Run a command that requires a connection to the cluster, such as `object list`: - -+ -[source, clc] +[source,shell] ---- -CLC> \object list -Connected to cluster: pr-3814 +clc viridian list-clusters ---- - + -As this is a new cluster, no objects are returned. +The details of all clusters linked to your Hazelcast {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. -+ -. Press kbd:[Ctrl+D] or type `\exit` to shut down the Hazelcast CLC while you complete the configuration for your production cluster. [[step-2-prod-configure]] -== Step 2. Add Configuration for the Production Cluster +== Step 3. Add Configuration to Connect to {hazelcast-cloud} -The configuration of the production cluster is the same as the previous step, but using the production cluster configuration. +To connect to each production cluster, you need to import additional cluster configuration from {hazelcast-cloud}. -Make sure that your Viridian production cluster is running before you start. - -. Download the Go client sample for your production cluster from the Viridian console. -. From the command line, execute the following command, replacing `PATH-TO-SAMPLE.zip` with the path to the downloaded sample: +. Run the `clc config import` to update the cluster configuration on your local machine. + -[source, bash] +[source, shell] ---- -clc config import prod PATH-TO-SAMPLE.zip -clc -c prod +clc viridian import-config Test1 ---- -+ -. CLC only connects to the cluster when necessary. -Run a command that requires a connection to the cluster, such as `object list`: +. Run the following command to connect to your cluster using the Hazelcast CLC. + -[source, clc] +[source, shell] ---- -CLC> \object list -Connected to cluster: pr-3690 - ------------------------------------- - Service Name | Object Name ------------------------------------- - executor | hot-backup-executor ------------------------------------- +clc -c Test1 ---- -[[step-3-cluster-switch]] -== Step 3. Switch Between Clusters - -Having separate local configurations for your development and production clusters allows you to switch between them without leaving the command line. Make sure both clusters are running before you start. - -. Execute the following command to use the `dev` configuration to connect to your development cluster: +. The Hazelcast CLC only connects to the cluster when necessary. Run a command that requires a connection to the cluster, such as `object list`: + -[source, bash] +[source, shell] ---- -clc -c dev +CLC> \object list +Connected to cluster: pr- ---- - -+ -As before, the Hazelcast CLC connects to your development cluster. -. Type `\exit` to return to the command line. -. Execute the following command to use the new configuration file for your production cluster. + -[source, bash] ----- -clc -c prod ----- +As this is a new cluster, no objects are returned. + -The Hazelcast CLC shell is started, using the configuration for the production cluster. You can exit the shell by typing `\exit`. +NOTE: This command shows the cluster ID prefixed with `pr` instead of the cluster name. You can see the cluster ID in *Cluster Details* on the {hazelcast-cloud} console. -[[step-4-write-data]] -== Step 4. Write Data to a Map +. Press kbd:[Ctrl+D] or type `\exit` to shut down the Hazelcast CLC while you complete the configuration of your second production cluster. -Now that you've connected to both your clusters, try using the Hazelcast CLC to write data to a map on your development cluster. -Let's write a script that adds some data to a map. +. Repeat steps 1 to 4 for Test2. -. Create a file called `data.script` on your local machine and copy in the following lines. -+ -.data.script -[source] ----- -\map set -n currency -k i32 1 -v json '{"Code": "CAD", "Currency": "Canadian Dollar"}' -\map set -n currency -k i32 2 -v json '{"Code": "INR", "Currency": "Indian Rupee"}' -\map set -n currency -k i32 3 -v json '{"Code": "MXN", "Currency": "Mexican Peso"}' -\map set -n currency -k i32 4 -v json '{"Code": "GBP", "Currency": "Pounds Sterling"}' -\map set -n currency -k i32 5 -v json '{"Code": "TRY", "Currency": "Turkish Lira"}' -\map set -n currency -k i32 6 -v json '{"Code": "USD", "Currency": "United States Dollar"}' ----- +[[step-3-cluster-switch]] +== Step 4. Switch Between Clusters + +Having separate local configurations for your two production clusters allows you to switch between them without leaving the command line. Make sure both clusters are running before you start. + +. Execute the following command to connect to your Test1 cluster. -. Run the script in the `data.script` file to update the `currency` map: -+ -On Linux and MacOS: + -[source,bash] +[source, shell] ---- -cat data.script | clc -c dev +clc -c Test1 ---- + -On Windows: +As before, the Hazelcast CLC connects to your Test1 cluster. + +. Type `\exit` to return to the command line. +. Execute the same command to connect to Test2. + -[source,bash] +[source, shell] ---- -type data.script | clc -c dev +clc -c Test2 ---- +. Again, type `\exit` to return to the command line. +[[step-4-write-data]] +== Step 5. Write Data to a Map -Do a quick check on your cluster to make sure that your data has been written successfully. - -. Open the dashboard of the development cluster and click *Management Center*. -. Go to *Storage* > *Maps*. You'll see that your cluster has a map called `currency` with six entries. - -[[step-5-query-map]] -== Step 5. Query Map Data -You can use SQL to query the data in your `currency` map. +Now that you've connected to both your clusters, try using the Hazelcast CLC to create a map on Test1, write some data to it, and query the data. You can use SQL to do this. -. Start by creating a mapping to the `currency` map. +. Start by creating a mapping to a new `currency` map. + -[source,bash] +[source,shell] ---- -clc sql -c dev "CREATE OR REPLACE MAPPING currency (__key INT, Code VARCHAR, Currency VARCHAR) TYPE IMap OPTIONS('keyFormat'='int', 'valueFormat'='json-flat')" +clc sql -c Test1 "CREATE OR REPLACE MAPPING currency (__key INT, Code VARCHAR, Currency VARCHAR) TYPE IMap OPTIONS('keyFormat'='int', 'valueFormat'='json-flat');" ---- ++ The SQL mapping statement does a number of things: ** Adds column headings for currencies and codes ** Creates a SQL connection to the map ** Tells Hazelcast how to serialize and deserialize the keys and values. +. Add some data to the map. ++ +[source,shell] +---- +clc sql -c Test1 "INSERT INTO currency VALUES (1, 'CAD','Canadian Dollar'),(2, 'INR','Indian Rupee'),(3, 'MXN', 'Mexican Peso'),(4, 'GBP', 'Pounds Sterling'),(5, 'TRY', 'Turkish Lira'),(6, 'USD', 'United States Dollar');" +---- + . Try running some simple queries against the `currency` map. For example, this query returns all data in the map and orders it by the currency code. + -[source,bash] +[source,shell] ---- -clc sql -c dev "SELECT * FROM currency ORDER BY Code" -f table +clc sql -c Test1 "SELECT * FROM currency ORDER BY Code" -f table ---- + The results look like this: + -[source,shell] +[source,sql] ---- -------------------------------------------------------------------------------- __key | Code | Currency @@ -202,7 +170,7 @@ The results look like this: When you're ready, combine the commands that you've learned about so far into a script and run them from the command line. -The script first writes the currency data to a new map called `currencydata` on your development server, queries it and then switches to your production cluster to perform the same actions. +The script writes the currency data to a new map called `currencydata` on a cluster and queries it. . Copy the following commands into a script. + @@ -237,47 +205,55 @@ SELECT * FROM currencydata ORDER BY Code; Linux and MacOS:: + -- -. To run the script on your development cluster, execute the following command: +. To run the script on your Test1 cluster, execute the following command. + -[source,bash] +[source,shell] ---- -cat myscript.sql | clc -c dev +cat myscript.sql | clc -c Test1 ---- + -. Then, to run the script on your production cluster, execute the following command: +. Then, to run the script on your Test2 cluster, execute the same command replacing the cluster name. + -[source,bash] +[source,shell] ---- -cat myscript.sql | clc -c prod +cat myscript.sql | clc -c Test2 ---- -- Windows:: + -- -. To run the script on your development cluster, execute the following command: +. To run the script on your Test1 cluster, execute the following command. + -[source,bash] +[source,shell] ---- -type myscript.sql | clc -c dev +type myscript.sql | clc -c Test1 ---- + -. Then, to run the script on your production cluster, execute the following command: +. Then, to run the script on your Test2 cluster, execute the same command replacing the cluster name. + -[source,bash] +[source,shell] ---- -type myscript.sql | clc -c prod +type myscript.sql | clc -c Test2 ---- -- ==== +== Step 7. Clean Up + +To delete both test clusters from your account. + +. link:https://viridian.hazelcast.com/[Sign in to {hazelcast-cloud}] and select a test cluster. +. Click *Delete* and confirm your deletion. + == Summary In this tutorial, you learned how to do the following: -* Connect to a Hazelcast {hazelcast-cloud} Serverless development cluster. -* Connect to a Hazelcast {hazelcast-cloud} Serverless production cluster. +* Authenticate with {hazelcast-cloud}. +* Create a cluster and check its status. +* Connect to a {hazelcast-cloud} production cluster. * Switch between clusters from the command line. * Write data to a map and query the data using SQL. * Automate commands by running a sequence of actions from a shell script. @@ -292,3 +268,7 @@ Use these resources to continue learning: - xref:clc-sql.adoc[]. +- xref:managing-viridian-clusters.adoc[] + +- xref:jet-job-management.adoc[] + diff --git a/docs/modules/ROOT/pages/jet-job-management.adoc b/docs/modules/ROOT/pages/jet-job-management.adoc index ec258668c..439cf0727 100644 --- a/docs/modules/ROOT/pages/jet-job-management.adoc +++ b/docs/modules/ROOT/pages/jet-job-management.adoc @@ -14,7 +14,7 @@ You need the following: - Gradle 8 or above. [[step-1-authenticating-with-viridian]] -== Step 1. Authenticating with Viridian +== Step 1. Authenticating with {hazelcast-cloud} To allow the Hazelcast CLC to perform cluster operations, including submitting Jet jobs, you must generate a {hazelcast-cloud} token. diff --git a/docs/modules/ROOT/pages/managing-viridian-clusters.adoc b/docs/modules/ROOT/pages/managing-viridian-clusters.adoc index 4edc209f8..0cb2dc3d3 100644 --- a/docs/modules/ROOT/pages/managing-viridian-clusters.adoc +++ b/docs/modules/ROOT/pages/managing-viridian-clusters.adoc @@ -13,9 +13,9 @@ You need the following: - xref:cloud:ROOT:developer.adoc[{hazelcast-cloud} API key and secret] [[step-1-authenticating-with-viridian]] -== Step 1. Authenticating with Viridian +== Step 1. Authenticating with {hazelcast-cloud} -To allow the Hazelcast CLC to perform cluster operations, you must generate a Viridian token. +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. + @@ -60,7 +60,7 @@ To check that the `my-cluster` is up and running, use the following command. ---- 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. [source, bash, subs="attributes+"] ---- diff --git a/docs/modules/ROOT/pages/overview.adoc b/docs/modules/ROOT/pages/overview.adoc index ac26092ca..d3dbb5727 100644 --- a/docs/modules/ROOT/pages/overview.adoc +++ b/docs/modules/ROOT/pages/overview.adoc @@ -1,6 +1,6 @@ = 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 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-cloud} and Hazelcast Platform direct from the command line or through scripts. {description} @@ -18,9 +18,9 @@ Example use cases for the Hazelcast CLC. Run xref:clc-sql.adoc[SQL] queries against your cluster and display the output as a table or in a variety of text formats, such as JSON and CSV. -//=== Manage Viridian Clusters +=== Manage {hazelcast-cloud} Clusters -//xref:clc-viridian.adoc[Create and manage] {hazelcast-cloud} clusters, and the custom classes on those clusters. +xref:clc-viridian.adoc[Create and manage] {hazelcast-cloud} clusters, and the custom classes on those clusters. === Create Data Pipelines 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 090f6dee5..fe69b7004 100644 --- a/docs/modules/ROOT/pages/release-notes-5.2.0.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.2.0.adoc @@ -3,19 +3,18 @@ == New Features * CLC can now read data serialized using Compact Serialization and Portable automatically. -//* Added the ability to select a configuration from a list or import a Viridian configuration when a configuration is not provided in the shell mode. +* 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 `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 {hazelcast-cloud} Serverless]. +* Added support for link:https://hazelcast.com/products/viridian/[{hazelcast-cloud} Serverless]. * Added the following commands: ** `object list`: Lists the distributed data structures in the cluster. ** `completion`: Generates the autocompletion script for the specified shell, either Bash, Fish, Powershell or Zsh. Use `--help` for more information. ** `config add`: Adds configuration to the Hazelcast CLC. For example, to connect to a specific cluster. -//** `config import`: Imports configuration into the Hazelcast CLC from various sources. Currently only the {hazelcast-cloud} Serverless Go Client sample is supported. ** `config list`: Lists known configurations. ** `home`: Outputs the Hazelcast CLC home directory, which stores all configuration, logs and other files. @@ -43,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} Serverless 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.3.0-BETA-1.adoc b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc index 6dfbf0dc1..240a856a5 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 @@ -10,7 +10,7 @@ ** `suspend`: Suspends a job. ** `resume`: Resumes a suspended job. ** `restart`: Restarts a job. -** `export-snapshot`: Exports a snapshot for a job. This feature requires a Hazelcast Enterprise cluster. +** `export-snapshot`: Exports a snapshot for a job. This feature requires a {hazelcast-cloud} or Hazelcast Enterprise cluster. * 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 499d17f73..230690cef 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 @@ -1,8 +1,8 @@ = 5.3.0-BETA-2 Release Notes == New Features -//// -* Viridian support. The following commands were added: + +* {hazelcast-cloud} support. The following commands were added: ** `viridian login` ** `viridian create-cluster` ** `viridian delete-cluster` @@ -15,7 +15,6 @@ ** `viridian download-custom-class` ** `viridian list-custom-classes` ** `viridian upload-custom-class` -//// * Added the following new Map commands: ** `map key-set` ** `map destroy` 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 7d73740f3..5e7688346 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.0.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.0.adoc @@ -1,8 +1,7 @@ = 5.3.0 Release Notes == New Features -//// -* Viridian support. The following commands were added: +* {hazelcast-cloud} support. The following commands were added: ** `viridian login` ** `viridian create-cluster` ** `viridian delete-cluster` @@ -17,7 +16,6 @@ ** `viridian download-custom-class` ** `viridian list-custom-classes` ** `viridian upload-custom-class` -//// * CLC can now submit Jet jobs and manage job snapshots. * Added the following `job` commands: ** `submit`: Creates a job from the given jar file. @@ -26,7 +24,7 @@ ** `suspend`: Suspends a job. ** `resume`: Resumes a suspended job. ** `restart`: Restarts a job. -** `export-snapshot`: Exports a snapshot for a job. This feature requires a Hazelcast Enterprise cluster. +** `export-snapshot`: Exports a snapshot for a job. This feature requires a {hazelcast-cloud} or Hazelcast Enterprise cluster. * Added the following `snapshot` commands: ** `list`: Lists the snapshots of a job. ** `delete`: Deletes a snapshot. @@ -36,5 +34,5 @@ == Improvements -//* Viridian errors are revamped to be more user-friendly. +* {hazelcast-cloud} errors are revamped to be more user-friendly. * Multiline columns are supported in table output. \ No newline at end of file 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 41f1c87ad..c14a2edb6 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.1.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.1.adoc @@ -8,4 +8,4 @@ * Added a workaround for a server side bug that causes an incorrect hash calculation error when certain type of Jet jars are submitted. See: https://github.com/hazelcast/hazelcast/pull/24674 * Extended cluster information retrieved from 'clc viridian get-cluster' command. -//* Improved error checking so a token error is displayed when a Viridian command fails with an authentication error. +* Improved error checking so a token error is displayed when a {hazelcast-cloud} command fails with an authentication error. From 1db0ae2ed16ea814c8b773768eee710e5e83b140 Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Thu, 10 Aug 2023 17:04:52 +0300 Subject: [PATCH 17/79] Add config name into CLC command promt [CLC-242] (#318) --- README.md | 4 ++-- base/commands/shell.go | 24 ++++++++++++++++++---- clc/paths/paths.go | 5 +++++ docs/modules/ROOT/pages/clc-sql.adoc | 2 +- docs/modules/ROOT/pages/clc.adoc | 6 +++--- docs/modules/ROOT/pages/config-wizard.adoc | 4 ++-- docs/modules/ROOT/pages/get-started.adoc | 2 +- docs/modules/ROOT/pages/overview.adoc | 4 ++-- internal/str/str.go | 12 ++++++++++- 9 files changed, 47 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1c85d3a02..a2f7d0698 100644 --- a/README.md +++ b/README.md @@ -150,12 +150,12 @@ $ clc Run SQL commands: ``` -CLC> select * from cities; +(default)> select * from cities; ``` Run CLC commands: ``` -CLC> \map set my-key my-value +(default)> \map set my-key my-value ``` ### Keyboard Shortcuts diff --git a/base/commands/shell.go b/base/commands/shell.go index d3f773db4..5e78396f4 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -5,6 +5,7 @@ package commands import ( "context" "fmt" + "path/filepath" "strings" "sync" @@ -16,6 +17,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" ) @@ -61,11 +63,11 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext if err != nil { return fmt.Errorf("cloning Main: %w", err) } - var cfgText, logText string + var cfgText, logText, cfgPath string if !terminal.IsPipe(ec.Stdin()) { - cfgPath := ec.Props().GetString(clc.PropertyConfig) + cfgPathProp := ec.Props().GetString(clc.PropertyConfig) + cfgPath = paths.ResolveConfigPath(cfgPathProp) if cfgPath != "" { - cfgPath = paths.ResolveConfigPath(cfgPath) cfgText = fmt.Sprintf("Configuration : %s\n", cfgPath) } logPath := ec.Props().GetString(clc.PropertyLogPath) @@ -94,7 +96,8 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext sh.SetCommentPrefix("--") return sh.Run(ctx) } - sh, err := shell.New("CLC> ", " ... ", path, ec.Stdout(), ec.Stderr(), ec.Stdin(), endLineFn, textFn) + p := makePrompt(cfgPath) + sh, err := shell.New(p, " ... ", path, ec.Stdout(), ec.Stderr(), ec.Stdin(), endLineFn, textFn) if err != nil { return err } @@ -103,6 +106,19 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext return sh.Start(ctx) } +func makePrompt(cfgPath string) string { + if cfgPath == "" { + return "> " + } + // Best effort for absolute path + p, err := filepath.Abs(cfgPath) + if err == nil { + cfgPath = p + } + pd := paths.ParentDir(cfgPath) + return fmt.Sprintf("(%s)> ", str.MaybeShorten(pd, 12)) +} + func (*ShellCommand) Unwrappable() {} func init() { diff --git a/clc/paths/paths.go b/clc/paths/paths.go index 2fd0c3de2..a50f5f090 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -187,3 +187,8 @@ func SplitExt(dest string) (base, ext string) { ext = filepath.Ext(dest) return dest[:len(dest)-len(ext)], ext } + +func ParentDir(path string) string { + dirs := filepath.Dir(path) + return filepath.Base(dirs) +} diff --git a/docs/modules/ROOT/pages/clc-sql.adoc b/docs/modules/ROOT/pages/clc-sql.adoc index 02e70d32c..6c10b79a4 100644 --- a/docs/modules/ROOT/pages/clc-sql.adoc +++ b/docs/modules/ROOT/pages/clc-sql.adoc @@ -58,7 +58,7 @@ Interactive mode:: [source,bash] ---- clc -CLC> SELECT * FROM cities +(default)> SELECT * FROM cities --------------------------------------------------------------------------------- __key | country | city | population --------------------------------------------------------------------------------- diff --git a/docs/modules/ROOT/pages/clc.adoc b/docs/modules/ROOT/pages/clc.adoc index 031d65468..0cf08c6f5 100644 --- a/docs/modules/ROOT/pages/clc.adoc +++ b/docs/modules/ROOT/pages/clc.adoc @@ -14,7 +14,7 @@ The shell defaults to running SQL queries, which you can execute from a command [source,clc] ---- -CLC> select * from cities; +(default)> select * from cities; --------------------------------------------------------------------------------- __key | country | city | population --------------------------------------------------------------------------------- @@ -27,7 +27,7 @@ CLC> select * from cities; In order to run Hazelcast CLC commands, prefix them with a backslash (`\`): [source,clc] ---- -CLC> \object list --show-hidden +(default)> \object list --show-hidden ------------------------------------------------- Service Name | Object Name ------------------------------------------------- @@ -41,7 +41,7 @@ CLC> \object list --show-hidden You can enter multiline commands by ending all lines except the last line with a backslash (`\`): [source,clc] ---- -CLC> \object list \ +(default)> \object list \ ... --show-hidden ------------------------------------------------- Service Name | Object Name diff --git a/docs/modules/ROOT/pages/config-wizard.adoc b/docs/modules/ROOT/pages/config-wizard.adoc index 83b8b79a5..10b2914df 100644 --- a/docs/modules/ROOT/pages/config-wizard.adoc +++ b/docs/modules/ROOT/pages/config-wizard.adoc @@ -31,7 +31,7 @@ $ clc . Execute an operation that requires a client connection to cluster. + ```bash -CLC> SHOW MAPPINGS; +(default)> SHOW MAPPINGS; ``` + @@ -83,7 +83,7 @@ $ clc . Execute an operation that requires a client connection to cluster. + ```bash -CLC> SHOW MAPPINGS; +(default)> SHOW MAPPINGS; ``` + diff --git a/docs/modules/ROOT/pages/get-started.adoc b/docs/modules/ROOT/pages/get-started.adoc index fe62f7052..d5baae661 100644 --- a/docs/modules/ROOT/pages/get-started.adoc +++ b/docs/modules/ROOT/pages/get-started.adoc @@ -79,7 +79,7 @@ clc -c Test1 + [source, shell] ---- -CLC> \object list +(default)> \object list Connected to cluster: pr- ---- + diff --git a/docs/modules/ROOT/pages/overview.adoc b/docs/modules/ROOT/pages/overview.adoc index d3dbb5727..c5a5f8fd8 100644 --- a/docs/modules/ROOT/pages/overview.adoc +++ b/docs/modules/ROOT/pages/overview.adoc @@ -68,7 +68,7 @@ clc In the interactive mode, you can run SQL queries directly. Make sure to add a semicolumn (`;`) to the end of the query, otherwise it is assumed that you want to enter a multiline query. ---- -CLC> select * from cities; +(default)> select * from cities; --------------------------------------------------------------------------------- __key | country | city | population --------------------------------------------------------------------------------- @@ -80,7 +80,7 @@ CLC> select * from cities; In order to run CLC commands, prefix them with a backslash `\`: ---- -CLC> \object list +(default)> \object list ------------------------------------ Service Name | Object Name ------------------------------------ diff --git a/internal/str/str.go b/internal/str/str.go index a09dde44e..77332356a 100644 --- a/internal/str/str.go +++ b/internal/str/str.go @@ -1,6 +1,9 @@ package str -import "strings" +import ( + "fmt" + "strings" +) // SplitByComma splits a string by commas, and optionally removes empty items. func SplitByComma(str string, removeEmpty bool) []string { @@ -27,3 +30,10 @@ func ParseKeyValue(kvStr string) (string, string) { } return strings.TrimSpace(kvStr[:idx]), strings.TrimSpace(kvStr[idx+1:]) } + +func MaybeShorten(s string, l int) string { + if len(s) < 3 || len(s) < l { + return s + } + return fmt.Sprintf("%s...", s[:l]) +} From e0a4dba29d60bdec4e46f16b0aeda4a7f4ee00fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 11 Aug 2023 11:17:50 +0300 Subject: [PATCH 18/79] Trivial update to the prompt (#335) --- README.md | 4 ++-- base/commands/shell.go | 2 +- docs/modules/ROOT/pages/clc-sql.adoc | 2 +- docs/modules/ROOT/pages/clc.adoc | 6 +++--- docs/modules/ROOT/pages/config-wizard.adoc | 4 ++-- docs/modules/ROOT/pages/get-started.adoc | 2 +- docs/modules/ROOT/pages/overview.adoc | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a2f7d0698..01ec79125 100644 --- a/README.md +++ b/README.md @@ -150,12 +150,12 @@ $ clc Run SQL commands: ``` -(default)> select * from cities; +> select * from cities; ``` Run CLC commands: ``` -(default)> \map set my-key my-value +> \map set my-key my-value ``` ### Keyboard Shortcuts diff --git a/base/commands/shell.go b/base/commands/shell.go index 5e78396f4..6b5db6214 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -116,7 +116,7 @@ func makePrompt(cfgPath string) string { cfgPath = p } pd := paths.ParentDir(cfgPath) - return fmt.Sprintf("(%s)> ", str.MaybeShorten(pd, 12)) + return fmt.Sprintf("%s> ", str.MaybeShorten(pd, 12)) } func (*ShellCommand) Unwrappable() {} diff --git a/docs/modules/ROOT/pages/clc-sql.adoc b/docs/modules/ROOT/pages/clc-sql.adoc index 6c10b79a4..287ce0f62 100644 --- a/docs/modules/ROOT/pages/clc-sql.adoc +++ b/docs/modules/ROOT/pages/clc-sql.adoc @@ -58,7 +58,7 @@ Interactive mode:: [source,bash] ---- clc -(default)> SELECT * FROM cities +> SELECT * FROM cities --------------------------------------------------------------------------------- __key | country | city | population --------------------------------------------------------------------------------- diff --git a/docs/modules/ROOT/pages/clc.adoc b/docs/modules/ROOT/pages/clc.adoc index 0cf08c6f5..9d1bf209a 100644 --- a/docs/modules/ROOT/pages/clc.adoc +++ b/docs/modules/ROOT/pages/clc.adoc @@ -14,7 +14,7 @@ The shell defaults to running SQL queries, which you can execute from a command [source,clc] ---- -(default)> select * from cities; +> select * from cities; --------------------------------------------------------------------------------- __key | country | city | population --------------------------------------------------------------------------------- @@ -27,7 +27,7 @@ The shell defaults to running SQL queries, which you can execute from a command In order to run Hazelcast CLC commands, prefix them with a backslash (`\`): [source,clc] ---- -(default)> \object list --show-hidden +> \object list --show-hidden ------------------------------------------------- Service Name | Object Name ------------------------------------------------- @@ -41,7 +41,7 @@ In order to run Hazelcast CLC commands, prefix them with a backslash (`\`): You can enter multiline commands by ending all lines except the last line with a backslash (`\`): [source,clc] ---- -(default)> \object list \ +> \object list \ ... --show-hidden ------------------------------------------------- Service Name | Object Name diff --git a/docs/modules/ROOT/pages/config-wizard.adoc b/docs/modules/ROOT/pages/config-wizard.adoc index 10b2914df..12c766d24 100644 --- a/docs/modules/ROOT/pages/config-wizard.adoc +++ b/docs/modules/ROOT/pages/config-wizard.adoc @@ -31,7 +31,7 @@ $ clc . Execute an operation that requires a client connection to cluster. + ```bash -(default)> SHOW MAPPINGS; +> SHOW MAPPINGS; ``` + @@ -83,7 +83,7 @@ $ clc . Execute an operation that requires a client connection to cluster. + ```bash -(default)> SHOW MAPPINGS; +> SHOW MAPPINGS; ``` + diff --git a/docs/modules/ROOT/pages/get-started.adoc b/docs/modules/ROOT/pages/get-started.adoc index d5baae661..c8b9321fe 100644 --- a/docs/modules/ROOT/pages/get-started.adoc +++ b/docs/modules/ROOT/pages/get-started.adoc @@ -79,7 +79,7 @@ clc -c Test1 + [source, shell] ---- -(default)> \object list +> \object list Connected to cluster: pr- ---- + diff --git a/docs/modules/ROOT/pages/overview.adoc b/docs/modules/ROOT/pages/overview.adoc index c5a5f8fd8..a2dcb0e65 100644 --- a/docs/modules/ROOT/pages/overview.adoc +++ b/docs/modules/ROOT/pages/overview.adoc @@ -68,7 +68,7 @@ clc In the interactive mode, you can run SQL queries directly. Make sure to add a semicolumn (`;`) to the end of the query, otherwise it is assumed that you want to enter a multiline query. ---- -(default)> select * from cities; +> select * from cities; --------------------------------------------------------------------------------- __key | country | city | population --------------------------------------------------------------------------------- @@ -80,7 +80,7 @@ In the interactive mode, you can run SQL queries directly. Make sure to add a se In order to run CLC commands, prefix them with a backslash `\`: ---- -(default)> \object list +> \object list ------------------------------------ Service Name | Object Name ------------------------------------ From 7738a3e60d0d1f969f920f62d1d537f9f8f0a905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 11 Aug 2023 11:33:35 +0300 Subject: [PATCH 19/79] Viridian Updates (#334) * Increase the Viridian clusters per page * Trivial * Config import related fixes and updates * Import Java and .Net client certificates * Added set test * Import JSON config --- base/commands/viridian/common.go | 85 +++++++++++++++++++ .../viridian/viridian_cluster_create.go | 45 +++------- .../viridian/viridian_import_config.go | 39 ++++----- clc/config/config.go | 26 +++++- clc/config/import.go | 30 ++++++- clc/paths/paths.go | 6 ++ clc/paths/paths_test.go | 36 ++++++++ internal/types/types.go | 27 ++++++ internal/types/types_test.go | 42 +++++++++ internal/viridian/api.go | 14 +-- internal/viridian/cluster.go | 2 +- 11 files changed, 285 insertions(+), 67 deletions(-) create mode 100644 internal/types/types_test.go diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 0f5204076..546167b7f 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -3,9 +3,11 @@ package viridian import ( + "archive/zip" "context" "errors" "fmt" + "io" "net/http" "os" "path/filepath" @@ -13,9 +15,12 @@ import ( "strings" "time" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) @@ -129,6 +134,86 @@ 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") + if err != nil { + return nil, err + } + defer stop() + cfgPath, err := config.CreateFromZip(ctx, ec, cfgName, zipPath) + if err != nil { + return nil, err + } + cfgDir, _ := filepath.Split(cfgPath) + // import the Java/.Net certificates + zipPath, stop, err = api.DownloadConfig(ctx, clusterID, "java") + if err != nil { + return nil, err + } + defer stop() + fns := types.NewSet("client.keystore", "client.pfx", "client.truststore") + imp, err := importFileFromZip(ctx, ec, fns, zipPath, cfgDir) + if err != nil { + return nil, err + } + if imp.Len() != fns.Len() { + ec.Logger().Warn("Could not import all artifacts") + } + return cfgPath, nil + }) + if err != nil { + return "", err + } + stop() + cp := cpv.(string) + ec.Logger().Info("Imported configuration %s and saved to %s", cfgName, cp) + ec.PrintlnUnnecessary(fmt.Sprintf("OK Imported configuration %s", cfgName)) + return cp, 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) { + s := types.NewSet[string]() + zr, err := zip.OpenReader(zipPath) + if err != nil { + return nil, err + } + defer zr.Close() + for _, rf := range zr.File { + if ctx.Err() != nil { + return nil, ctx.Err() + } + _, fn := filepath.Split(rf.Name) + if selectPaths.Has(fn) { + if err := copyZipFile(rf, paths.Join(targetDir, fn)); err != nil { + ec.Logger().Error(fmt.Errorf("extracting file: %w", err)) + continue + } + s.Add(fn) + } + } + return s, nil +} + +func copyZipFile(file *zip.File, path string) error { + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + r, err := file.Open() + if err != nil { + return err + } + defer r.Close() + if _, err = io.Copy(f, r); err != nil { + return err + } + return nil +} + func matchClusterState(cluster viridian.Cluster, state string) (bool, error) { if cluster.State == state { return true, nil diff --git a/base/commands/viridian/viridian_cluster_create.go b/base/commands/viridian/viridian_cluster_create.go index 5cbeb0355..787cf8b7e 100644 --- a/base/commands/viridian/viridian_cluster_create.go +++ b/base/commands/viridian/viridian_cluster_create.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/config" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -58,7 +57,19 @@ func (cm ClusterCreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error } stop() c := csi.(viridian.Cluster) - tryImportConfig(ctx, ec, api, c) + ec.PrintlnUnnecessary(fmt.Sprintf("Cluster %s was created.", c.Name)) + _, stop, err = ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText("Waiting for the cluster to get ready") + if err := waitClusterState(ctx, ec, api, c.ID, stateRunning); err != nil { + // do not import the config and exit early + return nil, err + } + return nil, nil + }) + if _, err = tryImportConfig(ctx, ec, api, c.ID, c.Name); err != nil { + err = handleErrorResponse(ec, err) + ec.PrintlnUnnecessary(fmt.Sprintf("FAIL Could not import cluster configuration: %s", err.Error())) + } verbose := ec.Props().GetBool(clc.PropertyVerbose) if verbose { row := output.Row{ @@ -89,36 +100,6 @@ func getFirstAvailableK8sCluster(ctx context.Context, api *viridian.API) (viridi return clusters[0], nil } -func tryImportConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, cluster viridian.Cluster) { - cfgName := cluster.Name - cp, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Waiting for the cluster to get ready") - if err := waitClusterState(ctx, ec, api, cluster.ID, stateRunning); err != nil { - // do not import the config and exit early - return nil, err - } - sp.SetText("Importing configuration") - zipPath, stop, err := api.DownloadConfig(ctx, cluster.ID) - if err != nil { - return nil, err - } - defer stop() - cfgPath, err := config.CreateFromZip(ctx, ec, cfgName, zipPath) - if err != nil { - return nil, err - } - return cfgPath, nil - }) - if err != nil { - ec.Logger().Error(err) - return - } - stop() - ec.PrintlnUnnecessary(fmt.Sprintf("Cluster %s was created.", cluster.Name)) - ec.Logger().Info("Imported configuration %s and saved to: %s", cfgName, cp) - ec.PrintlnUnnecessary(fmt.Sprintf("Imported configuration: %s", cfgName)) -} - func init() { Must(plug.Registry.RegisterCommand("viridian:create-cluster", &ClusterCreateCmd{})) } diff --git a/base/commands/viridian/viridian_import_config.go b/base/commands/viridian/viridian_import_config.go index 98bece065..ee0d26346 100644 --- a/base/commands/viridian/viridian_import_config.go +++ b/base/commands/viridian/viridian_import_config.go @@ -6,15 +6,14 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/config" + hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) type ImportConfigCmd struct{} -func (cmd ImportConfigCmd) Init(cc plug.InitContext) error { +func (ImportConfigCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("import-config [cluster-name/cluster-ID] [flags]") long := `Imports connection configuration of the given Viridian cluster. @@ -28,42 +27,36 @@ Make sure you login before running this command. return nil } -func (cmd ImportConfigCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm ImportConfigCmd) Exec(ctx context.Context, ec plug.ExecContext) error { + if err := cm.exec(ctx, ec); err != nil { + ec.PrintlnUnnecessary(fmt.Sprintf("FAIL Could not import cluster configuration: %s", err.Error())) + return hzerrors.WrappedError{Err: err} + } + return nil +} + +func (ImportConfigCmd) exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err } clusterNameOrID := ec.Args()[0] - cluster, err := api.FindCluster(ctx, clusterNameOrID) + c, err := api.FindCluster(ctx, clusterNameOrID) if err != nil { return handleErrorResponse(ec, err) } cfgName := ec.Props().GetString(flagName) if cfgName == "" { - cfgName = cluster.Name + cfgName = c.Name } - cp, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Importing configuration") - zipPath, stop, err := api.DownloadConfig(ctx, cluster.ID) - if err != nil { - return nil, err - } - defer stop() - cfgPath, err := config.CreateFromZip(ctx, ec, cfgName, zipPath) - if err != nil { - return nil, err - } - return cfgPath, nil - }) - if err != nil { + if _, err = tryImportConfig(ctx, ec, api, c.ID, cfgName); err != nil { return handleErrorResponse(ec, err) } - stop() - ec.Logger().Info("Imported configuration %s and saved to: %s", cfgName, cp) - ec.PrintlnUnnecessary(fmt.Sprintf("Imported configuration: %s", cfgName)) return nil } +func (ImportConfigCmd) Unwrappable() {} + func init() { Must(plug.Registry.RegisterCommand("viridian:import-config", &ImportConfigCmd{})) } diff --git a/clc/config/config.go b/clc/config/config.go index f1af444d0..4e6bf4479 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -2,6 +2,7 @@ package config import ( "crypto/tls" + "encoding/json" "errors" "fmt" "os" @@ -26,6 +27,24 @@ const ( ) func Create(path string, opts clc.KeyValues[string, string]) (dir, cfgPath string, err error) { + return createFile(path, func(cfgPath string) (string, []byte, error) { + text := CreateYAML(opts) + return cfgPath, []byte(text), nil + }) +} + +func CreateJSON(path string, opts map[string]any) (dir, cfgPath string, err error) { + return createFile(path, func(cfgPath string) (string, []byte, error) { + cfgPath = paths.ReplaceExt(cfgPath, ".json") + b, err := json.MarshalIndent(opts, "", " ") + if err != nil { + return "", nil, err + } + return cfgPath, b, nil + }) +} + +func createFile(path string, f func(string) (string, []byte, error)) (dir, cfgPath string, err error) { dir, cfgPath, err = DirAndFile(path) if err != nil { return "", "", err @@ -33,9 +52,12 @@ func Create(path string, opts clc.KeyValues[string, string]) (dir, cfgPath strin if err := os.MkdirAll(dir, 0700); err != nil { return "", "", err } - text := CreateYAML(opts) + cfgPath, b, err := f(cfgPath) + if err != nil { + return "", "", err + } path = filepath.Join(dir, cfgPath) - if err := os.WriteFile(path, []byte(text), 0600); err != nil { + if err := os.WriteFile(path, b, 0600); err != nil { return "", "", err } return dir, cfgPath, nil diff --git a/clc/config/import.go b/clc/config/import.go index 3af2072ff..b60c0047c 100644 --- a/clc/config/import.go +++ b/clc/config/import.go @@ -126,8 +126,9 @@ func download(ctx context.Context, ec plug.ExecContext, url string) (string, err } func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string) (string, 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 files from the sample") + sp.SetText("Extracting configuration files") reader, err := zip.OpenReader(path) if err != nil { return nil, err @@ -158,6 +159,12 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string if err != nil { return nil, err } + mopts := makeViridianOptsMap(clusterName, token, pw, apiBase) + // ignoring the JSON path for now + _, _, err = CreateJSON(target, mopts) + if err != nil { + ec.Logger().Warn("Failed creating the JSON configuration: %s", err.Error()) + } // copy pem files if err := copyFiles(ec, pemFiles, outDir); err != nil { return nil, err @@ -183,6 +190,24 @@ func makeViridianOpts(clusterName, token, password, apiBaseURL string) clc.KeyVa } } +func makeViridianOptsMap(clusterName, token, password, apiBaseURL string) map[string]any { + cm := map[string]any{ + "name": clusterName, + "discovery-token": token, + "api-base": apiBaseURL, + } + ssl := map[string]any{ + "ca-path": "ca.pem", + "cert-path": "cert.pem", + "key-path": "key.pem", + "key-password": password, + } + return map[string]any{ + "cluster": cm, + "ssl": ssl, + } +} + func extractConfigFields(reader *zip.ReadCloser, pyPaths []string) (token, clusterName, pw, apiBase string, cfgFound bool) { for _, p := range pyPaths { rc, err := reader.Open(p) @@ -219,7 +244,8 @@ func extractConfigFields(reader *zip.ReadCloser, pyPaths []string) (token, clust func copyFiles(ec plug.ExecContext, files []*zip.File, outDir string) error { for _, rf := range files { _, outFn := filepath.Split(rf.Name) - f, err := os.Create(paths.Join(outDir, outFn)) + path := paths.Join(outDir, outFn) + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } diff --git a/clc/paths/paths.go b/clc/paths/paths.go index a50f5f090..9685b5674 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -192,3 +192,9 @@ func ParentDir(path string) string { dirs := filepath.Dir(path) return filepath.Base(dirs) } + +// ReplaceExt removes path's extension and appends ext +func ReplaceExt(path string, ext string) string { + p, _ := SplitExt(path) + return p + ext +} diff --git a/clc/paths/paths_test.go b/clc/paths/paths_test.go index 450e89ebc..1ee1c1851 100644 --- a/clc/paths/paths_test.go +++ b/clc/paths/paths_test.go @@ -149,3 +149,39 @@ func TestJoin(t *testing.T) { }) } } + +func TestReplaceExt(t *testing.T) { + testCases := []struct { + in string + ext string + out string + }{ + { + in: "", + ext: "", + out: "", + }, + { + in: "foo.txt", + ext: "", + out: "foo", + }, + { + in: "foo.txt", + ext: ".mp3", + out: "foo.mp3", + }, + { + in: "/dev/shm/foo.txt", + ext: ".mp3", + out: "/dev/shm/foo.mp3", + }, + } + for _, tc := range testCases { + tc := tc + name := fmt.Sprintf("%s/%s", tc.in, tc.ext) + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.out, paths.ReplaceExt(tc.in, tc.ext)) + }) + } +} diff --git a/internal/types/types.go b/internal/types/types.go index 9859f18ba..4c208bad2 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -11,3 +11,30 @@ func MakeTuple2[A, B any](first A, second B) Tuple2[A, B] { Second: second, } } + +type Set[K comparable] struct { + m map[K]struct{} +} + +func NewSet[K comparable](items ...K) *Set[K] { + s := Set[K]{ + m: map[K]struct{}{}, + } + for _, v := range items { + s.Add(v) + } + return &s +} + +func (s *Set[K]) Add(item K) { + s.m[item] = struct{}{} +} + +func (s *Set[K]) Has(item K) bool { + _, ok := s.m[item] + return ok +} + +func (s *Set[K]) Len() int { + return len(s.m) +} diff --git a/internal/types/types_test.go b/internal/types/types_test.go new file mode 100644 index 000000000..14b2df575 --- /dev/null +++ b/internal/types/types_test.go @@ -0,0 +1,42 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/hazelcast/hazelcast-commandline-client/internal/types" +) + +func TestTypes(t *testing.T) { + testCases := []struct { + name string + f func(t *testing.T) + }{ + {name: "setAdd", f: setAddTest}, + {name: "setEmpty", f: setEmptyTest}, + {name: "setNew", f: setNewTest}, + } + for _, tc := range testCases { + t.Run(tc.name, tc.f) + } +} + +func setEmptyTest(t *testing.T) { + s := types.NewSet[string]() + assert.Equal(t, 0, s.Len()) +} + +func setNewTest(t *testing.T) { + s := types.NewSet[string]("foo", "bar") + assert.Equal(t, 2, s.Len()) + assert.True(t, s.Has("foo")) + assert.True(t, s.Has("bar")) +} + +func setAddTest(t *testing.T) { + s := types.NewSet[string]() + s.Add("foo") + assert.Equal(t, 1, s.Len()) + assert.True(t, s.Has("foo")) +} diff --git a/internal/viridian/api.go b/internal/viridian/api.go index 69499c672..d02ce24a6 100644 --- a/internal/viridian/api.go +++ b/internal/viridian/api.go @@ -89,9 +89,6 @@ func (a *API) UploadCustomClasses(ctx context.Context, p func(progress float32), } return nil, nil }) - if err != nil { - return err - } if err != nil { return fmt.Errorf("uploading custom class: %w", err) } @@ -150,8 +147,8 @@ func (a *API) DeleteCustomClass(ctx context.Context, cluster string, artifact st return nil } -func (a *API) DownloadConfig(ctx context.Context, clusterID string) (path string, stop func(), err error) { - url := makeConfigURL(a.APIBaseURL, clusterID) +func (a *API) DownloadConfig(ctx context.Context, clusterID, language string) (path string, stop func(), err error) { + url := makeConfigURL(a.APIBaseURL, clusterID, language) r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (types.Tuple2[string, func()], error) { path, stop, err = download(ctx, url, a.Token) if err != nil { @@ -159,6 +156,9 @@ func (a *API) DownloadConfig(ctx context.Context, clusterID string) (path string } return types.MakeTuple2(path, stop), nil }) + if err != nil { + return "", nil, err + } return r.First, r.Second, nil } @@ -425,8 +425,8 @@ func download(ctx context.Context, url, token string) (downloadPath string, stop return "", nil, fmt.Errorf("%d: %s", rawRes.StatusCode, string(rb)) } -func makeConfigURL(base, clusterID string) string { - return fmt.Sprintf("%s/client_samples/%s/python?source_identifier=default", base, clusterID) +func makeConfigURL(base, clusterID string, language string) string { + return fmt.Sprintf("%s/client_samples/%s/%s?source_identifier=default", base, clusterID, language) } func APIClass() string { diff --git a/internal/viridian/cluster.go b/internal/viridian/cluster.go index bf33c5de0..c7a46df50 100644 --- a/internal/viridian/cluster.go +++ b/internal/viridian/cluster.go @@ -84,7 +84,7 @@ func (a *API) StopCluster(ctx context.Context, idOrName string) error { func (a *API) ListClusters(ctx context.Context) ([]Cluster, error) { csw, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Wrapper[[]Cluster], error) { - u := a.makeURL("/cluster") + u := a.makeURL("/cluster?size=500") return doGet[Wrapper[[]Cluster]](ctx, u, a.Token) }) if err != nil { From db7c0208e327b493678197338edecc137f8969f1 Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:36:04 +0300 Subject: [PATCH 20/79] Add demo generate-data command [CLC-206] (#289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yüce Tekol --- base/commands/demo/const.go | 10 + base/commands/demo/demo.go | 30 +++ base/commands/demo/demo_generate_data.go | 244 +++++++++++++++++++++++ base/commands/demo/demo_it_test.go | 55 +++++ base/commands/project/utils.go | 2 +- clc/sql/sql.go | 3 +- cmd/clc/imports.go | 1 + docs/modules/ROOT/pages/clc-demo.adoc | 65 ++++++ internal/demo/event.go | 11 + internal/demo/sql_mapping.go | 69 +++++++ internal/demo/wikimedia/event.go | 118 +++++++++++ internal/demo/wikimedia/generate.go | 69 +++++++ internal/sse/client.go | 59 ++++++ internal/sse/event.go | 109 ++++++++++ internal/sse/event_test.go | 58 ++++++ 15 files changed, 901 insertions(+), 2 deletions(-) create mode 100644 base/commands/demo/const.go create mode 100644 base/commands/demo/demo.go create mode 100644 base/commands/demo/demo_generate_data.go create mode 100644 base/commands/demo/demo_it_test.go create mode 100644 docs/modules/ROOT/pages/clc-demo.adoc create mode 100644 internal/demo/event.go create mode 100644 internal/demo/sql_mapping.go create mode 100644 internal/demo/wikimedia/event.go create mode 100644 internal/demo/wikimedia/generate.go create mode 100644 internal/sse/client.go create mode 100644 internal/sse/event.go create mode 100644 internal/sse/event_test.go diff --git a/base/commands/demo/const.go b/base/commands/demo/const.go new file mode 100644 index 000000000..fb05a574c --- /dev/null +++ b/base/commands/demo/const.go @@ -0,0 +1,10 @@ +//go:build std || demo + +package demo + +const ( + GroupDemoID = "demo" + flagPreview = "preview" + flagMaxValues = "max-values" + pairMapName = "map" +) diff --git a/base/commands/demo/demo.go b/base/commands/demo/demo.go new file mode 100644 index 000000000..d9135f4a7 --- /dev/null +++ b/base/commands/demo/demo.go @@ -0,0 +1,30 @@ +//go:build std || demo + +package demo + +import ( + "context" + + . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" +) + +type Cmd struct{} + +func (cm *Cmd) Init(cc plug.InitContext) error { + cc.AddCommandGroup(GroupDemoID, "Demonstrations") + cc.SetCommandGroup(GroupDemoID) + cc.SetTopLevel(true) + help := "Demonstration commands" + cc.SetCommandUsage("demo [command]") + cc.SetCommandHelp(help, help) + return nil +} + +func (cm *Cmd) Exec(context.Context, plug.ExecContext) error { + return nil +} + +func init() { + Must(plug.Registry.RegisterCommand("demo", &Cmd{})) +} diff --git a/base/commands/demo/demo_generate_data.go b/base/commands/demo/demo_generate_data.go new file mode 100644 index 000000000..7db0262de --- /dev/null +++ b/base/commands/demo/demo_generate_data.go @@ -0,0 +1,244 @@ +//go:build std || demo + +package demo + +import ( + "context" + "encoding/json" + "fmt" + "math" + "sync/atomic" + "time" + + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/serialization" + + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/sql" + . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/demo" + "github.com/hazelcast/hazelcast-commandline-client/internal/demo/wikimedia" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" +) + +type DataStreamGenerator interface { + Stream(ctx context.Context) (chan demo.StreamItem, context.CancelFunc) + MappingQuery(mapName string) (string, error) +} + +var supportedEventStreams = map[string]DataStreamGenerator{ + "wikipedia-event-stream": wikimedia.StreamGenerator{}, +} + +type GenerateDataCmd struct{} + +func (cm GenerateDataCmd) Init(cc plug.InitContext) error { + cc.SetCommandUsage("generate-data [name] [key=value, ...] [--preview]") + long := `Generates a stream of events + +Generate data for given name, supported names are: + +* wikipedia-event-stream: Real-time Wikipedia event stream. + Following key-value pairs can be set: + * map=: the target map to update with the generated stream entries. + +` + short := "Generates a stream of events" + cc.SetCommandHelp(long, short) + cc.SetPositionalArgCount(1, math.MaxInt) + cc.AddIntFlag(flagMaxValues, "", 0, false, "number of events to create (default: 0, no limits)") + cc.AddBoolFlag(flagPreview, "", false, false, "print the generated data without interacting with the cluster") + return nil +} + +func (cm GenerateDataCmd) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Args()[0] + generator, ok := supportedEventStreams[name] + if !ok { + return fmt.Errorf("stream generator '%s' is not supported, run --help to see supported ones", name) + } + keyVals := keyValMap(ec) + ch, stopStream := generator.Stream(ctx) + defer stopStream() + preview := ec.Props().GetBool(flagPreview) + if preview { + return generatePreviewResult(ctx, ec, generator, ch, keyVals, stopStream) + } + return generateResult(ctx, ec, generator, ch, keyVals, stopStream) +} + +func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator DataStreamGenerator, itemCh <-chan demo.StreamItem, keyVals map[string]string, stopStream context.CancelFunc) error { + maxCount := ec.Props().GetInt(flagMaxValues) + if maxCount < 1 { + maxCount = 10 + } + mapName := keyVals[pairMapName] + if mapName == "" { + mapName = "" + } + mq, err := generator.MappingQuery(mapName) + if err != nil { + return err + } + ec.PrintlnUnnecessary(fmt.Sprintf("Following mapping will be created when run without preview:\n\n%s", mq)) + ec.PrintlnUnnecessary("Generating preview items...") + outCh := make(chan output.Row) + count := int64(0) + go func() { + loop: + for count < maxCount { + var ev demo.StreamItem + select { + case event, ok := <-itemCh: + if !ok { + break loop + } + ev = event + case <-ctx.Done(): + break loop + } + select { + case outCh <- ev.Row(): + case <-ctx.Done(): + break loop + } + count++ + } + close(outCh) + stopStream() + }() + return ec.AddOutputStream(ctx, outCh) +} + +func generateResult(ctx context.Context, ec plug.ExecContext, generator DataStreamGenerator, itemCh <-chan demo.StreamItem, keyVals map[string]string, stopStream context.CancelFunc) error { + mapName, ok := keyVals[pairMapName] + if !ok { + return fmt.Errorf("%s key-value pair must be given", pairMapName) + } + m, err := getMap(ctx, ec, mapName) + if err != nil { + return err + } + query, err := generator.MappingQuery(mapName) + if err != nil { + return err + } + err = runMappingQuery(ctx, ec, mapName, query) + if err != nil { + return err + } + ec.PrintlnUnnecessary(fmt.Sprintf("Following mapping is created:\n\n%s", query)) + ec.PrintlnUnnecessary(fmt.Sprintf(`Run the following SQL query to see the generated data + + SELECT + __key, meta_dt as "timestamp", user_name, comment + FROM "%s" + LIMIT 10; + +`, m.Name())) + maxCount := ec.Props().GetInt(flagMaxValues) + count := int64(0) + errCh := make(chan error) + go func() { + loop: + for { + var ev demo.StreamItem + select { + case event, ok := <-itemCh: + if !ok { + errCh <- nil + break loop + } + ev = event + case <-ctx.Done(): + errCh <- ctx.Err() + break loop + } + fm := ev.KeyValues() + b, err := json.Marshal(fm) + if err != nil { + ec.Logger().Warn("Could not marshall stream item: %s", err.Error()) + continue + } + _, err = m.Put(ctx, ev.ID(), serialization.JSON(b)) + if err != nil { + ec.Logger().Warn("Could not put stream item into map %s: %s", m.Name(), err.Error()) + continue + } + atomic.AddInt64(&count, 1) + if maxCount > 0 && atomic.LoadInt64(&count) == maxCount { + errCh <- nil + break + } + } + close(errCh) + }() + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { + select { + case err := <-errCh: + return nil, err + case <-ticker.C: + sp.SetText(fmt.Sprintf("Generated %d events", atomic.LoadInt64(&count))) + } + } + }) + stop() + stopStream() + ec.PrintlnUnnecessary(fmt.Sprintf("Generated %d events", atomic.LoadInt64(&count))) + return err +} + +func runMappingQuery(ctx context.Context, ec plug.ExecContext, mapName, query string) error { + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText(fmt.Sprintf("Creating mapping for map: %s", mapName)) + _, cancel, err := sql.ExecSQL(ctx, ec, query) + if err != nil { + return nil, err + } + cancel() + return nil, nil + }) + stop() + return err +} + +func getMap(ctx context.Context, ec plug.ExecContext, mapName string) (*hazelcast.Map, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + mv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText(fmt.Sprintf("Getting map %s", mapName)) + m, err := ci.Client().GetMap(ctx, mapName) + if err != nil { + return nil, err + } + return m, nil + }) + if err != nil { + return nil, err + } + stop() + return mv.(*hazelcast.Map), nil +} + +func keyValMap(ec plug.ExecContext) map[string]string { + keyVals := map[string]string{} + for _, keyval := range ec.Args()[1:] { + k, v := str.ParseKeyValue(keyval) + if k == "" { + continue + } + keyVals[k] = v + } + return keyVals +} + +func init() { + Must(plug.Registry.RegisterCommand("demo:generate-data", &GenerateDataCmd{})) +} diff --git a/base/commands/demo/demo_it_test.go b/base/commands/demo/demo_it_test.go new file mode 100644 index 000000000..91a78a0b8 --- /dev/null +++ b/base/commands/demo/demo_it_test.go @@ -0,0 +1,55 @@ +//go:build std || demo + +package demo_test + +import ( + "context" + "fmt" + "testing" + + hz "github.com/hazelcast/hazelcast-go-client" + "github.com/stretchr/testify/require" + + _ "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/it" +) + +func TestGenerateData(t *testing.T) { + testCases := []struct { + name string + f func(t *testing.T) + }{ + {name: "generateData_Wikipedia", f: generateData_WikipediaTest}, + {name: "generateData_Wikipedia_MaxValues", f: generateData_Wikipedia_MaxValues_Test}, + } + for _, tc := range testCases { + t.Run(tc.name, tc.f) + } +} + +func generateData_WikipediaTest(t *testing.T) { + it.MapTester(t, func(tcx it.TestContext, m *hz.Map) { + t := tcx.T + ctx := context.Background() + tcx.WithReset(func() { + err := tcx.CLCExecuteErr(ctx, "demo", "generate-data", "wikipedia-event-stream", "map="+m.Name(), "--timeout", "2s") + require.Error(t, err) + size := check.MustValue(m.Size(context.Background())) + require.Greater(t, size, 0) + }) + }) +} + +func generateData_Wikipedia_MaxValues_Test(t *testing.T) { + it.MapTester(t, func(tcx it.TestContext, m *hz.Map) { + t := tcx.T + ctx := context.Background() + count := 10 + tcx.WithReset(func() { + tcx.CLCExecute(ctx, "demo", "generate-data", "wikipedia-event-stream", "map="+m.Name(), fmt.Sprintf("--max-values=%d", count)) + size := check.MustValue(m.Size(context.Background())) + require.Equal(t, count, size) + }) + }) +} diff --git a/base/commands/project/utils.go b/base/commands/project/utils.go index 565d4d7e0..307aeade0 100644 --- a/base/commands/project/utils.go +++ b/base/commands/project/utils.go @@ -80,7 +80,7 @@ func convertToSnakeCase(s string) string { } func validateValueMap[T any](m map[string]T) (invalid string, ok bool) { - for k, _ := range m { + for k := range m { if !regexpValidKey.MatchString(k) { return k, false } diff --git a/clc/sql/sql.go b/clc/sql/sql.go index 9e475ebbe..da2383c54 100644 --- a/clc/sql/sql.go +++ b/clc/sql/sql.go @@ -112,6 +112,7 @@ func UpdateOutput(ctx context.Context, ec plug.ExecContext, res sql.Result, verb go func() { var row sql.Row var err error + loop: for it.HasNext() { row, err = it.Next() if err != nil { @@ -131,7 +132,7 @@ func UpdateOutput(ctx context.Context, ec plug.ExecContext, res sql.Result, verb select { case rowCh <- orow: case <-ctx.Done(): - break + break loop } } close(rowCh) diff --git a/cmd/clc/imports.go b/cmd/clc/imports.go index 3ab12291b..04661ceec 100644 --- a/cmd/clc/imports.go +++ b/cmd/clc/imports.go @@ -4,6 +4,7 @@ import ( _ "github.com/hazelcast/hazelcast-commandline-client/base/commands" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/atomic_long" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/config" + _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/demo" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/job" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/list" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/map" diff --git a/docs/modules/ROOT/pages/clc-demo.adoc b/docs/modules/ROOT/pages/clc-demo.adoc new file mode 100644 index 000000000..d5e090bf0 --- /dev/null +++ b/docs/modules/ROOT/pages/clc-demo.adoc @@ -0,0 +1,65 @@ += clc demo + +This command group provides commands for creating and preparing demos. + +Usage: + +[source,bash] +---- +clc demo [command] [options] +---- + +== Commands + +* <> + +== clc demo submit + +Generates stream events + +Generate data for given name, supported names are: + +- wikipedia-event-stream: Real-time Wikipedia event stream. Following key-value pairs can be set + - map=: generated stream items are written into the map + +Usage: + +[source,bash] +---- +clc demo generate-data [event-stream-name] [key=val, ...] [flags] +---- + +Parameters: + +[cols="1m,1a,2a,1a"] +|=== +|Parameter|Required|Description|Default + +|`event-stream-name` +|Required +|Name for the event stream source. Supported sources are mentioned above. +| + +|`key=val` +|Optional +|Key value pairs used by event stream sources. +| + +|`--preview` +|Optional +|Print the generated data without interacting with the cluster. +| + +|`--max-values` +|Optional +|Number of events to create. +| + +|=== + +Example: + +[source,bash] +---- +clc demo generate-data wikipedia-event-stream map=wiki-events --preview +---- diff --git a/internal/demo/event.go b/internal/demo/event.go new file mode 100644 index 000000000..7e992854d --- /dev/null +++ b/internal/demo/event.go @@ -0,0 +1,11 @@ +package demo + +import ( + "github.com/hazelcast/hazelcast-commandline-client/internal/output" +) + +type StreamItem interface { + ID() string + Row() output.Row + KeyValues() map[string]any +} diff --git a/internal/demo/sql_mapping.go b/internal/demo/sql_mapping.go new file mode 100644 index 000000000..8f76b921f --- /dev/null +++ b/internal/demo/sql_mapping.go @@ -0,0 +1,69 @@ +package demo + +import ( + "bytes" + "fmt" + "text/template" + "time" +) + +const addMapping = `CREATE OR REPLACE MAPPING "{{ .map_name }}" +( + {{ range $key, $val := .fields -}} + {{ $key }} {{ $val }}, + {{ end -}} + __key VARCHAR +) +TYPE IMap +OPTIONS ( + 'keyFormat' = 'varchar', + 'valueFormat' = 'json-flat' +); +` + +func GenerateMappingQuery(mapName string, fields map[string]any) (string, error) { + sqlFields := map[string]string{} + for k, v := range fields { + sqlFields[k] = findSqlType(v) + } + values := map[string]any{ + "map_name": mapName, + // template sorts map by key + "fields": sqlFields, + } + t, err := template.New("query").Parse(addMapping) + if err != nil { + return "", err + } + buf := new(bytes.Buffer) + err = t.Execute(buf, values) + if err != nil { + return "", err + } + return buf.String(), nil +} + +func findSqlType(v any) string { + switch t := v.(type) { + case bool: + return "BOOLEAN" + case string: + return "VARCHAR" + case int8, byte: + return "TINYINT" + case int16: + return "SMALLINT" + case int32: + return "INTEGER" + case int64: + return "BIGINT" + case float32: + return "REAL" + case float64: + return "DOUBLE" + case time.Time: + return "TIMESTAMP WITH TIME ZONE" + default: + panic(fmt.Sprintf("sql type conversion not supported for type %s", t)) + } +} diff --git a/internal/demo/wikimedia/event.go b/internal/demo/wikimedia/event.go new file mode 100644 index 000000000..45d1275b2 --- /dev/null +++ b/internal/demo/wikimedia/event.go @@ -0,0 +1,118 @@ +package wikimedia + +import ( + "time" + + "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" +) + +type event struct { + Schema string `json:"$schema,omitempty"` + Meta Meta `json:"meta,omitempty"` + ID_ int64 `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Namespace int64 `json:"namespace,omitempty"` + Title string `json:"title,omitempty"` + TitleURL string `json:"title_url,omitempty"` + Comment string `json:"comment,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + User string `json:"user,omitempty"` + Bot bool `json:"bot,omitempty"` + NotifyURL string `json:"notify_url,omitempty"` + Minor bool `json:"minor,omitempty"` + Length Length `json:"length,omitempty"` + Revision Revision `json:"revision,omitempty"` + ServerURL string `json:"server_url,omitempty"` + ServerName string `json:"server_name,omitempty"` + ServerScriptPath string `json:"server_script_path,omitempty"` + Wiki string `json:"wiki,omitempty"` + Parsedcomment string `json:"parsedcomment,omitempty"` +} + +type Meta struct { + URI string `json:"uri,omitempty"` + RequestID string `json:"request_id,omitempty"` + ID string `json:"id,omitempty"` + Timestamp time.Time `json:"dt,omitempty"` + Domain string `json:"domain,omitempty"` + Stream string `json:"stream,omitempty"` + Topic string `json:"topic,omitempty"` + Partition int64 `json:"partition,omitempty"` + Offset int64 `json:"offset,omitempty"` +} + +type Length struct { + Old int64 `json:"old,omitempty"` + New int64 `json:"new,omitempty"` +} + +type Revision struct { + Old int64 `json:"old,omitempty"` + New int64 `json:"new,omitempty"` +} + +func (ev event) ID() string { + return ev.Meta.ID +} + +func (ev event) Row() output.Row { + row := output.Row{ + output.Column{ + Name: "ID", + Type: serialization.TypeString, + Value: ev.Meta.ID, + }, + output.Column{ + Name: "Timestamp", + Type: serialization.TypeJavaOffsetDateTime, + Value: ev.Meta.Timestamp, + }, + output.Column{ + Name: "User", + Type: serialization.TypeString, + Value: ev.User, + }, + output.Column{ + Name: "Comment", + Type: serialization.TypeString, + Value: ev.Comment, + }, + } + return row +} + +func (ev event) KeyValues() map[string]any { + return map[string]any{ + "schema": ev.Schema, + "meta_uri": ev.Meta.URI, + "meta_request_id": ev.Meta.RequestID, + "meta_id": ev.Meta.ID, + "meta_dt": ev.Meta.Timestamp, + "meta_domain": ev.Meta.Domain, + "meta_stream": ev.Meta.Stream, + "meta_topic": ev.Meta.Topic, + "meta_partition": ev.Meta.Partition, + "meta_offset": ev.Meta.Offset, + "id": ev.ID_, + "type": ev.Type, + "namespace": ev.Namespace, + "title": ev.Title, + "title_url": ev.TitleURL, + "comment": ev.Comment, + "event_time": ev.Timestamp, + "user_name": ev.User, + "bot": ev.Bot, + "notify_url": ev.NotifyURL, + "minor": ev.Minor, + "length_old": ev.Length.Old, + "length_new": ev.Length.New, + "revision_old": ev.Revision.Old, + "revision_new": ev.Revision.New, + "server_url": ev.ServerURL, + "server_name": ev.ServerName, + "server_script_path": ev.ServerScriptPath, + "wiki": ev.Wiki, + "parsed_comment": ev.Parsedcomment, + } +} diff --git a/internal/demo/wikimedia/generate.go b/internal/demo/wikimedia/generate.go new file mode 100644 index 000000000..7b11a063b --- /dev/null +++ b/internal/demo/wikimedia/generate.go @@ -0,0 +1,69 @@ +package wikimedia + +import ( + "context" + "encoding/json" + "errors" + "os" + "os/signal" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/internal/demo" + "github.com/hazelcast/hazelcast-commandline-client/internal/sse" +) + +const ( + streamURL = "https://stream.wikimedia.org/v2/stream/recentchange" +) + +type StreamGenerator struct{} + +func (StreamGenerator) Stream(ctx context.Context) (chan demo.StreamItem, context.CancelFunc) { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, os.Kill) + itemCh := make(chan demo.StreamItem) + client := sse.NewClient(streamURL) + go func() { + // retry logic + for { + err := handleEvents(ctx, client, itemCh) + if err != nil { + if errors.Is(err, context.Canceled) { + break + } + if errors.Is(err, sse.ErrNoConnection) { + break + } + // Retry all other errors including EOF + time.Sleep(2 * time.Second) + continue + } + } + close(itemCh) + cancel() + }() + return itemCh, cancel +} + +func handleEvents(ctx context.Context, client *sse.Client, itemCh chan demo.StreamItem) error { + return client.SubscribeWithCallback(ctx, func(rawEv *sse.Event) error { + if rawEv == nil { + return nil + } + ev := event{} + err := json.Unmarshal(rawEv.Data, &ev) + if err != nil { + // XXX: should we log + return nil + } + select { + case itemCh <- ev: + case <-ctx.Done(): + return ctx.Err() + } + return nil + }) +} + +func (StreamGenerator) MappingQuery(mapName string) (string, error) { + return demo.GenerateMappingQuery(mapName, event{}.KeyValues()) +} diff --git a/internal/sse/client.go b/internal/sse/client.go new file mode 100644 index 000000000..3f9a22c97 --- /dev/null +++ b/internal/sse/client.go @@ -0,0 +1,59 @@ +package sse + +import ( + "context" + "errors" + "net/http" + + "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" +) + +var ( + ErrNoConnection = errors.New("no connection") +) + +type Client struct { + URL string + HTTPClient *http.Client +} + +func NewClient(url string) *Client { + return &Client{ + URL: url, + HTTPClient: &http.Client{}, + } +} + +func (c *Client) SubscribeWithCallback(ctx context.Context, process func(*Event) error) error { + resp, err := c.sendRequest(ctx) + if err != nil { + return ErrNoConnection + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return viridian.NewHTTPClientError(resp.StatusCode, nil) + } + reader := NewEventScanner(resp.Body) + for { + // can exit on context cancelation because uses response body + event, err := reader.ReadEvent() + if err != nil { + return err + } + if err := process(event); err != nil { + return err + } + } +} + +func (c *Client) sendRequest(ctx context.Context) (*http.Response, error) { + req, err := http.NewRequest("GET", c.URL, nil) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("Connection", "keep-alive") + return c.HTTPClient.Do(req) +} diff --git a/internal/sse/event.go b/internal/sse/event.go new file mode 100644 index 000000000..887a9ff15 --- /dev/null +++ b/internal/sse/event.go @@ -0,0 +1,109 @@ +package sse + +import ( + "bufio" + "bytes" + "io" +) + +var ( + eventSeparator = []byte("\n\n") + headerData = "data" +) + +type Event struct { + Data []byte + // Omitted fields: ID, Event, Retry +} + +func (e Event) Empty() bool { + return len(e.Data) == 0 +} + +func (e *Event) Unmarshal(msg []byte) { + var data []byte + // Normalize the new lines in the event + normalizedMsg := bytes.Replace(msg, []byte("\n\r"), []byte("\n"), -1) + lines := bytes.Split(normalizedMsg, []byte("\n")) + // Implementation spec is taken from: + // https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#fields + for _, line := range lines { + switch headerName(line) { + case headerData: + data = append(data, dataValue(line)...) + // append new line after each data line per spec + data = append(data, '\n') + default: + // Ignore all other headers. Note we omitted the "id", "event", and "retry" headers. + } + } + // Trim trailing new lines as specified in the spec + data = bytes.TrimRight(data, "\n") + e.Data = data +} + +func headerName(data []byte) string { + cIndex := bytes.Index(data, []byte(":")) + // If there is no colon, whole line is the header name + if cIndex == -1 { + return string(data) + } + return string(data[:cIndex]) +} + +func dataValue(data []byte) []byte { + cIndex := bytes.Index(data, []byte(":")) + if cIndex == -1 || len(data) == cIndex+1 { + return nil + } + data = data[cIndex+1:] + // remove the space after the colon + if len(data) > 0 && data[0] == ' ' { + data = data[1:] + } + return data +} + +type EventScanner struct { + scanner *bufio.Scanner +} + +func NewEventScanner(eventStream io.Reader) *EventScanner { + scanner := bufio.NewScanner(eventStream) + split := func(data []byte, isEOF bool) (int, []byte, error) { + if len(data) == 0 && isEOF { + return 0, nil, nil + } + // Send the event, we have found the event separator + if i := bytes.Index(data, eventSeparator); i >= 0 { + return i + len(eventSeparator), data[0:i], nil + } + // If we're at EOF, we have all of the data. + if isEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil + } + scanner.Split(split) + return &EventScanner{ + scanner: scanner, + } +} + +func (e *EventScanner) ReadEvent() (*Event, error) { + if !e.scanner.Scan() { + if err := e.scanner.Err(); err != nil { + return nil, err + } + // EOF is expected when the connection is closed by the server + return nil, io.EOF + } + event := e.scanner.Bytes() + var ev Event + ev.Unmarshal(event) + if ev.Empty() { + return nil, nil + } + return &ev, nil +} diff --git a/internal/sse/event_test.go b/internal/sse/event_test.go new file mode 100644 index 000000000..a42deab54 --- /dev/null +++ b/internal/sse/event_test.go @@ -0,0 +1,58 @@ +package sse + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEventUnmarshal(t *testing.T) { + testCases := []struct { + name string + given string + expectedData string + }{ + { + name: "without space", + given: "data:data1", + expectedData: "data1", + }, + { + name: "space after colon", + given: "data: data1", + expectedData: "data1", + }, + { + name: "data with other headers", + given: ":comment123\nid: id1\ndata: new_data\nretry: 1000", + expectedData: "new_data", + }, + { + name: "multiple data headers", + given: "data:new_data\ndata: new_data2", + expectedData: "new_data\nnew_data2", + }, + { + name: "empty data header", + given: "data:\ndata: new_data2", + expectedData: "\nnew_data2", + }, + { + name: "data header without colon", + given: "data\ndata: new_data2", + expectedData: "\nnew_data2", + }, + { + name: "erased trailing new lines", + given: "data: newdata\ndata:", + expectedData: "newdata", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ev := Event{} + ev.Unmarshal([]byte(tc.given)) + require.Equal(t, tc.expectedData, string(ev.Data)) + }) + } +} From e23ad315fbb8c14d036aaadd1541ede89fb7e81f Mon Sep 17 00:00:00 2001 From: Patrick McGleenon Date: Mon, 14 Aug 2023 08:00:17 +0100 Subject: [PATCH 21/79] Fixed unmarshalling error when IP Allowlist is configured on viridian (#339) 2023-08-11T19:47:32.962+0100 ERROR viridian/common.go:228 listing clusters: json: cannot unmarshal array into Go struct field Cluster.Content.allowedIps of type string Here's an example grabbed from the Viridian UI {id: 177, ip: "207.154.227.247", description: null} --- internal/viridian/types.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/viridian/types.go b/internal/viridian/types.go index 529e4cf38..3b6532719 100644 --- a/internal/viridian/types.go +++ b/internal/viridian/types.go @@ -18,7 +18,7 @@ type Cluster struct { HotRestartEnabled bool `json:"hotRestartEnabled"` PlanName string `json:"planName"` Regions []Region `json:"regions"` - AllowedIps []string `json:"allowedIps"` + AllowedIps []IP `json:"allowedIps"` IPWhitelistEnabled bool `json:"ipWhitelistEnabled"` MaxAvailableMemory int `json:"maxAvailableMemory"` } @@ -44,3 +44,9 @@ type ClusterType struct { type Region struct { Title string `json:"title"` } + +type IP struct { + ID int `json:"id"` + IP string `json:"ip"` + Description string `json:"description",omitempty` +} From db514cb3f37127c66a0ae10780a209c2c5664d91 Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:14:39 +0300 Subject: [PATCH 22/79] Add Badger as local store [CLC-234] (#309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yüce Tekol --- clc/store/store.go | 199 ++++++++++++++++++++++++++++++++++++++++ clc/store/store_test.go | 159 ++++++++++++++++++++++++++++++++ go.mod | 12 +++ go.sum | 77 ++++++++++++++++ internal/log/logger.go | 10 ++ 5 files changed, 457 insertions(+) create mode 100644 clc/store/store.go create mode 100644 clc/store/store_test.go diff --git a/clc/store/store.go b/clc/store/store.go new file mode 100644 index 000000000..ce6c3e2ec --- /dev/null +++ b/clc/store/store.go @@ -0,0 +1,199 @@ +package store + +import ( + "errors" + "sync" + + "github.com/dgraph-io/badger/v4" + + "github.com/hazelcast/hazelcast-commandline-client/internal/log" +) + +var ( + ErrKeyNotFound = errors.New("key not found") +) + +type StoreAccessor struct { + store *Store + mu sync.Mutex +} + +func NewStoreAccessor(dir string, logger log.Logger) *StoreAccessor { + return &StoreAccessor{ + store: newStore(dir, logger), + } +} + +func (sa *StoreAccessor) WithLock(fn func(s *Store) (any, error)) (any, error) { + sa.mu.Lock() + defer sa.mu.Unlock() + if err := sa.store.open(); err != nil { + return nil, err + } + defer sa.store.close() + return fn(sa.store) +} + +type Store struct { + dir string + db *badger.DB + logger log.Logger +} + +func newStore(dir string, logger log.Logger) *Store { + return &Store{ + dir: dir, + logger: logger, + } +} + +func (s *Store) open() error { + opts := badger.DefaultOptions(s.dir) + opts.Logger = badgerLogger{s.logger} + db, err := badger.Open(opts) + if err != nil { + return err + } + s.db = db + return nil +} + +type badgerLogger struct { + logger log.Logger +} + +func (l badgerLogger) Errorf(log string, args ...any) { + log = "Store ERROR: " + log + l.logger.Debugf(log, args...) +} +func (l badgerLogger) Warningf(log string, args ...any) { + log = "Store WARNING: " + log + l.logger.Debugf(log, args...) +} +func (l badgerLogger) Infof(log string, args ...any) { + log = "Store INFO: " + log + l.logger.Debugf(log, args...) +} +func (l badgerLogger) Debugf(log string, args ...any) { + log = "Store DEBUG: " + log + l.logger.Debugf(log, args...) +} + +func (s *Store) close() error { + return s.db.Close() +} + +func (s *Store) SetEntry(key, val []byte) error { + err := s.db.Update(func(txn *badger.Txn) error { + e := badger.NewEntry(key, val) + return txn.SetEntry(e) + }) + return err +} + +func (s *Store) GetEntry(key []byte) ([]byte, error) { + var item []byte + err := s.db.View(func(txn *badger.Txn) error { + it, err := txn.Get(key) + if err != nil { + if errors.Is(err, badger.ErrKeyNotFound) { + return ErrKeyNotFound + } + return err + } + item, err = it.ValueCopy(nil) + return err + }) + if err != nil { + return nil, err + } + return item, nil +} + +func (s *Store) GetKeysWithPrefix(prefix string) ([][]byte, error) { + keys := [][]byte{} + pb := []byte(prefix) + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false + err := s.db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(opts) + defer it.Close() + for it.Seek(pb); it.ValidForPrefix(pb); it.Next() { + c := it.Item().KeyCopy(nil) + keys = append(keys, c) + } + return nil + }) + if err != nil { + return nil, err + } + return keys, nil +} + +type UpdateFunc = func(current []byte, found bool) []byte + +func (s *Store) UpdateEntry(key []byte, f UpdateFunc) error { + var item []byte + err := s.db.Update(func(txn *badger.Txn) error { + it, err := txn.Get(key) + found := true + if err != nil { + if !errors.Is(err, badger.ErrKeyNotFound) { + return err + } + found = false + } else { + item, err = it.ValueCopy(nil) + if err != nil { + return err + } + } + newVal := f(item, found) + return txn.Set(key, newVal) + }) + return err +} + +type ForeachFunc = func(key, val []byte) (ok bool, err error) + +func (s *Store) RunForeachWithPrefix(prefix string, f ForeachFunc) error { + p := []byte(prefix) + err := s.db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + for it.Seek(p); it.ValidForPrefix(p); it.Next() { + item := it.Item() + // Note: We always copy key and value, if there is a performance issue this might be a reason + k := item.KeyCopy(nil) + v, err := item.ValueCopy(nil) + if err != nil { + return err + } + ok, err := f(k, v) + if err != nil { + return err + } + if !ok { + break + } + } + return nil + }) + return err +} + +func (s *Store) DeleteEntriesWithPrefix(prefix string) error { + keys, err := s.GetKeysWithPrefix(prefix) + if err != nil { + return err + } + err = s.db.Update(func(txn *badger.Txn) error { + for _, key := range keys { + if err := txn.Delete(key); err != nil { + return err + } + } + return nil + }) + return err +} diff --git a/clc/store/store_test.go b/clc/store/store_test.go new file mode 100644 index 000000000..0e4cd237d --- /dev/null +++ b/clc/store/store_test.go @@ -0,0 +1,159 @@ +package store + +import ( + "fmt" + "os" + "testing" + + badger "github.com/dgraph-io/badger/v4" + "github.com/stretchr/testify/require" + + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" +) + +func TestStore_GetSetEntry(t *testing.T) { + withStore(func(s *Store) { + check.Must(insertValues(s.db, map[string][]byte{ + "key1": []byte("val"), + })) + valb := check.MustValue(s.GetEntry([]byte("key1"))) + require.Equal(t, []byte("val"), valb) + check.Must(s.SetEntry([]byte("key1"), []byte("valnew"))) + valnew := check.MustValue(s.GetEntry([]byte("key1"))) + require.Equal(t, []byte("valnew"), valnew) + }) +} + +func TestStore_GetKeysWithPrefix(t *testing.T) { + withStore(func(s *Store) { + check.Must(insertValues(s.db, map[string][]byte{ + "prefix.key1": []byte(""), + "prefix.key2": []byte(""), + "noprefix": []byte(""), + })) + vals := check.MustValue(s.GetKeysWithPrefix("prefix")) + expected := [][]byte{[]byte("prefix.key1"), []byte("prefix.key2")} + require.ElementsMatch(t, expected, vals) + + }) +} + +func TestStore_UpdateEntry(t *testing.T) { + withStore(func(s *Store) { + check.Must(s.UpdateEntry([]byte("key"), func(current []byte, found bool) []byte { + if !found { + return []byte("notexist") + } + return nil + })) + valb := check.MustValue(s.GetEntry([]byte("key"))) + require.Equal(t, []byte("notexist"), valb) + check.Must(s.UpdateEntry([]byte("key"), func(current []byte, found bool) []byte { + if found { + return append(current, []byte(".nowexist")...) + } + return nil + })) + valnew := check.MustValue(s.GetEntry([]byte("key"))) + require.Equal(t, []byte("notexist.nowexist"), valnew) + }) +} + +func TestStore_RunForeachWithPrefix(t *testing.T) { + fromStore := make(map[string][]byte) + withStore(func(s *Store) { + check.Must(insertValues(s.db, map[string][]byte{ + "prefix.key1": []byte(""), + "prefix.key2": []byte(""), + "noprefix": []byte(""), + })) + check.Must(s.RunForeachWithPrefix("prefix", func(key, val []byte) (bool, error) { + fromStore[string(key)] = val + return true, nil + })) + expected := map[string][]byte{ + "prefix.key1": nil, + "prefix.key2": nil, + } + require.EqualValues(t, expected, fromStore) + }) +} + +func TestStore_DeleteEntriesWithPrefix(t *testing.T) { + withStore(func(s *Store) { + check.Must(insertValues(s.db, map[string][]byte{ + "prefix.key1": []byte(""), + "prefix.key2": []byte(""), + "noprefix": []byte(""), + })) + check.Must(s.DeleteEntriesWithPrefix("prefix")) + entries := check.MustValue(getAllEntries(s.db)) + expected := map[string][]byte{ + "noprefix": nil, + } + require.EqualValues(t, expected, entries) + }) +} + +func getAllEntries(db *badger.DB) (map[string][]byte, error) { + m := make(map[string][]byte) + err := db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchSize = 10 + it := txn.NewIterator(opts) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + k := item.Key() + b, err := item.ValueCopy(nil) + if err != nil { + return err + } + m[string(k)] = b + } + return nil + }) + if err != nil { + return nil, err + } + return m, nil +} + +func insertValues(db *badger.DB, vals map[string][]byte) error { + err := db.Update(func(txn *badger.Txn) error { + for k, v := range vals { + err := txn.SetEntry(badger.NewEntry([]byte(k), v)) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + return nil +} + +func WithTempDir(fn func(dir string)) { + dir, err := os.MkdirTemp("", "clc-store-*") + if err != nil { + panic(fmt.Errorf("creating temp dir: %w", err)) + } + defer func() { + // errors are ignored + os.RemoveAll(dir) + }() + fn(dir) +} + +func withStore(fn func(s *Store)) { + WithTempDir(func(dir string) { + s := NewStoreAccessor(dir, log.NopLogger{}) + s.WithLock(func(s *Store) (any, error) { + fn(s) + return nil, nil + }) + }) +} diff --git a/go.mod b/go.mod index f7df6434c..8e1dc3e42 100644 --- a/go.mod +++ b/go.mod @@ -28,20 +28,29 @@ require ( github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect github.com/go-ole/go-ole v1.2.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.3.1 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/google/flatbuffers v1.12.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.12.3 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -50,6 +59,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect @@ -59,6 +69,7 @@ require ( github.com/tklauser/go-sysconf v0.3.4 // indirect github.com/tklauser/numcpus v0.2.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + go.opencensus.io v0.22.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.11.0 // indirect @@ -78,6 +89,7 @@ require ( github.com/charmbracelet/bubbles v0.15.0 github.com/charmbracelet/bubbletea v0.23.2 github.com/charmbracelet/lipgloss v0.7.1 + github.com/dgraph-io/badger/v4 v4.1.0 github.com/fatih/color v1.13.0 github.com/go-git/go-git/v5 v5.8.1 github.com/mattn/go-colorable v0.1.12 diff --git a/go.sum b/go.sum index 73d9c185d..5ff34d845 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -29,6 +31,9 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI= github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74= github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= @@ -42,6 +47,7 @@ github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -51,8 +57,16 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4APOQ= +github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= @@ -68,10 +82,24 @@ github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+ github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c h1:yE35fKFwcelIte3q5q1/cPiY7pI7vvf5/j/0ddxNCKs= github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c/go.mod h1:9S/fKAutQ6wVHqm1jnp9D9sc5hu689s9AaTWFS92LaU= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 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= @@ -83,6 +111,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -170,7 +202,11 @@ github.com/tklauser/numcpus v0.2.1 h1:ct88eFm+Q7m2ZfXJdan1xYoXKlmwsfP+k88q05KvlZ github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= @@ -179,18 +215,33 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20221108223516-5d533826c662 h1:QIza2Vre5WI+NE5AQ6Wi2nGDgDOckLCHJdhcM/kxcfw= golang.org/x/exp v0.0.0-20221108223516-5d533826c662/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -199,14 +250,24 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -219,6 +280,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -242,11 +304,25 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -260,3 +336,4 @@ 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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/log/logger.go b/internal/log/logger.go index 51fc33edb..5fe475393 100644 --- a/internal/log/logger.go +++ b/internal/log/logger.go @@ -11,3 +11,13 @@ type Logger interface { Trace(f func() string) Log(weight hzlogger.Weight, f func() string) } + +type NopLogger struct{} + +func (NopLogger) Error(err error) {} +func (NopLogger) Warn(format string, args ...any) {} +func (NopLogger) Info(format string, args ...any) {} +func (NopLogger) Debug(func() string) {} +func (NopLogger) Debugf(format string, args ...any) {} +func (NopLogger) Trace(f func() string) {} +func (NopLogger) Log(weight hzlogger.Weight, f func() string) {} From 54ade817927673b3d76f2fc85d76a054e22a5a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 14 Aug 2023 17:25:25 +0300 Subject: [PATCH 23/79] Fixed the Jira workflow to use the CLC project (#341) --- .github/workflows/jira.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/jira.yml b/.github/workflows/jira.yml index 60a009cc1..785a35e63 100644 --- a/.github/workflows/jira.yml +++ b/.github/workflows/jira.yml @@ -15,6 +15,5 @@ jobs: JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - TARGET_JIRA_PROJECT: API - JIRA_LABEL: CLC + TARGET_JIRA_PROJECT: CLC ISSUE_TYPE: Bug From 381e2d0153a8997f0991c00a0036bc96c3ff01d0 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 14 Aug 2023 15:26:02 +0100 Subject: [PATCH 24/79] Update dependency:get command to latest syntax (#327) Update dependency:get command to latest syntax --- rc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rc.sh b/rc.sh index b7c728bbf..f9e562edd 100755 --- a/rc.sh +++ b/rc.sh @@ -58,7 +58,7 @@ download () { else log_info "Downloading: $jar_path ($artifact) from: $repo" set +e - output=$(mvn -q dependency:get -DrepoUrl=$repo -Dartifact=$artifact -Dtransitive=false -Ddest="$jar_path" 2>&1) + output=$(mvn -q org.apache.maven.plugins:maven-dependency-plugin:2.10:get -DremoteRepositories=$repo -Dartifact=$artifact -Dtransitive=false -Ddest="$jar_path" 2>&1) err=$? set -e if [ $err -ne 0 ]; then From b4a574447004d7825a7301fcf42310ac295090ae Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Mon, 14 Aug 2023 18:17:18 +0300 Subject: [PATCH 25/79] [CLC-165]: Add details to Snapshot List Command Output (#295) --- base/commands/snapshot/snapshot_list.go | 73 ++++++++++++++++++++----- clc/config/config.go | 2 + clc/config/config_test.go | 3 + internal/serialization/snapshot.go | 55 +++++++++++++++++++ 4 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 internal/serialization/snapshot.go diff --git a/base/commands/snapshot/snapshot_list.go b/base/commands/snapshot/snapshot_list.go index 8b5e5bcaf..456272a73 100644 --- a/base/commands/snapshot/snapshot_list.go +++ b/base/commands/snapshot/snapshot_list.go @@ -4,12 +4,15 @@ package snapshot import ( "context" + "time" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/types" ) type ListCmd struct{} @@ -33,20 +36,12 @@ func (cm ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } - es, err := m.GetKeySet(ctx) + rows, err := listDetailRows(ctx, *m) if err != nil { - return nil, err - } - rows := make([]output.Row, 0, len(es)) - for _, e := range es { - if s, ok := e.(string); ok { - rows = append(rows, output.Row{ - output.Column{ - Name: "Snapshot Name", - Type: serialization.TypeString, - Value: s, - }, - }) + ec.Logger().Error(err) + rows, err = listRows(ctx, *m) + if err != nil { + return nil, err } } return rows, nil @@ -61,3 +56,55 @@ func (cm ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { func init() { check.Must(plug.Registry.RegisterCommand("snapshot:list", ListCmd{})) } + +func listDetailRows(ctx context.Context, m hazelcast.Map) ([]output.Row, error) { + esd, err := m.GetEntrySet(ctx) + if err != nil { + return nil, err + } + rows := make([]output.Row, 0, len(esd)) + for _, e := range esd { + r := output.Row{} + if s, ok := e.Key.(string); ok { + r = append(r, output.Column{ + Name: "Snapshot Name", + Type: serialization.TypeString, + Value: s, + }) + } + if sd, ok := e.Value.(*serialization.Snapshot); ok { + r = append(r, output.Column{ + Name: "Job Name", + Type: serialization.TypeString, + Value: sd.JobName, + }) + r = append(r, output.Column{ + Name: "Time", + Type: serialization.TypeJavaLocalDateTime, + Value: types.LocalDateTime(time.UnixMilli(sd.CreationTime)), + }) + } + rows = append(rows, r) + } + return rows, nil +} + +func listRows(ctx context.Context, m hazelcast.Map) ([]output.Row, error) { + es, err := m.GetKeySet(ctx) + if err != nil { + return nil, err + } + rows := make([]output.Row, 0, len(es)) + for _, e := range es { + if s, ok := e.(string); ok { + rows = append(rows, output.Row{ + output.Column{ + Name: "Snapshot Name", + Type: serialization.TypeString, + Value: s, + }, + }) + } + } + return rows, nil +} diff --git a/clc/config/config.go b/clc/config/config.go index 4e6bf4479..6f7fde6e4 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-go-client" "golang.org/x/exp/slices" @@ -138,6 +139,7 @@ func MakeHzConfig(props plug.ReadOnlyProperties, lg log.Logger) (hazelcast.Confi lg.Debugf("Viridan API Base: %s", apiBase) cfg.Cluster.Cloud.ExperimentalAPIBaseURL = apiBase } + cfg.Serialization.SetIdentifiedDataSerializableFactories(serialization.SnapshotFactory{}) cfg.Labels = makeClientLabels() cfg.ClientName = makeClientName() usr := props.GetString(clc.PropertyClusterUser) diff --git a/clc/config/config_test.go b/clc/config/config_test.go index 1a30a5e5f..ba2476b51 100644 --- a/clc/config/config_test.go +++ b/clc/config/config_test.go @@ -9,6 +9,7 @@ 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" @@ -36,6 +37,7 @@ func TestMakeConfiguration_Default(t *testing.T) { target.Cluster.Unisocket = true target.Stats.Enabled = true target.Logger.CustomLogger = lg + target.Serialization.SetIdentifiedDataSerializableFactories(serialization.SnapshotFactory{}) require.Equal(t, target, cfg) } @@ -69,6 +71,7 @@ func TestMakeConfiguration_Viridian(t *testing.T) { target.Cluster.Network.SSL.SetTLSConfig(&tls.Config{ServerName: "hazelcast.cloud"}) target.Stats.Enabled = true target.Logger.CustomLogger = lg + target.Serialization.SetIdentifiedDataSerializableFactories(serialization.SnapshotFactory{}) require.Equal(t, target, cfg) } diff --git a/internal/serialization/snapshot.go b/internal/serialization/snapshot.go new file mode 100644 index 000000000..265418ab0 --- /dev/null +++ b/internal/serialization/snapshot.go @@ -0,0 +1,55 @@ +package serialization + +import ( + "fmt" + + "github.com/hazelcast/hazelcast-go-client/serialization" +) + +const snapshotClassID = 32 +const snapshotFactoryID = -10002 + +type Snapshot struct { + ID int64 + NumChunks int64 + NumBytes int64 + CreationTime int64 + JobID int64 + JobName string + DagJsonString string +} + +func (s *Snapshot) FactoryID() int32 { + return snapshotFactoryID +} + +func (s *Snapshot) ClassID() int32 { + return snapshotClassID +} + +type SnapshotFactory struct{} + +func (SnapshotFactory) Create(classID int32) serialization.IdentifiedDataSerializable { + if classID == snapshotClassID { + return &Snapshot{} + } + panic(fmt.Errorf("classID is not correct, it must be %d", snapshotClassID)) +} + +func (SnapshotFactory) FactoryID() int32 { + return snapshotFactoryID +} + +func (s *Snapshot) WriteData(output serialization.DataOutput) { + // not used +} + +func (s *Snapshot) ReadData(input serialization.DataInput) { + s.ID = input.ReadInt64() + s.NumChunks = input.ReadInt64() + s.NumBytes = input.ReadInt64() + s.CreationTime = input.ReadInt64() + s.JobID = input.ReadInt64() + s.JobName = input.ReadString() + s.DagJsonString = input.ReadString() +} From 6907906aba0392a8613732381c942c2faa6c722f Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Mon, 14 Aug 2023 18:17:39 +0300 Subject: [PATCH 26/79] [CLC-220]: make configuration menu available in non-interactive mode as well (#317) --- clc/config/wizard/provider.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/clc/config/wizard/provider.go b/clc/config/wizard/provider.go index 85d58a9a0..16eccb4ce 100644 --- a/clc/config/wizard/provider.go +++ b/clc/config/wizard/provider.go @@ -3,10 +3,13 @@ package wizard import ( "context" "errors" + "fmt" "os" "sync/atomic" tea "github.com/charmbracelet/bubbletea" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" "github.com/hazelcast/hazelcast-go-client" "github.com/spf13/pflag" @@ -47,12 +50,19 @@ func (p *Provider) BindFlag(name string, flag *pflag.Flag) { p.fp.Load().BindFlag(name, flag) } +func maybeUnwrapStdout(ec plug.ExecContext) any { + if v, ok := ec.Stdout().(clc.NopWriteCloser); ok { + return v.W + } + return ec.Stdout() +} + 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 !ec.Interactive() { - return hazelcast.Config{}, err - } // ask the config to the user name, err := p.runWizard(ctx, ec) if err != nil { From 2a3ad60433657e773d460707a5a94997a7d2faad Mon Sep 17 00:00:00 2001 From: Serdar Ozmen Date: Tue, 15 Aug 2023 23:49:18 +0300 Subject: [PATCH 27/79] Fix latest supported MC version (#344) --- docs/antora.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/antora.yml b/docs/antora.yml index b1b2ca053..28d2f66e4 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 From 2147de9e1036ecb5d164c1e669b47983397ba2f4 Mon Sep 17 00:00:00 2001 From: angelasimms <106963942+angelasimms@users.noreply.github.com> Date: Thu, 17 Aug 2023 09:37:59 +0100 Subject: [PATCH 28/79] DOCS-568 Text Changes for consistent renaming to Viridian Cloud (#336) * DOCS-568 Text Changes for conistent renaming to Viridian Cloud * DOCS-568 Minor updates * DOCS-568 Minor amends * DOCS-568 Typo --- docs/check-links-playbook.yml | 2 +- docs/modules/ROOT/pages/clc-job.adoc | 4 ++-- docs/modules/ROOT/pages/clc-multimap.adoc | 4 ++-- docs/modules/ROOT/pages/clc-project.adoc | 2 +- docs/modules/ROOT/pages/clc-queue.adoc | 8 ++++---- docs/modules/ROOT/pages/clc-set.adoc | 10 +++++----- docs/modules/ROOT/pages/clc-snapshot.adoc | 2 +- docs/modules/ROOT/pages/clc-topic.adoc | 8 +++----- docs/modules/ROOT/pages/clc-viridian.adoc | 6 +++--- docs/modules/ROOT/pages/connect-to-viridian.adoc | 8 ++++---- docs/modules/ROOT/pages/environment-variables.adoc | 2 +- docs/modules/ROOT/pages/get-started.adoc | 11 ++++++----- docs/modules/ROOT/pages/install-clc.adoc | 2 +- docs/modules/ROOT/pages/jet-job-management.adoc | 10 +++++----- .../ROOT/pages/managing-viridian-clusters.adoc | 6 +++--- docs/modules/ROOT/pages/overview.adoc | 6 +++--- docs/modules/ROOT/pages/release-notes-5.2.0.adoc | 8 ++++---- docs/modules/ROOT/pages/release-notes-5.2.1.adoc | 10 +++++----- .../ROOT/pages/release-notes-5.3.0-BETA-1.adoc | 4 ++-- .../ROOT/pages/release-notes-5.3.0-BETA-2.adoc | 2 +- docs/modules/ROOT/pages/release-notes-5.3.0.adoc | 4 ++-- docs/modules/ROOT/pages/release-notes-5.3.1.adoc | 2 +- 22 files changed, 60 insertions(+), 61 deletions(-) diff --git a/docs/check-links-playbook.yml b/docs/check-links-playbook.yml index cc88fe9d0..18e49ca1b 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 c0923bc1d..b40bcdf02 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 6ef3407b2..9122d0f14 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 8d1bd4363..16466409d 100644 --- a/docs/modules/ROOT/pages/clc-project.adoc +++ b/docs/modules/ROOT/pages/clc-project.adoc @@ -15,7 +15,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 9a8df97a5..e134e664c 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 6fc19d243..cc803f66c 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 d7cd049ee..e76bb7422 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 3db5062f3..36b9a6beb 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 b1bf63837..a8c7def9b 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 c7042a458..09ae1e941 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 1e93bb01c..24fefaeb2 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 c8b9321fe..c18f42227 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 5cb1e31cb..fffa57b63 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 439cf0727..cad6dc097 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 0cb2dc3d3..7840e3afc 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 a2dcb0e65..73b36cf91 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 fe69b7004..3f47a2a22 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 347e1bbeb..e22fe2e95 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 240a856a5..7847b744e 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 230690cef..4008a7bb6 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 5e7688346..a3ea8283f 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 c14a2edb6..53de998bf 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 From 7899be1be920e35bbc5fe27c12dc7da5aad553cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 17 Aug 2023 15:22:03 +0300 Subject: [PATCH 29/79] Disabled verbose test output (#347) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7e3dc2345..3e636a0e3 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 From 891e67fcd1163a35a0a835a1283ef00ccb5a42cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 21 Aug 2023 13:30:42 +0300 Subject: [PATCH 30/79] Removed yaml.v2 and updated the Go client (#348) Removed yaml.v2 and updated the Go client --- base/commands/project/utils.go | 8 ++++---- clc/config/provider.go | 15 +++++---------- go.mod | 5 ++--- go.sum | 6 ++---- internal/it/test_context.go | 2 +- internal/it/util.go | 2 +- 6 files changed, 15 insertions(+), 23 deletions(-) diff --git a/base/commands/project/utils.go b/base/commands/project/utils.go index 307aeade0..18e55ec52 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/clc/config/provider.go b/clc/config/provider.go index c6b5397cc..6c02d5831 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/go.mod b/go.mod index 8e1dc3e42..f26031a6d 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 5ff34d845..fa82f07d2 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/test_context.go b/internal/it/test_context.go index 6e02f7c9f..cdb2121b6 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 14746c49b..5827ff30a 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" From b8d55c2d6a77c839d42e5a9cb729d44ed1591bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 21 Aug 2023 15:00:33 +0300 Subject: [PATCH 31/79] Configuration Import/Add Improvements (#349) * Import CLC configuration from Viridian if possible * config add creates config.json alongside config.yaml --- base/commands/config/config_add.go | 6 + base/commands/config/config_import.go | 12 +- base/commands/config/config_it_test.go | 2 +- base/commands/demo/demo_it_test.go | 1 + base/commands/viridian/common.go | 34 +++++- .../viridian/download_logs_it_test.go | 41 ------- base/commands/viridian/viridian_it_test.go | 49 +++++++- .../viridian/viridian_it_unix_test.go | 3 +- .../viridian/viridian_it_windows_test.go | 2 +- clc/config/config.go | 34 +++++- clc/config/config_test.go | 22 +++- clc/config/import.go | 112 ++++++++++-------- 12 files changed, 201 insertions(+), 117 deletions(-) diff --git a/base/commands/config/config_add.go b/base/commands/config/config_add.go index df96f15dc..21a9b883d 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 5f648a1dd..a6047a99e 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 fa16e1188..7a5917757 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 91a78a0b8..3212d07db 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/viridian/common.go b/base/commands/viridian/common.go index 546167b7f..2fb231e3d 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 7720caecb..32576650e 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 07e2cddf8..d8bab0eac 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 51796f5ca..94dfacff7 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 e47021b36..e77da8c9d 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/config/config.go b/clc/config/config.go index 6f7fde6e4..9d3ab954b 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 ba2476b51..45b4e4f49 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 b60c0047c..5e201cada 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] { From 622e2e9a2e1dab46f1cec007820b0a86f2f5cf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 22 Aug 2023 09:55:17 +0300 Subject: [PATCH 32/79] Remove test coverage step from the nightlies (#351) --- .github/workflows/nightly_tests.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/nightly_tests.yaml b/.github/workflows/nightly_tests.yaml index 159a7ffa9..18b4622c5 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 From 16630d87e4934b5e8b86fde9581fda0319a8ebda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 22 Aug 2023 13:53:05 +0300 Subject: [PATCH 33/79] Stage support (#297) Initial stage implementation --- clc/cmd/exec_context.go | 20 ++++++- clc/ux/stage/stage.go | 117 +++++++++++++++++++++++++++++++++++++ clc/ux/stage/stage_test.go | 97 ++++++++++++++++++++++++++++++ internal/it/context.go | 57 +++++++++++++----- internal/iterator.go | 17 ++++++ internal/str/str.go | 10 ++++ internal/str/str_test.go | 22 +++++++ 7 files changed, 322 insertions(+), 18 deletions(-) create mode 100644 clc/ux/stage/stage.go create mode 100644 clc/ux/stage/stage_test.go create mode 100644 internal/iterator.go diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index 35e173370..14f7ef15b 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/ux/stage/stage.go b/clc/ux/stage/stage.go new file mode 100644 index 000000000..9129b4b56 --- /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 000000000..837608f9a --- /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/internal/it/context.go b/internal/it/context.go index 1d41133da..0313b91ea 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/iterator.go b/internal/iterator.go new file mode 100644 index 000000000..c6acae3f9 --- /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 77332356a..f2613f4e1 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 ab2c0b583..b7ca05a36 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) + }) + } +} From b4d1a70115f53b8034816bd6d6eec98c5f182ba1 Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Tue, 22 Aug 2023 17:36:27 +0300 Subject: [PATCH 34/79] fix pipe check (#345) --- clc/config/wizard/provider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clc/config/wizard/provider.go b/clc/config/wizard/provider.go index 16eccb4ce..f685c5c22 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 { From ef475e63c7959defda564af785b86ea7f6092854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 24 Aug 2023 10:12:55 +0300 Subject: [PATCH 35/79] Refactored project create test (#354) Added paths.CopyDir function, refactored project create test --- .../project/project_create_it_test.go | 31 +++++++-------- clc/paths/paths.go | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/base/commands/project/project_create_it_test.go b/base/commands/project/project_create_it_test.go index 850237ccd..53a240969 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/clc/paths/paths.go b/clc/paths/paths.go index 9685b5674..ccfec662c 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() From 30e1791cef4349de8cbc9aa30a13a2ddecec6984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 25 Aug 2023 18:33:15 +0300 Subject: [PATCH 36/79] [CLC-228] Scripting mode (#305) Added the scripting mode --- base/commands/demo/dummy.go | 3 ++ base/commands/script.go | 2 +- base/commands/shell.go | 2 +- clc/cmd/clc.go | 60 +++++++++++++++++++++---------------- clc/cmd/command_context.go | 34 ++++++++++----------- clc/cmd/exec_context.go | 52 ++++++++++++++++---------------- cmd/clc/imports.go | 1 - 7 files changed, 82 insertions(+), 72 deletions(-) create mode 100644 base/commands/demo/dummy.go diff --git a/base/commands/demo/dummy.go b/base/commands/demo/dummy.go new file mode 100644 index 000000000..4b59bed4c --- /dev/null +++ b/base/commands/demo/dummy.go @@ -0,0 +1,3 @@ +package demo + +// This file exists only for compilation diff --git a/base/commands/script.go b/base/commands/script.go index 0b4154b11..0c1ac9c8b 100644 --- a/base/commands/script.go +++ b/base/commands/script.go @@ -65,7 +65,7 @@ func (cm ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { Stderr: ec.Stderr(), Stdout: ec.Stdout(), } - m, err := ec.(*cmd.ExecContext).Main().Clone(false) + m, err := ec.(*cmd.ExecContext).Main().Clone(cmd.ModeScripting) if err != nil { return fmt.Errorf("cloning Main: %w", err) } diff --git a/base/commands/shell.go b/base/commands/shell.go index 6b5db6214..c92208adf 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -59,7 +59,7 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext if len(ec.Args()) > 0 { return puberrors.ErrNotAvailable } - m, err := ec.(*cmd.ExecContext).Main().Clone(true) + m, err := ec.(*cmd.ExecContext).Main().Clone(cmd.ModeInteractive) if err != nil { return fmt.Errorf("cloning Main: %w", err) } diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index 210f64d23..38e918a72 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -30,22 +30,30 @@ var ( MainCommandShortHelp = "Hazelcast CLC" ) +type Mode int + +const ( + ModeNonInteractive Mode = iota + ModeInteractive + ModeScripting +) + type Main struct { - root *cobra.Command - cmds map[string]*cobra.Command - lg *logger.Logger - stderr io.WriteCloser - stdout io.WriteCloser - stdin io.Reader - isInteractive bool - outputFormat string - configLoaded bool - props *plug.Properties - cc *CommandContext - cp config.Provider - arg0 string - ciMu *sync.Mutex - ci *atomic.Pointer[hazelcast.ClientInternal] + root *cobra.Command + cmds map[string]*cobra.Command + lg *logger.Logger + stderr io.WriteCloser + stdout io.WriteCloser + stdin io.Reader + mode Mode + outputFormat string + configLoaded bool + props *plug.Properties + cc *CommandContext + cp config.Provider + arg0 string + ciMu *sync.Mutex + ci *atomic.Pointer[hazelcast.ClientInternal] } func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLevel string, sio clc.IO) (*Main, error) { @@ -91,7 +99,7 @@ func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLeve m.props.Set(clc.PropertyConfig, cfgPath) m.props.Set(clc.PropertyLogPath, logPath) m.props.Set(clc.PropertyLogLevel, logLevel) - m.cc = NewCommandContext(rc, cfgProvider, m.isInteractive) + m.cc = NewCommandContext(rc, cfgProvider, m.mode) if err := m.runInitializers(m.cc); err != nil { return nil, err } @@ -101,9 +109,9 @@ func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLeve return m, nil } -func (m *Main) Clone(interactive bool) (*Main, error) { +func (m *Main) Clone(mode Mode) (*Main, error) { mc := *m - mc.isInteractive = true + mc.mode = mode rc := &cobra.Command{ SilenceErrors: true, } @@ -120,7 +128,7 @@ func (m *Main) Clone(interactive bool) (*Main, error) { }, }) mc.cmds = map[string]*cobra.Command{} - mc.cc = NewCommandContext(rc, mc.cp, interactive) + mc.cc = NewCommandContext(rc, mc.cp, mode) if err := mc.runInitializers(mc.cc); err != nil { return nil, err } @@ -138,7 +146,7 @@ func (m *Main) Execute(ctx context.Context, args ...string) error { var cm *cobra.Command var cmdArgs []string var err error - if !m.isInteractive { + if m.mode == ModeNonInteractive { cm, cmdArgs, err = m.root.Find(args) if err != nil { return err @@ -255,11 +263,11 @@ func (m *Main) createCommands() error { for _, c := range plug.Registry.Commands() { c := c // check if current command available in current mode - if !plug.Registry.IsAvailable(m.isInteractive, c.Name) { + if !plug.Registry.IsAvailable(m.mode != ModeNonInteractive, c.Name) { continue } // skip interactive commands in interactive mode - if m.isInteractive { + if m.mode == ModeInteractive { if _, ok := c.Item.(plug.InteractiveCommander); ok { continue } @@ -290,7 +298,7 @@ func (m *Main) createCommands() error { SilenceUsage: true, } cmd.SetUsageTemplate(usageTemplate) - cc := NewCommandContext(cmd, m.cp, m.isInteractive) + cc := NewCommandContext(cmd, m.cp, m.mode) if ci, ok := c.Item.(plug.Initializer); ok { if err := ci.Init(cc); err != nil { if errors.Is(err, puberrors.ErrNotAvailable) { @@ -300,7 +308,7 @@ func (m *Main) createCommands() error { } } // add the backslash prefix for top-level commands in the interactive mode - if m.isInteractive && parent == m.root { + if m.mode != ModeNonInteractive && parent == m.root { cmd.Use = fmt.Sprintf("\\%s", cmd.Use) } addUniqueCommandGroup(cc, parent) @@ -320,7 +328,7 @@ func (m *Main) createCommands() error { Stderr: m.stderr, Stdout: m.stdout, } - ec, err := NewExecContext(m.lg, sio, m.props, m.isInteractive) + ec, err := NewExecContext(m.lg, sio, m.props, m.mode) if err != nil { return err } @@ -354,7 +362,7 @@ func (m *Main) createCommands() error { return err } if ic, ok := c.Item.(plug.InteractiveCommander); ok { - ec.SetInteractive(true) + ec.SetMode(ModeInteractive) if _, ok := c.Item.(plug.UnwrappableCommander); ok { err = ic.ExecInteractive(ctx, ec) } else { diff --git a/clc/cmd/command_context.go b/clc/cmd/command_context.go index 55903e799..b17047926 100644 --- a/clc/cmd/command_context.go +++ b/clc/cmd/command_context.go @@ -10,24 +10,24 @@ import ( ) type CommandContext struct { - Cmd *cobra.Command - CP config.Provider - stringValues map[string]*string - boolValues map[string]*bool - intValues map[string]*int64 - isInteractive bool - isTopLevel bool - group *cobra.Group + Cmd *cobra.Command + CP config.Provider + stringValues map[string]*string + boolValues map[string]*bool + intValues map[string]*int64 + mode Mode + isTopLevel bool + group *cobra.Group } -func NewCommandContext(cmd *cobra.Command, cfgProvider config.Provider, isInteractive bool) *CommandContext { +func NewCommandContext(cmd *cobra.Command, cfgProvider config.Provider, mode Mode) *CommandContext { return &CommandContext{ - Cmd: cmd, - CP: cfgProvider, - stringValues: map[string]*string{}, - boolValues: map[string]*bool{}, - intValues: map[string]*int64{}, - isInteractive: isInteractive, + Cmd: cmd, + CP: cfgProvider, + stringValues: map[string]*string{}, + boolValues: map[string]*bool{}, + intValues: map[string]*int64{}, + mode: mode, } } @@ -85,7 +85,7 @@ func (cc *CommandContext) Hide() { } func (cc *CommandContext) Interactive() bool { - return cc.isInteractive + return cc.mode == ModeInteractive } func (cc *CommandContext) SetCommandHelp(long, short string) { @@ -118,7 +118,7 @@ func (cc *CommandContext) Group() *cobra.Group { func (cc *CommandContext) AddStringConfig(name, value, flag string, help string) { cc.CP.Set(name, value) - if flag != "" && !cc.isInteractive { + if flag != "" && !cc.Interactive() { f := cc.Cmd.Flag(flag) if f != nil { cc.CP.BindFlag(name, f) diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index 14f7ef15b..c834851e9 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -33,29 +33,29 @@ const ( type ClientFn func(ctx context.Context, cfg hazelcast.Config) (*hazelcast.ClientInternal, error) type ExecContext struct { - lg log.Logger - stdout io.Writer - stderr io.Writer - stdin io.Reader - args []string - props *plug.Properties - isInteractive bool - cmd *cobra.Command - main *Main - spinnerWait time.Duration - printer plug.Printer - cp config.Provider -} - -func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, interactive bool) (*ExecContext, error) { + lg log.Logger + stdout io.Writer + stderr io.Writer + stdin io.Reader + args []string + props *plug.Properties + mode Mode + cmd *cobra.Command + main *Main + spinnerWait time.Duration + printer plug.Printer + cp config.Provider +} + +func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, mode Mode) (*ExecContext, error) { return &ExecContext{ - lg: lg, - stdout: sio.Stdout, - stderr: sio.Stderr, - stdin: sio.Stdin, - props: props, - isInteractive: interactive, - spinnerWait: 1 * time.Second, + lg: lg, + stdout: sio.Stdout, + stderr: sio.Stderr, + stdin: sio.Stdin, + props: props, + mode: mode, + spinnerWait: 1 * time.Second, }, nil } @@ -137,7 +137,7 @@ func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInt } func (ec *ExecContext) Interactive() bool { - return ec.isInteractive + return ec.mode == ModeInteractive } func (ec *ExecContext) AddOutputRows(ctx context.Context, rows ...output.Row) error { @@ -159,7 +159,7 @@ func (ec *ExecContext) AddOutputStream(ctx context.Context, ch <-chan output.Row func (ec *ExecContext) ShowHelpAndExit() { Must(ec.cmd.Help()) - if !ec.isInteractive { + if !ec.Interactive() { os.Exit(0) } } @@ -168,8 +168,8 @@ func (ec *ExecContext) CommandName() string { return ec.cmd.CommandPath() } -func (ec *ExecContext) SetInteractive(value bool) { - ec.isInteractive = value +func (ec *ExecContext) SetMode(mode Mode) { + ec.mode = mode } // ExecuteBlocking runs the given blocking function. diff --git a/cmd/clc/imports.go b/cmd/clc/imports.go index 04661ceec..14b783d6c 100644 --- a/cmd/clc/imports.go +++ b/cmd/clc/imports.go @@ -11,7 +11,6 @@ import ( _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/multimap" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/object" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/project" - _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/set" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/queue" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/set" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands/snapshot" From 1acb688bebac7eceb5e5a0c517d10b3faf9d0538 Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Mon, 28 Aug 2023 14:54:22 +0300 Subject: [PATCH 37/79] [CLC-245]: Add Command to List Project Templates (#326) --- base/commands/project/project.go | 3 +- base/commands/project/project_list_it_test.go | 66 +++++ .../project/project_list_templates.go | 230 ++++++++++++++++++ base/commands/project/utils.go | 7 +- clc/paths/paths.go | 4 + docs/modules/ROOT/pages/clc-project.adoc | 39 +++ 6 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 base/commands/project/project_list_it_test.go create mode 100644 base/commands/project/project_list_templates.go diff --git a/base/commands/project/project.go b/base/commands/project/project.go index 877d6f648..4ba5c51b0 100644 --- a/base/commands/project/project.go +++ b/base/commands/project/project.go @@ -26,5 +26,6 @@ func (gc ProjectCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("project", &ProjectCommand{})) + cmd := &ProjectCommand{} + Must(plug.Registry.RegisterCommand("project", cmd)) } diff --git a/base/commands/project/project_list_it_test.go b/base/commands/project/project_list_it_test.go new file mode 100644 index 000000000..8fa68a524 --- /dev/null +++ b/base/commands/project/project_list_it_test.go @@ -0,0 +1,66 @@ +package project + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/store" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/it" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" +) + +func TestProjectListCommand(t *testing.T) { + testCases := []struct { + name string + f func(t *testing.T) + }{ + {name: "ProjectList_CachedTest", f: projectList_CachedTest}, + {name: "ProjectList_LocalTest", f: projectList_LocalTest}, + } + for _, tc := range testCases { + t.Run(tc.name, tc.f) + } +} + +func projectList_CachedTest(t *testing.T) { + tcx := it.TestContext{T: t} + tcx.Tester(func(tcx it.TestContext) { + sPath := filepath.Join(paths.Caches(), "templates") + defer func() { + os.RemoveAll(sPath) + }() + sa := store.NewStoreAccessor(sPath, log.NopLogger{}) + check.MustValue(sa.WithLock(func(s *store.Store) (any, error) { + v := []byte(strconv.FormatInt(time.Now().Add(cacheRefreshInterval).Unix(), 10)) + err := s.SetEntry([]byte(nextFetchTimeKey), v) + return nil, err + })) + check.MustValue(sa.WithLock(func(s *store.Store) (any, error) { + b := check.MustValue(json.Marshal([]Template{{Name: "test_template"}})) + err := s.SetEntry([]byte(templatesKey), b) + return nil, err + })) + cmd := []string{"project", "list-templates"} + check.Must(tcx.CLC().Execute(context.Background(), cmd...)) + tcx.AssertStdoutContains("test_template") + }) +} + +func projectList_LocalTest(t *testing.T) { + tcx := it.TestContext{T: t} + tcx.Tester(func(tcx it.TestContext) { + testHomeDir := "testdata/home" + check.Must(paths.CopyDir(testHomeDir, tcx.HomePath())) + cmd := []string{"project", "list-templates", "--local"} + check.Must(tcx.CLC().Execute(context.Background(), cmd...)) + tcx.AssertStdoutContains("simple") + tcx.AssertStdoutContains("local") + }) +} diff --git a/base/commands/project/project_list_templates.go b/base/commands/project/project_list_templates.go new file mode 100644 index 000000000..d0624d50c --- /dev/null +++ b/base/commands/project/project_list_templates.go @@ -0,0 +1,230 @@ +package project + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/store" + . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" +) + +type ListCmd struct{} + +const ( + flagRefresh = "refresh" + flagLocal = "local" + nextFetchTimeKey = "project.templates.nextFetchTime" + templatesKey = "project.templates" + cacheRefreshInterval = 10 * time.Minute +) + +type Template struct { + Name string `json:"name"` + Source string +} + +func (lc ListCmd) Init(cc plug.InitContext) error { + cc.SetPositionalArgCount(0, 0) + cc.SetCommandUsage("list-templates [flags]") + cc.AddBoolFlag(flagRefresh, "", false, false, "fetch most recent templates from remote") + cc.AddBoolFlag(flagLocal, "", false, false, "list the templates which exist on local environment") + help := "Lists templates that can be used while creating projects." + cc.SetCommandHelp(help, help) + return nil +} + +func (lc ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { + isLocal := ec.Props().GetBool(flagLocal) + isRefresh := ec.Props().GetBool(flagRefresh) + if isLocal && isRefresh { + return fmt.Errorf("%s and %s flags are mutually exclusive", flagRefresh, flagLocal) + } + ts, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText(fmt.Sprintf("Listing templates")) + return listTemplates(ec.Logger(), isLocal, isRefresh) + }) + if err != nil { + return err + } + stop() + tss := ts.([]Template) + if len(tss) == 0 { + ec.PrintlnUnnecessary("No templates found") + } + rows := make([]output.Row, len(tss)) + for i, t := range tss { + rows[i] = output.Row{ + output.Column{ + Name: "Source", + Type: serialization.TypeString, + Value: t.Source, + }, + output.Column{ + Name: "Name", + Type: serialization.TypeString, + Value: t.Name, + }, + } + } + return ec.AddOutputRows(ctx, rows...) +} + +func listTemplates(logger log.Logger, isLocal bool, isRefresh bool) ([]Template, error) { + sa := store.NewStoreAccessor(filepath.Join(paths.Caches(), "templates"), logger) + if isLocal { + return listLocalTemplates() + } + var fetch bool + var err error + if fetch, err = shouldFetch(sa); err != nil { + logger.Debugf("Error: checking template list expiry: %w", err) + // there is an error with database, so fetch templates from remote + fetch = true + } + if fetch || isRefresh { + ts, err := fetchTemplates() + if err != nil { + return nil, err + } + err = updateCache(sa, ts) + if err != nil { + logger.Debugf("Error: Updating templates cache: %w", err) + } + } + return listFromCache(sa) +} + +func listLocalTemplates() ([]Template, error) { + var templates []Template + ts, err := paths.FindAll(paths.Templates(), func(basePath string, entry os.DirEntry) (ok bool) { + return entry.IsDir() + }) + if err != nil { + return nil, err + } + for _, t := range ts { + templates = append(templates, Template{Name: t, Source: "local"}) + } + return templates, nil +} + +func fetchTemplates() ([]Template, error) { + var templates []Template + resp, err := http.Get(makeRepositoriesURL()) + if err != nil { + return nil, err + } + respData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var data []map[string]any + err = json.Unmarshal(respData, &data) + if err != nil { + return nil, err + } + for _, d := range data { + var tName string + var ok bool + if tName, ok = d["full_name"].(string); !ok { + return nil, errors.New("error fetching repositories in the organization") + } + sName := strings.Split(tName, "/") + source := fmt.Sprintf("%s/%s", "github.com", sName[0]) + name := sName[1] + templates = append(templates, Template{Name: name, Source: source}) + } + return templates, nil +} + +func updateNextFetchTime(s *store.Store) error { + _, err := func(s *store.Store) (any, error) { + v := []byte(strconv.FormatInt(time.Now().Add(cacheRefreshInterval).Unix(), 10)) + return nil, s.SetEntry([]byte(nextFetchTimeKey), v) + }(s) + return err +} + +func makeRepositoriesURL() string { + s := strings.TrimPrefix(templateOrgURL(), "https://github.com/") + ss := strings.ReplaceAll(s, "/", "") + return fmt.Sprintf("https://api.github.com/users/%s/repos", ss) +} + +func shouldFetch(s *store.StoreAccessor) (bool, error) { + entry, err := s.WithLock(func(s *store.Store) (any, error) { + return s.GetEntry([]byte(nextFetchTimeKey)) + }) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return true, nil + } + return false, err + } + var fetchTS time.Time + t, err := strconv.ParseInt(string(entry.([]byte)), 10, 64) + if err != nil { + return false, err + } + fetchTS = time.Unix(t, 0) + if time.Now().After(fetchTS) { + return true, nil + } + return false, nil +} + +func updateCache(sa *store.StoreAccessor, templates []Template) error { + b, err := json.Marshal(templates) + if err != nil { + return err + } + _, err = sa.WithLock(func(s *store.Store) (any, error) { + err = s.DeleteEntriesWithPrefix(templatesKey) + if err != nil { + return nil, err + } + err = s.SetEntry([]byte(templatesKey), b) + if err != nil { + return nil, err + } + if err = updateNextFetchTime(s); err != nil { + return nil, err + } + return nil, nil + }) + return err +} + +func listFromCache(sa *store.StoreAccessor) ([]Template, error) { + var templates []Template + b, err := sa.WithLock(func(s *store.Store) (any, error) { + return s.GetEntry([]byte(templatesKey)) + }) + if err != nil { + return nil, err + } + err = json.Unmarshal(b.([]byte), &templates) + if err != nil { + return nil, err + } + return templates, nil +} + +func init() { + Must(plug.Registry.RegisterCommand("project:list-templates", &ListCmd{})) +} diff --git a/base/commands/project/utils.go b/base/commands/project/utils.go index 18e55ec52..076f81bbb 100644 --- a/base/commands/project/utils.go +++ b/base/commands/project/utils.go @@ -145,11 +145,16 @@ func cloneTemplate(baseDir string, name string) error { return nil } -func templateRepoURL(templateName string) string { +func templateOrgURL() string { u := os.Getenv(envTemplateSource) if u == "" { u = hzTemplatesOrganization } + return u +} + +func templateRepoURL(templateName string) string { + u := templateOrgURL() u = strings.TrimSuffix(u, "/") return fmt.Sprintf("%s/%s", u, templateName) } diff --git a/clc/paths/paths.go b/clc/paths/paths.go index ccfec662c..8b5320ca6 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -51,6 +51,10 @@ func Templates() string { return filepath.Join(Home(), "templates") } +func Caches() string { + return filepath.Join(Home(), "caches") +} + func ResolveTemplatePath(t string) string { return filepath.Join(Templates(), t) } diff --git a/docs/modules/ROOT/pages/clc-project.adoc b/docs/modules/ROOT/pages/clc-project.adoc index 16466409d..6936b9306 100644 --- a/docs/modules/ROOT/pages/clc-project.adoc +++ b/docs/modules/ROOT/pages/clc-project.adoc @@ -12,6 +12,7 @@ clc project [command] [flags] == Commands * <> +* <> == clc project create @@ -100,4 +101,42 @@ clc project create^ simple-streaming-pipeline^ --output-dir my-project^ my_key1=my_value1 my_key2=my_value2 +---- + +== clc project list-templates + +Lists templates that can be used while creating projects. + +Usage: + +[source,bash] +---- +clc project list-templates [flags] +---- + +Parameters: + +[cols="1m,1a,2a,1a"] +|=== +|Parameter|Required|Description|Default + +|--local +|false +|When enabled, it only lists templates that exist in `/templates` +| + +|--force +|false +|Templates are fetched and cached in a local data store. iIf you want to force CLC to fetch the latest templates from the remote repositories, you should set this parameter. +| + +|=== + +WARNING: --force and --local parameters cannot be set at the same time, because they serve for different purposes. --force is used for templates in remote repositories, while --local is used to list templates in local environment. + +Example: + +[source,bash] +---- +clc project list-templates ---- \ No newline at end of file From a4a16f6e7d4842531a5d2d11d69e20c78a4aab1f Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Tue, 29 Aug 2023 10:42:47 +0300 Subject: [PATCH 38/79] [CLC-19]: `\di` Command to List Indexes (#292) --- base/commands/map/map_set.go | 3 +- base/commands/object/object_list.go | 57 +------------- base/commands/script.go | 2 +- base/commands/shell.go | 1 + base/commands/shell_script_common.go | 15 +--- base/commands/sql/sql_it_test.go | 36 +++++++++ base/commands/sql/testdata/list_indexes.txt | 5 ++ base/commands/sql/testdata/sql_help.txt | 4 +- base/maps/maps.go | 74 +++++++++++++++++++ base/objects/objects.go | 61 +++++++++++++++ clc/shell/common.go | 68 +++++++++++++---- docs/modules/ROOT/pages/clc.adoc | 73 ++++++++++++++++++ .../proto/codec/bitmap_index_options_codec.go | 37 ++++++++++ internal/proto/codec/builtin.go | 20 +++++ internal/proto/codec/index_config_codec.go | 47 ++++++++++++ .../proto/codec/mc_get_map_config_codec.go | 73 ++++++++++++++++++ 16 files changed, 488 insertions(+), 88 deletions(-) create mode 100644 base/commands/sql/testdata/list_indexes.txt create mode 100644 base/maps/maps.go create mode 100644 base/objects/objects.go create mode 100644 internal/proto/codec/bitmap_index_options_codec.go create mode 100644 internal/proto/codec/index_config_codec.go create mode 100644 internal/proto/codec/mc_get_map_config_codec.go diff --git a/base/commands/map/map_set.go b/base/commands/map/map_set.go index 6f5c6798b..c90402fe6 100644 --- a/base/commands/map/map_set.go +++ b/base/commands/map/map_set.go @@ -6,12 +6,11 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" + "github.com/hazelcast/hazelcast-go-client" ) type MapSetCommand struct{} diff --git a/base/commands/object/object_list.go b/base/commands/object/object_list.go index 7ae583a06..b38e571a6 100644 --- a/base/commands/object/object_list.go +++ b/base/commands/object/object_list.go @@ -8,8 +8,7 @@ import ( "sort" "strings" - "github.com/hazelcast/hazelcast-go-client/types" - + "github.com/hazelcast/hazelcast-commandline-client/base/objects" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -81,7 +80,7 @@ func (cm ObjectListCommand) Exec(ctx context.Context, ec plug.ExecContext) error typeFilter = ec.Args()[0] } showHidden := ec.Props().GetBool(flagShowHidden) - objs, err := getObjects(ctx, ec, typeFilter, showHidden) + objs, err := objects.GetAll(ctx, ec, typeFilter, showHidden) if err != nil { return err } @@ -100,7 +99,7 @@ func (cm ObjectListCommand) Exec(ctx context.Context, ec plug.ExecContext) error output.Column{ Name: "Service Name", Type: serialization.TypeString, - Value: shortType(o.ServiceName), + Value: objects.ShortType(o.ServiceName), }, valueCol, }) @@ -122,56 +121,6 @@ func objectFilterTypes() string { return sb.String() } -func getObjects(ctx context.Context, ec plug.ExecContext, typeFilter string, showHidden bool) ([]types.DistributedObjectInfo, error) { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - objs, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Getting distributed objects") - return ci.Client().GetDistributedObjectsInfo(ctx) - }) - if err != nil { - return nil, err - } - stop() - var r []types.DistributedObjectInfo - typeFilter = strings.ToLower(typeFilter) - for _, o := range objs.([]types.DistributedObjectInfo) { - if !showHidden && (o.Name == "" || strings.HasPrefix(o.Name, "__")) { - continue - } - if o.Name == "" { - o.Name = "(no name)" - } - if typeFilter == "" { - r = append(r, o) - continue - } - if typeFilter == shortType(o.ServiceName) { - r = append(r, o) - } - } - sort.Slice(r, func(i, j int) bool { - // first sort by type, then name - ri := r[i] - rj := r[j] - if ri.ServiceName < rj.ServiceName { - return true - } - if ri.ServiceName > rj.ServiceName { - return false - } - return ri.Name < rj.Name - }) - return r, nil -} - -func shortType(svcName string) string { - s := strings.TrimSuffix(strings.TrimPrefix(svcName, "hz:impl:"), "Service") - return strings.ToLower(s) -} - func init() { // sort objectTypes so they look better in help sort.Slice(objTypes, func(i, j int) bool { diff --git a/base/commands/script.go b/base/commands/script.go index 0c1ac9c8b..5c7451963 100644 --- a/base/commands/script.go +++ b/base/commands/script.go @@ -72,7 +72,7 @@ func (cm ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { verbose := ec.Props().GetBool(clc.PropertyVerbose) ie := ec.Props().GetBool(flagIgnoreErrors) echo := ec.Props().GetBool(flagEcho) - textFn := makeTextFunc(m, ec, verbose, ie, echo, func(shortcut string) bool { + textFn := makeTextFunc(m, ec, verbose, false, false, func(shortcut string) bool { // shortcuts are not supported in the script mode return false }) diff --git a/base/commands/shell.go b/base/commands/shell.go index c92208adf..33acda468 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -43,6 +43,7 @@ func (cm *ShellCommand) Init(cc plug.InitContext) error { cc.Hide() cm.mu.Lock() cm.shortcuts = map[string]struct{}{ + `\di`: {}, `\dm`: {}, `\dm+`: {}, `\exit`: {}, diff --git a/base/commands/shell_script_common.go b/base/commands/shell_script_common.go index 29c9e1a08..d9e4f078a 100644 --- a/base/commands/shell_script_common.go +++ b/base/commands/shell_script_common.go @@ -13,7 +13,6 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/shell" - "github.com/hazelcast/hazelcast-commandline-client/clc/sql" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -65,7 +64,7 @@ func makeTextFunc(m *cmd.Main, ec plug.ExecContext, verbose, ignoreErrors, echo return m.Execute(ctx, args...) } } - text, err := shell.ConvertStatement(text) + f, err := shell.ConvertStatement(ctx, ec, text, verbose) if err != nil { if errors.Is(err, shell.ErrHelp) { check.I2(fmt.Fprintln(stdout, shell.InteractiveHelp())) @@ -73,18 +72,6 @@ func makeTextFunc(m *cmd.Main, ec plug.ExecContext, verbose, ignoreErrors, echo } return err } - f := func() error { - res, stop, err := sql.ExecSQL(ctx, ec, text) - if err != nil { - return err - } - defer stop() - // TODO: update sql.UpdateOutput to use stdout - if err := sql.UpdateOutput(ctx, ec, res, verbose); err != nil { - return err - } - return nil - } if w, ok := ec.(plug.ResultWrapper); ok { return w.WrapResult(f) } diff --git a/base/commands/sql/sql_it_test.go b/base/commands/sql/sql_it_test.go index 331ccf1cb..dc9728af2 100644 --- a/base/commands/sql/sql_it_test.go +++ b/base/commands/sql/sql_it_test.go @@ -10,6 +10,9 @@ import ( "time" "github.com/hazelcast/hazelcast-go-client" + hz "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/serialization" + "github.com/hazelcast/hazelcast-go-client/types" "github.com/stretchr/testify/require" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands" @@ -165,10 +168,43 @@ $ | | %s | | | $-----------------------------------------------------------------------------------------------------------------------------$`, p1, p2, p1, p2) tcx.AssertStdoutDollar(target) }) + // di + tcx.WithReset(func() { + mm, err := tcx.Client.GetMap(ctx, "default") + check.Must(err) + check.Must(addIndex(mm)) + tcx.WriteStdinf("\\di\n") + tcx.AssertStdoutDollarWithPath("testdata/list_indexes.txt") + }) + // di NAME + tcx.WithReset(func() { + mm, err := tcx.Client.GetMap(ctx, "default") + check.Must(err) + check.Must(addIndex(mm)) + tcx.WriteStdinf("\\di default\n") + tcx.AssertStdoutDollarWithPath("testdata/list_indexes.txt") + }) }) }) } +func addIndex(m *hz.Map) error { + err := m.Set(context.Background(), "k1", serialization.JSON(`{"A": 10, "B": 40}`)) + if err != nil { + return err + } + indexConfig := types.IndexConfig{ + Name: "my-index", + Type: types.IndexTypeSorted, + Attributes: []string{"A"}, + BitmapIndexOptions: types.BitmapIndexOptions{UniqueKey: "B", UniqueKeyTransformation: types.UniqueKeyTransformationLong}, + } + if err = m.AddIndex(context.Background(), indexConfig); err != nil { + return err + } + return nil +} + func sqlSuggestion_Interactive(t *testing.T) { tcx := it.TestContext{T: t} tcx.Tester(func(tcx it.TestContext) { diff --git a/base/commands/sql/testdata/list_indexes.txt b/base/commands/sql/testdata/list_indexes.txt new file mode 100644 index 000000000..92a1c8519 --- /dev/null +++ b/base/commands/sql/testdata/list_indexes.txt @@ -0,0 +1,5 @@ +$----------------------------------$ +$ Map Name | Name | Attributes $ +$----------------------------------$ +$ default | my-index | [A] $ +$----------------------------------$ \ No newline at end of file diff --git a/base/commands/sql/testdata/sql_help.txt b/base/commands/sql/testdata/sql_help.txt index 641d50739..15eb86459 100644 --- a/base/commands/sql/testdata/sql_help.txt +++ b/base/commands/sql/testdata/sql_help.txt @@ -1,6 +1,8 @@ $Shortcut Commands:$ +$\di List Indexes$ +$\di MAPPING List Indexes for a specific mapping$ $\dm List mappings$ $\dm MAPPING Display information about a mapping$ $\dm+ MAPPING Describe a mapping$ $\exit Exit the shell$ -$\help Display help for CLC commands$ +$\help Display help for CLC commands$ \ No newline at end of file diff --git a/base/maps/maps.go b/base/maps/maps.go new file mode 100644 index 000000000..e85aeba63 --- /dev/null +++ b/base/maps/maps.go @@ -0,0 +1,74 @@ +package maps + +import ( + "context" + "fmt" + + "github.com/hazelcast/hazelcast-commandline-client/base/commands/object" + "github.com/hazelcast/hazelcast-commandline-client/base/objects" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-go-client/types" +) + +func Indexes(ctx context.Context, ec plug.ExecContext, mapName string) error { + var mapNames []string + if mapName != "" { + mapNames = append(mapNames, mapName) + } else { + maps, err := objects.GetAll(ctx, ec, object.Map, false) + if err != nil { + return err + } + for _, mm := range maps { + mapNames = append(mapNames, mm.Name) + } + } + ci, err := ec.ClientInternal(ctx) + if err != nil { + return err + } + resp, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + allIndexes := make(map[string][]types.IndexConfig) + for _, mn := range mapNames { + sp.SetText(fmt.Sprintf("Getting indexes of map %s", mn)) + req := codec.EncodeMCGetMapConfigRequest(mn) + // If member configurations are different, this may not work well, however it is nothing to do with CLC + resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) + if err != nil { + return nil, err + } + _, _, _, _, _, _, _, _, _, _, globalIndexes := codec.DecodeMCGetMapConfigResponse(resp) + if err != nil { + return nil, err + } + allIndexes[mn] = globalIndexes + } + return allIndexes, nil + }) + stop() + var rows []output.Row + for mn, indexes := range resp.(map[string][]types.IndexConfig) { + for _, index := range indexes { + rows = append(rows, + output.Row{ + output.Column{ + Name: "Map Name", + Type: serialization.TypeString, + Value: mn, + }, output.Column{ + Name: "Name", + Type: serialization.TypeString, + Value: index.Name, + }, output.Column{ + Name: "Attributes", + Type: serialization.TypeStringArray, + Value: index.Attributes, + }}) + } + } + return ec.AddOutputRows(ctx, rows...) +} diff --git a/base/objects/objects.go b/base/objects/objects.go new file mode 100644 index 000000000..1228df598 --- /dev/null +++ b/base/objects/objects.go @@ -0,0 +1,61 @@ +package objects + +import ( + "context" + "sort" + "strings" + + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-go-client/types" +) + +func GetAll(ctx context.Context, ec plug.ExecContext, typeFilter string, showHidden bool) ([]types.DistributedObjectInfo, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + objs, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText("Getting distributed objects") + return ci.Client().GetDistributedObjectsInfo(ctx) + }) + if err != nil { + return nil, err + } + stop() + var r []types.DistributedObjectInfo + typeFilter = strings.ToLower(typeFilter) + for _, o := range objs.([]types.DistributedObjectInfo) { + if !showHidden && (o.Name == "" || strings.HasPrefix(o.Name, "__")) { + continue + } + if o.Name == "" { + o.Name = "(no name)" + } + if typeFilter == "" { + r = append(r, o) + continue + } + if typeFilter == ShortType(o.ServiceName) { + r = append(r, o) + } + } + sort.Slice(r, func(i, j int) bool { + // first sort by type, then name + ri := r[i] + rj := r[j] + if ri.ServiceName < rj.ServiceName { + return true + } + if ri.ServiceName > rj.ServiceName { + return false + } + return ri.Name < rj.Name + }) + return r, nil +} + +func ShortType(svcName string) string { + s := strings.TrimSuffix(strings.TrimPrefix(svcName, "hz:impl:"), "Service") + return strings.ToLower(s) +} diff --git a/clc/shell/common.go b/clc/shell/common.go index c293e478d..8d66261a3 100644 --- a/clc/shell/common.go +++ b/clc/shell/common.go @@ -1,62 +1,98 @@ package shell import ( + "context" "errors" "fmt" "strings" + + "github.com/hazelcast/hazelcast-commandline-client/base/maps" + "github.com/hazelcast/hazelcast-commandline-client/clc/sql" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) const CmdPrefix = `\` var ErrHelp = errors.New("interactive help") -func ConvertStatement(stmt string) (string, error) { +func ConvertStatement(ctx context.Context, ec plug.ExecContext, stmt string, verbose bool) (func() error, error) { + var query string stmt = strings.TrimSpace(stmt) if strings.HasPrefix(stmt, "help") { - return "", ErrHelp + return nil, ErrHelp } if strings.HasPrefix(stmt, CmdPrefix) { // this is a shell command stmt = strings.TrimPrefix(stmt, CmdPrefix) parts := strings.Fields(stmt) switch parts[0] { - case "dm": + case "di": if len(parts) == 1 { - return "show mappings;", nil + return func() error { + return maps.Indexes(ctx, ec, "") + }, nil } if len(parts) == 2 { + return func() error { + return maps.Indexes(ctx, ec, parts[1]) + }, nil + } else { + return nil, fmt.Errorf("Usage: %sdi [mapping]", CmdPrefix) + } + case "dm": + if len(parts) == 1 { + query = "show mappings;" + } else if len(parts) == 2 { // escape single quote mn := strings.Replace(parts[1], "'", "''", -1) - return fmt.Sprintf(` + query = fmt.Sprintf(` SELECT * FROM information_schema.mappings WHERE table_name = '%s'; - `, mn), nil + `, mn) + } else { + return nil, fmt.Errorf("Usage: %sdm [mapping]", CmdPrefix) } - return "", fmt.Errorf("Usage: %sdm [mapping]", CmdPrefix) case "dm+": if len(parts) == 1 { - return "show mappings;", nil - } - if len(parts) == 2 { + query = "show mappings;" + } else if len(parts) == 2 { // escape single quote mn := strings.Replace(parts[1], "'", "''", -1) - return fmt.Sprintf(` + query = fmt.Sprintf(` SELECT * FROM information_schema.columns WHERE table_name = '%s'; - `, mn), nil + `, mn) + } else { + return nil, fmt.Errorf("Usage: %sdm+ [mapping]", CmdPrefix) } - return "", fmt.Errorf("Usage: %sdm+ [mapping]", CmdPrefix) case "exit": - return "", ErrExit + return nil, ErrExit + default: + return nil, fmt.Errorf("Unknown shell command: %s", stmt) + } + } else { + query = stmt + } + f := func() error { + res, stop, err := sql.ExecSQL(ctx, ec, query) + if err != nil { + return err + } + defer stop() + // TODO: update sql.UpdateOutput to use stdout + if err := sql.UpdateOutput(ctx, ec, res, verbose); err != nil { + return err } - return "", fmt.Errorf("Unknown shell command: %s", stmt) + return nil } - return stmt, nil + return f, nil } func InteractiveHelp() string { return ` Shortcut Commands: + \di List Indexes + \di MAPPING List Indexes for a specific mapping \dm List mappings \dm MAPPING Display information about a mapping \dm+ MAPPING Describe a mapping diff --git a/docs/modules/ROOT/pages/clc.adoc b/docs/modules/ROOT/pages/clc.adoc index 9d1bf209a..d48a14d05 100644 --- a/docs/modules/ROOT/pages/clc.adoc +++ b/docs/modules/ROOT/pages/clc.adoc @@ -52,3 +52,76 @@ You can enter multiline commands by ending all lines except the last line with a replicatedmap | __sql.catalog ------------------------------------------------- ---- + +== Shortcut Commands + +* <> +* <> +* <> +* <> + +== exit +Exits the shell. + +Usage: + +[source,bash] +---- +exit +---- + +== help +Display help for CLC commands + +Usage: + +[source,bash] +---- +help +---- + +== di +Lists indexes. If you provide a mapping, it lists indexes to that specific mapping, otherwise it lists all the indexes. + +Usage: + +[source,bash] +---- +di [MAPPING] +---- + +Parameters: + +[cols="1m,1a,2a,1a"] +|=== +|Parameter|Required|Description|Default + +|`MAPPING` +|Optional +|Name of the mapping. +| + +|=== + +== dm(+) +If you don't provide the `MAPPING` parameter, it lists mappings. If you add `+` postfix, it describes the given mapping, otherwise it displays information about it. + +Usage: + +[source,bash] +---- +dm(+) [MAPPING] +---- + +Parameters: + +[cols="1m,1a,2a,1a"] +|=== +|Parameter|Required|Description|Default + +|`MAPPING` +|Optional +|Name of the mapping. +| + +|==== \ No newline at end of file diff --git a/internal/proto/codec/bitmap_index_options_codec.go b/internal/proto/codec/bitmap_index_options_codec.go new file mode 100644 index 000000000..fe3fe8cdd --- /dev/null +++ b/internal/proto/codec/bitmap_index_options_codec.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2008-2021, Hazelcast, Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package codec + +import ( + proto "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/types" +) + +const ( + BitmapIndexOptionsCodecUniqueKeyTransformationFieldOffset = 0 + BitmapIndexOptionsCodecUniqueKeyTransformationInitialFrameSize = BitmapIndexOptionsCodecUniqueKeyTransformationFieldOffset + proto.IntSizeInBytes +) + +func DecodeBitmapIndexOptions(frameIterator *proto.ForwardFrameIterator) types.BitmapIndexOptions { + // begin frame + frameIterator.Next() + initialFrame := frameIterator.Next() + uniqueKeyTransformation := DecodeInt(initialFrame.Content, BitmapIndexOptionsCodecUniqueKeyTransformationFieldOffset) + + uniqueKey := DecodeString(frameIterator) + FastForwardToEndFrame(frameIterator) + return types.BitmapIndexOptions{UniqueKey: uniqueKey, UniqueKeyTransformation: types.UniqueKeyTransformation(uniqueKeyTransformation)} +} diff --git a/internal/proto/codec/builtin.go b/internal/proto/codec/builtin.go index 9f012494c..0482768c7 100644 --- a/internal/proto/codec/builtin.go +++ b/internal/proto/codec/builtin.go @@ -266,3 +266,23 @@ func DecodeListMultiFrameForData(frameIterator *proto.ForwardFrameIterator) []*i frameIterator.Next() return result } + +func DecodeListMultiFrameForIndexConfig(frameIterator *proto.ForwardFrameIterator) []types.IndexConfig { + var result []types.IndexConfig + if frameIterator.HasNext() { + frameIterator.Next() + + for !NextFrameIsDataStructureEndFrame(frameIterator) { + result = append(result, DecodeIndexConfig(frameIterator)) + } + frameIterator.Next() + } + return result +} + +func DecodeNullableForBitmapIndexOptions(frameIterator *proto.ForwardFrameIterator) types.BitmapIndexOptions { + if NextFrameIsNullFrame(frameIterator) { + return types.BitmapIndexOptions{} + } + return DecodeBitmapIndexOptions(frameIterator) +} diff --git a/internal/proto/codec/index_config_codec.go b/internal/proto/codec/index_config_codec.go new file mode 100644 index 000000000..8be4dffe9 --- /dev/null +++ b/internal/proto/codec/index_config_codec.go @@ -0,0 +1,47 @@ +/* +* Copyright (c) 2008-2022, Hazelcast, Inc. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License") +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. + */ + +package codec + +import ( + proto "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/types" + pubtypes "github.com/hazelcast/hazelcast-go-client/types" +) + +const ( + IndexConfigCodecTypeFieldOffset = 0 + IndexConfigCodecTypeInitialFrameSize = IndexConfigCodecTypeFieldOffset + proto.IntSizeInBytes +) + +func DecodeIndexConfig(frameIterator *proto.ForwardFrameIterator) pubtypes.IndexConfig { + // begin frame + frameIterator.Next() + initialFrame := frameIterator.Next() + _type := DecodeInt(initialFrame.Content, IndexConfigCodecTypeFieldOffset) + + name := DecodeNullableForString(frameIterator) + attributes := DecodeListMultiFrameForString(frameIterator) + bitmapIndexOptions := DecodeNullableForBitmapIndexOptions(frameIterator) + FastForwardToEndFrame(frameIterator) + + return pubtypes.IndexConfig{ + Name: name, + Type: types.IndexType(_type), + Attributes: attributes, + BitmapIndexOptions: bitmapIndexOptions, + } +} diff --git a/internal/proto/codec/mc_get_map_config_codec.go b/internal/proto/codec/mc_get_map_config_codec.go new file mode 100644 index 000000000..b65714348 --- /dev/null +++ b/internal/proto/codec/mc_get_map_config_codec.go @@ -0,0 +1,73 @@ +/* +* Copyright (c) 2008-2022, Hazelcast, Inc. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License") +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. + */ + +package codec + +import ( + proto "github.com/hazelcast/hazelcast-go-client" + pubtypes "github.com/hazelcast/hazelcast-go-client/types" +) + +const ( + MCGetMapConfigCodecRequestMessageType = int32(0x200300) + MCGetMapConfigCodecResponseMessageType = int32(0x200301) + + MCGetMapConfigCodecRequestInitialFrameSize = proto.PartitionIDOffset + proto.IntSizeInBytes + + MCGetMapConfigResponseInMemoryFormatOffset = proto.ResponseBackupAcksOffset + proto.ByteSizeInBytes + MCGetMapConfigResponseBackupCountOffset = MCGetMapConfigResponseInMemoryFormatOffset + proto.IntSizeInBytes + MCGetMapConfigResponseAsyncBackupCountOffset = MCGetMapConfigResponseBackupCountOffset + proto.IntSizeInBytes + MCGetMapConfigResponseTimeToLiveSecondsOffset = MCGetMapConfigResponseAsyncBackupCountOffset + proto.IntSizeInBytes + MCGetMapConfigResponseMaxIdleSecondsOffset = MCGetMapConfigResponseTimeToLiveSecondsOffset + proto.IntSizeInBytes + MCGetMapConfigResponseMaxSizeOffset = MCGetMapConfigResponseMaxIdleSecondsOffset + proto.IntSizeInBytes + MCGetMapConfigResponseMaxSizePolicyOffset = MCGetMapConfigResponseMaxSizeOffset + proto.IntSizeInBytes + MCGetMapConfigResponseReadBackupDataOffset = MCGetMapConfigResponseMaxSizePolicyOffset + proto.IntSizeInBytes + MCGetMapConfigResponseEvictionPolicyOffset = MCGetMapConfigResponseReadBackupDataOffset + proto.BooleanSizeInBytes +) + +// Gets the config of a map on the member it's called on. + +func EncodeMCGetMapConfigRequest(mapName string) *proto.ClientMessage { + clientMessage := proto.NewClientMessageForEncode() + clientMessage.SetRetryable(true) + + initialFrame := proto.NewFrameWith(make([]byte, MCGetMapConfigCodecRequestInitialFrameSize), proto.UnfragmentedMessage) + clientMessage.AddFrame(initialFrame) + clientMessage.SetMessageType(MCGetMapConfigCodecRequestMessageType) + clientMessage.SetPartitionId(-1) + + EncodeString(clientMessage, mapName) + + return clientMessage +} + +func DecodeMCGetMapConfigResponse(clientMessage *proto.ClientMessage) (inMemoryFormat int32, backupCount int32, asyncBackupCount int32, timeToLiveSeconds int32, maxIdleSeconds int32, maxSize int32, maxSizePolicy int32, readBackupData bool, evictionPolicy int32, mergePolicy string, globalIndexes []pubtypes.IndexConfig) { + frameIterator := clientMessage.FrameIterator() + initialFrame := frameIterator.Next() + + inMemoryFormat = DecodeInt(initialFrame.Content, MCGetMapConfigResponseInMemoryFormatOffset) + backupCount = DecodeInt(initialFrame.Content, MCGetMapConfigResponseBackupCountOffset) + asyncBackupCount = DecodeInt(initialFrame.Content, MCGetMapConfigResponseAsyncBackupCountOffset) + timeToLiveSeconds = DecodeInt(initialFrame.Content, MCGetMapConfigResponseTimeToLiveSecondsOffset) + maxIdleSeconds = DecodeInt(initialFrame.Content, MCGetMapConfigResponseMaxIdleSecondsOffset) + maxSize = DecodeInt(initialFrame.Content, MCGetMapConfigResponseMaxSizeOffset) + maxSizePolicy = DecodeInt(initialFrame.Content, MCGetMapConfigResponseMaxSizePolicyOffset) + readBackupData = DecodeBoolean(initialFrame.Content, MCGetMapConfigResponseReadBackupDataOffset) + evictionPolicy = DecodeInt(initialFrame.Content, MCGetMapConfigResponseEvictionPolicyOffset) + mergePolicy = DecodeString(frameIterator) + globalIndexes = DecodeListMultiFrameForIndexConfig(frameIterator) + return inMemoryFormat, backupCount, asyncBackupCount, timeToLiveSeconds, maxIdleSeconds, maxSize, maxSizePolicy, readBackupData, evictionPolicy, mergePolicy, globalIndexes +} From f3c8ab68c2d218a228b0085fe811586690029ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 29 Aug 2023 16:33:14 +0300 Subject: [PATCH 39/79] Skip sql_NonInteractiveStreamingTest test (#359) --- base/commands/sql/sql_it_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/base/commands/sql/sql_it_test.go b/base/commands/sql/sql_it_test.go index dc9728af2..60c2c4264 100644 --- a/base/commands/sql/sql_it_test.go +++ b/base/commands/sql/sql_it_test.go @@ -68,6 +68,7 @@ func sql_NonInteractiveTest(t *testing.T) { } func sql_NonInteractiveStreamingTest(t *testing.T) { + it.MarkFlaky(t, "https://github.com/hazelcast/hazelcast-commandline-client/issues/357") tcx := it.TestContext{T: t} tcx.Tester(func(tcx it.TestContext) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) From 4daf3b029e3a7ceb72583704dbd3b72c79612dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 31 Aug 2023 11:56:37 +0300 Subject: [PATCH 40/79] [CLC-296] Install Script (#356) Added the install script --- extras/unix/install.sh | 379 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 extras/unix/install.sh diff --git a/extras/unix/install.sh b/extras/unix/install.sh new file mode 100644 index 000000000..8d29e51d7 --- /dev/null +++ b/extras/unix/install.sh @@ -0,0 +1,379 @@ +#! /bin/bash + +# Hazelcast CLC Install script +# (c) 2023 Hazelcast, Inc. + +set -eu -o pipefail + +check_ok () { + local what="$1" + local e=no + which "$what" > /dev/null && e=yes + case "$what" in + awk*) state_awk_ok=$e;; + curl*) state_curl_ok=$e;; + tar*) state_tar_ok=$e;; + unzip*) state_unzip_ok=$e;; + wget*) state_wget_ok=$e;; + xattr*) state_xattr_ok=$e;; + zsh*) state_zsh_ok=$e;; + *) log_debug "invalid check: $what" + esac +} + +log_warn () { + echo "WARN $1" 1>&2 +} + +log_info () { + echo "INFO $1" 1>&2 +} + +log_debug () { + if [[ "${state_debug}" == "yes" ]]; then + echo "DEBUG $1" 1>&2 + fi +} + +echo_indent () { + printf " %s\n" "$1" 1>&2 +} + +echo_note () { + echo "NOTE $1" 1>&2 +} + +echo_ok () { + echo " OK $1" 1>&2 +} + +bye () { + if [[ "${1:-}" != "" ]]; then + echo "ERROR $*" 1>&2 + fi + exit 1 +} + +print_usage () { + echo "This script installs Hazelcast CLC to a system or user directory." + echo + echo "Usage: $0 [--beta | --debug | --help]" + echo + echo " --beta Enable downloading BETA and PREVIEW releases" + echo " --debug Enable DEBUG logging" + echo " --help Show help" + echo + exit 0 +} + +setup () { + detect_tmpdir + for cmd in $DEPENDENCIES; do + check_ok "$cmd" + done + detect_httpget +} + +detect_tmpdir () { + state_tmp_dir="${TMPDIR:-/tmp}" +} + +do_curl () { + curl -Ls "$1" +} + +do_wget () { + wget -O- "$1" +} + +detect_uncompress () { + local ext=${state_archive_ext} + if [[ "$ext" == "tar.gz" ]]; then + state_uncompress=do_untar + elif [[ "$ext" == "zip" ]]; then + state_uncompress=do_unzip + else + bye "$ext archive is not supported" + fi +} + +do_untar () { + if [[ "$state_tar_ok" != "yes" ]]; then + bye "tar is required for install" + fi + local path="$1" + local base="$2" + tar xf "$path" -C "$base" +} + +do_unzip () { + if [[ "$state_unzip_ok" != "yes" ]]; then + bye "unzip is required for install" + fi + local path="$1" + local base="$2" + unzip -o -q "$path" -d "$base" +} + +install_release () { + # create base + local tmp="${state_tmp_dir}" + local base="$tmp/clc" + mkdir -p "$base" + # uncompress release package + local path="${state_archive_path}" + log_debug "UNCOMPRESS $path => $base" + ${state_uncompress} "$path" "$base" + # move files to their place + base="$base/${state_clc_name}" + local bin="$state_bin_dir/clc" + mv_path "$base/clc" "$bin" + local files="README.txt LICENSE.txt" + for item in $files; do + mv_path "$base/$item" "$CLC_HOME/$item" + done + # on MacOS remove the clc binary from quarantine + if [[ "$state_xattr_ok" == "yes" && "$state_os" == "darwin" ]]; then + set +e + remove_from_quarantine "$bin" + set -e + fi +} + +remove_from_quarantine () { + local qa + local path + qa="com.apple.quarantine" + path="$1" + for a in $(xattr "$path"); do + if [[ "$a" == "$qa" ]]; then + log_debug "REMOVE FROM QUARANTINE: $path" + xattr -d $qa "$path" + break + fi + done +} + +update_config_files () { + update_rc "BASH" "$HOME/.bashrc" + if [[ "$state_zsh_ok" == "yes" ]]; then + update_rc "ZSH" "$HOME/.zshenv" + fi +} + +update_rc () { + local prefix="$1" + local path="$2" + local installed="CLC_INSTALLED_$prefix=1" + local code=" +if [[ \"\$CLC_INSTALLED_$prefix\" != \"1\" ]]; then + export CLC_INSTALLED_$prefix=1 + export PATH=\$PATH:${state_bin_dir} +fi +" + if [[ -e "$path" ]]; then + # check if this file is a symbolic link + if [[ -L "$path" ]]; then + log_warn "$path is a symbolic link. Writing to symbolic links is not supported." + echo_indent "You can manually add the following in $path" + echo_indent "$code" + return + fi + local text + text=$(cat "$path" | grep "$installed") + if [[ "$text" != "" ]]; then + # CLC PATH is already exported in this file + log_debug "CLC PATH is already installed in $path" + return + fi + fi + # Add the CLC PATH to this file + printf '\n# Added by Hazelcast CLC installer' >> "$path" + printf "$code" >> "$path" + log_info "Added CLC path to $path" +} + +mv_path () { + log_debug "MOVE $1 to $2" + mv "$1" "$2" +} + +detect_httpget () { + if [[ "${state_curl_ok}" == "yes" ]]; then + state_httpget=do_curl + elif [[ "${state_wget_ok}" == "yes" ]]; then + state_httpget=do_wget + else + bye "either curl or wget is required" + fi + log_debug "state_httpget=$state_httpget" +} + +httpget () { + log_debug "GET ${state_httpget} $1" + ${state_httpget} "$@" +} + +print_banner () { + echo + echo "Hazelcast CLC Installer (c) 2023 Hazelcast, Inc." + echo +} + +print_success () { + echo + echo_ok "Hazelcast CLC ${state_download_version} is installed at $CLC_HOME" + echo + echo_indent 'Next steps:' + echo_indent '1. Open a new terminal,' + echo_indent '2. Run `clc version` to confirm that CLC is installed,' + echo_indent '3. Enjoy!' + maybe_print_old_clc_warning + echo + echo_note 'If the steps above do not work, try copying `clc` binary to your $PATH:' + echo_indent "$ sudo cp $state_bin_dir/clc /usr/local/bin" + echo +} + +maybe_print_old_clc_warning () { + # create and assign the variable separately + # so the exit status is not lost + local clc_path + set +e + clc_path=$(which clc) + set -e + local bin_path="$state_bin_dir/clc" + if [[ "$clc_path" != "" && "$clc_path" != "$bin_path" ]]; then + echo + echo_note "A binary named 'clc' already exists at ${clc_path}." + echo_indent ' You may want to delete it before running the installed CLC.' + echo_indent " $ sudo rm -f ${clc_path}" + fi +} + +detect_last_release () { + if [[ "$state_awk_ok" != "yes" ]]; then + bye "Awk is required for install" + fi + local re + local text + local v + re='$1 ~ /tag_name/ { gsub(/[",]/, "", $2); print($2) }' + text="$(httpget https://api.github.com/repos/hazelcast/hazelcast-commandline-client/releases)" + if [[ "$state_beta" == "yes" ]]; then + v=$(echo "$text" | awk "$re" | head -1) + else + v=$(echo "$text" | awk "$re" | grep -vi preview | grep -vi beta | head -1) + fi + if [[ "$v" == "" ]]; then + bye "could not determine the latest version" + fi + state_download_version="$v" + log_debug "state_download_version=$state_download_version" +} + +detect_platform () { + local os + os="$(uname -s)" + case "$os" in + Linux*) os=linux; ext="tar.gz";; + Darwin*) os=darwin; ext="zip";; + *) bye "This script supports only Linux and MacOS, not $os";; + esac + state_os=$os + log_debug "state_os=$state_os" + state_archive_ext=$ext + arch="$(uname -m)" + case "$arch" in + x86_64*) arch=amd64;; + amd64*) arch=amd64;; + armv6l*) arch=arm;; + armv7l*) arch=arm;; + arm64*) arch=arm64;; + aarch64*) arch=arm64;; + *) bye "This script supports only 64bit Intel and 32/64bit ARM architecture, not $arch" + esac + state_arch="$arch" + log_debug "state_arch=$state_arch" +} + +make_download_url () { + local v=${state_download_version} + local clc_name=${state_clc_name} + local ext=${state_archive_ext} + state_download_url="https://github.com/hazelcast/hazelcast-commandline-client/releases/download/$v/${clc_name}.${ext}" +} + +make_clc_name () { + local v="${state_download_version}" + local os="${state_os}" + local arch="${state_arch}" + state_clc_name="hazelcast-clc_${v}_${os}_${arch}" +} + +create_home () { + log_info "Creating the Home directory: $CLC_HOME" + mkdir -p "$state_bin_dir" "$CLC_HOME/etc" + echo "install-script" > "$CLC_HOME/etc/.source" +} + +download_release () { + detect_tmpdir + detect_platform + detect_uncompress + detect_last_release + make_clc_name + make_download_url + log_info "Downloading: ${state_download_url}" + local tmp + local ext + tmp="${state_tmp_dir}" + ext="${state_archive_ext}" + state_archive_path="$tmp/clc.${ext}" + httpget "${state_download_url}" > "${state_archive_path}" +} + +process_flags () { + for flag in "$@"; do + case "$flag" in + --beta*) state_beta=yes;; + --debug*) state_debug=yes;; + --help*) print_banner; print_usage;; + *) bye "Unknown option: $flag";; + esac + done +} + +DEPENDENCIES="awk wget curl unzip tar xattr zsh" +CLC_HOME="${CLC_HOME:-$HOME/.hazelcast}" + +state_arch= +state_archive_ext= +state_archive_path= +state_beta=no +state_bin_dir="$CLC_HOME/bin" +state_clc_name= +state_debug=no +state_download_url= +state_download_version= +state_httpget= +state_os= +state_tmp_dir= +state_uncompress= + +state_awk_ok=no +state_curl_ok=no +state_tar_ok=no +state_unzip_ok=no +state_wget_ok=no +state_xattr_ok=no +state_zsh_ok=no + +process_flags "$@" +print_banner +setup +create_home +download_release +install_release +update_config_files +print_success From bb86d74df271e91f7aaf8a1d62dfefc58b2e1f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 31 Aug 2023 14:46:25 +0300 Subject: [PATCH 41/79] Fix install script text (#360) --- extras/unix/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extras/unix/install.sh b/extras/unix/install.sh index 8d29e51d7..a42842d67 100644 --- a/extras/unix/install.sh +++ b/extras/unix/install.sh @@ -246,8 +246,8 @@ maybe_print_old_clc_warning () { if [[ "$clc_path" != "" && "$clc_path" != "$bin_path" ]]; then echo echo_note "A binary named 'clc' already exists at ${clc_path}." - echo_indent ' You may want to delete it before running the installed CLC.' - echo_indent " $ sudo rm -f ${clc_path}" + echo_indent 'You may want to delete it before running the installed CLC.' + echo_indent "$ sudo rm -f ${clc_path}" fi } From f4a1883d4489cca0e4691658e24d3c1e95d22f7a Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Mon, 4 Sep 2023 09:21:47 +0300 Subject: [PATCH 42/79] [CLC-258]: Add demo map setmany command (#343) --- base/commands/demo/demo_it_test.go | 13 +++ base/commands/demo/demo_map_set_many.go | 115 ++++++++++++++++++++++++ docs/modules/ROOT/pages/clc-demo.adoc | 39 +++++++- 3 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 base/commands/demo/demo_map_set_many.go diff --git a/base/commands/demo/demo_it_test.go b/base/commands/demo/demo_it_test.go index 3212d07db..b0e7fd3ae 100644 --- a/base/commands/demo/demo_it_test.go +++ b/base/commands/demo/demo_it_test.go @@ -54,3 +54,16 @@ func generateData_Wikipedia_MaxValues_Test(t *testing.T) { }) }) } + +func TestMapSetMany(t *testing.T) { + it.MapTester(t, func(tcx it.TestContext, m *hz.Map) { + t := tcx.T + ctx := context.Background() + count := 10 + tcx.WithReset(func() { + tcx.CLCExecute(ctx, "demo", "map-setmany", "10", "--name", m.Name(), "--size", "1") + require.Equal(t, count, check.MustValue(m.Size(context.Background()))) + require.Equal(t, "a", check.MustValue(m.Get(ctx, "k1"))) + }) + }) +} diff --git a/base/commands/demo/demo_map_set_many.go b/base/commands/demo/demo_map_set_many.go new file mode 100644 index 000000000..3bb32bccf --- /dev/null +++ b/base/commands/demo/demo_map_set_many.go @@ -0,0 +1,115 @@ +//go:build std || demo + +package demo + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hazelcast/hazelcast-commandline-client/clc" + . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-go-client" +) + +type MapSetManyCmd struct{} + +const ( + flagName = "name" + flagSize = "size" + kb = "KB" + mb = "MB" + kbs = 1024 + mbs = kbs * 1024 +) + +func (m MapSetManyCmd) Init(cc plug.InitContext) error { + cc.SetCommandUsage("map-setmany [entry-count] [flags]") + cc.SetPositionalArgCount(1, 1) + cc.AddStringFlag(flagName, "n", "default", false, "Name of the map.") + cc.AddStringFlag(flagSize, "", "", true, `Size of the map value in bytes, the following suffixes can also be used: kb, mb, e.g., 42kb)`) + help := "Generates multiple map entries." + cc.SetCommandHelp(help, help) + return nil +} + +func (m MapSetManyCmd) Exec(ctx context.Context, ec plug.ExecContext) error { + entryCount := ec.Args()[0] + c, err := strconv.Atoi(entryCount) + if err != nil { + return err + } + mapName := ec.Props().GetString(flagName) + size := ec.Props().GetString(flagSize) + ci, err := ec.ClientInternal(ctx) + if err != nil { + return err + } + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText(fmt.Sprintf("Creating entries in map %s with %d entries", mapName, c)) + mm, err := ci.Client().GetMap(ctx, mapName) + if err != nil { + return nil, err + } + return nil, createEntries(ctx, c, size, mm) + }) + if err != nil { + return err + } + stop() + return nil +} + +func createEntries(ctx context.Context, entryCount int, size string, m *hazelcast.Map) error { + v, err := makeValue(size) + if err != nil { + return err + } + for i := 1; i <= entryCount; i++ { + k := fmt.Sprintf("k%d", i) + err := m.Set(ctx, k, v) + if err != nil { + return err + } + } + return nil +} + +func makeValue(size string) (string, error) { + b, err := getValueSize(size) + if err != nil { + return "", err + } + return strings.Repeat("a", int(b)), nil +} + +func getValueSize(sizeStr string) (int64, error) { + sizeStr = strings.ToUpper(sizeStr) + if strings.HasSuffix(sizeStr, kb) { + sizeStr = strings.TrimSuffix(sizeStr, kb) + size, err := strconv.ParseInt(sizeStr, 10, 64) + if err != nil { + return 0, err + } + return size * kbs, nil + } + if strings.HasSuffix(sizeStr, mb) { + sizeStr = strings.TrimSuffix(sizeStr, mb) + size, err := strconv.ParseInt(sizeStr, 10, 64) + if err != nil { + return 0, err + } + return size * mbs, nil + } + size, err := strconv.ParseInt(sizeStr, 10, 64) + if err != nil { + return 0, err + } + return size, nil +} + +func init() { + Must(plug.Registry.RegisterCommand("demo:map-setmany", &MapSetManyCmd{})) +} diff --git a/docs/modules/ROOT/pages/clc-demo.adoc b/docs/modules/ROOT/pages/clc-demo.adoc index d5e090bf0..cdacc2ac8 100644 --- a/docs/modules/ROOT/pages/clc-demo.adoc +++ b/docs/modules/ROOT/pages/clc-demo.adoc @@ -12,8 +12,9 @@ clc demo [command] [options] == Commands * <> +* <> -== clc demo submit +== clc demo generate data Generates stream events @@ -63,3 +64,39 @@ Example: ---- clc demo generate-data wikipedia-event-stream map=wiki-events --preview ---- + +== clc demo map setmany + +Generates multiple map entries. + +Usage: + +[source,bash] +---- +clc demo map-setmany [entry-count] [flags] +---- + +Parameters: + +[cols="1m,1a,2a,1a"] +|=== +|Parameter|Required|Description|Default + +|`--name`, `-n` +|Optional +|Name of the map. +|`default` + +|`--size` +|Optional +|Size of the map value in bytes, the following suffixes can also be used: kb, mb, e.g., 42kb +|`1` + +|=== + +Example: + +[source,bash] +---- +clc demo map-setmany 10 --name myMap --size 1kb +---- \ No newline at end of file From 761876b925c526efc7b7f354c2d12d0060599cfc Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Mon, 4 Sep 2023 12:07:11 +0300 Subject: [PATCH 43/79] [CLC-291] Add prerelease and development tags to create cluster (#355) Add prerelease and development tags to create cluster --- base/commands/viridian/common.go | 7 ++ base/commands/viridian/const.go | 3 +- .../viridian/viridian_cluster_create.go | 15 +++- .../commands/viridian/viridian_cluster_get.go | 5 ++ .../viridian/viridian_cluster_list.go | 10 +++ .../viridian/viridian_cluster_types.go | 68 ------------------- base/commands/viridian/viridian_it_test.go | 2 +- docs/modules/ROOT/pages/clc-viridian.adoc | 36 ++-------- internal/viridian/cluster.go | 4 +- internal/viridian/types.go | 7 +- 10 files changed, 51 insertions(+), 106 deletions(-) delete mode 100644 base/commands/viridian/viridian_cluster_types.go diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 2fb231e3d..6450cf758 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -268,3 +268,10 @@ func fixClusterState(state string) string { state = strings.Replace(state, "STOP", "PAUSE", 1) return state } + +func ClusterType(isDev bool) string { + if isDev { + return "Development" + } + return "Production" +} diff --git a/base/commands/viridian/const.go b/base/commands/viridian/const.go index 7372f8510..2d99ddbb5 100644 --- a/base/commands/viridian/const.go +++ b/base/commands/viridian/const.go @@ -4,7 +4,8 @@ package viridian const ( flagName = "name" - flagClusterType = "cluster-type" + flagPrerelease = "prerelease" + flagDevelopment = "development" flagOutputDir = "output-dir" flagHazelcastVersion = "hazelcast-version" fmtSecretFileName = "%s-%s.secret" diff --git a/base/commands/viridian/viridian_cluster_create.go b/base/commands/viridian/viridian_cluster_create.go index 787cf8b7e..a6b4c02b4 100644 --- a/base/commands/viridian/viridian_cluster_create.go +++ b/base/commands/viridian/viridian_cluster_create.go @@ -28,7 +28,8 @@ Make sure you login before running this command. cc.SetPositionalArgCount(0, 0) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(flagName, "", "", false, "specify the cluster name; if not given an auto-generated name is used.") - cc.AddStringFlag(flagClusterType, "", viridian.ClusterTypeServerless, false, "type for the cluster") + cc.AddBoolFlag(flagDevelopment, "", false, false, "create a development cluster") + cc.AddBoolFlag(flagPrerelease, "", false, false, "create a prerelease cluster") return nil } @@ -38,7 +39,8 @@ func (cm ClusterCreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error return err } name := ec.Props().GetString(flagName) - clusterType := ec.Props().GetString(flagClusterType) + dev := ec.Props().GetBool(flagDevelopment) + prerelease := ec.Props().GetBool(flagPrerelease) hzVersion := ec.Props().GetString(flagHazelcastVersion) csi, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Creating the cluster") @@ -46,7 +48,7 @@ func (cm ClusterCreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return nil, err } - cs, err := api.CreateCluster(ctx, name, clusterType, k8sCluster.ID, hzVersion) + cs, err := api.CreateCluster(ctx, name, getClusterType(dev), k8sCluster.ID, prerelease, hzVersion) if err != nil { return nil, err } @@ -89,6 +91,13 @@ func (cm ClusterCreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error return nil } +func getClusterType(dev bool) string { + if dev { + return viridian.ClusterTypeDevMode + } + return viridian.ClusterTypeServerless +} + func getFirstAvailableK8sCluster(ctx context.Context, api *viridian.API) (viridian.K8sCluster, error) { clusters, err := api.ListAvailableK8sClusters(ctx) if err != nil { diff --git a/base/commands/viridian/viridian_cluster_get.go b/base/commands/viridian/viridian_cluster_get.go index 18e6e2f6e..43df0ee6a 100644 --- a/base/commands/viridian/viridian_cluster_get.go +++ b/base/commands/viridian/viridian_cluster_get.go @@ -102,6 +102,11 @@ func (cm ClusterGetCmd) Exec(ctx context.Context, ec plug.ExecContext) error { Type: serialization.TypeStringArray, Value: regionTitleSlice(c.Regions), }, + output.Column{ + Name: "Cluster Type", + Type: serialization.TypeString, + Value: ClusterType(c.ClusterType.DevMode), + }, ) } return ec.AddOutputRows(ctx, row) diff --git a/base/commands/viridian/viridian_cluster_list.go b/base/commands/viridian/viridian_cluster_list.go index e896ece95..aa3ab6d72 100644 --- a/base/commands/viridian/viridian_cluster_list.go +++ b/base/commands/viridian/viridian_cluster_list.go @@ -50,6 +50,7 @@ func (cm ClusterListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { ec.PrintlnUnnecessary("No clusters found") } rows := make([]output.Row, len(cs)) + verbose := ec.Props().GetBool(clc.PropertyVerbose) for i, c := range cs { rows[i] = output.Row{ output.Column{ @@ -73,6 +74,15 @@ func (cm ClusterListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { Value: c.HazelcastVersion, }, } + if verbose { + rows[i] = append(rows[i], + output.Column{ + Name: "Cluster Type", + Type: serialization.TypeString, + Value: ClusterType(c.ClusterType.DevMode), + }, + ) + } } return ec.AddOutputRows(ctx, rows...) } diff --git a/base/commands/viridian/viridian_cluster_types.go b/base/commands/viridian/viridian_cluster_types.go deleted file mode 100644 index 82020de30..000000000 --- a/base/commands/viridian/viridian_cluster_types.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build std || viridian - -package viridian - -import ( - "context" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" - "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" - "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" -) - -type ClusterTypeListCmd struct{} - -func (ct ClusterTypeListCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("list-cluster-types [flags]") - long := `Lists available cluster types that can be used while creating a Viridian cluster. - -Make sure you login before running this command. -` - short := "Lists Viridian cluster types" - cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(0, 0) - cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") - return nil -} - -func (ct ClusterTypeListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - api, err := getAPI(ec) - if err != nil { - return err - } - verbose := ec.Props().GetBool(clc.PropertyVerbose) - csi, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Retrieving cluster types") - return api.ListClusterTypes(ctx) - }) - if err != nil { - return handleErrorResponse(ec, err) - } - stop() - cs := csi.([]viridian.ClusterType) - var rows []output.Row - for _, c := range cs { - var r output.Row - if verbose { - r = append(r, output.Column{ - Name: "ID", - Type: serialization.TypeInt64, - Value: c.ID, - }) - } - r = append(r, output.Column{ - Name: "Name", - Type: serialization.TypeString, - Value: c.Name, - }) - rows = append(rows, r) - } - return ec.AddOutputRows(ctx, rows...) -} - -func init() { - Must(plug.Registry.RegisterCommand("viridian:list-cluster-types", &ClusterTypeListCmd{})) -} diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index d8bab0eac..125b46807 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -160,7 +160,7 @@ func createCluster_InteractiveTest(t *testing.T) { ensureNoClusterRunning(ctx, tcx) tcx.WithReset(func() { clusterName := it.UniqueClusterName() - tcx.WriteStdinf("\\viridian create-cluster --cluster-type devmode --verbose --name %s \n", clusterName) + tcx.WriteStdinf("\\viridian create-cluster --development --verbose --name %s \n", clusterName) time.Sleep(10 * time.Second) check.Must(waitState(ctx, tcx, "", "RUNNING")) tcx.AssertStdoutContains(fmt.Sprintf("Imported configuration: %s", clusterName)) diff --git a/docs/modules/ROOT/pages/clc-viridian.adoc b/docs/modules/ROOT/pages/clc-viridian.adoc index a8c7def9b..10ff22d0c 100644 --- a/docs/modules/ROOT/pages/clc-viridian.adoc +++ b/docs/modules/ROOT/pages/clc-viridian.adoc @@ -14,7 +14,6 @@ clc viridian [command] [options] == Commands * <> -* <> * <> * <> * <> @@ -65,32 +64,6 @@ Parameters: |=== -== clc viridian list-cluster-types - -Lists available cluster types that can be used while creating a {hazelcast-cloud} cluster. - -Make sure you authenticate with the {hazelcast-cloud} API using `viridian login` before running this command. - -Usage: - -[source,bash] ----- -clc viridian list-cluster-types [flags] ----- - -Parameters: - -[cols="1m,1a,2a,1a"] -|=== -|Parameter|Required|Description|Default - -|`--api-key` -|Optional -|Sets the API key. Overrides the `CLC_VIRIDIAN_API_KEY` environment variable. If not given, one of the existing API keys will be used. -| - -|=== - == clc viridian create-cluster Creates a {hazelcast-cloud} cluster. @@ -120,9 +93,14 @@ Parameters: |Sets the name of the created cluster. If not given, an auto-generated name will be used. | -|`--cluster-type` +|`--development` +|Optional +|Creates a development cluster. +| + +|`--prerelease` |Optional -|Sets the cluster-type for the created cluster. +|Creates the cluster with a prerelease image. | |=== diff --git a/internal/viridian/cluster.go b/internal/viridian/cluster.go index c7a46df50..d82cf3958 100644 --- a/internal/viridian/cluster.go +++ b/internal/viridian/cluster.go @@ -19,11 +19,12 @@ type createClusterRequest struct { Name string `json:"name"` ClusterTypeID int64 `json:"clusterTypeId"` PlanName string `json:"planName"` + Prerelease bool `json:"preRelease"` } type createClusterResponse Cluster -func (a *API) CreateCluster(ctx context.Context, name string, clusterType string, k8sClusterID int, hzVersion string) (Cluster, error) { +func (a *API) CreateCluster(ctx context.Context, name string, clusterType string, k8sClusterID int, prerelease bool, hzVersion string) (Cluster, error) { if name == "" { name = clusterName() } @@ -41,6 +42,7 @@ func (a *API) CreateCluster(ctx context.Context, name string, clusterType string Name: name, ClusterTypeID: clusterTypeID, PlanName: planName, + Prerelease: prerelease, } cluster, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (Cluster, error) { u := a.makeURL("/cluster") diff --git a/internal/viridian/types.go b/internal/viridian/types.go index 3b6532719..b2038a6c8 100644 --- a/internal/viridian/types.go +++ b/internal/viridian/types.go @@ -37,8 +37,9 @@ type K8sCluster struct { } type ClusterType struct { - ID int64 `json:"id"` - Name string `json:"name"` + ID int64 `json:"id"` + Name string `json:"name"` + DevMode bool `json:"devMode"` } type Region struct { @@ -48,5 +49,5 @@ type Region struct { type IP struct { ID int `json:"id"` IP string `json:"ip"` - Description string `json:"description",omitempty` + Description string `json:"description,omitempty"` } From bdf548c58d91c384de5bbefa6a3da3f4d707ebf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Tokg=C3=B6z?= <56408993+mehmettokgoz@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:20:03 +0300 Subject: [PATCH 44/79] Fix configuration wizard banner. (#361) --- clc/config/wizard/input.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clc/config/wizard/input.go b/clc/config/wizard/input.go index 368581760..f641a6571 100644 --- a/clc/config/wizard/input.go +++ b/clc/config/wizard/input.go @@ -131,9 +131,9 @@ For other clusters use the following command: 1. Enter the desired name in the "Configuration Name" field. 2. On Viridian console, visit: - Dashboard -> Connect Client -> Quick connection guide -> Go + Dashboard -> Connect Client -> CLI -3. Copy the text in box 1 and paste it in the "Source" field. +3. Copy the URL in second box and pass it to "Source" field. 4. Navigate to the [Submit] button and press enter. Alternatively, you can use the following command: From 10af31c4f616d1e3d906bb325c704f535dc0eeb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 5 Sep 2023 10:53:35 +0300 Subject: [PATCH 45/79] Moved KeyValue to internal/types package (#366) --- base/commands/config/config_add.go | 5 +++-- clc/config/config.go | 27 ++++++++++++++------------- clc/config/config_test.go | 9 +++++---- clc/config/import.go | 5 +++-- {clc => internal/types}/key_value.go | 6 ++++-- 5 files changed, 29 insertions(+), 23 deletions(-) rename {clc => internal/types}/key_value.go (53%) diff --git a/base/commands/config/config_add.go b/base/commands/config/config_add.go index 21a9b883d..f6ecc41ff 100644 --- a/base/commands/config/config_add.go +++ b/base/commands/config/config_add.go @@ -13,6 +13,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/config" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) type AddCmd struct{} @@ -48,13 +49,13 @@ The following keys are supported: func (cm AddCmd) Exec(_ context.Context, ec plug.ExecContext) error { target := ec.Args()[0] - var opts clc.KeyValues[string, string] + var opts types.KeyValues[string, string] for _, arg := range ec.Args()[1:] { ps := strings.SplitN(arg, "=", 2) if len(ps) != 2 { return fmt.Errorf("invalid key=value pair: %s", arg) } - opts = append(opts, clc.KeyValue[string, string]{ + opts = append(opts, types.KeyValue[string, string]{ Key: ps[0], Value: ps[1], }) diff --git a/clc/config/config.go b/clc/config/config.go index 9d3ab954b..6244779c4 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -15,6 +15,7 @@ import ( "golang.org/x/exp/slices" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" @@ -28,7 +29,7 @@ const ( envClientLabels = "CLC_CLIENT_LABELS" ) -func Create(path string, opts clc.KeyValues[string, string]) (dir, cfgPath string, err error) { +func Create(path string, opts types.KeyValues[string, string]) (dir, cfgPath string, err error) { return createFile(path, func(cfgPath string) (string, []byte, error) { text := CreateYAML(opts) return cfgPath, []byte(text), nil @@ -46,7 +47,7 @@ func CreateJSON(path string, opts map[string]any) (dir, cfgPath string, err erro }) } -func ConvertKeyValuesToMap(kvs clc.KeyValues[string, string]) map[string]any { +func ConvertKeyValuesToMap(kvs types.KeyValues[string, string]) map[string]any { m := map[string]any{} for _, kv := range kvs { mp := m @@ -253,23 +254,23 @@ func DirAndFile(path string) (string, string, error) { return strings.TrimSuffix(d, "/"), f, nil } -func CreateYAML(opts clc.KeyValues[string, string]) string { +func CreateYAML(opts types.KeyValues[string, string]) string { // TODO: refactor this function to be more robust, probably using Viper sb := &strings.Builder{} copySection("", 0, sb, opts) return sb.String() } -func copySection(name string, level int, sb *strings.Builder, opts clc.KeyValues[string, string]) { - slices.SortFunc(opts, func(a, b clc.KeyValue[string, string]) bool { +func copySection(name string, level int, sb *strings.Builder, opts types.KeyValues[string, string]) { + slices.SortFunc(opts, func(a, b types.KeyValue[string, string]) bool { return a.Key < b.Key }) if len(opts) == 0 { return } - var leaves clc.KeyValues[string, string] - var sect clc.KeyValues[string, string] - sub := map[string]clc.KeyValues[string, string]{} + var leaves types.KeyValues[string, string] + var sect types.KeyValues[string, string] + sub := map[string]types.KeyValues[string, string]{} for _, opt := range opts { idx := strings.Index(opt.Key, ".") if idx < 0 { @@ -301,17 +302,17 @@ func copySection(name string, level int, sb *strings.Builder, opts clc.KeyValues for _, opt := range sect { copyOpt(level, sb, opt) } - subSlice := make([]clc.KeyValue[string, clc.KeyValues[string, string]], 0, len(sub)) + subSlice := make([]types.KeyValue[string, types.KeyValues[string, string]], 0, len(sub)) for k, v := range sub { - slices.SortFunc(v, func(a, b clc.KeyValue[string, string]) bool { + slices.SortFunc(v, func(a, b types.KeyValue[string, string]) bool { return a.Key < b.Key }) - subSlice = append(subSlice, clc.KeyValue[string, clc.KeyValues[string, string]]{ + subSlice = append(subSlice, types.KeyValue[string, types.KeyValues[string, string]]{ Key: k, Value: v, }) } - slices.SortFunc(subSlice, func(a, b clc.KeyValue[string, clc.KeyValues[string, string]]) bool { + slices.SortFunc(subSlice, func(a, b types.KeyValue[string, types.KeyValues[string, string]]) bool { return a.Key < b.Key }) for _, ss := range subSlice { @@ -319,7 +320,7 @@ func copySection(name string, level int, sb *strings.Builder, opts clc.KeyValues } } -func copyOpt(level int, sb *strings.Builder, opt clc.KeyValue[string, string]) { +func copyOpt(level int, sb *strings.Builder, opt types.KeyValue[string, string]) { sb.WriteString(strings.Repeat(" ", level*2)) sb.WriteString(opt.Key) sb.WriteString(": ") diff --git a/clc/config/config_test.go b/clc/config/config_test.go index 45b4e4f49..26fa5c358 100644 --- a/clc/config/config_test.go +++ b/clc/config/config_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" @@ -191,7 +192,7 @@ func TestConfigDirFile_Windows(t *testing.T) { } func TestCreateYAML(t *testing.T) { - type KV clc.KeyValue[string, string] + type KV types.KeyValue[string, string] testCases := []struct { name string kvs []KV @@ -267,9 +268,9 @@ ssl: } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - kvs := make(clc.KeyValues[string, string], len(tc.kvs)) + kvs := make(types.KeyValues[string, string], len(tc.kvs)) for i, kv := range tc.kvs { - kvs[i] = *(*clc.KeyValue[string, string])(&kv) + kvs[i] = *(*types.KeyValue[string, string])(&kv) } s := config.CreateYAML(kvs) t.Logf(s) @@ -279,7 +280,7 @@ ssl: } func TestConvertKeyValuesToMap(t *testing.T) { - kvs := clc.KeyValues[string, string]{ + kvs := types.KeyValues[string, string]{ {Key: "cluster.name", Value: "de-foobar"}, {Key: "ssl.ca-path", Value: "ca.pem"}, {Key: "cluster.discovery-token", Value: "tok123"}, diff --git a/clc/config/import.go b/clc/config/import.go index 5e201cada..d991fcf03 100644 --- a/clc/config/import.go +++ b/clc/config/import.go @@ -15,6 +15,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) func ImportSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, error) { @@ -186,8 +187,8 @@ func CreateFromZipLegacy(ctx context.Context, ec plug.ExecContext, target, path return p.(string), true, nil } -func makeViridianOpts(clusterName, token, password, apiBaseURL string) clc.KeyValues[string, string] { - return clc.KeyValues[string, string]{ +func makeViridianOpts(clusterName, token, password, apiBaseURL string) types.KeyValues[string, string] { + return types.KeyValues[string, string]{ {Key: "cluster.name", Value: clusterName}, {Key: "cluster.discovery-token", Value: token}, {Key: "cluster.api-base", Value: apiBaseURL}, diff --git a/clc/key_value.go b/internal/types/key_value.go similarity index 53% rename from clc/key_value.go rename to internal/types/key_value.go index c44bd84e1..975cd718a 100644 --- a/clc/key_value.go +++ b/internal/types/key_value.go @@ -1,8 +1,10 @@ -package clc +package types import "golang.org/x/exp/constraints" -type KeyValue[K, V any] struct { +// TODO: consolidate KeyValue with Pair2 + +type KeyValue[K constraints.Ordered, V any] struct { Key K Value V } From 70bac97bef66887b025bbf73955e5f068b91f9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Wed, 6 Sep 2023 17:35:52 +0300 Subject: [PATCH 46/79] Named positional arguments (#365) Introduced named positional arguments --- .../atomic_long/atomic_long_decrement_get.go | 5 +- base/commands/atomic_long/atomic_long_get.go | 3 +- .../atomic_long/atomic_long_increment_get.go | 5 +- base/commands/atomic_long/atomic_long_set.go | 15 +- base/commands/config/config_add.go | 21 +- base/commands/config/config_import.go | 5 +- base/commands/config/config_list.go | 1 - base/commands/config/const.go | 12 + base/commands/demo/const.go | 5 +- base/commands/demo/demo_generate_data.go | 37 ++- base/commands/demo/demo_map_set_many.go | 37 ++- base/commands/home.go | 29 ++- base/commands/home_test.go | 44 ++-- base/commands/job/common.go | 4 +- base/commands/job/const.go | 2 + base/commands/job/job_export_snapshot.go | 6 +- base/commands/job/job_list.go | 1 - base/commands/job/job_resume.go | 6 +- base/commands/job/job_submit.go | 16 +- base/commands/job/job_terminate.go | 4 +- base/commands/list/const.go | 4 + base/commands/list/list_add.go | 10 +- base/commands/list/list_clear.go | 2 +- base/commands/list/list_contains.go | 10 +- base/commands/list/list_destroy.go | 2 +- base/commands/list/list_remove_index.go | 12 +- base/commands/list/list_remove_value.go | 8 +- base/commands/list/list_set.go | 15 +- base/commands/list/list_size.go | 3 +- base/commands/map/const.go | 2 + base/commands/map/map.go | 8 +- base/commands/map/map_clear.go | 2 +- base/commands/map/map_destroy.go | 2 +- base/commands/map/map_entry_set.go | 3 +- base/commands/map/map_get.go | 6 +- base/commands/map/map_key_set.go | 3 +- base/commands/map/map_load_all.go | 14 +- base/commands/map/map_lock.go | 8 +- base/commands/map/map_remove.go | 8 +- base/commands/map/map_set.go | 21 +- base/commands/map/map_size.go | 3 +- base/commands/map/map_try_lock.go | 14 +- base/commands/map/map_unlock.go | 8 +- base/commands/map/map_values.go | 3 +- base/commands/multimap/const.go | 2 + base/commands/multimap/multimap_clear.go | 2 +- base/commands/multimap/multimap_destroy.go | 2 +- base/commands/multimap/multimap_entry_set.go | 1 - base/commands/multimap/multimap_get.go | 8 +- base/commands/multimap/multimap_key_set.go | 3 +- base/commands/multimap/multimap_lock.go | 8 +- base/commands/multimap/multimap_put.go | 21 +- base/commands/multimap/multimap_remove.go | 8 +- base/commands/multimap/multimap_size.go | 3 +- base/commands/multimap/multimap_try_lock.go | 10 +- base/commands/multimap/multimap_unlock.go | 8 +- base/commands/multimap/multimap_values.go | 1 - base/commands/object/object_list.go | 13 +- base/commands/project/help.go | 60 +++++ base/commands/project/project_create.go | 71 ++---- .../project/project_list_templates.go | 7 +- base/commands/project/utils.go | 16 +- base/commands/queue/queue_clear.go | 2 +- base/commands/queue/queue_destroy.go | 2 +- base/commands/queue/queue_offer.go | 17 +- base/commands/queue/queue_poll.go | 7 +- base/commands/queue/queue_size.go | 3 +- base/commands/script.go | 14 +- base/commands/set/const.go | 2 + base/commands/set/set_add.go | 9 +- base/commands/set/set_clear.go | 2 +- base/commands/set/set_destroy.go | 2 +- base/commands/set/set_get_all.go | 3 +- base/commands/set/set_remove.go | 9 +- base/commands/set/set_size.go | 3 +- base/commands/shell.go | 1 - base/commands/snapshot/snapshot_delete.go | 11 +- base/commands/snapshot/snapshot_list.go | 6 +- base/commands/sql/sql.go | 8 +- base/commands/topic/topic_destroy.go | 2 +- base/commands/topic/topic_publish.go | 14 +- base/commands/topic/topic_subscribe.go | 2 +- base/commands/version.go | 19 +- base/commands/version_test.go | 28 +-- base/commands/viridian/const.go | 4 + base/commands/viridian/custom_class_delete.go | 9 +- .../viridian/custom_class_download.go | 9 +- base/commands/viridian/custom_class_list.go | 6 +- base/commands/viridian/custom_class_upload.go | 16 +- base/commands/viridian/download_logs.go | 6 +- .../viridian/viridian_cluster_create.go | 3 +- .../viridian/viridian_cluster_delete.go | 6 +- .../commands/viridian/viridian_cluster_get.go | 6 +- .../viridian/viridian_cluster_list.go | 1 - .../viridian/viridian_cluster_resume.go | 6 +- .../viridian/viridian_cluster_stop.go | 6 +- .../viridian/viridian_import_config.go | 6 +- base/commands/viridian/viridian_log_stream.go | 6 +- base/commands/viridian/viridian_login.go | 1 - clc/cmd/clc.go | 18 +- clc/cmd/cmd_test.go | 214 ++++++++++++++++++ clc/cmd/cobra.go | 2 +- clc/cmd/command_context.go | 162 +++++++++++-- clc/cmd/exec_context.go | 118 +++++++++- clc/const.go | 1 + internal/it/context.go | 56 +++-- internal/maps/maps.go | 55 +++++ internal/plug/context.go | 10 +- internal/types/key_value.go | 8 + 109 files changed, 1083 insertions(+), 491 deletions(-) create mode 100644 base/commands/config/const.go create mode 100644 base/commands/project/help.go create mode 100644 clc/cmd/cmd_test.go create mode 100644 internal/maps/maps.go diff --git a/base/commands/atomic_long/atomic_long_decrement_get.go b/base/commands/atomic_long/atomic_long_decrement_get.go index 2b194a192..e93d894f3 100644 --- a/base/commands/atomic_long/atomic_long_decrement_get.go +++ b/base/commands/atomic_long/atomic_long_decrement_get.go @@ -12,11 +12,10 @@ import ( type AtomicLongDecrementGetCommand struct{} func (mc *AtomicLongDecrementGetCommand) Init(cc plug.InitContext) error { - cc.SetPositionalArgCount(0, 0) + cc.SetCommandUsage("decrement-get") help := "Decrement the AtomicLong by the given value" - cc.AddIntFlag(atomicLongFlagBy, "", 1, false, "value to decrement by") cc.SetCommandHelp(help, help) - cc.SetCommandUsage("decrement-get [flags]") + cc.AddIntFlag(atomicLongFlagBy, "", 1, false, "value to decrement by") return nil } diff --git a/base/commands/atomic_long/atomic_long_get.go b/base/commands/atomic_long/atomic_long_get.go index f1e893216..12d2dee91 100644 --- a/base/commands/atomic_long/atomic_long_get.go +++ b/base/commands/atomic_long/atomic_long_get.go @@ -18,10 +18,9 @@ import ( type AtomicLongGetCommand struct{} func (mc *AtomicLongGetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("get") help := "Get the value of the AtomicLong" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("get") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/atomic_long/atomic_long_increment_get.go b/base/commands/atomic_long/atomic_long_increment_get.go index c4978de09..a50e5ee43 100644 --- a/base/commands/atomic_long/atomic_long_increment_get.go +++ b/base/commands/atomic_long/atomic_long_increment_get.go @@ -12,11 +12,10 @@ import ( type AtomicLongIncrementGetCommand struct{} func (mc *AtomicLongIncrementGetCommand) Init(cc plug.InitContext) error { - cc.SetPositionalArgCount(0, 0) + cc.SetCommandUsage("increment-get") help := "Increment the atomic long by the given value" - cc.AddIntFlag(atomicLongFlagBy, "", 1, false, "value to increment by") cc.SetCommandHelp(help, help) - cc.SetCommandUsage("increment-get [flags]") + cc.AddIntFlag(atomicLongFlagBy, "", 1, false, "value to increment by") return nil } diff --git a/base/commands/atomic_long/atomic_long_set.go b/base/commands/atomic_long/atomic_long_set.go index 82134db78..a3e08ce03 100644 --- a/base/commands/atomic_long/atomic_long_set.go +++ b/base/commands/atomic_long/atomic_long_set.go @@ -5,7 +5,6 @@ package atomiclong import ( "context" "fmt" - "strconv" "github.com/hazelcast/hazelcast-go-client" @@ -14,13 +13,18 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) +const ( + argValue = "value" + argTitleValue = "value" +) + type AtomicLongSetCommand struct{} func (mc *AtomicLongSetCommand) Init(cc plug.InitContext) error { - cc.SetPositionalArgCount(1, 1) + cc.SetCommandUsage("set") help := "Set the value of the AtomicLong" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("set [value] [flags]") + cc.AddInt64Arg(argValue, argTitleValue) return nil } @@ -29,10 +33,7 @@ func (mc *AtomicLongSetCommand) Exec(ctx context.Context, ec plug.ExecContext) e if err != nil { return err } - value, err := strconv.Atoi(ec.Args()[0]) - if err != nil { - return err - } + value := ec.GetInt64Arg(argValue) ali := al.(*hazelcast.AtomicLong) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText(fmt.Sprintf("Setting value of AtomicLong %s", ali.Name())) diff --git a/base/commands/config/config_add.go b/base/commands/config/config_add.go index f6ecc41ff..4ba3427ce 100644 --- a/base/commands/config/config_add.go +++ b/base/commands/config/config_add.go @@ -7,21 +7,21 @@ import ( "fmt" "math" "path/filepath" - "strings" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) type AddCmd struct{} func (cm AddCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("add [configuration-name] [flags]") + cc.SetCommandUsage("add") short := "Adds a configuration" - long := `Adds a configuration with the given name/path and KEY=VALUE pairs + long := `Adds a configuration with the given KEY=VALUE pairs and saves it with configuration name. Overrides the previous configuration if it exists. @@ -43,21 +43,22 @@ The following keys are supported: ` cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, math.MaxInt) + cc.AddStringArg(argConfigName, argTitleConfigName) + cc.AddStringSliceArg(argKeyValues, argTitleKeyValues, 0, math.MaxInt) return nil } func (cm AddCmd) Exec(_ context.Context, ec plug.ExecContext) error { - target := ec.Args()[0] + target := ec.GetStringArg(argConfigName) var opts types.KeyValues[string, string] - for _, arg := range ec.Args()[1:] { - ps := strings.SplitN(arg, "=", 2) - if len(ps) != 2 { + for _, arg := range ec.GetStringSliceArg(argKeyValues) { + k, v := str.ParseKeyValue(arg) + if k == "" { return fmt.Errorf("invalid key=value pair: %s", arg) } opts = append(opts, types.KeyValue[string, string]{ - Key: ps[0], - Value: ps[1], + Key: k, + Value: v, }) } dir, cfgPath, err := config.Create(target, opts) diff --git a/base/commands/config/config_import.go b/base/commands/config/config_import.go index a6047a99e..04cdabd9a 100644 --- a/base/commands/config/config_import.go +++ b/base/commands/config/config_import.go @@ -15,7 +15,7 @@ import ( type ImportCmd struct{} func (cm ImportCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("import [configuration-name] [source] [flags]") + cc.SetCommandUsage("import") short := "Imports configuration from an arbitrary source" long := `Imports configuration from an arbitrary source @@ -32,7 +32,8 @@ Currently importing Viridian connection configuration is supported only. ` cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(2, 2) + cc.AddStringArg(argConfigName, argTitleConfigName) + cc.AddStringArg(argSource, argTitleSource) return nil } diff --git a/base/commands/config/config_list.go b/base/commands/config/config_list.go index 6a29de9ee..f2ed0af5c 100644 --- a/base/commands/config/config_list.go +++ b/base/commands/config/config_list.go @@ -25,7 +25,6 @@ Directory names which start with . or _ are ignored. `, paths.Configs()) short := "Lists known configurations" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/config/const.go b/base/commands/config/const.go new file mode 100644 index 000000000..11007d738 --- /dev/null +++ b/base/commands/config/const.go @@ -0,0 +1,12 @@ +//go:build std || config + +package config + +const ( + argConfigName = "configName" + argTitleConfigName = "configuration name" + argKeyValues = "keyValues" + argTitleKeyValues = "key=value" + argSource = "source" + argTitleSource = "source" +) diff --git a/base/commands/demo/const.go b/base/commands/demo/const.go index fb05a574c..eaf7043c2 100644 --- a/base/commands/demo/const.go +++ b/base/commands/demo/const.go @@ -3,8 +3,5 @@ package demo const ( - GroupDemoID = "demo" - flagPreview = "preview" - flagMaxValues = "max-values" - pairMapName = "map" + GroupDemoID = "demo" ) diff --git a/base/commands/demo/demo_generate_data.go b/base/commands/demo/demo_generate_data.go index 7db0262de..f9aa8bea9 100644 --- a/base/commands/demo/demo_generate_data.go +++ b/base/commands/demo/demo_generate_data.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "fmt" - "math" "sync/atomic" "time" @@ -20,7 +19,16 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/demo/wikimedia" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/str" +) + +const ( + flagPreview = "preview" + flagMaxValues = "max-values" + pairMapName = "map" + argGeneratorName = "name" + argTitleGeneratorName = "generator name" + argKeyValues = "keyValue" + argTitleKeyValues = "key=value" ) type DataStreamGenerator interface { @@ -35,7 +43,7 @@ var supportedEventStreams = map[string]DataStreamGenerator{ type GenerateDataCmd struct{} func (cm GenerateDataCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("generate-data [name] [key=value, ...] [--preview]") + cc.SetCommandUsage("generate-data") long := `Generates a stream of events Generate data for given name, supported names are: @@ -47,26 +55,27 @@ Generate data for given name, supported names are: ` short := "Generates a stream of events" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, math.MaxInt) cc.AddIntFlag(flagMaxValues, "", 0, false, "number of events to create (default: 0, no limits)") cc.AddBoolFlag(flagPreview, "", false, false, "print the generated data without interacting with the cluster") + cc.AddStringArg(argGeneratorName, argTitleGeneratorName) + cc.AddKeyValueSliceArg(argKeyValues, argTitleKeyValues, 0, clc.MaxArgs) return nil } func (cm GenerateDataCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Args()[0] + name := ec.GetStringArg(argGeneratorName) generator, ok := supportedEventStreams[name] if !ok { return fmt.Errorf("stream generator '%s' is not supported, run --help to see supported ones", name) } - keyVals := keyValMap(ec) + kvs := ec.GetKeyValuesArg(argKeyValues) ch, stopStream := generator.Stream(ctx) defer stopStream() preview := ec.Props().GetBool(flagPreview) if preview { - return generatePreviewResult(ctx, ec, generator, ch, keyVals, stopStream) + return generatePreviewResult(ctx, ec, generator, ch, kvs.Map(), stopStream) } - return generateResult(ctx, ec, generator, ch, keyVals, stopStream) + return generateResult(ctx, ec, generator, ch, kvs.Map(), stopStream) } func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator DataStreamGenerator, itemCh <-chan demo.StreamItem, keyVals map[string]string, stopStream context.CancelFunc) error { @@ -227,18 +236,6 @@ func getMap(ctx context.Context, ec plug.ExecContext, mapName string) (*hazelcas return mv.(*hazelcast.Map), nil } -func keyValMap(ec plug.ExecContext) map[string]string { - keyVals := map[string]string{} - for _, keyval := range ec.Args()[1:] { - k, v := str.ParseKeyValue(keyval) - if k == "" { - continue - } - keyVals[k] = v - } - return keyVals -} - func init() { Must(plug.Registry.RegisterCommand("demo:generate-data", &GenerateDataCmd{})) } diff --git a/base/commands/demo/demo_map_set_many.go b/base/commands/demo/demo_map_set_many.go index 3bb32bccf..466016f8d 100644 --- a/base/commands/demo/demo_map_set_many.go +++ b/base/commands/demo/demo_map_set_many.go @@ -8,39 +8,38 @@ import ( "strconv" "strings" + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-go-client" ) type MapSetManyCmd struct{} const ( - flagName = "name" - flagSize = "size" - kb = "KB" - mb = "MB" - kbs = 1024 - mbs = kbs * 1024 + flagName = "name" + flagSize = "size" + argEntryCount = "entryCount" + argTitleEntryCount = "entry count" + kb = "KB" + mb = "MB" + kbs = 1024 + mbs = kbs * 1024 ) func (m MapSetManyCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("map-setmany [entry-count] [flags]") - cc.SetPositionalArgCount(1, 1) - cc.AddStringFlag(flagName, "n", "default", false, "Name of the map.") - cc.AddStringFlag(flagSize, "", "", true, `Size of the map value in bytes, the following suffixes can also be used: kb, mb, e.g., 42kb)`) - help := "Generates multiple map entries." + cc.SetCommandUsage("map-setmany") + help := "Generates multiple map entries" cc.SetCommandHelp(help, help) + cc.AddStringFlag(flagName, "n", "default", false, "Name of the map.") + cc.AddStringFlag(flagSize, "", "1", false, `Size of the map value in bytes, the following suffixes can also be used: kb, mb, e.g., 42kb)`) + cc.AddInt64Arg(argEntryCount, argTitleEntryCount) return nil } func (m MapSetManyCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - entryCount := ec.Args()[0] - c, err := strconv.Atoi(entryCount) - if err != nil { - return err - } + c := ec.GetInt64Arg(argEntryCount) mapName := ec.Props().GetString(flagName) size := ec.Props().GetString(flagSize) ci, err := ec.ClientInternal(ctx) @@ -62,12 +61,12 @@ func (m MapSetManyCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return nil } -func createEntries(ctx context.Context, entryCount int, size string, m *hazelcast.Map) error { +func createEntries(ctx context.Context, entryCount int64, size string, m *hazelcast.Map) error { v, err := makeValue(size) if err != nil { return err } - for i := 1; i <= entryCount; i++ { + for i := int64(1); i <= entryCount; i++ { k := fmt.Sprintf("k%d", i) err := m.Set(ctx, k, v) if err != nil { diff --git a/base/commands/home.go b/base/commands/home.go index 63d16e1fc..27c064494 100644 --- a/base/commands/home.go +++ b/base/commands/home.go @@ -4,18 +4,25 @@ package commands import ( "context" - "fmt" - "math" "path/filepath" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" +) + +const ( + argSubPath = "subpath" + argTitleSubPath = "subpath" ) type HomeCommand struct{} func (hc HomeCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("home") short := "Print the CLC home directory" long := `Print the CLC home directory @@ -26,19 +33,23 @@ Example: /home/user/.hazelcast/foo/bar ` cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(0, math.MaxInt) - cc.SetCommandUsage("home [subpath ...] [flags]") + cc.AddStringSliceArg(argSubPath, argTitleSubPath, 0, clc.MaxArgs) return nil } func (hc HomeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - dir := paths.Home() - args := ec.Args() + path := paths.Home() + args := ec.GetStringSliceArg(argSubPath) if len(args) > 0 { - dir = filepath.Join(append([]string{dir}, args...)...) + path = filepath.Join(append([]string{path}, args...)...) } - I2(fmt.Fprintln(ec.Stdout(), dir)) - return nil + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Path", + Type: serialization.TypeString, + Value: path, + }, + }) } func (HomeCommand) Unwrappable() {} diff --git a/base/commands/home_test.go b/base/commands/home_test.go index 69ec44702..0dc200b71 100644 --- a/base/commands/home_test.go +++ b/base/commands/home_test.go @@ -4,15 +4,8 @@ package commands_test import ( "context" - "os" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/hazelcast/hazelcast-commandline-client/base/commands" - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/it" "github.com/hazelcast/hazelcast-commandline-client/internal/it/skip" ) @@ -22,8 +15,8 @@ func TestHome(t *testing.T) { name string f func(t *testing.T) }{ - {name: "home", f: homeTest_Unix}, - {name: "homeWithEnv", f: homeWithEnvTest}, + {name: "home_unix", f: homeTest_Unix}, + {name: "home_ArgsUnix", f: homeTest_ArgsUnix}, } for _, tc := range testCases { t.Run(tc.name, tc.f) @@ -32,31 +25,22 @@ func TestHome(t *testing.T) { func homeTest_Unix(t *testing.T) { skip.If(t, "os = windows") - homeTester(t, nil, func(t *testing.T, ec *it.ExecContext) { - output := ec.StdoutText() - target := check.MustValue(os.UserHomeDir()) + "/.hazelcast\n" - assert.Equal(t, target, output) + ctx := context.Background() + tcx := it.TestContext{T: t} + tcx.Tester(func(tcx it.TestContext) { + tcx.CLCExecute(ctx, "home") + tcx.AssertStdoutEquals(tcx.HomePath() + "\n") }) } -// TODO: TestHome_Windows - -func homeWithEnvTest(t *testing.T) { +func homeTest_ArgsUnix(t *testing.T) { skip.If(t, "os = windows") - it.WithEnv(paths.EnvCLCHome, "/home/foo/dir", func() { - homeTester(t, nil, func(t *testing.T, ec *it.ExecContext) { - output := ec.StdoutText() - target := "/home/foo/dir\n" - assert.Equal(t, target, output) - }) + ctx := context.Background() + tcx := it.TestContext{T: t} + tcx.Tester(func(tcx it.TestContext) { + tcx.CLCExecute(ctx, "home", "foo", "bar") + tcx.AssertStdoutEquals(tcx.HomePath() + "/foo/bar\n") }) } -func homeTester(t *testing.T, args []string, f func(t *testing.T, ec *it.ExecContext)) { - cmd := &commands.HomeCommand{} - cc := &it.CommandContext{} - require.NoError(t, cmd.Init(cc)) - ec := it.NewExecuteContext(args) - require.NoError(t, cmd.Exec(context.Background(), ec)) - f(t, ec) -} +// TODO: TestHome_Windows diff --git a/base/commands/job/common.go b/base/commands/job/common.go index 4f38a043b..583f33de4 100644 --- a/base/commands/job/common.go +++ b/base/commands/job/common.go @@ -66,7 +66,7 @@ func idToString(id int64) string { } func terminateJob(ctx context.Context, ec plug.ExecContext, name string, terminateMode int32, text string, waitState int32) error { - nameOrID := ec.Args()[0] + nameOrID := ec.GetStringArg(argJobID) ci, err := ec.ClientInternal(ctx) if err != nil { return err @@ -84,7 +84,7 @@ func terminateJob(ctx context.Context, ec plug.ExecContext, name string, termina } jid, ok := jm.GetIDForName(nameOrID) if !ok { - return nil, jet.ErrInvalidJobID + return nil, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) } ec.Logger().Info("%s %s (%s)", text, nameOrID, idToString(jid)) ji, ok := jm.GetInfoForID(jid) diff --git a/base/commands/job/const.go b/base/commands/job/const.go index 7c9b0c1b1..9bb7f9b7e 100644 --- a/base/commands/job/const.go +++ b/base/commands/job/const.go @@ -12,4 +12,6 @@ const ( flagCancel = "cancel" flagRetries = "retries" flagWait = "wait" + argJobID = "jobID" + argTitleJobID = "job ID or name" ) diff --git a/base/commands/job/job_export_snapshot.go b/base/commands/job/job_export_snapshot.go index 977caa30e..cfa9c8b4c 100644 --- a/base/commands/job/job_export_snapshot.go +++ b/base/commands/job/job_export_snapshot.go @@ -17,13 +17,13 @@ import ( type ExportSnapshotCmd struct{} func (cm ExportSnapshotCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("export-snapshot [job-ID/name]") + cc.SetCommandUsage("export-snapshot") long := "Exports a snapshot for a job.\nThis feature requires a Viridian or Hazelcast Enterprise cluster." short := "Exports a snapshot for a job" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(flagName, "", "", false, "specify the snapshot. By default an auto-genertaed snapshot name is used") cc.AddBoolFlag(flagCancel, "", false, false, "cancel the job after taking the snapshot") + cc.AddStringArg(argJobID, argTitleJobID) return nil } @@ -35,7 +35,7 @@ func (cm ExportSnapshotCmd) Exec(ctx context.Context, ec plug.ExecContext) error var jm *JobsInfo var jid int64 var ok bool - jobNameOrID := ec.Args()[0] + jobNameOrID := ec.GetStringArg(argJobID) name := ec.Props().GetString(flagName) cancel := ec.Props().GetBool(flagCancel) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { diff --git a/base/commands/job/job_list.go b/base/commands/job/job_list.go index 26e55c25e..5f6e94e12 100644 --- a/base/commands/job/job_list.go +++ b/base/commands/job/job_list.go @@ -23,7 +23,6 @@ func (cm ListCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("list") help := "List jobs" cc.SetCommandHelp(help, help) - cc.SetPositionalArgCount(0, 0) cc.AddBoolFlag(flagIncludeSQL, "", false, false, "include SQL jobs") cc.AddBoolFlag(flagIncludeUserCancelled, "", false, false, "include user cancelled jobs") return nil diff --git a/base/commands/job/job_resume.go b/base/commands/job/job_resume.go index ef94ee0a4..7d6cfcf23 100644 --- a/base/commands/job/job_resume.go +++ b/base/commands/job/job_resume.go @@ -16,11 +16,11 @@ import ( type ResumeCmd struct{} func (cm ResumeCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("resume [job-ID/name]") + cc.SetCommandUsage("resume") help := "Resumes a suspended job" cc.SetCommandHelp(help, help) - cc.SetPositionalArgCount(1, 1) cc.AddBoolFlag(flagWait, "", false, false, "wait for the job to be resumed") + cc.AddStringArg(argJobID, argTitleJobID) return nil } @@ -29,7 +29,7 @@ func (cm ResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - nameOrID := ec.Args()[0] + nameOrID := ec.GetStringArg(argJobID) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText(fmt.Sprintf("Resuming job: %s", nameOrID)) j := jet.New(ci, sp, ec.Logger()) diff --git a/base/commands/job/job_submit.go b/base/commands/job/job_submit.go index d194c8c31..b1ac797f5 100644 --- a/base/commands/job/job_submit.go +++ b/base/commands/job/job_submit.go @@ -5,7 +5,6 @@ package job import ( "context" "fmt" - "math" "path/filepath" "strings" "time" @@ -23,12 +22,16 @@ import ( const ( minServerVersion = "5.3.0" + argJarPath = "jarPath" + argTitleJarPath = "jar path" + argArg = "arg" + argTitleArg = "argument" ) type SubmitCmd struct{} func (cm SubmitCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("submit [jar-file] [arg, ...]") + cc.SetCommandUsage("submit") long := fmt.Sprintf(`Submits a jar file to create a Jet job This command requires a Viridian or a Hazelcast cluster having version %s or newer. @@ -40,14 +43,15 @@ This command requires a Viridian or a Hazelcast cluster having version %s or new cc.AddStringFlag(flagClass, "", "", false, "the class that contains the main method that creates the Jet job") cc.AddIntFlag(flagRetries, "", 0, false, "number of times to retry a failed upload attempt") cc.AddBoolFlag(flagWait, "", false, false, "wait for the job to be started") - cc.SetPositionalArgCount(1, math.MaxInt) + cc.AddStringArg(argJarPath, argTitleJarPath) + cc.AddStringSliceArg(argArg, argTitleArg, 0, clc.MaxArgs) return nil } func (cm SubmitCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - path := ec.Args()[0] + path := ec.GetStringArg(argJarPath) if !paths.Exists(path) { - return fmt.Errorf("file does not exists: %s", path) + return fmt.Errorf("file does not exist: %s", path) } if !strings.HasSuffix(path, ".jar") { return fmt.Errorf("submitted file is not a jar file: %s", path) @@ -77,7 +81,7 @@ func submitJar(ctx context.Context, ci *hazelcast.ClientInternal, ec plug.ExecCo tries++ _, fn := filepath.Split(path) fn = strings.TrimSuffix(fn, ".jar") - args := ec.Args()[1:] + args := ec.GetStringSliceArg(argArg) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { j := jet.New(ci, sp, ec.Logger()) err := retry(tries, ec.Logger(), func(try int) error { diff --git a/base/commands/job/job_terminate.go b/base/commands/job/job_terminate.go index d38b9256d..e1d8b30a3 100644 --- a/base/commands/job/job_terminate.go +++ b/base/commands/job/job_terminate.go @@ -22,11 +22,11 @@ type TerminateCmd struct { } func (cm TerminateCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage(fmt.Sprintf("%s [job-ID/name]", cm.name)) + cc.SetCommandUsage(cm.name) cc.SetCommandHelp(cm.longHelp, cm.shortHelp) - cc.SetPositionalArgCount(1, 1) cc.AddBoolFlag(flagForce, "", false, false, fmt.Sprintf("force %s the job", cm.name)) cc.AddBoolFlag(flagWait, "", false, false, "wait for the operation to finish") + cc.AddStringArg(argJobID, argTitleJobID) return nil } diff --git a/base/commands/list/const.go b/base/commands/list/const.go index 1285f2825..8917a5549 100644 --- a/base/commands/list/const.go +++ b/base/commands/list/const.go @@ -7,4 +7,8 @@ const ( listFlagValueType = "value-type" listFlagIndex = "index" defaultListName = "default" + argValue = "value" + argTitleValue = "value" + argIndex = "index" + argTitleIndex = "index" ) diff --git a/base/commands/list/list_add.go b/base/commands/list/list_add.go index 844751add..1f7a1f76b 100644 --- a/base/commands/list/list_add.go +++ b/base/commands/list/list_add.go @@ -17,12 +17,12 @@ import ( type ListAddCommand struct{} func (mc *ListAddCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) - cc.SetPositionalArgCount(1, 1) + cc.SetCommandUsage("add") help := "Add a value in the given list" - cc.AddIntFlag(listFlagIndex, "", -1, false, "index for the value") cc.SetCommandHelp(help, help) - cc.SetCommandUsage("add [value] [flags]") + addValueTypeFlag(cc) + cc.AddIntFlag(listFlagIndex, "", -1, false, "index for the value") + cc.AddStringArg(argValue, argTitleValue) return nil } @@ -36,7 +36,7 @@ func (mc *ListAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { return err } - valueStr := ec.Args()[0] + valueStr := ec.GetStringArg(argValue) vd, err := makeValueData(ec, ci, valueStr) if err != nil { return err diff --git a/base/commands/list/list_clear.go b/base/commands/list/list_clear.go index 6604d5620..aadb7c691 100644 --- a/base/commands/list/list_clear.go +++ b/base/commands/list/list_clear.go @@ -19,10 +19,10 @@ import ( type ListClearCommand struct{} func (mc *ListClearCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("clear") help := "Delete all entries of a List" cc.SetCommandHelp(help, help) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - cc.SetCommandUsage("clear [flags]") return nil } diff --git a/base/commands/list/list_contains.go b/base/commands/list/list_contains.go index 3c13fec7f..ccf1364dd 100644 --- a/base/commands/list/list_contains.go +++ b/base/commands/list/list_contains.go @@ -19,11 +19,11 @@ import ( type ListContainsCommand struct{} func (mc *ListContainsCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) - cc.SetPositionalArgCount(1, 1) - help := "Check if the value is present in the list." + cc.SetCommandUsage("contains") + help := "Check if the value is present in the list" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("contains [value] [flags]") + addValueTypeFlag(cc) + cc.AddStringArg(argValue, argTitleValue) return nil } @@ -37,7 +37,7 @@ func (mc *ListContainsCommand) Exec(ctx context.Context, ec plug.ExecContext) er if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { return err } - valueStr := ec.Args()[0] + valueStr := ec.GetStringArg(argValue) vd, err := makeValueData(ec, ci, valueStr) if err != nil { return err diff --git a/base/commands/list/list_destroy.go b/base/commands/list/list_destroy.go index 03191648d..70b3dfb33 100644 --- a/base/commands/list/list_destroy.go +++ b/base/commands/list/list_destroy.go @@ -21,10 +21,10 @@ func (mc *ListDestroyCommand) Init(cc plug.InitContext) error { long := `Destroy a List This command will delete the List and the data in it will not be available anymore.` + cc.SetCommandUsage("destroy") short := "Destroy a List" cc.SetCommandHelp(long, short) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - cc.SetCommandUsage("destroy") return nil } diff --git a/base/commands/list/list_remove_index.go b/base/commands/list/list_remove_index.go index 8ad9298a4..44d13e0d0 100644 --- a/base/commands/list/list_remove_index.go +++ b/base/commands/list/list_remove_index.go @@ -6,7 +6,6 @@ import ( "context" "errors" "fmt" - "strconv" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -17,10 +16,10 @@ import ( type ListRemoveIndexCommand struct{} func (mc *ListRemoveIndexCommand) Init(cc plug.InitContext) error { - cc.SetPositionalArgCount(1, 1) + cc.SetCommandUsage("remove-index") help := "Remove the value at the given index in the list" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("remove-index [index] [flags]") + cc.AddInt64Arg(argIndex, argTitleIndex) return nil } @@ -34,12 +33,9 @@ func (mc *ListRemoveIndexCommand) Exec(ctx context.Context, ec plug.ExecContext) if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { return err } - index, err := strconv.Atoi(ec.Args()[0]) - if err != nil { - return err - } + index := ec.GetInt64Arg(argIndex) if index < 0 { - return errors.New("index cannot be smaller than 0") + return errors.New("index must be non-negative") } pid, err := stringToPartitionID(ci, name) if err != nil { diff --git a/base/commands/list/list_remove_value.go b/base/commands/list/list_remove_value.go index c1a17dd63..297978884 100644 --- a/base/commands/list/list_remove_value.go +++ b/base/commands/list/list_remove_value.go @@ -15,11 +15,11 @@ import ( type ListRemoveValueCommand struct{} func (mc *ListRemoveValueCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) - cc.SetPositionalArgCount(1, 1) + cc.SetCommandUsage("remove-value") help := "Remove a value from the given list" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("remove-value [value] [flags]") + addValueTypeFlag(cc) + cc.AddStringArg(argValue, argTitleValue) return nil } @@ -33,7 +33,7 @@ func (mc *ListRemoveValueCommand) Exec(ctx context.Context, ec plug.ExecContext) if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { return err } - valueStr := ec.Args()[0] + valueStr := ec.GetStringArg(argValue) vd, err := makeValueData(ec, ci, valueStr) if err != nil { return err diff --git a/base/commands/list/list_set.go b/base/commands/list/list_set.go index 972d00c7d..fb7f7d073 100644 --- a/base/commands/list/list_set.go +++ b/base/commands/list/list_set.go @@ -5,7 +5,6 @@ package list import ( "context" "fmt" - "strconv" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -16,11 +15,12 @@ import ( type ListSetCommand struct{} func (mc *ListSetCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) - cc.SetPositionalArgCount(2, 2) + cc.SetCommandUsage("set") help := "Set a value at the given index in the list" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("set [index] [value] [flags]") + addValueTypeFlag(cc) + cc.AddInt64Arg(argIndex, argTitleIndex) + cc.AddStringArg(argValue, argTitleValue) return nil } @@ -34,11 +34,8 @@ func (mc *ListSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { return err } - index, err := strconv.Atoi(ec.Args()[0]) - if err != nil { - return err - } - valueStr := ec.Args()[1] + index := ec.GetInt64Arg(argIndex) + valueStr := ec.GetStringArg(argValue) vd, err := makeValueData(ec, ci, valueStr) if err != nil { return err diff --git a/base/commands/list/list_size.go b/base/commands/list/list_size.go index 937ce1cb5..3923daa3e 100644 --- a/base/commands/list/list_size.go +++ b/base/commands/list/list_size.go @@ -18,10 +18,9 @@ import ( type ListSizeCommand struct{} func (mc *ListSizeCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("size") help := "Return the size of the given List" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("size") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/map/const.go b/base/commands/map/const.go index 630c6cd7e..f253df95f 100644 --- a/base/commands/map/const.go +++ b/base/commands/map/const.go @@ -11,4 +11,6 @@ const ( // TODO: move ttlUnset = -1 defaultMapName = "default" + argKey = "key" + argTitleKey = "key" ) diff --git a/base/commands/map/map.go b/base/commands/map/map.go index 49b222723..40de412fb 100644 --- a/base/commands/map/map.go +++ b/base/commands/map/map.go @@ -24,17 +24,17 @@ type MapCommand struct { } func (mc *MapCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("map") + cc.SetTopLevel(true) cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) + help := "Map operations" + cc.SetCommandHelp(help, help) cc.AddStringFlag(mapFlagName, "n", defaultMapName, false, "map name") cc.AddBoolFlag(mapFlagShowType, "", false, false, "add the type names to the output") if !cc.Interactive() { cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") } - cc.SetTopLevel(true) - cc.SetCommandUsage("map [command] [flags]") - help := "Map operations" - cc.SetCommandHelp(help, help) return nil } diff --git a/base/commands/map/map_clear.go b/base/commands/map/map_clear.go index b48adde35..a48daf496 100644 --- a/base/commands/map/map_clear.go +++ b/base/commands/map/map_clear.go @@ -20,10 +20,10 @@ import ( type MapClearCommand struct{} func (mc *MapClearCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("clear") help := "Delete all entries of a Map" cc.SetCommandHelp(help, help) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - cc.SetCommandUsage("clear") return nil } diff --git a/base/commands/map/map_destroy.go b/base/commands/map/map_destroy.go index b6cd1afd7..e4d7df355 100644 --- a/base/commands/map/map_destroy.go +++ b/base/commands/map/map_destroy.go @@ -18,13 +18,13 @@ import ( type MapDestroyCommand struct{} func (mc *MapDestroyCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("destroy") long := `Destroy a Map This command will delete the Map and the data in it will not be available anymore.` short := "Destroy a Map" cc.SetCommandHelp(long, short) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - cc.SetCommandUsage("destroy") return nil } diff --git a/base/commands/map/map_entry_set.go b/base/commands/map/map_entry_set.go index e308db4ce..2b749c7ae 100644 --- a/base/commands/map/map_entry_set.go +++ b/base/commands/map/map_entry_set.go @@ -19,10 +19,9 @@ import ( type MapEntrySetCommand struct{} func (mc *MapEntrySetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("entry-set") help := "Get all entries of a Map" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("entry-set") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/map/map_get.go b/base/commands/map/map_get.go index 01991939c..78741f599 100644 --- a/base/commands/map/map_get.go +++ b/base/commands/map/map_get.go @@ -19,11 +19,11 @@ import ( type MapGetCommand struct{} func (mc *MapGetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("get") addKeyTypeFlag(cc) help := "Get a value from the given Map" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("get [key] [flags]") - cc.SetPositionalArgCount(1, 1) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -33,7 +33,7 @@ func (mc *MapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/map/map_key_set.go b/base/commands/map/map_key_set.go index e9d49e8ec..27d27b7c7 100644 --- a/base/commands/map/map_key_set.go +++ b/base/commands/map/map_key_set.go @@ -21,10 +21,9 @@ import ( type MapKeySetCommand struct{} func (mc *MapKeySetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("key-set") help := "Get all keys of a Map" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("key-set") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/map/map_load_all.go b/base/commands/map/map_load_all.go index cf46e3c5f..bf8cb0ed2 100644 --- a/base/commands/map/map_load_all.go +++ b/base/commands/map/map_load_all.go @@ -5,7 +5,6 @@ package _map import ( "context" "fmt" - "math" "github.com/hazelcast/hazelcast-go-client" @@ -18,12 +17,15 @@ import ( type MapLoadAllCommand struct{} func (mc *MapLoadAllCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("load-all") + long := `Load keys from map-store into the map + +If no key is given, all keys are loaded.` + short := "Load keys from map-store into the map" + cc.SetCommandHelp(long, short) addKeyTypeFlag(cc) - help := "Load keys from map-store into the map. If no key is given, all keys are loaded." cc.AddBoolFlag(mapFlagReplace, "", false, false, "replace keys if they exist in the map") - cc.SetCommandHelp(help, help) - cc.SetCommandUsage("load-all [keys] [flags]") - cc.SetPositionalArgCount(0, math.MaxInt) + cc.AddStringSliceArg(argKey, argTitleKey, 0, clc.MaxArgs) return nil } @@ -34,7 +36,7 @@ func (mc *MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) erro return err } var keys []hazelcast.Data - for _, keyStr := range ec.Args() { + for _, keyStr := range ec.GetStringSliceArg(argKey) { keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/map/map_lock.go b/base/commands/map/map_lock.go index 7807b6b22..02e2c53fd 100644 --- a/base/commands/map/map_lock.go +++ b/base/commands/map/map_lock.go @@ -17,15 +17,15 @@ import ( type MapLock struct{} func (mc *MapLock) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) + cc.SetCommandUsage("lock") long := `Lock a key in the given Map This command is only available in the interactive mode.` short := "Lock a key in the given Map" cc.SetCommandHelp(long, short) + addKeyTypeFlag(cc) cc.AddIntFlag(mapTTL, "", ttlUnset, false, "time-to-live (ms)") - cc.SetCommandUsage("lock [key] [flags]") - cc.SetPositionalArgCount(1, 1) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -40,7 +40,7 @@ func (mc *MapLock) Exec(ctx context.Context, ec plug.ExecContext) error { return err } m := mv.(*hazelcast.Map) - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/map/map_remove.go b/base/commands/map/map_remove.go index adb977304..2261f377b 100644 --- a/base/commands/map/map_remove.go +++ b/base/commands/map/map_remove.go @@ -19,11 +19,11 @@ import ( type MapRemoveCommand struct{} func (mc *MapRemoveCommand) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) + cc.SetCommandUsage("remove") help := "Remove a value from the given Map" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("remove [-n map-name] [key] [flags]") - cc.SetPositionalArgCount(1, 1) + addKeyTypeFlag(cc) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -33,7 +33,7 @@ func (mc *MapRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return err } - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/map/map_set.go b/base/commands/map/map_set.go index c90402fe6..717542de7 100644 --- a/base/commands/map/map_set.go +++ b/base/commands/map/map_set.go @@ -6,24 +6,31 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-go-client" +) + +const ( + argValue = "value" + argTitleValue = "value" ) type MapSetCommand struct{} func (mc *MapSetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("set") + help := "Set a value in the given Map" + cc.SetCommandHelp(help, help) addKeyTypeFlag(cc) addValueTypeFlag(cc) cc.AddIntFlag(mapTTL, "", ttlUnset, false, "time-to-live (ms)") cc.AddIntFlag(mapMaxIdle, "", ttlUnset, false, "max idle (ms)") - cc.SetPositionalArgCount(2, 2) - help := "Set a value in the given Map" - cc.SetCommandHelp(help, help) - cc.SetCommandUsage("set [key] [value] [flags]") + cc.AddStringArg(argKey, argTitleKey) + cc.AddStringArg(argValue, argTitleValue) return nil } @@ -37,8 +44,8 @@ func (mc *MapSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if _, err := ec.Props().GetBlocking(mapPropertyName); err != nil { return err } - keyStr := ec.Args()[0] - valueStr := ec.Args()[1] + keyStr := ec.GetStringArg(argKey) + valueStr := ec.GetStringArg(argValue) kd, vd, err := makeKeyValueData(ec, ci, keyStr, valueStr) if err != nil { return err diff --git a/base/commands/map/map_size.go b/base/commands/map/map_size.go index 21f20bd78..884bf6269 100644 --- a/base/commands/map/map_size.go +++ b/base/commands/map/map_size.go @@ -18,10 +18,9 @@ import ( type MapSizeCommand struct{} func (mc *MapSizeCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("size") help := "Return the size of the given Map" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("size") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/map/map_try_lock.go b/base/commands/map/map_try_lock.go index 6dc7813cd..42686707a 100644 --- a/base/commands/map/map_try_lock.go +++ b/base/commands/map/map_try_lock.go @@ -19,15 +19,17 @@ import ( type MapTryLock struct{} func (mc *MapTryLock) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) - long := `Try to lock a key in the given map. Directly returns the result + cc.SetCommandUsage("try-lock") + long := `Try to lock a key in the given map + +Returns the result without waiting for the lock to be unlocked. This command is only available in the interactive mode.` - short := "Try to lock a key in the given map. Directly returns the result" + short := "Try to lock a key in the given map" cc.SetCommandHelp(long, short) + addKeyTypeFlag(cc) cc.AddIntFlag(mapTTL, "", ttlUnset, false, "time-to-live (ms)") - cc.SetCommandUsage("try-lock [key] [flags]") - cc.SetPositionalArgCount(1, 1) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -42,7 +44,7 @@ func (mc *MapTryLock) Exec(ctx context.Context, ec plug.ExecContext) error { return err } m := mv.(*hazelcast.Map) - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/map/map_unlock.go b/base/commands/map/map_unlock.go index 874171d4f..a558b7e4c 100644 --- a/base/commands/map/map_unlock.go +++ b/base/commands/map/map_unlock.go @@ -16,14 +16,14 @@ import ( type MapUnlock struct{} func (mc *MapUnlock) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) + cc.SetCommandUsage("unlock") long := `Unlock a key in the given Map This command is only available in the interactive mode.` short := "Unlock a key in the given Map" cc.SetCommandHelp(long, short) - cc.SetCommandUsage("unlock [key] [flags]") - cc.SetPositionalArgCount(1, 1) + addKeyTypeFlag(cc) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -38,7 +38,7 @@ func (mc *MapUnlock) Exec(ctx context.Context, ec plug.ExecContext) error { return err } m := mv.(*hazelcast.Map) - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/map/map_values.go b/base/commands/map/map_values.go index 1ad148925..aa1cc9394 100644 --- a/base/commands/map/map_values.go +++ b/base/commands/map/map_values.go @@ -19,10 +19,9 @@ import ( type MapValuesCommand struct{} func (mc *MapValuesCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("values") help := "Get all values of a Map" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("values [flags]") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/multimap/const.go b/base/commands/multimap/const.go index b8ea7feec..6584a02bc 100644 --- a/base/commands/multimap/const.go +++ b/base/commands/multimap/const.go @@ -8,4 +8,6 @@ const ( defaultMultiMapName = "default" multiMapTTL = "ttl" ttlUnset = -1 + argKey = "key" + argTitleKey = "key" ) diff --git a/base/commands/multimap/multimap_clear.go b/base/commands/multimap/multimap_clear.go index 061e1c5f4..274b1eba8 100644 --- a/base/commands/multimap/multimap_clear.go +++ b/base/commands/multimap/multimap_clear.go @@ -18,10 +18,10 @@ import ( type MultiMapClearCommand struct{} func (mc *MultiMapClearCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("clear") help := "Delete all entries of a MultiMap" cc.SetCommandHelp(help, help) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - cc.SetCommandUsage("clear") return nil } diff --git a/base/commands/multimap/multimap_destroy.go b/base/commands/multimap/multimap_destroy.go index be7f0db80..6ff48cbbf 100644 --- a/base/commands/multimap/multimap_destroy.go +++ b/base/commands/multimap/multimap_destroy.go @@ -21,10 +21,10 @@ func (mc *MultiMapDestroyCommand) Init(cc plug.InitContext) error { long := `Destroy a MultiMap This command will delete the MultiMap and the data in it will not be available anymore.` + cc.SetCommandUsage("destroy") short := "Destroy a MultiMap" cc.SetCommandHelp(long, short) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - cc.SetCommandUsage("destroy") return nil } diff --git a/base/commands/multimap/multimap_entry_set.go b/base/commands/multimap/multimap_entry_set.go index 11ac420e3..958598be9 100644 --- a/base/commands/multimap/multimap_entry_set.go +++ b/base/commands/multimap/multimap_entry_set.go @@ -21,7 +21,6 @@ func (mc *MultiMapEntrySetCommand) Init(cc plug.InitContext) error { help := "Get all entries of a MultiMap" cc.SetCommandHelp(help, help) cc.SetCommandUsage("entry-set") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/multimap/multimap_get.go b/base/commands/multimap/multimap_get.go index 2ab4f4a99..51bd4f9cc 100644 --- a/base/commands/multimap/multimap_get.go +++ b/base/commands/multimap/multimap_get.go @@ -19,11 +19,11 @@ import ( type MultiMapGetCommand struct{} func (mc *MultiMapGetCommand) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) + cc.SetCommandUsage("get") help := "Get a value from the given MultiMap" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("get [key] [flags]") - cc.SetPositionalArgCount(1, 1) + addKeyTypeFlag(cc) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -33,7 +33,7 @@ func (mc *MultiMapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) err if err != nil { return err } - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/multimap/multimap_key_set.go b/base/commands/multimap/multimap_key_set.go index 962515764..77efd2303 100644 --- a/base/commands/multimap/multimap_key_set.go +++ b/base/commands/multimap/multimap_key_set.go @@ -19,10 +19,9 @@ import ( type MultiMapKeySetCommand struct{} func (mc *MultiMapKeySetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("key-set") help := "Get all keys of a MultiMap" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("key-set") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/multimap/multimap_lock.go b/base/commands/multimap/multimap_lock.go index 857234d42..50f178554 100644 --- a/base/commands/multimap/multimap_lock.go +++ b/base/commands/multimap/multimap_lock.go @@ -17,14 +17,14 @@ import ( type MultiMapLockCommand struct{} func (m MultiMapLockCommand) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) + cc.SetCommandUsage("lock") long := `Lock a key in the given MultiMap This command is only available in the interactive mode.` short := "Lock a key in the given MultiMap" cc.SetCommandHelp(long, short) - cc.SetCommandUsage("lock [key] [flags]") - cc.SetPositionalArgCount(1, 1) + addKeyTypeFlag(cc) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -34,7 +34,7 @@ func (m MultiMapLockCommand) Exec(ctx context.Context, ec plug.ExecContext) erro if err != nil { return err } - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) ci, err := ec.ClientInternal(ctx) if err != nil { return err diff --git a/base/commands/multimap/multimap_put.go b/base/commands/multimap/multimap_put.go index dc62cef81..87f0880b4 100644 --- a/base/commands/multimap/multimap_put.go +++ b/base/commands/multimap/multimap_put.go @@ -16,16 +16,21 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MultiMapPutCommand struct { -} +const ( + argValue = "value" + argTitleValue = "value" +) + +type MultiMapPutCommand struct{} func (m MultiMapPutCommand) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) - addValueTypeFlag(cc) - cc.SetPositionalArgCount(2, 2) + cc.SetCommandUsage("put") help := "Put a value in the given MultiMap" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("put [key] [value] [flags]") + addKeyTypeFlag(cc) + addValueTypeFlag(cc) + cc.AddStringArg(argKey, argTitleKey) + cc.AddStringArg(argValue, argTitleValue) return nil } @@ -38,8 +43,8 @@ func (m MultiMapPutCommand) Exec(ctx context.Context, ec plug.ExecContext) error if _, err := ec.Props().GetBlocking(multiMapPropertyName); err != nil { return err } - keyStr := ec.Args()[0] - valueStr := ec.Args()[1] + keyStr := ec.GetStringArg(argKey) + valueStr := ec.GetStringArg(argValue) kd, vd, err := makeKeyValueData(ec, ci, keyStr, valueStr) if err != nil { return err diff --git a/base/commands/multimap/multimap_remove.go b/base/commands/multimap/multimap_remove.go index f58b1aa39..fe8249fda 100644 --- a/base/commands/multimap/multimap_remove.go +++ b/base/commands/multimap/multimap_remove.go @@ -19,11 +19,11 @@ import ( type MultiMapRemoveCommand struct{} func (mc *MultiMapRemoveCommand) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) + cc.SetCommandUsage("remove") help := "Remove values from the given MultiMap" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("remove [key] [flags]") - cc.SetPositionalArgCount(1, 1) + addKeyTypeFlag(cc) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -33,7 +33,7 @@ func (mc *MultiMapRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) if err != nil { return err } - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/multimap/multimap_size.go b/base/commands/multimap/multimap_size.go index d8963c7b9..395bc76cb 100644 --- a/base/commands/multimap/multimap_size.go +++ b/base/commands/multimap/multimap_size.go @@ -18,10 +18,9 @@ import ( type MultiMapSizeCommand struct{} func (mc *MultiMapSizeCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("size") help := "Return the size of the given MultiMap" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("size") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/multimap/multimap_try_lock.go b/base/commands/multimap/multimap_try_lock.go index 998a1ce4d..08a47cfa5 100644 --- a/base/commands/multimap/multimap_try_lock.go +++ b/base/commands/multimap/multimap_try_lock.go @@ -19,15 +19,15 @@ import ( type MultiMapTryLockCommand struct{} func (m MultiMapTryLockCommand) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) - cc.AddIntFlag(multiMapTTL, "", ttlUnset, false, "time-to-live (ms)") + cc.SetCommandUsage("try-lock") long := `Try to lock a key in the given MultiMap. Directly returns the result This command is only available in the interactive mode.` short := "Try to lock a key in the given MultiMap. Directly returns the result" cc.SetCommandHelp(long, short) - cc.SetCommandUsage("try-lock [key] [flags]") - cc.SetPositionalArgCount(1, 1) + addKeyTypeFlag(cc) + cc.AddIntFlag(multiMapTTL, "", ttlUnset, false, "time-to-live (ms)") + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -37,7 +37,7 @@ func (m MultiMapTryLockCommand) Exec(ctx context.Context, ec plug.ExecContext) e if err != nil { return err } - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) ci, err := ec.ClientInternal(ctx) if err != nil { return err diff --git a/base/commands/multimap/multimap_unlock.go b/base/commands/multimap/multimap_unlock.go index 0c86e1190..9af9406d4 100644 --- a/base/commands/multimap/multimap_unlock.go +++ b/base/commands/multimap/multimap_unlock.go @@ -15,14 +15,14 @@ import ( type MultiMapUnlockCommand struct{} func (m MultiMapUnlockCommand) Init(cc plug.InitContext) error { - addKeyTypeFlag(cc) + cc.SetCommandUsage("unlock") long := `Unlock a key in the given MultiMap This command is only available in the interactive mode.` short := "Unlock a key in the given MultiMap" cc.SetCommandHelp(long, short) - cc.SetCommandUsage("unlock [key] [flags]") - cc.SetPositionalArgCount(1, 1) + addKeyTypeFlag(cc) + cc.AddStringArg(argKey, argTitleKey) return nil } @@ -32,7 +32,7 @@ func (m MultiMapUnlockCommand) Exec(ctx context.Context, ec plug.ExecContext) er if err != nil { return err } - keyStr := ec.Args()[0] + keyStr := ec.GetStringArg(argKey) keyData, err := makeKeyData(ec, ci, keyStr) if err != nil { return err diff --git a/base/commands/multimap/multimap_values.go b/base/commands/multimap/multimap_values.go index 7f1a77096..0513eda54 100644 --- a/base/commands/multimap/multimap_values.go +++ b/base/commands/multimap/multimap_values.go @@ -22,7 +22,6 @@ func (m MultiMapValuesCommand) Init(cc plug.InitContext) error { help := "Get all values of a MultiMap" cc.SetCommandHelp(help, help) cc.SetCommandUsage("values") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/object/object_list.go b/base/commands/object/object_list.go index b38e571a6..d96caa805 100644 --- a/base/commands/object/object_list.go +++ b/base/commands/object/object_list.go @@ -54,13 +54,15 @@ var objTypes = []string{ } const ( - flagShowHidden = "show-hidden" + flagShowHidden = "show-hidden" + argObjectType = "objectType" + argTitleObjectType = "object type" ) type ObjectListCommand struct{} func (cm ObjectListCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("list [object-type]") + cc.SetCommandUsage("list") long := fmt.Sprintf(`List distributed objects, optionally filter by type. The object-type filter may be one of: @@ -70,14 +72,15 @@ CP objects such as AtomicLong cannot be listed. `, objectFilterTypes()) cc.SetCommandHelp(long, "List distributed objects") cc.AddBoolFlag(flagShowHidden, "", false, false, "show hidden and system objects") - cc.SetPositionalArgCount(0, 1) + cc.AddStringSliceArg(argObjectType, argTitleObjectType, 0, 1) return nil } func (cm ObjectListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { var typeFilter string - if len(ec.Args()) > 0 { - typeFilter = ec.Args()[0] + fs := ec.GetStringSliceArg(argObjectType) + if len(fs) > 0 { + typeFilter = fs[0] } showHidden := ec.Props().GetBool(flagShowHidden) objs, err := objects.GetAll(ctx, ec, typeFilter, showHidden) diff --git a/base/commands/project/help.go b/base/commands/project/help.go new file mode 100644 index 000000000..be121be05 --- /dev/null +++ b/base/commands/project/help.go @@ -0,0 +1,60 @@ +package project + +import "fmt" + +func longHelp() string { + help := fmt.Sprintf(`Create project from the given template. + +This command is in BETA, it may change in future versions. + +Templates are located in %s. +You can override it by using CLC_EXPERIMENTAL_TEMPLATE_SOURCE environment variable. + +Rules while creating your own templates: + + * Templates are in Go template format. + See: https://pkg.go.dev/text/template + * You can create a "defaults.yaml" file for default values in template's root directory. + * Template files must have the ".template" extension. + * Files with "." and "_" prefixes are ignored unless they have the ".keep" extension. + * All files with ".keep" extension are copied by stripping the ".keep" extension. + * Other files are copied verbatim. + +Properties are read from the following resources in order: + + 1. defaults.yaml (keys should be in lowercase letters, digits or underscore) + 2. config.yaml + 3. User passed key-values in the "KEY=VALUE" format. The keys can only contain lowercase letters, digits or underscore. + +You can use the placeholders in "defaults.yaml" and the following configuration item placeholders: + + * cluster_name + * cluster_address + * cluster_user + * cluster_password + * cluster_discovery_token + * ssl_enabled + * ssl_server + * ssl_skip_verify + * ssl_ca_path + * ssl_key_path + * ssl_key_password + * log_path + * log_level + +Example (Linux and MacOS): + +$ clc project create \ + simple-streaming-pipeline\ + --output-dir my-project\ + my_key1=my_value1 my_key2=my_value2 + +Example (Windows): + +> clc project create^ + simple-streaming-pipeline^ + --output-dir my-project^ + my_key1=my_value1 my_key2=my_value2 +`, hzTemplatesOrganization) + return help +} diff --git a/base/commands/project/project_create.go b/base/commands/project/project_create.go index e41297ec5..a5f6c660b 100644 --- a/base/commands/project/project_create.go +++ b/base/commands/project/project_create.go @@ -6,7 +6,6 @@ import ( "context" "fmt" "io/fs" - "math" "os" "path/filepath" "regexp" @@ -19,74 +18,30 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) +const ( + argTemplateName = "templateName" + argTitleTemplateName = "template name" + argPlaceholder = "placeholder" + argTitlePlaceholder = "placeholder" +) + var regexpValidKey = regexp.MustCompile(`^[a-z0-9_]+$`) type CreateCmd struct{} func (pc CreateCmd) Init(cc plug.InitContext) error { - cc.SetPositionalArgCount(1, math.MaxInt) - cc.SetCommandUsage("create [template-name] [placeholder-values] [flags]") - cc.AddStringFlag(flagOutputDir, "o", "", false, "the directory to create the project at") + cc.SetCommandUsage("create") short := "Create project from the given template (BETA)" - long := fmt.Sprintf(`Create project from the given template. - -This command is in BETA, it may change in future versions. - -Templates are located in %s. -You can override it by using CLC_EXPERIMENTAL_TEMPLATE_SOURCE environment variable. - -Rules while creating your own templates: - - * Templates are in Go template format. - See: https://pkg.go.dev/text/template - * You can create a "defaults.yaml" file for default values in template's root directory. - * Template files must have the ".template" extension. - * Files with "." and "_" prefixes are ignored unless they have the ".keep" extension. - * All files with ".keep" extension are copied by stripping the ".keep" extension. - * Other files are copied verbatim. - -Properties are read from the following resources in order: - - 1. defaults.yaml (keys should be in lowercase letters, digits or underscore) - 2. config.yaml - 3. User passed key-values in the "KEY=VALUE" format. The keys can only contain lowercase letters, digits or underscore. - -You can use the placeholders in "defaults.yaml" and the following configuration item placeholders: - - * cluster_name - * cluster_address - * cluster_user - * cluster_password - * cluster_discovery_token - * ssl_enabled - * ssl_server - * ssl_skip_verify - * ssl_ca_path - * ssl_key_path - * ssl_key_password - * log_path - * log_level - -Example (Linux and MacOS): - -$ clc project create \ - simple-streaming-pipeline\ - --output-dir my-project\ - my_key1=my_value1 my_key2=my_value2 - -Example (Windows): - -> clc project create^ - simple-streaming-pipeline^ - --output-dir my-project^ - my_key1=my_value1 my_key2=my_value2 -`, hzTemplatesOrganization) + long := longHelp() cc.SetCommandHelp(long, short) + cc.AddStringFlag(flagOutputDir, "o", "", false, "the directory to create the project at") + cc.AddStringArg(argTemplateName, argTitleTemplateName) + cc.AddKeyValueSliceArg(argPlaceholder, argTitlePlaceholder, 0, clc.MaxArgs) return nil } func (pc CreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - templateName := ec.Args()[0] + templateName := ec.GetStringArg(argTemplateName) outputDir := ec.Props().GetString(flagOutputDir) if outputDir == "" { outputDir = templateName diff --git a/base/commands/project/project_list_templates.go b/base/commands/project/project_list_templates.go index d0624d50c..3d1a25657 100644 --- a/base/commands/project/project_list_templates.go +++ b/base/commands/project/project_list_templates.go @@ -39,12 +39,11 @@ type Template struct { } func (lc ListCmd) Init(cc plug.InitContext) error { - cc.SetPositionalArgCount(0, 0) - cc.SetCommandUsage("list-templates [flags]") - cc.AddBoolFlag(flagRefresh, "", false, false, "fetch most recent templates from remote") - cc.AddBoolFlag(flagLocal, "", false, false, "list the templates which exist on local environment") + cc.SetCommandUsage("list-templates") help := "Lists templates that can be used while creating projects." cc.SetCommandHelp(help, help) + cc.AddBoolFlag(flagRefresh, "", false, false, "fetch most recent templates from remote") + cc.AddBoolFlag(flagLocal, "", false, false, "list the templates which exist on local environment") return nil } diff --git a/base/commands/project/utils.go b/base/commands/project/utils.go index 076f81bbb..904202639 100644 --- a/base/commands/project/utils.go +++ b/base/commands/project/utils.go @@ -15,7 +15,6 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/str" ) func loadFromDefaults(templateDir string) (map[string]string, error) { @@ -38,18 +37,11 @@ func loadFromDefaults(templateDir string) (map[string]string, error) { } func updatePropsWithUserValues(ec plug.ExecContext, props map[string]string) error { - for _, arg := range ec.Args() { - k, v := str.ParseKeyValue(arg) - if k == "" { - continue + for _, kv := range ec.GetKeyValuesArg(argPlaceholder) { + if !regexpValidKey.MatchString(kv.Key) { + return fmt.Errorf("invalid key: %s, only letters and numbers are allowed", kv.Key) } - if !regexpValidKey.MatchString(k) { - return fmt.Errorf("invalid key: %s, only letters and numbers are allowed", k) - } - if k == "" { - return fmt.Errorf("blank keys are not allowed") - } - props[k] = v + props[kv.Key] = kv.Value } return nil } diff --git a/base/commands/queue/queue_clear.go b/base/commands/queue/queue_clear.go index 4f1d80920..63a3a1983 100644 --- a/base/commands/queue/queue_clear.go +++ b/base/commands/queue/queue_clear.go @@ -18,10 +18,10 @@ import ( type QueueClearCommand struct{} func (qc *QueueClearCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("clear") help := "Delete all entries of a Queue" cc.SetCommandHelp(help, help) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - cc.SetCommandUsage("clear [flags]") return nil } diff --git a/base/commands/queue/queue_destroy.go b/base/commands/queue/queue_destroy.go index 2207935ed..9d39651cd 100644 --- a/base/commands/queue/queue_destroy.go +++ b/base/commands/queue/queue_destroy.go @@ -18,13 +18,13 @@ import ( type QueueDestroyCommand struct{} func (qc *QueueDestroyCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("destroy") long := `Destroy a Queue This command will delete the Queue and the data in it will not be available anymore.` short := "Destroy a Queue" cc.SetCommandHelp(long, short) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - cc.SetCommandUsage("destroy") return nil } diff --git a/base/commands/queue/queue_offer.go b/base/commands/queue/queue_offer.go index 9b4ceb203..3aed47979 100644 --- a/base/commands/queue/queue_offer.go +++ b/base/commands/queue/queue_offer.go @@ -5,7 +5,6 @@ package queue import ( "context" "fmt" - "math" "github.com/hazelcast/hazelcast-go-client" @@ -14,15 +13,19 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type QueueOfferCommand struct { -} +const ( + argValue = "value" + argTitleValue = "value" +) + +type QueueOfferCommand struct{} func (qc *QueueOfferCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) - cc.SetPositionalArgCount(1, math.MaxInt) + cc.SetCommandUsage("offer") help := "Add values to the given Queue" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("offer [values] [flags]") + addValueTypeFlag(cc) + cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) return nil } @@ -40,7 +43,7 @@ func (qc *QueueOfferCommand) Exec(ctx context.Context, ec plug.ExecContext) erro _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText(fmt.Sprintf("Adding values into queue %s", queueName)) - for _, arg := range ec.Args() { + for _, arg := range ec.GetStringSliceArg(argValue) { vd, err := makeValueData(ec, ci, arg) if err != nil { return nil, err diff --git a/base/commands/queue/queue_poll.go b/base/commands/queue/queue_poll.go index 978eb9b32..17e1f1756 100644 --- a/base/commands/queue/queue_poll.go +++ b/base/commands/queue/queue_poll.go @@ -23,12 +23,11 @@ type QueuePollCommand struct { } func (qc *QueuePollCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) + cc.SetCommandUsage("poll") help := "Remove the given number of elements from the given Queue" - cc.AddIntFlag(flagCount, "", 1, false, "number of element to be removed from the given queue") cc.SetCommandHelp(help, help) - cc.SetCommandUsage("poll [flags]") - cc.SetPositionalArgCount(0, 0) + addValueTypeFlag(cc) + cc.AddIntFlag(flagCount, "", 1, false, "number of element to be removed from the given queue") return nil } diff --git a/base/commands/queue/queue_size.go b/base/commands/queue/queue_size.go index 4c5822e60..8fc32fc17 100644 --- a/base/commands/queue/queue_size.go +++ b/base/commands/queue/queue_size.go @@ -18,10 +18,9 @@ import ( type QueueSizeCommand struct{} func (qc *QueueSizeCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("size") help := "Return the size of the given Queue" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("size") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/script.go b/base/commands/script.go index 5c7451963..66223dee3 100644 --- a/base/commands/script.go +++ b/base/commands/script.go @@ -20,15 +20,17 @@ import ( ) const ( - prefixFile = "file://" - prefixHTTP = "http://" - prefixHTTPS = "https://" + prefixFile = "file://" + prefixHTTP = "http://" + prefixHTTPS = "https://" + argPath = "path" + argTitlePath = "path" ) type ScriptCommand struct{} func (cm ScriptCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("script [path] [flags]") + cc.SetCommandUsage("script") long := `Runs the script in the given local or HTTP location. The script can contain: @@ -43,14 +45,14 @@ See examples/sql/dessert.sql for a sample script. ` short := "Runs the given script" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(0, 1) cc.AddBoolFlag(flagIgnoreErrors, "", false, false, "ignore errors during script execution") cc.AddBoolFlag(flagEcho, "", false, false, "print the executed command") + cc.AddStringSliceArg(argPath, argTitlePath, 0, 1) return nil } func (cm ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - args := ec.Args() + args := ec.GetStringSliceArg(argPath) in := ec.Stdin() if len(args) > 0 { f, err := openScript(args[0]) diff --git a/base/commands/set/const.go b/base/commands/set/const.go index 645273f78..9e1fd13ba 100644 --- a/base/commands/set/const.go +++ b/base/commands/set/const.go @@ -5,4 +5,6 @@ package set const ( setFlagValueType = "value-type" defaultSetName = "default" + argValue = "value" + argTitleValue = "value" ) diff --git a/base/commands/set/set_add.go b/base/commands/set/set_add.go index 1947c3515..31d06d6a9 100644 --- a/base/commands/set/set_add.go +++ b/base/commands/set/set_add.go @@ -5,7 +5,6 @@ package set import ( "context" "fmt" - "math" "github.com/hazelcast/hazelcast-go-client" @@ -17,11 +16,11 @@ import ( type SetAddCommand struct{} func (sc *SetAddCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) - cc.SetPositionalArgCount(1, math.MaxInt) + cc.SetCommandUsage("add") help := "Add values to the given Set" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("add [values] [flags]") + addValueTypeFlag(cc) + cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) return nil } @@ -38,7 +37,7 @@ func (sc *SetAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { s := sv.(*hazelcast.Set) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText(fmt.Sprintf("Adding values into set %s", name)) - for _, arg := range ec.Args() { + for _, arg := range ec.GetStringSliceArg(argValue) { vd, err := makeValueData(ec, ci, arg) if err != nil { return nil, err diff --git a/base/commands/set/set_clear.go b/base/commands/set/set_clear.go index 4886a8da9..9ceddcc6a 100644 --- a/base/commands/set/set_clear.go +++ b/base/commands/set/set_clear.go @@ -18,10 +18,10 @@ import ( type SetClearCommand struct{} func (qc *SetClearCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("clear") help := "Delete all entries of a Set" cc.SetCommandHelp(help, help) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - cc.SetCommandUsage("clear") return nil } diff --git a/base/commands/set/set_destroy.go b/base/commands/set/set_destroy.go index 00daa6492..6274d57c0 100644 --- a/base/commands/set/set_destroy.go +++ b/base/commands/set/set_destroy.go @@ -18,12 +18,12 @@ import ( type SetDestroyCommand struct{} func (qc *SetDestroyCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("destroy") long := `Destroy a Set This command will delete the Set and the data in it will not be available anymore.` short := "Destroy a Set" cc.SetCommandHelp(long, short) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - cc.SetCommandUsage("destroy") return nil } diff --git a/base/commands/set/set_get_all.go b/base/commands/set/set_get_all.go index c332ec262..7899a9ead 100644 --- a/base/commands/set/set_get_all.go +++ b/base/commands/set/set_get_all.go @@ -19,10 +19,9 @@ import ( type SetGetAllCommand struct{} func (sc *SetGetAllCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("get-all") help := "Return the elements of the given Set" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("get-all") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/set/set_remove.go b/base/commands/set/set_remove.go index 2c772c513..178c9afb2 100644 --- a/base/commands/set/set_remove.go +++ b/base/commands/set/set_remove.go @@ -5,7 +5,6 @@ package set import ( "context" "fmt" - "math" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -18,11 +17,11 @@ import ( type SetRemoveCommand struct{} func (sc *SetRemoveCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) - cc.SetPositionalArgCount(1, math.MaxInt) + cc.SetCommandUsage("remove") help := "Remove values from the given Set" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("remove [values] [flags]") + addValueTypeFlag(cc) + cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) return nil } @@ -36,7 +35,7 @@ func (sc *SetRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error rows, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText(fmt.Sprintf("Removing from set %s", name)) var rows []output.Row - for _, arg := range ec.Args() { + for _, arg := range ec.GetStringSliceArg(argValue) { vd, err := makeValueData(ec, ci, arg) if err != nil { return nil, err diff --git a/base/commands/set/set_size.go b/base/commands/set/set_size.go index 1c999cd31..5349eeda1 100644 --- a/base/commands/set/set_size.go +++ b/base/commands/set/set_size.go @@ -18,10 +18,9 @@ import ( type SetSizeCommand struct{} func (sc *SetSizeCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("size") help := "Return the size of the given Set" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("size") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/shell.go b/base/commands/shell.go index 33acda468..fa31b5129 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -39,7 +39,6 @@ func (cm *ShellCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("shell") help := "Start the interactive shell" cc.SetCommandHelp(help, help) - cc.SetPositionalArgCount(0, 0) cc.Hide() cm.mu.Lock() cm.shortcuts = map[string]struct{}{ diff --git a/base/commands/snapshot/snapshot_delete.go b/base/commands/snapshot/snapshot_delete.go index 78b04a52c..089a77d83 100644 --- a/base/commands/snapshot/snapshot_delete.go +++ b/base/commands/snapshot/snapshot_delete.go @@ -10,13 +10,18 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) +const ( + argSnapshotName = "snapshotName" + argTitleSnapshotName = "snapshot name" +) + type DeleteCmd struct{} func (cm DeleteCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("delete [snapshot-name]") + cc.SetCommandUsage("delete") help := "Delete a snapshot" cc.SetCommandHelp(help, help) - cc.SetPositionalArgCount(1, 1) + cc.AddStringArg(argSnapshotName, argTitleSnapshotName) return nil } @@ -25,7 +30,7 @@ func (cm DeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - name := ec.Args()[0] + name := ec.GetStringArg(argTitleSnapshotName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Deleting the snapshot") sm, err := ci.Client().GetMap(ctx, jetExportedSnapshotsMap) diff --git a/base/commands/snapshot/snapshot_list.go b/base/commands/snapshot/snapshot_list.go index 456272a73..e7624901c 100644 --- a/base/commands/snapshot/snapshot_list.go +++ b/base/commands/snapshot/snapshot_list.go @@ -6,13 +6,14 @@ import ( "context" "time" + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/types" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-go-client/types" ) type ListCmd struct{} @@ -21,7 +22,6 @@ func (cm ListCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("list") help := "List snapshots" cc.SetCommandHelp(help, help) - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/base/commands/sql/sql.go b/base/commands/sql/sql.go index 8181530ea..571c584c2 100644 --- a/base/commands/sql/sql.go +++ b/base/commands/sql/sql.go @@ -19,6 +19,8 @@ import ( const ( minServerVersion = "5.0.0" + argQuery = "query" + argTitleQuery = "query" ) type arg0er interface { @@ -41,8 +43,7 @@ func (cm *SQLCommand) Init(cc plug.InitContext) error { if cc.Interactive() { return errors.ErrNotAvailable } - cc.SetCommandUsage("sql [query] [flags]") - cc.SetPositionalArgCount(1, 1) + cc.SetCommandUsage("sql") cc.AddCommandGroup("sql", "SQL") cc.SetCommandGroup("sql") long := fmt.Sprintf(`Runs the given SQL query or starts the SQL shell @@ -54,6 +55,7 @@ having version %s or better. `, minServerVersion) cc.SetCommandHelp(long, "Run SQL") cc.AddBoolFlag(clcsql.PropertyUseMappingSuggestion, "", false, false, "execute the proposed CREATE MAPPING suggestion and retry the query") + cc.AddStringArg(argQuery, argTitleQuery) return nil } @@ -69,7 +71,7 @@ func (cm *SQLCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { return fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) } - query := ec.Args()[0] + query := ec.GetStringArg(argQuery) res, stop, err := cm.execQuery(ctx, query, ec) if err != nil { return err diff --git a/base/commands/topic/topic_destroy.go b/base/commands/topic/topic_destroy.go index 11a251ca3..15e2756eb 100644 --- a/base/commands/topic/topic_destroy.go +++ b/base/commands/topic/topic_destroy.go @@ -19,13 +19,13 @@ import ( type topicDestroyCommand struct{} func (tdc *topicDestroyCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("destroy") long := `Destroy a Topic This command will delete the Topic and the data in it will not be available anymore.` short := "Destroy a Topic" cc.SetCommandHelp(long, short) cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - cc.SetCommandUsage("destroy") return nil } diff --git a/base/commands/topic/topic_publish.go b/base/commands/topic/topic_publish.go index aa97c920a..9a6455e21 100644 --- a/base/commands/topic/topic_publish.go +++ b/base/commands/topic/topic_publish.go @@ -5,7 +5,6 @@ package topic import ( "context" "fmt" - "math" "github.com/hazelcast/hazelcast-go-client" @@ -16,14 +15,19 @@ import ( . "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) +const ( + argValue = "value" + argTitleValue = "value" +) + type topicPublishCommand struct{} func (tpc *topicPublishCommand) Init(cc plug.InitContext) error { - addValueTypeFlag(cc) + cc.SetCommandUsage("publish [values] [flags]") help := "Publish new messages for a Topic." cc.SetCommandHelp(help, help) - cc.SetPositionalArgCount(1, math.MaxInt) - cc.SetCommandUsage("publish [values] [flags]") + addValueTypeFlag(cc) + cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) return nil } @@ -42,7 +46,7 @@ func (tpc *topicPublishCommand) Exec(ctx context.Context, ec plug.ExecContext) e _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText(fmt.Sprintf("Publishing values into topic %s", topicName)) var vals []hazelcast.Data - for _, valStr := range ec.Args() { + for _, valStr := range ec.GetStringSliceArg(argValue) { val, err := makeValueData(ec, ci, valStr) if err != nil { return nil, err diff --git a/base/commands/topic/topic_subscribe.go b/base/commands/topic/topic_subscribe.go index f536d3a32..da9834f84 100644 --- a/base/commands/topic/topic_subscribe.go +++ b/base/commands/topic/topic_subscribe.go @@ -17,10 +17,10 @@ import ( type topicSubscribeCommand struct{} func (tsc *topicSubscribeCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("subscribe") help := "Subscribe to a Topic for new messages." cc.SetCommandHelp(help, help) cc.AddIntFlag(topicFlagCount, "", 0, false, "number of messages to receive") - cc.SetCommandUsage("subscribe") return nil } diff --git a/base/commands/version.go b/base/commands/version.go index 0251c6883..5c282a78e 100644 --- a/base/commands/version.go +++ b/base/commands/version.go @@ -9,7 +9,6 @@ import ( "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -23,9 +22,9 @@ type VersionCommand struct { } func (vc VersionCommand) Init(cc plug.InitContext) error { - help := "Print CLC version" + cc.SetCommandUsage("version") + help := "Print the version" cc.SetCommandHelp(help, help) - cc.SetCommandUsage("version [flags]") return nil } @@ -38,13 +37,13 @@ func (vc VersionCommand) Exec(ctx context.Context, ec plug.ExecContext) error { vc.row("Go", fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)), ) } - if ec.Props().GetString(clc.PropertyFormat) == base.PrinterDelimited { - I2(fmt.Fprintln(ec.Stdout(), internal.Version)) - } else { - return ec.AddOutputRows(ctx, vc.row("Hazelcast CLC", internal.Version)) - } - ec.Logger().Debugf("version command ran OK") - return nil + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Version", + Type: serialization.TypeString, + Value: internal.Version, + }, + }) } func (vc VersionCommand) row(key, value string) output.Row { diff --git a/base/commands/version_test.go b/base/commands/version_test.go index bf72de73e..b539e4e65 100644 --- a/base/commands/version_test.go +++ b/base/commands/version_test.go @@ -6,29 +6,17 @@ import ( "context" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/base/commands" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/it" ) func TestVersion(t *testing.T) { - internal.Version = "v5.2.0" - cmd := &commands.VersionCommand{} - cc := &it.CommandContext{} - require.NoError(t, cmd.Init(cc)) - ec := it.NewExecuteContext(nil) - ec.Set(clc.PropertyFormat, base.PrinterDelimited) - require.NoError(t, cmd.Exec(context.TODO(), ec)) - assert.Equal(t, "v5.2.0\n", ec.StdoutText()) - ec.Set(clc.PropertyVerbose, true) - require.NoError(t, cmd.Exec(context.TODO(), ec)) - assert.Equal(t, ec.Rows[0][0].Value, "Hazelcast CLC") - assert.Contains(t, ec.Rows[1][0].Value, "Latest Git Commit Hash") - assert.Contains(t, ec.Rows[2][0].Value, "Hazelcast Go Client") - assert.Contains(t, ec.Rows[3][0].Value, "Go") + tcx := it.TestContext{T: t} + ctx := context.Background() + tcx.Tester(func(tcx it.TestContext) { + tcx.WithReset(func() { + tcx.CLCExecute(ctx, "version") + tcx.AssertStdoutEquals(internal.Version + "\n") + }) + }) } diff --git a/base/commands/viridian/const.go b/base/commands/viridian/const.go index 2d99ddbb5..f447ad1be 100644 --- a/base/commands/viridian/const.go +++ b/base/commands/viridian/const.go @@ -9,4 +9,8 @@ const ( flagOutputDir = "output-dir" flagHazelcastVersion = "hazelcast-version" fmtSecretFileName = "%s-%s.secret" + argClusterID = "clusterID" + argTitleClusterID = "cluster name or ID" + argArtifactID = "artifactID" + argTitleArtifactID = "artifact name or ID" ) diff --git a/base/commands/viridian/custom_class_delete.go b/base/commands/viridian/custom_class_delete.go index fe20942c1..ea109c138 100644 --- a/base/commands/viridian/custom_class_delete.go +++ b/base/commands/viridian/custom_class_delete.go @@ -15,16 +15,17 @@ import ( type CustomClassDeleteCmd struct{} func (cmd CustomClassDeleteCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("delete-custom-class [cluster-name/cluster-ID] [file-name/artifact-ID] [flags]") + cc.SetCommandUsage("delete-custom-class") long := `Deletes a custom class from the given Viridian cluster. Make sure you login before running this command. ` short := "Deletes a custom class from the given Viridian cluster." cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(2, 2) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the delete operation") + cc.AddStringArg(argClusterID, argTitleClusterID) + cc.AddStringArg(argArtifactID, argTitleArtifactID) return nil } @@ -46,8 +47,8 @@ func (cmd CustomClassDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) e } } // inputs - cluster := ec.Args()[0] - artifact := ec.Args()[1] + cluster := ec.GetStringArg(argClusterID) + artifact := ec.GetStringArg(argArtifactID) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Deleting custom class") err = api.DeleteCustomClass(ctx, cluster, artifact) diff --git a/base/commands/viridian/custom_class_download.go b/base/commands/viridian/custom_class_download.go index 3bc9eb260..b6f59aafe 100644 --- a/base/commands/viridian/custom_class_download.go +++ b/base/commands/viridian/custom_class_download.go @@ -18,16 +18,17 @@ const flagOutputPath = "output-path" type CustomClassDownloadCmd struct{} func (cmd CustomClassDownloadCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("download-custom-class [cluster-name/cluster-ID] [file-name/artifact-ID] [flags]") + cc.SetCommandUsage("download-custom-class") long := `Downloads a custom class from the given Viridian cluster. Make sure you login before running this command. ` short := "Downloads a custom class from the given Viridian cluster." cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(2, 2) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(flagOutputPath, "o", "", false, "download path") + cc.AddStringArg(argClusterID, argTitleClusterID) + cc.AddStringArg(argArtifactID, argTitleArtifactID) return nil } @@ -37,8 +38,8 @@ func (cmd CustomClassDownloadCmd) Exec(ctx context.Context, ec plug.ExecContext) return err } // inputs - clusterName := ec.Args()[0] - artifact := ec.Args()[1] + clusterName := ec.GetStringArg(argClusterID) + artifact := ec.GetStringArg(argArtifactID) target := ec.Props().GetString(flagOutputPath) // extract target info t, err := viridian.CreateTargetInfo(target) diff --git a/base/commands/viridian/custom_class_list.go b/base/commands/viridian/custom_class_list.go index 755c1d925..8e990ef0c 100644 --- a/base/commands/viridian/custom_class_list.go +++ b/base/commands/viridian/custom_class_list.go @@ -16,15 +16,15 @@ import ( type CustomClassListCmd struct{} func (cmd CustomClassListCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("list-custom-classes [cluster-name/cluster-ID] [flags]") + cc.SetCommandUsage("list-custom-classes") long := `Lists all custom classes in the given Viridian cluster. Make sure you login before running this command. ` short := "Lists all custom classes in the given Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -33,7 +33,7 @@ func (cmd CustomClassListCmd) Exec(ctx context.Context, ec plug.ExecContext) err if err != nil { return err } - cn := ec.Args()[0] + cn := ec.GetStringArg(argClusterID) verbose := ec.Props().GetBool(clc.PropertyVerbose) csi, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Retrieving custom classes") diff --git a/base/commands/viridian/custom_class_upload.go b/base/commands/viridian/custom_class_upload.go index 5e79bfdb0..d64755aaf 100644 --- a/base/commands/viridian/custom_class_upload.go +++ b/base/commands/viridian/custom_class_upload.go @@ -10,18 +10,24 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) +const ( + argPath = "path" + argTitlePath = "path" +) + type CustomClassUploadCmd struct{} func (cmd CustomClassUploadCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("upload-custom-class [cluster-name/cluster-ID] [file-name] [flags]") + cc.SetCommandUsage("upload-custom-class") long := `Uploads a new Custom Class to the specified Viridian cluster. Make sure you login before running this command. ` short := "Uploads a Custom Class to the specified Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(2, 2) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") + cc.AddStringArg(argClusterID, argTitleClusterID) + cc.AddStringArg(argPath, argTitlePath) return nil } @@ -30,11 +36,11 @@ func (cmd CustomClassUploadCmd) Exec(ctx context.Context, ec plug.ExecContext) e if err != nil { return err } - cn := ec.Args()[0] - filePath := ec.Args()[1] + cn := ec.GetStringArg(argClusterID) + path := ec.GetStringArg(argPath) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Uploading custom class") - err := api.UploadCustomClasses(ctx, sp.SetProgress, cn, filePath) + err := api.UploadCustomClasses(ctx, sp.SetProgress, cn, path) if err != nil { return nil, err } diff --git a/base/commands/viridian/download_logs.go b/base/commands/viridian/download_logs.go index ca3124494..b32e89cc4 100644 --- a/base/commands/viridian/download_logs.go +++ b/base/commands/viridian/download_logs.go @@ -15,16 +15,16 @@ import ( type DownloadLogsCmd struct{} func (cm DownloadLogsCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("download-logs [cluster-ID/name] [flags]") + cc.SetCommandUsage("download-logs") long := `Downloads the logs of the given Viridian cluster for the logged in API key. Make sure you login before running this command. ` short := "Downloads the logs of the given Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(flagOutputDir, "o", "", false, "output directory for the log files, if not given current directory is used") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -33,7 +33,7 @@ func (cm DownloadLogsCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - clusterNameOrID := ec.Args()[0] + clusterNameOrID := ec.GetStringArg(argClusterID) outDir := ec.Props().GetString(flagOutputDir) // extract target info if err := validateOutputDir(outDir); err != nil { diff --git a/base/commands/viridian/viridian_cluster_create.go b/base/commands/viridian/viridian_cluster_create.go index a6b4c02b4..00cc4705d 100644 --- a/base/commands/viridian/viridian_cluster_create.go +++ b/base/commands/viridian/viridian_cluster_create.go @@ -18,14 +18,13 @@ import ( type ClusterCreateCmd struct{} func (cm ClusterCreateCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("create-cluster [flags]") + cc.SetCommandUsage("create-cluster") long := `Creates a Viridian cluster. Make sure you login before running this command. ` short := "Creates a Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(0, 0) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(flagName, "", "", false, "specify the cluster name; if not given an auto-generated name is used.") cc.AddBoolFlag(flagDevelopment, "", false, false, "create a development cluster") diff --git a/base/commands/viridian/viridian_cluster_delete.go b/base/commands/viridian/viridian_cluster_delete.go index 8e5a61c38..4245c9716 100644 --- a/base/commands/viridian/viridian_cluster_delete.go +++ b/base/commands/viridian/viridian_cluster_delete.go @@ -16,16 +16,16 @@ import ( type ClusterDeleteCmd struct{} func (cm ClusterDeleteCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("delete-cluster [cluster-ID/name] [flags]") + cc.SetCommandUsage("delete-cluster") long := `Deletes the given Viridian cluster. Make sure you login before running this command. ` short := "Deletes the given Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the delete operation") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -46,7 +46,7 @@ func (cm ClusterDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error return errors.ErrUserCancelled } } - clusterNameOrID := ec.Args()[0] + clusterNameOrID := ec.GetStringArg(argClusterID) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Deleting the cluster") err := api.DeleteCluster(ctx, clusterNameOrID) diff --git a/base/commands/viridian/viridian_cluster_get.go b/base/commands/viridian/viridian_cluster_get.go index 43df0ee6a..6f69aa0aa 100644 --- a/base/commands/viridian/viridian_cluster_get.go +++ b/base/commands/viridian/viridian_cluster_get.go @@ -17,15 +17,15 @@ import ( type ClusterGetCmd struct{} func (cm ClusterGetCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("get-cluster [cluster-ID/name] [flags]") + cc.SetCommandUsage("get-cluster") long := `Gets the information about the given Viridian cluster. Make sure you login before running this command. ` short := "Gets the information about the given Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -34,7 +34,7 @@ func (cm ClusterGetCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - clusterNameOrID := ec.Args()[0] + clusterNameOrID := ec.GetStringArg(argClusterID) ci, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Retrieving the cluster") c, err := api.GetCluster(ctx, clusterNameOrID) diff --git a/base/commands/viridian/viridian_cluster_list.go b/base/commands/viridian/viridian_cluster_list.go index aa3ab6d72..014fc2f9d 100644 --- a/base/commands/viridian/viridian_cluster_list.go +++ b/base/commands/viridian/viridian_cluster_list.go @@ -23,7 +23,6 @@ Make sure you login before running this command. ` short := "Lists Viridian clusters" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(0, 0) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") return nil } diff --git a/base/commands/viridian/viridian_cluster_resume.go b/base/commands/viridian/viridian_cluster_resume.go index 35a59bd43..586084a96 100644 --- a/base/commands/viridian/viridian_cluster_resume.go +++ b/base/commands/viridian/viridian_cluster_resume.go @@ -14,15 +14,15 @@ import ( type ClusterResumeCmd struct{} func (cm ClusterResumeCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("resume-cluster [cluster-ID/name] [flags]") + cc.SetCommandUsage("resume-cluster") long := `Resumes the given Viridian cluster. Make sure you login before running this command. ` short := "Resumes the given Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -31,7 +31,7 @@ func (cm ClusterResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return err } - clusterNameOrID := ec.Args()[0] + clusterNameOrID := ec.GetStringArg(argClusterID) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Resuming the cluster") err := api.ResumeCluster(ctx, clusterNameOrID) diff --git a/base/commands/viridian/viridian_cluster_stop.go b/base/commands/viridian/viridian_cluster_stop.go index 223f1a564..462329744 100644 --- a/base/commands/viridian/viridian_cluster_stop.go +++ b/base/commands/viridian/viridian_cluster_stop.go @@ -14,15 +14,15 @@ import ( type ClusterStopCmd struct{} func (cm ClusterStopCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("stop-cluster [cluster-ID/name] [flags]") + cc.SetCommandUsage("stop-cluster") long := `Stops the given Viridian cluster. Make sure you login before running this command. ` short := "Stops the given Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -31,7 +31,7 @@ func (cm ClusterStopCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - clusterNameOrID := ec.Args()[0] + clusterNameOrID := ec.GetStringArg(argClusterID) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Pausing the cluster") err := api.StopCluster(ctx, clusterNameOrID) diff --git a/base/commands/viridian/viridian_import_config.go b/base/commands/viridian/viridian_import_config.go index ee0d26346..4d934ee7e 100644 --- a/base/commands/viridian/viridian_import_config.go +++ b/base/commands/viridian/viridian_import_config.go @@ -14,16 +14,16 @@ import ( type ImportConfigCmd struct{} func (ImportConfigCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("import-config [cluster-name/cluster-ID] [flags]") + cc.SetCommandUsage("import-config") long := `Imports connection configuration of the given Viridian cluster. Make sure you login before running this command. ` short := "Imports connection configuration of the given Viridian cluster." cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(flagName, "", "", false, "name of the connection configuration") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -40,7 +40,7 @@ func (ImportConfigCmd) exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - clusterNameOrID := ec.Args()[0] + clusterNameOrID := ec.GetStringArg(argClusterID) c, err := api.FindCluster(ctx, clusterNameOrID) if err != nil { return handleErrorResponse(ec, err) diff --git a/base/commands/viridian/viridian_log_stream.go b/base/commands/viridian/viridian_log_stream.go index e04ee6712..c44912a26 100644 --- a/base/commands/viridian/viridian_log_stream.go +++ b/base/commands/viridian/viridian_log_stream.go @@ -25,7 +25,7 @@ const ( type StreamLogCmd struct{} func (cm StreamLogCmd) Init(cc plug.InitContext) error { - cc.SetCommandUsage("stream-logs [cluster-ID/name]") + cc.SetCommandUsage("stream-logs") long := `Outputs the logs of the given Viridian cluster as a stream. Make sure you authenticate to the Viridian API using 'viridian login' before running this command. @@ -39,10 +39,10 @@ The log format may be one of: ` short := "Streams logs of a Viridian cluster" cc.SetCommandHelp(long, short) - cc.SetPositionalArgCount(1, 1) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(propLogFormat, "", "basic", false, "set the log format, either predefined or free form") + cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -56,7 +56,7 @@ func (cm StreamLogCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return fmt.Errorf("invalid log format %s: %w", f, err) } - clusterNameOrID := ec.Args()[0] + clusterNameOrID := ec.GetStringArg(argClusterID) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { lf := newLogFixer(ec.Stdout(), t) for { diff --git a/base/commands/viridian/viridian_login.go b/base/commands/viridian/viridian_login.go index 8b371a81c..848802317 100644 --- a/base/commands/viridian/viridian_login.go +++ b/base/commands/viridian/viridian_login.go @@ -40,7 +40,6 @@ Alternatively, you can use the following environment variables: cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(propAPISecret, "", "", false, "Viridian API Secret") cc.AddStringFlag(propAPIBase, "", "", false, "Viridian API Base") - cc.SetPositionalArgCount(0, 0) return nil } diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index 38e918a72..228d50bcc 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -284,7 +284,7 @@ func (m *Main) createCommands() error { p, ok := m.cmds[name] if !ok { p = &cobra.Command{ - Use: fmt.Sprintf("%s [command] [flags]", ps[i-1]), + Use: fmt.Sprintf("%s {command} [flags]", ps[i-1]), } p.SetUsageTemplate(usageTemplate) m.cmds[name] = p @@ -294,7 +294,7 @@ func (m *Main) createCommands() error { } // current command cmd := &cobra.Command{ - Use: ps[len(ps)-1], + Use: fmt.Sprintf("%s {command} [flags]", ps[len(ps)-1]), SilenceUsage: true, } cmd.SetUsageTemplate(usageTemplate) @@ -307,12 +307,10 @@ func (m *Main) createCommands() error { return fmt.Errorf("initializing command: %w", err) } } - // add the backslash prefix for top-level commands in the interactive mode - if m.mode != ModeNonInteractive && parent == m.root { - cmd.Use = fmt.Sprintf("\\%s", cmd.Use) - } addUniqueCommandGroup(cc, parent) if !cc.TopLevel() { + cmd.Args = cc.ArgsFunc() + cmd.Use = cc.GetCommandUsage() cmd.RunE = func(cmd *cobra.Command, args []string) error { cfs := cmd.Flags() props := m.props @@ -334,7 +332,9 @@ func (m *Main) createCommands() error { } ec.SetConfigProvider(m.cp) ec.SetMain(m) - ec.SetArgs(args) + if err := ec.SetArgs(args, cc.argSpecs); err != nil { + return err + } ec.SetCmd(cmd) ctx := context.Background() t, err := parseDuration(ec.Props().GetString(clc.PropertyTimeout)) @@ -378,6 +378,10 @@ func (m *Main) createCommands() error { return nil } } + // add the backslash prefix for top-level commands in the interactive mode + if m.mode != ModeNonInteractive && parent == m.root { + cmd.Use = fmt.Sprintf("\\%s", cmd.Use) + } parent.AddCommand(cmd) m.cmds[c.Name] = cmd } diff --git a/clc/cmd/cmd_test.go b/clc/cmd/cmd_test.go new file mode 100644 index 000000000..c78a2e6c9 --- /dev/null +++ b/clc/cmd/cmd_test.go @@ -0,0 +1,214 @@ +package cmd + +import ( + "fmt" + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hazelcast/hazelcast-commandline-client/clc" +) + +func TestMakeKeywordArgs(t *testing.T) { + testCases := []struct { + name string + args []string + specs []ArgSpec + target map[string]any + errString string + }{ + { + name: "no args", + args: nil, + specs: nil, + target: map[string]any{}, + }, + { + name: "one string arg", + args: []string{"foo"}, + specs: []ArgSpec{ + {Key: "id", Title: "ID", Type: ArgTypeString, Min: 1, Max: 1}, + }, + target: map[string]any{ + "id": "foo", + }, + }, + { + name: "two string args", + args: []string{"foo", "bar"}, + specs: []ArgSpec{ + {Key: "id", Title: "ID", Type: ArgTypeString, Min: 1, Max: 1}, + {Key: "other", Title: "Other arg", Type: ArgTypeString, Min: 1, Max: 1}, + }, + target: map[string]any{ + "id": "foo", + "other": "bar", + }, + }, + { + name: "one optional string slice arg", + args: nil, + specs: []ArgSpec{ + {Key: "strings", Title: "String", Type: ArgTypeStringSlice, Max: 10}, + }, + target: map[string]any{ + "strings": []string{}, + }, + }, + { + name: "two optional string slice args", + args: []string{"foo", "bar"}, + specs: []ArgSpec{ + {Key: "strings", Title: "String", Type: ArgTypeStringSlice, Max: 10}, + }, + target: map[string]any{ + "strings": []string{"foo", "bar"}, + }, + }, + { + name: "one missing required arg", + args: nil, + specs: []ArgSpec{ + {Key: "id", Title: "ID", Min: 1, Max: 1}, + }, + errString: "ID is required", + }, + { + name: "one missing string slice arg", + args: nil, + specs: []ArgSpec{ + {Key: "strings", Title: "String", Type: ArgTypeStringSlice, Min: 1, Max: 10}, + }, + errString: "expected at least 1 String arguments, but received 0", + }, + { + name: "more args for string slice", + args: []string{"foo", "bar", "zoo"}, + specs: []ArgSpec{ + {Key: "strings", Title: "String", Type: ArgTypeStringSlice, Min: 1, Max: 2}, + }, + errString: "expected at most 2 String arguments, but received 3", + }, + { + name: "unknown type for string arg", + args: []string{"foo"}, + specs: []ArgSpec{ + {Key: "id", Title: "ID", Type: ArgTypeNone}, + }, + errString: "converting argument ID: unknown type: 0", + }, + { + name: "unknown type for string slice arg", + args: []string{"foo"}, + specs: []ArgSpec{ + {Key: "id", Title: "ID", Type: ArgTypeNone, Min: 0, Max: 1}, + }, + errString: "converting argument ID: unknown type: 0", + }, + { + name: "string slice arg before the last arg", + args: []string{"foo"}, + specs: []ArgSpec{ + {Key: "id", Title: "ID", Min: 1, Max: 10}, + {Key: "other", Title: "Other", Min: 1, Max: 1}, + }, + errString: "invalid argument spec: only the last argument may take a range", + }, + { + name: "more arguments than expected", + args: []string{"foo", "bar", "zoo"}, + specs: []ArgSpec{ + {Key: "id", Title: "ID", Min: 1, Max: 1, Type: ArgTypeString}, + }, + errString: "unexpected arguments", + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + kw, err := makeKeywordArgs(tc.args, tc.specs) + if tc.errString == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Equal(t, tc.errString, err.Error()) + return + } + require.Equal(t, tc.target, kw) + }) + } +} + +func TestAddWithOverflow(t *testing.T) { + testCases := []struct { + a int + b int + target int + }{ + {a: 0, b: 1, target: 1}, + {a: 10, b: 20, target: 30}, + {a: 0, b: math.MaxInt, target: math.MaxInt}, + {a: 10, b: math.MaxInt, target: math.MaxInt}, + {a: math.MaxInt, b: 1, target: math.MaxInt}, + {a: math.MaxInt, b: math.MaxInt, target: math.MaxInt}, + } + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("%d + %d", tc.a, tc.b), func(t *testing.T) { + r := addWithOverflow(tc.a, tc.b) + assert.Equal(t, tc.target, r) + }) + } +} + +func TestMakeCommandUsageString(t *testing.T) { + testCases := []struct { + argSpecs []ArgSpec + target string + }{ + { + argSpecs: nil, + target: "cmd [flags]", + }, + { + argSpecs: []ArgSpec{ + {Title: "key", Min: 1, Max: 1}, + }, + target: "cmd {key} [flags]", + }, + { + argSpecs: []ArgSpec{ + {Title: "placeholder", Min: 0, Max: clc.MaxArgs}, + }, + target: "cmd [placeholder, ...] [flags]", + }, + { + argSpecs: []ArgSpec{ + {Title: "placeholder", Min: 1, Max: clc.MaxArgs}, + }, + target: "cmd {placeholder, ...} [flags]", + }, + { + argSpecs: []ArgSpec{ + {Title: "placeholder", Min: 2, Max: clc.MaxArgs}, + }, + target: "cmd {placeholder, placeholder, ...} [flags]", + }, + { + argSpecs: []ArgSpec{ + {Title: "key", Min: 1, Max: 1}, + {Title: "placeholder", Min: 0, Max: clc.MaxArgs}, + }, + target: "cmd {key} [placeholder, ...] [flags]", + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.target, func(t *testing.T) { + u := makeCommandUsageString("cmd", tc.argSpecs) + assert.Equal(t, tc.target, u) + }) + } +} diff --git a/clc/cmd/cobra.go b/clc/cmd/cobra.go index 57820e817..946b81cc9 100644 --- a/clc/cmd/cobra.go +++ b/clc/cmd/cobra.go @@ -27,5 +27,5 @@ Global Flags: Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +Use "{{.CommandPath}} {command} --help" for more information about a command.{{end}} ` diff --git a/clc/cmd/command_context.go b/clc/cmd/command_context.go index b17047926..cb88d8b24 100644 --- a/clc/cmd/command_context.go +++ b/clc/cmd/command_context.go @@ -1,7 +1,9 @@ package cmd import ( + "fmt" "math" + "strings" "github.com/spf13/cobra" @@ -9,6 +11,25 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) +type ArgType int + +const ( + ArgTypeNone ArgType = iota + ArgTypeString + ArgTypeStringSlice + ArgTypeInt64 + ArgTypeInt64Slice + ArgTypeKeyValueSlice +) + +type ArgSpec struct { + Key string + Title string + Type ArgType + Min int + Max int +} + type CommandContext struct { Cmd *cobra.Command CP config.Provider @@ -18,6 +39,8 @@ type CommandContext struct { mode Mode isTopLevel bool group *cobra.Group + argSpecs []ArgSpec + usage string } func NewCommandContext(cmd *cobra.Command, cfgProvider config.Provider, mode Mode) *CommandContext { @@ -59,25 +82,54 @@ func (cc *CommandContext) AddBoolFlag(long, short string, value bool, required b cc.boolValues[long] = &b } -// SetPositionalArgCount sets the number minimum and maximum positional arguments. -// if min and max are the same, the pos args are set as the exact num of args. -// otherwise, if max == math.MaxInt, num of pos args are set as the minumum of min args. -// otherwise, if min == 0, num of pos args are set as the maximum of max args. -// otherwise num of pos args is the range of min, max args. -func (cc *CommandContext) SetPositionalArgCount(min, max int) { - if min == max { - cc.Cmd.Args = cobra.ExactArgs(min) - return +func (cc *CommandContext) AddStringArg(key, title string) { + s := ArgSpec{ + Key: key, + Title: title, + Type: ArgTypeString, + Min: 1, + Max: 1, } - if max == math.MaxInt { - cc.Cmd.Args = cobra.MinimumNArgs(min) - return + cc.argSpecs = append(cc.argSpecs, s) +} + +func (cc *CommandContext) AddStringSliceArg(key, title string, min, max int) { + if max < min { + panic("CommandContext.AddStringSliceArg: max cannot be less than min") } - if min == 0 { - cc.Cmd.Args = cobra.MaximumNArgs(max) - return + s := ArgSpec{ + Key: key, + Title: title, + Type: ArgTypeStringSlice, + Min: min, + Max: max, } - cc.Cmd.Args = cobra.RangeArgs(min, max) + cc.argSpecs = append(cc.argSpecs, s) +} + +func (cc *CommandContext) AddKeyValueSliceArg(key, title string, min, max int) { + if max < min { + panic("CommandContext.AddKeyValueSliceArg: max cannot be less than min") + } + s := ArgSpec{ + Key: key, + Title: title, + Type: ArgTypeKeyValueSlice, + Min: min, + Max: max, + } + cc.argSpecs = append(cc.argSpecs, s) +} + +func (cc *CommandContext) AddInt64Arg(key, title string) { + s := ArgSpec{ + Key: key, + Title: title, + Type: ArgTypeInt64, + Min: 1, + Max: 1, + } + cc.argSpecs = append(cc.argSpecs, s) } func (cc *CommandContext) Hide() { @@ -98,7 +150,11 @@ func (cc *CommandContext) SetCommandHelp(long, short string) { } func (cc *CommandContext) SetCommandUsage(usage string) { - cc.Cmd.Use = usage + cc.usage = usage +} + +func (cc *CommandContext) GetCommandUsage() string { + return makeCommandUsageString(cc.usage, cc.argSpecs) } func (cc *CommandContext) SetCommandGroup(id string) { @@ -133,3 +189,75 @@ func (cc *CommandContext) SetTopLevel(b bool) { func (cc *CommandContext) TopLevel() bool { return cc.isTopLevel } + +func (cc *CommandContext) ArgsFunc() func(*cobra.Command, []string) error { + if len(cc.argSpecs) == 0 { + return cobra.NoArgs + } + // validate specs + for i, s := range cc.argSpecs { + // min and max should be 1 if this is not the last argspec + if i < len(cc.argSpecs)-1 { + if s.Min != 1 || s.Max != 1 { + panic("only the last argument may take a range of values") + } + } + } + fn := func(_ *cobra.Command, args []string) error { + var minCnt, maxCnt int + c := len(args) + for _, s := range cc.argSpecs { + if c < minCnt+s.Min { + return fmt.Errorf("%s is required", s.Title) + } + minCnt += s.Min + maxCnt = addWithOverflow(maxCnt, s.Max) + } + if len(args) > maxCnt { + return fmt.Errorf("expected at most %d argument(s)", maxCnt) + } + return nil + } + return fn +} + +// addWithOverflow adds two integers and returns the result +// If the sum is greater than math.MaxInt, it returns math.MaxInt. +// a and b are assumed to be non-negative. +func addWithOverflow(a, b int) int { + if a > math.MaxInt-b { + return math.MaxInt + } + return a + b +} + +func makeCommandUsageString(usage string, specs []ArgSpec) string { + var sb strings.Builder + sb.WriteString(usage) + for _, s := range specs { + sb.WriteByte(' ') + if s.Min == 0 { + sb.WriteByte('[') + } else { + sb.WriteByte('{') + } + sb.WriteString(s.Title) + if s.Min > 1 { + for i := 1; i < s.Min; i++ { + sb.WriteString(", ") + sb.WriteString(s.Title) + } + } + if s.Max-s.Min > 1 { + sb.WriteString(", ") + sb.WriteString("...") + } + if s.Min == 0 { + sb.WriteByte(']') + } else { + sb.WriteByte('}') + } + } + sb.WriteString(" [flags]") + return sb.String() +} diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index c834851e9..a933f0f12 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -8,6 +8,7 @@ import ( "io" "os" "os/signal" + "strconv" "strings" "time" @@ -21,9 +22,12 @@ import ( cmderrors "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/hazelcast/hazelcast-commandline-client/internal/maps" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) const ( @@ -38,6 +42,7 @@ type ExecContext struct { stderr io.Writer stdin io.Reader args []string + kwargs map[string]any props *plug.Properties mode Mode cmd *cobra.Command @@ -56,6 +61,7 @@ func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, mode Mode props: props, mode: mode, spinnerWait: 1 * time.Second, + kwargs: map[string]any{}, }, nil } @@ -63,8 +69,14 @@ func (ec *ExecContext) SetConfigProvider(cfgProvider config.Provider) { ec.cp = cfgProvider } -func (ec *ExecContext) SetArgs(args []string) { +func (ec *ExecContext) SetArgs(args []string, argSpecs []ArgSpec) error { ec.args = args + kw, err := makeKeywordArgs(args, argSpecs) + if err != nil { + return err + } + ec.kwargs = kw + return nil } func (ec *ExecContext) SetCmd(cmd *cobra.Command) { @@ -103,6 +115,22 @@ func (ec *ExecContext) Arg0() string { return ec.main.Arg0() } +func (ec *ExecContext) GetStringArg(key string) string { + return maps.GetString(ec.kwargs, key) +} + +func (ec *ExecContext) GetStringSliceArg(key string) []string { + return maps.GetStringSlice(ec.kwargs, key) +} + +func (ec *ExecContext) GetKeyValuesArg(key string) types.KeyValues[string, string] { + return maps.GetKeyValues[string, any, string, string](ec.kwargs, key) +} + +func (ec *ExecContext) GetInt64Arg(key string) int64 { + return maps.GetInt64(ec.kwargs, key) +} + func (ec *ExecContext) Props() plug.ReadOnlyProperties { return ec.props } @@ -315,6 +343,94 @@ func makeErrorStringFromHTTPResponse(text string) string { return text } +func makeKeywordArgs(args []string, argSpecs []ArgSpec) (map[string]any, error) { + kw := make(map[string]any, len(argSpecs)) + var maxCnt int + for i, s := range argSpecs { + spec := argSpecs[i] + maxCnt = addWithOverflow(maxCnt, s.Max) + if s.Max-s.Min > 0 { + if i == len(argSpecs)-1 { + // if this is the last spec and a range of orguments is expected + arg := args[i:] + if len(arg) < spec.Min { + return nil, fmt.Errorf("expected at least %d %s arguments, but received %d", spec.Min, spec.Title, len(arg)) + } + if len(arg) > spec.Max { + return nil, fmt.Errorf("expected at most %d %s arguments, but received %d", spec.Max, spec.Title, len(arg)) + } + vs, err := convertSliceArg(arg, spec.Type) + if err != nil { + return nil, fmt.Errorf("converting argument %s: %w", spec.Title, err) + } + kw[s.Key] = vs + break + } + return nil, errors.New("invalid argument spec: only the last argument may take a range") + } + // note that this code is never executed under normal circumstances + // since the arguments are validated before running this function + if i >= len(args) { + return nil, fmt.Errorf("%s is required", spec.Title) + } + value, err := convertArg(args[i], spec.Type) + if err != nil { + return nil, fmt.Errorf("converting argument %s: %w", spec.Title, err) + } + kw[s.Key] = value + } + // note that this code is never executed under normal circumstances + // since the arguments are validated before running this function + if len(args) > maxCnt { + return nil, fmt.Errorf("unexpected arguments") + } + return kw, nil +} + +func convertArg(value string, typ ArgType) (any, error) { + switch typ { + case ArgTypeString: + return value, nil + case ArgTypeInt64: + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + return v, nil + } + return nil, fmt.Errorf("unknown type: %d", typ) +} + +func convertSliceArg(values []string, typ ArgType) (any, error) { + switch typ { + case ArgTypeStringSlice: + args := make([]string, len(values)) + copy(args, values) + return args, nil + case ArgTypeInt64Slice: + args := make([]int64, len(values)) + for i, v := range values { + vi, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return nil, err + } + args[i] = vi + } + return args, nil + case ArgTypeKeyValueSlice: + args := make(types.KeyValues[string, string], len(values)) + for i, kv := range values { + k, v := str.ParseKeyValue(kv) + if k == "" { + continue + } + args[i] = types.KeyValue[string, string]{Key: k, Value: v} + } + return args, nil + } + return nil, fmt.Errorf("unknown type: %d", typ) +} + type simpleSpinner struct { sp *yacspin.Spinner text string diff --git a/clc/const.go b/clc/const.go index 149f74152..75f883ab7 100644 --- a/clc/const.go +++ b/clc/const.go @@ -35,4 +35,5 @@ const ( EnvConfig = "CLC_CONFIG" EnvSkipServerVersionCheck = "CLC_SKIP_SERVER_VERSION_CHECK" FlagAutoYes = "yes" + MaxArgs = 65535 ) diff --git a/internal/it/context.go b/internal/it/context.go index 0313b91ea..a93a4bbfc 100644 --- a/internal/it/context.go +++ b/internal/it/context.go @@ -14,6 +14,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) type CommandContext struct { @@ -23,24 +24,40 @@ type CommandContext struct { IsInteractive bool } +func (c CommandContext) AddKeyValueSliceArg(key, title string, min, max int) { + panic("implement me") +} + +func (c CommandContext) AddStringArg(key, title string) { + panic("implement me") +} + +func (c CommandContext) AddStringSliceArg(key, title string, min, max int) { + panic("implement me") +} + +func (c CommandContext) AddInt64Arg(key, title string) { + panic("implement me") +} + func (c CommandContext) AddStringFlag(long, short, value string, required bool, help string) { - //TODO implement me + panic("implement me") } func (c CommandContext) AddBoolFlag(long, short string, value bool, required bool, help string) { - //TODO implement me + panic("implement me") } func (c CommandContext) AddIntFlag(long, short string, value int64, required bool, help string) { - //TODO implement me + panic("implement me") } func (c CommandContext) SetPositionalArgCount(min, max int) { - //TODO implement me + panic("implement me") } func (c CommandContext) Hide() { - //TODO implement me + panic("implement me") } func (c CommandContext) Interactive() bool { @@ -57,19 +74,19 @@ func (c *CommandContext) SetCommandUsage(usage string) { } func (c CommandContext) AddCommandGroup(id, title string) { - //TODO implement me + panic("implement me") } func (c CommandContext) SetCommandGroup(id string) { - //TODO implement me + panic("implement me") } func (c CommandContext) AddStringConfig(name, value, flag string, help string) { - //TODO implement me + panic("implement me") } func (c CommandContext) SetTopLevel(b bool) { - //TODO implement me + panic("implement me") } type ExecContext struct { @@ -94,6 +111,11 @@ func NewExecuteContext(args []string) *ExecContext { Spinner: NewSpinner(), } } + +func (ec *ExecContext) GetKeyValuesArg(key string) types.KeyValues[string, string] { + 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() {} @@ -105,7 +127,6 @@ func (ec *ExecContext) Props() plug.ReadOnlyProperties { } func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInternal, error) { - //TODO implement me panic("implement me") } @@ -114,7 +135,6 @@ func (ec *ExecContext) Interactive() bool { } func (ec *ExecContext) AddOutputStream(ctx context.Context, ch <-chan output.Row) error { - //TODO implement me panic("implement me") } @@ -144,12 +164,10 @@ func (ec *ExecContext) Args() []string { } func (ec *ExecContext) ShowHelpAndExit() { - //TODO implement me panic("implement me") } func (ec *ExecContext) CommandName() string { - //TODO implement me panic("implement me") } @@ -187,6 +205,18 @@ func (ec *ExecContext) PrintlnUnnecessary(text string) { } } +func (ec *ExecContext) GetStringArg(key string) string { + panic("implement me") +} + +func (ec *ExecContext) GetStringSliceArg(key string) []string { + panic("implement me") +} + +func (ec *ExecContext) GetInt64Arg(key string) int64 { + panic("implement me") +} + func (ec *ExecContext) WrapResult(f func() error) error { return f() } diff --git a/internal/maps/maps.go b/internal/maps/maps.go new file mode 100644 index 000000000..cf9e771b2 --- /dev/null +++ b/internal/maps/maps.go @@ -0,0 +1,55 @@ +package maps + +import ( + "golang.org/x/exp/constraints" + + "github.com/hazelcast/hazelcast-commandline-client/internal/types" +) + +func GetValueIfExists[MK constraints.Ordered, MV, T any](m map[MK]MV, key MK) T { + if v, ok := m[key]; ok { + if vs, ok := any(v).(T); ok { + return vs + } + } + var v T + return v +} + +// GetString returns the string value corresponding to the key. +// It returns a blank string if the value doesn't exist or it is not a string. +func GetString[K constraints.Ordered, V any](m map[K]V, key K) string { + return GetValueIfExists[K, V, string](m, key) +} + +// GetStringSlice returns the string value corresponding to the key. +// It returns a blank string if the value doesn't exist or it is not a string. +func GetStringSlice[K constraints.Ordered, V any](m map[K]V, key K) []string { + return GetValueIfExists[K, V, []string](m, key) +} + +// GetKeyValues returns the KeyValue pairs for the corresponding key. +// It returns an empty slice if the value doesn't exist or it is not a types.KeyValues +func GetKeyValues[MK constraints.Ordered, MV any, K constraints.Ordered, V any](m map[MK]MV, key MK) types.KeyValues[K, V] { + return GetValueIfExists[MK, MV, types.KeyValues[K, V]](m, key) +} + +// GetInt64 returns the int64 value corresponding to the key. +// It returns 0 if the value doesn't exist or it is not a signed integer. +func GetInt64[K constraints.Ordered, V any](m map[K]V, key K) int64 { + if v, ok := m[key]; ok { + switch vv := any(v).(type) { + case int: + return int64(vv) + case int8: + return int64(vv) + case int16: + return int64(vv) + case int32: + return int64(vv) + case int64: + return vv + } + } + return 0 +} diff --git a/internal/plug/context.go b/internal/plug/context.go index fd1bc6185..e6f96143b 100644 --- a/internal/plug/context.go +++ b/internal/plug/context.go @@ -9,6 +9,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) type InitContext interface { @@ -17,12 +18,15 @@ type InitContext interface { AddIntFlag(long, short string, value int64, required bool, help string) AddStringConfig(name, value, flag string, help string) AddStringFlag(long, short, value string, required bool, help string) + AddStringArg(key, title string) + AddStringSliceArg(key, title string, min, max int) + AddKeyValueSliceArg(key, title string, min, max int) + AddInt64Arg(key, title string) Hide() Interactive() bool SetCommandGroup(id string) SetCommandHelp(long, short string) SetCommandUsage(usage string) - SetPositionalArgCount(min, max int) SetTopLevel(b bool) } @@ -30,6 +34,10 @@ type ExecContext interface { AddOutputRows(ctx context.Context, rows ...output.Row) error AddOutputStream(ctx context.Context, ch <-chan output.Row) error Args() []string + GetStringArg(key string) string + GetStringSliceArg(key string) []string + GetKeyValuesArg(key string) types.KeyValues[string, string] + GetInt64Arg(key string) int64 ClientInternal(ctx context.Context) (*hazelcast.ClientInternal, error) CommandName() string Interactive() bool diff --git a/internal/types/key_value.go b/internal/types/key_value.go index 975cd718a..bf4d9c400 100644 --- a/internal/types/key_value.go +++ b/internal/types/key_value.go @@ -10,3 +10,11 @@ type KeyValue[K constraints.Ordered, V any] struct { } type KeyValues[K constraints.Ordered, V any] []KeyValue[K, V] + +func (kvs KeyValues[K, V]) Map() map[K]V { + m := make(map[K]V, len(kvs)) + for _, kv := range kvs { + m[kv.Key] = kv.Value + } + return m +} From c6210241eb2654de9ee2fbd215403c7389a521a1 Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Thu, 7 Sep 2023 14:13:54 +0300 Subject: [PATCH 47/79] [CLC-278] Fix CLC prompt after running config wizard (#353) --- base/commands/shell.go | 34 +++++++------- clc/cmd/config_providers.go | 18 -------- clc/cmd/exec_context.go | 4 ++ clc/config/{provider.go => file_provider.go} | 2 +- clc/config/wizard/input.go | 2 +- clc/config/wizard/list.go | 2 +- .../provider.go => wizard_provider.go} | 44 ++++++++++--------- clc/shell/shell.go | 14 +++--- cmd/clc/main.go | 4 +- internal/it/context.go | 5 +++ internal/plug/context.go | 1 + 11 files changed, 63 insertions(+), 67 deletions(-) delete mode 100644 clc/cmd/config_providers.go rename clc/config/{provider.go => file_provider.go} (98%) rename clc/config/{wizard/provider.go => wizard_provider.go} (64%) diff --git a/base/commands/shell.go b/base/commands/shell.go index fa31b5129..73b0051a8 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -63,10 +63,9 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext if err != nil { return fmt.Errorf("cloning Main: %w", err) } - var cfgText, logText, cfgPath string + var cfgText, logText string if !terminal.IsPipe(ec.Stdin()) { - cfgPathProp := ec.Props().GetString(clc.PropertyConfig) - cfgPath = paths.ResolveConfigPath(cfgPathProp) + cfgPath := ec.ConfigPath() if cfgPath != "" { cfgText = fmt.Sprintf("Configuration : %s\n", cfgPath) } @@ -96,8 +95,20 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext sh.SetCommentPrefix("--") return sh.Run(ctx) } - p := makePrompt(cfgPath) - sh, err := shell.New(p, " ... ", path, ec.Stdout(), ec.Stderr(), ec.Stdin(), endLineFn, textFn) + promptFn := func() string { + cfgPath := ec.ConfigPath() + if cfgPath == "" { + return "> " + } + // Best effort for absolute path + p, err := filepath.Abs(cfgPath) + if err == nil { + cfgPath = p + } + pd := paths.ParentDir(cfgPath) + return fmt.Sprintf("%s> ", str.MaybeShorten(pd, 12)) + } + sh, err := shell.New(promptFn, " ... ", path, ec.Stdout(), ec.Stderr(), ec.Stdin(), endLineFn, textFn) if err != nil { return err } @@ -106,19 +117,6 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext return sh.Start(ctx) } -func makePrompt(cfgPath string) string { - if cfgPath == "" { - return "> " - } - // Best effort for absolute path - p, err := filepath.Abs(cfgPath) - if err == nil { - cfgPath = p - } - pd := paths.ParentDir(cfgPath) - return fmt.Sprintf("%s> ", str.MaybeShorten(pd, 12)) -} - func (*ShellCommand) Unwrappable() {} func init() { diff --git a/clc/cmd/config_providers.go b/clc/cmd/config_providers.go deleted file mode 100644 index 026fb9473..000000000 --- a/clc/cmd/config_providers.go +++ /dev/null @@ -1,18 +0,0 @@ -package cmd - -import ( - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc/config" - "github.com/hazelcast/hazelcast-commandline-client/clc/logger" - "github.com/hazelcast/hazelcast-commandline-client/internal/plug" -) - -type SimpleConfigProvider struct { - Props plug.ReadOnlyProperties - Logger *logger.Logger -} - -func (s SimpleConfigProvider) ClientConfig() (hazelcast.Config, error) { - return config.MakeHzConfig(s.Props, s.Logger) -} diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index a933f0f12..101223622 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -135,6 +135,10 @@ func (ec *ExecContext) Props() plug.ReadOnlyProperties { return ec.props } +func (ec *ExecContext) ConfigPath() string { + return ec.cp.GetString(clc.PropertyConfig) +} + func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInternal, error) { ci := ec.main.clientInternal() if ci != nil { diff --git a/clc/config/provider.go b/clc/config/file_provider.go similarity index 98% rename from clc/config/provider.go rename to clc/config/file_provider.go index 6c02d5831..4fe8628c6 100644 --- a/clc/config/provider.go +++ b/clc/config/file_provider.go @@ -55,7 +55,7 @@ func (p *FileProvider) load(path string) error { path = paths.ResolveConfigPath(path) if !paths.Exists(path) { if path == "" { - // the user is trying to load the default config. + // there is no default config, user will be prompted for config later return nil } return fmt.Errorf("configuration does not exist %s: %w", path, os.ErrNotExist) diff --git a/clc/config/wizard/input.go b/clc/config/wizard/input.go index f641a6571..38136a527 100644 --- a/clc/config/wizard/input.go +++ b/clc/config/wizard/input.go @@ -25,7 +25,7 @@ type textModel struct { inputs []textinput.Model } -func initialModel() textModel { +func InitialModel() textModel { m := textModel{ inputs: make([]textinput.Model, 2), quitting: false, diff --git a/clc/config/wizard/list.go b/clc/config/wizard/list.go index d8c6bddc5..dfe42c46e 100644 --- a/clc/config/wizard/list.go +++ b/clc/config/wizard/list.go @@ -83,7 +83,7 @@ func (m model) View() string { return m.list.View() } -func initializeList(dirs []string) model { +func InitializeList(dirs []string) model { var items []list.Item for _, k := range dirs { items = append(items, item(k)) diff --git a/clc/config/wizard/provider.go b/clc/config/wizard_provider.go similarity index 64% rename from clc/config/wizard/provider.go rename to clc/config/wizard_provider.go index f685c5c22..d27296bb4 100644 --- a/clc/config/wizard/provider.go +++ b/clc/config/wizard_provider.go @@ -1,4 +1,4 @@ -package wizard +package config import ( "context" @@ -13,40 +13,40 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/spf13/pflag" - "github.com/hazelcast/hazelcast-commandline-client/clc/config" + "github.com/hazelcast/hazelcast-commandline-client/clc/config/wizard" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" clcerrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type Provider struct { - fp *atomic.Pointer[config.FileProvider] +type WizardProvider struct { + fp *atomic.Pointer[FileProvider] cfg hazelcast.Config } -func NewProvider(path string) (*Provider, error) { - fp, err := config.NewFileProvider(path) +func NewWizardProvider(path string) (*WizardProvider, error) { + fp, err := NewFileProvider(path) if err != nil { return nil, err } - var fpp atomic.Pointer[config.FileProvider] + var fpp atomic.Pointer[FileProvider] fpp.Store(fp) - return &Provider{fp: &fpp}, nil + return &WizardProvider{fp: &fpp}, nil } -func (p *Provider) GetString(key string) string { +func (p *WizardProvider) GetString(key string) string { return p.fp.Load().GetString(key) } -func (p *Provider) Set(key string, value any) { +func (p *WizardProvider) Set(key string, value any) { p.fp.Load().Set(key, value) } -func (p *Provider) All() map[string]any { +func (p *WizardProvider) All() map[string]any { return p.fp.Load().All() } -func (p *Provider) BindFlag(name string, flag *pflag.Flag) { +func (p *WizardProvider) BindFlag(name string, flag *pflag.Flag) { p.fp.Load().BindFlag(name, flag) } @@ -57,7 +57,7 @@ func maybeUnwrapStdout(ec plug.ExecContext) any { return ec.Stdout() } -func (p *Provider) ClientConfig(ctx context.Context, ec plug.ExecContext) (hazelcast.Config, error) { +func (p *WizardProvider) ClientConfig(ctx context.Context, ec plug.ExecContext) (hazelcast.Config, error) { cfg, err := p.fp.Load().ClientConfig(ctx, ec) if err != nil { if terminal.IsPipe(maybeUnwrapStdout(ec)) { @@ -68,18 +68,22 @@ func (p *Provider) ClientConfig(ctx context.Context, ec plug.ExecContext) (hazel if err != nil { return hazelcast.Config{}, err } - fp, err := config.NewFileProvider(name) + fp, err := NewFileProvider(name) if err != nil { return cfg, err } + config, err := fp.ClientConfig(ctx, ec) + if err != nil { + return hazelcast.Config{}, err + } p.fp.Store(fp) - return fp.ClientConfig(ctx, ec) + return config, nil } return cfg, nil } -func (p *Provider) runWizard(ctx context.Context, ec plug.ExecContext) (string, error) { - cs, err := config.FindAll(paths.Configs()) +func (p *WizardProvider) runWizard(ctx context.Context, ec plug.ExecContext) (string, error) { + cs, err := FindAll(paths.Configs()) if err != nil { if errors.Is(err, os.ErrNotExist) { err = os.MkdirAll(paths.Configs(), 0700) @@ -89,7 +93,7 @@ func (p *Provider) runWizard(ctx context.Context, ec plug.ExecContext) (string, } } if len(cs) == 0 { - m := initialModel() + m := wizard.InitialModel() mv, err := tea.NewProgram(m).Run() if err != nil { return "", err @@ -98,13 +102,13 @@ func (p *Provider) runWizard(ctx context.Context, ec plug.ExecContext) (string, return "", clcerrors.ErrNoClusterConfig } args := m.GetInputs() - _, err = config.ImportSource(ctx, ec, args[0], args[1]) + _, err = ImportSource(ctx, ec, args[0], args[1]) if err != nil { return "", err } return args[0], nil } - m := initializeList(cs) + m := wizard.InitializeList(cs) model, err := tea.NewProgram(m).Run() if err != nil { return "", err diff --git a/clc/shell/shell.go b/clc/shell/shell.go index baf65c474..bab8bc34c 100644 --- a/clc/shell/shell.go +++ b/clc/shell/shell.go @@ -32,11 +32,12 @@ type EndLineFn func(line string, multiline bool) (string, bool) type TextFn func(ctx context.Context, stdout io.Writer, text string) error +type PromptFn func() string type Shell struct { lr LineReader endLineFn EndLineFn textFn TextFn - prompt1 string + prompt1Fn PromptFn prompt2 string historyPath string stderr io.Writer @@ -45,12 +46,12 @@ type Shell struct { commentPrefix string } -func New(prompt1, prompt2, historyPath string, stdout, stderr io.Writer, stdin io.Reader, endLineFn EndLineFn, textFn TextFn) (*Shell, error) { +func New(prompt1Fn PromptFn, prompt2, historyPath string, stdout, stderr io.Writer, stdin io.Reader, endLineFn EndLineFn, textFn TextFn) (*Shell, error) { stdout, stderr = fixStdoutStderr(stdout, stderr) sh := &Shell{ endLineFn: endLineFn, textFn: textFn, - prompt1: prompt1, + prompt1Fn: prompt1Fn, prompt2: prompt2, historyPath: historyPath, stderr: stderr, @@ -63,11 +64,12 @@ func New(prompt1, prompt2, historyPath string, stdout, stderr io.Writer, stdin i // ny is default on Windows rl = "ny" } + fp := prompt1Fn() if rl == "ny" { - if err := sh.createNyLineReader(prompt1); err != nil { + if err := sh.createNyLineReader(fp); err != nil { return nil, err } - } else if err := sh.createGohxsLineReader(prompt1); err != nil { + } else if err := sh.createGohxsLineReader(fp); err != nil { return nil, err } return sh, nil @@ -116,7 +118,7 @@ func (sh *Shell) Start(ctx context.Context) error { func (sh *Shell) readTextReadline(ctx context.Context) (string, error) { // NOTE: when this implementation is changed, // clc/shell/oneshot_shell.go:readTextBasic should also change! - prompt := sh.prompt1 + prompt := sh.prompt1Fn() multiline := false var sb strings.Builder for { diff --git a/cmd/clc/main.go b/cmd/clc/main.go index 1c8ae5030..3e7c3c0dd 100644 --- a/cmd/clc/main.go +++ b/cmd/clc/main.go @@ -11,7 +11,7 @@ import ( clc "github.com/hazelcast/hazelcast-commandline-client/clc" cmd "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - "github.com/hazelcast/hazelcast-commandline-client/clc/config/wizard" + "github.com/hazelcast/hazelcast-commandline-client/clc/config" hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" ) @@ -34,7 +34,7 @@ func main() { if err != nil { bye(err) } - cp, err := wizard.NewProvider(cfgPath) + cp, err := config.NewWizardProvider(cfgPath) if err != nil { bye(err) } diff --git a/internal/it/context.go b/internal/it/context.go index a93a4bbfc..8c308821e 100644 --- a/internal/it/context.go +++ b/internal/it/context.go @@ -163,6 +163,11 @@ func (ec *ExecContext) Args() []string { return ec.args } +func (ec *ExecContext) ConfigPath() string { + //TODO implement me + panic("implement me") +} + func (ec *ExecContext) ShowHelpAndExit() { panic("implement me") } diff --git a/internal/plug/context.go b/internal/plug/context.go index e6f96143b..502558eef 100644 --- a/internal/plug/context.go +++ b/internal/plug/context.go @@ -38,6 +38,7 @@ type ExecContext interface { GetStringSliceArg(key string) []string GetKeyValuesArg(key string) types.KeyValues[string, string] GetInt64Arg(key string) int64 + ConfigPath() string ClientInternal(ctx context.Context) (*hazelcast.ClientInternal, error) CommandName() string Interactive() bool From c787c3a6bb7ac3bfe0618d77857fe97295aae7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 8 Sep 2023 11:57:29 +0300 Subject: [PATCH 48/79] Integrate stage (#369) Updated output, phase 1 --- .github/workflows/build_release-windows.yml | 2 +- .github/workflows/build_release.yml | 2 +- .github/workflows/coverage_runner.yaml | 2 +- .github/workflows/nightly_tests.yaml | 2 +- .github/workflows/test-all-386.yaml | 2 +- .github/workflows/test-all.yaml | 2 +- README.md | 2 +- base/commands/atomic_long/atomic_long.go | 45 +-- .../atomic_long/atomic_long_decrement_get.go | 2 + base/commands/atomic_long/atomic_long_get.go | 18 +- .../atomic_long/atomic_long_increment_get.go | 2 + base/commands/atomic_long/atomic_long_set.go | 38 +- base/commands/atomic_long/common.go | 41 ++- base/commands/config/config_add.go | 9 +- base/commands/config/config_import.go | 26 +- base/commands/config/config_it_test.go | 3 +- base/commands/config/config_list.go | 2 +- base/commands/demo/demo_generate_data.go | 61 ++-- base/commands/demo/demo_map_set_many.go | 10 +- base/commands/job/common.go | 140 ++++---- base/commands/job/job_export_snapshot.go | 6 +- base/commands/job/job_it_test.go | 32 +- base/commands/job/job_list.go | 4 +- base/commands/job/job_resume.go | 75 ++-- base/commands/job/job_submit.go | 61 ++-- base/commands/job/job_terminate.go | 30 +- base/commands/viridian/common.go | 104 +----- base/commands/viridian/custom_class_delete.go | 4 +- .../viridian/custom_class_download.go | 16 +- base/commands/viridian/custom_class_list.go | 6 + base/commands/viridian/custom_class_upload.go | 4 +- base/commands/viridian/download_logs.go | 45 ++- .../viridian/viridian_cluster_create.go | 107 ++++-- .../viridian/viridian_cluster_delete.go | 49 ++- .../commands/viridian/viridian_cluster_get.go | 8 +- .../viridian/viridian_cluster_list.go | 7 +- .../viridian/viridian_cluster_resume.go | 38 +- .../viridian/viridian_cluster_stop.go | 38 +- .../viridian/viridian_import_config.go | 27 +- base/commands/viridian/viridian_it_test.go | 6 +- base/commands/viridian/viridian_log_stream.go | 2 + base/commands/viridian/viridian_login.go | 72 +++- clc/cmd/exec_context.go | 18 +- clc/cmd/utils.go | 6 + clc/config/import.go | 331 +++++------------- clc/config/wizard_provider.go | 9 +- clc/ux/stage/common_stages.go | 24 ++ clc/ux/stage/stage.go | 67 ++-- clc/ux/stage/stage_test.go | 45 ++- cmd/clc/main.go | 15 +- docs/antora.yml | 2 +- errors/error.go | 12 +- go.mod | 2 +- go.sum | 10 + internal/http/http.go | 30 ++ internal/it/test_context.go | 2 +- internal/viridian/cluster.go | 28 +- internal/viridian/cluster_log.go | 14 +- 58 files changed, 928 insertions(+), 839 deletions(-) create mode 100644 clc/ux/stage/common_stages.go create mode 100644 internal/http/http.go diff --git a/.github/workflows/build_release-windows.yml b/.github/workflows/build_release-windows.yml index 9756e97d2..ff4504423 100644 --- a/.github/workflows/build_release-windows.yml +++ b/.github/workflows/build_release-windows.yml @@ -23,7 +23,7 @@ jobs: - name: "Set up Go" uses: "actions/setup-go@v3" with: - go-version: "1.19" + go-version: "1.21" - name: "Download Inno Setup installer" run: "curl -L -o installer.exe http://files.jrsoftware.org/is/6/innosetup-${{ env.INNO_VERSION }}.exe" diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 5f3c8659d..69a4ef9e5 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -20,7 +20,7 @@ jobs: - name: "Set up Go" uses: "actions/setup-go@v3" with: - go-version: "1.19" + go-version: "1.21" - name: "Build the Project" run: | diff --git a/.github/workflows/coverage_runner.yaml b/.github/workflows/coverage_runner.yaml index f4d5e5151..19a3701e7 100644 --- a/.github/workflows/coverage_runner.yaml +++ b/.github/workflows/coverage_runner.yaml @@ -42,7 +42,7 @@ jobs: - uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.21' - name: Install JDK uses: actions/setup-java@v2 diff --git a/.github/workflows/nightly_tests.yaml b/.github/workflows/nightly_tests.yaml index 18b4622c5..d0af0ed40 100644 --- a/.github/workflows/nightly_tests.yaml +++ b/.github/workflows/nightly_tests.yaml @@ -62,7 +62,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v3" with: - go-version: "1.19" + go-version: "1.21" - name: "Install Go tools" run: | diff --git a/.github/workflows/test-all-386.yaml b/.github/workflows/test-all-386.yaml index 19e940936..f839be0b3 100644 --- a/.github/workflows/test-all-386.yaml +++ b/.github/workflows/test-all-386.yaml @@ -40,7 +40,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v3" with: - go-version: "1.19" + go-version: "1.21" - name: "Install Go tools" run: | diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml index c1a2b8be7..1ecb75df5 100644 --- a/.github/workflows/test-all.yaml +++ b/.github/workflows/test-all.yaml @@ -53,7 +53,7 @@ jobs: - name: "Setup Go" uses: "actions/setup-go@v3" with: - go-version: "1.19" + go-version: "1.21" - name: "Install Go Tools" run: | diff --git a/README.md b/README.md index 01ec79125..29b1d2399 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ The prior versions of the given targets would also work, but that's not tested. ### Requirements -* Go 1.19 or better +* Go 1.21 or better * Git * GNU Make (on Linux and MacOS) * Command Prompt or Powershell (on Windows) diff --git a/base/commands/atomic_long/atomic_long.go b/base/commands/atomic_long/atomic_long.go index ca71dcf79..d6e2dca42 100644 --- a/base/commands/atomic_long/atomic_long.go +++ b/base/commands/atomic_long/atomic_long.go @@ -4,9 +4,6 @@ package atomiclong import ( "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -14,19 +11,17 @@ import ( ) const ( - atomicLongFlagName = "name" - atomicLongPropertyName = "atomic-long" + flagName = "name" ) -type AtomicLongCommand struct { -} +type AtomicLongCommand struct{} -func (mc *AtomicLongCommand) Init(cc plug.InitContext) error { +func (AtomicLongCommand) Init(cc plug.InitContext) error { cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(atomicLongFlagName, "n", defaultAtomicLongName, false, "atomic long name") + cc.AddStringFlag(flagName, "n", defaultAtomicLongName, false, "atomic long name") cc.SetTopLevel(true) - cc.SetCommandUsage("atomic-long [command] [flags]") + cc.SetCommandUsage("atomic-long") help := "Atomic long operations" cc.SetCommandHelp(help, help) return nil @@ -36,34 +31,6 @@ func (mc *AtomicLongCommand) Exec(context.Context, plug.ExecContext) error { return nil } -func (mc *AtomicLongCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { - ctx := context.TODO() - props.SetBlocking(atomicLongPropertyName, func() (any, error) { - name := ec.Props().GetString(atomicLongFlagName) - // empty atomic long name is allowed - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - mv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting atomic long %s", name)) - m, err := ci.Client().CPSubsystem().GetAtomicLong(ctx, name) - if err != nil { - return nil, err - } - return m, nil - }) - if err != nil { - return nil, err - } - stop() - return mv.(*hazelcast.AtomicLong), nil - }) - return nil -} - func init() { - cmd := &AtomicLongCommand{} - Must(plug.Registry.RegisterCommand("atomic-long", cmd)) - plug.Registry.RegisterAugmentor("20-atomic-long", cmd) + Must(plug.Registry.RegisterCommand("atomic-long", &AtomicLongCommand{})) } diff --git a/base/commands/atomic_long/atomic_long_decrement_get.go b/base/commands/atomic_long/atomic_long_decrement_get.go index e93d894f3..a4b92f815 100644 --- a/base/commands/atomic_long/atomic_long_decrement_get.go +++ b/base/commands/atomic_long/atomic_long_decrement_get.go @@ -11,6 +11,8 @@ import ( type AtomicLongDecrementGetCommand struct{} +func (mc *AtomicLongDecrementGetCommand) Unwrappable() {} + func (mc *AtomicLongDecrementGetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("decrement-get") help := "Decrement the AtomicLong by the given value" diff --git a/base/commands/atomic_long/atomic_long_get.go b/base/commands/atomic_long/atomic_long_get.go index 12d2dee91..3d9be8076 100644 --- a/base/commands/atomic_long/atomic_long_get.go +++ b/base/commands/atomic_long/atomic_long_get.go @@ -6,8 +6,6 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -17,6 +15,8 @@ import ( type AtomicLongGetCommand struct{} +func (mc *AtomicLongGetCommand) Unwrappable() {} + func (mc *AtomicLongGetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("get") help := "Get the value of the AtomicLong" @@ -25,13 +25,12 @@ func (mc *AtomicLongGetCommand) Init(cc plug.InitContext) error { } func (mc *AtomicLongGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - al, err := ec.Props().GetBlocking(atomicLongPropertyName) - if err != nil { - return err - } - ali := al.(*hazelcast.AtomicLong) - vali, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Setting value into AtomicLong %s", ali.Name())) + val, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ali, err := getAtomicLong(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Getting value of AtomicLong %s", ali.Name())) val, err := ali.Get(ctx) if err != nil { return nil, err @@ -42,7 +41,6 @@ func (mc *AtomicLongGetCommand) Exec(ctx context.Context, ec plug.ExecContext) e return err } stop() - val := vali.(int64) row := output.Row{ output.Column{ Name: "Value", diff --git a/base/commands/atomic_long/atomic_long_increment_get.go b/base/commands/atomic_long/atomic_long_increment_get.go index a50e5ee43..d2bff150f 100644 --- a/base/commands/atomic_long/atomic_long_increment_get.go +++ b/base/commands/atomic_long/atomic_long_increment_get.go @@ -11,6 +11,8 @@ import ( type AtomicLongIncrementGetCommand struct{} +func (mc *AtomicLongIncrementGetCommand) Unwrappable() {} + func (mc *AtomicLongIncrementGetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("increment-get") help := "Increment the atomic long by the given value" diff --git a/base/commands/atomic_long/atomic_long_set.go b/base/commands/atomic_long/atomic_long_set.go index a3e08ce03..a653d0ca3 100644 --- a/base/commands/atomic_long/atomic_long_set.go +++ b/base/commands/atomic_long/atomic_long_set.go @@ -6,11 +6,11 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) const ( @@ -20,6 +20,8 @@ const ( type AtomicLongSetCommand struct{} +func (mc *AtomicLongSetCommand) Unwrappable() {} + func (mc *AtomicLongSetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("set") help := "Set the value of the AtomicLong" @@ -29,25 +31,37 @@ func (mc *AtomicLongSetCommand) Init(cc plug.InitContext) error { } func (mc *AtomicLongSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - al, err := ec.Props().GetBlocking(atomicLongPropertyName) - if err != nil { - return err - } - value := ec.GetInt64Arg(argValue) - ali := al.(*hazelcast.AtomicLong) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + stateV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ali, err := getAtomicLong(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Setting value of AtomicLong %s", ali.Name())) - err := ali.Set(ctx, int64(value)) + v := ec.GetInt64Arg(argValue) + err = ali.Set(ctx, v) if err != nil { return nil, err } - return nil, nil + state := executeState{ + Name: ali.Name(), + Value: v, + } + return state, nil }) if err != nil { return err } stop() - return nil + s := stateV.(executeState) + msg := fmt.Sprintf("OK Set AtomicLong %s.\n", s.Name) + ec.PrintlnUnnecessary(msg) + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Value", + Type: serialization.TypeInt64, + Value: s.Value, + }, + }) } func init() { diff --git a/base/commands/atomic_long/common.go b/base/commands/atomic_long/common.go index ab3300a99..f6d6a6628 100644 --- a/base/commands/atomic_long/common.go +++ b/base/commands/atomic_long/common.go @@ -9,39 +9,58 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) +type executeState struct { + Name string + Value int64 +} + func atomicLongChangeValue(ctx context.Context, ec plug.ExecContext, verb string, change func(int64) int64) error { - al, err := ec.Props().GetBlocking(atomicLongPropertyName) - if err != nil { - return err - } by := ec.Props().GetInt(atomicLongFlagBy) - by = change(by) - ali := al.(*hazelcast.AtomicLong) - vali, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + stateV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ali, err := getAtomicLong(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("%sing the AtomicLong %s", verb, ali.Name())) - val, err := ali.AddAndGet(ctx, by) + val, err := ali.AddAndGet(ctx, change(by)) if err != nil { return nil, err } - return val, nil + state := executeState{ + Name: ali.Name(), + Value: val, + } + return state, nil }) if err != nil { return err } stop() - val := vali.(int64) + s := stateV.(executeState) + msg := fmt.Sprintf("OK %sed AtomicLong %s by %d.\n", verb, s.Name, by) + ec.PrintlnUnnecessary(msg) row := output.Row{ output.Column{ Name: "Value", Type: serialization.TypeInt64, - Value: val, + Value: s.Value, }, } return ec.AddOutputRows(ctx, row) +} +func getAtomicLong(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.AtomicLong, error) { + name := ec.Props().GetString(flagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Getting atomic long %s", name)) + return ci.Client().CPSubsystem().GetAtomicLong(ctx, name) } diff --git a/base/commands/config/config_add.go b/base/commands/config/config_add.go index 4ba3427ce..b38290228 100644 --- a/base/commands/config/config_add.go +++ b/base/commands/config/config_add.go @@ -8,7 +8,6 @@ import ( "math" "path/filepath" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,6 +17,8 @@ import ( type AddCmd struct{} +func (cm AddCmd) Unwrappable() {} + func (cm AddCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("add") short := "Adds a configuration" @@ -32,6 +33,7 @@ The following keys are supported: * cluster.user STRING * cluster.password STRING * cluster.discovery-token STRING + * cluster.api-base STRING * ssl.enabled BOOLEAN (true / false) * ssl.server STRING * ssl.skip-verify BOOLEAN (true / false) @@ -65,15 +67,14 @@ func (cm AddCmd) Exec(_ context.Context, ec plug.ExecContext) error { if err != nil { return err } - 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()) } + msg := fmt.Sprintf("OK Created the configuration at: %s", filepath.Join(dir, cfgPath)) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/config/config_import.go b/base/commands/config/config_import.go index 04cdabd9a..613932d8e 100644 --- a/base/commands/config/config_import.go +++ b/base/commands/config/config_import.go @@ -4,16 +4,19 @@ package config import ( "context" - "fmt" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) type ImportCmd struct{} +func (cm ImportCmd) Unwrappable() {} + func (cm ImportCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("import") short := "Imports configuration from an arbitrary source" @@ -38,16 +41,21 @@ Currently importing Viridian connection configuration is supported only. } func (cm ImportCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - target := ec.Args()[0] - src := ec.Args()[1] - path, err := config.ImportSource(ctx, ec, target, src) + target := ec.GetStringArg(argConfigName) + src := ec.GetStringArg(argSource) + stages := config.MakeImportStages(ec, target) + path, err := stage.Execute(ctx, ec, src, stage.NewFixedProvider(stages...)) if err != nil { return err } - if ec.Interactive() || ec.Props().GetBool(clc.PropertyVerbose) { - I2(fmt.Fprintf(ec.Stdout(), "Created configuration at: %s\n", path)) - } - return nil + ec.PrintlnUnnecessary("") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Configuration Path", + Type: serialization.TypeString, + Value: path, + }, + }) } func init() { diff --git a/base/commands/config/config_it_test.go b/base/commands/config/config_it_test.go index 7a5917757..1bef5da9f 100644 --- a/base/commands/config/config_it_test.go +++ b/base/commands/config/config_it_test.go @@ -38,7 +38,6 @@ func importTest(t *testing.T) { ctx := context.Background() tcx.WithReset(func() { check.Must(tcx.CLC().Execute(ctx, "config", "import", name, configURL)) - tcx.AssertStderrContains("OK\n") path := paths.Join(paths.ResolveConfigPath(name)) tcx.T.Logf("config path: %s", path) assert.True(tcx.T, paths.Exists(path)) @@ -57,7 +56,7 @@ func addTest(t *testing.T) { ctx := context.Background() tcx.WithReset(func() { check.Must(tcx.CLC().Execute(ctx, "config", "add", name, "cluster.address=foobar.com")) - tcx.AssertStderrContains("OK\n") + tcx.AssertStdoutContains("OK Created the configuration") }) tcx.WithReset(func() { check.Must(tcx.CLC().Execute(ctx, "config", "list")) diff --git a/base/commands/config/config_list.go b/base/commands/config/config_list.go index f2ed0af5c..45e7436bf 100644 --- a/base/commands/config/config_list.go +++ b/base/commands/config/config_list.go @@ -35,7 +35,7 @@ func (cm ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { ec.Logger().Warn("Cannot access configs directory at: %s: %s", cd, err.Error()) } if len(cs) == 0 { - ec.PrintlnUnnecessary("No configurations found.") + ec.PrintlnUnnecessary("OK No configurations found.") return nil } var rows []output.Row diff --git a/base/commands/demo/demo_generate_data.go b/base/commands/demo/demo_generate_data.go index f9aa8bea9..df142b05a 100644 --- a/base/commands/demo/demo_generate_data.go +++ b/base/commands/demo/demo_generate_data.go @@ -42,6 +42,8 @@ var supportedEventStreams = map[string]DataStreamGenerator{ type GenerateDataCmd struct{} +func (cm GenerateDataCmd) Unwrappable() {} + func (cm GenerateDataCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("generate-data") long := `Generates a stream of events @@ -92,39 +94,46 @@ func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator D return err } ec.PrintlnUnnecessary(fmt.Sprintf("Following mapping will be created when run without preview:\n\n%s", mq)) - ec.PrintlnUnnecessary("Generating preview items...") - outCh := make(chan output.Row) - count := int64(0) - go func() { - loop: - for count < maxCount { - var ev demo.StreamItem - select { - case event, ok := <-itemCh: - if !ok { + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText("") + outCh := make(chan output.Row) + count := int64(0) + go func() { + loop: + for count < maxCount { + var ev demo.StreamItem + select { + case event, ok := <-itemCh: + if !ok { + break loop + } + ev = event + case <-ctx.Done(): break loop } - ev = event - case <-ctx.Done(): - break loop - } - select { - case outCh <- ev.Row(): - case <-ctx.Done(): - break loop + select { + case outCh <- ev.Row(): + case <-ctx.Done(): + break loop + } + count++ } - count++ - } - close(outCh) - stopStream() - }() - return ec.AddOutputStream(ctx, outCh) + close(outCh) + stopStream() + }() + return nil, ec.AddOutputStream(ctx, outCh) + }) + if err != nil { + return err + } + stop() + return nil } func generateResult(ctx context.Context, ec plug.ExecContext, generator DataStreamGenerator, itemCh <-chan demo.StreamItem, keyVals map[string]string, stopStream context.CancelFunc) error { mapName, ok := keyVals[pairMapName] if !ok { - return fmt.Errorf("%s key-value pair must be given", pairMapName) + return fmt.Errorf("either %s key-value pair must be given or --preview must be used", pairMapName) } m, err := getMap(ctx, ec, mapName) if err != nil { @@ -198,7 +207,7 @@ func generateResult(ctx context.Context, ec plug.ExecContext, generator DataStre }) stop() stopStream() - ec.PrintlnUnnecessary(fmt.Sprintf("Generated %d events", atomic.LoadInt64(&count))) + ec.PrintlnUnnecessary(fmt.Sprintf("OK Generated %d events", atomic.LoadInt64(&count))) return err } diff --git a/base/commands/demo/demo_map_set_many.go b/base/commands/demo/demo_map_set_many.go index 466016f8d..fa76de7af 100644 --- a/base/commands/demo/demo_map_set_many.go +++ b/base/commands/demo/demo_map_set_many.go @@ -17,6 +17,8 @@ import ( type MapSetManyCmd struct{} +func (m MapSetManyCmd) Unwrappable() {} + const ( flagName = "name" flagSize = "size" @@ -39,7 +41,7 @@ func (m MapSetManyCmd) Init(cc plug.InitContext) error { } func (m MapSetManyCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - c := ec.GetInt64Arg(argEntryCount) + count := ec.GetInt64Arg(argEntryCount) mapName := ec.Props().GetString(flagName) size := ec.Props().GetString(flagSize) ci, err := ec.ClientInternal(ctx) @@ -47,17 +49,19 @@ func (m MapSetManyCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return err } _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Creating entries in map %s with %d entries", mapName, c)) + sp.SetText(fmt.Sprintf("Creating entries in map %s with %d entries", mapName, count)) mm, err := ci.Client().GetMap(ctx, mapName) if err != nil { return nil, err } - return nil, createEntries(ctx, c, size, mm) + return nil, createEntries(ctx, count, size, mm) }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Generated %d entries", count) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/job/common.go b/base/commands/job/common.go index 583f33de4..1badd7cd7 100644 --- a/base/commands/job/common.go +++ b/base/commands/job/common.go @@ -12,43 +12,33 @@ import ( "github.com/hazelcast/hazelcast-go-client/types" - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec/control" ) -func WaitJobState(ctx context.Context, ec plug.ExecContext, msg, jobNameOrID string, state int32, duration time.Duration) error { +func WaitJobState(ctx context.Context, ec plug.ExecContext, sp stage.Statuser[any], jobNameOrID string, state int32, duration time.Duration) error { ci, err := ec.ClientInternal(ctx) if err != nil { return err } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - if msg != "" { - sp.SetText(msg) + j := jet.New(ci, sp, ec.Logger()) + for { + jl, err := j.GetJobList(ctx) + if err != nil { + return err } - j := jet.New(ci, sp, ec.Logger()) - for { - jl, err := j.GetJobList(ctx) - if err != nil { - return nil, err - } - ok, err := jet.EnsureJobState(jl, jobNameOrID, state) - if err != nil { - return nil, err - } - if ok { - return nil, nil - } - ec.Logger().Debugf("Waiting %s for job %s to transition to state %s", duration.String(), jobNameOrID, jet.StatusToString(state)) - time.Sleep(duration) + ok, err := jet.EnsureJobState(jl, jobNameOrID, state) + if err != nil { + return err } - }) - if err != nil { - return err + if ok { + return nil + } + ec.Logger().Debugf("Waiting %s for job %s to transition to state %s", duration.String(), jobNameOrID, jet.StatusToString(state)) + time.Sleep(duration) } - stop() - return nil } func idToString(id int64) string { @@ -65,56 +55,64 @@ func idToString(id int64) string { return string(buf[:]) } -func terminateJob(ctx context.Context, ec plug.ExecContext, name string, terminateMode int32, text string, waitState int32) error { +func terminateJob(ctx context.Context, ec plug.ExecContext, tm int32, cm TerminateCmd) error { nameOrID := ec.GetStringArg(argJobID) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - jidv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("%s %s", text, nameOrID)) - j := jet.New(ci, sp, ec.Logger()) - jis, err := j.GetJobList(ctx) - if err != nil { - return nil, err - } - jm, err := NewJobNameToIDMap(jis) - if err != nil { - return nil, err - } - jid, ok := jm.GetIDForName(nameOrID) - if !ok { - return nil, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) - } - ec.Logger().Info("%s %s (%s)", text, nameOrID, idToString(jid)) - ji, ok := jm.GetInfoForID(jid) - if !ok { - return nil, jet.ErrInvalidJobID - } - var coord types.UUID - if ji.LightJob { - conns := ci.ConnectionManager().ActiveConnections() - if len(conns) == 0 { - return nil, errors.New("not connected") - } - coord = conns[0].MemberUUID() - } - return jid, j.TerminateJob(ctx, jid, terminateMode, coord) - }) - if err != nil { - return err + stages := []stage.Stage[any]{ + stage.MakeConnectStage[any](ec), + { + ProgressMsg: fmt.Sprintf(cm.inProgressMsg, nameOrID), + SuccessMsg: fmt.Sprintf(cm.successMsg, nameOrID), + FailureMsg: cm.failureMsg, + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + j := jet.New(ci, status, ec.Logger()) + jis, err := j.GetJobList(ctx) + if err != nil { + return nil, err + } + jm, err := NewJobNameToIDMap(jis) + if err != nil { + return nil, err + } + jid, ok := jm.GetIDForName(nameOrID) + if !ok { + return nil, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) + } + ec.Logger().Info("%s %s (%s)", cm.inProgressMsg, nameOrID, idToString(jid)) + ji, ok := jm.GetInfoForID(jid) + if !ok { + return nil, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) + } + var coord types.UUID + if ji.LightJob { + conns := ci.ConnectionManager().ActiveConnections() + if len(conns) == 0 { + return nil, errors.New("not connected") + } + coord = conns[0].MemberUUID() + } + return nil, j.TerminateJob(ctx, jid, cm.terminateMode, coord) + }, + }, } - stop() - err = nil - if ec.Props().GetBool(flagWait) { - msg := fmt.Sprintf("Waiting for the operation to finish for job %s", nameOrID) - ec.Logger().Info(msg) - err = WaitJobState(ctx, ec, msg, nameOrID, waitState, 1*time.Second) + wait := ec.Props().GetBool(flagWait) + if wait { + stages = append(stages, stage.Stage[any]{ + ProgressMsg: fmt.Sprintf("Waiting for job to be %sed", cm.name), + SuccessMsg: fmt.Sprintf("Job %s is %sed", nameOrID, cm.name), + FailureMsg: fmt.Sprintf("Failed to %s %s", cm.name, nameOrID), + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + msg := fmt.Sprintf("Waiting for job %s to be %sed", nameOrID, cm.name) + ec.Logger().Info(msg) + return nil, WaitJobState(ctx, ec, status, nameOrID, cm.waitState, 1*time.Second) + }, + }) } + _, err := stage.Execute[any](ctx, ec, nil, stage.NewFixedProvider(stages...)) if err != nil { - if ec.Props().GetBool(clc.PropertyVerbose) { - ec.PrintlnUnnecessary(fmt.Sprintf("Job %sed: %s", name, idToString(jidv.(int64)))) - } return err } return nil diff --git a/base/commands/job/job_export_snapshot.go b/base/commands/job/job_export_snapshot.go index cfa9c8b4c..04cfb7981 100644 --- a/base/commands/job/job_export_snapshot.go +++ b/base/commands/job/job_export_snapshot.go @@ -38,7 +38,7 @@ func (cm ExportSnapshotCmd) Exec(ctx context.Context, ec plug.ExecContext) error jobNameOrID := ec.GetStringArg(argJobID) name := ec.Props().GetString(flagName) cancel := ec.Props().GetBool(flagCancel) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + nameV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { j := jet.New(ci, sp, ec.Logger()) jis, err := j.GetJobList(ctx) if err != nil { @@ -74,12 +74,14 @@ func (cm ExportSnapshotCmd) Exec(ctx context.Context, ec plug.ExecContext) error return nil, err } sp.SetText(fmt.Sprintf("Exporting snapshot: %s", name)) - return nil, j.ExportSnapshot(ctx, jid, name, cancel) + return name, j.ExportSnapshot(ctx, jid, name, cancel) }) if err != nil { return err } stop() + name = nameV.(string) + ec.PrintlnUnnecessary(fmt.Sprintf("OK Exported snapshot %s", name)) return nil } diff --git a/base/commands/job/job_it_test.go b/base/commands/job/job_it_test.go index 9b50bbe0c..49877946c 100644 --- a/base/commands/job/job_it_test.go +++ b/base/commands/job/job_it_test.go @@ -42,7 +42,7 @@ func submit_NonInteractiveTest(t *testing.T) { defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) }) } @@ -58,7 +58,7 @@ func submit_InteractiveTest(t *testing.T) { defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) }) }) @@ -71,14 +71,14 @@ func list_NonInteractiveTest(t *testing.T) { name := it.NewUniqueObjectName("job") tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--retries", "0", "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") tcx.AssertStdoutContains(name + "\tRUNNING") }) + tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") tcx.WithReset(func() { - tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") tcx.CLCExecute(ctx, "job", "list") tcx.AssertStdoutNotContains(name) }) @@ -93,14 +93,14 @@ func list_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--retries", "0", "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.WriteStdinString("\\job list\n") tcx.AssertStdoutDollar(fmt.Sprintf("%s$|$RUNNING", name)) }) + tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") tcx.WithReset(func() { - tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") tcx.WriteStdinString("\\job list\n") tcx.AssertStdoutNotContains(name) }) @@ -115,14 +115,14 @@ func suspendResume_NonInteractiveTest(t *testing.T) { name := it.NewUniqueObjectName("job") tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "suspend", name) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [2/2] Started") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -130,7 +130,7 @@ func suspendResume_NonInteractiveTest(t *testing.T) { }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "resume", name) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [2/2] Initiated") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -147,14 +147,14 @@ func suspendResume_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "suspend", name, "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -162,7 +162,7 @@ func suspendResume_InteractiveTest(t *testing.T) { }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "resume", name, "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -179,14 +179,14 @@ func restart_NonInteractiveTest(t *testing.T) { name := it.NewUniqueObjectName("job") tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "restart", name, "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -203,14 +203,14 @@ func restart_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.WriteStdinf("\\job restart %s --wait\n", name) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") diff --git a/base/commands/job/job_list.go b/base/commands/job/job_list.go index 5f6e94e12..9029416c8 100644 --- a/base/commands/job/job_list.go +++ b/base/commands/job/job_list.go @@ -19,6 +19,8 @@ import ( type ListCmd struct{} +func (cm ListCmd) Unwrappable() {} + func (cm ListCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("list") help := "List jobs" @@ -113,7 +115,7 @@ func outputJetJobs(ctx context.Context, ec plug.ExecContext, lsi interface{}) er rows = append(rows, row) } if len(rows) == 0 { - ec.PrintlnUnnecessary("No jobs found.") + ec.PrintlnUnnecessary("OK No jobs found.") } return ec.AddOutputRows(ctx, rows...) } diff --git a/base/commands/job/job_resume.go b/base/commands/job/job_resume.go index 7d6cfcf23..9ca8fc359 100644 --- a/base/commands/job/job_resume.go +++ b/base/commands/job/job_resume.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -15,6 +15,8 @@ import ( type ResumeCmd struct{} +func (cm ResumeCmd) Unwrappable() {} + func (cm ResumeCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("resume") help := "Resumes a suspended job" @@ -25,43 +27,48 @@ func (cm ResumeCmd) Init(cc plug.InitContext) error { } func (cm ResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } nameOrID := ec.GetStringArg(argJobID) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Resuming job: %s", nameOrID)) - j := jet.New(ci, sp, ec.Logger()) - jis, err := j.GetJobList(ctx) - if err != nil { - return nil, err - } - jm, err := NewJobNameToIDMap(jis) - if err != nil { - return nil, err - } - jid, ok := jm.GetIDForName(nameOrID) - if !ok { - return nil, jet.ErrInvalidJobID - } - return nil, j.ResumeJob(ctx, jid) - }) - if err != nil { - return err + stages := []stage.Stage[any]{ + stage.MakeConnectStage[any](ec), + { + ProgressMsg: fmt.Sprintf("Initiating resume of job: %s", nameOrID), + SuccessMsg: fmt.Sprintf("Initiated resume of job %s", nameOrID), + FailureMsg: fmt.Sprintf("Failed initiating job resume %s", nameOrID), + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + j := jet.New(ci, status, ec.Logger()) + jis, err := j.GetJobList(ctx) + if err != nil { + return nil, err + } + jm, err := NewJobNameToIDMap(jis) + if err != nil { + return nil, err + } + jid, ok := jm.GetIDForName(nameOrID) + if !ok { + return nil, jet.ErrInvalidJobID + } + return nil, j.ResumeJob(ctx, jid) + }, + }, } - stop() if ec.Props().GetBool(flagWait) { - msg := fmt.Sprintf("Waiting for job %s to start", nameOrID) - ec.Logger().Info(msg) - err = WaitJobState(ctx, ec, msg, nameOrID, jet.JobStatusRunning, 2*time.Second) - if err != nil { - return err - } + stages = append(stages, stage.Stage[any]{ + ProgressMsg: fmt.Sprintf("Waiting for job %s to resume", nameOrID), + SuccessMsg: fmt.Sprintf("Job %s is resumed", nameOrID), + FailureMsg: fmt.Sprintf("Job %s failed to resume", nameOrID), + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + return nil, WaitJobState(ctx, ec, status, nameOrID, jet.JobStatusRunning, 2*time.Second) + }, + }) } - verbose := ec.Props().GetBool(clc.PropertyVerbose) - if verbose { - ec.PrintlnUnnecessary(fmt.Sprintf("Job resumed: %s", nameOrID)) + _, err := stage.Execute[any](ctx, ec, nil, stage.NewFixedProvider(stages...)) + if err != nil { + return err } return nil } diff --git a/base/commands/job/job_submit.go b/base/commands/job/job_submit.go index b1ac797f5..ba9d337cf 100644 --- a/base/commands/job/job_submit.go +++ b/base/commands/job/job_submit.go @@ -14,6 +14,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/log" @@ -30,6 +31,8 @@ const ( type SubmitCmd struct{} +func (cm SubmitCmd) Unwrappable() {} + func (cm SubmitCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("submit") long := fmt.Sprintf(`Submits a jar file to create a Jet job @@ -82,34 +85,40 @@ func submitJar(ctx context.Context, ci *hazelcast.ClientInternal, ec plug.ExecCo _, fn := filepath.Split(path) fn = strings.TrimSuffix(fn, ".jar") args := ec.GetStringSliceArg(argArg) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - j := jet.New(ci, sp, ec.Logger()) - err := retry(tries, ec.Logger(), func(try int) error { - msg := "Submitting the job" - if try == 0 { - sp.SetText(msg) - } else { - sp.SetText(fmt.Sprintf("%s: retry %d", msg, try)) - } - br := jet.CreateBinaryReaderForPath(path) - return j.SubmitJob(ctx, path, jobName, className, snapshot, args, br) - }) - if err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return fmt.Errorf("submitting the job: %w", err) + stages := []stage.Stage[any]{ + stage.MakeConnectStage[any](ec), + { + ProgressMsg: "Submitting the job", + SuccessMsg: "Submitted the job", + FailureMsg: "Failed submitting the job", + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + j := jet.New(ci, status, ec.Logger()) + err := retry(tries, ec.Logger(), func(try int) error { + if try == 0 { + ec.Logger().Info("Submitting %s", jobName) + } else { + ec.Logger().Info("Submitting %s, retry: %d", jobName, try) + } + br := jet.CreateBinaryReaderForPath(path) + return j.SubmitJob(ctx, path, jobName, className, snapshot, args, br) + }) + return nil, err + }, + }, } - stop() if wait { - msg := fmt.Sprintf("Waiting for job %s to start", jobName) - ec.Logger().Info(msg) - err = WaitJobState(ctx, ec, msg, jobName, jet.JobStatusRunning, 2*time.Second) - if err != nil { - return err - } + stages = append(stages, stage.Stage[any]{ + ProgressMsg: fmt.Sprintf("Waiting for job %s to start", jobName), + SuccessMsg: fmt.Sprintf("Job %s started", jobName), + FailureMsg: fmt.Sprintf("Job %s failed to start", jobName), + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + return nil, WaitJobState(ctx, ec, status, jobName, jet.JobStatusRunning, 2*time.Second) + }, + }) + } + _, err := stage.Execute[any](ctx, ec, nil, stage.NewFixedProvider(stages...)) + if err != nil { + return err } return nil } diff --git a/base/commands/job/job_terminate.go b/base/commands/job/job_terminate.go index e1d8b30a3..51d6cc220 100644 --- a/base/commands/job/job_terminate.go +++ b/base/commands/job/job_terminate.go @@ -17,10 +17,14 @@ type TerminateCmd struct { shortHelp string terminateMode int32 terminateModeForce int32 - msg string + inProgressMsg string + successMsg string + failureMsg string waitState int32 } +func (cm TerminateCmd) Unwrappable() {} + func (cm TerminateCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage(cm.name) cc.SetCommandHelp(cm.longHelp, cm.shortHelp) @@ -31,15 +35,11 @@ func (cm TerminateCmd) Init(cc plug.InitContext) error { } func (cm TerminateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - // just preloading the client - if _, err := ec.ClientInternal(ctx); err != nil { - return err - } tm := cm.terminateMode if ec.Props().GetBool(flagForce) { tm = cm.terminateModeForce } - if err := terminateJob(ctx, ec, cm.name, tm, cm.msg, cm.waitState); err != nil { + if err := terminateJob(ctx, ec, tm, cm); err != nil { return err } return nil @@ -48,29 +48,35 @@ func (cm TerminateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { func init() { Must(plug.Registry.RegisterCommand("job:cancel", &TerminateCmd{ name: "cancel", - longHelp: "Cancels the job with the given ID or name.", + longHelp: "Cancels the job with the given ID or name", shortHelp: "Cancels the job with the given ID or name", terminateMode: jet.TerminateModeCancelGraceful, terminateModeForce: jet.TerminateModeCancelForceful, - msg: "Cancelling the job", waitState: jet.JobStatusFailed, + inProgressMsg: "Starting to cancel %s", + successMsg: "Started cancellation of %s", + failureMsg: "Failed to start job cancellation", })) Must(plug.Registry.RegisterCommand("job:suspend", &TerminateCmd{ name: "suspend", - longHelp: "Suspends the job with the given ID or name.", + longHelp: "Suspends the job with the given ID or name", shortHelp: "Suspends the job with the given ID or name", terminateMode: jet.TerminateModeSuspendGraceful, terminateModeForce: jet.TerminateModeSuspendForceful, - msg: "Suspending the job", waitState: jet.JobStatusSuspended, + inProgressMsg: "Starting to suspend %s", + successMsg: "Started suspension of %s", + failureMsg: "Failed to start job suspension", })) Must(plug.Registry.RegisterCommand("job:restart", &TerminateCmd{ name: "restart", - longHelp: "Restarts the job with the given ID or name.", + longHelp: "Restarts the job with the given ID or name", shortHelp: "Restarts the job with the given ID or name", terminateMode: jet.TerminateModeRestartGraceful, terminateModeForce: jet.TerminateModeRestartForceful, - msg: "Restarting the job", waitState: jet.JobStatusRunning, + inProgressMsg: "Initiating the restart of %s", + successMsg: "Initiated the restart of %s", + failureMsg: "Failed to initiate job restart", })) } diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 6450cf758..93892e1df 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -3,11 +3,9 @@ package viridian import ( - "archive/zip" "context" "errors" "fmt" - "io" "net/http" "os" "path/filepath" @@ -15,12 +13,11 @@ import ( "strings" "time" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/types" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) @@ -31,7 +28,7 @@ const ( var ( ErrClusterFailed = errors.New("cluster failed") - ErrLoadingSecrets = errors.New("could not load Viridian secrets, did you login?") + ErrLoadingSecrets = errors.New("could not load Viridian secrets, did you retrieve the access token using the login command?") ) func findTokenPath(apiKey string) (string, error) { @@ -135,107 +132,16 @@ 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") - cfgPath, ok, err := importCLCConfig(ctx, ec, api, clusterID, cfgName) - if err != nil { - ec.Logger().Error(err) - } else if ok { - return cfgPath, err - } - 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") - if err != nil { - return nil, err - } - defer stop() - fns := types.NewSet("client.keystore", "client.pfx", "client.truststore") - imp, err := importFileFromZip(ctx, ec, fns, zipPath, cfgDir) - if err != nil { - return nil, err - } - if imp.Len() != fns.Len() { - ec.Logger().Warn("Could not import all artifacts") - } - return cfgPath, nil - }) - if err != nil { - return "", err - } - stop() - cp := cpv.(string) - ec.Logger().Info("Imported configuration %s and saved to %s", cfgName, cp) - ec.PrintlnUnnecessary(fmt.Sprintf("OK Imported configuration %s", cfgName)) - 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) { +func importConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName, language string, f func(context.Context, string, string, log.Logger) (string, error)) (configPath string, err error) { zipPath, stop, err := api.DownloadConfig(ctx, clusterID, language) if err != nil { - return "", false, err + return "", 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) { - s := types.NewSet[string]() - zr, err := zip.OpenReader(zipPath) - if err != nil { - return nil, err - } - defer zr.Close() - for _, rf := range zr.File { - if ctx.Err() != nil { - return nil, ctx.Err() - } - _, fn := filepath.Split(rf.Name) - if selectPaths.Has(fn) { - if err := copyZipFile(rf, paths.Join(targetDir, fn)); err != nil { - ec.Logger().Error(fmt.Errorf("extracting file: %w", err)) - continue - } - s.Add(fn) - } - } - return s, nil -} - -func copyZipFile(file *zip.File, path string) error { - f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - r, err := file.Open() - if err != nil { - return err - } - defer r.Close() - if _, err = io.Copy(f, r); err != nil { - return err - } - return nil + return f(ctx, cfgName, zipPath, ec.Logger()) } func matchClusterState(cluster viridian.Cluster, state string) (bool, error) { diff --git a/base/commands/viridian/custom_class_delete.go b/base/commands/viridian/custom_class_delete.go index ea109c138..5c1b08006 100644 --- a/base/commands/viridian/custom_class_delete.go +++ b/base/commands/viridian/custom_class_delete.go @@ -14,6 +14,8 @@ import ( type CustomClassDeleteCmd struct{} +func (cmd CustomClassDeleteCmd) Unwrappable() {} + func (cmd CustomClassDeleteCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("delete-custom-class") long := `Deletes a custom class from the given Viridian cluster. @@ -61,7 +63,7 @@ func (cmd CustomClassDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) e return handleErrorResponse(ec, err) } stop() - ec.PrintlnUnnecessary("Custom class was deleted.") + ec.PrintlnUnnecessary("OK Custom class was deleted.") return nil } diff --git a/base/commands/viridian/custom_class_download.go b/base/commands/viridian/custom_class_download.go index b6f59aafe..fdf98c9b2 100644 --- a/base/commands/viridian/custom_class_download.go +++ b/base/commands/viridian/custom_class_download.go @@ -8,8 +8,10 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) @@ -17,6 +19,8 @@ const flagOutputPath = "output-path" type CustomClassDownloadCmd struct{} +func (cmd CustomClassDownloadCmd) Unwrappable() {} + func (cmd CustomClassDownloadCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("download-custom-class") long := `Downloads a custom class from the given Viridian cluster. @@ -27,6 +31,7 @@ Make sure you login before running this command. cc.SetCommandHelp(long, short) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") cc.AddStringFlag(flagOutputPath, "o", "", false, "download path") + cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming overwrite") cc.AddStringArg(argClusterID, argTitleClusterID) cc.AddStringArg(argArtifactID, argTitleArtifactID) return nil @@ -73,8 +78,15 @@ func (cmd CustomClassDownloadCmd) Exec(ctx context.Context, ec plug.ExecContext) return handleErrorResponse(ec, err) } stop() - ec.PrintlnUnnecessary("Custom class was downloaded.") - return nil + ec.PrintlnUnnecessary("OK Custom class was saved.\n") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Path", + Type: serialization.TypeString, + // TODO: t.Path should not have / as the suffix + Value: t.Path + t.FileName, + }, + }) } func init() { diff --git a/base/commands/viridian/custom_class_list.go b/base/commands/viridian/custom_class_list.go index 8e990ef0c..7483a5985 100644 --- a/base/commands/viridian/custom_class_list.go +++ b/base/commands/viridian/custom_class_list.go @@ -15,6 +15,8 @@ import ( type CustomClassListCmd struct{} +func (cmd CustomClassListCmd) Unwrappable() {} + func (cmd CustomClassListCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("list-custom-classes") long := `Lists all custom classes in the given Viridian cluster. @@ -48,6 +50,10 @@ func (cmd CustomClassListCmd) Exec(ctx context.Context, ec plug.ExecContext) err } stop() cs := csi.([]viridian.CustomClass) + if len(cs) == 0 { + ec.PrintlnUnnecessary("OK There are no custom classes on this cluster.") + return nil + } rows := make([]output.Row, len(cs)) for i, c := range cs { r := output.Row{ diff --git a/base/commands/viridian/custom_class_upload.go b/base/commands/viridian/custom_class_upload.go index d64755aaf..9e5a7d180 100644 --- a/base/commands/viridian/custom_class_upload.go +++ b/base/commands/viridian/custom_class_upload.go @@ -17,6 +17,8 @@ const ( type CustomClassUploadCmd struct{} +func (cmd CustomClassUploadCmd) Unwrappable() {} + func (cmd CustomClassUploadCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("upload-custom-class") long := `Uploads a new Custom Class to the specified Viridian cluster. @@ -50,7 +52,7 @@ func (cmd CustomClassUploadCmd) Exec(ctx context.Context, ec plug.ExecContext) e return handleErrorResponse(ec, err) } stop() - ec.PrintlnUnnecessary("Custom class was uploaded.") + ec.PrintlnUnnecessary("OK Custom class was uploaded.") return nil } diff --git a/base/commands/viridian/download_logs.go b/base/commands/viridian/download_logs.go index b32e89cc4..7cd3c7240 100644 --- a/base/commands/viridian/download_logs.go +++ b/base/commands/viridian/download_logs.go @@ -4,16 +4,21 @@ package viridian import ( "context" - "errors" + "fmt" "os" + "path/filepath" - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) type DownloadLogsCmd struct{} +func (cm DownloadLogsCmd) Unwrappable() {} + func (cm DownloadLogsCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("download-logs") long := `Downloads the logs of the given Viridian cluster for the logged in API key. @@ -23,7 +28,7 @@ Make sure you login before running this command. short := "Downloads the logs of the given Viridian cluster" cc.SetCommandHelp(long, short) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") - cc.AddStringFlag(flagOutputDir, "o", "", false, "output directory for the log files, if not given current directory is used") + cc.AddStringFlag(flagOutputDir, "o", ".", false, "output directory for the log files; current directory is used by default") cc.AddStringArg(argClusterID, argTitleClusterID) return nil } @@ -35,23 +40,33 @@ func (cm DownloadLogsCmd) Exec(ctx context.Context, ec plug.ExecContext) error { } clusterNameOrID := ec.GetStringArg(argClusterID) outDir := ec.Props().GetString(flagOutputDir) - // extract target info + outDir, err = filepath.Abs(outDir) + if err != nil { + return err + } if err := validateOutputDir(outDir); err != nil { return err } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Downloading cluster logs") - err := api.DownloadClusterLogs(ctx, outDir, clusterNameOrID) - if err != nil { - return nil, err - } - return nil, nil - }) + st := stage.Stage[string]{ + ProgressMsg: "Downloading the cluster logs", + SuccessMsg: "Downloaded the cluster logs", + FailureMsg: "Failed downloading the cluster logs", + Func: func(ctx context.Context, status stage.Statuser[string]) (string, error) { + return api.DownloadClusterLogs(ctx, outDir, clusterNameOrID) + }, + } + dir, err := stage.Execute(ctx, ec, "", stage.NewFixedProvider(st)) if err != nil { return handleErrorResponse(ec, err) } - stop() - return nil + ec.PrintlnUnnecessary("") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Directory", + Type: serialization.TypeString, + Value: dir, + }, + }) } func init() { @@ -69,5 +84,5 @@ func validateOutputDir(dir string) error { if info.IsDir() { return nil } - return errors.New("output-dir is not a directory") + return fmt.Errorf("not a directory: %s", dir) } diff --git a/base/commands/viridian/viridian_cluster_create.go b/base/commands/viridian/viridian_cluster_create.go index 00cc4705d..3577bafce 100644 --- a/base/commands/viridian/viridian_cluster_create.go +++ b/base/commands/viridian/viridian_cluster_create.go @@ -5,9 +5,9 @@ package viridian import ( "context" "errors" - "fmt" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -17,6 +17,8 @@ import ( type ClusterCreateCmd struct{} +func (cm ClusterCreateCmd) Unwrappable() {} + func (cm ClusterCreateCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("create-cluster") long := `Creates a Viridian cluster. @@ -41,53 +43,79 @@ func (cm ClusterCreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error dev := ec.Props().GetBool(flagDevelopment) prerelease := ec.Props().GetBool(flagPrerelease) hzVersion := ec.Props().GetString(flagHazelcastVersion) - csi, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Creating the cluster") - k8sCluster, err := getFirstAvailableK8sCluster(ctx, api) - if err != nil { - return nil, err - } - cs, err := api.CreateCluster(ctx, name, getClusterType(dev), k8sCluster.ID, prerelease, hzVersion) - if err != nil { - return nil, err - } - return cs, nil - }) + stages := []stage.Stage[createStageState]{ + { + ProgressMsg: "Initiating cluster creation", + SuccessMsg: "Initiated cluster creation", + FailureMsg: "Failed initiating cluster creation", + Func: func(ctx context.Context, status stage.Statuser[createStageState]) (createStageState, error) { + state := createStageState{} + k8sCluster, err := getFirstAvailableK8sCluster(ctx, api) + if err != nil { + return state, err + } + cs, err := api.CreateCluster(ctx, name, getClusterType(dev), k8sCluster.ID, prerelease, hzVersion) + if err != nil { + return state, err + } + state.Cluster = cs + return state, nil + }, + }, + { + ProgressMsg: "Waiting for the cluster to get ready", + SuccessMsg: "Cluster is ready", + FailureMsg: "Failed while waiting for cluster to get ready", + Func: func(ctx context.Context, status stage.Statuser[createStageState]) (createStageState, error) { + state := status.Value() + if err := waitClusterState(ctx, ec, api, state.Cluster.ID, stateRunning); err != nil { + return state, err + } + return state, nil + }, + }, + { + ProgressMsg: "Importing the configuration", + SuccessMsg: "Imported the configuration", + FailureMsg: "Failed importing the configuration", + Func: func(ctx context.Context, status stage.Statuser[createStageState]) (createStageState, error) { + state := status.Value() + path, err := tryImportConfig(ctx, ec, api, state.Cluster.ID, state.Cluster.Name) + if err != nil { + return state, nil + } + state.ConfigPath = path + return state, nil + }, + }, + } + state, err := stage.Execute(ctx, ec, createStageState{}, stage.NewFixedProvider(stages...)) if err != nil { return handleErrorResponse(ec, err) } - stop() - c := csi.(viridian.Cluster) - ec.PrintlnUnnecessary(fmt.Sprintf("Cluster %s was created.", c.Name)) - _, stop, err = ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Waiting for the cluster to get ready") - if err := waitClusterState(ctx, ec, api, c.ID, stateRunning); err != nil { - // do not import the config and exit early - return nil, err - } - return nil, nil - }) - if _, err = tryImportConfig(ctx, ec, api, c.ID, c.Name); err != nil { - err = handleErrorResponse(ec, err) - ec.PrintlnUnnecessary(fmt.Sprintf("FAIL Could not import cluster configuration: %s", err.Error())) + ec.PrintlnUnnecessary("OK Created the cluster.\n") + rows := output.Row{ + output.Column{ + Name: "ID", + Type: serialization.TypeString, + Value: state.Cluster.ID, + }, } - verbose := ec.Props().GetBool(clc.PropertyVerbose) - if verbose { - row := output.Row{ + if ec.Props().GetBool(clc.PropertyVerbose) { + rows = append(rows, output.Column{ - Name: "ID", + Name: "Name", Type: serialization.TypeString, - Value: c.ID, + Value: state.Cluster.Name, }, output.Column{ - Name: "Name", + Name: "Configuration Path", Type: serialization.TypeString, - Value: c.Name, + Value: state.ConfigPath, }, - } - return ec.AddOutputRows(ctx, row) + ) } - return nil + return ec.AddOutputRows(ctx, rows) } func getClusterType(dev bool) string { @@ -108,6 +136,11 @@ func getFirstAvailableK8sCluster(ctx context.Context, api *viridian.API) (viridi return clusters[0], nil } +type createStageState struct { + Cluster viridian.Cluster + ConfigPath string +} + func init() { Must(plug.Registry.RegisterCommand("viridian:create-cluster", &ClusterCreateCmd{})) } diff --git a/base/commands/viridian/viridian_cluster_delete.go b/base/commands/viridian/viridian_cluster_delete.go index 4245c9716..c5d02f716 100644 --- a/base/commands/viridian/viridian_cluster_delete.go +++ b/base/commands/viridian/viridian_cluster_delete.go @@ -4,17 +4,22 @@ package viridian import ( "context" - "fmt" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) type ClusterDeleteCmd struct{} +func (cm ClusterDeleteCmd) Unwrappable() {} + func (cm ClusterDeleteCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("delete-cluster") long := `Deletes the given Viridian cluster. @@ -46,21 +51,39 @@ func (cm ClusterDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error return errors.ErrUserCancelled } } - clusterNameOrID := ec.GetStringArg(argClusterID) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Deleting the cluster") - err := api.DeleteCluster(ctx, clusterNameOrID) - if err != nil { - return nil, err - } - return nil, nil - }) + nameOrID := ec.GetStringArg(argClusterID) + st := stage.Stage[viridian.Cluster]{ + ProgressMsg: "Initiating cluster deletion", + SuccessMsg: "Inititated cluster deletion", + FailureMsg: "Failed to inititate cluster deletion", + Func: func(ctx context.Context, status stage.Statuser[viridian.Cluster]) (viridian.Cluster, error) { + cluster, err := api.DeleteCluster(ctx, nameOrID) + if err != nil { + return cluster, err + } + return cluster, nil + }, + } + cluster, err := stage.Execute(ctx, ec, viridian.Cluster{}, stage.NewFixedProvider(st)) if err != nil { return handleErrorResponse(ec, err) } - stop() - ec.PrintlnUnnecessary(fmt.Sprintf("Cluster %s was deleted.", clusterNameOrID)) - return nil + ec.PrintlnUnnecessary("") + row := []output.Column{ + { + Name: "ID", + Type: serialization.TypeString, + Value: cluster.ID, + }, + } + if ec.Props().GetBool(clc.PropertyVerbose) { + row = append(row, output.Column{ + Name: "ID", + Type: serialization.TypeString, + Value: cluster.ID, + }) + } + return ec.AddOutputRows(ctx, row) } func init() { diff --git a/base/commands/viridian/viridian_cluster_get.go b/base/commands/viridian/viridian_cluster_get.go index 6f69aa0aa..38c0a90dd 100644 --- a/base/commands/viridian/viridian_cluster_get.go +++ b/base/commands/viridian/viridian_cluster_get.go @@ -16,6 +16,8 @@ import ( type ClusterGetCmd struct{} +func (cm ClusterGetCmd) Unwrappable() {} + func (cm ClusterGetCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("get-cluster") long := `Gets the information about the given Viridian cluster. @@ -34,10 +36,10 @@ func (cm ClusterGetCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - clusterNameOrID := ec.GetStringArg(argClusterID) + nameOrID := ec.GetStringArg(argClusterID) ci, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Retrieving the cluster") - c, err := api.GetCluster(ctx, clusterNameOrID) + sp.SetText("Retrieving cluster information") + c, err := api.GetCluster(ctx, nameOrID) if err != nil { return nil, err } diff --git a/base/commands/viridian/viridian_cluster_list.go b/base/commands/viridian/viridian_cluster_list.go index 014fc2f9d..c61583a23 100644 --- a/base/commands/viridian/viridian_cluster_list.go +++ b/base/commands/viridian/viridian_cluster_list.go @@ -15,6 +15,8 @@ import ( type ClusterListCmd struct{} +func (cm ClusterListCmd) Unwrappable() {} + func (cm ClusterListCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("list-clusters") long := `Lists all Viridian clusters for the logged in API key. @@ -33,7 +35,7 @@ func (cm ClusterListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return err } csi, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Retrieving clusters") + sp.SetText("Retrieving the clusters") cs, err := api.ListClusters(ctx) if err != nil { return nil, err @@ -46,7 +48,8 @@ func (cm ClusterListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { stop() cs := csi.([]viridian.Cluster) if len(cs) == 0 { - ec.PrintlnUnnecessary("No clusters found") + ec.PrintlnUnnecessary("OK No clusters found") + return nil } rows := make([]output.Row, len(cs)) verbose := ec.Props().GetBool(clc.PropertyVerbose) diff --git a/base/commands/viridian/viridian_cluster_resume.go b/base/commands/viridian/viridian_cluster_resume.go index 586084a96..5ae9f9953 100644 --- a/base/commands/viridian/viridian_cluster_resume.go +++ b/base/commands/viridian/viridian_cluster_resume.go @@ -4,15 +4,19 @@ package viridian import ( "context" - "fmt" - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) type ClusterResumeCmd struct{} +func (cm ClusterResumeCmd) Unwrappable() {} + func (cm ClusterResumeCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("resume-cluster") long := `Resumes the given Viridian cluster. @@ -31,21 +35,27 @@ func (cm ClusterResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return err } - clusterNameOrID := ec.GetStringArg(argClusterID) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Resuming the cluster") - err := api.ResumeCluster(ctx, clusterNameOrID) - if err != nil { - return nil, err - } - return nil, nil - }) + nameOrID := ec.GetStringArg(argClusterID) + st := stage.Stage[viridian.Cluster]{ + ProgressMsg: "Starting to resume the cluster", + SuccessMsg: "Started to resume the cluster", + FailureMsg: "Failed to start resuming the cluster", + Func: func(ctx context.Context, status stage.Statuser[viridian.Cluster]) (viridian.Cluster, error) { + return api.ResumeCluster(ctx, nameOrID) + }, + } + cluster, err := stage.Execute(ctx, ec, viridian.Cluster{}, stage.NewFixedProvider(st)) if err != nil { return handleErrorResponse(ec, err) } - stop() - ec.PrintlnUnnecessary(fmt.Sprintf("Cluster %s was resumed.", clusterNameOrID)) - return nil + ec.PrintlnUnnecessary("") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "ID", + Type: serialization.TypeString, + Value: cluster.ID, + }, + }) } func init() { diff --git a/base/commands/viridian/viridian_cluster_stop.go b/base/commands/viridian/viridian_cluster_stop.go index 462329744..fdbb04b44 100644 --- a/base/commands/viridian/viridian_cluster_stop.go +++ b/base/commands/viridian/viridian_cluster_stop.go @@ -4,15 +4,19 @@ package viridian import ( "context" - "fmt" - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) type ClusterStopCmd struct{} +func (cm ClusterStopCmd) Unwrappable() {} + func (cm ClusterStopCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("stop-cluster") long := `Stops the given Viridian cluster. @@ -31,21 +35,27 @@ func (cm ClusterStopCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return err } - clusterNameOrID := ec.GetStringArg(argClusterID) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Pausing the cluster") - err := api.StopCluster(ctx, clusterNameOrID) - if err != nil { - return nil, err - } - return nil, nil - }) + nameOrID := ec.GetStringArg(argClusterID) + st := stage.Stage[viridian.Cluster]{ + ProgressMsg: "Initiating cluster stop", + SuccessMsg: "Initiated cluster stop", + FailureMsg: "Failed to initiate cluster stop", + Func: func(ctx context.Context, status stage.Statuser[viridian.Cluster]) (viridian.Cluster, error) { + return api.StopCluster(ctx, nameOrID) + }, + } + cluster, err := stage.Execute(ctx, ec, viridian.Cluster{}, stage.NewFixedProvider(st)) if err != nil { return handleErrorResponse(ec, err) } - stop() - ec.PrintlnUnnecessary(fmt.Sprintf("Cluster %s was stopped.", clusterNameOrID)) - return nil + ec.PrintlnUnnecessary("") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "ID", + Type: serialization.TypeString, + Value: cluster.ID, + }, + }) } func init() { diff --git a/base/commands/viridian/viridian_import_config.go b/base/commands/viridian/viridian_import_config.go index 4d934ee7e..937b8bb25 100644 --- a/base/commands/viridian/viridian_import_config.go +++ b/base/commands/viridian/viridian_import_config.go @@ -6,13 +6,18 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + iserialization "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) type ImportConfigCmd struct{} +func (ImportConfigCmd) Unwrappable() {} + func (ImportConfigCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("import-config") long := `Imports connection configuration of the given Viridian cluster. @@ -49,14 +54,28 @@ func (ImportConfigCmd) exec(ctx context.Context, ec plug.ExecContext) error { if cfgName == "" { cfgName = c.Name } - if _, err = tryImportConfig(ctx, ec, api, c.ID, cfgName); err != nil { + st := stage.Stage[string]{ + ProgressMsg: "Importing the configuration", + SuccessMsg: "Imported the configuration", + FailureMsg: "Failed importing the configuration", + Func: func(ctx context.Context, status stage.Statuser[string]) (string, error) { + return tryImportConfig(ctx, ec, api, c.ID, cfgName) + }, + } + path, err := stage.Execute(ctx, ec, "", stage.NewFixedProvider(st)) + if err != nil { return handleErrorResponse(ec, err) } - return nil + ec.PrintlnUnnecessary("") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Configuration Path", + Type: iserialization.TypeString, + Value: path, + }, + }) } -func (ImportConfigCmd) Unwrappable() {} - func init() { Must(plug.Registry.RegisterCommand("viridian:import-config", &ImportConfigCmd{})) } diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index 125b46807..ecef81c88 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -223,8 +223,8 @@ func getCluster_NonInteractiveTest(t *testing.T) { viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "") tcx.CLCExecute(ctx, "viridian", "get-cluster", c.ID, "--verbose", "-f", "json") - tcx.AssertStderrContains("OK") - fields := tcx.AssertJSONStdoutHasRowWithFields("ID", "Name", "State", "Hazelcast Version", "Creation Time", "Start Time", "Hot Backup Enabled", "Hot Restart Enabled", "IP Whitelist Enabled", "Regions") + tcx.AssertStdoutContains("Name") + fields := tcx.AssertJSONStdoutHasRowWithFields("ID", "Name", "State", "Hazelcast Version", "Creation Time", "Start Time", "Hot Backup Enabled", "Hot Restart Enabled", "IP Whitelist Enabled", "Regions", "Cluster Type") require.Equal(t, c.ID, fields["ID"]) require.Equal(t, c.Name, fields["Name"]) }) @@ -249,7 +249,7 @@ func deleteCluster_NonInteractiveTest(t *testing.T) { viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.CLCExecute(ctx, "viridian", "delete-cluster", c.ID, "--yes") - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK Inititated cluster deletion.") require.Eventually(t, func() bool { _, err := tcx.Viridian.GetCluster(ctx, c.ID) return err != nil diff --git a/base/commands/viridian/viridian_log_stream.go b/base/commands/viridian/viridian_log_stream.go index c44912a26..eb9c5d754 100644 --- a/base/commands/viridian/viridian_log_stream.go +++ b/base/commands/viridian/viridian_log_stream.go @@ -24,6 +24,8 @@ const ( type StreamLogCmd struct{} +func (cm StreamLogCmd) Unwrappable() {} + func (cm StreamLogCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("stream-logs") long := `Outputs the logs of the given Viridian cluster as a stream. diff --git a/base/commands/viridian/viridian_login.go b/base/commands/viridian/viridian_login.go index 848802317..b907cdd57 100644 --- a/base/commands/viridian/viridian_login.go +++ b/base/commands/viridian/viridian_login.go @@ -11,9 +11,12 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) @@ -26,15 +29,21 @@ const ( type LoginCmd struct{} +func (cm LoginCmd) Unwrappable() {} + func (cm LoginCmd) Init(cc plug.InitContext) error { cc.SetCommandUsage("login") short := "Logs in to Viridian using the given API key and API secret" - long := fmt.Sprintf(`Logs in to Viridian using the given API key and API secret. -If not specified, the key and the secret will be asked in a prompt. + long := fmt.Sprintf(`Logs in to Viridian to get an access token using the given API key and API secret. +Other Viridian commands use the access token retrieved by this command. +Running this command is only necessary when a new API key is generated. + +If not specified, the key and the secret will be asked in a prompt. Alternatively, you can use the following environment variables: -* %s -* %s + + * %s + * %s `, viridian.EnvAPIKey, viridian.EnvAPISecret) cc.SetCommandHelp(long, short) cc.AddStringFlag(propAPIKey, "", "", false, "Viridian API Key") @@ -49,22 +58,47 @@ func (cm LoginCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return err } ab := getAPIBase(ec) - token, err := cm.retrieveToken(ctx, ec, key, secret, ab) - if err != nil { - return err + stages := []stage.Stage[string]{ + { + ProgressMsg: "Retrieving the access token", + SuccessMsg: "Retrieved the access token", + FailureMsg: "Failed retrieving the access token", + Func: func(ctx context.Context, status stage.Statuser[string]) (string, error) { + return cm.retrieveToken(ctx, ec, key, secret, ab) + }, + }, + { + ProgressMsg: "Saving the access token", + SuccessMsg: "Saved the access token", + FailureMsg: "Failed saving the access token", + Func: func(ctx context.Context, status stage.Statuser[string]) (string, error) { + token := status.Value() + secret += "\n" + ab + sk := fmt.Sprintf(fmtSecretFileName, viridian.APIClass(), key) + if err = secrets.Save(ctx, secretPrefix, sk, secret); err != nil { + return "", err + } + tk := fmt.Sprintf(viridian.FmtTokenFileName, viridian.APIClass(), key) + if err = secrets.Save(ctx, secretPrefix, tk, token); err != nil { + return "", err + } + return key, nil + }, + }, } - secret += "\n" + ab - sk := fmt.Sprintf(fmtSecretFileName, viridian.APIClass(), key) - if err = secrets.Save(ctx, secretPrefix, sk, secret); err != nil { - return err - } - tk := fmt.Sprintf(viridian.FmtTokenFileName, viridian.APIClass(), key) - if err = secrets.Save(ctx, secretPrefix, tk, token); err != nil { - return err + // not using the output of the stage since it is the key + _, err = stage.Execute(ctx, ec, "", stage.NewFixedProvider(stages...)) + if err != nil { + return handleErrorResponse(ec, err) } ec.PrintlnUnnecessary("") - ec.PrintlnUnnecessary("Viridian token was fetched and saved.") - return nil + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "API Key", + Type: serialization.TypeString, + Value: key, + }, + }) } func (cm LoginCmd) retrieveToken(ctx context.Context, ec plug.ExecContext, key, secret, apiBase string) (string, error) { @@ -101,7 +135,7 @@ func getAPIKeySecret(ec plug.ExecContext) (key, secret string, err error) { key = os.Getenv(viridian.EnvAPIKey) } if key == "" { - key, err = pr.Text("API Key : ") + key, err = pr.Text(" API Key : ") if err != nil { return "", "", fmt.Errorf("reading API key: %w", err) } @@ -114,7 +148,7 @@ func getAPIKeySecret(ec plug.ExecContext) (key, secret string, err error) { secret = os.Getenv(viridian.EnvAPISecret) } if secret == "" { - secret, err = pr.Password("API Secret : ") + secret, err = pr.Password(" API Secret : ") if err != nil { return "", "", fmt.Errorf("reading API secret: %w", err) } diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index 101223622..468752712 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -148,24 +148,10 @@ func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInt if err != nil { return nil, err } - civ, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Connecting to the cluster") - if err := ec.main.ensureClient(ctx, cfg); err != nil { - return nil, err - } - return ec.main.clientInternal(), nil - }) - if err != nil { + if err := ec.main.ensureClient(ctx, cfg); err != nil { return nil, err } - stop() - ci = civ.(*hazelcast.ClientInternal) - verbose := ec.Props().GetBool(clc.PropertyVerbose) - if verbose || ec.Interactive() { - cn := ci.ClusterService().FailoverService().Current().ClusterName - ec.PrintlnUnnecessary(fmt.Sprintf("Connected to cluster: %s", cn)) - } - return ci, nil + return ec.main.clientInternal(), nil } func (ec *ExecContext) Interactive() bool { diff --git a/clc/cmd/utils.go b/clc/cmd/utils.go index dbfcb8403..25e5c4305 100644 --- a/clc/cmd/utils.go +++ b/clc/cmd/utils.go @@ -15,6 +15,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" cmderrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) func ExtractStartupArgs(args []string) (cfgPath, logFile, logLevel string, err error) { @@ -73,6 +74,11 @@ func MakeErrStr(err error) string { return fmt.Sprintf("Error: %s", errStr) } +func ClientInternal(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.ClientInternal, error) { + sp.SetText("Connecting to the cluster") + return ec.ClientInternal(ctx) +} + func parseDuration(duration string) (time.Duration, error) { // input can be like: 10_000_000 or 10_000_000ms, so remove underscores ds := strings.ReplaceAll(duration, "_", "") diff --git a/clc/config/import.go b/clc/config/import.go index d991fcf03..0e28a0bd7 100644 --- a/clc/config/import.go +++ b/clc/config/import.go @@ -3,255 +3,154 @@ package config import ( "archive/zip" "context" - "errors" "fmt" "io" - "net/http" "os" "path/filepath" - "regexp" "strings" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" + ihttp "github.com/hazelcast/hazelcast-commandline-client/internal/http" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) +func MakeImportStages(ec plug.ExecContext, target string) []stage.Stage[string] { + stages := []stage.Stage[string]{ + { + ProgressMsg: "Retrieving the configuration", + SuccessMsg: "Retrieved the configuration", + FailureMsg: "Failed retrieving the configuration", + Func: func(ctx context.Context, status stage.Statuser[string]) (string, error) { + source := status.Value() + if !strings.HasPrefix(source, "https://") || strings.HasSuffix(source, "http://") { + if !paths.Exists(source) { + return "", fmt.Errorf("%s does not exist", source) + } + return source, nil + } + path, err := download(ctx, source) + if err != nil { + return "", err + } + return path, nil + }, + }, + { + ProgressMsg: "Preparing the configuration", + SuccessMsg: "The configuration is ready", + FailureMsg: "Failed preparing the configuration", + Func: func(ctx context.Context, status stage.Statuser[string]) (string, error) { + path := status.Value() + path, err := CreateFromZip(ctx, target, path, ec.Logger()) + if err != nil { + return "", err + } + return path, nil + }, + }, + } + return stages +} + +/* func ImportSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, error) { target = strings.TrimSpace(target) src = strings.TrimSpace(src) - // check whether this an HTTP source - path, ok, err := tryImportHTTPSource(ctx, ec, target, src) + // check whether this is an HTTP source + path, err := tryImportHTTPSource(ctx, ec, target, src) if err != nil { return "", err } - // import is successful - if ok { + if path != "" { + // import is successful return path, nil } // import is not successful, so assume this is a zip file path and try to import from it. - path, ok, err = tryImportViridianZipSource(ctx, ec, target, src) + path, err = tryImportViridianZipSource(ctx, target, src, ec.Logger()) if err != nil { return "", err } - if !ok { + if path != "" { return "", fmt.Errorf("unusable source: %s", src) } return path, nil } -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 - } - path, err := download(ctx, ec, url) +func tryImportHTTPSource(ctx context.Context, target, url string, lg log.Logger) (string, error) { + path, err := download(ctx, url) if err != nil { - return "", false, err + return "", err } - ec.Logger().Info("Downloaded the configuration at: %s", path) - return tryImportViridianZipSource(ctx, ec, target, path) + lg.Info("Downloaded the configuration at: %s", path) + return tryImportViridianZipSource(ctx, target, path, lg) } +*/ -// 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) { - path, ok, err := CreateFromZip(ctx, ec, target, src) - if ok { - return path, true, nil +func download(ctx context.Context, url string) (string, error) { + f, err := os.CreateTemp("", "clc-download-*") + if err != nil { + return "", err } - path, ok, err = CreateFromZipLegacy(ctx, ec, target, src) + defer f.Close() + client := ihttp.NewClient() + resp, err := client.Get(ctx, url) if err != nil { - return "", ok, err + return "", err + } + defer resp.Body.Close() + if _, err := io.Copy(f, resp.Body); err != nil { + return "", fmt.Errorf("downloading file: %w", err) } - 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 configuration") - f, err := os.CreateTemp("", "clc-download-*") - if err != nil { - return "", err - } - defer f.Close() - resp, err := http.Get(url) - defer resp.Body.Close() - if _, err := io.Copy(f, resp.Body); err != nil { - return "", fmt.Errorf("downloading file: %w", err) - } - return f.Name(), nil - }) if err != nil { return "", nil } - stop() - return p.(string), nil + return f.Name(), nil } -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 - }) +func CreateFromZip(ctx context.Context, target, path string, lg log.Logger) (string, error) { + reader, err := zip.OpenReader(path) if err != nil { - return "", false, err - } - stop() - if p == false { - return "", false, nil + return "", err } - 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") - reader, err := zip.OpenReader(path) - if err != nil { - return nil, err - } - defer reader.Close() - var pyPaths []string - var pemFiles []*zip.File - // find .py and .pem paths - for _, rf := range reader.File { - if strings.HasSuffix(rf.Name, ".py") { - pyPaths = append(pyPaths, rf.Name) - continue - } - // copy only pem files - if !strings.HasSuffix(rf.Name, ".pem") { - continue - } - pemFiles = append(pemFiles, rf) - } - var cfgFound bool - // find the configuration bits - token, clusterName, pw, apiBase, cfgFound := extractConfigFields(reader, pyPaths) - if !cfgFound { - return nil, errors.New("python file with configuration not found") - } - opts := makeViridianOpts(clusterName, token, pw, apiBase) - outDir, cfgPath, err := Create(target, opts) - 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 ctx.Err() != nil { + return "", ctx.Err() } - mopts := makeViridianOptsMap(clusterName, token, pw, apiBase) - // ignoring the JSON path for now - _, _, err = CreateJSON(target, mopts) - if err != nil { - ec.Logger().Warn("Failed creating the JSON configuration: %s", err.Error()) + if strings.HasSuffix(rf.Name, "/config.json") { + newConfig = true } - // copy pem files - if err := copyFiles(ec, pemFiles, outDir); err != nil { - return nil, err + if !rf.FileInfo().IsDir() { + files = append(files, rf) } - return paths.Join(outDir, cfgPath), nil - }) - if err != nil { - return "", false, err - } - stop() - return p.(string), true, nil -} - -func makeViridianOpts(clusterName, token, password, apiBaseURL string) types.KeyValues[string, string] { - return types.KeyValues[string, string]{ - {Key: "cluster.name", Value: clusterName}, - {Key: "cluster.discovery-token", Value: token}, - {Key: "cluster.api-base", Value: apiBaseURL}, - {Key: "ssl.ca-path", Value: "ca.pem"}, - {Key: "ssl.cert-path", Value: "cert.pem"}, - {Key: "ssl.key-path", Value: "key.pem"}, - {Key: "ssl.key-password", Value: password}, } -} - -func makeViridianOptsMap(clusterName, token, password, apiBaseURL string) map[string]any { - cm := map[string]any{ - "name": clusterName, - "discovery-token": token, - "api-base": apiBaseURL, + if !newConfig { + return "", nil } - ssl := map[string]any{ - "ca-path": "ca.pem", - "cert-path": "cert.pem", - "key-path": "key.pem", - "key-password": password, + // this is the new config zip, just extract to target + outDir, cfgFileName, err := DirAndFile(target) + if err != nil { + return "", err } - return map[string]any{ - "cluster": cm, - "ssl": ssl, + if err = os.MkdirAll(outDir, 0700); err != nil { + return "", err } -} - -func extractConfigFields(reader *zip.ReadCloser, pyPaths []string) (token, clusterName, pw, apiBase string, cfgFound bool) { - for _, p := range pyPaths { - rc, err := reader.Open(p) - if err != nil { - continue - } - b, err := io.ReadAll(rc) - _ = rc.Close() - if err != nil { - continue - } - text := string(b) - token = extractViridianToken(text) - if token == "" { - continue - } - clusterName = extractClusterName(text) - if clusterName == "" { - continue - } - pw = extractKeyPassword(text) - // it's OK if password is not found - apiBase = extractClusterAPIBaseURL(text) - if apiBase != "" { - apiBase = "https://" + apiBase - } - // it's OK if apiBase is not found - cfgFound = true - break + if err = copyFiles(ctx, files, outDir, lg); err != nil { + return "", err } - return + return paths.Join(outDir, cfgFileName), nil } -func copyFiles(ec plug.ExecContext, files []*zip.File, outDir string) error { +func copyFiles(ctx context.Context, files []*zip.File, outDir string, lg log.Logger) error { for _, rf := range files { + if ctx.Err() != nil { + return ctx.Err() + } _, outFn := filepath.Split(rf.Name) path := paths.Join(outDir, outFn) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) @@ -266,44 +165,8 @@ func copyFiles(ec plug.ExecContext, files []*zip.File, outDir string) error { // ignoring the error here _ = rc.Close() if err != nil { - ec.Logger().Error(err) + lg.Error(err) } } return nil } - -func extractClusterName(text string) string { - // extract from cluster_name="XXXX" - const re = `cluster_name="([^"]+)"` - return extractSimpleString(re, text) -} - -func extractClusterAPIBaseURL(text string) string { - // extract from HazelcastCloudDiscovery._CLOUD_URL_BASE = "XXXX" - const re = `HazelcastCloudDiscovery._CLOUD_URL_BASE\s*=\s*"([^"]+)"` - return extractSimpleString(re, text) -} - -func extractViridianToken(text string) string { - // extract from: cloud_discovery_token="XXXX", - const re = `cloud_discovery_token="([^"]+)"` - return extractSimpleString(re, text) -} - -func extractKeyPassword(text string) string { - // extract from: ssl_password="XXXX", - const re = `ssl_password="([^"]+)"` - return extractSimpleString(re, text) -} - -func extractSimpleString(pattern, text string) string { - re, err := regexp.Compile(pattern) - if err != nil { - panic(err) - } - grps := re.FindStringSubmatch(text) - if len(grps) != 2 { - return "" - } - return grps[1] -} diff --git a/clc/config/wizard_provider.go b/clc/config/wizard_provider.go index d27296bb4..98bd0d048 100644 --- a/clc/config/wizard_provider.go +++ b/clc/config/wizard_provider.go @@ -8,11 +8,13 @@ import ( "sync/atomic" tea "github.com/charmbracelet/bubbletea" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" "github.com/hazelcast/hazelcast-go-client" "github.com/spf13/pflag" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" + "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" + "github.com/hazelcast/hazelcast-commandline-client/clc/config/wizard" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" clcerrors "github.com/hazelcast/hazelcast-commandline-client/errors" @@ -102,7 +104,8 @@ func (p *WizardProvider) runWizard(ctx context.Context, ec plug.ExecContext) (st return "", clcerrors.ErrNoClusterConfig } args := m.GetInputs() - _, err = ImportSource(ctx, ec, args[0], args[1]) + stages := MakeImportStages(ec, args[0]) + _, err = stage.Execute(ctx, ec, args[1], stage.NewFixedProvider(stages...)) if err != nil { return "", err } diff --git a/clc/ux/stage/common_stages.go b/clc/ux/stage/common_stages.go new file mode 100644 index 000000000..e994c1c4d --- /dev/null +++ b/clc/ux/stage/common_stages.go @@ -0,0 +1,24 @@ +package stage + +import ( + "context" + + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" +) + +func MakeConnectStage[T any](ec plug.ExecContext) Stage[T] { + s := Stage[T]{ + ProgressMsg: "Connecting to the cluster", + SuccessMsg: "Connected to the cluster", + FailureMsg: "Failed connecting to the cluster", + Func: func(ctx context.Context, status Statuser[T]) (T, error) { + var v T + _, err := ec.ClientInternal(ctx) + if err != nil { + return v, err + } + return v, nil + }, + } + return s +} diff --git a/clc/ux/stage/stage.go b/clc/ux/stage/stage.go index 9129b4b56..8c1fe91f9 100644 --- a/clc/ux/stage/stage.go +++ b/clc/ux/stage/stage.go @@ -11,23 +11,25 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/str" ) -type Statuser interface { +type Statuser[T any] interface { SetProgress(progress float32) SetRemainingDuration(dur time.Duration) + Value() T } -type basicStatuser struct { +type basicStatuser[T any] struct { text string textFmtWithRemaining string indexText string sp clc.Spinner + value T } -func (s *basicStatuser) SetProgress(progress float32) { +func (s basicStatuser[T]) SetProgress(progress float32) { s.sp.SetProgress(progress) } -func (s *basicStatuser) SetRemainingDuration(dur time.Duration) { +func (s basicStatuser[T]) SetRemainingDuration(dur time.Duration) { text := s.text if dur > 0 { text = fmt.Sprintf(s.textFmtWithRemaining, dur) @@ -35,31 +37,35 @@ func (s *basicStatuser) SetRemainingDuration(dur time.Duration) { s.sp.SetText(s.indexText + " " + text) } -type Stage struct { +func (s basicStatuser[T]) Value() T { + return s.value +} + +type Stage[T any] struct { ProgressMsg string SuccessMsg string FailureMsg string - Func func(status Statuser) error + Func func(ctx context.Context, status Statuser[T]) (T, error) } -type Provider internal.Iterator[Stage] +type Provider[T any] internal.Iterator[Stage[T]] type Counter interface { StageCount() int } -type FixedProvider struct { - stages []Stage +type FixedProvider[T any] struct { + stages []Stage[T] offset int - current Stage + current Stage[T] err error } -func NewFixedProvider(stages ...Stage) *FixedProvider { - return &FixedProvider{stages: stages} +func NewFixedProvider[T any](stages ...Stage[T]) *FixedProvider[T] { + return &FixedProvider[T]{stages: stages} } -func (sp *FixedProvider) Next() bool { +func (sp *FixedProvider[T]) Next() bool { if sp.offset >= len(sp.stages) { return false } @@ -68,20 +74,19 @@ func (sp *FixedProvider) Next() bool { return true } -func (sp *FixedProvider) Value() Stage { +func (sp *FixedProvider[T]) Value() Stage[T] { return sp.current } -func (sp *FixedProvider) Err() error { +func (sp *FixedProvider[T]) Err() error { return sp.err } -func (sp *FixedProvider) StageCount() int { +func (sp *FixedProvider[T]) StageCount() int { return len(sp.stages) } -func Execute(ctx context.Context, ec plug.ExecContext, sp Provider) error { - ss := &basicStatuser{} +func Execute[T any](ctx context.Context, ec plug.ExecContext, value T, sp Provider[T]) (T, error) { var index int var stageCount int if sc, ok := sp.(Counter); ok { @@ -89,29 +94,39 @@ func Execute(ctx context.Context, ec plug.ExecContext, sp Provider) error { } for sp.Next() { if sp.Err() != nil { - return sp.Err() + return value, sp.Err() } stg := sp.Value() index++ + ss := basicStatuser[T]{value: value} ss.text = stg.ProgressMsg ss.textFmtWithRemaining = stg.ProgressMsg + " (%s left)" - if stageCount > 0 { + ss.indexText = "" + if stageCount > 1 { 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) { + v, 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) + v, err := stg.Func(ctx, ss) + if err != nil { + return nil, err + } + return any(v), nil }) if err != nil { ec.PrintlnUnnecessary(fmt.Sprintf("FAIL %s: %s", stg.FailureMsg, err.Error())) - return err + return value, err } stop() ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg)) + if v == nil { + var vv T + value = vv + } else { + value = v.(T) + } } - return nil + return value, nil } diff --git a/clc/ux/stage/stage_test.go b/clc/ux/stage/stage_test.go index 837608f9a..7c271ceaa 100644 --- a/clc/ux/stage/stage_test.go +++ b/clc/ux/stage/stage_test.go @@ -12,44 +12,57 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/it" ) -func TestExecute(t *testing.T) { - stages := []stage.Stage{ +func TestStage(t *testing.T) { + testCases := []struct { + name string + f func(t *testing.T) + }{ + {name: "execute", f: executeTest}, + {name: "execute_WithFailureTest", f: execute_WithFailureTest}, + } + for _, tc := range testCases { + t.Run(tc.name, tc.f) + } +} + +func executeTest(t *testing.T) { + stages := []stage.Stage[any]{ { ProgressMsg: "Progressing 1", SuccessMsg: "Success 1", FailureMsg: "Failure 1", - Func: func(status stage.Statuser) error { + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { time.Sleep(1 * time.Millisecond) - return nil + return nil, nil }, }, { ProgressMsg: "Progressing 2", SuccessMsg: "Success 2", FailureMsg: "Failure 2", - Func: func(status stage.Statuser) error { + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { for i := 0; i < 5; i++ { status.SetProgress(float32(i+1) / float32(5)) } time.Sleep(1 * time.Millisecond) - return nil + return nil, nil }, }, { ProgressMsg: "Progressing 3", SuccessMsg: "Success 3", FailureMsg: "Failure 3", - Func: func(status stage.Statuser) error { + Func: func(ctx context.Context, status stage.Statuser[any]) (any, 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 + return nil, nil }, }, } ec := it.NewExecuteContext(nil) - err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...)) + _, err := stage.Execute[any](context.TODO(), ec, nil, stage.NewFixedProvider(stages...)) assert.NoError(t, err) texts := []string{ "[1/3] Progressing 1", @@ -68,27 +81,27 @@ func TestExecute(t *testing.T) { assert.Equal(t, text, ec.StdoutText()) } -func TestExecute_WithFailure(t *testing.T) { - stages := []stage.Stage{ +func execute_WithFailureTest(t *testing.T) { + stages := []stage.Stage[any]{ { ProgressMsg: "Progressing 1", SuccessMsg: "Success 1", FailureMsg: "Failure 1", - Func: func(status stage.Statuser) error { - return fmt.Errorf("some error") + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + return nil, fmt.Errorf("some error") }, }, { ProgressMsg: "Progressing 2", SuccessMsg: "Success 2", FailureMsg: "Failure 2", - Func: func(status stage.Statuser) error { - return nil + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + return nil, nil }, }, } ec := it.NewExecuteContext(nil) - err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...)) + _, err := stage.Execute[any](context.TODO(), ec, nil, stage.NewFixedProvider(stages...)) assert.Error(t, err) texts := []string{"[1/2] Progressing 1"} assert.Equal(t, texts, ec.Spinner.Texts) diff --git a/cmd/clc/main.go b/cmd/clc/main.go index 3e7c3c0dd..d9d06348b 100644 --- a/cmd/clc/main.go +++ b/cmd/clc/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "os" "path/filepath" @@ -19,11 +18,12 @@ const ( ExitCodeSuccess = 0 ExitCodeGenericFailure = 1 ExitCodeTimeout = 2 + ExitCodeUserCanceled = 3 ) func bye(err error) { _, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) - os.Exit(1) + os.Exit(ExitCodeGenericFailure) } func main() { @@ -47,17 +47,20 @@ func main() { if err != nil { // print the error only if it wasn't printed before if _, ok := err.(hzerrors.WrappedError); !ok { - fmt.Println(cmd.MakeErrStr(err)) + if !hzerrors.IsUserCancelled(err) { + fmt.Println(cmd.MakeErrStr(err)) + } } } // ignoring the error here _ = m.Exit() if err != nil { - // keeping the hzerrors.ErrTimeout for now - // it may be useful to send that error in the future. --YT - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, hzerrors.ErrTimeout) { + if hzerrors.IsTimeout(err) { os.Exit(ExitCodeTimeout) } + if hzerrors.IsUserCancelled(err) { + os.Exit(ExitCodeUserCanceled) + } os.Exit(ExitCodeGenericFailure) } os.Exit(ExitCodeSuccess) diff --git a/docs/antora.yml b/docs/antora.yml index 28d2f66e4..426a1d43b 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -16,7 +16,7 @@ asciidoc: snapshot: true page-toclevels: 3@ # Required Go version for build - go-version: 1.19 + go-version: 1.21 page-latest-supported-mc: '5.4-snapshot' nav: - modules/ROOT/nav.adoc \ No newline at end of file diff --git a/errors/error.go b/errors/error.go index 139a73020..0c38dd6df 100644 --- a/errors/error.go +++ b/errors/error.go @@ -17,12 +17,14 @@ package errors import ( + "context" "errors" + + "github.com/gohxs/readline" ) var ( ErrUserCancelled = errors.New("cancelled") - ErrTimeout = errors.New("timeout") ErrNotDecoded = errors.New("not decoded") ErrNotAvailable = errors.New("not available") ErrNoClusterConfig = errors.New("no configuration was specified") @@ -44,3 +46,11 @@ type HTTPError interface { Text() string Code() int } + +func IsUserCancelled(err error) bool { + return errors.Is(err, context.Canceled) || errors.Is(err, ErrUserCancelled) || errors.Is(err, readline.ErrInterrupt) +} + +func IsTimeout(err error) bool { + return errors.Is(err, context.DeadlineExceeded) +} diff --git a/go.mod b/go.mod index f26031a6d..87491747d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/hazelcast/hazelcast-commandline-client -go 1.19 +go 1.21 require ( github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 diff --git a/go.sum b/go.sum index fa82f07d2..9fdb714aa 100644 --- a/go.sum +++ b/go.sum @@ -20,9 +20,11 @@ github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBo github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apache/thrift v0.14.1 h1:Yh8v0hpCj63p5edXOLaqTJW0IJ1p+eMW6+YSOqw1d6s= github.com/apache/thrift v0.14.1/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= @@ -30,6 +32,7 @@ github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -68,16 +71,19 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= @@ -101,6 +107,7 @@ github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6 github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.20230818105346-0389b1c5849c h1:V3Hhid/bU2mrR51+FG/FfmwyAEmq8gEFNRxfY/UerEw= @@ -118,6 +125,7 @@ github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -170,6 +178,7 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= @@ -210,6 +219,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= diff --git a/internal/http/http.go b/internal/http/http.go new file mode 100644 index 000000000..a8a6a1556 --- /dev/null +++ b/internal/http/http.go @@ -0,0 +1,30 @@ +package http + +import ( + "bytes" + "context" + "net/http" +) + +type Client struct { + client *http.Client +} + +func NewClient() *Client { + return &Client{ + client: &http.Client{}, + } +} + +func (c *Client) Get(ctx context.Context, url string) (*http.Response, error) { + buf := &bytes.Buffer{} + req, err := http.NewRequestWithContext(ctx, "GET", url, buf) + if err != nil { + return nil, err + } + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/internal/it/test_context.go b/internal/it/test_context.go index cdb2121b6..2140ac657 100644 --- a/internal/it/test_context.go +++ b/internal/it/test_context.go @@ -260,9 +260,9 @@ func (tcx TestContext) AssertStdoutDollar(text string) { func (tcx TestContext) AssertJSONStdoutHasRowWithFields(fields ...string) map[string]any { stdout := tcx.ExpectStdout.String() + tcx.T.Log("STDOUT:", stdout) var m map[string]any check.Must(json.Unmarshal([]byte(stdout), &m)) - tcx.T.Log("STDOUT:", stdout) if len(fields) != len(m) { tcx.T.Fatalf("stdout does not have the same number fields as %v", fields) } diff --git a/internal/viridian/cluster.go b/internal/viridian/cluster.go index d82cf3958..b89f48fce 100644 --- a/internal/viridian/cluster.go +++ b/internal/viridian/cluster.go @@ -66,22 +66,22 @@ func clusterName() string { return fmt.Sprintf("%s-%s-%.4d", base, date, num) } -func (a *API) StopCluster(ctx context.Context, idOrName string) error { +func (a *API) StopCluster(ctx context.Context, idOrName string) (Cluster, error) { c, err := a.FindCluster(ctx, idOrName) if err != nil { - return err + return c, err } ok, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (bool, error) { u := a.makeURL("/cluster/%s/stop", c.ID) return doPost[[]byte, bool](ctx, u, a.Token, nil) }) if err != nil { - return fmt.Errorf("stopping cluster: %w", err) + return c, fmt.Errorf("stopping cluster: %w", err) } if !ok { - return errors.New("could not stop the cluster") + return c, errors.New("could not stop the cluster") } - return nil + return c, nil } func (a *API) ListClusters(ctx context.Context) ([]Cluster, error) { @@ -95,28 +95,28 @@ func (a *API) ListClusters(ctx context.Context) ([]Cluster, error) { return csw.Content, nil } -func (a *API) ResumeCluster(ctx context.Context, idOrName string) error { +func (a *API) ResumeCluster(ctx context.Context, idOrName string) (Cluster, error) { c, err := a.FindCluster(ctx, idOrName) if err != nil { - return err + return c, err } ok, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (bool, error) { u := a.makeURL("/cluster/%s/resume", c.ID) return doPost[[]byte, bool](ctx, u, a.Token, nil) }) if err != nil { - return fmt.Errorf("resuming cluster: %w", err) + return c, fmt.Errorf("resuming cluster: %w", err) } if !ok { - return errors.New("could not resume the cluster") + return c, errors.New("could not resume the cluster") } - return nil + return c, nil } -func (a *API) DeleteCluster(ctx context.Context, idOrName string) error { +func (a *API) DeleteCluster(ctx context.Context, idOrName string) (Cluster, error) { c, err := a.FindCluster(ctx, idOrName) if err != nil { - return err + return c, err } _, err = RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (any, error) { u := a.makeURL("/cluster/%s", c.ID) @@ -127,9 +127,9 @@ func (a *API) DeleteCluster(ctx context.Context, idOrName string) error { return nil, nil }) if err != nil { - return fmt.Errorf("deleting cluster: %w", err) + return c, fmt.Errorf("deleting cluster: %w", err) } - return nil + return c, nil } func (a *API) GetCluster(ctx context.Context, idOrName string) (Cluster, error) { diff --git a/internal/viridian/cluster_log.go b/internal/viridian/cluster_log.go index cedeee18c..bc781527f 100644 --- a/internal/viridian/cluster_log.go +++ b/internal/viridian/cluster_log.go @@ -12,10 +12,10 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) -func (a *API) DownloadClusterLogs(ctx context.Context, destDir string, idOrName string) error { +func (a *API) DownloadClusterLogs(ctx context.Context, destDir string, idOrName string) (string, error) { c, err := a.FindCluster(ctx, idOrName) if err != nil { - return err + return "", err } r, err := RetryOnAuthFail(ctx, a, func(ctx context.Context, token string) (types.Tuple2[string, func()], error) { u := a.makeURL("/cluster/%s/logs", c.ID) @@ -26,22 +26,22 @@ func (a *API) DownloadClusterLogs(ctx context.Context, destDir string, idOrName return types.MakeTuple2(path, stop), nil }) if err != nil { - return err + return "", err } defer r.Second() zipFile, err := os.Open(r.First) if err != nil { - return err + return "", err } st, err := zipFile.Stat() if err != nil || st.Size() == 0 { - return fmt.Errorf("logs are not available yet, retry later") + return "", fmt.Errorf("logs are not available yet, retry later") } err = unzip(zipFile, destDir) if err != nil { - return err + return "", err } - return nil + return destDir, nil } func unzip(zipFile *os.File, destDir string) error { From b0ee81ddd99397505b4cc84a1dae6afdebf66e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 8 Sep 2023 14:29:55 +0300 Subject: [PATCH 49/79] Refactor integrate stage 2 (#372) Integrate stage phase 2 --- base/commands/demo/demo_map_set_many.go | 9 +- base/commands/list/common.go | 113 +++++++++++++++++++++++- base/commands/list/const.go | 11 +-- base/commands/list/list.go | 52 ++--------- base/commands/list/list_add.go | 80 ++++++++++------- base/commands/list/list_clear.go | 19 ++-- base/commands/list/list_contains.go | 58 ++++++------ base/commands/list/list_destroy.go | 19 ++-- base/commands/list/list_it_test.go | 1 - base/commands/list/list_remove_index.go | 33 ++----- base/commands/list/list_remove_value.go | 39 ++------ base/commands/list/list_set.go | 55 +++++++----- base/commands/list/list_size.go | 16 ++-- base/const.go | 11 +++ go.mod | 2 +- go.sum | 4 +- 16 files changed, 297 insertions(+), 225 deletions(-) create mode 100644 base/const.go diff --git a/base/commands/demo/demo_map_set_many.go b/base/commands/demo/demo_map_set_many.go index fa76de7af..6fa15c444 100644 --- a/base/commands/demo/demo_map_set_many.go +++ b/base/commands/demo/demo_map_set_many.go @@ -11,6 +11,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -44,11 +45,11 @@ func (m MapSetManyCmd) Exec(ctx context.Context, ec plug.ExecContext) error { count := ec.GetInt64Arg(argEntryCount) mapName := ec.Props().GetString(flagName) size := ec.Props().GetString(flagSize) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Creating entries in map %s with %d entries", mapName, count)) mm, err := ci.Client().GetMap(ctx, mapName) if err != nil { diff --git a/base/commands/list/common.go b/base/commands/list/common.go index 7595a88bd..3cc4d982c 100644 --- a/base/commands/list/common.go +++ b/base/commands/list/common.go @@ -3,23 +3,30 @@ package list import ( + "context" "fmt" "strings" "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) func addValueTypeFlag(cc plug.InitContext) { help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(listFlagValueType, "v", "string", false, help) + cc.AddStringFlag(base.FlagValueType, "v", "string", false, help) } func makeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { - vt := ec.Props().GetString(listFlagValueType) + vt := ec.Props().GetString(base.FlagValueType) if vt == "" { vt = "string" } @@ -43,3 +50,105 @@ func stringToPartitionID(ci *hazelcast.ClientInternal, name string) (int32, erro } return partitionID, nil } + +func getList(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.List, error) { + name := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Getting list %s", name)) + return ci.Client().GetList(ctx, name) +} + +func removeFromList(ctx context.Context, ec plug.ExecContext, name string, index int32, valueStr string) error { + rowV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + indexCall := valueStr == "" + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + pid, err := stringToPartitionID(ci, name) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Removing value from list %s", name)) + var req *hazelcast.ClientMessage + if indexCall { + req = codec.EncodeListRemoveWithIndexRequest(name, index) + } else { + vd, err := makeValueData(ec, ci, valueStr) + if err != nil { + return nil, err + } + req = codec.EncodeListRemoveRequest(name, vd) + } + resp, err := ci.InvokeOnPartition(ctx, req, pid, nil) + if err != nil { + return nil, err + } + var vt int32 + var value any + var colName string + if indexCall { + raw := codec.DecodeListRemoveWithIndexResponse(resp) + vt = raw.Type() + value, err = ci.DecodeData(raw) + colName = "Removed Value" + if err != nil { + ec.Logger().Info("The value was not decoded, due to error: %s", err.Error()) + value = serialization.NondecodedType(serialization.TypeToLabel(vt)) + } + } else { + vt = serialization.TypeBool + value = codec.DecodeListRemoveResponse(resp) + colName = "Removed" + } + row := output.Row{ + output.Column{ + Name: colName, + Type: vt, + Value: value, + }, + } + if ec.Props().GetBool(base.FlagShowType) { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(vt), + }) + } + return row, nil + }) + if err != nil { + return err + } + stop() + msg := fmt.Sprintf("OK List %s was updated.\n", name) + ec.PrintlnUnnecessary(msg) + row := rowV.(output.Row) + return ec.AddOutputRows(ctx, row) +} + +func convertDataToRow(ci *hazelcast.ClientInternal, name string, data hazelcast.Data, showType bool) (output.Row, error) { + vt := data.Type() + value, err := ci.DecodeData(data) + if err != nil { + return nil, err + } + row := output.Row{ + output.Column{ + Name: name, + Type: vt, + Value: value, + }, + } + if showType { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(vt), + }) + } + return row, nil +} diff --git a/base/commands/list/const.go b/base/commands/list/const.go index 8917a5549..b9f916bb8 100644 --- a/base/commands/list/const.go +++ b/base/commands/list/const.go @@ -3,12 +3,7 @@ package list const ( - listFlagKeyType = "key-type" - listFlagValueType = "value-type" - listFlagIndex = "index" - defaultListName = "default" - argValue = "value" - argTitleValue = "value" - argIndex = "index" - argTitleIndex = "index" + flagIndex = "index" + argIndex = "index" + argTitleIndex = "index" ) diff --git a/base/commands/list/list.go b/base/commands/list/list.go index 1b7036185..b7af813e5 100644 --- a/base/commands/list/list.go +++ b/base/commands/list/list.go @@ -4,35 +4,23 @@ package list import ( "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - listFlagName = "name" - listFlagShowType = "show-type" - listPropertyName = "list" -) - type ListCommand struct { } func (mc *ListCommand) Init(cc plug.InitContext) error { cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(listFlagName, "n", defaultListName, false, "list name") - cc.AddBoolFlag(listFlagShowType, "", false, false, "add the type names to the output") - if !cc.Interactive() { - cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") - } + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "list name") + cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") cc.SetTopLevel(true) - cc.SetCommandUsage("list [command] [flags]") + cc.SetCommandUsage("list") help := "List operations" cc.SetCommandHelp(help, help) return nil @@ -42,34 +30,6 @@ func (mc *ListCommand) Exec(context.Context, plug.ExecContext) error { return nil } -func (mc *ListCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { - ctx := context.TODO() - props.SetBlocking(listPropertyName, func() (any, error) { - listName := ec.Props().GetString(listFlagName) - // empty list name is allowed - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - mv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting list %s", listName)) - m, err := ci.Client().GetList(ctx, listName) - if err != nil { - return nil, err - } - return m, nil - }) - if err != nil { - return nil, err - } - stop() - return mv.(*hazelcast.List), nil - }) - return nil -} - func init() { - cmd := &ListCommand{} - Must(plug.Registry.RegisterCommand("list", cmd)) - plug.Registry.RegisterAugmentor("20-list", cmd) + check.Must(plug.Registry.RegisterCommand("list", &ListCommand{})) } diff --git a/base/commands/list/list_add.go b/base/commands/list/list_add.go index 1f7a1f76b..69daa9f7c 100644 --- a/base/commands/list/list_add.go +++ b/base/commands/list/list_add.go @@ -8,59 +8,79 @@ import ( "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) type ListAddCommand struct{} +func (mc *ListAddCommand) Unwrappable() {} + func (mc *ListAddCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("add") help := "Add a value in the given list" cc.SetCommandHelp(help, help) addValueTypeFlag(cc) - cc.AddIntFlag(listFlagIndex, "", -1, false, "index for the value") - cc.AddStringArg(argValue, argTitleValue) + cc.AddIntFlag(flagIndex, "", -1, false, "index for the value") + cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } func (mc *ListAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(listFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - // get the list just to ensure the corresponding proxy is created - if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { - return err - } - valueStr := ec.GetStringArg(argValue) - vd, err := makeValueData(ec, ci, valueStr) - if err != nil { - return err - } - index := ec.Props().GetInt(listFlagIndex) - var req *hazelcast.ClientMessage - if index >= 0 { - req = codec.EncodeListAddWithIndexRequest(name, int32(index), vd) - } else { - req = codec.EncodeListAddRequest(name, vd) - } - pid, err := stringToPartitionID(ci, name) - if err != nil { - return err - } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + name := ec.Props().GetString(base.FlagName) + val, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + // get the list just to ensure the corresponding proxy is created + _, err = getList(ctx, ec, sp) + if err != nil { + return nil, err + } + valueStr := ec.GetStringArg(base.ArgValue) + vd, err := makeValueData(ec, ci, valueStr) + if err != nil { + return nil, err + } + index := ec.Props().GetInt(flagIndex) + var req *hazelcast.ClientMessage + if index >= 0 { + req = codec.EncodeListAddWithIndexRequest(name, int32(index), vd) + } else { + req = codec.EncodeListAddRequest(name, vd) + } + pid, err := stringToPartitionID(ci, name) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Adding value at index %d into list %s", index, name)) - return ci.InvokeOnPartition(ctx, req, pid, nil) + resp, err := ci.InvokeOnPartition(ctx, req, pid, nil) + if err != nil { + return nil, err + } + return codec.DecodeListAddResponse(resp), err }) if err != nil { return err } stop() - return nil + msg := fmt.Sprintf("OK Updated list %s.\n", name) + ec.PrintlnUnnecessary(msg) + row := output.Row{ + output.Column{ + Name: "Value", + Type: serialization.TypeBool, + Value: val, + }, + } + return ec.AddOutputRows(ctx, row) } func init() { diff --git a/base/commands/list/list_clear.go b/base/commands/list/list_clear.go index aadb7c691..09175b47b 100644 --- a/base/commands/list/list_clear.go +++ b/base/commands/list/list_clear.go @@ -6,8 +6,6 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,6 +16,8 @@ import ( type ListClearCommand struct{} +func (mc *ListClearCommand) Unwrappable() {} + func (mc *ListClearCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("clear") help := "Delete all entries of a List" @@ -27,10 +27,6 @@ func (mc *ListClearCommand) Init(cc plug.InitContext) error { } func (mc *ListClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - lv, err := ec.Props().GetBlocking(listPropertyName) - if err != nil { - return err - } autoYes := ec.Props().GetBool(clc.FlagAutoYes) if !autoYes { p := prompt.New(ec.Stdin(), ec.Stdout()) @@ -43,18 +39,23 @@ func (mc *ListClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error return errors.ErrUserCancelled } } - l := lv.(*hazelcast.List) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + l, err := getList(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Clearing list %s", l.Name())) if err := l.Clear(ctx); err != nil { return nil, err } - return nil, nil + return l.Name(), nil }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Cleared list %s", name) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/list/list_contains.go b/base/commands/list/list_contains.go index ccf1364dd..cecfefff3 100644 --- a/base/commands/list/list_contains.go +++ b/base/commands/list/list_contains.go @@ -6,9 +6,9 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,50 +18,56 @@ import ( type ListContainsCommand struct{} +func (mc *ListContainsCommand) Unwrappable() {} + func (mc *ListContainsCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("contains") help := "Check if the value is present in the list" cc.SetCommandHelp(help, help) addValueTypeFlag(cc) - cc.AddStringArg(argValue, argTitleValue) + cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } func (mc *ListContainsCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(listFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - // get the list just to ensure the corresponding proxy is created - if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { - return err - } - valueStr := ec.GetStringArg(argValue) - vd, err := makeValueData(ec, ci, valueStr) - if err != nil { - return err - } - pid, err := stringToPartitionID(ci, name) - if err != nil { - return err - } - cmi, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + name := ec.Props().GetString(base.FlagName) + ok, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + // get the list just to ensure the corresponding proxy is created + _, err = getList(ctx, ec, sp) + if err != nil { + return nil, err + } + valueStr := ec.GetStringArg(base.ArgValue) + vd, err := makeValueData(ec, ci, valueStr) + if err != nil { + return nil, err + } + pid, err := stringToPartitionID(ci, name) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Checking if value exists in the list %s", name)) req := codec.EncodeListContainsRequest(name, vd) - return ci.InvokeOnPartition(ctx, req, pid, nil) + resp, err := ci.InvokeOnPartition(ctx, req, pid, nil) + if err != nil { + return nil, err + } + contains := codec.DecodeListContainsResponse(resp) + return contains, nil }) if err != nil { return err } stop() - cm := cmi.(*hazelcast.ClientMessage) - contains := codec.DecodeListContainsResponse(cm) return ec.AddOutputRows(ctx, output.Row{ { Name: "Contains", Type: serialization.TypeBool, - Value: contains, + Value: ok, }, }) } diff --git a/base/commands/list/list_destroy.go b/base/commands/list/list_destroy.go index 70b3dfb33..9dc612a7c 100644 --- a/base/commands/list/list_destroy.go +++ b/base/commands/list/list_destroy.go @@ -6,8 +6,6 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -17,6 +15,8 @@ import ( type ListDestroyCommand struct{} +func (mc *ListDestroyCommand) Unwrappable() {} + func (mc *ListDestroyCommand) Init(cc plug.InitContext) error { long := `Destroy a List @@ -29,10 +29,6 @@ This command will delete the List and the data in it will not be available anymo } func (mc *ListDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - lv, err := ec.Props().GetBlocking(listPropertyName) - if err != nil { - return err - } autoYes := ec.Props().GetBool(clc.FlagAutoYes) if !autoYes { p := prompt.New(ec.Stdin(), ec.Stdout()) @@ -45,18 +41,23 @@ func (mc *ListDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) err return errors.ErrUserCancelled } } - l := lv.(*hazelcast.List) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + l, err := getList(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Destroying list %s", l.Name())) if err := l.Destroy(ctx); err != nil { return nil, err } - return nil, nil + return l.Name(), nil }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Destroyed list %s.", name) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/list/list_it_test.go b/base/commands/list/list_it_test.go index 5236c94f2..5ceb4107e 100644 --- a/base/commands/list/list_it_test.go +++ b/base/commands/list/list_it_test.go @@ -142,7 +142,6 @@ func size_InteractiveTest(t *testing.T) { tcx.WithReset(func() { _ = check.MustValue(l.Add(ctx, "foo")) tcx.WriteStdin([]byte(fmt.Sprintf("\\list -n %s size\n", l.Name()))) - tcx.AssertStderrContains("OK") tcx.AssertStdoutDollarWithPath("testdata/list_size_1.txt") }) }) diff --git a/base/commands/list/list_remove_index.go b/base/commands/list/list_remove_index.go index 44d13e0d0..4ccb827e5 100644 --- a/base/commands/list/list_remove_index.go +++ b/base/commands/list/list_remove_index.go @@ -5,16 +5,17 @@ package list import ( "context" "errors" - "fmt" + "math" - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/base" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) type ListRemoveIndexCommand struct{} +func (mc *ListRemoveIndexCommand) Unwrappable() {} + func (mc *ListRemoveIndexCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("remove-index") help := "Remove the value at the given index in the list" @@ -24,33 +25,15 @@ func (mc *ListRemoveIndexCommand) Init(cc plug.InitContext) error { } func (mc *ListRemoveIndexCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(listFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - // get the list just to ensure the corresponding proxy is created - if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { - return err - } + name := ec.Props().GetString(base.FlagName) index := ec.GetInt64Arg(argIndex) if index < 0 { return errors.New("index must be non-negative") } - pid, err := stringToPartitionID(ci, name) - if err != nil { - return err + if index > math.MaxInt32 { + return errors.New("index must fit into a 32bit unsigned integer") } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Removing value from the list %s", name)) - req := codec.EncodeListRemoveWithIndexRequest(name, int32(index)) - return ci.InvokeOnPartition(ctx, req, pid, nil) - }) - if err != nil { - return err - } - stop() - return nil + return removeFromList(ctx, ec, name, int32(index), "") } func init() { diff --git a/base/commands/list/list_remove_value.go b/base/commands/list/list_remove_value.go index 297978884..00bc5ef23 100644 --- a/base/commands/list/list_remove_value.go +++ b/base/commands/list/list_remove_value.go @@ -4,54 +4,29 @@ package list import ( "context" - "fmt" - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/base" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) type ListRemoveValueCommand struct{} +func (l ListRemoveValueCommand) Unwrappable() {} + func (mc *ListRemoveValueCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("remove-value") help := "Remove a value from the given list" cc.SetCommandHelp(help, help) addValueTypeFlag(cc) - cc.AddStringArg(argValue, argTitleValue) + cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } func (mc *ListRemoveValueCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(listFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - // get the list just to ensure the corresponding proxy is created - if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { - return err - } - valueStr := ec.GetStringArg(argValue) - vd, err := makeValueData(ec, ci, valueStr) - if err != nil { - return err - } - pid, err := stringToPartitionID(ci, name) - if err != nil { - return err - } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Removing value from the list %s", name)) - req := codec.EncodeListRemoveRequest(name, vd) - return ci.InvokeOnPartition(ctx, req, pid, nil) - }) - if err != nil { - return err - } - stop() - return nil + name := ec.Props().GetString(base.FlagName) + value := ec.GetStringArg(base.ArgValue) + return removeFromList(ctx, ec, name, 0, value) } func init() { diff --git a/base/commands/list/list_set.go b/base/commands/list/list_set.go index fb7f7d073..09ad4ef8b 100644 --- a/base/commands/list/list_set.go +++ b/base/commands/list/list_set.go @@ -6,54 +6,65 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) type ListSetCommand struct{} +func (mc *ListSetCommand) Unwrappable() {} + func (mc *ListSetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("set") help := "Set a value at the given index in the list" cc.SetCommandHelp(help, help) addValueTypeFlag(cc) cc.AddInt64Arg(argIndex, argTitleIndex) - cc.AddStringArg(argValue, argTitleValue) + cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } func (mc *ListSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(listFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - // get the list just to ensure the corresponding proxy is created - if _, err := ec.Props().GetBlocking(listPropertyName); err != nil { - return err - } + name := ec.Props().GetString(base.FlagName) index := ec.GetInt64Arg(argIndex) - valueStr := ec.GetStringArg(argValue) - vd, err := makeValueData(ec, ci, valueStr) - if err != nil { - return err - } - pid, err := stringToPartitionID(ci, name) - if err != nil { - return err - } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + valueStr := ec.GetStringArg(base.ArgValue) + rowV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + // get the list just to ensure the corresponding proxy is created + _, err = getList(ctx, ec, sp) + if err != nil { + return nil, err + } + vd, err := makeValueData(ec, ci, valueStr) + if err != nil { + return nil, err + } + pid, err := stringToPartitionID(ci, name) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Setting the value of the list %s", name)) req := codec.EncodeListSetRequest(name, int32(index), vd) - return ci.InvokeOnPartition(ctx, req, pid, nil) + resp, err := ci.InvokeOnPartition(ctx, req, pid, nil) + if err != nil { + return nil, err + } + data := codec.DecodeListSetResponse(resp) + return convertDataToRow(ci, "Last Value", data, ec.Props().GetBool(base.FlagShowType)) }) if err != nil { return err } stop() - return nil + return ec.AddOutputRows(ctx, rowV.(output.Row)) } func init() { diff --git a/base/commands/list/list_size.go b/base/commands/list/list_size.go index 3923daa3e..c785bedb7 100644 --- a/base/commands/list/list_size.go +++ b/base/commands/list/list_size.go @@ -6,8 +6,7 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -17,6 +16,8 @@ import ( type ListSizeCommand struct{} +func (mc *ListSizeCommand) Unwrappable() {} + func (mc *ListSizeCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("size") help := "Return the size of the given List" @@ -25,13 +26,12 @@ func (mc *ListSizeCommand) Init(cc plug.InitContext) error { } func (mc *ListSizeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(listFlagName) - lv, err := ec.Props().GetBlocking(listPropertyName) - if err != nil { - return err - } - l := lv.(*hazelcast.List) + name := ec.Props().GetString(base.FlagName) sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + l, err := getList(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Getting the size of list %s", name)) return l.Size(ctx) }) diff --git a/base/const.go b/base/const.go new file mode 100644 index 000000000..da513941f --- /dev/null +++ b/base/const.go @@ -0,0 +1,11 @@ +package base + +const ( + FlagName = "name" + FlagShowType = "show-type" + FlagKeyType = "key-type" + FlagValueType = "value-type" + DefaultName = "default" + ArgValue = "value" + ArgTitleValue = "value" +) diff --git a/go.mod b/go.mod index 87491747d..e1e2ca309 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.20230818105346-0389b1c5849c + github.com/hazelcast/hazelcast-go-client v1.4.2-0.20230908105658-19ade8678cb0 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 diff --git a/go.sum b/go.sum index 9fdb714aa..9aeca604b 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.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/hazelcast/hazelcast-go-client v1.4.2-0.20230908105658-19ade8678cb0 h1:NIl9B/ckHJ07RpwhvefQKPPe8ASC3cT5KyPcLvad4gg= +github.com/hazelcast/hazelcast-go-client v1.4.2-0.20230908105658-19ade8678cb0/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= From cd6e0ff446b1281ef0b9f74e266cb0eec881b4cd Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:51:27 +0300 Subject: [PATCH 50/79] Fix error in update_rc function (#373) --- extras/unix/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extras/unix/install.sh b/extras/unix/install.sh index a42842d67..f7a28d2a9 100644 --- a/extras/unix/install.sh +++ b/extras/unix/install.sh @@ -180,7 +180,7 @@ fi return fi local text - text=$(cat "$path" | grep "$installed") + text="$(cat "$path" | grep "$installed")" if [[ "$text" != "" ]]; then # CLC PATH is already exported in this file log_debug "CLC PATH is already installed in $path" From ab852bababd9e8f870d52c2a511533a59911400b Mon Sep 17 00:00:00 2001 From: rebekah-lawrence <142301480+rebekah-lawrence@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:45:51 +0100 Subject: [PATCH 51/79] DOCS-632 Requested link update (#364) On investigation, it was decided to introduce conditional text, and only document build from source for pre-release SNAPSHOT CLC --- docs/modules/ROOT/pages/install-clc.adoc | 80 +++++++++++++++++++++--- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/docs/modules/ROOT/pages/install-clc.adoc b/docs/modules/ROOT/pages/install-clc.adoc index fffa57b63..d0bfe1eae 100644 --- a/docs/modules/ROOT/pages/install-clc.adoc +++ b/docs/modules/ROOT/pages/install-clc.adoc @@ -9,6 +9,69 @@ * Windows installer * Building from source +ifdef::snapshot[] +[NOTE] +Pre-release versions can only be built from source. + +== Build Pre-release from Source + +Supported OS: + +- Ubuntu 22.04 or later +- MacOS 12 or later +- Windows 10 or later + +Requirements: + +- Go 1.19 or later +- Git +- For Linux and MacOS, GNU Make +- Command prompt or Windows Powershell +- For Windows, https://github.com/tc-hib/go-winres[go-winres] + +To build from source, complete the following steps: + +. Clone the source from Git ++ +[source,shell] +---- +$ git clone https://github.com/hazelcast/hazelcast-commandline-client.git +---- + +. Navigate to your project ++ +[source,shell] +---- +$ cd hazelcast-commandline-client +---- + +. Build the project ++ +[source,shell] +---- +$ make +---- ++ +The ``clc``, or ``clc.exe``, binary is created in the ``build`` directory. + +. Run the project ++ +-- On Linux or MacOS: ++ +[source,shell] +---- +./build/clc +---- ++ +-- On Windows: ++ +[source,shell] +---- +.\build\clc.exe +---- + +endif::[] +ifndef::snapshot[] == Installing on macOS The Hazelcast CLC is supported on macOS 13 or newer versions. @@ -17,7 +80,7 @@ The Hazelcast CLC is supported on macOS 13 or newer versions. ==== ZIP (Intel):: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases/latest[latest release page], and locate the AMD compressed file (`hazelcast-clc_v{full-version}_darwin_amd64.zip`). +. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the AMD compressed file (`hazelcast-clc_v{full-version}_darwin_amd64.zip`). . Download and unzip the file. . Remove the `clc` binary from quarantine if you get a security warning when running it: + @@ -34,7 +97,7 @@ sudo mv hazelcast-clc_v{full-version}_darwin_amd64/clc /usr/local/bin ZIP (Apple Silicon):: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases/latest[latest release page], and locate the ARM compressed file (`hazelcast-clc_v{full-version}_darwin_arm64.zip`). +. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the ARM compressed file (`hazelcast-clc_v{full-version}_darwin_arm64.zip`). . Download and unzip the file. . Remove the `clc` binary from quarantine if you get a security warning when running it: + @@ -61,7 +124,7 @@ xcode-select --install + ** https://go.dev/doc/install[Go {go-version}] or newer (check with the `go version` command) + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases/latest[latest release page], and locate the compressed source file (`v{full-version}.zip`). +. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the compressed source file (`v{full-version}.zip`). . Download and unzip the file. . Change into the `hazelcast-commandline-client-{full-version}` directory. + @@ -101,7 +164,7 @@ The Hazelcast CLC runs on any recent Linux distribution. We test it on Ubuntu 22 ==== Tarball (AMD64):: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases/latest[latest release page], and locate the tarball for Linux (`hazelcast-clc_v{full-version}_linux_amd64.tar.gz`). +. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the tarball for Linux (`hazelcast-clc_v{full-version}_linux_amd64.tar.gz`). . Download and unzip the file. . Optionally make `clc` available without using its full path. You can do that by moving `clc` to one of the directories in the `$PATH` environment variable. `/usr/local/bin` is a safe choice. + @@ -116,7 +179,7 @@ Build from Source:: ** GNU Make (check with the `make --version` command). It is installed by default on most Linux distributions. ** https://go.dev/doc/install[Go {go-version}] or newer (check with the `go version` command) + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases/latest[latest release page], and locate the source tarball (`v{full-version}.tar.gz`). +. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the source tarball (`v{full-version}.tar.gz`). . Download and uncompress the file. + [source,shell,subs="attributes"] @@ -161,7 +224,7 @@ The Hazelcast CLC is supported on Windows 10 or newer versions. We provide pre-b ==== Installer:: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases/latest[latest release page], and locate the Windows installer file (`hazelcast-clc-setup-v{full-version}.exe`). +. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the Windows installer file (`hazelcast-clc-setup-v{full-version}.exe`). . Download and the run the installer on your system to start the installation wizard. . Follow the steps on the wizard; when you see the "Completing the Hazelcast CLC Setup Wizard" dialog, press kbd:[Finish] to complete the installation. . `clc.exe` is automatically added to the `PATH` environment variable, so it can be started in the terminal without its full path. @@ -174,12 +237,13 @@ clc.exe ZIP:: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases/latest[latest release page], and locate the Windows ZIP file (`hazelcast-clc_v{full-version}_windows_amd64.zip`). +. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the Windows ZIP file (`hazelcast-clc_v{full-version}_windows_amd64.zip`). . Download and unzip the file. . Optionally make `clc.exe` available without using its full path. You can do that by adding the full path of the extracted directory to the `PATH` environment variable. ==== +endif::[] == Verifying the Hazelcast CLC Installation To check whether the Hazelcast CLC is installed properly, run the following command on a terminal. @@ -189,7 +253,7 @@ To check whether the Hazelcast CLC is installed properly, run the following comm clc version ---- -You should see the Hazelcast CLC version information. +If installed, the Hazelcast CLC version information displays. == Uninstalling the Hazelcast CLC From 7883e395486f266439c442cdc8de64c96c77efde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 8 Sep 2023 17:52:44 +0300 Subject: [PATCH 52/79] Update the map command output (#374) Updated the map command --- base/commands/map/common.go | 14 +++++ base/commands/map/map.go | 47 ++--------------- base/commands/map/map_clear.go | 19 +++---- base/commands/map/map_destroy.go | 19 +++---- base/commands/map/map_entry_set.go | 41 +++++++++------ base/commands/map/map_get.go | 79 +++++++++++++++------------- base/commands/map/map_it_test.go | 13 ++--- base/commands/map/map_key_set.go | 70 +++++++++++++------------ base/commands/map/map_load_all.go | 49 +++++++++++------- base/commands/map/map_lock.go | 37 +++++++------ base/commands/map/map_remove.go | 83 +++++++++++++++++------------- base/commands/map/map_set.go | 58 ++++++++++++--------- base/commands/map/map_size.go | 16 +++--- base/commands/map/map_try_lock.go | 35 +++++++------ base/commands/map/map_unlock.go | 38 +++++++------- base/commands/map/map_values.go | 67 +++++++++++++----------- clc/ux/stage/stage.go | 3 +- 17 files changed, 363 insertions(+), 325 deletions(-) diff --git a/base/commands/map/common.go b/base/commands/map/common.go index 4cc97ebac..4edf5c9a9 100644 --- a/base/commands/map/common.go +++ b/base/commands/map/common.go @@ -3,11 +3,15 @@ package _map import ( + "context" "fmt" "strings" "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/mk" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -58,3 +62,13 @@ func makeKeyValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr, } return kd, vd, nil } + +func getMap(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.Map, error) { + name := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Getting map %s", name)) + return ci.Client().GetMap(ctx, name) +} diff --git a/base/commands/map/map.go b/base/commands/map/map.go index 40de412fb..b43256b5e 100644 --- a/base/commands/map/map.go +++ b/base/commands/map/map.go @@ -4,24 +4,15 @@ package _map import ( "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - mapFlagName = "name" - mapFlagShowType = "show-type" - mapPropertyName = "map" -) - -type MapCommand struct { -} +type MapCommand struct{} func (mc *MapCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("map") @@ -30,8 +21,8 @@ func (mc *MapCommand) Init(cc plug.InitContext) error { cc.SetCommandGroup(clc.GroupDDSID) help := "Map operations" cc.SetCommandHelp(help, help) - cc.AddStringFlag(mapFlagName, "n", defaultMapName, false, "map name") - cc.AddBoolFlag(mapFlagShowType, "", false, false, "add the type names to the output") + cc.AddStringFlag(base.FlagName, "n", defaultMapName, false, "map name") + cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") if !cc.Interactive() { cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") } @@ -42,34 +33,6 @@ func (mc *MapCommand) Exec(context.Context, plug.ExecContext) error { return nil } -func (mc *MapCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { - ctx := context.TODO() - props.SetBlocking(mapPropertyName, func() (any, error) { - mapName := ec.Props().GetString(mapFlagName) - // empty map name is allowed - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - mv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting map %s", mapName)) - m, err := ci.Client().GetMap(ctx, mapName) - if err != nil { - return nil, err - } - return m, nil - }) - if err != nil { - return nil, err - } - stop() - return mv.(*hazelcast.Map), nil - }) - return nil -} - func init() { - cmd := &MapCommand{} - Must(plug.Registry.RegisterCommand("map", cmd)) - plug.Registry.RegisterAugmentor("20-map", cmd) + Must(plug.Registry.RegisterCommand("map", &MapCommand{})) } diff --git a/base/commands/map/map_clear.go b/base/commands/map/map_clear.go index a48daf496..baf330431 100644 --- a/base/commands/map/map_clear.go +++ b/base/commands/map/map_clear.go @@ -6,8 +6,6 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" @@ -19,6 +17,8 @@ import ( type MapClearCommand struct{} +func (mc *MapClearCommand) Unwrappable() {} + func (mc *MapClearCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("clear") help := "Delete all entries of a Map" @@ -28,10 +28,6 @@ func (mc *MapClearCommand) Init(cc plug.InitContext) error { } func (mc *MapClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mv, err := ec.Props().GetBlocking(mapPropertyName) - if err != nil { - return err - } autoYes := ec.Props().GetBool(clc.FlagAutoYes) if !autoYes { p := prompt.New(ec.Stdin(), ec.Stdout()) @@ -44,18 +40,23 @@ func (mc *MapClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error return errors.ErrUserCancelled } } - m := mv.(*hazelcast.Map) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + m, err := getMap(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Clearing map %s", m.Name())) if err := m.Clear(ctx); err != nil { return nil, err } - return nil, nil + return m.Name(), nil }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Cleared map: %s.", name) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_destroy.go b/base/commands/map/map_destroy.go index e4d7df355..3b2866251 100644 --- a/base/commands/map/map_destroy.go +++ b/base/commands/map/map_destroy.go @@ -6,8 +6,6 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -17,6 +15,8 @@ import ( type MapDestroyCommand struct{} +func (mc *MapDestroyCommand) Unwrappable() {} + func (mc *MapDestroyCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("destroy") long := `Destroy a Map @@ -29,10 +29,6 @@ This command will delete the Map and the data in it will not be available anymor } func (mc *MapDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mv, err := ec.Props().GetBlocking(mapPropertyName) - if err != nil { - return err - } autoYes := ec.Props().GetBool(clc.FlagAutoYes) if !autoYes { p := prompt.New(ec.Stdin(), ec.Stdout()) @@ -45,18 +41,23 @@ func (mc *MapDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) erro return errors.ErrUserCancelled } } - m := mv.(*hazelcast.Map) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + m, err := getMap(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Destroying map %s", m.Name())) if err := m.Destroy(ctx); err != nil { return nil, err } - return nil, nil + return m.Name(), nil }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Destroyed map %s.", name) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_entry_set.go b/base/commands/map/map_entry_set.go index 2b749c7ae..ed7874dca 100644 --- a/base/commands/map/map_entry_set.go +++ b/base/commands/map/map_entry_set.go @@ -6,9 +6,9 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -18,6 +18,8 @@ import ( type MapEntrySetCommand struct{} +func (mc *MapEntrySetCommand) Unwrappable() {} + func (mc *MapEntrySetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("entry-set") help := "Get all entries of a Map" @@ -26,28 +28,33 @@ func (mc *MapEntrySetCommand) Init(cc plug.InitContext) error { } func (mc *MapEntrySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - showType := ec.Props().GetBool(mapFlagShowType) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - req := codec.EncodeMapEntrySetRequest(mapName) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + mapName := ec.Props().GetString(base.FlagName) + showType := ec.Props().GetBool(base.FlagShowType) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + req := codec.EncodeMapEntrySetRequest(mapName) sp.SetText(fmt.Sprintf("Getting entries of %s", mapName)) - return ci.InvokeOnRandomTarget(ctx, req, nil) + resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) + if err != nil { + return nil, err + } + pairs := codec.DecodeMapEntrySetResponse(resp) + rows := output.DecodePairs(ci, pairs, showType) + return rows, nil }) if err != nil { return err } stop() - pairs := codec.DecodeMapEntrySetResponse(rv.(*hazelcast.ClientMessage)) - rows := output.DecodePairs(ci, pairs, showType) - if len(rows) > 0 { - return ec.AddOutputRows(ctx, rows...) + rows := rowsV.([]output.Row) + if len(rows) == 0 { + ec.PrintlnUnnecessary("OK No entries found.") + return nil } - ec.PrintlnUnnecessary("No entries found.") - return nil + return ec.AddOutputRows(ctx, rows...) } func init() { diff --git a/base/commands/map/map_get.go b/base/commands/map/map_get.go index 78741f599..b52a6130f 100644 --- a/base/commands/map/map_get.go +++ b/base/commands/map/map_get.go @@ -6,9 +6,9 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,6 +18,8 @@ import ( type MapGetCommand struct{} +func (mc *MapGetCommand) Unwrappable() {} + func (mc *MapGetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("get") addKeyTypeFlag(cc) @@ -28,47 +30,52 @@ func (mc *MapGetCommand) Init(cc plug.InitContext) error { } func (mc *MapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } + mapName := ec.Props().GetString(base.FlagName) keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } - req := codec.EncodeMapGetRequest(mapName, keyData, 0) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + showType := ec.Props().GetBool(base.FlagShowType) + rowV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Getting from map %s", mapName)) - return ci.InvokeOnKey(ctx, req, keyData, nil) + keyData, err := makeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + req := codec.EncodeMapGetRequest(mapName, keyData, 0) + resp, err := ci.InvokeOnKey(ctx, req, keyData, nil) + if err != nil { + return nil, err + } + data := codec.DecodeMapGetResponse(resp) + vt := data.Type() + value, err := ci.DecodeData(data) + if err != nil { + ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) + value = serialization.NondecodedType(serialization.TypeToLabel(vt)) + } + row := output.Row{ + output.Column{ + Name: output.NameValue, + Type: vt, + Value: value, + }, + } + if showType { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(vt), + }) + } + return row, nil }) if err != nil { return err } stop() - raw := codec.DecodeMapGetResponse(rv.(*hazelcast.ClientMessage)) - vt := raw.Type() - value, err := ci.DecodeData(raw) - if err != nil { - ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) - value = serialization.NondecodedType(serialization.TypeToLabel(vt)) - } - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: vt, - Value: value, - }, - } - if ec.Props().GetBool(mapFlagShowType) { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(vt), - }) - } - return ec.AddOutputRows(ctx, row) + return ec.AddOutputRows(ctx, rowV.(output.Row)) } func init() { diff --git a/base/commands/map/map_it_test.go b/base/commands/map/map_it_test.go index ae5882ff5..a9d36eec4 100644 --- a/base/commands/map/map_it_test.go +++ b/base/commands/map/map_it_test.go @@ -154,7 +154,6 @@ func size_InteractiveTest(t *testing.T) { tcx.WithReset(func() { check.Must(m.Set(ctx, "foo", "bar")) tcx.WriteStdin([]byte(fmt.Sprintf("\\map -n %s size\n", m.Name()))) - tcx.AssertStderrContains("OK") tcx.AssertStdoutDollarWithPath("testdata/map_size_1.txt") }) }) @@ -196,14 +195,12 @@ func keySet_InteractiveTest(t *testing.T) { tcx.WithReset(func() { check.Must(m.Set(ctx, "foo", "bar")) tcx.WriteStdin([]byte(fmt.Sprintf("\\map -n %s key-set\n", m.Name()))) - tcx.AssertStderrContains("OK") tcx.AssertStdoutDollarWithPath("testdata/map_key_set.txt") }) // show type tcx.WithReset(func() { check.Must(m.Set(ctx, "foo", "bar")) tcx.WriteStdin([]byte(fmt.Sprintf("\\map -n %s key-set --show-type\n", m.Name()))) - tcx.AssertStderrContains("OK") tcx.AssertStdoutDollarWithPath("testdata/map_key_set_show_type.txt") }) }) @@ -259,13 +256,13 @@ func lock_InteractiveTest(t *testing.T) { go tcx.WithShell(context.TODO(), func(tcx it.TestContext) { tcx.WithReset(func() { tcx.WriteStdinf(fmt.Sprintf("\\map -n %s lock %s\n", m.Name(), key)) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") contUnlock <- true }) tcx.WithReset(func() { <-contLock tcx.WriteStdinf(fmt.Sprintf("\\map -n %s unlock %s\n", m.Name(), key)) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") contUnlock <- true }) }) @@ -334,7 +331,7 @@ func loadAll_Replacing_NonInteractiveTest(t *testing.T) { check.Must(m.PutTransient(context.Background(), "k0", "new-v0")) check.Must(m.PutTransient(context.Background(), "k1", "new-v1")) check.Must(tcx.CLC().Execute(ctx, "map", "-n", m.Name(), "load-all", "--replace")) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") require.Equal(t, "v0", check.MustValue(m.Get(ctx, "k0"))) require.Equal(t, "v1", check.MustValue(m.Get(ctx, "k1"))) }) @@ -353,7 +350,7 @@ func loadAll_NonReplacing_NonInteractiveTest(t *testing.T) { check.Must(m.PutTransient(context.Background(), "k0", "new-v0")) check.Must(m.PutTransient(context.Background(), "k1", "new-v1")) check.Must(tcx.CLC().Execute(ctx, "map", "-n", m.Name(), "load-all")) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") require.Equal(t, "new-v0", check.MustValue(m.Get(ctx, "k0"))) require.Equal(t, "new-v1", check.MustValue(m.Get(ctx, "k1"))) }) @@ -372,7 +369,7 @@ func loadAll_Replacing_WithKeys_NonInteractiveTest(t *testing.T) { check.Must(m.PutTransient(context.Background(), "k0", "new-v0")) check.Must(m.PutTransient(context.Background(), "k1", "new-v1")) check.Must(tcx.CLC().Execute(ctx, "map", "-n", m.Name(), "load-all", "k0", "--replace")) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") require.Equal(t, "v0", check.MustValue(m.Get(ctx, "k0"))) require.Equal(t, "new-v1", check.MustValue(m.Get(ctx, "k1"))) }) diff --git a/base/commands/map/map_key_set.go b/base/commands/map/map_key_set.go index 27d27b7c7..0c678c9c6 100644 --- a/base/commands/map/map_key_set.go +++ b/base/commands/map/map_key_set.go @@ -6,11 +6,11 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -20,6 +20,8 @@ import ( type MapKeySetCommand struct{} +func (mc *MapKeySetCommand) Unwrappable() {} + func (mc *MapKeySetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("key-set") help := "Get all keys of a Map" @@ -28,43 +30,47 @@ func (mc *MapKeySetCommand) Init(cc plug.InitContext) error { } func (mc *MapKeySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - showType := ec.Props().GetBool(mapFlagShowType) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - req := codec.EncodeMapKeySetRequest(mapName) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + mapName := ec.Props().GetString(base.FlagName) + showType := ec.Props().GetBool(base.FlagShowType) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + req := codec.EncodeMapKeySetRequest(mapName) sp.SetText(fmt.Sprintf("Getting keys of %s", mapName)) - return ci.InvokeOnRandomTarget(ctx, req, nil) + resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) + if err != nil { + return nil, err + } + data := codec.DecodeMapKeySetResponse(resp) + var rows []output.Row + for _, r := range data { + var row output.Row + t := r.Type() + v, err := ci.DecodeData(*r) + if err != nil { + v = serialization.NondecodedType(serialization.TypeToLabel(t)) + } + row = append(row, output.NewKeyColumn(t, v)) + if showType { + row = append(row, output.NewKeyTypeColumn(t)) + } + rows = append(rows, row) + } + return rows, nil }) if err != nil { return err } stop() - raw := codec.DecodeMapKeySetResponse(rv.(*hazelcast.ClientMessage)) - var rows []output.Row - for _, r := range raw { - var row output.Row - t := r.Type() - v, err := ci.DecodeData(*r) - if err != nil { - v = serialization.NondecodedType(serialization.TypeToLabel(t)) - } - row = append(row, output.NewKeyColumn(t, v)) - if showType { - row = append(row, output.NewKeyTypeColumn(t)) - } - rows = append(rows, row) - } - if len(rows) > 0 { - return ec.AddOutputRows(ctx, rows...) - } - - ec.PrintlnUnnecessary("No entries found.") + rows := rowsV.([]output.Row) + if len(rows) == 0 { + ec.PrintlnUnnecessary("OK No entries found.") + return nil - return nil + } + return ec.AddOutputRows(ctx, rows...) } func init() { diff --git a/base/commands/map/map_load_all.go b/base/commands/map/map_load_all.go index bf8cb0ed2..8afd19ba0 100644 --- a/base/commands/map/map_load_all.go +++ b/base/commands/map/map_load_all.go @@ -8,7 +8,9 @@ import ( "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -16,6 +18,8 @@ import ( type MapLoadAllCommand struct{} +func (mc *MapLoadAllCommand) Unwrappable() {} + func (mc *MapLoadAllCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("load-all") long := `Load keys from map-store into the map @@ -30,34 +34,39 @@ If no key is given, all keys are loaded.` } func (mc *MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - var keys []hazelcast.Data - for _, keyStr := range ec.GetStringSliceArg(argKey) { - keyData, err := makeKeyData(ec, ci, keyStr) + mapName := ec.Props().GetString(base.FlagName) + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { - return err + return nil, err + } + var keys []hazelcast.Data + for _, keyStr := range ec.GetStringSliceArg(argKey) { + keyData, err := makeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + keys = append(keys, keyData) + } + replace := ec.Props().GetBool(mapFlagReplace) + var req *hazelcast.ClientMessage + if len(keys) == 0 { + req = codec.EncodeMapLoadAllRequest(mapName, replace) + } else { + req = codec.EncodeMapLoadGivenKeysRequest(mapName, keys, replace) } - keys = append(keys, keyData) - } - replace := ec.Props().GetBool(mapFlagReplace) - var req *hazelcast.ClientMessage - if len(keys) == 0 { - req = codec.EncodeMapLoadAllRequest(mapName, replace) - } else { - req = codec.EncodeMapLoadGivenKeysRequest(mapName, keys, replace) - } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText(fmt.Sprintf("Loading keys into the map %s", mapName)) - return ci.InvokeOnRandomTarget(ctx, req, nil) + if _, err = ci.InvokeOnRandomTarget(ctx, req, nil); err != nil { + return nil, err + } + return nil, nil }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Loaded the keys into map %s", mapName) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_lock.go b/base/commands/map/map_lock.go index 02e2c53fd..4023f6660 100644 --- a/base/commands/map/map_lock.go +++ b/base/commands/map/map_lock.go @@ -7,15 +7,17 @@ import ( "fmt" "time" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) type MapLock struct{} +func (mc *MapLock) Unwrappable() {} + func (mc *MapLock) Init(cc plug.InitContext) error { cc.SetCommandUsage("lock") long := `Lock a key in the given Map @@ -30,22 +32,21 @@ This command is only available in the interactive mode.` } func (mc *MapLock) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - mv, err := ec.Props().GetBlocking(mapPropertyName) - if err != nil { - return err - } - m := mv.(*hazelcast.Map) - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } + mapName := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + m, err := getMap(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(argKey) + keyData, err := makeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Locking key in map %s", mapName)) if ttl := GetTTL(ec); ttl != ttlUnset { return nil, m.LockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) @@ -56,6 +57,8 @@ func (mc *MapLock) Exec(ctx context.Context, ec plug.ExecContext) error { return err } stop() + msg := fmt.Sprintf("OK Locked the key in map %s", mapName) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_remove.go b/base/commands/map/map_remove.go index 2261f377b..059577ca8 100644 --- a/base/commands/map/map_remove.go +++ b/base/commands/map/map_remove.go @@ -6,9 +6,9 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,6 +18,8 @@ import ( type MapRemoveCommand struct{} +func (mc *MapRemoveCommand) Unwrappable() {} + func (mc *MapRemoveCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("remove") help := "Remove a value from the given Map" @@ -28,47 +30,54 @@ func (mc *MapRemoveCommand) Init(cc plug.InitContext) error { } func (mc *MapRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } - req := codec.EncodeMapRemoveRequest(mapName, keyData, 0) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + mapName := ec.Props().GetString(base.FlagName) + showType := ec.Props().GetBool(base.FlagShowType) + rowV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(argKey) + keyData, err := makeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + req := codec.EncodeMapRemoveRequest(mapName, keyData, 0) sp.SetText(fmt.Sprintf("Removing from map %s", mapName)) - return ci.InvokeOnKey(ctx, req, keyData, nil) + resp, err := ci.InvokeOnKey(ctx, req, keyData, nil) + if err != nil { + return nil, err + } + raw := codec.DecodeMapRemoveResponse(resp) + vt := raw.Type() + value, err := ci.DecodeData(raw) + if err != nil { + ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) + value = serialization.NondecodedType(serialization.TypeToLabel(vt)) + } + row := output.Row{ + output.Column{ + Name: output.NameValue, + Type: vt, + Value: value, + }, + } + if showType { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(vt), + }) + } + return row, nil }) if err != nil { return err } stop() - raw := codec.DecodeMapRemoveResponse(rv.(*hazelcast.ClientMessage)) - vt := raw.Type() - value, err := ci.DecodeData(raw) - if err != nil { - ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) - value = serialization.NondecodedType(serialization.TypeToLabel(vt)) - } - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: vt, - Value: value, - }, - } - if ec.Props().GetBool(mapFlagShowType) { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(vt), - }) - } - return ec.AddOutputRows(ctx, row) + msg := fmt.Sprintf("OK Removed the entry from map: %s.\n", mapName) + ec.PrintlnUnnecessary(msg) + return ec.AddOutputRows(ctx, rowV.(output.Row)) } func init() { diff --git a/base/commands/map/map_set.go b/base/commands/map/map_set.go index 717542de7..13a1fdae3 100644 --- a/base/commands/map/map_set.go +++ b/base/commands/map/map_set.go @@ -8,7 +8,9 @@ import ( "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -21,6 +23,8 @@ const ( type MapSetCommand struct{} +func (mc *MapSetCommand) Unwrappable() {} + func (mc *MapSetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("set") help := "Set a value in the given Map" @@ -35,37 +39,43 @@ func (mc *MapSetCommand) Init(cc plug.InitContext) error { } func (mc *MapSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - // get the map just to ensure the corresponding proxy is created - if _, err := ec.Props().GetBlocking(mapPropertyName); err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - valueStr := ec.GetStringArg(argValue) - kd, vd, err := makeKeyValueData(ec, ci, keyStr, valueStr) - if err != nil { - return err - } - ttl := GetTTL(ec) - maxIdle := GetMaxIdle(ec) - var req *hazelcast.ClientMessage - if maxIdle >= 0 { - req = codec.EncodeMapSetWithMaxIdleRequest(mapName, kd, vd, 0, ttl, maxIdle) - } else { - req = codec.EncodeMapSetRequest(mapName, kd, vd, 0, ttl) - } + mapName := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Setting value into map %s", mapName)) - return ci.InvokeOnKey(ctx, req, kd, nil) + _, err = getMap(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(argKey) + valueStr := ec.GetStringArg(argValue) + kd, vd, err := makeKeyValueData(ec, ci, keyStr, valueStr) + if err != nil { + return nil, err + } + ttl := GetTTL(ec) + maxIdle := GetMaxIdle(ec) + var req *hazelcast.ClientMessage + if maxIdle >= 0 { + req = codec.EncodeMapSetWithMaxIdleRequest(mapName, kd, vd, 0, ttl, maxIdle) + } else { + req = codec.EncodeMapSetRequest(mapName, kd, vd, 0, ttl) + } + _, err = ci.InvokeOnKey(ctx, req, kd, nil) + if err != nil { + return nil, err + } + return nil, nil }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Set the value into the map %s", mapName) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_size.go b/base/commands/map/map_size.go index 884bf6269..044bf259e 100644 --- a/base/commands/map/map_size.go +++ b/base/commands/map/map_size.go @@ -6,8 +6,7 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -17,6 +16,8 @@ import ( type MapSizeCommand struct{} +func (mc *MapSizeCommand) Unwrappable() {} + func (mc *MapSizeCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("size") help := "Return the size of the given Map" @@ -25,13 +26,12 @@ func (mc *MapSizeCommand) Init(cc plug.InitContext) error { } func (mc *MapSizeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - mv, err := ec.Props().GetBlocking(mapPropertyName) - if err != nil { - return err - } - m := mv.(*hazelcast.Map) + mapName := ec.Props().GetString(base.FlagName) sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + m, err := getMap(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Getting the size of the map %s", mapName)) return m.Size(ctx) }) diff --git a/base/commands/map/map_try_lock.go b/base/commands/map/map_try_lock.go index 42686707a..1d075d4f5 100644 --- a/base/commands/map/map_try_lock.go +++ b/base/commands/map/map_try_lock.go @@ -7,9 +7,9 @@ import ( "fmt" "time" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,6 +18,8 @@ import ( type MapTryLock struct{} +func (mc *MapTryLock) Unwrappable() {} + func (mc *MapTryLock) Init(cc plug.InitContext) error { cc.SetCommandUsage("try-lock") long := `Try to lock a key in the given map @@ -34,23 +36,22 @@ This command is only available in the interactive mode.` } func (mc *MapTryLock) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - mv, err := ec.Props().GetBlocking(mapPropertyName) - if err != nil { - return err - } - m := mv.(*hazelcast.Map) - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + mapName := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Locking key in map %s", mapName)) + m, err := getMap(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(argKey) + keyData, err := makeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } if ttl := GetTTL(ec); ttl != ttlUnset { return m.TryLockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) } diff --git a/base/commands/map/map_unlock.go b/base/commands/map/map_unlock.go index a558b7e4c..5ad0b4ee4 100644 --- a/base/commands/map/map_unlock.go +++ b/base/commands/map/map_unlock.go @@ -6,8 +6,7 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -15,6 +14,8 @@ import ( type MapUnlock struct{} +func (mc *MapUnlock) Unwrappable() {} + func (mc *MapUnlock) Init(cc plug.InitContext) error { cc.SetCommandUsage("unlock") long := `Unlock a key in the given Map @@ -28,29 +29,30 @@ This command is only available in the interactive mode.` } func (mc *MapUnlock) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - mv, err := ec.Props().GetBlocking(mapPropertyName) - if err != nil { - return err - } - m := mv.(*hazelcast.Map) - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } + mapName := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Locking key in map %s", mapName)) + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Unlocking key in map %s", mapName)) + m, err := getMap(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(argKey) + keyData, err := makeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } return nil, m.Unlock(ctx, keyData) }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Unlocked the key in map %s", mapName) + ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_values.go b/base/commands/map/map_values.go index aa1cc9394..5b98b67cd 100644 --- a/base/commands/map/map_values.go +++ b/base/commands/map/map_values.go @@ -6,9 +6,9 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,6 +18,8 @@ import ( type MapValuesCommand struct{} +func (mc *MapValuesCommand) Unwrappable() {} + func (mc *MapValuesCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("values") help := "Get all values of a Map" @@ -26,41 +28,46 @@ func (mc *MapValuesCommand) Init(cc plug.InitContext) error { } func (mc *MapValuesCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(mapFlagName) - showType := ec.Props().GetBool(mapFlagShowType) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - req := codec.EncodeMapValuesRequest(mapName) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + mapName := ec.Props().GetString(base.FlagName) + showType := ec.Props().GetBool(base.FlagShowType) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Getting values of %s", mapName)) - return ci.InvokeOnRandomTarget(ctx, req, nil) + req := codec.EncodeMapValuesRequest(mapName) + resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) + if err != nil { + return nil, err + } + data := codec.DecodeMapValuesResponse(resp) + var rows []output.Row + for _, r := range data { + var row output.Row + t := r.Type() + v, err := ci.DecodeData(*r) + if err != nil { + v = serialization.NondecodedType(serialization.TypeToLabel(t)) + } + row = append(row, output.NewValueColumn(t, v)) + if showType { + row = append(row, output.NewValueTypeColumn(t)) + } + rows = append(rows, row) + } + return rows, nil }) if err != nil { return err } stop() - raw := codec.DecodeMapValuesResponse(rv.(*hazelcast.ClientMessage)) - var rows []output.Row - for _, r := range raw { - var row output.Row - t := r.Type() - v, err := ci.DecodeData(*r) - if err != nil { - v = serialization.NondecodedType(serialization.TypeToLabel(t)) - } - row = append(row, output.NewValueColumn(t, v)) - if showType { - row = append(row, output.NewValueTypeColumn(t)) - } - rows = append(rows, row) - } - if len(rows) > 0 { - return ec.AddOutputRows(ctx, rows...) + rows := rowsV.([]output.Row) + if len(rows) == 0 { + ec.PrintlnUnnecessary("OK the map has no values.") + return nil } - ec.PrintlnUnnecessary("No values found.") - return nil + return ec.AddOutputRows(ctx, rows...) } func init() { diff --git a/clc/ux/stage/stage.go b/clc/ux/stage/stage.go index 8c1fe91f9..fa71255dc 100644 --- a/clc/ux/stage/stage.go +++ b/clc/ux/stage/stage.go @@ -6,6 +6,7 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" + hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/str" @@ -117,7 +118,7 @@ func Execute[T any](ctx context.Context, ec plug.ExecContext, value T, sp Provid }) if err != nil { ec.PrintlnUnnecessary(fmt.Sprintf("FAIL %s: %s", stg.FailureMsg, err.Error())) - return value, err + return value, hzerrors.WrappedError{Err: err} } stop() ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg)) From 865eb41b9482e1b1adda708db797e0040f15d211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 8 Sep 2023 18:47:03 +0300 Subject: [PATCH 53/79] Fix install sh (#375) Fixed the install script --- extras/unix/install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extras/unix/install.sh b/extras/unix/install.sh index f7a28d2a9..3bfa695e4 100644 --- a/extras/unix/install.sh +++ b/extras/unix/install.sh @@ -180,7 +180,9 @@ fi return fi local text - text="$(cat "$path" | grep "$installed")" + set +e + text=$(cat "$path" | grep "$installed") + set -e if [[ "$text" != "" ]]; then # CLC PATH is already exported in this file log_debug "CLC PATH is already installed in $path" From 53a2666d9ee836c081a90ad254399f7bd21e6863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 11 Sep 2023 13:42:14 +0300 Subject: [PATCH 54/79] More refactoring and code removal [CLC-330] (#376) More refactoring and code removal --- base/commands/atomic_long/atomic_long.go | 21 +- .../atomic_long/atomic_long_decrement_get.go | 14 +- .../atomic_long/atomic_long_destroy.go | 14 + base/commands/atomic_long/atomic_long_get.go | 31 +- .../atomic_long/atomic_long_increment_get.go | 16 +- base/commands/atomic_long/atomic_long_set.go | 52 +- base/commands/atomic_long/common.go | 35 +- base/commands/atomic_long/const.go | 3 +- base/commands/common.go | 261 ++++++++- base/commands/config/config.go | 12 +- base/commands/config/config_add.go | 39 +- base/commands/config/config_import.go | 12 +- base/commands/config/config_it_test.go | 2 +- base/commands/config/config_list.go | 48 +- base/commands/const.go | 6 + base/commands/demo/demo.go | 12 +- base/commands/demo/demo_generate_data.go | 242 ++++----- base/commands/demo/demo_map_set_many.go | 2 - base/commands/home.go | 10 +- base/commands/job/common.go | 2 +- base/commands/job/job.go | 10 +- base/commands/job/job_export_snapshot.go | 40 +- base/commands/job/job_it_test.go | 28 +- base/commands/job/job_list.go | 40 +- base/commands/job/job_resume.go | 10 +- base/commands/job/job_submit.go | 35 +- base/commands/job/job_terminate.go | 26 +- base/commands/list/common.go | 44 +- base/commands/list/list.go | 15 +- base/commands/list/list_add.go | 22 +- base/commands/list/list_clear.go | 58 +- base/commands/list/list_contains.go | 12 +- base/commands/list/list_destroy.go | 60 +-- base/commands/list/list_remove_index.go | 2 - base/commands/list/list_remove_value.go | 5 +- base/commands/list/list_set.go | 13 +- base/commands/list/list_size.go | 47 +- base/commands/map/common.go | 89 ++-- base/commands/map/const.go | 12 +- base/commands/map/map.go | 14 +- base/commands/map/map_clear.go | 59 +-- base/commands/map/map_destroy.go | 60 +-- base/commands/map/map_entry_set.go | 55 +- base/commands/map/map_get.go | 76 +-- base/commands/map/map_it_test.go | 2 +- base/commands/map/map_key_set.go | 71 +-- base/commands/map/map_load_all.go | 25 +- base/commands/map/map_lock.go | 61 +-- base/commands/map/map_remove.go | 78 +-- base/commands/map/map_set.go | 41 +- base/commands/map/map_size.go | 47 +- base/commands/map/map_try_lock.go | 70 +-- base/commands/map/map_unlock.go | 55 +- base/commands/map/map_values.go | 68 +-- base/commands/map/util.go | 21 - base/commands/map_common.go | 496 ++++++++++++++++++ base/commands/multimap/common.go | 86 +-- base/commands/multimap/const.go | 13 - base/commands/multimap/multimap.go | 59 +-- base/commands/multimap/multimap_clear.go | 56 +- base/commands/multimap/multimap_destroy.go | 59 +-- base/commands/multimap/multimap_entry_set.go | 47 +- base/commands/multimap/multimap_get.go | 73 +-- base/commands/multimap/multimap_key_set.go | 61 +-- base/commands/multimap/multimap_lock.go | 57 +- base/commands/multimap/multimap_put.go | 94 ++-- base/commands/multimap/multimap_remove.go | 73 +-- base/commands/multimap/multimap_size.go | 47 +- base/commands/multimap/multimap_try_lock.go | 74 +-- base/commands/multimap/multimap_unlock.go | 48 +- base/commands/multimap/multimap_values.go | 70 +-- base/commands/multimap/util.go | 12 - base/commands/object/object.go | 10 +- base/commands/object/object_list.go | 19 +- base/commands/project/const.go | 2 +- base/commands/project/help.go | 2 + base/commands/project/project.go | 14 +- base/commands/project/project_create.go | 76 ++- .../project/project_list_templates.go | 13 +- base/commands/project/utils.go | 30 +- base/commands/queue/common.go | 24 +- base/commands/queue/const.go | 8 - base/commands/queue/queue.go | 57 +- base/commands/queue/queue_clear.go | 56 +- base/commands/queue/queue_destroy.go | 59 +-- base/commands/queue/queue_offer.go | 69 +-- base/commands/queue/queue_poll.go | 68 +-- base/commands/queue/queue_size.go | 47 +- base/commands/script.go | 9 +- base/commands/set/common.go | 38 +- base/commands/set/const.go | 10 - base/commands/set/set.go | 56 +- base/commands/set/set_add.go | 67 ++- base/commands/set/set_clear.go | 56 +- base/commands/set/set_destroy.go | 58 +- base/commands/set/set_get_all.go | 97 ++-- base/commands/set/set_remove.go | 38 +- base/commands/set/set_size.go | 47 +- base/commands/shell.go | 11 +- base/commands/shell_it_test.go | 6 +- base/commands/shell_script_common.go | 7 +- base/commands/snapshot/snapshot.go | 10 +- base/commands/snapshot/snapshot_delete.go | 22 +- base/commands/snapshot/snapshot_list.go | 31 +- base/commands/sql/sql.go | 33 +- base/commands/sql/sql_it_test.go | 9 +- base/commands/topic/common.go | 118 +---- base/commands/topic/const.go | 4 +- base/commands/topic/topic.go | 57 +- base/commands/topic/topic_destroy.go | 61 +-- base/commands/topic/topic_it_test.go | 1 - base/commands/topic/topic_publish.go | 71 +-- base/commands/topic/topic_subscribe.go | 169 +++++- base/commands/version.go | 24 +- base/commands/viridian/custom_class_delete.go | 12 +- .../viridian/custom_class_download.go | 12 +- base/commands/viridian/custom_class_list.go | 12 +- base/commands/viridian/custom_class_upload.go | 12 +- base/commands/viridian/download_logs.go | 18 +- base/commands/viridian/viridian.go | 16 +- .../viridian/viridian_cluster_create.go | 12 +- .../viridian/viridian_cluster_delete.go | 12 +- .../commands/viridian/viridian_cluster_get.go | 12 +- .../viridian/viridian_cluster_list.go | 14 +- .../viridian/viridian_cluster_resume.go | 12 +- .../viridian/viridian_cluster_stop.go | 12 +- .../viridian/viridian_import_config.go | 23 +- base/commands/viridian/viridian_it_test.go | 12 +- base/commands/viridian/viridian_log_stream.go | 10 +- base/commands/viridian/viridian_login.go | 14 +- base/initializers.go | 1 - base/objects/objects.go | 3 +- clc/cmd/clc.go | 19 +- clc/cmd/exec_context.go | 62 +-- clc/cmd/utils.go | 34 +- clc/config/config.go | 4 - clc/const.go | 2 +- clc/shell/common.go | 20 +- clc/shell/shell.go | 8 +- clc/sql/sql.go | 81 ++- clc/ux/stage/errors.go | 17 + clc/ux/stage/stage.go | 17 +- clc/ux/stage/stage_test.go | 20 +- cmd/clc/main.go | 7 +- errors/error.go | 11 - errors/utils.go | 58 ++ internal/client.go | 20 + internal/demo/wikimedia/generate.go | 2 +- internal/plug/command.go | 4 - internal/plug/context.go | 4 - internal/str/str.go | 12 + internal/topic/topic.go | 85 --- 152 files changed, 2428 insertions(+), 3572 deletions(-) create mode 100644 base/commands/atomic_long/atomic_long_destroy.go create mode 100644 base/commands/const.go delete mode 100644 base/commands/map/util.go create mode 100644 base/commands/map_common.go delete mode 100644 base/commands/multimap/const.go delete mode 100644 base/commands/multimap/util.go delete mode 100644 base/commands/queue/const.go delete mode 100644 base/commands/set/const.go create mode 100644 clc/ux/stage/errors.go create mode 100644 errors/utils.go create mode 100644 internal/client.go delete mode 100644 internal/topic/topic.go diff --git a/base/commands/atomic_long/atomic_long.go b/base/commands/atomic_long/atomic_long.go index d6e2dca42..99dd13808 100644 --- a/base/commands/atomic_long/atomic_long.go +++ b/base/commands/atomic_long/atomic_long.go @@ -5,32 +5,29 @@ package atomiclong import ( "context" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - flagName = "name" -) - -type AtomicLongCommand struct{} +type Command struct{} -func (AtomicLongCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("atomic-long") cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(flagName, "n", defaultAtomicLongName, false, "atomic long name") cc.SetTopLevel(true) - cc.SetCommandUsage("atomic-long") - help := "Atomic long operations" + help := "AtomicLong operations" cc.SetCommandHelp(help, help) + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "AtomicLong name") return nil } -func (mc *AtomicLongCommand) Exec(context.Context, plug.ExecContext) error { +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - Must(plug.Registry.RegisterCommand("atomic-long", &AtomicLongCommand{})) + check.Must(plug.Registry.RegisterCommand("atomic-long", &Command{})) } diff --git a/base/commands/atomic_long/atomic_long_decrement_get.go b/base/commands/atomic_long/atomic_long_decrement_get.go index a4b92f815..da0115d0d 100644 --- a/base/commands/atomic_long/atomic_long_decrement_get.go +++ b/base/commands/atomic_long/atomic_long_decrement_get.go @@ -5,26 +5,24 @@ package atomiclong import ( "context" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type AtomicLongDecrementGetCommand struct{} +type DecrementGetCommand struct{} -func (mc *AtomicLongDecrementGetCommand) Unwrappable() {} - -func (mc *AtomicLongDecrementGetCommand) Init(cc plug.InitContext) error { +func (DecrementGetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("decrement-get") help := "Decrement the AtomicLong by the given value" cc.SetCommandHelp(help, help) - cc.AddIntFlag(atomicLongFlagBy, "", 1, false, "value to decrement by") + cc.AddIntFlag(flagBy, "", 1, false, "value to decrement by") return nil } -func (mc *AtomicLongDecrementGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (DecrementGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { return atomicLongChangeValue(ctx, ec, "Decrement", func(i int64) int64 { return -1 * i }) } func init() { - Must(plug.Registry.RegisterCommand("atomic-long:decrement-get", &AtomicLongDecrementGetCommand{})) + check.Must(plug.Registry.RegisterCommand("atomic-long:decrement-get", &DecrementGetCommand{})) } diff --git a/base/commands/atomic_long/atomic_long_destroy.go b/base/commands/atomic_long/atomic_long_destroy.go new file mode 100644 index 000000000..1918dc5e9 --- /dev/null +++ b/base/commands/atomic_long/atomic_long_destroy.go @@ -0,0 +1,14 @@ +//go:build std || atomiclong + +package atomiclong + +import ( + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" +) + +func init() { + c := commands.NewDestroyCommand("AtomicLong", getAtomicLong) + check.Must(plug.Registry.RegisterCommand("atomic-long:destroy", c)) +} diff --git a/base/commands/atomic_long/atomic_long_get.go b/base/commands/atomic_long/atomic_long_get.go index 3d9be8076..8a0389868 100644 --- a/base/commands/atomic_long/atomic_long_get.go +++ b/base/commands/atomic_long/atomic_long_get.go @@ -7,25 +7,24 @@ import ( "fmt" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type AtomicLongGetCommand struct{} +type GetCommand struct{} -func (mc *AtomicLongGetCommand) Unwrappable() {} - -func (mc *AtomicLongGetCommand) Init(cc plug.InitContext) error { +func (GetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("get") help := "Get the value of the AtomicLong" cc.SetCommandHelp(help, help) return nil } -func (mc *AtomicLongGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - val, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { +func (GetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + row, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (output.Row, error) { ali, err := getAtomicLong(ctx, ec, sp) if err != nil { return nil, err @@ -35,22 +34,22 @@ func (mc *AtomicLongGetCommand) Exec(ctx context.Context, ec plug.ExecContext) e if err != nil { return nil, err } - return val, nil + row := output.Row{ + output.Column{ + Name: "Value", + Type: serialization.TypeInt64, + Value: val, + }, + } + return row, nil }) if err != nil { return err } stop() - row := output.Row{ - output.Column{ - Name: "Value", - Type: serialization.TypeInt64, - Value: val, - }, - } return ec.AddOutputRows(ctx, row) } func init() { - Must(plug.Registry.RegisterCommand("atomic-long:get", &AtomicLongGetCommand{})) + check.Must(plug.Registry.RegisterCommand("atomic-long:get", &GetCommand{})) } diff --git a/base/commands/atomic_long/atomic_long_increment_get.go b/base/commands/atomic_long/atomic_long_increment_get.go index d2bff150f..a1db69da6 100644 --- a/base/commands/atomic_long/atomic_long_increment_get.go +++ b/base/commands/atomic_long/atomic_long_increment_get.go @@ -5,26 +5,24 @@ package atomiclong import ( "context" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type AtomicLongIncrementGetCommand struct{} +type IncrementGetCommand struct{} -func (mc *AtomicLongIncrementGetCommand) Unwrappable() {} - -func (mc *AtomicLongIncrementGetCommand) Init(cc plug.InitContext) error { +func (mc *IncrementGetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("increment-get") - help := "Increment the atomic long by the given value" + help := "Increment the AtomicLong by the given value" cc.SetCommandHelp(help, help) - cc.AddIntFlag(atomicLongFlagBy, "", 1, false, "value to increment by") + cc.AddIntFlag(flagBy, "", 1, false, "value to increment by") return nil } -func (mc *AtomicLongIncrementGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (mc *IncrementGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { return atomicLongChangeValue(ctx, ec, "Increment", func(i int64) int64 { return i }) } func init() { - Must(plug.Registry.RegisterCommand("atomic-long:increment-get", &AtomicLongIncrementGetCommand{})) + check.Must(plug.Registry.RegisterCommand("atomic-long:increment-get", &IncrementGetCommand{})) } diff --git a/base/commands/atomic_long/atomic_long_set.go b/base/commands/atomic_long/atomic_long_set.go index a653d0ca3..b131b7c63 100644 --- a/base/commands/atomic_long/atomic_long_set.go +++ b/base/commands/atomic_long/atomic_long_set.go @@ -6,64 +6,60 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -const ( - argValue = "value" - argTitleValue = "value" -) - -type AtomicLongSetCommand struct{} - -func (mc *AtomicLongSetCommand) Unwrappable() {} +type SetCommand struct{} -func (mc *AtomicLongSetCommand) Init(cc plug.InitContext) error { +func (mc *SetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("set") help := "Set the value of the AtomicLong" cc.SetCommandHelp(help, help) - cc.AddInt64Arg(argValue, argTitleValue) + cc.AddInt64Arg(base.ArgValue, base.ArgTitleValue) return nil } -func (mc *AtomicLongSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - stateV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { +func (mc *SetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + row, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (output.Row, error) { ali, err := getAtomicLong(ctx, ec, sp) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Setting value of AtomicLong %s", ali.Name())) - v := ec.GetInt64Arg(argValue) + sp.SetText(fmt.Sprintf("Setting value of AtomicLong %s", name)) + v := ec.GetInt64Arg(base.ArgValue) err = ali.Set(ctx, v) if err != nil { return nil, err } - state := executeState{ - Name: ali.Name(), + s := executeState{ + Name: name, Value: v, } - return state, nil + row := output.Row{ + output.Column{ + Name: "Value", + Type: serialization.TypeInt64, + Value: s.Value, + }, + } + return row, nil }) if err != nil { return err } stop() - s := stateV.(executeState) - msg := fmt.Sprintf("OK Set AtomicLong %s.\n", s.Name) + msg := fmt.Sprintf("OK Set AtomicLong %s.\n", name) ec.PrintlnUnnecessary(msg) - return ec.AddOutputRows(ctx, output.Row{ - output.Column{ - Name: "Value", - Type: serialization.TypeInt64, - Value: s.Value, - }, - }) + return ec.AddOutputRows(ctx, row) } func init() { - Must(plug.Registry.RegisterCommand("atomic-long:set", &AtomicLongSetCommand{})) + check.Must(plug.Registry.RegisterCommand("atomic-long:set", &SetCommand{})) } diff --git a/base/commands/atomic_long/common.go b/base/commands/atomic_long/common.go index f6d6a6628..d6b6df56d 100644 --- a/base/commands/atomic_long/common.go +++ b/base/commands/atomic_long/common.go @@ -8,6 +8,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -21,46 +22,46 @@ type executeState struct { } func atomicLongChangeValue(ctx context.Context, ec plug.ExecContext, verb string, change func(int64) int64) error { - by := ec.Props().GetInt(atomicLongFlagBy) - stateV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + name := ec.Props().GetString(base.FlagName) + by := ec.Props().GetInt(flagBy) + row, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (output.Row, error) { ali, err := getAtomicLong(ctx, ec, sp) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("%sing the AtomicLong %s", verb, ali.Name())) + sp.SetText(fmt.Sprintf("%sing the AtomicLong %s", verb, name)) val, err := ali.AddAndGet(ctx, change(by)) if err != nil { return nil, err } - state := executeState{ - Name: ali.Name(), + s := executeState{ + Name: name, Value: val, } - return state, nil + row := output.Row{ + output.Column{ + Name: "Value", + Type: serialization.TypeInt64, + Value: s.Value, + }, + } + return row, nil }) if err != nil { return err } stop() - s := stateV.(executeState) - msg := fmt.Sprintf("OK %sed AtomicLong %s by %d.\n", verb, s.Name, by) + msg := fmt.Sprintf("OK %sed AtomicLong %s by %d.\n", verb, name, by) ec.PrintlnUnnecessary(msg) - row := output.Row{ - output.Column{ - Name: "Value", - Type: serialization.TypeInt64, - Value: s.Value, - }, - } return ec.AddOutputRows(ctx, row) } func getAtomicLong(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.AtomicLong, error) { - name := ec.Props().GetString(flagName) + name := ec.Props().GetString(base.FlagName) ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Getting atomic long %s", name)) + sp.SetText(fmt.Sprintf("Getting AtomicLong '%s'", name)) return ci.Client().CPSubsystem().GetAtomicLong(ctx, name) } diff --git a/base/commands/atomic_long/const.go b/base/commands/atomic_long/const.go index 500b9d251..62313e61a 100644 --- a/base/commands/atomic_long/const.go +++ b/base/commands/atomic_long/const.go @@ -3,6 +3,5 @@ package atomiclong const ( - atomicLongFlagBy = "by" - defaultAtomicLongName = "default" + flagBy = "by" ) diff --git a/base/commands/common.go b/base/commands/common.go index 17f6c8391..1653af99f 100644 --- a/base/commands/common.go +++ b/base/commands/common.go @@ -1,3 +1,262 @@ package commands -import _ "github.com/hazelcast/hazelcast-commandline-client/base" +import ( + "context" + "fmt" + "strings" + + "github.com/hazelcast/hazelcast-go-client" + + "github.com/hazelcast/hazelcast-commandline-client/base" + _ "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/errors" + "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" +) + +const ( + FlagKeyType = "key-type" + FlagValueType = "value-type" + ArgKey = "key" + ArgTitleKey = "key" +) + +type Destroyer interface { + Destroy(ctx context.Context) error +} + +type getDestroyerFunc[T Destroyer] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) + +type DestroyCommand[T Destroyer] struct { + typeName string + getDestroyerFn getDestroyerFunc[T] +} + +func NewDestroyCommand[T Destroyer](typeName string, getFn getDestroyerFunc[T]) *DestroyCommand[T] { + return &DestroyCommand[T]{ + typeName: typeName, + getDestroyerFn: getFn, + } +} + +func (cm DestroyCommand[T]) Init(cc plug.InitContext) error { + long := fmt.Sprintf(`Destroy a %s + +This command will delete the %s and the data in it will not be available anymore.`, cm.typeName, cm.typeName) + cc.SetCommandUsage("destroy") + short := fmt.Sprintf("Destroy a %s", cm.typeName) + cc.SetCommandHelp(long, short) + cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") + return nil +} + +func (cm DestroyCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + autoYes := ec.Props().GetBool(clc.FlagAutoYes) + if !autoYes { + p := prompt.New(ec.Stdin(), ec.Stdout()) + yes, err := p.YesNo(fmt.Sprintf("%s '%s' will be deleted irreversibly, proceed?", cm.typeName, name)) + if err != nil { + ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) + return errors.ErrUserCancelled + } + if !yes { + return errors.ErrUserCancelled + } + } + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + m, err := cm.getDestroyerFn(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Destroying %s '%s'", cm.typeName, name)) + if err := m.Destroy(ctx); err != nil { + return nil, err + } + return nil, nil + }) + if err != nil { + return err + } + stop() + msg := fmt.Sprintf("OK Destroyed %s '%s'.", cm.typeName, name) + ec.PrintlnUnnecessary(msg) + return nil +} + +type Clearer interface { + Clear(ctx context.Context) error +} + +type getClearerFunc[T Clearer] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) + +type ClearCommand[T Clearer] struct { + typeName string + getClearerFn getClearerFunc[T] +} + +func NewClearCommand[T Clearer](typeName string, getFn getClearerFunc[T]) *ClearCommand[T] { + return &ClearCommand[T]{ + typeName: typeName, + getClearerFn: getFn, + } +} + +func (cm ClearCommand[T]) Init(cc plug.InitContext) error { + cc.SetCommandUsage("clear") + help := fmt.Sprintf("Deletes all entries of a %s", cm.typeName) + cc.SetCommandHelp(help, help) + cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") + return nil +} + +func (cm ClearCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + autoYes := ec.Props().GetBool(clc.FlagAutoYes) + if !autoYes { + p := prompt.New(ec.Stdin(), ec.Stdout()) + yes, err := p.YesNo(fmt.Sprintf("Content of %s '%s' will be deleted irreversibly. Proceed?", cm.typeName, name)) + if err != nil { + ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) + return errors.ErrUserCancelled + } + if !yes { + return errors.ErrUserCancelled + } + } + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + m, err := cm.getClearerFn(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Clearing %s '%s'", cm.typeName, name)) + if err := m.Clear(ctx); err != nil { + return nil, err + } + return nil, nil + }) + if err != nil { + return err + } + stop() + msg := fmt.Sprintf("OK Cleared %s '%s'.", cm.typeName, name) + ec.PrintlnUnnecessary(msg) + return nil +} + +type Sizer interface { + Size(ctx context.Context) (int, error) +} + +type getSizerFunc[T Sizer] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) + +type SizeCommand[T Sizer] struct { + typeName string + getSizerFn getSizerFunc[T] +} + +func NewSizeCommand[T Sizer](typeName string, getFn getSizerFunc[T]) *SizeCommand[T] { + return &SizeCommand[T]{ + typeName: typeName, + getSizerFn: getFn, + } +} + +func (cm SizeCommand[T]) Init(cc plug.InitContext) error { + cc.SetCommandUsage("size") + help := fmt.Sprintf("Returns the size of the given %s", cm.typeName) + cc.SetCommandHelp(help, help) + return nil +} + +func (cm SizeCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + m, err := cm.getSizerFn(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Getting the size of %s '%s'", cm.typeName, name)) + return m.Size(ctx) + }) + if err != nil { + return err + } + stop() + return ec.AddOutputRows(ctx, output.Row{ + { + Name: "Size", + Type: serialization.TypeInt32, + Value: int32(sv.(int)), + }, + }) +} + +func AddKeyTypeFlag(cc plug.InitContext) { + help := fmt.Sprintf("key type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) + cc.AddStringFlag(base.FlagKeyType, "k", "string", false, help) +} + +func AddValueTypeFlag(cc plug.InitContext) { + help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) + cc.AddStringFlag(FlagValueType, "v", "string", false, help) +} + +func MakeKeyData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr string) (hazelcast.Data, error) { + kt := ec.Props().GetString(FlagKeyType) + if kt == "" { + kt = "string" + } + key, err := mk.ValueFromString(keyStr, kt) + if err != nil { + return nil, err + } + return ci.EncodeData(key) +} + +func MakeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { + vt := ec.Props().GetString(base.FlagValueType) + if vt == "" { + vt = "string" + } + value, err := mk.ValueFromString(valueStr, vt) + if err != nil { + return nil, err + } + return ci.EncodeData(value) +} + +func MakeKeyValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr, valueStr string) (hazelcast.Data, hazelcast.Data, error) { + kd, err := MakeKeyData(ec, ci, keyStr) + if err != nil { + return nil, nil, err + } + vd, err := MakeValueData(ec, ci, valueStr) + if err != nil { + return nil, nil, err + } + return kd, vd, nil +} + +func AddDDSRows(ctx context.Context, ec plug.ExecContext, typeName, what string, rows []output.Row) error { + name := ec.Props().GetString(base.FlagName) + if len(rows) == 0 { + msg := fmt.Sprintf("OK No %s found in %s '%s'.", what, typeName, name) + ec.PrintlnUnnecessary(msg) + return nil + + } + return ec.AddOutputRows(ctx, rows...) +} + +func GetTTL(ec plug.ExecContext) int64 { + if _, ok := ec.Props().Get(FlagTTL); ok { + return ec.Props().GetInt(FlagTTL) + } + return clc.TTLUnset +} diff --git a/base/commands/config/config.go b/base/commands/config/config.go index 47701dc98..823e9d4f5 100644 --- a/base/commands/config/config.go +++ b/base/commands/config/config.go @@ -5,24 +5,24 @@ package config import ( "context" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type Cmd struct{} +type Command struct{} -func (cm Cmd) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("config") cc.SetTopLevel(true) - cc.SetCommandUsage("config [command] [flags]") help := "Show, add or change configuration" cc.SetCommandHelp(help, help) return nil } -func (cm Cmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (Command) Exec(ctx context.Context, ec plug.ExecContext) error { return nil } func init() { - Must(plug.Registry.RegisterCommand("config", &Cmd{})) + check.Must(plug.Registry.RegisterCommand("config", &Command{})) } diff --git a/base/commands/config/config_add.go b/base/commands/config/config_add.go index b38290228..a36097825 100644 --- a/base/commands/config/config_add.go +++ b/base/commands/config/config_add.go @@ -8,18 +8,18 @@ import ( "math" "path/filepath" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/config" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/str" "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) -type AddCmd struct{} +type AddCommand struct{} -func (cm AddCmd) Unwrappable() {} - -func (cm AddCmd) Init(cc plug.InitContext) error { +func (AddCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("add") short := "Adds a configuration" long := `Adds a configuration with the given KEY=VALUE pairs and saves it with configuration name. @@ -34,6 +34,7 @@ The following keys are supported: * cluster.password STRING * cluster.discovery-token STRING * cluster.api-base STRING + * cluster.viridian-id STRING * ssl.enabled BOOLEAN (true / false) * ssl.server STRING * ssl.skip-verify BOOLEAN (true / false) @@ -50,7 +51,7 @@ The following keys are supported: return nil } -func (cm AddCmd) Exec(_ context.Context, ec plug.ExecContext) error { +func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { target := ec.GetStringArg(argConfigName) var opts types.KeyValues[string, string] for _, arg := range ec.GetStringSliceArg(argKeyValues) { @@ -63,21 +64,29 @@ func (cm AddCmd) Exec(_ context.Context, ec plug.ExecContext) error { Value: v, }) } - dir, cfgPath, err := config.Create(target, opts) + path, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (string, error) { + sp.SetText("Creating the configuration") + dir, cfgPath, err := config.Create(target, opts) + if err != nil { + return "", err + } + 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 filepath.Join(dir, cfgPath), nil + }) if err != nil { return err } - 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()) - } - msg := fmt.Sprintf("OK Created the configuration at: %s", filepath.Join(dir, cfgPath)) + stop() + msg := fmt.Sprintf("OK Created the configuration at: %s.", path) ec.PrintlnUnnecessary(msg) return nil } func init() { - Must(plug.Registry.RegisterCommand("config:add", &AddCmd{})) + check.Must(plug.Registry.RegisterCommand("config:add", &AddCommand{})) } diff --git a/base/commands/config/config_import.go b/base/commands/config/config_import.go index 613932d8e..1ed8ef377 100644 --- a/base/commands/config/config_import.go +++ b/base/commands/config/config_import.go @@ -7,17 +7,15 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ImportCmd struct{} +type ImportCommand struct{} -func (cm ImportCmd) Unwrappable() {} - -func (cm ImportCmd) Init(cc plug.InitContext) error { +func (ImportCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("import") short := "Imports configuration from an arbitrary source" long := `Imports configuration from an arbitrary source @@ -40,7 +38,7 @@ Currently importing Viridian connection configuration is supported only. return nil } -func (cm ImportCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ImportCommand) Exec(ctx context.Context, ec plug.ExecContext) error { target := ec.GetStringArg(argConfigName) src := ec.GetStringArg(argSource) stages := config.MakeImportStages(ec, target) @@ -59,5 +57,5 @@ func (cm ImportCmd) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("config:import", &ImportCmd{})) + check.Must(plug.Registry.RegisterCommand("config:import", &ImportCommand{})) } diff --git a/base/commands/config/config_it_test.go b/base/commands/config/config_it_test.go index 1bef5da9f..f1e1a7529 100644 --- a/base/commands/config/config_it_test.go +++ b/base/commands/config/config_it_test.go @@ -56,7 +56,7 @@ func addTest(t *testing.T) { ctx := context.Background() tcx.WithReset(func() { check.Must(tcx.CLC().Execute(ctx, "config", "add", name, "cluster.address=foobar.com")) - tcx.AssertStdoutContains("OK Created the configuration") + tcx.AssertStdoutContains(" OK Created the configuration") }) tcx.WithReset(func() { check.Must(tcx.CLC().Execute(ctx, "config", "list")) diff --git a/base/commands/config/config_list.go b/base/commands/config/config_list.go index 45e7436bf..ce34b1f54 100644 --- a/base/commands/config/config_list.go +++ b/base/commands/config/config_list.go @@ -6,17 +6,19 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ListCmd struct{} +type ListCommand struct{} -func (cm ListCmd) Init(cc plug.InitContext) error { +func (ListCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("list") long := fmt.Sprintf(`Lists known configurations @@ -28,29 +30,37 @@ Directory names which start with . or _ are ignored. return nil } -func (cm ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - cd := paths.Configs() - cs, err := config.FindAll(cd) +func (ListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + rows, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) ([]output.Row, error) { + sp.SetText("Finding configurations") + cd := paths.Configs() + cs, err := config.FindAll(cd) + if err != nil { + ec.Logger().Warn("Cannot access configuration directory at: %s: %s", cd, err.Error()) + } + var rows []output.Row + for _, c := range cs { + rows = append(rows, output.Row{output.Column{ + Name: "Configuration Name", + Type: serialization.TypeString, + Value: c, + }}) + } + return rows, nil + }) if err != nil { - ec.Logger().Warn("Cannot access configs directory at: %s: %s", cd, err.Error()) + return err } - if len(cs) == 0 { + stop() + if len(rows) == 0 { ec.PrintlnUnnecessary("OK No configurations found.") return nil } - var rows []output.Row - for _, c := range cs { - rows = append(rows, output.Row{output.Column{ - Name: "Config Name", - Type: serialization.TypeString, - Value: c, - }}) - } + msg := fmt.Sprintf("OK Found %d configurations.", len(rows)) + defer ec.PrintlnUnnecessary(msg) return ec.AddOutputRows(ctx, rows...) } -func (ListCmd) Unwrappable() {} - func init() { - Must(plug.Registry.RegisterCommand("config:list", &ListCmd{})) + check.Must(plug.Registry.RegisterCommand("config:list", &ListCommand{})) } diff --git a/base/commands/const.go b/base/commands/const.go new file mode 100644 index 000000000..4554246df --- /dev/null +++ b/base/commands/const.go @@ -0,0 +1,6 @@ +package commands + +const ( + FlagTTL = "ttl" + FlagOutputDir = "output-dir" +) diff --git a/base/commands/demo/demo.go b/base/commands/demo/demo.go index d9135f4a7..fbcdf0ed6 100644 --- a/base/commands/demo/demo.go +++ b/base/commands/demo/demo.go @@ -5,26 +5,26 @@ package demo import ( "context" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type Cmd struct{} +type Command struct{} -func (cm *Cmd) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("demo") cc.AddCommandGroup(GroupDemoID, "Demonstrations") cc.SetCommandGroup(GroupDemoID) cc.SetTopLevel(true) help := "Demonstration commands" - cc.SetCommandUsage("demo [command]") cc.SetCommandHelp(help, help) return nil } -func (cm *Cmd) Exec(context.Context, plug.ExecContext) error { +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - Must(plug.Registry.RegisterCommand("demo", &Cmd{})) + check.Must(plug.Registry.RegisterCommand("demo", &Command{})) } diff --git a/base/commands/demo/demo_generate_data.go b/base/commands/demo/demo_generate_data.go index df142b05a..ba425e4b3 100644 --- a/base/commands/demo/demo_generate_data.go +++ b/base/commands/demo/demo_generate_data.go @@ -13,8 +13,10 @@ import ( "github.com/hazelcast/hazelcast-go-client/serialization" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/sql" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/demo" "github.com/hazelcast/hazelcast-commandline-client/internal/demo/wikimedia" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -31,20 +33,9 @@ const ( argTitleKeyValues = "key=value" ) -type DataStreamGenerator interface { - Stream(ctx context.Context) (chan demo.StreamItem, context.CancelFunc) - MappingQuery(mapName string) (string, error) -} - -var supportedEventStreams = map[string]DataStreamGenerator{ - "wikipedia-event-stream": wikimedia.StreamGenerator{}, -} +type GenerateDataCommand struct{} -type GenerateDataCmd struct{} - -func (cm GenerateDataCmd) Unwrappable() {} - -func (cm GenerateDataCmd) Init(cc plug.InitContext) error { +func (GenerateDataCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("generate-data") long := `Generates a stream of events @@ -64,23 +55,21 @@ Generate data for given name, supported names are: return nil } -func (cm GenerateDataCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (GenerateDataCommand) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.GetStringArg(argGeneratorName) generator, ok := supportedEventStreams[name] if !ok { return fmt.Errorf("stream generator '%s' is not supported, run --help to see supported ones", name) } kvs := ec.GetKeyValuesArg(argKeyValues) - ch, stopStream := generator.Stream(ctx) - defer stopStream() preview := ec.Props().GetBool(flagPreview) if preview { - return generatePreviewResult(ctx, ec, generator, ch, kvs.Map(), stopStream) + return generatePreviewResult(ctx, ec, generator, kvs.Map()) } - return generateResult(ctx, ec, generator, ch, kvs.Map(), stopStream) + return generateResult(ctx, ec, generator, kvs.Map()) } -func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator DataStreamGenerator, itemCh <-chan demo.StreamItem, keyVals map[string]string, stopStream context.CancelFunc) error { +func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator dataStreamGenerator, keyVals map[string]string) error { maxCount := ec.Props().GetInt(flagMaxValues) if maxCount < 1 { maxCount = 10 @@ -89,38 +78,17 @@ func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator D if mapName == "" { mapName = "" } - mq, err := generator.MappingQuery(mapName) + mq, err := generator.GenerateMappingQuery(mapName) if err != nil { return err } + itemCh, stopStream := generator.Stream(ctx) + defer stopStream() ec.PrintlnUnnecessary(fmt.Sprintf("Following mapping will be created when run without preview:\n\n%s", mq)) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + _, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("") outCh := make(chan output.Row) - count := int64(0) - go func() { - loop: - for count < maxCount { - var ev demo.StreamItem - select { - case event, ok := <-itemCh: - if !ok { - break loop - } - ev = event - case <-ctx.Done(): - break loop - } - select { - case outCh <- ev.Row(): - case <-ctx.Done(): - break loop - } - count++ - } - close(outCh) - stopStream() - }() + go feedPreviewItems(ctx, maxCount, outCh, itemCh) return nil, ec.AddOutputStream(ctx, outCh) }) if err != nil { @@ -130,24 +98,28 @@ func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator D return nil } -func generateResult(ctx context.Context, ec plug.ExecContext, generator DataStreamGenerator, itemCh <-chan demo.StreamItem, keyVals map[string]string, stopStream context.CancelFunc) error { +func generateResult(ctx context.Context, ec plug.ExecContext, generator dataStreamGenerator, keyVals map[string]string) error { mapName, ok := keyVals[pairMapName] if !ok { return fmt.Errorf("either %s key-value pair must be given or --preview must be used", pairMapName) } - m, err := getMap(ctx, ec, mapName) - if err != nil { - return err - } - query, err := generator.MappingQuery(mapName) - if err != nil { - return err - } - err = runMappingQuery(ctx, ec, mapName, query) + maxCount := ec.Props().GetInt(flagMaxValues) + query, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (string, error) { + sp.SetText("Creating the mapping") + query, err := generator.GenerateMappingQuery(mapName) + if err != nil { + return "", err + } + if _, err := sql.ExecSQL(ctx, ec, query); err != nil { + return "", err + } + return query, nil + }) if err != nil { return err } - ec.PrintlnUnnecessary(fmt.Sprintf("Following mapping is created:\n\n%s", query)) + stop() + ec.PrintlnUnnecessary(fmt.Sprintf("OK Following mapping is created:\n\n%s", query)) ec.PrintlnUnnecessary(fmt.Sprintf(`Run the following SQL query to see the generated data SELECT @@ -155,45 +127,21 @@ func generateResult(ctx context.Context, ec plug.ExecContext, generator DataStre FROM "%s" LIMIT 10; -`, m.Name())) - maxCount := ec.Props().GetInt(flagMaxValues) - count := int64(0) - errCh := make(chan error) - go func() { - loop: - for { - var ev demo.StreamItem - select { - case event, ok := <-itemCh: - if !ok { - errCh <- nil - break loop - } - ev = event - case <-ctx.Done(): - errCh <- ctx.Err() - break loop - } - fm := ev.KeyValues() - b, err := json.Marshal(fm) - if err != nil { - ec.Logger().Warn("Could not marshall stream item: %s", err.Error()) - continue - } - _, err = m.Put(ctx, ev.ID(), serialization.JSON(b)) - if err != nil { - ec.Logger().Warn("Could not put stream item into map %s: %s", m.Name(), err.Error()) - continue - } - atomic.AddInt64(&count, 1) - if maxCount > 0 && atomic.LoadInt64(&count) == maxCount { - errCh <- nil - break - } +`, mapName)) + var count int64 + _, stop, err = cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (any, error) { + errCh := make(chan error) + itemCh, stopStream := generator.Stream(ctx) + defer stopStream() + ci, err := ec.ClientInternal(ctx) + if err != nil { + return 0, err } - close(errCh) - }() - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + m, err := ci.Client().GetMap(ctx, mapName) + if err != nil { + return 0, err + } + go feedResultItems(ctx, ec, m, maxCount, itemCh, errCh, &count) ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { @@ -205,46 +153,98 @@ func generateResult(ctx context.Context, ec plug.ExecContext, generator DataStre } } }) + if err != nil { + if !hzerrors.IsUserCancelled(err) && !hzerrors.IsTimeout(err) { + return err + } + } stop() - stopStream() - ec.PrintlnUnnecessary(fmt.Sprintf("OK Generated %d events", atomic.LoadInt64(&count))) - return err + msg := fmt.Sprintf("OK Generated %d events.", atomic.LoadInt64(&count)) + ec.PrintlnUnnecessary(msg) + return nil } -func runMappingQuery(ctx context.Context, ec plug.ExecContext, mapName, query string) error { - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Creating mapping for map: %s", mapName)) - _, cancel, err := sql.ExecSQL(ctx, ec, query) +func feedResultItems(ctx context.Context, ec plug.ExecContext, m *hazelcast.Map, maxCount int64, itemCh <-chan demo.StreamItem, errCh chan<- error, outCount *int64) { +loop: + for { + var ev demo.StreamItem + select { + case event, ok := <-itemCh: + if !ok { + errCh <- nil + break loop + } + ev = event + case <-ctx.Done(): + errCh <- ctx.Err() + break loop + } + fm := ev.KeyValues() + b, err := json.Marshal(fm) if err != nil { - return nil, err + ec.Logger().Warn("Could not marshall stream item: %s", err.Error()) + continue } - cancel() - return nil, nil - }) - stop() - return err + _, err = m.Put(ctx, ev.ID(), serialization.JSON(b)) + if err != nil { + ec.Logger().Warn("Could not put stream item into map %s: %s", m.Name(), err.Error()) + continue + } + atomic.AddInt64(outCount, 1) + if maxCount > 0 && atomic.LoadInt64(outCount) == maxCount { + errCh <- nil + break + } + } + close(errCh) } -func getMap(ctx context.Context, ec plug.ExecContext, mapName string) (*hazelcast.Map, error) { - ci, err := ec.ClientInternal(ctx) +func feedPreviewItems(ctx context.Context, maxCount int64, outCh chan<- output.Row, itemCh <-chan demo.StreamItem) { + var count int64 +loop: + for count < maxCount { + var ev demo.StreamItem + select { + case event, ok := <-itemCh: + if !ok { + break loop + } + ev = event + case <-ctx.Done(): + break loop + } + select { + case outCh <- ev.Row(): + case <-ctx.Done(): + break loop + } + count++ + } + close(outCh) +} + +type dataStreamGenerator interface { + Stream(ctx context.Context) (chan demo.StreamItem, context.CancelFunc) + GenerateMappingQuery(mapName string) (string, error) +} + +var supportedEventStreams = map[string]dataStreamGenerator{ + "wikipedia-event-stream": wikimedia.StreamGenerator{}, +} + +func getMap(ctx context.Context, ec plug.ExecContext, sp clc.Spinner, mapName string) (*hazelcast.Map, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - mv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting map %s", mapName)) - m, err := ci.Client().GetMap(ctx, mapName) - if err != nil { - return nil, err - } - return m, nil - }) + sp.SetText(fmt.Sprintf("Getting Map '%s'", mapName)) + m, err := ci.Client().GetMap(ctx, mapName) if err != nil { return nil, err } - stop() - return mv.(*hazelcast.Map), nil + return m, nil } func init() { - Must(plug.Registry.RegisterCommand("demo:generate-data", &GenerateDataCmd{})) + check.Must(plug.Registry.RegisterCommand("demo:generate-data", &GenerateDataCommand{})) } diff --git a/base/commands/demo/demo_map_set_many.go b/base/commands/demo/demo_map_set_many.go index 6fa15c444..5e7eb1be1 100644 --- a/base/commands/demo/demo_map_set_many.go +++ b/base/commands/demo/demo_map_set_many.go @@ -18,8 +18,6 @@ import ( type MapSetManyCmd struct{} -func (m MapSetManyCmd) Unwrappable() {} - const ( flagName = "name" flagSize = "size" diff --git a/base/commands/home.go b/base/commands/home.go index 27c064494..f789aea10 100644 --- a/base/commands/home.go +++ b/base/commands/home.go @@ -8,7 +8,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" @@ -21,7 +21,7 @@ const ( type HomeCommand struct{} -func (hc HomeCommand) Init(cc plug.InitContext) error { +func (HomeCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("home") short := "Print the CLC home directory" long := `Print the CLC home directory @@ -37,7 +37,7 @@ Example: return nil } -func (hc HomeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (HomeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { path := paths.Home() args := ec.GetStringSliceArg(argSubPath) if len(args) > 0 { @@ -52,8 +52,6 @@ func (hc HomeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { }) } -func (HomeCommand) Unwrappable() {} - func init() { - Must(plug.Registry.RegisterCommand("home", &HomeCommand{})) + check.Must(plug.Registry.RegisterCommand("home", &HomeCommand{})) } diff --git a/base/commands/job/common.go b/base/commands/job/common.go index 1badd7cd7..bfaae82ab 100644 --- a/base/commands/job/common.go +++ b/base/commands/job/common.go @@ -55,7 +55,7 @@ func idToString(id int64) string { return string(buf[:]) } -func terminateJob(ctx context.Context, ec plug.ExecContext, tm int32, cm TerminateCmd) error { +func terminateJob(ctx context.Context, ec plug.ExecContext, tm int32, cm TerminateCommand) error { nameOrID := ec.GetStringArg(argJobID) stages := []stage.Stage[any]{ stage.MakeConnectStage[any](ec), diff --git a/base/commands/job/job.go b/base/commands/job/job.go index 9a57dc02d..e34b988a1 100644 --- a/base/commands/job/job.go +++ b/base/commands/job/job.go @@ -10,22 +10,22 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type Cmd struct{} +type Command struct{} -func (cm Cmd) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("job") cc.AddCommandGroup(clc.GroupJetID, clc.GroupJetTitle) cc.SetCommandGroup(clc.GroupJetID) cc.SetTopLevel(true) help := "Jet job operations" - cc.SetCommandUsage("job [command]") cc.SetCommandHelp(help, help) return nil } -func (cm Cmd) Exec(context.Context, plug.ExecContext) error { +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - Must(plug.Registry.RegisterCommand("job", &Cmd{})) + Must(plug.Registry.RegisterCommand("job", &Command{})) } diff --git a/base/commands/job/job_export_snapshot.go b/base/commands/job/job_export_snapshot.go index 04cfb7981..55cd0b153 100644 --- a/base/commands/job/job_export_snapshot.go +++ b/base/commands/job/job_export_snapshot.go @@ -9,14 +9,17 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ExportSnapshotCmd struct{} +type ExportSnapshotCommand struct{} -func (cm ExportSnapshotCmd) Init(cc plug.InitContext) error { +func (ExportSnapshotCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("export-snapshot") long := "Exports a snapshot for a job.\nThis feature requires a Viridian or Hazelcast Enterprise cluster." short := "Exports a snapshot for a job" @@ -27,18 +30,18 @@ func (cm ExportSnapshotCmd) Init(cc plug.InitContext) error { return nil } -func (cm ExportSnapshotCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } +func (ExportSnapshotCommand) Exec(ctx context.Context, ec plug.ExecContext) error { var jm *JobsInfo var jid int64 var ok bool jobNameOrID := ec.GetStringArg(argJobID) name := ec.Props().GetString(flagName) cancel := ec.Props().GetBool(flagCancel) - nameV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + row, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (output.Row, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } j := jet.New(ci, sp, ec.Logger()) jis, err := j.GetJobList(ctx) if err != nil { @@ -73,16 +76,25 @@ func (cm ExportSnapshotCmd) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Exporting snapshot: %s", name)) - return name, j.ExportSnapshot(ctx, jid, name, cancel) + sp.SetText(fmt.Sprintf("Exporting snapshot '%s'", name)) + if err := j.ExportSnapshot(ctx, jid, name, cancel); err != nil { + return nil, err + } + row := output.Row{ + { + Name: "Name", + Type: serialization.TypeString, + Value: name, + }, + } + return row, nil }) if err != nil { return err } stop() - name = nameV.(string) - ec.PrintlnUnnecessary(fmt.Sprintf("OK Exported snapshot %s", name)) - return nil + ec.PrintlnUnnecessary("OK Exported the snapshot.\n") + return ec.AddOutputRows(ctx, row) } func autoGenerateSnapshotName(jobName string) string { @@ -92,5 +104,5 @@ func autoGenerateSnapshotName(jobName string) string { } func init() { - Must(plug.Registry.RegisterCommand("job:export-snapshot", &ExportSnapshotCmd{})) + Must(plug.Registry.RegisterCommand("job:export-snapshot", &ExportSnapshotCommand{})) } diff --git a/base/commands/job/job_it_test.go b/base/commands/job/job_it_test.go index 49877946c..9de9f9573 100644 --- a/base/commands/job/job_it_test.go +++ b/base/commands/job/job_it_test.go @@ -42,7 +42,7 @@ func submit_NonInteractiveTest(t *testing.T) { defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) }) } @@ -58,7 +58,7 @@ func submit_InteractiveTest(t *testing.T) { defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) }) }) @@ -71,7 +71,7 @@ func list_NonInteractiveTest(t *testing.T) { name := it.NewUniqueObjectName("job") tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--retries", "0", "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -93,7 +93,7 @@ func list_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--retries", "0", "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.WriteStdinString("\\job list\n") @@ -115,14 +115,14 @@ func suspendResume_NonInteractiveTest(t *testing.T) { name := it.NewUniqueObjectName("job") tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "suspend", name) - tcx.AssertStdoutContains("OK [2/2] Started") + tcx.AssertStdoutContains("OK [2/2] Started") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -130,7 +130,7 @@ func suspendResume_NonInteractiveTest(t *testing.T) { }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "resume", name) - tcx.AssertStdoutContains("OK [2/2] Initiated") + tcx.AssertStdoutContains("OK [2/2] Initiated") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -147,14 +147,14 @@ func suspendResume_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "suspend", name, "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -162,7 +162,7 @@ func suspendResume_InteractiveTest(t *testing.T) { }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "resume", name, "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -179,14 +179,14 @@ func restart_NonInteractiveTest(t *testing.T) { name := it.NewUniqueObjectName("job") tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "restart", name, "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") @@ -203,14 +203,14 @@ func restart_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "submit", "--name", name, jobPath, "--wait") - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) defer func() { tcx.CLCExecute(ctx, "job", "cancel", name, "--wait") }() tcx.WithReset(func() { tcx.WriteStdinf("\\job restart %s --wait\n", name) - tcx.AssertStdoutContains("OK [3/3] Job") + tcx.AssertStdoutContains("OK [3/3] Job") }) tcx.WithReset(func() { tcx.CLCExecute(ctx, "job", "list") diff --git a/base/commands/job/job_list.go b/base/commands/job/job_list.go index 9029416c8..8e6185a5f 100644 --- a/base/commands/job/job_list.go +++ b/base/commands/job/job_list.go @@ -9,6 +9,7 @@ import ( "github.com/hazelcast/hazelcast-go-client/types" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -17,11 +18,9 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ListCmd struct{} +type ListCommand struct{} -func (cm ListCmd) Unwrappable() {} - -func (cm ListCmd) Init(cc plug.InitContext) error { +func (ListCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("list") help := "List jobs" cc.SetCommandHelp(help, help) @@ -30,24 +29,32 @@ func (cm ListCmd) Init(cc plug.InitContext) error { return nil } -func (cm ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - ls, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { +func (ListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + rows, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) ([]output.Row, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText("Getting the job list") j := jet.New(ci, sp, ec.Logger()) - return j.GetJobList(ctx) + jl, err := j.GetJobList(ctx) + if err != nil { + return nil, err + } + return createJetJobRows(ec, jl), nil }) if err != nil { return err } stop() - return outputJetJobs(ctx, ec, ls) + if len(rows) == 0 { + ec.PrintlnUnnecessary("OK No jobs found.") + return nil + } + return ec.AddOutputRows(ctx, rows...) } -func outputJetJobs(ctx context.Context, ec plug.ExecContext, lsi interface{}) error { +func createJetJobRows(ec plug.ExecContext, lsi interface{}) []output.Row { ls := lsi.([]control.JobAndSqlSummary) rows := make([]output.Row, 0, len(ls)) verbose := ec.Props().GetBool(clc.PropertyVerbose) @@ -114,10 +121,7 @@ func outputJetJobs(ctx context.Context, ec plug.ExecContext, lsi interface{}) er } rows = append(rows, row) } - if len(rows) == 0 { - ec.PrintlnUnnecessary("OK No jobs found.") - } - return ec.AddOutputRows(ctx, rows...) + return rows } func msToOffsetDateTimeColumn(ms int64, name string) output.Column { @@ -135,5 +139,5 @@ func msToOffsetDateTimeColumn(ms int64, name string) output.Column { } func init() { - Must(plug.Registry.RegisterCommand("job:list", &ListCmd{})) + Must(plug.Registry.RegisterCommand("job:list", &ListCommand{})) } diff --git a/base/commands/job/job_resume.go b/base/commands/job/job_resume.go index 9ca8fc359..b8c7e9e5d 100644 --- a/base/commands/job/job_resume.go +++ b/base/commands/job/job_resume.go @@ -13,11 +13,9 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type ResumeCmd struct{} +type ResumeCommand struct{} -func (cm ResumeCmd) Unwrappable() {} - -func (cm ResumeCmd) Init(cc plug.InitContext) error { +func (ResumeCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("resume") help := "Resumes a suspended job" cc.SetCommandHelp(help, help) @@ -26,7 +24,7 @@ func (cm ResumeCmd) Init(cc plug.InitContext) error { return nil } -func (cm ResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ResumeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { nameOrID := ec.GetStringArg(argJobID) stages := []stage.Stage[any]{ stage.MakeConnectStage[any](ec), @@ -74,5 +72,5 @@ func (cm ResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("job:resume", &ResumeCmd{})) + Must(plug.Registry.RegisterCommand("job:resume", &ResumeCommand{})) } diff --git a/base/commands/job/job_submit.go b/base/commands/job/job_submit.go index ba9d337cf..845483f3b 100644 --- a/base/commands/job/job_submit.go +++ b/base/commands/job/job_submit.go @@ -9,8 +9,6 @@ import ( "strings" "time" - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" @@ -29,11 +27,9 @@ const ( argTitleArg = "argument" ) -type SubmitCmd struct{} - -func (cm SubmitCmd) Unwrappable() {} +type SubmitCommand struct{} -func (cm SubmitCmd) Init(cc plug.InitContext) error { +func (SubmitCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("submit") long := fmt.Sprintf(`Submits a jar file to create a Jet job @@ -51,7 +47,7 @@ This command requires a Viridian or a Hazelcast cluster having version %s or new return nil } -func (cm SubmitCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (SubmitCommand) Exec(ctx context.Context, ec plug.ExecContext) error { path := ec.GetStringArg(argJarPath) if !paths.Exists(path) { return fmt.Errorf("file does not exist: %s", path) @@ -59,17 +55,10 @@ func (cm SubmitCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if !strings.HasSuffix(path, ".jar") { return fmt.Errorf("submitted file is not a jar file: %s", path) } - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { - return fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) - } - return submitJar(ctx, ci, ec, path) + return submitJar(ctx, ec, path) } -func submitJar(ctx context.Context, ci *hazelcast.ClientInternal, ec plug.ExecContext, path string) error { +func submitJar(ctx context.Context, ec plug.ExecContext, path string) error { wait := ec.Props().GetBool(flagWait) jobName := ec.Props().GetString(flagName) snapshot := ec.Props().GetString(flagSnapshot) @@ -92,12 +81,20 @@ func submitJar(ctx context.Context, ci *hazelcast.ClientInternal, ec plug.ExecCo SuccessMsg: "Submitted the job", FailureMsg: "Failed submitting the job", Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { + err := fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) + return nil, err + } j := jet.New(ci, status, ec.Logger()) - err := retry(tries, ec.Logger(), func(try int) error { + err = retry(tries, ec.Logger(), func(try int) error { if try == 0 { ec.Logger().Info("Submitting %s", jobName) } else { - ec.Logger().Info("Submitting %s, retry: %d", jobName, try) + ec.Logger().Info("Submitting %s, retry %d", jobName, try) } br := jet.CreateBinaryReaderForPath(path) return j.SubmitJob(ctx, path, jobName, className, snapshot, args, br) @@ -138,5 +135,5 @@ func retry(times int, lg log.Logger, f func(try int) error) error { } func init() { - Must(plug.Registry.RegisterCommand("job:submit", &SubmitCmd{})) + Must(plug.Registry.RegisterCommand("job:submit", &SubmitCommand{})) } diff --git a/base/commands/job/job_terminate.go b/base/commands/job/job_terminate.go index 51d6cc220..61169e987 100644 --- a/base/commands/job/job_terminate.go +++ b/base/commands/job/job_terminate.go @@ -11,7 +11,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type TerminateCmd struct { +type TerminateCommand struct { name string longHelp string shortHelp string @@ -23,9 +23,7 @@ type TerminateCmd struct { waitState int32 } -func (cm TerminateCmd) Unwrappable() {} - -func (cm TerminateCmd) Init(cc plug.InitContext) error { +func (cm TerminateCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage(cm.name) cc.SetCommandHelp(cm.longHelp, cm.shortHelp) cc.AddBoolFlag(flagForce, "", false, false, fmt.Sprintf("force %s the job", cm.name)) @@ -34,7 +32,7 @@ func (cm TerminateCmd) Init(cc plug.InitContext) error { return nil } -func (cm TerminateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm TerminateCommand) Exec(ctx context.Context, ec plug.ExecContext) error { tm := cm.terminateMode if ec.Props().GetBool(flagForce) { tm = cm.terminateModeForce @@ -46,37 +44,37 @@ func (cm TerminateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("job:cancel", &TerminateCmd{ + Must(plug.Registry.RegisterCommand("job:cancel", &TerminateCommand{ name: "cancel", longHelp: "Cancels the job with the given ID or name", shortHelp: "Cancels the job with the given ID or name", terminateMode: jet.TerminateModeCancelGraceful, terminateModeForce: jet.TerminateModeCancelForceful, waitState: jet.JobStatusFailed, - inProgressMsg: "Starting to cancel %s", - successMsg: "Started cancellation of %s", + inProgressMsg: "Starting to cancel '%s'", + successMsg: "Started cancellation of '%s'", failureMsg: "Failed to start job cancellation", })) - Must(plug.Registry.RegisterCommand("job:suspend", &TerminateCmd{ + Must(plug.Registry.RegisterCommand("job:suspend", &TerminateCommand{ name: "suspend", longHelp: "Suspends the job with the given ID or name", shortHelp: "Suspends the job with the given ID or name", terminateMode: jet.TerminateModeSuspendGraceful, terminateModeForce: jet.TerminateModeSuspendForceful, waitState: jet.JobStatusSuspended, - inProgressMsg: "Starting to suspend %s", - successMsg: "Started suspension of %s", + inProgressMsg: "Starting to suspend '%s'", + successMsg: "Started suspension of '%s'", failureMsg: "Failed to start job suspension", })) - Must(plug.Registry.RegisterCommand("job:restart", &TerminateCmd{ + Must(plug.Registry.RegisterCommand("job:restart", &TerminateCommand{ name: "restart", longHelp: "Restarts the job with the given ID or name", shortHelp: "Restarts the job with the given ID or name", terminateMode: jet.TerminateModeRestartGraceful, terminateModeForce: jet.TerminateModeRestartForceful, waitState: jet.JobStatusRunning, - inProgressMsg: "Initiating the restart of %s", - successMsg: "Initiated the restart of %s", + inProgressMsg: "Initiating the restart of '%s'", + successMsg: "Initiated the restart of '%s'", failureMsg: "Failed to initiate job restart", })) } diff --git a/base/commands/list/common.go b/base/commands/list/common.go index 3cc4d982c..8af5b295f 100644 --- a/base/commands/list/common.go +++ b/base/commands/list/common.go @@ -5,59 +5,27 @@ package list import ( "context" "fmt" - "strings" "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" - "github.com/hazelcast/hazelcast-commandline-client/internal/mk" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -func addValueTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(base.FlagValueType, "v", "string", false, help) -} - -func makeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { - vt := ec.Props().GetString(base.FlagValueType) - if vt == "" { - vt = "string" - } - value, err := mk.ValueFromString(valueStr, vt) - if err != nil { - return nil, err - } - return ci.EncodeData(value) -} - -func stringToPartitionID(ci *hazelcast.ClientInternal, name string) (int32, error) { - var partitionID int32 - var keyData hazelcast.Data - var err error - idx := strings.Index(name, "@") - if keyData, err = ci.EncodeData(name[idx+1:]); err != nil { - return 0, err - } - if partitionID, err = ci.GetPartitionID(keyData); err != nil { - return 0, err - } - return partitionID, nil -} - func getList(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.List, error) { name := ec.Props().GetString(base.FlagName) ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Getting list %s", name)) + sp.SetText(fmt.Sprintf("Getting List '%s'", name)) return ci.Client().GetList(ctx, name) } @@ -68,16 +36,16 @@ func removeFromList(ctx context.Context, ec plug.ExecContext, name string, index if err != nil { return nil, err } - pid, err := stringToPartitionID(ci, name) + pid, err := internal.StringToPartitionID(ci, name) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Removing value from list %s", name)) + sp.SetText(fmt.Sprintf("Removing value from List '%s'", name)) var req *hazelcast.ClientMessage if indexCall { req = codec.EncodeListRemoveWithIndexRequest(name, index) } else { - vd, err := makeValueData(ec, ci, valueStr) + vd, err := commands.MakeValueData(ec, ci, valueStr) if err != nil { return nil, err } @@ -124,7 +92,7 @@ func removeFromList(ctx context.Context, ec plug.ExecContext, name string, index return err } stop() - msg := fmt.Sprintf("OK List %s was updated.\n", name) + msg := fmt.Sprintf("OK List '%s' was updated.\n", name) ec.PrintlnUnnecessary(msg) row := rowV.(output.Row) return ec.AddOutputRows(ctx, row) diff --git a/base/commands/list/list.go b/base/commands/list/list.go index b7af813e5..818c7a42b 100644 --- a/base/commands/list/list.go +++ b/base/commands/list/list.go @@ -11,25 +11,24 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type ListCommand struct { -} +type Command struct{} -func (mc *ListCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("list") cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "list name") - cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") cc.SetTopLevel(true) - cc.SetCommandUsage("list") help := "List operations" cc.SetCommandHelp(help, help) + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "list name") + cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") return nil } -func (mc *ListCommand) Exec(context.Context, plug.ExecContext) error { +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - check.Must(plug.Registry.RegisterCommand("list", &ListCommand{})) + check.Must(plug.Registry.RegisterCommand("list", &Command{})) } diff --git a/base/commands/list/list_add.go b/base/commands/list/list_add.go index 69daa9f7c..1b3dfa5c5 100644 --- a/base/commands/list/list_add.go +++ b/base/commands/list/list_add.go @@ -9,8 +9,10 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,21 +20,19 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ListAddCommand struct{} +type AddCommand struct{} -func (mc *ListAddCommand) Unwrappable() {} - -func (mc *ListAddCommand) Init(cc plug.InitContext) error { +func (AddCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("add") help := "Add a value in the given list" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) + commands.AddValueTypeFlag(cc) cc.AddIntFlag(flagIndex, "", -1, false, "index for the value") cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } -func (mc *ListAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) val, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { ci, err := cmd.ClientInternal(ctx, ec, sp) @@ -45,7 +45,7 @@ func (mc *ListAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { return nil, err } valueStr := ec.GetStringArg(base.ArgValue) - vd, err := makeValueData(ec, ci, valueStr) + vd, err := commands.MakeValueData(ec, ci, valueStr) if err != nil { return nil, err } @@ -56,11 +56,11 @@ func (mc *ListAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } else { req = codec.EncodeListAddRequest(name, vd) } - pid, err := stringToPartitionID(ci, name) + pid, err := internal.StringToPartitionID(ci, name) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Adding value at index %d into list %s", index, name)) + sp.SetText(fmt.Sprintf("Adding value at index %d into List '%s'", index, name)) resp, err := ci.InvokeOnPartition(ctx, req, pid, nil) if err != nil { return nil, err @@ -71,7 +71,7 @@ func (mc *ListAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { return err } stop() - msg := fmt.Sprintf("OK Updated list %s.\n", name) + msg := fmt.Sprintf("OK Updated List '%s'.\n", name) ec.PrintlnUnnecessary(msg) row := output.Row{ output.Column{ @@ -84,5 +84,5 @@ func (mc *ListAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("list:add", &ListAddCommand{})) + Must(plug.Registry.RegisterCommand("list:add", &AddCommand{})) } diff --git a/base/commands/list/list_clear.go b/base/commands/list/list_clear.go index 09175b47b..7ee632bb4 100644 --- a/base/commands/list/list_clear.go +++ b/base/commands/list/list_clear.go @@ -3,62 +3,12 @@ package list import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" - - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) -type ListClearCommand struct{} - -func (mc *ListClearCommand) Unwrappable() {} - -func (mc *ListClearCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("clear") - help := "Delete all entries of a List" - cc.SetCommandHelp(help, help) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - return nil -} - -func (mc *ListClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("List content will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - l, err := getList(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Clearing list %s", l.Name())) - if err := l.Clear(ctx); err != nil { - return nil, err - } - return l.Name(), nil - }) - if err != nil { - return err - } - stop() - msg := fmt.Sprintf("OK Cleared list %s", name) - ec.PrintlnUnnecessary(msg) - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("list:clear", &ListClearCommand{})) + cmd := commands.NewClearCommand("List", getList) + check.Must(plug.Registry.RegisterCommand("list:clear", cmd)) } diff --git a/base/commands/list/list_contains.go b/base/commands/list/list_contains.go index cecfefff3..2a2f3314f 100644 --- a/base/commands/list/list_contains.go +++ b/base/commands/list/list_contains.go @@ -7,8 +7,10 @@ import ( "fmt" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -18,13 +20,11 @@ import ( type ListContainsCommand struct{} -func (mc *ListContainsCommand) Unwrappable() {} - func (mc *ListContainsCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("contains") help := "Check if the value is present in the list" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) + commands.AddValueTypeFlag(cc) cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } @@ -42,15 +42,15 @@ func (mc *ListContainsCommand) Exec(ctx context.Context, ec plug.ExecContext) er return nil, err } valueStr := ec.GetStringArg(base.ArgValue) - vd, err := makeValueData(ec, ci, valueStr) + vd, err := commands.MakeValueData(ec, ci, valueStr) if err != nil { return nil, err } - pid, err := stringToPartitionID(ci, name) + pid, err := internal.StringToPartitionID(ci, name) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Checking if value exists in the list %s", name)) + sp.SetText(fmt.Sprintf("Checking if value exists in the List '%s'", name)) req := codec.EncodeListContainsRequest(name, vd) resp, err := ci.InvokeOnPartition(ctx, req, pid, nil) if err != nil { diff --git a/base/commands/list/list_destroy.go b/base/commands/list/list_destroy.go index 9dc612a7c..1fd6b75a7 100644 --- a/base/commands/list/list_destroy.go +++ b/base/commands/list/list_destroy.go @@ -3,64 +3,12 @@ package list import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type ListDestroyCommand struct{} - -func (mc *ListDestroyCommand) Unwrappable() {} - -func (mc *ListDestroyCommand) Init(cc plug.InitContext) error { - long := `Destroy a List - -This command will delete the List and the data in it will not be available anymore.` - cc.SetCommandUsage("destroy") - short := "Destroy a List" - cc.SetCommandHelp(long, short) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - return nil -} - -func (mc *ListDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("List will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - l, err := getList(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Destroying list %s", l.Name())) - if err := l.Destroy(ctx); err != nil { - return nil, err - } - return l.Name(), nil - }) - if err != nil { - return err - } - stop() - msg := fmt.Sprintf("OK Destroyed list %s.", name) - ec.PrintlnUnnecessary(msg) - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("list:destroy", &ListDestroyCommand{})) + cmd := commands.NewDestroyCommand("List", getList) + check.Must(plug.Registry.RegisterCommand("list:destroy", cmd)) } diff --git a/base/commands/list/list_remove_index.go b/base/commands/list/list_remove_index.go index 4ccb827e5..7ee03f43b 100644 --- a/base/commands/list/list_remove_index.go +++ b/base/commands/list/list_remove_index.go @@ -14,8 +14,6 @@ import ( type ListRemoveIndexCommand struct{} -func (mc *ListRemoveIndexCommand) Unwrappable() {} - func (mc *ListRemoveIndexCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("remove-index") help := "Remove the value at the given index in the list" diff --git a/base/commands/list/list_remove_value.go b/base/commands/list/list_remove_value.go index 00bc5ef23..3276fe9dd 100644 --- a/base/commands/list/list_remove_value.go +++ b/base/commands/list/list_remove_value.go @@ -6,19 +6,18 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) type ListRemoveValueCommand struct{} -func (l ListRemoveValueCommand) Unwrappable() {} - func (mc *ListRemoveValueCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("remove-value") help := "Remove a value from the given list" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) + commands.AddValueTypeFlag(cc) cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } diff --git a/base/commands/list/list_set.go b/base/commands/list/list_set.go index 09ad4ef8b..d58c8ab38 100644 --- a/base/commands/list/list_set.go +++ b/base/commands/list/list_set.go @@ -7,8 +7,10 @@ import ( "fmt" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -17,13 +19,11 @@ import ( type ListSetCommand struct{} -func (mc *ListSetCommand) Unwrappable() {} - func (mc *ListSetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("set") help := "Set a value at the given index in the list" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) + commands.AddValueTypeFlag(cc) cc.AddInt64Arg(argIndex, argTitleIndex) cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil @@ -43,15 +43,15 @@ func (mc *ListSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } - vd, err := makeValueData(ec, ci, valueStr) + vd, err := commands.MakeValueData(ec, ci, valueStr) if err != nil { return nil, err } - pid, err := stringToPartitionID(ci, name) + pid, err := internal.StringToPartitionID(ci, name) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Setting the value of the list %s", name)) + sp.SetText(fmt.Sprintf("Setting the value of the List '%s'", name)) req := codec.EncodeListSetRequest(name, int32(index), vd) resp, err := ci.InvokeOnPartition(ctx, req, pid, nil) if err != nil { @@ -64,6 +64,7 @@ func (mc *ListSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { return err } stop() + ec.PrintlnUnnecessary("OK Set the value in the List.\n") return ec.AddOutputRows(ctx, rowV.(output.Row)) } diff --git a/base/commands/list/list_size.go b/base/commands/list/list_size.go index c785bedb7..f2b0ee2b8 100644 --- a/base/commands/list/list_size.go +++ b/base/commands/list/list_size.go @@ -3,51 +3,12 @@ package list import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ListSizeCommand struct{} - -func (mc *ListSizeCommand) Unwrappable() {} - -func (mc *ListSizeCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("size") - help := "Return the size of the given List" - cc.SetCommandHelp(help, help) - return nil -} - -func (mc *ListSizeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(base.FlagName) - sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - l, err := getList(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Getting the size of list %s", name)) - return l.Size(ctx) - }) - if err != nil { - return err - } - stop() - return ec.AddOutputRows(ctx, output.Row{ - { - Name: "Size", - Type: serialization.TypeInt32, - Value: int32(sv.(int)), - }, - }) -} - func init() { - Must(plug.Registry.RegisterCommand("list:size", &ListSizeCommand{})) + cmd := commands.NewSizeCommand("List", getList) + check.Must(plug.Registry.RegisterCommand("list:size", cmd)) } diff --git a/base/commands/map/common.go b/base/commands/map/common.go index 4edf5c9a9..15d962d72 100644 --- a/base/commands/map/common.go +++ b/base/commands/map/common.go @@ -5,70 +5,63 @@ package _map import ( "context" "fmt" - "strings" "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - "github.com/hazelcast/hazelcast-commandline-client/internal" - "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -func addKeyTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("key type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(mapFlagKeyType, "k", "string", false, help) -} - -func addValueTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(mapFlagValueType, "v", "string", false, help) -} - -func makeKeyData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr string) (hazelcast.Data, error) { - kt := ec.Props().GetString(mapFlagKeyType) - if kt == "" { - kt = "string" - } - key, err := mk.ValueFromString(keyStr, kt) - if err != nil { - return nil, err - } - return ci.EncodeData(key) -} - -func makeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { - vt := ec.Props().GetString(mapFlagValueType) - if vt == "" { - vt = "string" - } - value, err := mk.ValueFromString(valueStr, vt) +func getMap(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.Map, error) { + name := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - return ci.EncodeData(value) + sp.SetText(fmt.Sprintf("Getting Map '%s'", name)) + return ci.Client().GetMap(ctx, name) } -func makeKeyValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr, valueStr string) (hazelcast.Data, hazelcast.Data, error) { - kd, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return nil, nil, err - } - vd, err := makeValueData(ec, ci, valueStr) - if err != nil { - return nil, nil, err +func makeDecodeResponseRowsFunc(decoder func(*hazelcast.ClientMessage) hazelcast.Data) func(context.Context, plug.ExecContext, *hazelcast.ClientMessage) ([]output.Row, error) { + return func(ctx context.Context, ec plug.ExecContext, res *hazelcast.ClientMessage) ([]output.Row, error) { + key := ec.GetStringArg(commands.ArgKey) + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + data := decoder(res) + vt := data.Type() + value, err := ci.DecodeData(data) + if err != nil { + ec.Logger().Info("The value for %s was not decoded, due to error: %s", key, err.Error()) + value = serialization.NondecodedType(serialization.TypeToLabel(vt)) + } + row := output.Row{ + output.Column{ + Name: output.NameValue, + Type: vt, + Value: value, + }, + } + if ec.Props().GetBool(base.FlagShowType) { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(vt), + }) + } + return []output.Row{row}, nil } - return kd, vd, nil } -func getMap(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.Map, error) { - name := ec.Props().GetString(base.FlagName) - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err +func getMaxIdle(ec plug.ExecContext) int64 { + if _, ok := ec.Props().Get(mapMaxIdle); ok { + return ec.Props().GetInt(mapMaxIdle) } - sp.SetText(fmt.Sprintf("Getting map %s", name)) - return ci.Client().GetMap(ctx, name) + return clc.TTLUnset } diff --git a/base/commands/map/const.go b/base/commands/map/const.go index f253df95f..ad7a0ccbe 100644 --- a/base/commands/map/const.go +++ b/base/commands/map/const.go @@ -3,14 +3,6 @@ package _map const ( - mapFlagKeyType = "key-type" - mapFlagValueType = "value-type" - mapFlagReplace = "replace" - mapTTL = "ttl" - mapMaxIdle = "max-idle" - // TODO: move - ttlUnset = -1 - defaultMapName = "default" - argKey = "key" - argTitleKey = "key" + mapFlagReplace = "replace" + mapMaxIdle = "max-idle" ) diff --git a/base/commands/map/map.go b/base/commands/map/map.go index b43256b5e..83bf34856 100644 --- a/base/commands/map/map.go +++ b/base/commands/map/map.go @@ -7,32 +7,28 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type MapCommand struct{} +type Command struct{} -func (mc *MapCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { cc.SetCommandUsage("map") cc.SetTopLevel(true) cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) help := "Map operations" cc.SetCommandHelp(help, help) - cc.AddStringFlag(base.FlagName, "n", defaultMapName, false, "map name") + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "map name") cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") - if !cc.Interactive() { - cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") - } return nil } -func (mc *MapCommand) Exec(context.Context, plug.ExecContext) error { +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - Must(plug.Registry.RegisterCommand("map", &MapCommand{})) + Must(plug.Registry.RegisterCommand("map", &Command{})) } diff --git a/base/commands/map/map_clear.go b/base/commands/map/map_clear.go index baf330431..e9f900b9a 100644 --- a/base/commands/map/map_clear.go +++ b/base/commands/map/map_clear.go @@ -3,63 +3,12 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/errors" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" - - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) -type MapClearCommand struct{} - -func (mc *MapClearCommand) Unwrappable() {} - -func (mc *MapClearCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("clear") - help := "Delete all entries of a Map" - cc.SetCommandHelp(help, help) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - return nil -} - -func (mc *MapClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("Map content will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - m, err := getMap(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Clearing map %s", m.Name())) - if err := m.Clear(ctx); err != nil { - return nil, err - } - return m.Name(), nil - }) - if err != nil { - return err - } - stop() - msg := fmt.Sprintf("OK Cleared map: %s.", name) - ec.PrintlnUnnecessary(msg) - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("map:clear", &MapClearCommand{})) + cmd := commands.NewClearCommand("Map", getMap) + check.Must(plug.Registry.RegisterCommand("map:clear", cmd)) } diff --git a/base/commands/map/map_destroy.go b/base/commands/map/map_destroy.go index 3b2866251..0824758dc 100644 --- a/base/commands/map/map_destroy.go +++ b/base/commands/map/map_destroy.go @@ -3,64 +3,12 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type MapDestroyCommand struct{} - -func (mc *MapDestroyCommand) Unwrappable() {} - -func (mc *MapDestroyCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("destroy") - long := `Destroy a Map - -This command will delete the Map and the data in it will not be available anymore.` - short := "Destroy a Map" - cc.SetCommandHelp(long, short) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - return nil -} - -func (mc *MapDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("Map will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - name, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - m, err := getMap(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Destroying map %s", m.Name())) - if err := m.Destroy(ctx); err != nil { - return nil, err - } - return m.Name(), nil - }) - if err != nil { - return err - } - stop() - msg := fmt.Sprintf("OK Destroyed map %s.", name) - ec.PrintlnUnnecessary(msg) - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("map:destroy", &MapDestroyCommand{})) + cmd := commands.NewDestroyCommand("Map", getMap) + check.Must(plug.Registry.RegisterCommand("map:destroy", cmd)) } diff --git a/base/commands/map/map_entry_set.go b/base/commands/map/map_entry_set.go index ed7874dca..21610a6c9 100644 --- a/base/commands/map/map_entry_set.go +++ b/base/commands/map/map_entry_set.go @@ -3,60 +3,13 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) -type MapEntrySetCommand struct{} - -func (mc *MapEntrySetCommand) Unwrappable() {} - -func (mc *MapEntrySetCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("entry-set") - help := "Get all entries of a Map" - cc.SetCommandHelp(help, help) - return nil -} - -func (mc *MapEntrySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - showType := ec.Props().GetBool(base.FlagShowType) - rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - req := codec.EncodeMapEntrySetRequest(mapName) - sp.SetText(fmt.Sprintf("Getting entries of %s", mapName)) - resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) - if err != nil { - return nil, err - } - pairs := codec.DecodeMapEntrySetResponse(resp) - rows := output.DecodePairs(ci, pairs, showType) - return rows, nil - }) - if err != nil { - return err - } - stop() - rows := rowsV.([]output.Row) - if len(rows) == 0 { - ec.PrintlnUnnecessary("OK No entries found.") - return nil - } - return ec.AddOutputRows(ctx, rows...) -} - func init() { - Must(plug.Registry.RegisterCommand("map:entry-set", &MapEntrySetCommand{})) + c := commands.NewMapEntrySetCommand("Map", codec.EncodeMapEntrySetRequest, codec.DecodeMapEntrySetResponse) + check.Must(plug.Registry.RegisterCommand("map:entry-set", c)) } diff --git a/base/commands/map/map_get.go b/base/commands/map/map_get.go index b52a6130f..2eaba89d0 100644 --- a/base/commands/map/map_get.go +++ b/base/commands/map/map_get.go @@ -3,81 +3,13 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MapGetCommand struct{} - -func (mc *MapGetCommand) Unwrappable() {} - -func (mc *MapGetCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("get") - addKeyTypeFlag(cc) - help := "Get a value from the given Map" - cc.SetCommandHelp(help, help) - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (mc *MapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - keyStr := ec.GetStringArg(argKey) - showType := ec.Props().GetBool(base.FlagShowType) - rowV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Getting from map %s", mapName)) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return nil, err - } - req := codec.EncodeMapGetRequest(mapName, keyData, 0) - resp, err := ci.InvokeOnKey(ctx, req, keyData, nil) - if err != nil { - return nil, err - } - data := codec.DecodeMapGetResponse(resp) - vt := data.Type() - value, err := ci.DecodeData(data) - if err != nil { - ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) - value = serialization.NondecodedType(serialization.TypeToLabel(vt)) - } - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: vt, - Value: value, - }, - } - if showType { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(vt), - }) - } - return row, nil - }) - if err != nil { - return err - } - stop() - return ec.AddOutputRows(ctx, rowV.(output.Row)) -} - func init() { - Must(plug.Registry.RegisterCommand("map:get", &MapGetCommand{})) + c := commands.NewMapGetCommand("Map", codec.EncodeMapGetRequest, makeDecodeResponseRowsFunc(codec.DecodeMapGetResponse)) + check.Must(plug.Registry.RegisterCommand("map:get", c)) } diff --git a/base/commands/map/map_it_test.go b/base/commands/map/map_it_test.go index a9d36eec4..7c4bb0ea7 100644 --- a/base/commands/map/map_it_test.go +++ b/base/commands/map/map_it_test.go @@ -189,7 +189,7 @@ func keySet_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.WriteStdin([]byte(fmt.Sprintf("\\map -n %s key-set\n", m.Name()))) - tcx.AssertStdoutContains("No entries found.") + tcx.AssertStdoutContains("OK No keys found in Map") }) // set an entry tcx.WithReset(func() { diff --git a/base/commands/map/map_key_set.go b/base/commands/map/map_key_set.go index 0c678c9c6..bcdb67fbe 100644 --- a/base/commands/map/map_key_set.go +++ b/base/commands/map/map_key_set.go @@ -3,76 +3,13 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" - - "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) -type MapKeySetCommand struct{} - -func (mc *MapKeySetCommand) Unwrappable() {} - -func (mc *MapKeySetCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("key-set") - help := "Get all keys of a Map" - cc.SetCommandHelp(help, help) - return nil -} - -func (mc *MapKeySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - showType := ec.Props().GetBool(base.FlagShowType) - rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - req := codec.EncodeMapKeySetRequest(mapName) - sp.SetText(fmt.Sprintf("Getting keys of %s", mapName)) - resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) - if err != nil { - return nil, err - } - data := codec.DecodeMapKeySetResponse(resp) - var rows []output.Row - for _, r := range data { - var row output.Row - t := r.Type() - v, err := ci.DecodeData(*r) - if err != nil { - v = serialization.NondecodedType(serialization.TypeToLabel(t)) - } - row = append(row, output.NewKeyColumn(t, v)) - if showType { - row = append(row, output.NewKeyTypeColumn(t)) - } - rows = append(rows, row) - } - return rows, nil - }) - if err != nil { - return err - } - stop() - rows := rowsV.([]output.Row) - if len(rows) == 0 { - ec.PrintlnUnnecessary("OK No entries found.") - return nil - - } - return ec.AddOutputRows(ctx, rows...) -} - func init() { - Must(plug.Registry.RegisterCommand("map:key-set", &MapKeySetCommand{})) + c := commands.NewMapKeySetCommand("Map", codec.EncodeMapKeySetRequest, codec.DecodeMapKeySetResponse) + check.Must(plug.Registry.RegisterCommand("map:key-set", c)) } diff --git a/base/commands/map/map_load_all.go b/base/commands/map/map_load_all.go index 8afd19ba0..41c9ca06b 100644 --- a/base/commands/map/map_load_all.go +++ b/base/commands/map/map_load_all.go @@ -9,6 +9,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -18,31 +19,29 @@ import ( type MapLoadAllCommand struct{} -func (mc *MapLoadAllCommand) Unwrappable() {} - -func (mc *MapLoadAllCommand) Init(cc plug.InitContext) error { +func (MapLoadAllCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("load-all") long := `Load keys from map-store into the map If no key is given, all keys are loaded.` short := "Load keys from map-store into the map" cc.SetCommandHelp(long, short) - addKeyTypeFlag(cc) + commands.AddKeyTypeFlag(cc) cc.AddBoolFlag(mapFlagReplace, "", false, false, "replace keys if they exist in the map") - cc.AddStringSliceArg(argKey, argTitleKey, 0, clc.MaxArgs) + cc.AddStringSliceArg(commands.ArgKey, commands.ArgTitleKey, 0, clc.MaxArgs) return nil } -func (mc *MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) +func (MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } var keys []hazelcast.Data - for _, keyStr := range ec.GetStringSliceArg(argKey) { - keyData, err := makeKeyData(ec, ci, keyStr) + for _, keyStr := range ec.GetStringSliceArg(commands.ArgKey) { + keyData, err := commands.MakeKeyData(ec, ci, keyStr) if err != nil { return nil, err } @@ -51,11 +50,11 @@ func (mc *MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) erro replace := ec.Props().GetBool(mapFlagReplace) var req *hazelcast.ClientMessage if len(keys) == 0 { - req = codec.EncodeMapLoadAllRequest(mapName, replace) + req = codec.EncodeMapLoadAllRequest(name, replace) } else { - req = codec.EncodeMapLoadGivenKeysRequest(mapName, keys, replace) + req = codec.EncodeMapLoadGivenKeysRequest(name, keys, replace) } - sp.SetText(fmt.Sprintf("Loading keys into the map %s", mapName)) + sp.SetText(fmt.Sprintf("Loading keys into the Map '%s'", name)) if _, err = ci.InvokeOnRandomTarget(ctx, req, nil); err != nil { return nil, err } @@ -65,7 +64,7 @@ func (mc *MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) erro return err } stop() - msg := fmt.Sprintf("OK Loaded the keys into map %s", mapName) + msg := fmt.Sprintf("OK Loaded the keys into Map '%s'", name) ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_lock.go b/base/commands/map/map_lock.go index 4023f6660..8ccb3838e 100644 --- a/base/commands/map/map_lock.go +++ b/base/commands/map/map_lock.go @@ -3,65 +3,12 @@ package _map import ( - "context" - "fmt" - "time" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type MapLock struct{} - -func (mc *MapLock) Unwrappable() {} - -func (mc *MapLock) Init(cc plug.InitContext) error { - cc.SetCommandUsage("lock") - long := `Lock a key in the given Map - -This command is only available in the interactive mode.` - short := "Lock a key in the given Map" - cc.SetCommandHelp(long, short) - addKeyTypeFlag(cc) - cc.AddIntFlag(mapTTL, "", ttlUnset, false, "time-to-live (ms)") - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (mc *MapLock) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - m, err := getMap(ctx, ec, sp) - if err != nil { - return nil, err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Locking key in map %s", mapName)) - if ttl := GetTTL(ec); ttl != ttlUnset { - return nil, m.LockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) - } - return nil, m.Lock(ctx, keyData) - }) - if err != nil { - return err - } - stop() - msg := fmt.Sprintf("OK Locked the key in map %s", mapName) - ec.PrintlnUnnecessary(msg) - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("map:lock", &MapLock{}, plug.OnlyInteractive{})) + c := commands.NewLockCommand("Map", getMap) + check.Must(plug.Registry.RegisterCommand("map:lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/map/map_remove.go b/base/commands/map/map_remove.go index 059577ca8..f0bb5a86a 100644 --- a/base/commands/map/map_remove.go +++ b/base/commands/map/map_remove.go @@ -3,83 +3,13 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MapRemoveCommand struct{} - -func (mc *MapRemoveCommand) Unwrappable() {} - -func (mc *MapRemoveCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("remove") - help := "Remove a value from the given Map" - cc.SetCommandHelp(help, help) - addKeyTypeFlag(cc) - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (mc *MapRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - showType := ec.Props().GetBool(base.FlagShowType) - rowV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return nil, err - } - req := codec.EncodeMapRemoveRequest(mapName, keyData, 0) - sp.SetText(fmt.Sprintf("Removing from map %s", mapName)) - resp, err := ci.InvokeOnKey(ctx, req, keyData, nil) - if err != nil { - return nil, err - } - raw := codec.DecodeMapRemoveResponse(resp) - vt := raw.Type() - value, err := ci.DecodeData(raw) - if err != nil { - ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) - value = serialization.NondecodedType(serialization.TypeToLabel(vt)) - } - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: vt, - Value: value, - }, - } - if showType { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(vt), - }) - } - return row, nil - }) - if err != nil { - return err - } - stop() - msg := fmt.Sprintf("OK Removed the entry from map: %s.\n", mapName) - ec.PrintlnUnnecessary(msg) - return ec.AddOutputRows(ctx, rowV.(output.Row)) -} - func init() { - Must(plug.Registry.RegisterCommand("map:remove", &MapRemoveCommand{})) + c := commands.NewMapRemoveCommand("Map", codec.EncodeMapRemoveRequest, makeDecodeResponseRowsFunc(codec.DecodeMapRemoveResponse)) + check.Must(plug.Registry.RegisterCommand("map:remove", c)) } diff --git a/base/commands/map/map_set.go b/base/commands/map/map_set.go index 13a1fdae3..d00331ec5 100644 --- a/base/commands/map/map_set.go +++ b/base/commands/map/map_set.go @@ -9,6 +9,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -16,56 +17,48 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) -const ( - argValue = "value" - argTitleValue = "value" -) - type MapSetCommand struct{} -func (mc *MapSetCommand) Unwrappable() {} - -func (mc *MapSetCommand) Init(cc plug.InitContext) error { +func (MapSetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("set") help := "Set a value in the given Map" cc.SetCommandHelp(help, help) - addKeyTypeFlag(cc) - addValueTypeFlag(cc) - cc.AddIntFlag(mapTTL, "", ttlUnset, false, "time-to-live (ms)") - cc.AddIntFlag(mapMaxIdle, "", ttlUnset, false, "max idle (ms)") - cc.AddStringArg(argKey, argTitleKey) - cc.AddStringArg(argValue, argTitleValue) + commands.AddKeyTypeFlag(cc) + commands.AddValueTypeFlag(cc) + cc.AddIntFlag(commands.FlagTTL, "", clc.TTLUnset, false, "time-to-live (ms)") + cc.AddIntFlag(mapMaxIdle, "", clc.TTLUnset, false, "max idle (ms)") + cc.AddStringArg(commands.ArgKey, commands.ArgTitleKey) + cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } -func (mc *MapSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (MapSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { mapName := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - sp.SetText(fmt.Sprintf("Setting value into map %s", mapName)) + sp.SetText(fmt.Sprintf("Setting value into Map '%s'", mapName)) _, err = getMap(ctx, ec, sp) if err != nil { return nil, err } - keyStr := ec.GetStringArg(argKey) - valueStr := ec.GetStringArg(argValue) - kd, vd, err := makeKeyValueData(ec, ci, keyStr, valueStr) + key := ec.GetStringArg(commands.ArgKey) + value := ec.GetStringArg(base.ArgValue) + kd, vd, err := commands.MakeKeyValueData(ec, ci, key, value) if err != nil { return nil, err } - ttl := GetTTL(ec) - maxIdle := GetMaxIdle(ec) + ttl := commands.GetTTL(ec) + maxIdle := getMaxIdle(ec) var req *hazelcast.ClientMessage if maxIdle >= 0 { req = codec.EncodeMapSetWithMaxIdleRequest(mapName, kd, vd, 0, ttl, maxIdle) } else { req = codec.EncodeMapSetRequest(mapName, kd, vd, 0, ttl) } - _, err = ci.InvokeOnKey(ctx, req, kd, nil) - if err != nil { + if _, err = ci.InvokeOnKey(ctx, req, kd, nil); err != nil { return nil, err } return nil, nil @@ -74,7 +67,7 @@ func (mc *MapSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { return err } stop() - msg := fmt.Sprintf("OK Set the value into the map %s", mapName) + msg := fmt.Sprintf("OK Set the value into the Map '%s'.", mapName) ec.PrintlnUnnecessary(msg) return nil } diff --git a/base/commands/map/map_size.go b/base/commands/map/map_size.go index 044bf259e..26c822ec4 100644 --- a/base/commands/map/map_size.go +++ b/base/commands/map/map_size.go @@ -3,51 +3,12 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MapSizeCommand struct{} - -func (mc *MapSizeCommand) Unwrappable() {} - -func (mc *MapSizeCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("size") - help := "Return the size of the given Map" - cc.SetCommandHelp(help, help) - return nil -} - -func (mc *MapSizeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - m, err := getMap(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Getting the size of the map %s", mapName)) - return m.Size(ctx) - }) - if err != nil { - return err - } - stop() - return ec.AddOutputRows(ctx, output.Row{ - { - Name: "Size", - Type: serialization.TypeInt32, - Value: int32(sv.(int)), - }, - }) -} - func init() { - Must(plug.Registry.RegisterCommand("map:size", &MapSizeCommand{})) + cmd := commands.NewSizeCommand("Map", getMap) + check.Must(plug.Registry.RegisterCommand("map:size", cmd)) } diff --git a/base/commands/map/map_try_lock.go b/base/commands/map/map_try_lock.go index 1d075d4f5..c3d9bc95d 100644 --- a/base/commands/map/map_try_lock.go +++ b/base/commands/map/map_try_lock.go @@ -3,74 +3,12 @@ package _map import ( - "context" - "fmt" - "time" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MapTryLock struct{} - -func (mc *MapTryLock) Unwrappable() {} - -func (mc *MapTryLock) Init(cc plug.InitContext) error { - cc.SetCommandUsage("try-lock") - long := `Try to lock a key in the given map - -Returns the result without waiting for the lock to be unlocked. - -This command is only available in the interactive mode.` - short := "Try to lock a key in the given map" - cc.SetCommandHelp(long, short) - addKeyTypeFlag(cc) - cc.AddIntFlag(mapTTL, "", ttlUnset, false, "time-to-live (ms)") - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (mc *MapTryLock) Exec(ctx context.Context, ec plug.ExecContext) error { - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - mapName := ec.Props().GetString(base.FlagName) - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Locking key in map %s", mapName)) - m, err := getMap(ctx, ec, sp) - if err != nil { - return nil, err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return nil, err - } - if ttl := GetTTL(ec); ttl != ttlUnset { - return m.TryLockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) - } - return m.TryLock(ctx, keyData) - }) - if err != nil { - return err - } - stop() - locked := rv.(bool) - return ec.AddOutputRows(ctx, output.Row{ - { - Name: "Locked", - Type: serialization.TypeBool, - Value: locked, - }, - }) -} - func init() { - Must(plug.Registry.RegisterCommand("map:try-lock", &MapTryLock{}, plug.OnlyInteractive{})) + c := commands.NewTryLockCommand("Map", getMap) + check.Must(plug.Registry.RegisterCommand("map:try-lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/map/map_unlock.go b/base/commands/map/map_unlock.go index 5ad0b4ee4..1cc8d0318 100644 --- a/base/commands/map/map_unlock.go +++ b/base/commands/map/map_unlock.go @@ -3,59 +3,12 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type MapUnlock struct{} - -func (mc *MapUnlock) Unwrappable() {} - -func (mc *MapUnlock) Init(cc plug.InitContext) error { - cc.SetCommandUsage("unlock") - long := `Unlock a key in the given Map - -This command is only available in the interactive mode.` - short := "Unlock a key in the given Map" - cc.SetCommandHelp(long, short) - addKeyTypeFlag(cc) - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (mc *MapUnlock) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Unlocking key in map %s", mapName)) - m, err := getMap(ctx, ec, sp) - if err != nil { - return nil, err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return nil, err - } - return nil, m.Unlock(ctx, keyData) - }) - if err != nil { - return err - } - stop() - msg := fmt.Sprintf("OK Unlocked the key in map %s", mapName) - ec.PrintlnUnnecessary(msg) - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("map:unlock", &MapUnlock{}, plug.OnlyInteractive{})) + c := commands.NewMapUnlockCommand("Map", getMap) + check.Must(plug.Registry.RegisterCommand("map:unlock", c)) } diff --git a/base/commands/map/map_values.go b/base/commands/map/map_values.go index 5b98b67cd..b0bd093c3 100644 --- a/base/commands/map/map_values.go +++ b/base/commands/map/map_values.go @@ -3,73 +3,13 @@ package _map import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/base" - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MapValuesCommand struct{} - -func (mc *MapValuesCommand) Unwrappable() {} - -func (mc *MapValuesCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("values") - help := "Get all values of a Map" - cc.SetCommandHelp(help, help) - return nil -} - -func (mc *MapValuesCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mapName := ec.Props().GetString(base.FlagName) - showType := ec.Props().GetBool(base.FlagShowType) - rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Getting values of %s", mapName)) - req := codec.EncodeMapValuesRequest(mapName) - resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) - if err != nil { - return nil, err - } - data := codec.DecodeMapValuesResponse(resp) - var rows []output.Row - for _, r := range data { - var row output.Row - t := r.Type() - v, err := ci.DecodeData(*r) - if err != nil { - v = serialization.NondecodedType(serialization.TypeToLabel(t)) - } - row = append(row, output.NewValueColumn(t, v)) - if showType { - row = append(row, output.NewValueTypeColumn(t)) - } - rows = append(rows, row) - } - return rows, nil - }) - if err != nil { - return err - } - stop() - rows := rowsV.([]output.Row) - if len(rows) == 0 { - ec.PrintlnUnnecessary("OK the map has no values.") - return nil - } - return ec.AddOutputRows(ctx, rows...) -} - func init() { - Must(plug.Registry.RegisterCommand("map:values", &MapValuesCommand{})) + c := commands.NewMapValuesCommand("Map", codec.EncodeMapValuesRequest, codec.DecodeMapValuesResponse) + check.Must(plug.Registry.RegisterCommand("map:values", c)) } diff --git a/base/commands/map/util.go b/base/commands/map/util.go deleted file mode 100644 index e258cdf03..000000000 --- a/base/commands/map/util.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build std || map - -package _map - -import ( - "github.com/hazelcast/hazelcast-commandline-client/internal/plug" -) - -func GetTTL(ec plug.ExecContext) int64 { - if _, ok := ec.Props().Get(mapTTL); ok { - return ec.Props().GetInt(mapTTL) - } - return ttlUnset -} - -func GetMaxIdle(ec plug.ExecContext) int64 { - if _, ok := ec.Props().Get(mapMaxIdle); ok { - return ec.Props().GetInt(mapMaxIdle) - } - return ttlUnset -} diff --git a/base/commands/map_common.go b/base/commands/map_common.go new file mode 100644 index 000000000..cdf9182a9 --- /dev/null +++ b/base/commands/map_common.go @@ -0,0 +1,496 @@ +package commands + +import ( + "context" + "fmt" + "time" + + "github.com/hazelcast/hazelcast-go-client" + + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" +) + +type nameRequestEncodeFunc func(name string) *hazelcast.ClientMessage +type pairsResponseDecodeFunc func(message *hazelcast.ClientMessage) []hazelcast.Pair + +type MapEntrySetCommand struct { + typeName string + encoder nameRequestEncodeFunc + decoder pairsResponseDecodeFunc +} + +func NewMapEntrySetCommand(typeName string, encoder nameRequestEncodeFunc, decoder pairsResponseDecodeFunc) *MapEntrySetCommand { + return &MapEntrySetCommand{ + typeName: typeName, + encoder: encoder, + decoder: decoder, + } +} + +func (cm MapEntrySetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("entry-set") + help := fmt.Sprintf("Get all entries of a %s", cm.typeName) + cc.SetCommandHelp(help, help) + return nil +} + +func (cm MapEntrySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + showType := ec.Props().GetBool(base.FlagShowType) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + req := cm.encoder(name) + sp.SetText(fmt.Sprintf("Getting entries of %s", name)) + resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) + if err != nil { + return nil, err + } + pairs := cm.decoder(resp) + rows := output.DecodePairs(ci, pairs, showType) + return rows, nil + }) + if err != nil { + return err + } + stop() + return AddDDSRows(ctx, ec, cm.typeName, "entries", rowsV.([]output.Row)) +} + +type getRequestEncodeFunc func(name string, keyData hazelcast.Data, threadID int64) *hazelcast.ClientMessage +type getResponseDecodeFunc func(ctx context.Context, ec plug.ExecContext, res *hazelcast.ClientMessage) ([]output.Row, error) + +type MapGetCommand struct { + typeName string + encoder getRequestEncodeFunc + decoder getResponseDecodeFunc +} + +func NewMapGetCommand(typeName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc) *MapGetCommand { + return &MapGetCommand{ + typeName: typeName, + encoder: encoder, + decoder: decoder, + } +} + +func (cm MapGetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("get") + AddKeyTypeFlag(cc) + help := fmt.Sprintf("Get a value from the given %s", cm.typeName) + cc.SetCommandHelp(help, help) + cc.AddStringArg(ArgKey, ArgTitleKey) + return nil +} + +func (cm MapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + keyStr := ec.GetStringArg(ArgKey) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Getting from %s '%s'", cm.typeName, name)) + keyData, err := MakeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + req := cm.encoder(name, keyData, 0) + resp, err := ci.InvokeOnKey(ctx, req, keyData, nil) + if err != nil { + return nil, err + } + return cm.decoder(ctx, ec, resp) + }) + if err != nil { + return err + } + stop() + rows := rowsV.([]output.Row) + if len(rows) == 0 { + ec.PrintlnUnnecessary("OK No values.") + return nil + } + return ec.AddOutputRows(ctx, rowsV.([]output.Row)...) +} + +type dataSliceDecoderFunc func(message *hazelcast.ClientMessage) []*hazelcast.Data + +type MapKeySetCommand struct { + typeName string + encoder nameRequestEncodeFunc + decoder dataSliceDecoderFunc +} + +func NewMapKeySetCommand(typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc) *MapKeySetCommand { + return &MapKeySetCommand{ + typeName: typeName, + encoder: encoder, + decoder: decoder, + } +} + +func (cm MapKeySetCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("key-set") + help := fmt.Sprintf("Get all keys of a %s", cm.typeName) + cc.SetCommandHelp(help, help) + return nil +} + +func (cm MapKeySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + showType := ec.Props().GetBool(base.FlagShowType) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + req := cm.encoder(name) + sp.SetText(fmt.Sprintf("Getting keys of %s '%s'", cm.typeName, name)) + resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) + if err != nil { + return nil, err + } + data := cm.decoder(resp) + var rows []output.Row + for _, r := range data { + var row output.Row + t := r.Type() + v, err := ci.DecodeData(*r) + if err != nil { + v = serialization.NondecodedType(serialization.TypeToLabel(t)) + } + row = append(row, output.NewKeyColumn(t, v)) + if showType { + row = append(row, output.NewKeyTypeColumn(t)) + } + rows = append(rows, row) + } + return rows, nil + }) + if err != nil { + return err + } + stop() + return AddDDSRows(ctx, ec, cm.typeName, "keys", rowsV.([]output.Row)) +} + +type MapRemoveCommand struct { + typeName string + encoder getRequestEncodeFunc + decoder getResponseDecodeFunc +} + +func NewMapRemoveCommand(typeName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc) *MapRemoveCommand { + return &MapRemoveCommand{ + typeName: typeName, + encoder: encoder, + decoder: decoder, + } +} + +func (cm MapRemoveCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("remove") + AddKeyTypeFlag(cc) + help := fmt.Sprintf("Remove a value from the given %s", cm.typeName) + cc.SetCommandHelp(help, help) + cc.AddStringArg(ArgKey, ArgTitleKey) + return nil +} + +func (cm MapRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + keyStr := ec.GetStringArg(ArgKey) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Removing from %s '%s'", cm.typeName, name)) + keyData, err := MakeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + req := cm.encoder(name, keyData, 0) + resp, err := ci.InvokeOnKey(ctx, req, keyData, nil) + if err != nil { + return nil, err + } + return cm.decoder(ctx, ec, resp) + }) + if err != nil { + return err + } + stop() + msg := fmt.Sprintf("OK Removed the entry from %s '%s'.\n", cm.typeName, name) + ec.PrintlnUnnecessary(msg) + return ec.AddOutputRows(ctx, rowsV.([]output.Row)...) +} + +type Locker interface { + LockWithLease(ctx context.Context, key any, leaseTime time.Duration) error + Lock(ctx context.Context, key any) error +} + +type getLockerFunc[T Locker] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) + +type LockCommand[T Locker] struct { + typeName string + getFn getLockerFunc[T] +} + +func NewLockCommand[T Locker](typeName string, getFn getLockerFunc[T]) *LockCommand[T] { + return &LockCommand[T]{ + typeName: typeName, + getFn: getFn, + } +} + +func (cm LockCommand[T]) Init(cc plug.InitContext) error { + cc.SetCommandUsage("lock") + long := fmt.Sprintf(`Lock a key in the given %s + +This command is only available in the interactive mode.`, cm.typeName) + short := fmt.Sprintf("Lock a key in the given %s", cm.typeName) + cc.SetCommandHelp(long, short) + AddKeyTypeFlag(cc) + cc.AddIntFlag(FlagTTL, "", clc.TTLUnset, false, "time-to-live (ms)") + cc.AddStringArg(ArgKey, ArgTitleKey) + return nil +} + +func (cm LockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + m, err := cm.getFn(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(ArgKey) + keyData, err := MakeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Locking the key in %s '%s'", cm.typeName, name)) + if ttl := GetTTL(ec); ttl != clc.TTLUnset { + return nil, m.LockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) + } + return nil, m.Lock(ctx, keyData) + }) + if err != nil { + return err + } + stop() + msg := fmt.Sprintf("OK Locked the key in %s '%s'.", cm.typeName, name) + ec.PrintlnUnnecessary(msg) + return nil +} + +type LockTrier interface { + TryLockWithLease(ctx context.Context, key any, leaseTime time.Duration) (bool, error) + TryLock(ctx context.Context, key any) (bool, error) +} + +type getLockTrierFunc[T LockTrier] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) + +type MapTryLockCommand[T LockTrier] struct { + typeName string + getFn getLockTrierFunc[T] +} + +func NewTryLockCommand[T LockTrier](typeName string, getFn getLockTrierFunc[T]) *MapTryLockCommand[T] { + return &MapTryLockCommand[T]{ + typeName: typeName, + getFn: getFn, + } +} + +func (cm MapTryLockCommand[T]) Init(cc plug.InitContext) error { + cc.SetCommandUsage("try-lock") + long := fmt.Sprintf(`Try to lock a key in the given %s + +Returns the result without waiting for the lock to be unlocked. + +This command is only available in the interactive mode.`, cm.typeName) + short := fmt.Sprintf("Try to lock a key in the given %s", cm.typeName) + cc.SetCommandHelp(long, short) + AddKeyTypeFlag(cc) + cc.AddIntFlag(FlagTTL, "", clc.TTLUnset, false, "time-to-live (ms)") + cc.AddStringArg(ArgKey, ArgTitleKey) + return nil +} + +func (cm MapTryLockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + mapName := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Locking key in map %s", mapName)) + m, err := cm.getFn(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(ArgKey) + keyData, err := MakeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + var locked bool + if ttl := GetTTL(ec); ttl != clc.TTLUnset { + locked, err = m.TryLockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) + } else { + locked, err = m.TryLock(ctx, keyData) + } + row := output.Row{ + { + Name: "Locked", + Type: serialization.TypeBool, + Value: locked, + }, + } + if ec.Props().GetBool(base.FlagShowType) { + row = append(row, output.Column{ + Name: "Type", + Type: serialization.TypeString, + Value: serialization.TypeToLabel(serialization.TypeBool), + }) + } + return []output.Row{row}, nil + }) + if err != nil { + return err + } + stop() + return ec.AddOutputRows(ctx, rowsV.([]output.Row)...) +} + +type Unlocker interface { + Unlock(ctx context.Context, key any) error +} + +type getUnlockerFunc[T Unlocker] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) + +type MapUnlockCommand[T Unlocker] struct { + typeName string + getFn getUnlockerFunc[T] +} + +func NewMapUnlockCommand[T Unlocker](typeName string, getFn getUnlockerFunc[T]) *MapUnlockCommand[T] { + return &MapUnlockCommand[T]{ + typeName: typeName, + getFn: getFn, + } +} + +func (cm MapUnlockCommand[T]) Init(cc plug.InitContext) error { + cc.SetCommandUsage("unlock") + long := fmt.Sprintf(`Unlock a key in the given %s + +This command is only available in the interactive mode.`, cm.typeName) + short := fmt.Sprintf("Unlock a key in the given %s", cm.typeName) + cc.SetCommandHelp(long, short) + AddKeyTypeFlag(cc) + cc.AddStringArg(ArgKey, ArgTitleKey) + return nil +} + +func (cm MapUnlockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Unlocking key in %s '%s'", cm.typeName, name)) + m, err := cm.getFn(ctx, ec, sp) + if err != nil { + return nil, err + } + keyStr := ec.GetStringArg(ArgKey) + keyData, err := MakeKeyData(ec, ci, keyStr) + if err != nil { + return nil, err + } + return nil, m.Unlock(ctx, keyData) + }) + if err != nil { + return err + } + stop() + msg := fmt.Sprintf("OK Unlocked the key in %s '%s'.", cm.typeName, name) + ec.PrintlnUnnecessary(msg) + return nil +} + +type MapValuesCommand struct { + typeName string + encoder nameRequestEncodeFunc + decoder dataSliceDecoderFunc +} + +func NewMapValuesCommand(typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc) *MapValuesCommand { + return &MapValuesCommand{ + typeName: typeName, + encoder: encoder, + decoder: decoder, + } +} + +func (cm MapValuesCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("values") + help := fmt.Sprintf("Get all values of a %s", cm.typeName) + cc.SetCommandHelp(help, help) + return nil +} + +func (cm *MapValuesCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + showType := ec.Props().GetBool(base.FlagShowType) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Getting values of %s", name)) + req := cm.encoder(name) + resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) + if err != nil { + return nil, err + } + data := cm.decoder(resp) + var rows []output.Row + for _, r := range data { + var row output.Row + t := r.Type() + v, err := ci.DecodeData(*r) + if err != nil { + v = serialization.NondecodedType(serialization.TypeToLabel(t)) + } + row = append(row, output.NewValueColumn(t, v)) + if showType { + row = append(row, output.NewValueTypeColumn(t)) + } + rows = append(rows, row) + } + return rows, nil + }) + if err != nil { + return err + } + stop() + return AddDDSRows(ctx, ec, cm.typeName, "values", rowsV.([]output.Row)) +} diff --git a/base/commands/multimap/common.go b/base/commands/multimap/common.go index c86f3b739..8b3c694db 100644 --- a/base/commands/multimap/common.go +++ b/base/commands/multimap/common.go @@ -3,58 +3,62 @@ package multimap import ( + "context" "fmt" - "strings" "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/internal" - "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -func addKeyTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("key type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(multiMapFlagKeyType, "k", "string", false, help) -} - -func addValueTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(multiMapFlagValueType, "v", "string", false, help) -} - -func makeKeyData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr string) (hazelcast.Data, error) { - kt := ec.Props().GetString(multiMapFlagKeyType) - if kt == "" { - kt = "string" - } - key, err := mk.ValueFromString(keyStr, kt) - if err != nil { - return nil, err - } - return ci.EncodeData(key) -} - -func makeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { - vt := ec.Props().GetString(multiMapFlagValueType) - if vt == "" { - vt = "string" - } - value, err := mk.ValueFromString(valueStr, vt) +func getMultiMap(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.MultiMap, error) { + name := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - return ci.EncodeData(value) + sp.SetText(fmt.Sprintf("Getting MultiMap '%s'", name)) + return ci.Client().GetMultiMap(ctx, name) } -func makeKeyValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr, valueStr string) (hazelcast.Data, hazelcast.Data, error) { - kd, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return nil, nil, err - } - vd, err := makeValueData(ec, ci, valueStr) - if err != nil { - return nil, nil, err +func makeDecodeResponseRowsFunc(decoder func(*hazelcast.ClientMessage) []*hazelcast.Data) func(context.Context, plug.ExecContext, *hazelcast.ClientMessage) ([]output.Row, error) { + return func(ctx context.Context, ec plug.ExecContext, res *hazelcast.ClientMessage) ([]output.Row, error) { + key := ec.GetStringArg(commands.ArgKey) + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + var rows []output.Row + data := decoder(res) + for _, r := range data { + vt := r.Type() + value, err := ci.DecodeData(*r) + if err != nil { + ec.Logger().Info("The value for %s was not decoded, due to error: %s", key, err.Error()) + value = serialization.NondecodedType(serialization.TypeToLabel(vt)) + } + row := output.Row{ + output.Column{ + Name: output.NameValue, + Type: vt, + Value: value, + }, + } + if ec.Props().GetBool(base.FlagShowType) { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(vt), + }) + } + rows = append(rows, row) + } + return rows, nil } - return kd, vd, nil } diff --git a/base/commands/multimap/const.go b/base/commands/multimap/const.go deleted file mode 100644 index 6584a02bc..000000000 --- a/base/commands/multimap/const.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build std || multimap - -package multimap - -const ( - multiMapFlagKeyType = "key-type" - multiMapFlagValueType = "value-type" - defaultMultiMapName = "default" - multiMapTTL = "ttl" - ttlUnset = -1 - argKey = "key" - argTitleKey = "key" -) diff --git a/base/commands/multimap/multimap.go b/base/commands/multimap/multimap.go index 6eaf9b38f..59ca55e99 100644 --- a/base/commands/multimap/multimap.go +++ b/base/commands/multimap/multimap.go @@ -4,72 +4,31 @@ package multimap import ( "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - multiMapFlagName = "name" - multiMapFlagShowType = "show-type" - multiMapPropertyName = "multiMap" -) - -type MultiMapCommand struct { -} +type Command struct{} -func (m MultiMapCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("multi-map") cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(multiMapFlagName, "n", defaultMultiMapName, false, "multimap name") - cc.AddBoolFlag(multiMapFlagShowType, "", false, false, "add the type names to the output") - if !cc.Interactive() { - cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") - } cc.SetTopLevel(true) - cc.SetCommandUsage("multi-map [command] [flags]") help := "MultiMap operations" cc.SetCommandHelp(help, help) + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "MultiMap name") + cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") return nil } -func (m MultiMapCommand) Exec(context.Context, plug.ExecContext) error { - return nil -} - -func (m MultiMapCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { - ctx := context.TODO() - props.SetBlocking(multiMapPropertyName, func() (any, error) { - mmName := ec.Props().GetString(multiMapFlagName) - // empty multiMap name is allowed - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - mv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting multimap %s", mmName)) - m, err := ci.Client().GetMultiMap(ctx, mmName) - if err != nil { - return nil, err - } - return m, nil - }) - if err != nil { - return nil, err - } - stop() - return mv.(*hazelcast.MultiMap), nil - }) +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - cmd := &MultiMapCommand{} - Must(plug.Registry.RegisterCommand("multi-map", cmd)) - plug.Registry.RegisterAugmentor("20-multi-map", cmd) + check.Must(plug.Registry.RegisterCommand("multi-map", &Command{})) } diff --git a/base/commands/multimap/multimap_clear.go b/base/commands/multimap/multimap_clear.go index 274b1eba8..64d645adc 100644 --- a/base/commands/multimap/multimap_clear.go +++ b/base/commands/multimap/multimap_clear.go @@ -3,60 +3,12 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type MultiMapClearCommand struct{} - -func (mc *MultiMapClearCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("clear") - help := "Delete all entries of a MultiMap" - cc.SetCommandHelp(help, help) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - return nil -} - -func (mc *MultiMapClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mv, err := ec.Props().GetBlocking(multiMapPropertyName) - if err != nil { - return err - } - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("MultiMap will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - m := mv.(*hazelcast.MultiMap) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Clearing multimap %s", m.Name())) - if err := m.Clear(ctx); err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:clear", &MultiMapClearCommand{})) + cmd := commands.NewClearCommand("MultiMap", getMultiMap) + check.Must(plug.Registry.RegisterCommand("multi-map:clear", cmd)) } diff --git a/base/commands/multimap/multimap_destroy.go b/base/commands/multimap/multimap_destroy.go index 6ff48cbbf..1af96c25f 100644 --- a/base/commands/multimap/multimap_destroy.go +++ b/base/commands/multimap/multimap_destroy.go @@ -3,63 +3,12 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type MultiMapDestroyCommand struct{} - -func (mc *MultiMapDestroyCommand) Init(cc plug.InitContext) error { - long := `Destroy a MultiMap - -This command will delete the MultiMap and the data in it will not be available anymore.` - cc.SetCommandUsage("destroy") - short := "Destroy a MultiMap" - cc.SetCommandHelp(long, short) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - return nil -} - -func (mc *MultiMapDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mv, err := ec.Props().GetBlocking(multiMapPropertyName) - if err != nil { - return err - } - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("MultiMap will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - m := mv.(*hazelcast.MultiMap) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Destroying multimap %s", m.Name())) - if err := m.Destroy(ctx); err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:destroy", &MultiMapDestroyCommand{})) + cmd := commands.NewDestroyCommand("MultiMap", getMultiMap) + check.Must(plug.Registry.RegisterCommand("multi-map:destroy", cmd)) } diff --git a/base/commands/multimap/multimap_entry_set.go b/base/commands/multimap/multimap_entry_set.go index 958598be9..b919d8858 100644 --- a/base/commands/multimap/multimap_entry_set.go +++ b/base/commands/multimap/multimap_entry_set.go @@ -3,52 +3,13 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) -type MultiMapEntrySetCommand struct{} - -func (mc *MultiMapEntrySetCommand) Init(cc plug.InitContext) error { - help := "Get all entries of a MultiMap" - cc.SetCommandHelp(help, help) - cc.SetCommandUsage("entry-set") - return nil -} - -func (mc *MultiMapEntrySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - showType := ec.Props().GetBool(multiMapFlagShowType) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - req := codec.EncodeMultiMapEntrySetRequest(mmName) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting entries of multimap %s", mmName)) - return ci.InvokeOnRandomTarget(ctx, req, nil) - }) - if err != nil { - return err - } - stop() - pairs := codec.DecodeMultiMapEntrySetResponse(rv.(*hazelcast.ClientMessage)) - rows := output.DecodePairs(ci, pairs, showType) - if len(rows) > 0 { - return ec.AddOutputRows(ctx, rows...) - } - ec.PrintlnUnnecessary("No entries found.") - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:entry-set", &MultiMapEntrySetCommand{})) + c := commands.NewMapEntrySetCommand("MultiMap", codec.EncodeMultiMapEntrySetRequest, codec.DecodeMultiMapEntrySetResponse) + check.Must(plug.Registry.RegisterCommand("multi-map:entry-set", c)) } diff --git a/base/commands/multimap/multimap_get.go b/base/commands/multimap/multimap_get.go index 51bd4f9cc..46b7acb5b 100644 --- a/base/commands/multimap/multimap_get.go +++ b/base/commands/multimap/multimap_get.go @@ -3,78 +3,13 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MultiMapGetCommand struct{} - -func (mc *MultiMapGetCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("get") - help := "Get a value from the given MultiMap" - cc.SetCommandHelp(help, help) - addKeyTypeFlag(cc) - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (mc *MultiMapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } - req := codec.EncodeMultiMapGetRequest(mmName, keyData, 0) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting value from multimap %s", mmName)) - return ci.InvokeOnKey(ctx, req, keyData, nil) - }) - if err != nil { - return err - } - stop() - var rows []output.Row - raw := codec.DecodeMultiMapGetResponse(rv.(*hazelcast.ClientMessage)) - for _, r := range raw { - vt := r.Type() - value, err := ci.DecodeData(*r) - if err != nil { - ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) - value = serialization.NondecodedType(serialization.TypeToLabel(vt)) - } - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: vt, - Value: value, - }, - } - if ec.Props().GetBool(multiMapFlagShowType) { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(vt), - }) - } - rows = append(rows, row) - } - return ec.AddOutputRows(ctx, rows...) -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:get", &MultiMapGetCommand{})) + c := commands.NewMapGetCommand("MultiMap", codec.EncodeMultiMapGetRequest, makeDecodeResponseRowsFunc(codec.DecodeMultiMapGetResponse)) + check.Must(plug.Registry.RegisterCommand("multi-map:get", c)) } diff --git a/base/commands/multimap/multimap_key_set.go b/base/commands/multimap/multimap_key_set.go index 77efd2303..659dbb4e8 100644 --- a/base/commands/multimap/multimap_key_set.go +++ b/base/commands/multimap/multimap_key_set.go @@ -3,66 +3,13 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MultiMapKeySetCommand struct{} - -func (mc *MultiMapKeySetCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("key-set") - help := "Get all keys of a MultiMap" - cc.SetCommandHelp(help, help) - return nil -} - -func (mc *MultiMapKeySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - showType := ec.Props().GetBool(multiMapFlagShowType) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - req := codec.EncodeMultiMapKeySetRequest(mmName) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting keys of %s", mmName)) - return ci.InvokeOnRandomTarget(ctx, req, nil) - }) - if err != nil { - return err - } - stop() - raw := codec.DecodeMultiMapKeySetResponse(rv.(*hazelcast.ClientMessage)) - var rows []output.Row - for _, r := range raw { - var row output.Row - t := r.Type() - v, err := ci.DecodeData(*r) - if err != nil { - v = serialization.NondecodedType(serialization.TypeToLabel(t)) - } - row = append(row, output.NewKeyColumn(t, v)) - if showType { - row = append(row, output.NewKeyTypeColumn(t)) - } - rows = append(rows, row) - } - if len(rows) > 0 { - return ec.AddOutputRows(ctx, rows...) - } - ec.PrintlnUnnecessary("No entries found.") - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:key-set", &MultiMapKeySetCommand{})) + c := commands.NewMapKeySetCommand("MultiMap", codec.EncodeMultiMapKeySetRequest, codec.DecodeMultiMapKeySetResponse) + check.Must(plug.Registry.RegisterCommand("multi-map:key-set", c)) } diff --git a/base/commands/multimap/multimap_lock.go b/base/commands/multimap/multimap_lock.go index 50f178554..ca12da29f 100644 --- a/base/commands/multimap/multimap_lock.go +++ b/base/commands/multimap/multimap_lock.go @@ -3,61 +3,12 @@ package multimap import ( - "context" - "fmt" - "time" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type MultiMapLockCommand struct{} - -func (m MultiMapLockCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("lock") - long := `Lock a key in the given MultiMap - -This command is only available in the interactive mode.` - short := "Lock a key in the given MultiMap" - cc.SetCommandHelp(long, short) - addKeyTypeFlag(cc) - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (m MultiMapLockCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - mv, err := ec.Props().GetBlocking(multiMapPropertyName) - if err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } - mm := mv.(*hazelcast.MultiMap) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Locking key of multimap %s", mmName)) - if ttl := GetTTL(ec); ttl != ttlUnset { - return mm.LockWithLease(ctx, keyData, time.Duration(GetTTL(ec))), nil - } - return mm.Lock(ctx, keyData), nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:lock", &MultiMapLockCommand{}, plug.OnlyInteractive{})) + c := commands.NewLockCommand("MultiMap", getMultiMap) + check.Must(plug.Registry.RegisterCommand("multi-map:lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/multimap/multimap_put.go b/base/commands/multimap/multimap_put.go index 87f0880b4..6d5ffb700 100644 --- a/base/commands/multimap/multimap_put.go +++ b/base/commands/multimap/multimap_put.go @@ -6,76 +6,72 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -const ( - argValue = "value" - argTitleValue = "value" -) - type MultiMapPutCommand struct{} -func (m MultiMapPutCommand) Init(cc plug.InitContext) error { +func (MultiMapPutCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("put") help := "Put a value in the given MultiMap" cc.SetCommandHelp(help, help) - addKeyTypeFlag(cc) - addValueTypeFlag(cc) - cc.AddStringArg(argKey, argTitleKey) - cc.AddStringArg(argValue, argTitleValue) + commands.AddKeyTypeFlag(cc) + commands.AddValueTypeFlag(cc) + cc.AddStringArg(commands.ArgKey, commands.ArgTitleKey) + cc.AddStringArg(base.ArgValue, base.ArgTitleValue) return nil } -func (m MultiMapPutCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - if _, err := ec.Props().GetBlocking(multiMapPropertyName); err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - valueStr := ec.GetStringArg(argValue) - kd, vd, err := makeKeyValueData(ec, ci, keyStr, valueStr) - if err != nil { - return err - } - req := codec.EncodeMultiMapPutRequest(mmName, kd, vd, 0) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Putting value into multimap %s", mmName)) - return ci.InvokeOnKey(ctx, req, kd, nil) +func (MultiMapPutCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + keyStr := ec.GetStringArg(commands.ArgKey) + valueStr := ec.GetStringArg(base.ArgValue) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Putting value into MultiMap '%s'", name)) + kd, vd, err := commands.MakeKeyValueData(ec, ci, keyStr, valueStr) + if err != nil { + return nil, err + } + req := codec.EncodeMultiMapPutRequest(name, kd, vd, 0) + resp, err := ci.InvokeOnKey(ctx, req, kd, nil) + if err != nil { + return nil, err + } + value := codec.DecodeMultiMapPutResponse(resp) + row := output.Row{ + output.Column{ + Name: output.NameValue, + Type: serialization.TypeBool, + Value: value, + }, + } + if ec.Props().GetBool(base.FlagShowType) { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(serialization.TypeBool), + }) + } + return []output.Row{row}, nil }) if err != nil { return err } stop() - resp := codec.DecodeMultiMapPutResponse(rv.(*hazelcast.ClientMessage)) - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: serialization.TypeBool, - Value: resp, - }, - } - if ec.Props().GetBool(multiMapFlagShowType) { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(serialization.TypeBool), - }) - } - return ec.AddOutputRows(ctx, row) + return ec.AddOutputRows(ctx, rowsV.([]output.Row)...) } func init() { - Must(plug.Registry.RegisterCommand("multi-map:put", &MultiMapPutCommand{})) + check.Must(plug.Registry.RegisterCommand("multi-map:put", &MultiMapPutCommand{})) } diff --git a/base/commands/multimap/multimap_remove.go b/base/commands/multimap/multimap_remove.go index fe8249fda..c741259d0 100644 --- a/base/commands/multimap/multimap_remove.go +++ b/base/commands/multimap/multimap_remove.go @@ -3,78 +3,13 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MultiMapRemoveCommand struct{} - -func (mc *MultiMapRemoveCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("remove") - help := "Remove values from the given MultiMap" - cc.SetCommandHelp(help, help) - addKeyTypeFlag(cc) - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (mc *MultiMapRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } - req := codec.EncodeMultiMapRemoveRequest(mmName, keyData, 0) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Removing from multimap %s", mmName)) - return ci.InvokeOnKey(ctx, req, keyData, nil) - }) - if err != nil { - return err - } - stop() - raw := codec.DecodeMultiMapRemoveResponse(rv.(*hazelcast.ClientMessage)) - var rows []output.Row - for _, r := range raw { - vt := r.Type() - value, err := ci.DecodeData(*r) - if err != nil { - ec.Logger().Info("The value for %s was not decoded, due to error: %s", keyStr, err.Error()) - value = serialization.NondecodedType(serialization.TypeToLabel(vt)) - } - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: vt, - Value: value, - }, - } - if ec.Props().GetBool(multiMapFlagShowType) { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(vt), - }) - } - rows = append(rows, row) - } - return ec.AddOutputRows(ctx, rows...) -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:remove", &MultiMapRemoveCommand{})) + c := commands.NewMapRemoveCommand("MultiMap", codec.EncodeMultiMapRemoveRequest, makeDecodeResponseRowsFunc(codec.DecodeMultiMapRemoveResponse)) + check.Must(plug.Registry.RegisterCommand("multi-map:remove", c)) } diff --git a/base/commands/multimap/multimap_size.go b/base/commands/multimap/multimap_size.go index 395bc76cb..bcf0d902b 100644 --- a/base/commands/multimap/multimap_size.go +++ b/base/commands/multimap/multimap_size.go @@ -3,51 +3,12 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MultiMapSizeCommand struct{} - -func (mc *MultiMapSizeCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("size") - help := "Return the size of the given MultiMap" - cc.SetCommandHelp(help, help) - return nil -} - -func (mc *MultiMapSizeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - mv, err := ec.Props().GetBlocking(multiMapPropertyName) - if err != nil { - return err - } - m := mv.(*hazelcast.MultiMap) - sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting the size of the multimap %s", mmName)) - return m.Size(ctx) - }) - if err != nil { - return err - } - stop() - return ec.AddOutputRows(ctx, output.Row{ - { - Name: "Size", - Type: serialization.TypeInt32, - Value: int32(sv.(int)), - }, - }) -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:size", &MultiMapSizeCommand{})) + cmd := commands.NewSizeCommand("MultiMap", getMultiMap) + check.Must(plug.Registry.RegisterCommand("multi-map:size", cmd)) } diff --git a/base/commands/multimap/multimap_try_lock.go b/base/commands/multimap/multimap_try_lock.go index 08a47cfa5..3df4ef224 100644 --- a/base/commands/multimap/multimap_try_lock.go +++ b/base/commands/multimap/multimap_try_lock.go @@ -3,78 +3,12 @@ package multimap import ( - "context" - "fmt" - "time" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MultiMapTryLockCommand struct{} - -func (m MultiMapTryLockCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("try-lock") - long := `Try to lock a key in the given MultiMap. Directly returns the result - -This command is only available in the interactive mode.` - short := "Try to lock a key in the given MultiMap. Directly returns the result" - cc.SetCommandHelp(long, short) - addKeyTypeFlag(cc) - cc.AddIntFlag(multiMapTTL, "", ttlUnset, false, "time-to-live (ms)") - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (m MultiMapTryLockCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - mv, err := ec.Props().GetBlocking(multiMapPropertyName) - if err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } - mm := mv.(*hazelcast.MultiMap) - lv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Trying to lock multimap %s", mmName)) - if ttl := GetTTL(ec); ttl != ttlUnset { - return mm.TryLockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) - } - return mm.TryLock(ctx, keyData) - }) - if err != nil { - return err - } - stop() - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: serialization.TypeBool, - Value: lv.(bool), - }, - } - if ec.Props().GetBool(multiMapFlagShowType) { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(serialization.TypeBool), - }) - } - return ec.AddOutputRows(ctx, row) -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:try-lock", &MultiMapTryLockCommand{}, plug.OnlyInteractive{})) + c := commands.NewTryLockCommand("MultiMap", getMultiMap) + check.Must(plug.Registry.RegisterCommand("multi-map:try-lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/multimap/multimap_unlock.go b/base/commands/multimap/multimap_unlock.go index 9af9406d4..dd4a73de2 100644 --- a/base/commands/multimap/multimap_unlock.go +++ b/base/commands/multimap/multimap_unlock.go @@ -3,52 +3,12 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) -type MultiMapUnlockCommand struct{} - -func (m MultiMapUnlockCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("unlock") - long := `Unlock a key in the given MultiMap - -This command is only available in the interactive mode.` - short := "Unlock a key in the given MultiMap" - cc.SetCommandHelp(long, short) - addKeyTypeFlag(cc) - cc.AddStringArg(argKey, argTitleKey) - return nil -} - -func (m MultiMapUnlockCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - keyStr := ec.GetStringArg(argKey) - keyData, err := makeKeyData(ec, ci, keyStr) - if err != nil { - return err - } - req := codec.EncodeMultiMapUnlockRequest(mmName, keyData, 0, 0) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Unlocking key of multimap %s", mmName)) - return ci.InvokeOnKey(ctx, req, keyData, nil) - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:unlock", &MultiMapUnlockCommand{}, plug.OnlyInteractive{})) + c := commands.NewMapUnlockCommand("MultiMap", getMultiMap) + check.Must(plug.Registry.RegisterCommand("multi-map:unlock", c)) } diff --git a/base/commands/multimap/multimap_values.go b/base/commands/multimap/multimap_values.go index 0513eda54..c3b370e8e 100644 --- a/base/commands/multimap/multimap_values.go +++ b/base/commands/multimap/multimap_values.go @@ -3,75 +3,13 @@ package multimap import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type MultiMapValuesCommand struct{} - -func (m MultiMapValuesCommand) Init(cc plug.InitContext) error { - help := "Get all values of a MultiMap" - cc.SetCommandHelp(help, help) - cc.SetCommandUsage("values") - return nil -} - -func (m MultiMapValuesCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - mmName := ec.Props().GetString(multiMapFlagName) - showType := ec.Props().GetBool(multiMapFlagShowType) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - req := codec.EncodeMultiMapValuesRequest(mmName) - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting values of %s", mmName)) - return ci.InvokeOnRandomTarget(ctx, req, nil) - }) - if err != nil { - return err - } - stop() - raw := codec.DecodeMultiMapValuesResponse(rv.(*hazelcast.ClientMessage)) - var rows []output.Row - for _, r := range raw { - t := r.Type() - v, err := ci.DecodeData(*r) - if err != nil { - v = serialization.NondecodedType(serialization.TypeToLabel(t)) - } - row := output.Row{ - output.Column{ - Name: output.NameValue, - Type: t, - Value: v, - }, - } - if showType { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(t), - }) - } - rows = append(rows, row) - } - if len(rows) > 0 { - return ec.AddOutputRows(ctx, rows...) - } - ec.PrintlnUnnecessary("No values found.") - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("multi-map:values", &MultiMapValuesCommand{})) + c := commands.NewMapValuesCommand("MultiMap", codec.EncodeMultiMapValuesRequest, codec.DecodeMultiMapValuesResponse) + check.Must(plug.Registry.RegisterCommand("multi-map:values", c)) } diff --git a/base/commands/multimap/util.go b/base/commands/multimap/util.go deleted file mode 100644 index f58f5fb39..000000000 --- a/base/commands/multimap/util.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build std || multimap - -package multimap - -import "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - -func GetTTL(ec plug.ExecContext) int64 { - if _, ok := ec.Props().Get(multiMapTTL); ok { - return ec.Props().GetInt(multiMapTTL) - } - return ttlUnset -} diff --git a/base/commands/object/object.go b/base/commands/object/object.go index d76a8badd..d1d143b9e 100644 --- a/base/commands/object/object.go +++ b/base/commands/object/object.go @@ -10,22 +10,22 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type ObjectCommand struct{} +type Command struct{} -func (cm ObjectCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("object") cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) cc.SetTopLevel(true) help := "Generic distributed data structure operations" - cc.SetCommandUsage("object [command]") cc.SetCommandHelp(help, help) return nil } -func (cm ObjectCommand) Exec(context.Context, plug.ExecContext) error { +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - Must(plug.Registry.RegisterCommand("object", &ObjectCommand{})) + Must(plug.Registry.RegisterCommand("object", &Command{})) } diff --git a/base/commands/object/object_list.go b/base/commands/object/object_list.go index d96caa805..3d01a36e4 100644 --- a/base/commands/object/object_list.go +++ b/base/commands/object/object_list.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/hazelcast/hazelcast-commandline-client/base/objects" - "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -59,9 +58,9 @@ const ( argTitleObjectType = "object type" ) -type ObjectListCommand struct{} +type ListCommand struct{} -func (cm ObjectListCommand) Init(cc plug.InitContext) error { +func (ListCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("list") long := fmt.Sprintf(`List distributed objects, optionally filter by type. @@ -76,7 +75,7 @@ CP objects such as AtomicLong cannot be listed. return nil } -func (cm ObjectListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { var typeFilter string fs := ec.GetStringSliceArg(argObjectType) if len(fs) > 0 { @@ -107,13 +106,11 @@ func (cm ObjectListCommand) Exec(ctx context.Context, ec plug.ExecContext) error valueCol, }) } - if len(rows) > 0 { - return ec.AddOutputRows(ctx, rows...) + if len(rows) == 0 { + ec.PrintlnUnnecessary("OK No objects found.") + return nil } - if !ec.Props().GetBool(clc.PropertyQuiet) { - I2(fmt.Fprintln(ec.Stdout(), "No objects found")) - } - return nil + return ec.AddOutputRows(ctx, rows...) } func objectFilterTypes() string { @@ -129,5 +126,5 @@ func init() { sort.Slice(objTypes, func(i, j int) bool { return objTypes[i] < objTypes[j] }) - Must(plug.Registry.RegisterCommand("object:list", &ObjectListCommand{})) + Must(plug.Registry.RegisterCommand("object:list", &ListCommand{})) } diff --git a/base/commands/project/const.go b/base/commands/project/const.go index 02a4994f0..085da85c1 100644 --- a/base/commands/project/const.go +++ b/base/commands/project/const.go @@ -10,5 +10,5 @@ const ( hzTemplatesOrganization = "https://github.com/hazelcast-templates" defaultsFileName = "defaults.yaml" envTemplateSource = "CLC_EXPERIMENTAL_TEMPLATE_SOURCE" - flagOutputDir = "output-dir" + groupProject = "project" ) diff --git a/base/commands/project/help.go b/base/commands/project/help.go index be121be05..7502b58e8 100644 --- a/base/commands/project/help.go +++ b/base/commands/project/help.go @@ -33,6 +33,8 @@ You can use the placeholders in "defaults.yaml" and the following configuration * cluster_user * cluster_password * cluster_discovery_token + * cluster_api_base + * cluster_viridian_id * ssl_enabled * ssl_server * ssl_skip_verify diff --git a/base/commands/project/project.go b/base/commands/project/project.go index 4ba5c51b0..d064b4ff8 100644 --- a/base/commands/project/project.go +++ b/base/commands/project/project.go @@ -9,23 +9,23 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type ProjectCommand struct{} +type Command struct{} -func (gc *ProjectCommand) Init(cc plug.InitContext) error { - cc.AddCommandGroup("project", "Project") - cc.SetCommandGroup("project") +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("project") + cc.AddCommandGroup(groupProject, "Project") + cc.SetCommandGroup(groupProject) cc.SetTopLevel(true) - cc.SetCommandUsage("project [command] [flags]") help := "Project commands" cc.SetCommandHelp(help, help) return nil } -func (gc ProjectCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (gc Command) Exec(ctx context.Context, ec plug.ExecContext) error { return nil } func init() { - cmd := &ProjectCommand{} + cmd := &Command{} Must(plug.Registry.RegisterCommand("project", cmd)) } diff --git a/base/commands/project/project_create.go b/base/commands/project/project_create.go index a5f6c660b..879068ed7 100644 --- a/base/commands/project/project_create.go +++ b/base/commands/project/project_create.go @@ -11,11 +11,15 @@ import ( "regexp" "strings" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + iserialization "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) const ( @@ -27,46 +31,80 @@ const ( var regexpValidKey = regexp.MustCompile(`^[a-z0-9_]+$`) -type CreateCmd struct{} +type CreateCommand struct{} -func (pc CreateCmd) Init(cc plug.InitContext) error { +func (pc CreateCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("create") short := "Create project from the given template (BETA)" long := longHelp() cc.SetCommandHelp(long, short) - cc.AddStringFlag(flagOutputDir, "o", "", false, "the directory to create the project at") + cc.AddStringFlag(commands.FlagOutputDir, "o", "", false, "the directory to create the project at") cc.AddStringArg(argTemplateName, argTitleTemplateName) cc.AddKeyValueSliceArg(argPlaceholder, argTitlePlaceholder, 0, clc.MaxArgs) return nil } -func (pc CreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (pc CreateCommand) Exec(ctx context.Context, ec plug.ExecContext) error { templateName := ec.GetStringArg(argTemplateName) - outputDir := ec.Props().GetString(flagOutputDir) + outputDir := ec.Props().GetString(commands.FlagOutputDir) if outputDir == "" { outputDir = templateName } + var stages []stage.Stage[any] templatesDir := paths.Templates() templateExists := paths.Exists(filepath.Join(templatesDir, templateName)) - if !templateExists { - ec.Logger().Debug(func() string { - return fmt.Sprintf("template %s does not exist, cloning it into %s", templateName, templatesDir) + if templateExists { + stages = append(stages, stage.Stage[any]{ + ProgressMsg: "Updating the template", + SuccessMsg: fmt.Sprintf("Updated template '%s'", templateName), + FailureMsg: "Failed updating the template", + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + err := updateTemplate(ctx, templatesDir, templateName) + if err != nil { + ec.Logger().Error(err) + return nil, stage.IgnoreError(err) + } + return nil, nil + }, + }) + } else { + stages = append(stages, stage.Stage[any]{ + ProgressMsg: "Retrieving the template", + SuccessMsg: fmt.Sprintf("Retrieved template '%s'", templateName), + FailureMsg: "Failed retrieving the template", + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + ec.Logger().Debug(func() string { + return fmt.Sprintf("template %s does not exist, cloning it into %s", templateName, templatesDir) + }) + err := cloneTemplate(ctx, templatesDir, templateName) + if err != nil { + ec.Logger().Error(err) + return nil, err + } + return nil, nil + }, }) - err := cloneTemplate(templatesDir, templateName) - if err != nil { - ec.Logger().Error(err) - return err - } } - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Creating project from template %s", templateName)) - return nil, createProject(ec, outputDir, templateName) + stages = append(stages, stage.Stage[any]{ + ProgressMsg: "Creating the project", + SuccessMsg: "Created the project", + FailureMsg: "Failed creating the project", + Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + return nil, createProject(ec, outputDir, templateName) + }, }) - stop() + _, err := stage.Execute[any](ctx, ec, nil, stage.NewFixedProvider(stages...)) if err != nil { return err } - return nil + ec.PrintlnUnnecessary("") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Path", + Type: iserialization.TypeString, + Value: outputDir, + }, + }) } func createProject(ec plug.ExecContext, outputDir, templateName string) error { @@ -155,5 +193,5 @@ func isDefaultPropertiesFile(d fs.DirEntry) bool { } func init() { - Must(plug.Registry.RegisterCommand("project:create", &CreateCmd{})) + Must(plug.Registry.RegisterCommand("project:create", &CreateCommand{})) } diff --git a/base/commands/project/project_list_templates.go b/base/commands/project/project_list_templates.go index 3d1a25657..5074ba620 100644 --- a/base/commands/project/project_list_templates.go +++ b/base/commands/project/project_list_templates.go @@ -23,8 +23,6 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ListCmd struct{} - const ( flagRefresh = "refresh" flagLocal = "local" @@ -38,7 +36,9 @@ type Template struct { Source string } -func (lc ListCmd) Init(cc plug.InitContext) error { +type ListTemplatesCommand struct{} + +func (ListTemplatesCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("list-templates") help := "Lists templates that can be used while creating projects." cc.SetCommandHelp(help, help) @@ -47,7 +47,7 @@ func (lc ListCmd) Init(cc plug.InitContext) error { return nil } -func (lc ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ListTemplatesCommand) Exec(ctx context.Context, ec plug.ExecContext) error { isLocal := ec.Props().GetBool(flagLocal) isRefresh := ec.Props().GetBool(flagRefresh) if isLocal && isRefresh { @@ -63,7 +63,8 @@ func (lc ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { stop() tss := ts.([]Template) if len(tss) == 0 { - ec.PrintlnUnnecessary("No templates found") + ec.PrintlnUnnecessary("No templates found.") + return nil } rows := make([]output.Row, len(tss)) for i, t := range tss { @@ -225,5 +226,5 @@ func listFromCache(sa *store.StoreAccessor) ([]Template, error) { } func init() { - Must(plug.Registry.RegisterCommand("project:list-templates", &ListCmd{})) + Must(plug.Registry.RegisterCommand("project:list-templates", &ListTemplatesCommand{})) } diff --git a/base/commands/project/utils.go b/base/commands/project/utils.go index 904202639..27cbd6407 100644 --- a/base/commands/project/utils.go +++ b/base/commands/project/utils.go @@ -3,6 +3,7 @@ package project import ( + "context" "errors" "fmt" "os" @@ -121,9 +122,10 @@ func marshalYAML(m map[string]any) []byte { return d } -func cloneTemplate(baseDir string, name string) error { +func cloneTemplate(ctx context.Context, baseDir, name string) error { u := templateRepoURL(name) - _, err := git.PlainClone(filepath.Join(baseDir, name), false, &git.CloneOptions{ + path := filepath.Join(baseDir, name) + _, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ URL: u, Progress: nil, Depth: 1, @@ -137,6 +139,30 @@ func cloneTemplate(baseDir string, name string) error { return nil } +func updateTemplate(ctx context.Context, baseDir, name string) error { + path := filepath.Join(baseDir, name) + r, err := git.PlainOpen(path) + if err != nil { + return fmt.Errorf("opening local git repository: %w", err) + } + wt, err := r.Worktree() + if err != nil { + return fmt.Errorf("opening work tree: %w", err) + } + opts := &git.PullOptions{ + SingleBranch: true, + Depth: 1, + Progress: nil, + Force: true, + } + if err = wt.PullContext(ctx, opts); err != nil { + if !errors.Is(err, git.NoErrAlreadyUpToDate) { + return err + } + } + return nil +} + func templateOrgURL() string { u := os.Getenv(envTemplateSource) if u == "" { diff --git a/base/commands/queue/common.go b/base/commands/queue/common.go index f630c2224..6f9a7c463 100644 --- a/base/commands/queue/common.go +++ b/base/commands/queue/common.go @@ -3,29 +3,23 @@ package queue import ( + "context" "fmt" - "strings" "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/internal" - "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -func addValueTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(queueFlagValueType, "v", "string", false, help) -} - -func makeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { - vt := ec.Props().GetString(queueFlagValueType) - if vt == "" { - vt = "string" - } - value, err := mk.ValueFromString(valueStr, vt) +func getQueue(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.Queue, error) { + name := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - return ci.EncodeData(value) + sp.SetText(fmt.Sprintf("Getting Queue '%s'", name)) + return ci.Client().GetQueue(ctx, name) } diff --git a/base/commands/queue/const.go b/base/commands/queue/const.go deleted file mode 100644 index 7427d61fe..000000000 --- a/base/commands/queue/const.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build std || queue - -package queue - -const ( - queueFlagValueType = "value-type" - defaultQueueName = "default" -) diff --git a/base/commands/queue/queue.go b/base/commands/queue/queue.go index bc0ec04f0..275514822 100644 --- a/base/commands/queue/queue.go +++ b/base/commands/queue/queue.go @@ -4,72 +4,31 @@ package queue import ( "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - queueFlagName = "name" - queueFlagShowType = "show-type" - queuePropertyName = "queue" -) - -type QueueCommand struct { -} +type Command struct{} -func (qc *QueueCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("queue") cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(queueFlagName, "n", defaultQueueName, false, "queue name") - cc.AddBoolFlag(queueFlagShowType, "", false, false, "add the type names to the output") - if !cc.Interactive() { - cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") - } cc.SetTopLevel(true) - cc.SetCommandUsage("queue [command] [flags]") help := "Queue operations" cc.SetCommandHelp(help, help) + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "queue name") + cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") return nil } -func (qc *QueueCommand) Exec(context.Context, plug.ExecContext) error { - return nil -} - -func (qc *QueueCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { - ctx := context.TODO() - props.SetBlocking(queuePropertyName, func() (any, error) { - queueName := ec.Props().GetString(queueFlagName) - // empty queue name is allowed - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - val, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting queue %s", queueName)) - q, err := ci.Client().GetQueue(ctx, queueName) - if err != nil { - return nil, err - } - return q, nil - }) - if err != nil { - return nil, err - } - stop() - return val.(*hazelcast.Queue), nil - }) +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - cmd := &QueueCommand{} - check.Must(plug.Registry.RegisterCommand("queue", cmd)) - plug.Registry.RegisterAugmentor("20-queue", cmd) + check.Must(plug.Registry.RegisterCommand("queue", &Command{})) } diff --git a/base/commands/queue/queue_clear.go b/base/commands/queue/queue_clear.go index 63a3a1983..022e8f5e4 100644 --- a/base/commands/queue/queue_clear.go +++ b/base/commands/queue/queue_clear.go @@ -3,60 +3,12 @@ package queue import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type QueueClearCommand struct{} - -func (qc *QueueClearCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("clear") - help := "Delete all entries of a Queue" - cc.SetCommandHelp(help, help) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - return nil -} - -func (qc *QueueClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - qv, err := ec.Props().GetBlocking(queuePropertyName) - if err != nil { - return err - } - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("Queue content will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - q := qv.(*hazelcast.Queue) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Clearing queue %s", q.Name())) - if err = q.Clear(ctx); err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("queue:clear", &QueueClearCommand{})) + c := commands.NewClearCommand("Queue", getQueue) + check.Must(plug.Registry.RegisterCommand("queue:clear", c)) } diff --git a/base/commands/queue/queue_destroy.go b/base/commands/queue/queue_destroy.go index 9d39651cd..f33b02621 100644 --- a/base/commands/queue/queue_destroy.go +++ b/base/commands/queue/queue_destroy.go @@ -3,63 +3,12 @@ package queue import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type QueueDestroyCommand struct{} - -func (qc *QueueDestroyCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("destroy") - long := `Destroy a Queue - -This command will delete the Queue and the data in it will not be available anymore.` - short := "Destroy a Queue" - cc.SetCommandHelp(long, short) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - return nil -} - -func (qc *QueueDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - qv, err := ec.Props().GetBlocking(queuePropertyName) - if err != nil { - return err - } - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("Queue will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - q := qv.(*hazelcast.Queue) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Destroying queue %s", q.Name())) - if err := q.Destroy(ctx); err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("queue:destroy", &QueueDestroyCommand{})) + c := commands.NewDestroyCommand("Queue", getQueue) + check.Must(plug.Registry.RegisterCommand("queue:destroy", c)) } diff --git a/base/commands/queue/queue_offer.go b/base/commands/queue/queue_offer.go index 3aed47979..50fbcd75a 100644 --- a/base/commands/queue/queue_offer.go +++ b/base/commands/queue/queue_offer.go @@ -6,60 +6,69 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" -) - -const ( - argValue = "value" - argTitleValue = "value" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) type QueueOfferCommand struct{} -func (qc *QueueOfferCommand) Init(cc plug.InitContext) error { +func (QueueOfferCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("offer") help := "Add values to the given Queue" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) - cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) + commands.AddValueTypeFlag(cc) + cc.AddStringSliceArg(base.ArgValue, base.ArgTitleValue, 1, clc.MaxArgs) return nil } -func (qc *QueueOfferCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - queueName := ec.Props().GetString(queueFlagName) - qv, err := ec.Props().GetBlocking(queuePropertyName) - if err != nil { - return err - } - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - q := qv.(*hazelcast.Queue) - - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Adding values into queue %s", queueName)) - for _, arg := range ec.GetStringSliceArg(argValue) { - vd, err := makeValueData(ec, ci, arg) +func (QueueOfferCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + q, err := ci.Client().GetQueue(ctx, name) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Adding values into Queue '%s'", name)) + var rows []output.Row + for _, arg := range ec.GetStringSliceArg(base.ArgValue) { + vd, err := commands.MakeValueData(ec, ci, arg) if err != nil { return nil, err } - rv, err := q.Add(ctx, vd) + v, err := q.Add(ctx, vd) if err != nil { - return rv, err + return nil, err } + rows = append(rows, output.Row{ + output.Column{ + Name: "Value", + Type: serialization.TypeString, + Value: arg, + }, + output.Column{ + Name: "Added", + Type: serialization.TypeBool, + Value: v, + }, + }) } - return nil, err + return rows, nil }) if err != nil { return nil } stop() - return nil + return ec.AddOutputRows(ctx, rowsV.([]output.Row)...) } func init() { diff --git a/base/commands/queue/queue_poll.go b/base/commands/queue/queue_poll.go index 17e1f1756..b076bf434 100644 --- a/base/commands/queue/queue_poll.go +++ b/base/commands/queue/queue_poll.go @@ -5,11 +5,12 @@ package queue import ( "context" "fmt" - "strings" - - "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -19,60 +20,59 @@ import ( const flagCount = "count" -type QueuePollCommand struct { -} +type PollCommand struct{} -func (qc *QueuePollCommand) Init(cc plug.InitContext) error { +func (PollCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("poll") help := "Remove the given number of elements from the given Queue" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) + commands.AddValueTypeFlag(cc) cc.AddIntFlag(flagCount, "", 1, false, "number of element to be removed from the given queue") return nil } -func (qc *QueuePollCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - queueName := ec.Props().GetString(queueFlagName) +func (PollCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + queueName := ec.Props().GetString(base.FlagName) count := int(ec.Props().GetInt(flagCount)) if count < 0 { return fmt.Errorf("%s cannot be negative", flagCount) } - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } rows, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Polling from queue %s", queueName)) + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Polling from Queue '%s'", queueName)) + req := codec.EncodeQueuePollRequest(queueName, 0) + pID, err := internal.StringToPartitionID(ci, queueName) + if err != nil { + return nil, err + } var rows []output.Row for i := 0; i < count; i++ { - req := codec.EncodeQueuePollRequest(queueName, 0) - pID, err := stringToPartitionID(ci, queueName) - if err != nil { - return nil, err - } rv, err := ci.InvokeOnPartition(ctx, req, pID, nil) if err != nil { return nil, err } - raw := codec.DecodeQueuePollResponse(rv) - valueType := raw.Type() - value, err := ci.DecodeData(raw) + data := codec.DecodeQueuePollResponse(rv) + vt := data.Type() + value, err := ci.DecodeData(data) if err != nil { ec.Logger().Info("The value was not decoded, due to error: %s", err.Error()) - value = serialization.NondecodedType(serialization.TypeToLabel(valueType)) + value = serialization.NondecodedType(serialization.TypeToLabel(vt)) } row := output.Row{ output.Column{ Name: output.NameValue, - Type: valueType, + Type: vt, Value: value, }, } - if ec.Props().GetBool(queueFlagShowType) { + if ec.Props().GetBool(base.FlagShowType) { row = append(row, output.Column{ Name: output.NameValueType, Type: serialization.TypeString, - Value: serialization.TypeToLabel(valueType), + Value: serialization.TypeToLabel(vt), }) } rows = append(rows, row) @@ -87,19 +87,5 @@ func (qc *QueuePollCommand) Exec(ctx context.Context, ec plug.ExecContext) error } func init() { - Must(plug.Registry.RegisterCommand("queue:poll", &QueuePollCommand{})) -} - -func stringToPartitionID(ci *hazelcast.ClientInternal, name string) (int32, error) { - var partitionID int32 - var keyData hazelcast.Data - var err error - idx := strings.Index(name, "@") - if keyData, err = ci.EncodeData(name[idx+1:]); err != nil { - return 0, err - } - if partitionID, err = ci.GetPartitionID(keyData); err != nil { - return 0, err - } - return partitionID, nil + Must(plug.Registry.RegisterCommand("queue:poll", &PollCommand{})) } diff --git a/base/commands/queue/queue_size.go b/base/commands/queue/queue_size.go index 8fc32fc17..4c3554789 100644 --- a/base/commands/queue/queue_size.go +++ b/base/commands/queue/queue_size.go @@ -3,51 +3,12 @@ package queue import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type QueueSizeCommand struct{} - -func (qc *QueueSizeCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("size") - help := "Return the size of the given Queue" - cc.SetCommandHelp(help, help) - return nil -} - -func (qc *QueueSizeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - queueName := ec.Props().GetString(queueFlagName) - qv, err := ec.Props().GetBlocking(queuePropertyName) - if err != nil { - return err - } - q := qv.(*hazelcast.Queue) - sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting the size of the queue %s", queueName)) - return q.Size(ctx) - }) - if err != nil { - return err - } - stop() - return ec.AddOutputRows(ctx, output.Row{ - { - Name: "Size", - Type: serialization.TypeInt32, - Value: int32(sv.(int)), - }, - }) -} - func init() { - Must(plug.Registry.RegisterCommand("queue:size", &QueueSizeCommand{})) + c := commands.NewSizeCommand("Queue", getQueue) + check.Must(plug.Registry.RegisterCommand("queue:size", c)) } diff --git a/base/commands/script.go b/base/commands/script.go index 66223dee3..a8a13b53d 100644 --- a/base/commands/script.go +++ b/base/commands/script.go @@ -29,7 +29,7 @@ const ( type ScriptCommand struct{} -func (cm ScriptCommand) Init(cc plug.InitContext) error { +func (ScriptCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("script") long := `Runs the script in the given local or HTTP location. @@ -51,7 +51,7 @@ See examples/sql/dessert.sql for a sample script. return nil } -func (cm ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { args := ec.GetStringSliceArg(argPath) in := ec.Stdin() if len(args) > 0 { @@ -71,10 +71,9 @@ func (cm ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return fmt.Errorf("cloning Main: %w", err) } - verbose := ec.Props().GetBool(clc.PropertyVerbose) ie := ec.Props().GetBool(flagIgnoreErrors) echo := ec.Props().GetBool(flagEcho) - textFn := makeTextFunc(m, ec, verbose, false, false, func(shortcut string) bool { + textFn := makeTextFunc(m, ec, func(shortcut string) bool { // shortcuts are not supported in the script mode return false }) @@ -85,8 +84,6 @@ func (cm ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { return sh.Run(ctx) } -func (cm ScriptCommand) Unwrappable() {} - func openScript(location string) (io.ReadCloser, error) { if filepath.Ext(location) != ".clc" && filepath.Ext(location) != ".sql" { return nil, errors.New("the script should have either .clc or .sql extension") diff --git a/base/commands/set/common.go b/base/commands/set/common.go index 3a63ba436..95d8603e6 100644 --- a/base/commands/set/common.go +++ b/base/commands/set/common.go @@ -3,43 +3,23 @@ package set import ( + "context" "fmt" - "strings" "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-commandline-client/internal" - "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -func addValueTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(setFlagValueType, "v", "string", false, help) -} - -func makeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { - vt := ec.Props().GetString(setFlagValueType) - if vt == "" { - vt = "string" - } - value, err := mk.ValueFromString(valueStr, vt) +func getSet(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.Set, error) { + name := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - return ci.EncodeData(value) -} - -func stringToPartitionID(ci *hazelcast.ClientInternal, name string) (int32, error) { - var partitionID int32 - var keyData hazelcast.Data - var err error - idx := strings.Index(name, "@") - if keyData, err = ci.EncodeData(name[idx+1:]); err != nil { - return 0, err - } - if partitionID, err = ci.GetPartitionID(keyData); err != nil { - return 0, err - } - return partitionID, nil + sp.SetText(fmt.Sprintf("Getting Set '%s'", name)) + return ci.Client().GetSet(ctx, name) } diff --git a/base/commands/set/const.go b/base/commands/set/const.go deleted file mode 100644 index 9e1fd13ba..000000000 --- a/base/commands/set/const.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build std || set - -package set - -const ( - setFlagValueType = "value-type" - defaultSetName = "default" - argValue = "value" - argTitleValue = "value" -) diff --git a/base/commands/set/set.go b/base/commands/set/set.go index 6d504872e..c1819c860 100644 --- a/base/commands/set/set.go +++ b/base/commands/set/set.go @@ -4,71 +4,31 @@ package set import ( "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - setFlagName = "name" - setFlagShowType = "show-type" - setPropertyName = "set" -) - -type SetCommand struct{} +type Command struct{} -func (sc *SetCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("set") cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(setFlagName, "n", defaultSetName, false, "set name") - cc.AddBoolFlag(setFlagShowType, "", false, false, "add the type names to the output") - if !cc.Interactive() { - cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") - } cc.SetTopLevel(true) - cc.SetCommandUsage("set [command] [flags]") help := "Set operations" cc.SetCommandHelp(help, help) + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "set name") + cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") return nil } -func (sc *SetCommand) Exec(context.Context, plug.ExecContext) error { - return nil -} - -func (sc *SetCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { - ctx := context.TODO() - props.SetBlocking(setPropertyName, func() (any, error) { - name := ec.Props().GetString(setFlagName) - // empty set name is allowed - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - val, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting set %s", name)) - q, err := ci.Client().GetSet(ctx, name) - if err != nil { - return nil, err - } - return q, nil - }) - if err != nil { - return nil, err - } - stop() - return val.(*hazelcast.Set), nil - }) +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - cmd := &SetCommand{} - check.Must(plug.Registry.RegisterCommand("set", cmd)) - plug.Registry.RegisterAugmentor("20-set", cmd) + check.Must(plug.Registry.RegisterCommand("set", &Command{})) } diff --git a/base/commands/set/set_add.go b/base/commands/set/set_add.go index 31d06d6a9..e58d53f23 100644 --- a/base/commands/set/set_add.go +++ b/base/commands/set/set_add.go @@ -6,56 +6,71 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type SetAddCommand struct{} +type AddCommand struct{} -func (sc *SetAddCommand) Init(cc plug.InitContext) error { +func (AddCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("add") help := "Add values to the given Set" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) - cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) + commands.AddValueTypeFlag(cc) + cc.AddStringSliceArg(base.ArgValue, base.ArgTitleValue, 1, clc.MaxArgs) return nil } -func (sc *SetAddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(setFlagName) - sv, err := ec.Props().GetBlocking(setPropertyName) - if err != nil { - return err - } - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - s := sv.(*hazelcast.Set) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Adding values into set %s", name)) - for _, arg := range ec.GetStringSliceArg(argValue) { - vd, err := makeValueData(ec, ci, arg) +func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + s, err := ci.Client().GetSet(ctx, name) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Adding values into Set '%s'", name)) + var rows []output.Row + for _, arg := range ec.GetStringSliceArg(base.ArgValue) { + vd, err := commands.MakeValueData(ec, ci, arg) if err != nil { return nil, err } - _, err = s.Add(ctx, vd) + v, err := s.Add(ctx, vd) if err != nil { return nil, err } + rows = append(rows, output.Row{ + output.Column{ + Name: "Value", + Type: serialization.TypeString, + Value: arg, + }, + output.Column{ + Name: "Added", + Type: serialization.TypeBool, + Value: v, + }, + }) } - return nil, nil + return rows, nil }) if err != nil { return err } stop() - return nil + return ec.AddOutputRows(ctx, rowsV.([]output.Row)...) } func init() { - Must(plug.Registry.RegisterCommand("set:add", &SetAddCommand{})) + check.Must(plug.Registry.RegisterCommand("set:add", &AddCommand{})) } diff --git a/base/commands/set/set_clear.go b/base/commands/set/set_clear.go index 9ceddcc6a..68d0eeecc 100644 --- a/base/commands/set/set_clear.go +++ b/base/commands/set/set_clear.go @@ -3,60 +3,12 @@ package set import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type SetClearCommand struct{} - -func (qc *SetClearCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("clear") - help := "Delete all entries of a Set" - cc.SetCommandHelp(help, help) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the clear operation") - return nil -} - -func (qc *SetClearCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - sv, err := ec.Props().GetBlocking(setPropertyName) - if err != nil { - return err - } - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("Set content will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - s := sv.(*hazelcast.Set) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Clearing set %s", s.Name())) - if err = s.Clear(ctx); err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("set:clear", &SetClearCommand{})) + c := commands.NewClearCommand("Set", getSet) + check.Must(plug.Registry.RegisterCommand("set:clear", c)) } diff --git a/base/commands/set/set_destroy.go b/base/commands/set/set_destroy.go index 6274d57c0..c70ed31b8 100644 --- a/base/commands/set/set_destroy.go +++ b/base/commands/set/set_destroy.go @@ -3,62 +3,12 @@ package set import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type SetDestroyCommand struct{} - -func (qc *SetDestroyCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("destroy") - long := `Destroy a Set -This command will delete the Set and the data in it will not be available anymore.` - short := "Destroy a Set" - cc.SetCommandHelp(long, short) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - return nil -} - -func (qc *SetDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - sv, err := ec.Props().GetBlocking(setPropertyName) - if err != nil { - return err - } - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("Set will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - s := sv.(*hazelcast.Set) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Destroying set %s", s.Name())) - if err := s.Destroy(ctx); err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("set:destroy", &SetDestroyCommand{})) + c := commands.NewDestroyCommand("Set", getSet) + check.Must(plug.Registry.RegisterCommand("set:destroy", c)) } diff --git a/base/commands/set/set_get_all.go b/base/commands/set/set_get_all.go index 7899a9ead..f71c77db9 100644 --- a/base/commands/set/set_get_all.go +++ b/base/commands/set/set_get_all.go @@ -6,9 +6,10 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -16,61 +17,71 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type SetGetAllCommand struct{} +type GetAllCommand struct{} -func (sc *SetGetAllCommand) Init(cc plug.InitContext) error { +func (GetAllCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("get-all") help := "Return the elements of the given Set" cc.SetCommandHelp(help, help) return nil } -func (sc *SetGetAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(setFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - req := codec.EncodeSetGetAllRequest(name) - pID, err := stringToPartitionID(ci, name) - if err != nil { - return err - } - sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Removing from set %s", name)) - return ci.InvokeOnPartition(ctx, req, pID, nil) +func (GetAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + req := codec.EncodeSetGetAllRequest(name) + pID, err := internal.StringToPartitionID(ci, name) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Removing from Set '%s'", name)) + resp, err := ci.InvokeOnPartition(ctx, req, pID, nil) + if err != nil { + return nil, err + } + data := codec.DecodeSetGetAllResponse(resp) + showType := ec.Props().GetBool(base.FlagShowType) + var rows []output.Row + for _, r := range data { + val, err := ci.DecodeData(*r) + if err != nil { + ec.Logger().Info("The value was not decoded, due to error: %s", err.Error()) + val = serialization.NondecodedType(serialization.TypeToLabel(r.Type())) + } + row := output.Row{ + { + Name: "Value", + Type: r.Type(), + Value: val, + }, + } + if showType { + row = append(row, output.Column{ + Name: output.NameValueType, + Type: serialization.TypeString, + Value: serialization.TypeToLabel(r.Type()), + }) + } + rows = append(rows, row) + } + return rows, nil }) if err != nil { return err } stop() - resp := codec.DecodeSetGetAllResponse(sv.(*hazelcast.ClientMessage)) - var rows []output.Row - for _, r := range resp { - val, err := ci.DecodeData(*r) - if err != nil { - ec.Logger().Info("The value was not decoded, due to error: %s", err.Error()) - val = serialization.NondecodedType(serialization.TypeToLabel(r.Type())) - } - row := output.Row{ - { - Name: "Value", - Type: r.Type(), - Value: val, - }, - } - if ec.Props().GetBool(setFlagShowType) { - row = append(row, output.Column{ - Name: output.NameValueType, - Type: serialization.TypeString, - Value: serialization.TypeToLabel(r.Type()), - }) - } - rows = append(rows, row) + rows := rowsV.([]output.Row) + if len(rows) == 0 { + ec.PrintlnUnnecessary("OK No items in the set.") + return nil } - return ec.AddOutputRows(ctx, rows...) + return ec.AddOutputRows(ctx, rowsV.([]output.Row)...) } func init() { - Must(plug.Registry.RegisterCommand("set:get-all", &SetGetAllCommand{})) + Must(plug.Registry.RegisterCommand("set:get-all", &GetAllCommand{})) } diff --git a/base/commands/set/set_remove.go b/base/commands/set/set_remove.go index 178c9afb2..8ee576978 100644 --- a/base/commands/set/set_remove.go +++ b/base/commands/set/set_remove.go @@ -6,7 +6,11 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -14,34 +18,34 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type SetRemoveCommand struct{} +type RemoveCommand struct{} -func (sc *SetRemoveCommand) Init(cc plug.InitContext) error { +func (RemoveCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("remove") help := "Remove values from the given Set" cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) - cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) + commands.AddValueTypeFlag(cc) + cc.AddStringSliceArg(base.ArgValue, base.ArgTitleValue, 1, clc.MaxArgs) return nil } -func (sc *SetRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(setFlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - +func (sc *RemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) rows, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Removing from set %s", name)) + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Removing from Set '%s'", name)) + showType := ec.Props().GetBool(base.FlagShowType) var rows []output.Row - for _, arg := range ec.GetStringSliceArg(argValue) { - vd, err := makeValueData(ec, ci, arg) + for _, arg := range ec.GetStringSliceArg(base.ArgValue) { + vd, err := commands.MakeValueData(ec, ci, arg) if err != nil { return nil, err } req := codec.EncodeSetRemoveRequest(name, vd) - pID, err := stringToPartitionID(ci, name) + pID, err := internal.StringToPartitionID(ci, name) if err != nil { return nil, err } @@ -57,7 +61,7 @@ func (sc *SetRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error Value: resp, }, } - if ec.Props().GetBool(setFlagShowType) { + if showType { row = append(row, output.Column{ Name: output.NameValueType, Type: serialization.TypeString, @@ -76,5 +80,5 @@ func (sc *SetRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error } func init() { - Must(plug.Registry.RegisterCommand("set:remove", &SetRemoveCommand{})) + Must(plug.Registry.RegisterCommand("set:remove", &RemoveCommand{})) } diff --git a/base/commands/set/set_size.go b/base/commands/set/set_size.go index 5349eeda1..086b52673 100644 --- a/base/commands/set/set_size.go +++ b/base/commands/set/set_size.go @@ -3,51 +3,12 @@ package set import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type SetSizeCommand struct{} - -func (sc *SetSizeCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("size") - help := "Return the size of the given Set" - cc.SetCommandHelp(help, help) - return nil -} - -func (sc *SetSizeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - name := ec.Props().GetString(setFlagName) - qv, err := ec.Props().GetBlocking(setPropertyName) - if err != nil { - return err - } - s := qv.(*hazelcast.Set) - sv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting the size of the set %s", name)) - return s.Size(ctx) - }) - if err != nil { - return err - } - stop() - return ec.AddOutputRows(ctx, output.Row{ - { - Name: "Size", - Type: serialization.TypeInt32, - Value: int32(sv.(int)), - }, - }) -} - func init() { - Must(plug.Registry.RegisterCommand("set:size", &SetSizeCommand{})) + c := commands.NewSizeCommand("Set", getSet) + check.Must(plug.Registry.RegisterCommand("set:size", c)) } diff --git a/base/commands/shell.go b/base/commands/shell.go index 73b0051a8..692464f9b 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -15,7 +15,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/shell" puberrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/str" "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" @@ -74,11 +74,10 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext logLevel := strings.ToUpper(ec.Props().GetString(clc.PropertyLogLevel)) logText = fmt.Sprintf("Log %9s : %s", logLevel, logPath) } - I2(fmt.Fprintf(ec.Stdout(), banner, internal.Version, cfgText, logText)) + check.I2(fmt.Fprintf(ec.Stdout(), banner, internal.Version, cfgText, logText)) } - verbose := ec.Props().GetBool(clc.PropertyVerbose) endLineFn := makeEndLineFunc() - textFn := makeTextFunc(m, ec, verbose, false, false, func(shortcut string) bool { + textFn := makeTextFunc(m, ec, func(shortcut string) bool { cm.mu.RLock() _, ok := cm.shortcuts[shortcut] cm.mu.RUnlock() @@ -117,8 +116,6 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext return sh.Start(ctx) } -func (*ShellCommand) Unwrappable() {} - func init() { - Must(plug.Registry.RegisterCommand("shell", &ShellCommand{})) + check.Must(plug.Registry.RegisterCommand("shell", &ShellCommand{})) } diff --git a/base/commands/shell_it_test.go b/base/commands/shell_it_test.go index d69aff6e0..c5a51666d 100644 --- a/base/commands/shell_it_test.go +++ b/base/commands/shell_it_test.go @@ -37,12 +37,12 @@ func shellErrorsTest(t *testing.T) { { name: "invalid command", command: "\\foobar", - errText: "unknown command \\foobar", + errText: "Unknown command \\foobar", }, { name: "invalid flag", command: "\\object list --foobar", - errText: "unknown flag: --foobar", + errText: "Unknown flag: --foobar", }, } for _, tc := range testCases { @@ -54,7 +54,7 @@ func shellErrorsTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.WriteStdinString(tc.command + "\n") - tcx.AssertStderrEquals(fmt.Sprintf("Error: %s\n", tc.errText)) + tcx.AssertStderrContains(fmt.Sprintf("ERROR %s\n", tc.errText)) }) }) }) diff --git a/base/commands/shell_script_common.go b/base/commands/shell_script_common.go index d9e4f078a..4f97c824f 100644 --- a/base/commands/shell_script_common.go +++ b/base/commands/shell_script_common.go @@ -47,7 +47,7 @@ func makeEndLineFunc() shell.EndLineFn { } } -func makeTextFunc(m *cmd.Main, ec plug.ExecContext, verbose, ignoreErrors, echo bool, sf shortcutFunc) shell.TextFn { +func makeTextFunc(m *cmd.Main, ec plug.ExecContext, sf shortcutFunc) shell.TextFn { return func(ctx context.Context, stdout io.Writer, text string) error { if strings.HasPrefix(strings.TrimSpace(text), shell.CmdPrefix) { parts := strings.Fields(text) @@ -64,7 +64,7 @@ func makeTextFunc(m *cmd.Main, ec plug.ExecContext, verbose, ignoreErrors, echo return m.Execute(ctx, args...) } } - f, err := shell.ConvertStatement(ctx, ec, text, verbose) + f, err := shell.ConvertStatement(ctx, ec, text) if err != nil { if errors.Is(err, shell.ErrHelp) { check.I2(fmt.Fprintln(stdout, shell.InteractiveHelp())) @@ -72,9 +72,6 @@ func makeTextFunc(m *cmd.Main, ec plug.ExecContext, verbose, ignoreErrors, echo } return err } - if w, ok := ec.(plug.ResultWrapper); ok { - return w.WrapResult(f) - } return f() } } diff --git a/base/commands/snapshot/snapshot.go b/base/commands/snapshot/snapshot.go index ee65014cb..87c404a9e 100644 --- a/base/commands/snapshot/snapshot.go +++ b/base/commands/snapshot/snapshot.go @@ -10,22 +10,22 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type Cmd struct{} +type Command struct{} -func (cm Cmd) Init(cc plug.InitContext) error { +func (cm Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("snapshot") cc.AddCommandGroup(clc.GroupJetID, clc.GroupJetTitle) cc.SetCommandGroup(clc.GroupJetID) cc.SetTopLevel(true) help := "Jet snapshot operations" - cc.SetCommandUsage("snapshot [command]") cc.SetCommandHelp(help, help) return nil } -func (cm Cmd) Exec(context.Context, plug.ExecContext) error { +func (cm Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - check.Must(plug.Registry.RegisterCommand("snapshot", &Cmd{})) + check.Must(plug.Registry.RegisterCommand("snapshot", &Command{})) } diff --git a/base/commands/snapshot/snapshot_delete.go b/base/commands/snapshot/snapshot_delete.go index 089a77d83..8c5aebec0 100644 --- a/base/commands/snapshot/snapshot_delete.go +++ b/base/commands/snapshot/snapshot_delete.go @@ -4,8 +4,10 @@ package snapshot import ( "context" + "fmt" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -15,9 +17,9 @@ const ( argTitleSnapshotName = "snapshot name" ) -type DeleteCmd struct{} +type DeleteCommand struct{} -func (cm DeleteCmd) Init(cc plug.InitContext) error { +func (DeleteCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("delete") help := "Delete a snapshot" cc.SetCommandHelp(help, help) @@ -25,14 +27,14 @@ func (cm DeleteCmd) Init(cc plug.InitContext) error { return nil } -func (cm DeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } +func (DeleteCommand) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.GetStringArg(argTitleSnapshotName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Deleting the snapshot") + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + sp.SetText(fmt.Sprintf("Deleting the snapshot '%s'", name)) sm, err := ci.Client().GetMap(ctx, jetExportedSnapshotsMap) if err != nil { return nil, err @@ -50,9 +52,11 @@ func (cm DeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return err } stop() + msg := fmt.Sprintf("OK Destroyed snapshot '%s'.", name) + ec.PrintlnUnnecessary(msg) return nil } func init() { - check.Must(plug.Registry.RegisterCommand("snapshot:delete", DeleteCmd{})) + check.Must(plug.Registry.RegisterCommand("snapshot:delete", DeleteCommand{})) } diff --git a/base/commands/snapshot/snapshot_list.go b/base/commands/snapshot/snapshot_list.go index e7624901c..bd9274e08 100644 --- a/base/commands/snapshot/snapshot_list.go +++ b/base/commands/snapshot/snapshot_list.go @@ -10,36 +10,37 @@ import ( "github.com/hazelcast/hazelcast-go-client/types" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ListCmd struct{} +type ListCommand struct{} -func (cm ListCmd) Init(cc plug.InitContext) error { +func (ListCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("list") help := "List snapshots" cc.SetCommandHelp(help, help) return nil } -func (cm ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } +func (ListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { rows, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText("Getting the snapshot list") m, err := ci.Client().GetMap(ctx, jetExportedSnapshotsMap) if err != nil { return nil, err } - rows, err := listDetailRows(ctx, *m) + rows, err := listDetailRows(ctx, m) if err != nil { ec.Logger().Error(err) - rows, err = listRows(ctx, *m) + rows, err = listRows(ctx, m) if err != nil { return nil, err } @@ -53,11 +54,7 @@ func (cm ListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { return ec.AddOutputRows(ctx, rows.([]output.Row)...) } -func init() { - check.Must(plug.Registry.RegisterCommand("snapshot:list", ListCmd{})) -} - -func listDetailRows(ctx context.Context, m hazelcast.Map) ([]output.Row, error) { +func listDetailRows(ctx context.Context, m *hazelcast.Map) ([]output.Row, error) { esd, err := m.GetEntrySet(ctx) if err != nil { return nil, err @@ -89,7 +86,7 @@ func listDetailRows(ctx context.Context, m hazelcast.Map) ([]output.Row, error) return rows, nil } -func listRows(ctx context.Context, m hazelcast.Map) ([]output.Row, error) { +func listRows(ctx context.Context, m *hazelcast.Map) ([]output.Row, error) { es, err := m.GetKeySet(ctx) if err != nil { return nil, err @@ -108,3 +105,7 @@ func listRows(ctx context.Context, m hazelcast.Map) ([]output.Row, error) { } return rows, nil } + +func init() { + check.Must(plug.Registry.RegisterCommand("snapshot:list", ListCommand{})) +} diff --git a/base/commands/sql/sql.go b/base/commands/sql/sql.go index 571c584c2..f6b85d8c7 100644 --- a/base/commands/sql/sql.go +++ b/base/commands/sql/sql.go @@ -29,7 +29,7 @@ type arg0er interface { type SQLCommand struct{} -func (cm *SQLCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { +func (SQLCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { // set the default format to table in the interactive mode if ecc, ok := ec.(arg0er); ok { if ec.CommandName() == ecc.Arg0()+" shell" && len(ec.Args()) == 0 { @@ -39,7 +39,7 @@ func (cm *SQLCommand) Augment(ec plug.ExecContext, props *plug.Properties) error return nil } -func (cm *SQLCommand) Init(cc plug.InitContext) error { +func (SQLCommand) Init(cc plug.InitContext) error { if cc.Interactive() { return errors.ErrNotAvailable } @@ -59,31 +59,30 @@ having version %s or better. return nil } -func (cm *SQLCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (SQLCommand) Exec(ctx context.Context, ec plug.ExecContext) error { // this method is only for the non-interactive mode if len(ec.Args()) < 1 { return nil } - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { - return fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) - } query := ec.GetStringArg(argQuery) - res, stop, err := cm.execQuery(ctx, query, ec) + resV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { + return nil, fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) + } + sp.SetText("Executing SQL") + return clcsql.ExecSQL(ctx, ec, query) + }) if err != nil { return err } // this should be deferred because UpdateOutput will iterate on the result defer stop() - verbose := ec.Props().GetBool(clc.PropertyVerbose) - return clcsql.UpdateOutput(ctx, ec, res, verbose) -} - -func (cm *SQLCommand) execQuery(ctx context.Context, query string, ec plug.ExecContext) (sql.Result, context.CancelFunc, error) { - return clcsql.ExecSQL(ctx, ec, query) + res := resV.(sql.Result) + return clcsql.UpdateOutput(ctx, ec, res) } func init() { diff --git a/base/commands/sql/sql_it_test.go b/base/commands/sql/sql_it_test.go index 60c2c4264..753b20c47 100644 --- a/base/commands/sql/sql_it_test.go +++ b/base/commands/sql/sql_it_test.go @@ -13,6 +13,7 @@ import ( hz "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-go-client/serialization" "github.com/hazelcast/hazelcast-go-client/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" _ "github.com/hazelcast/hazelcast-commandline-client/base/commands" @@ -227,10 +228,10 @@ func sqlSuggestion_NonInteractive(t *testing.T) { ctx := context.Background() it.WithMap(tcx, func(m *hazelcast.Map) { check.Must(m.Set(ctx, "foo", "bar")) - // ignoring the error here - _ = tcx.CLC().Execute(ctx, "sql", fmt.Sprintf(`SELECT * FROM "%s";`, m.Name())) - tcx.AssertStderrContains("CREATE MAPPING") - tcx.AssertStderrContains("--use-mapping-suggestion") + err := tcx.CLC().Execute(ctx, "sql", fmt.Sprintf(`SELECT * FROM "%s";`, m.Name())) + t.Logf("ERROR %s", err.Error()) + assert.Contains(t, err.Error(), "CREATE MAPPING") + assert.Contains(t, err.Error(), "--use-mapping-suggestion") check.Must(tcx.CLC().Execute(ctx, "sql", fmt.Sprintf(`SELECT * FROM "%s";`, m.Name()), "--use-mapping-suggestion")) tcx.AssertStdoutContains("foo\tbar") }) diff --git a/base/commands/topic/common.go b/base/commands/topic/common.go index 8173104b1..33786f536 100644 --- a/base/commands/topic/common.go +++ b/base/commands/topic/common.go @@ -5,125 +5,21 @@ package topic import ( "context" "fmt" - "os" - "os/signal" - "strings" "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/internal" - "github.com/hazelcast/hazelcast-commandline-client/internal/mk" - "github.com/hazelcast/hazelcast-commandline-client/internal/output" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" - "github.com/hazelcast/hazelcast-commandline-client/internal/topic" ) -func addValueTypeFlag(cc plug.InitContext) { - help := fmt.Sprintf("value type (one of: %s)", strings.Join(internal.SupportedTypeNames, ", ")) - cc.AddStringFlag(topicFlagValueType, "v", "string", false, help) -} - -func makeValueData(ec plug.ExecContext, ci *hazelcast.ClientInternal, valueStr string) (hazelcast.Data, error) { - vt := ec.Props().GetString(topicFlagValueType) - if vt == "" { - vt = "string" - } - value, err := mk.ValueFromString(valueStr, vt) +func getTopic(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.Topic, error) { + name := ec.Props().GetString(base.FlagName) + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } - return ci.EncodeData(value) -} - -func updateOutput(ctx context.Context, ec plug.ExecContext, events <-chan topic.TopicEvent) error { - wantedCount := ec.Props().GetInt(topicFlagCount) - printedCount := 0 - rowCh := make(chan output.Row) - ctx, stop := signal.NotifyContext(ctx, os.Interrupt, os.Kill) - defer stop() - name := ec.Props().GetString(topicFlagName) - ec.PrintlnUnnecessary(fmt.Sprintf("Listening to messages of topic %s", name)) - go func() { - loop: - for { - var e topic.TopicEvent - select { - case e = <-events: - case <-ctx.Done(): - break loop - } - row := eventRow(e, ec) - select { - case rowCh <- row: - case <-ctx.Done(): - break loop - } - printedCount++ - if wantedCount > 0 && printedCount == int(wantedCount) { - break loop - } - } - close(rowCh) - }() - return ec.AddOutputStream(ctx, rowCh) -} - -func eventRow(e topic.TopicEvent, ec plug.ExecContext) (row output.Row) { - if ec.Props().GetBool(clc.PropertyVerbose) { - row = append(row, - output.Column{ - Name: "Time", - Type: serialization.TypeJavaLocalDateTime, - Value: e.PublishTime, - }, - output.Column{ - Name: "Topic", - Type: serialization.TypeString, - Value: e.TopicName, - }, - output.Column{ - Name: "Value", - Type: e.ValueType, - Value: e.Value, - }, - ) - if ec.Props().GetBool(topicFlagShowType) { - row = append(row, - output.Column{ - Name: "Type", - Type: serialization.TypeString, - Value: serialization.TypeToLabel(e.ValueType), - }) - } - row = append(row, - output.Column{ - Name: "Member UUID", - Type: serialization.TypeUUID, - Value: e.Member.UUID, - }, - output.Column{ - Name: "Member Address", - Type: serialization.TypeString, - Value: string(e.Member.Address), - }) - return row - } - row = output.Row{ - output.Column{ - Name: "Value", - Type: e.ValueType, - Value: e.Value, - }, - } - if ec.Props().GetBool(topicFlagShowType) { - row = append(row, - output.Column{ - Name: "Type", - Type: serialization.TypeString, - Value: serialization.TypeToLabel(e.ValueType), - }) - } - return row + sp.SetText(fmt.Sprintf("Getting Topic '%s'", name)) + return ci.Client().GetTopic(ctx, name) } diff --git a/base/commands/topic/const.go b/base/commands/topic/const.go index dc49cd972..bbeeec22c 100644 --- a/base/commands/topic/const.go +++ b/base/commands/topic/const.go @@ -3,7 +3,5 @@ package topic const ( - topicFlagValueType = "value-type" - topicFlagCount = "count" - defaultTopicName = "default" + topicFlagCount = "count" ) diff --git a/base/commands/topic/topic.go b/base/commands/topic/topic.go index ab8cc4ccf..e8a180242 100644 --- a/base/commands/topic/topic.go +++ b/base/commands/topic/topic.go @@ -4,72 +4,31 @@ package topic import ( "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - topicFlagName = "name" - topicFlagShowType = "show-type" - topicPropertyName = "topic" -) - -type TopicCommand struct { -} +type Command struct{} -func (mc *TopicCommand) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("topic") cc.AddCommandGroup(clc.GroupDDSID, clc.GroupDDSTitle) cc.SetCommandGroup(clc.GroupDDSID) - cc.AddStringFlag(topicFlagName, "n", defaultTopicName, false, "topic name") - cc.AddBoolFlag(topicFlagShowType, "", false, false, "add the type names to the output") - if !cc.Interactive() { - cc.AddStringFlag(clc.PropertySchemaDir, "", paths.Schemas(), false, "set the schema directory") - } cc.SetTopLevel(true) - cc.SetCommandUsage("topic [command] [flags]") help := "Topic operations" cc.SetCommandHelp(help, help) + cc.AddStringFlag(base.FlagName, "n", base.DefaultName, false, "topic name") + cc.AddBoolFlag(base.FlagShowType, "", false, false, "add the type names to the output") return nil } -func (tc *TopicCommand) Exec(context.Context, plug.ExecContext) error { - return nil -} - -func (tc *TopicCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { - ctx := context.TODO() - props.SetBlocking(topicPropertyName, func() (any, error) { - topicName := ec.Props().GetString(topicFlagName) - // empty topic name is allowed - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } - tv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Getting topic %s", topicName)) - t, err := ci.Client().GetTopic(ctx, topicName) - if err != nil { - return nil, err - } - return t, nil - }) - if err != nil { - return nil, err - } - stop() - return tv.(*hazelcast.Topic), nil - }) +func (Command) Exec(context.Context, plug.ExecContext) error { return nil } func init() { - cmd := &TopicCommand{} - Must(plug.Registry.RegisterCommand("topic", cmd)) - plug.Registry.RegisterAugmentor("20-topic", cmd) + Must(plug.Registry.RegisterCommand("topic", &Command{})) } diff --git a/base/commands/topic/topic_destroy.go b/base/commands/topic/topic_destroy.go index 15e2756eb..4a0735f68 100644 --- a/base/commands/topic/topic_destroy.go +++ b/base/commands/topic/topic_destroy.go @@ -3,65 +3,12 @@ package topic import ( - "context" - "fmt" - - "github.com/hazelcast/hazelcast-go-client" - - "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/errors" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" - - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) -type topicDestroyCommand struct{} - -func (tdc *topicDestroyCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("destroy") - long := `Destroy a Topic - -This command will delete the Topic and the data in it will not be available anymore.` - short := "Destroy a Topic" - cc.SetCommandHelp(long, short) - cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the destroy operation") - return nil -} - -func (tdc *topicDestroyCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - tp, err := ec.Props().GetBlocking(topicPropertyName) - if err != nil { - return err - } - autoYes := ec.Props().GetBool(clc.FlagAutoYes) - if !autoYes { - p := prompt.New(ec.Stdin(), ec.Stdout()) - yes, err := p.YesNo("Topic will be deleted irreversibly, proceed?") - if err != nil { - ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) - return errors.ErrUserCancelled - } - if !yes { - return errors.ErrUserCancelled - } - } - t := tp.(*hazelcast.Topic) - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Destroying topic %s", t.Name())) - err := t.Destroy(ctx) - if err != nil { - return nil, err - } - return nil, nil - }) - if err != nil { - return err - } - stop() - return nil -} - func init() { - Must(plug.Registry.RegisterCommand("topic:destroy", &topicDestroyCommand{})) + c := commands.NewDestroyCommand("Topic", getTopic) + check.Must(plug.Registry.RegisterCommand("topic:destroy", c)) } diff --git a/base/commands/topic/topic_it_test.go b/base/commands/topic/topic_it_test.go index 5fa06c84c..5abe9318f 100644 --- a/base/commands/topic/topic_it_test.go +++ b/base/commands/topic/topic_it_test.go @@ -76,7 +76,6 @@ func subscribe_NonInteractiveTest(t *testing.T) { check.Must(tp.PublishAll(ctx, "value1", "value2")) tcx.AssertStdoutContains("value1") tcx.AssertStdoutContains("value2") - tcx.AssertStderrContains("OK") }) }) } diff --git a/base/commands/topic/topic_publish.go b/base/commands/topic/topic_publish.go index 9a6455e21..45cc6f9ef 100644 --- a/base/commands/topic/topic_publish.go +++ b/base/commands/topic/topic_publish.go @@ -6,62 +6,63 @@ import ( "context" "fmt" - "github.com/hazelcast/hazelcast-go-client" - + "github.com/hazelcast/hazelcast-commandline-client/base" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/topic" - + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/mk" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -const ( - argValue = "value" - argTitleValue = "value" -) - -type topicPublishCommand struct{} +type PublishCommand struct{} -func (tpc *topicPublishCommand) Init(cc plug.InitContext) error { - cc.SetCommandUsage("publish [values] [flags]") +func (PublishCommand) Init(cc plug.InitContext) error { + cc.SetCommandUsage("publish") help := "Publish new messages for a Topic." cc.SetCommandHelp(help, help) - addValueTypeFlag(cc) - cc.AddStringSliceArg(argValue, argTitleValue, 1, clc.MaxArgs) + commands.AddValueTypeFlag(cc) + cc.AddStringSliceArg(base.ArgValue, base.ArgTitleValue, 1, clc.MaxArgs) return nil } -func (tpc *topicPublishCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - topicName := ec.Props().GetString(topicFlagName) - // get the topic just to ensure the corresponding proxy is created - _, err := ec.Props().GetBlocking(topicPropertyName) - if err != nil { - return err - } - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } - - _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Publishing values into topic %s", topicName)) - var vals []hazelcast.Data - for _, valStr := range ec.GetStringSliceArg(argValue) { - val, err := makeValueData(ec, ci, valStr) +func (PublishCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) + vt := ec.Props().GetString(base.FlagValueType) + countV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + // get the topic just to ensure the corresponding proxy is created + t, err := ci.Client().GetTopic(ctx, name) + if err != nil { + return nil, err + } + args := ec.GetStringSliceArg(base.ArgValue) + vs := make([]any, len(args)) + for i, arg := range args { + val, err := mk.ValueFromString(arg, vt) if err != nil { return nil, err } - vals = append(vals, val) + vs[i] = val + } + sp.SetText(fmt.Sprintf("Publishing %d values to Topic '%s'", len(vs), name)) + if err := t.PublishAll(ctx, vs...); err != nil { + return nil, fmt.Errorf("publishing values: %w", err) } - return nil, topic.PublishAll(ctx, ci, topicName, vals) + return len(vs), nil }) if err != nil { return err } stop() + msg := fmt.Sprintf("OK Published %d values to Topic '%s'.", countV.(int), name) + ec.PrintlnUnnecessary(msg) return nil } func init() { - Must(plug.Registry.RegisterCommand("topic:publish", &topicPublishCommand{})) + Must(plug.Registry.RegisterCommand("topic:publish", &PublishCommand{})) } diff --git a/base/commands/topic/topic_subscribe.go b/base/commands/topic/topic_subscribe.go index da9834f84..86140d4c3 100644 --- a/base/commands/topic/topic_subscribe.go +++ b/base/commands/topic/topic_subscribe.go @@ -5,18 +5,27 @@ package topic import ( "context" "fmt" + "os" + "os/signal" + "time" + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-go-client/cluster" "github.com/hazelcast/hazelcast-go-client/types" + "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-commandline-client/internal/topic" + "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type topicSubscribeCommand struct{} +type SubscribeCommand struct{} -func (tsc *topicSubscribeCommand) Init(cc plug.InitContext) error { +func (SubscribeCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("subscribe") help := "Subscribe to a Topic for new messages." cc.SetCommandHelp(help, help) @@ -24,17 +33,17 @@ func (tsc *topicSubscribeCommand) Init(cc plug.InitContext) error { return nil } -func (tsc *topicSubscribeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { - topicName := ec.Props().GetString(topicFlagName) +func (SubscribeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + name := ec.Props().GetString(base.FlagName) ci, err := ec.ClientInternal(ctx) if err != nil { return err } - events := make(chan topic.TopicEvent, 1) + events := make(chan TopicEvent, 1) // Channel is not closed intentionally sid, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText(fmt.Sprintf("Listening to messages of topic %s", topicName)) - sid, err := topic.AddListener(ctx, ci, topicName, ec.Logger(), func(event topic.TopicEvent) { + sp.SetText(fmt.Sprintf("Listening to messages of topic %s", name)) + sid, err := addListener(ctx, ci, name, ec.Logger(), func(event TopicEvent) { select { case events <- event: case <-ctx.Done(): @@ -48,11 +57,151 @@ func (tsc *topicSubscribeCommand) Exec(ctx context.Context, ec plug.ExecContext) if err != nil { return err } - defer topic.RemoveListener(ctx, ci, sid.(types.UUID)) + defer removeListener(ctx, ci, sid.(types.UUID)) defer stop() return updateOutput(ctx, ec, events) } +type TopicEvent struct { + PublishTime time.Time + Value any + ValueType int32 + TopicName string + Member cluster.MemberInfo +} + +func newTopicEvent(name string, value any, valueType int32, publishTime time.Time, member cluster.MemberInfo) TopicEvent { + return TopicEvent{ + TopicName: name, + Value: value, + ValueType: valueType, + PublishTime: publishTime, + Member: member, + } +} + +func addListener(ctx context.Context, ci *hazelcast.ClientInternal, topic string, logger log.Logger, handler func(event TopicEvent)) (types.UUID, error) { + subscriptionID := types.NewUUID() + addRequest := codec.EncodeTopicAddMessageListenerRequest(topic, false) + removeRequest := codec.EncodeTopicRemoveMessageListenerRequest(topic, subscriptionID) + listenerHandler := func(msg *hazelcast.ClientMessage) { + codec.HandleTopicAddMessageListener(msg, func(itemData hazelcast.Data, publishTime int64, uuid types.UUID) { + itemType := itemData.Type() + item, err := ci.DecodeData(itemData) + if err != nil { + logger.Warn("The value was not decoded, due to error: %s", err.Error()) + item = serialization.NondecodedType(serialization.TypeToLabel(itemType)) + } + var member cluster.MemberInfo + if m := ci.ClusterService().GetMemberByUUID(uuid); m != nil { + member = *m + } + handler(newTopicEvent(topic, item, itemType, time.Unix(0, publishTime*1_000_000), member)) + }) + } + binder := ci.ListenerBinder() + err := binder.Add(ctx, subscriptionID, addRequest, removeRequest, listenerHandler) + return subscriptionID, err +} + +// removeListener removes the given subscription from this topic. +func removeListener(ctx context.Context, ci *hazelcast.ClientInternal, subscriptionID types.UUID) error { + return ci.ListenerBinder().Remove(ctx, subscriptionID) +} + +func updateOutput(ctx context.Context, ec plug.ExecContext, events <-chan TopicEvent) error { + wantedCount := int(ec.Props().GetInt(topicFlagCount)) + rowCh := make(chan output.Row) + ctx, stop := signal.NotifyContext(ctx, os.Interrupt, os.Kill) + defer stop() + name := ec.Props().GetString(base.FlagName) + ec.PrintlnUnnecessary(fmt.Sprintf("Listening to messages of Topic '%s'", name)) + go retrieveMessages(ctx, ec, wantedCount, events, rowCh) + return ec.AddOutputStream(ctx, rowCh) +} + +func retrieveMessages(ctx context.Context, ec plug.ExecContext, wanted int, events <-chan TopicEvent, rowCh chan<- output.Row) { + printed := 0 +loop: + for { + var e TopicEvent + select { + case e = <-events: + case <-ctx.Done(): + break loop + } + row := eventRow(e, ec) + select { + case rowCh <- row: + case <-ctx.Done(): + break loop + } + printed++ + if wanted > 0 && printed == wanted { + break loop + } + } + close(rowCh) +} + +func eventRow(e TopicEvent, ec plug.ExecContext) (row output.Row) { + if ec.Props().GetBool(clc.PropertyVerbose) { + row = append(row, + output.Column{ + Name: "Time", + Type: serialization.TypeJavaLocalDateTime, + Value: e.PublishTime, + }, + output.Column{ + Name: "Topic", + Type: serialization.TypeString, + Value: e.TopicName, + }, + output.Column{ + Name: "Value", + Type: e.ValueType, + Value: e.Value, + }, + ) + if ec.Props().GetBool(base.FlagShowType) { + row = append(row, + output.Column{ + Name: "Type", + Type: serialization.TypeString, + Value: serialization.TypeToLabel(e.ValueType), + }) + } + row = append(row, + output.Column{ + Name: "Member UUID", + Type: serialization.TypeUUID, + Value: e.Member.UUID, + }, + output.Column{ + Name: "Member Address", + Type: serialization.TypeString, + Value: string(e.Member.Address), + }) + return row + } + row = output.Row{ + output.Column{ + Name: "Value", + Type: e.ValueType, + Value: e.Value, + }, + } + if ec.Props().GetBool(base.FlagShowType) { + row = append(row, + output.Column{ + Name: "Type", + Type: serialization.TypeString, + Value: serialization.TypeToLabel(e.ValueType), + }) + } + return row +} + func init() { - Must(plug.Registry.RegisterCommand("topic:subscribe", &topicSubscribeCommand{})) + Must(plug.Registry.RegisterCommand("topic:subscribe", &SubscribeCommand{})) } diff --git a/base/commands/version.go b/base/commands/version.go index 5c282a78e..255100c1c 100644 --- a/base/commands/version.go +++ b/base/commands/version.go @@ -11,30 +11,28 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" - - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) -type VersionCommand struct { -} +type VersionCommand struct{} -func (vc VersionCommand) Init(cc plug.InitContext) error { +func (VersionCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("version") help := "Print the version" cc.SetCommandHelp(help, help) return nil } -func (vc VersionCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (VersionCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if ec.Props().GetBool(clc.PropertyVerbose) { return ec.AddOutputRows(ctx, - vc.row("Hazelcast CLC", internal.Version), - vc.row("Latest Git Commit Hash", internal.GitCommit), - vc.row("Hazelcast Go Client", hazelcast.ClientVersion), - vc.row("Go", fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)), + makeRow("Hazelcast CLC", internal.Version), + makeRow("Latest Git Commit Hash", internal.GitCommit), + makeRow("Hazelcast Go Client", hazelcast.ClientVersion), + makeRow("Go", fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)), ) } return ec.AddOutputRows(ctx, output.Row{ @@ -46,7 +44,7 @@ func (vc VersionCommand) Exec(ctx context.Context, ec plug.ExecContext) error { }) } -func (vc VersionCommand) row(key, value string) output.Row { +func makeRow(key, value string) output.Row { return output.Row{ output.Column{ Name: "Name", @@ -61,8 +59,6 @@ func (vc VersionCommand) row(key, value string) output.Row { } } -func (VersionCommand) Unwrappable() {} - func init() { - Must(plug.Registry.RegisterCommand("version", &VersionCommand{})) + check.Must(plug.Registry.RegisterCommand("version", &VersionCommand{})) } diff --git a/base/commands/viridian/custom_class_delete.go b/base/commands/viridian/custom_class_delete.go index 5c1b08006..f15db97d4 100644 --- a/base/commands/viridian/custom_class_delete.go +++ b/base/commands/viridian/custom_class_delete.go @@ -7,16 +7,14 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) -type CustomClassDeleteCmd struct{} +type CustomClassDeleteCommand struct{} -func (cmd CustomClassDeleteCmd) Unwrappable() {} - -func (cmd CustomClassDeleteCmd) Init(cc plug.InitContext) error { +func (CustomClassDeleteCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("delete-custom-class") long := `Deletes a custom class from the given Viridian cluster. @@ -31,7 +29,7 @@ Make sure you login before running this command. return nil } -func (cmd CustomClassDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (CustomClassDeleteCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -68,5 +66,5 @@ func (cmd CustomClassDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) e } func init() { - Must(plug.Registry.RegisterCommand("viridian:delete-custom-class", &CustomClassDeleteCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:delete-custom-class", &CustomClassDeleteCommand{})) } diff --git a/base/commands/viridian/custom_class_download.go b/base/commands/viridian/custom_class_download.go index fdf98c9b2..4e3d643d0 100644 --- a/base/commands/viridian/custom_class_download.go +++ b/base/commands/viridian/custom_class_download.go @@ -7,7 +7,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" @@ -17,11 +17,9 @@ import ( const flagOutputPath = "output-path" -type CustomClassDownloadCmd struct{} +type CustomClassDownloadCommand struct{} -func (cmd CustomClassDownloadCmd) Unwrappable() {} - -func (cmd CustomClassDownloadCmd) Init(cc plug.InitContext) error { +func (CustomClassDownloadCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("download-custom-class") long := `Downloads a custom class from the given Viridian cluster. @@ -37,7 +35,7 @@ Make sure you login before running this command. return nil } -func (cmd CustomClassDownloadCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (CustomClassDownloadCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -90,5 +88,5 @@ func (cmd CustomClassDownloadCmd) Exec(ctx context.Context, ec plug.ExecContext) } func init() { - Must(plug.Registry.RegisterCommand("viridian:download-custom-class", &CustomClassDownloadCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:download-custom-class", &CustomClassDownloadCommand{})) } diff --git a/base/commands/viridian/custom_class_list.go b/base/commands/viridian/custom_class_list.go index 7483a5985..1c21b6012 100644 --- a/base/commands/viridian/custom_class_list.go +++ b/base/commands/viridian/custom_class_list.go @@ -6,18 +6,16 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) -type CustomClassListCmd struct{} +type CustomClassListCommand struct{} -func (cmd CustomClassListCmd) Unwrappable() {} - -func (cmd CustomClassListCmd) Init(cc plug.InitContext) error { +func (CustomClassListCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("list-custom-classes") long := `Lists all custom classes in the given Viridian cluster. @@ -30,7 +28,7 @@ Make sure you login before running this command. return nil } -func (cmd CustomClassListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (CustomClassListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -91,5 +89,5 @@ func (cmd CustomClassListCmd) Exec(ctx context.Context, ec plug.ExecContext) err } func init() { - Must(plug.Registry.RegisterCommand("viridian:list-custom-classes", &CustomClassListCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:list-custom-classes", &CustomClassListCommand{})) } diff --git a/base/commands/viridian/custom_class_upload.go b/base/commands/viridian/custom_class_upload.go index 9e5a7d180..1685be52d 100644 --- a/base/commands/viridian/custom_class_upload.go +++ b/base/commands/viridian/custom_class_upload.go @@ -6,7 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -15,11 +15,9 @@ const ( argTitlePath = "path" ) -type CustomClassUploadCmd struct{} +type CustomClassUploadCommand struct{} -func (cmd CustomClassUploadCmd) Unwrappable() {} - -func (cmd CustomClassUploadCmd) Init(cc plug.InitContext) error { +func (CustomClassUploadCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("upload-custom-class") long := `Uploads a new Custom Class to the specified Viridian cluster. @@ -33,7 +31,7 @@ Make sure you login before running this command. return nil } -func (cmd CustomClassUploadCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (CustomClassUploadCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -57,5 +55,5 @@ func (cmd CustomClassUploadCmd) Exec(ctx context.Context, ec plug.ExecContext) e } func init() { - Must(plug.Registry.RegisterCommand("viridian:upload-custom-class", &CustomClassUploadCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:upload-custom-class", &CustomClassUploadCommand{})) } diff --git a/base/commands/viridian/download_logs.go b/base/commands/viridian/download_logs.go index 7cd3c7240..bce1607dc 100644 --- a/base/commands/viridian/download_logs.go +++ b/base/commands/viridian/download_logs.go @@ -9,17 +9,15 @@ import ( "path/filepath" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type DownloadLogsCmd struct{} +type DownloadLogsCommand struct{} -func (cm DownloadLogsCmd) Unwrappable() {} - -func (cm DownloadLogsCmd) Init(cc plug.InitContext) error { +func (DownloadLogsCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("download-logs") long := `Downloads the logs of the given Viridian cluster for the logged in API key. @@ -33,7 +31,7 @@ Make sure you login before running this command. return nil } -func (cm DownloadLogsCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (DownloadLogsCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -69,10 +67,6 @@ func (cm DownloadLogsCmd) Exec(ctx context.Context, ec plug.ExecContext) error { }) } -func init() { - Must(plug.Registry.RegisterCommand("viridian:download-logs", &DownloadLogsCmd{})) -} - func validateOutputDir(dir string) error { info, err := os.Stat(dir) if err != nil { @@ -86,3 +80,7 @@ func validateOutputDir(dir string) error { } return fmt.Errorf("not a directory: %s", dir) } + +func init() { + check.Must(plug.Registry.RegisterCommand("viridian:download-logs", &DownloadLogsCommand{})) +} diff --git a/base/commands/viridian/viridian.go b/base/commands/viridian/viridian.go index 04dc500b4..9e894c922 100644 --- a/base/commands/viridian/viridian.go +++ b/base/commands/viridian/viridian.go @@ -5,26 +5,26 @@ package viridian import ( "context" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -type Cmd struct{} +type Command struct{} -func (cm Cmd) Init(cc plug.InitContext) error { +func (Command) Init(cc plug.InitContext) error { + cc.SetCommandUsage("viridian") + cc.AddCommandGroup("viridian", "Viridian") + cc.SetCommandGroup("viridian") cc.SetTopLevel(true) - cc.SetCommandUsage("viridian [command]") help := "Various Viridian operations" cc.SetCommandHelp(help, help) - cc.AddCommandGroup("viridian", "Viridian") - cc.SetCommandGroup("viridian") return nil } -func (cm Cmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (Command) Exec(ctx context.Context, ec plug.ExecContext) error { return nil } func init() { - Must(plug.Registry.RegisterCommand("viridian", &Cmd{})) + check.Must(plug.Registry.RegisterCommand("viridian", &Command{})) } diff --git a/base/commands/viridian/viridian_cluster_create.go b/base/commands/viridian/viridian_cluster_create.go index 3577bafce..be8dfdb12 100644 --- a/base/commands/viridian/viridian_cluster_create.go +++ b/base/commands/viridian/viridian_cluster_create.go @@ -8,18 +8,16 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) -type ClusterCreateCmd struct{} +type ClusterCreateCommand struct{} -func (cm ClusterCreateCmd) Unwrappable() {} - -func (cm ClusterCreateCmd) Init(cc plug.InitContext) error { +func (ClusterCreateCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("create-cluster") long := `Creates a Viridian cluster. @@ -34,7 +32,7 @@ Make sure you login before running this command. return nil } -func (cm ClusterCreateCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ClusterCreateCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -142,5 +140,5 @@ type createStageState struct { } func init() { - Must(plug.Registry.RegisterCommand("viridian:create-cluster", &ClusterCreateCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:create-cluster", &ClusterCreateCommand{})) } diff --git a/base/commands/viridian/viridian_cluster_delete.go b/base/commands/viridian/viridian_cluster_delete.go index c5d02f716..ee7ca67f3 100644 --- a/base/commands/viridian/viridian_cluster_delete.go +++ b/base/commands/viridian/viridian_cluster_delete.go @@ -8,7 +8,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" @@ -16,11 +16,9 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) -type ClusterDeleteCmd struct{} +type ClusterDeleteCommand struct{} -func (cm ClusterDeleteCmd) Unwrappable() {} - -func (cm ClusterDeleteCmd) Init(cc plug.InitContext) error { +func (ClusterDeleteCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("delete-cluster") long := `Deletes the given Viridian cluster. @@ -34,7 +32,7 @@ Make sure you login before running this command. return nil } -func (cm ClusterDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ClusterDeleteCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -87,5 +85,5 @@ func (cm ClusterDeleteCmd) Exec(ctx context.Context, ec plug.ExecContext) error } func init() { - Must(plug.Registry.RegisterCommand("viridian:delete-cluster", &ClusterDeleteCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:delete-cluster", &ClusterDeleteCommand{})) } diff --git a/base/commands/viridian/viridian_cluster_get.go b/base/commands/viridian/viridian_cluster_get.go index 38c0a90dd..20fd3241b 100644 --- a/base/commands/viridian/viridian_cluster_get.go +++ b/base/commands/viridian/viridian_cluster_get.go @@ -7,18 +7,16 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) -type ClusterGetCmd struct{} +type ClusterGetCommand struct{} -func (cm ClusterGetCmd) Unwrappable() {} - -func (cm ClusterGetCmd) Init(cc plug.InitContext) error { +func (ClusterGetCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("get-cluster") long := `Gets the information about the given Viridian cluster. @@ -31,7 +29,7 @@ Make sure you login before running this command. return nil } -func (cm ClusterGetCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ClusterGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -130,5 +128,5 @@ func regionTitleSlice(regions []viridian.Region) []string { } func init() { - Must(plug.Registry.RegisterCommand("viridian:get-cluster", &ClusterGetCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:get-cluster", &ClusterGetCommand{})) } diff --git a/base/commands/viridian/viridian_cluster_list.go b/base/commands/viridian/viridian_cluster_list.go index c61583a23..4104abd75 100644 --- a/base/commands/viridian/viridian_cluster_list.go +++ b/base/commands/viridian/viridian_cluster_list.go @@ -6,18 +6,16 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) -type ClusterListCmd struct{} +type ClusterListCommand struct{} -func (cm ClusterListCmd) Unwrappable() {} - -func (cm ClusterListCmd) Init(cc plug.InitContext) error { +func (ClusterListCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("list-clusters") long := `Lists all Viridian clusters for the logged in API key. @@ -29,7 +27,7 @@ Make sure you login before running this command. return nil } -func (cm ClusterListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ClusterListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -48,7 +46,7 @@ func (cm ClusterListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { stop() cs := csi.([]viridian.Cluster) if len(cs) == 0 { - ec.PrintlnUnnecessary("OK No clusters found") + ec.PrintlnUnnecessary("OK No clusters found.") return nil } rows := make([]output.Row, len(cs)) @@ -90,5 +88,5 @@ func (cm ClusterListCmd) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("viridian:list-clusters", &ClusterListCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:list-clusters", &ClusterListCommand{})) } diff --git a/base/commands/viridian/viridian_cluster_resume.go b/base/commands/viridian/viridian_cluster_resume.go index 5ae9f9953..e93a7447c 100644 --- a/base/commands/viridian/viridian_cluster_resume.go +++ b/base/commands/viridian/viridian_cluster_resume.go @@ -6,18 +6,16 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) -type ClusterResumeCmd struct{} +type ClusterResumeCommand struct{} -func (cm ClusterResumeCmd) Unwrappable() {} - -func (cm ClusterResumeCmd) Init(cc plug.InitContext) error { +func (ClusterResumeCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("resume-cluster") long := `Resumes the given Viridian cluster. @@ -30,7 +28,7 @@ Make sure you login before running this command. return nil } -func (cm ClusterResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ClusterResumeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -59,5 +57,5 @@ func (cm ClusterResumeCmd) Exec(ctx context.Context, ec plug.ExecContext) error } func init() { - Must(plug.Registry.RegisterCommand("viridian:resume-cluster", &ClusterResumeCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:resume-cluster", &ClusterResumeCommand{})) } diff --git a/base/commands/viridian/viridian_cluster_stop.go b/base/commands/viridian/viridian_cluster_stop.go index fdbb04b44..8f242186c 100644 --- a/base/commands/viridian/viridian_cluster_stop.go +++ b/base/commands/viridian/viridian_cluster_stop.go @@ -6,18 +6,16 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/viridian" ) -type ClusterStopCmd struct{} +type ClusterStopCommand struct{} -func (cm ClusterStopCmd) Unwrappable() {} - -func (cm ClusterStopCmd) Init(cc plug.InitContext) error { +func (ClusterStopCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("stop-cluster") long := `Stops the given Viridian cluster. @@ -30,7 +28,7 @@ Make sure you login before running this command. return nil } -func (cm ClusterStopCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (ClusterStopCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -59,5 +57,5 @@ func (cm ClusterStopCmd) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("viridian:stop-cluster", &ClusterStopCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:stop-cluster", &ClusterStopCommand{})) } diff --git a/base/commands/viridian/viridian_import_config.go b/base/commands/viridian/viridian_import_config.go index 937b8bb25..320d79fcc 100644 --- a/base/commands/viridian/viridian_import_config.go +++ b/base/commands/viridian/viridian_import_config.go @@ -7,18 +7,15 @@ import ( "fmt" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" iserialization "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) -type ImportConfigCmd struct{} +type ImportConfigCommand struct{} -func (ImportConfigCmd) Unwrappable() {} - -func (ImportConfigCmd) Init(cc plug.InitContext) error { +func (cm ImportConfigCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("import-config") long := `Imports connection configuration of the given Viridian cluster. @@ -32,15 +29,15 @@ Make sure you login before running this command. return nil } -func (cm ImportConfigCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm ImportConfigCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err := cm.exec(ctx, ec); err != nil { - ec.PrintlnUnnecessary(fmt.Sprintf("FAIL Could not import cluster configuration: %s", err.Error())) - return hzerrors.WrappedError{Err: err} + err = handleErrorResponse(ec, err) + return fmt.Errorf("could not import cluster configuration: %w", err) } return nil } -func (ImportConfigCmd) exec(ctx context.Context, ec plug.ExecContext) error { +func (cm ImportConfigCommand) exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -48,7 +45,7 @@ func (ImportConfigCmd) exec(ctx context.Context, ec plug.ExecContext) error { clusterNameOrID := ec.GetStringArg(argClusterID) c, err := api.FindCluster(ctx, clusterNameOrID) if err != nil { - return handleErrorResponse(ec, err) + return err } cfgName := ec.Props().GetString(flagName) if cfgName == "" { @@ -64,7 +61,7 @@ func (ImportConfigCmd) exec(ctx context.Context, ec plug.ExecContext) error { } path, err := stage.Execute(ctx, ec, "", stage.NewFixedProvider(st)) if err != nil { - return handleErrorResponse(ec, err) + return err } ec.PrintlnUnnecessary("") return ec.AddOutputRows(ctx, output.Row{ @@ -77,5 +74,5 @@ func (ImportConfigCmd) exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("viridian:import-config", &ImportConfigCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:import-config", &ImportConfigCommand{})) } diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index ecef81c88..0f982fbc8 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -114,8 +114,12 @@ func loginWithEnvVariables_NonInteractiveTest(t *testing.T) { func listClusters_NonInteractiveTest(t *testing.T) { viridianTester(t, func(ctx context.Context, tcx it.TestContext) { - tcx.CLCExecute(ctx, "viridian", "list-clusters") - tcx.AssertStderrContains("OK") + /* + // cannot test this at the moment, since trial cluster on dev2 cannot be deleted + tcx.CLCExecute(ctx, "viridian", "list-clusters") + tcx.AssertStdoutContains("No clusters found") + + */ c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.CLCExecute(ctx, "viridian", "list-clusters") tcx.AssertStderrContains("OK") @@ -249,7 +253,7 @@ func deleteCluster_NonInteractiveTest(t *testing.T) { viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.CLCExecute(ctx, "viridian", "delete-cluster", c.ID, "--yes") - tcx.AssertStdoutContains("OK Inititated cluster deletion.") + tcx.AssertStdoutContains("Inititated cluster deletion") require.Eventually(t, func() bool { _, err := tcx.Viridian.GetCluster(ctx, c.ID) return err != nil @@ -315,7 +319,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", "--api-base", "dev2", "login", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) - tcx.AssertStdoutContains("Viridian token was fetched and saved.") + tcx.AssertStdoutContains("Saved the access token") tcx.WithReset(func() { f(ctx, tcx) }) diff --git a/base/commands/viridian/viridian_log_stream.go b/base/commands/viridian/viridian_log_stream.go index eb9c5d754..8a4c989bf 100644 --- a/base/commands/viridian/viridian_log_stream.go +++ b/base/commands/viridian/viridian_log_stream.go @@ -22,11 +22,9 @@ const ( propLogFormat = "log-format" ) -type StreamLogCmd struct{} +type StreamLogCommand struct{} -func (cm StreamLogCmd) Unwrappable() {} - -func (cm StreamLogCmd) Init(cc plug.InitContext) error { +func (StreamLogCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("stream-logs") long := `Outputs the logs of the given Viridian cluster as a stream. @@ -48,7 +46,7 @@ The log format may be one of: return nil } -func (cm StreamLogCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (StreamLogCommand) Exec(ctx context.Context, ec plug.ExecContext) error { api, err := getAPI(ec) if err != nil { return err @@ -198,5 +196,5 @@ func loggerTemplate(format string) string { } func init() { - check.Must(plug.Registry.RegisterCommand("viridian:stream-logs", &StreamLogCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:stream-logs", &StreamLogCommand{})) } diff --git a/base/commands/viridian/viridian_login.go b/base/commands/viridian/viridian_login.go index b907cdd57..38d4acaaa 100644 --- a/base/commands/viridian/viridian_login.go +++ b/base/commands/viridian/viridian_login.go @@ -12,7 +12,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" @@ -27,11 +27,9 @@ const ( secretPrefix = "viridian" ) -type LoginCmd struct{} +type LoginCommand struct{} -func (cm LoginCmd) Unwrappable() {} - -func (cm LoginCmd) Init(cc plug.InitContext) error { +func (cm LoginCommand) Init(cc plug.InitContext) error { cc.SetCommandUsage("login") short := "Logs in to Viridian using the given API key and API secret" long := fmt.Sprintf(`Logs in to Viridian to get an access token using the given API key and API secret. @@ -52,7 +50,7 @@ Alternatively, you can use the following environment variables: return nil } -func (cm LoginCmd) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm LoginCommand) Exec(ctx context.Context, ec plug.ExecContext) error { key, secret, err := getAPIKeySecret(ec) if err != nil { return err @@ -101,7 +99,7 @@ func (cm LoginCmd) Exec(ctx context.Context, ec plug.ExecContext) error { }) } -func (cm LoginCmd) retrieveToken(ctx context.Context, ec plug.ExecContext, key, secret, apiBase string) (string, error) { +func (cm LoginCommand) retrieveToken(ctx context.Context, ec plug.ExecContext, key, secret, apiBase string) (string, error) { ti, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Logging in") api, err := viridian.Login(ctx, secretPrefix, key, secret, apiBase) @@ -160,5 +158,5 @@ func getAPIKeySecret(ec plug.ExecContext) (key, secret string, err error) { } func init() { - Must(plug.Registry.RegisterCommand("viridian:login", &LoginCmd{})) + check.Must(plug.Registry.RegisterCommand("viridian:login", &LoginCommand{})) } diff --git a/base/initializers.go b/base/initializers.go index 2bd3e8a34..aa29ba288 100644 --- a/base/initializers.go +++ b/base/initializers.go @@ -33,7 +33,6 @@ func (g GlobalInitializer) Init(cc plug.InitContext) error { cc.AddStringConfig(clc.PropertyClusterName, "dev", "", "cluster name") cc.AddStringConfig(clc.PropertyLogPath, "", clc.PropertyLogPath, "log path") cc.AddStringConfig(clc.PropertyLogLevel, "", clc.PropertyLogLevel, "log level") - cc.AddStringConfig(clc.PropertySchemaDir, "", clc.PropertySchemaDir, "schema directory") cc.AddStringConfig(clc.PropertyClusterDiscoveryToken, "", "", "Viridian token") return nil } diff --git a/base/objects/objects.go b/base/objects/objects.go index 1228df598..cd0b2263d 100644 --- a/base/objects/objects.go +++ b/base/objects/objects.go @@ -5,9 +5,10 @@ import ( "sort" "strings" + "github.com/hazelcast/hazelcast-go-client/types" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" - "github.com/hazelcast/hazelcast-go-client/types" ) func GetAll(ctx context.Context, ec plug.ExecContext, typeFilter string, showHidden bool) ([]types.DistributedObjectInfo, error) { diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index 228d50bcc..18531ea23 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -349,27 +349,12 @@ func (m *Main) createCommands() error { if err := m.runAugmentors(ec, props); err != nil { return err } - // to wrap or not to wrap - // that's the problem - if _, ok := c.Item.(plug.UnwrappableCommander); ok { - err = c.Item.Exec(ctx, ec) - } else { - err = ec.WrapResult(func() error { - return c.Item.Exec(ctx, ec) - }) - } - if err != nil { + if err = c.Item.Exec(ctx, ec); err != nil { return err } if ic, ok := c.Item.(plug.InteractiveCommander); ok { ec.SetMode(ModeInteractive) - if _, ok := c.Item.(plug.UnwrappableCommander); ok { - err = ic.ExecInteractive(ctx, ec) - } else { - err = ec.WrapResult(func() error { - return ic.ExecInteractive(ctx, ec) - }) - } + err = ic.ExecInteractive(ctx, ec) if errors.Is(err, puberrors.ErrNotAvailable) { return nil } diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index 468752712..4038ab555 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -2,17 +2,14 @@ package cmd import ( "context" - "encoding/json" "errors" "fmt" "io" "os" "os/signal" "strconv" - "strings" "time" - "github.com/fatih/color" "github.com/hazelcast/hazelcast-go-client" "github.com/spf13/cobra" "github.com/theckman/yacspin" @@ -253,38 +250,9 @@ func (ec *ExecContext) ExecuteBlocking(ctx context.Context, f func(context.Conte } } -func (ec *ExecContext) WrapResult(f func() error) error { - t := time.Now() - err := f() - took := time.Since(t) - verbose := ec.Props().GetBool(clc.PropertyVerbose) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, cmderrors.ErrUserCancelled) { - return nil - } - msg := MakeErrStr(err) - if ec.Interactive() { - I2(fmt.Fprintln(ec.stderr, color.RedString(msg))) - } else { - I2(fmt.Fprintln(ec.stderr, msg)) - } - return cmderrors.WrappedError{Err: err} - } - if ec.Quiet() { - return nil - } - if verbose || ec.Interactive() { - msg := fmt.Sprintf("OK (%d ms)", took.Milliseconds()) - I2(fmt.Fprintln(ec.stderr, msg)) - } else { - I2(fmt.Fprintln(ec.stderr, "OK")) - } - return nil -} - func (ec *ExecContext) PrintlnUnnecessary(text string) { if !ec.Quiet() { - I2(fmt.Fprintln(ec.Stdout(), colorizeText(text))) + I2(fmt.Fprintln(ec.Stdout(), str.Colorize(text))) } } @@ -305,34 +273,6 @@ 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 { - return text - } - if v, ok := m["errorCode"]; ok { - if v == "ClusterTokenNotFound" { - return "Discovery token is not valid for this cluster" - } - } - if v, ok := m["message"]; ok { - if vs, ok := v.(string); ok { - return vs - } - } - return text -} - func makeKeywordArgs(args []string, argSpecs []ArgSpec) (map[string]any, error) { kw := make(map[string]any, len(argSpecs)) var maxCnt int diff --git a/clc/cmd/utils.go b/clc/cmd/utils.go index 25e5c4305..ef928188d 100644 --- a/clc/cmd/utils.go +++ b/clc/cmd/utils.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "errors" "fmt" "os" "strconv" @@ -10,10 +9,8 @@ import ( "time" "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-go-client/hzerrors" "github.com/hazelcast/hazelcast-commandline-client/clc" - cmderrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -60,25 +57,28 @@ func CheckServerCompatible(ci *hazelcast.ClientInternal, targetVersion string) ( return sv, ok } -func MakeErrStr(err error) string { - var httpErr cmderrors.HTTPError - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, hzerrors.ErrTimeout) { - return "Timeout" - } - var errStr string - if errors.As(err, &httpErr) { - errStr = makeErrorStringFromHTTPResponse(httpErr.Text()) - } else { - errStr = err.Error() - } - return fmt.Sprintf("Error: %s", errStr) -} - func ClientInternal(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (*hazelcast.ClientInternal, error) { sp.SetText("Connecting to the cluster") return ec.ClientInternal(ctx) } +// ExecuteBlocking runs the given blocking function. +// It displays a spinner in the interactive mode after a timeout. +// The returned stop function must be called at least once to prevent leaks if there's no error. +// Calling returned stop more than once has no effect. +func ExecuteBlocking[T any](ctx context.Context, ec plug.ExecContext, f func(context.Context, clc.Spinner) (T, error)) (value T, stop context.CancelFunc, err error) { + v, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + return f(ctx, sp) + }) + if v == nil { + var vv T + value = vv + } else { + value = v.(T) + } + return value, stop, err +} + func parseDuration(duration string) (time.Duration, error) { // input can be like: 10_000_000 or 10_000_000ms, so remove underscores ds := strings.ReplaceAll(duration, "_", "") diff --git a/clc/config/config.go b/clc/config/config.go index 6244779c4..bf81bb432 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -112,10 +112,6 @@ func MakeHzConfig(props plug.ReadOnlyProperties, lg log.Logger) (hazelcast.Confi lg.Debugf("Cluster name: %s", cn) cfg.Cluster.Name = cn } - sd := props.GetString(clc.PropertySchemaDir) - if sd == "" { - sd = paths.Join(paths.Home(), "schemas") - } var viridianEnabled bool if vt := props.GetString(clc.PropertyClusterDiscoveryToken); vt != "" { lg.Debugf("Viridan token: XXX") diff --git a/clc/const.go b/clc/const.go index 75f883ab7..fbb3a2f37 100644 --- a/clc/const.go +++ b/clc/const.go @@ -18,7 +18,6 @@ const ( PropertyConfig = "config" PropertyLogLevel = "log.level" PropertyLogPath = "log.path" - PropertySchemaDir = "schema-dir" PropertySSLEnabled = "ssl.enabled" PropertySSLServerName = "ssl.server" PropertySSLCAPath = "ssl.ca-path" @@ -36,4 +35,5 @@ const ( EnvSkipServerVersionCheck = "CLC_SKIP_SERVER_VERSION_CHECK" FlagAutoYes = "yes" MaxArgs = 65535 + TTLUnset = -1 ) diff --git a/clc/shell/common.go b/clc/shell/common.go index 8d66261a3..3549cc632 100644 --- a/clc/shell/common.go +++ b/clc/shell/common.go @@ -6,8 +6,11 @@ import ( "fmt" "strings" + "github.com/hazelcast/hazelcast-go-client/sql" + "github.com/hazelcast/hazelcast-commandline-client/base/maps" - "github.com/hazelcast/hazelcast-commandline-client/clc/sql" + "github.com/hazelcast/hazelcast-commandline-client/clc" + clcsql "github.com/hazelcast/hazelcast-commandline-client/clc/sql" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -15,7 +18,7 @@ const CmdPrefix = `\` var ErrHelp = errors.New("interactive help") -func ConvertStatement(ctx context.Context, ec plug.ExecContext, stmt string, verbose bool) (func() error, error) { +func ConvertStatement(ctx context.Context, ec plug.ExecContext, stmt string) (func() error, error) { var query string stmt = strings.TrimSpace(stmt) if strings.HasPrefix(stmt, "help") { @@ -74,13 +77,20 @@ func ConvertStatement(ctx context.Context, ec plug.ExecContext, stmt string, ver query = stmt } f := func() error { - res, stop, err := sql.ExecSQL(ctx, ec, query) + resV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText("Executing SQL") + res, err := clcsql.ExecSQL(ctx, ec, query) + if err != nil { + return nil, err + } + return res, nil + }) if err != nil { return err } defer stop() - // TODO: update sql.UpdateOutput to use stdout - if err := sql.UpdateOutput(ctx, ec, res, verbose); err != nil { + res := resV.(sql.Result) + if err := clcsql.UpdateOutput(ctx, ec, res); err != nil { return err } return nil diff --git a/clc/shell/shell.go b/clc/shell/shell.go index bab8bc34c..888b1b7c6 100644 --- a/clc/shell/shell.go +++ b/clc/shell/shell.go @@ -16,8 +16,9 @@ import ( ny "github.com/nyaosorg/go-readline-ny" "github.com/hazelcast/hazelcast-commandline-client/clc" - cmderrors "github.com/hazelcast/hazelcast-commandline-client/errors" + hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" ) const ( @@ -107,9 +108,8 @@ func (sh *Shell) Start(ctx context.Context) error { if errors.Is(err, ErrExit) { return nil } - var werr cmderrors.WrappedError - if !errors.As(err, &werr) { - I2(fmt.Fprintf(sh.stderr, color.RedString("Error: %s\n", err.Error()))) + if !hzerrors.IsUserCancelled(err) { + I2(fmt.Fprintln(sh.stderr, str.Colorize(hzerrors.MakeString(err)))) } } } diff --git a/clc/sql/sql.go b/clc/sql/sql.go index da2383c54..52b405a9f 100644 --- a/clc/sql/sql.go +++ b/clc/sql/sql.go @@ -6,13 +6,12 @@ import ( "fmt" "os" "os/signal" + "sync/atomic" "time" - "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-go-client/hzerrors" "github.com/hazelcast/hazelcast-go-client/sql" - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -21,70 +20,62 @@ import ( const PropertyUseMappingSuggestion = "use-mapping-suggestion" -func ExecSQL(ctx context.Context, ec plug.ExecContext, query string) (sql.Result, context.CancelFunc, error) { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, nil, err - } +func ExecSQL(ctx context.Context, ec plug.ExecContext, query string) (sql.Result, error) { as := ec.Props().GetBool(PropertyUseMappingSuggestion) - rv, stop, err := execSQL(ctx, ec, ci, query) + result, err := execSQL(ctx, ec, query) if err != nil { // check whether this is an SQL error with a suggestion, // so we can improve the error message or apply the suggestion if there's one var serr *sql.Error if !errors.As(err, &serr) { - return nil, stop, err + return nil, err } // TODO: This changes the error in order to remove 'decoding SQL execute response:' prefix. // Once that is removed from the Go client, the code below may be removed. err = adaptSQLError(err) if !as { if serr.Suggestion != "" && !ec.Interactive() { - return nil, stop, fmt.Errorf("%w\n\nUse --%s to automatically apply the suggestion", err, PropertyUseMappingSuggestion) + return nil, fmt.Errorf("%w\n\nUse --%s to automatically apply the suggestion", err, PropertyUseMappingSuggestion) } - return nil, stop, err + return nil, err } if serr.Suggestion != "" { ec.Logger().Debug(func() string { return fmt.Sprintf("Re-trying executing SQL with suggestion: %s", serr.Suggestion) }) // execute the suggested query - _, stop, err := execSQL(ctx, ec, ci, serr.Suggestion) + _, err := execSQL(ctx, ec, serr.Suggestion) if err != nil { - return nil, stop, err + return nil, err } - stop() // execute the original query - return execSQL(ctx, ec, ci, query) + return execSQL(ctx, ec, query) } } - return rv, stop, nil + return result, nil } -func execSQL(ctx context.Context, ec plug.ExecContext, ci *hazelcast.ClientInternal, query string) (sql.Result, context.CancelFunc, error) { - rv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Executing SQL") - for { - if ctx.Err() != nil { - return nil, ctx.Err() - } - r, err := ci.Client().SQL().Execute(ctx, query) - // If Go client cannot find a connection, it returns immediately with ErrIO - // Retry logic here - if err != nil { - if errors.Is(err, hzerrors.ErrIO) { - time.Sleep(1 * time.Second) - continue - } - return nil, err +func execSQL(ctx context.Context, ec plug.ExecContext, query string) (sql.Result, error) { + ci, err := ec.ClientInternal(ctx) + if err != nil { + return nil, err + } + for { + if ctx.Err() != nil { + return nil, ctx.Err() + } + r, err := ci.Client().SQL().Execute(ctx, query) + // If Go client cannot find a connection, it returns immediately with ErrIO + // Retry logic here + if err != nil { + if errors.Is(err, hzerrors.ErrIO) { + time.Sleep(1 * time.Second) + continue } - return r, nil + return nil, err } - }) - if err != nil { - return nil, stop, err + return r, nil } - return rv.(sql.Result), stop, nil } func adaptSQLError(err error) error { @@ -97,8 +88,9 @@ func adaptSQLError(err error) error { return fmt.Errorf(serr.Message) } -func UpdateOutput(ctx context.Context, ec plug.ExecContext, res sql.Result, verbose bool) error { +func UpdateOutput(ctx context.Context, ec plug.ExecContext, res sql.Result) error { if !res.IsRowSet() { + ec.PrintlnUnnecessary("OK Executed the query.") return nil } it, err := res.Iterator() @@ -109,7 +101,8 @@ func UpdateOutput(ctx context.Context, ec plug.ExecContext, res sql.Result, verb errCh := make(chan error, 1) ctx, stop := signal.NotifyContext(ctx, os.Interrupt, os.Kill) defer stop() - go func() { + var count int64 + go func(count *int64) { var row sql.Row var err error loop: @@ -118,6 +111,7 @@ func UpdateOutput(ctx context.Context, ec plug.ExecContext, res sql.Result, verb if err != nil { break } + atomic.AddInt64(count, 1) // have to create a new output row // since it is processed by another goroutine cols := row.Metadata().Columns() @@ -137,12 +131,17 @@ func UpdateOutput(ctx context.Context, ec plug.ExecContext, res sql.Result, verb } close(rowCh) errCh <- err - }() + }(&count) // XXX: the error is ignored, the reason must be noted. _ = ec.AddOutputStream(ctx, rowCh) select { case err = <-errCh: - return err + if err != nil { + return err + } + msg := fmt.Sprintf("OK Returned %d rows.", atomic.LoadInt64(&count)) + ec.PrintlnUnnecessary(msg) + return nil case <-ctx.Done(): return ctx.Err() } diff --git a/clc/ux/stage/errors.go b/clc/ux/stage/errors.go new file mode 100644 index 000000000..f2736e567 --- /dev/null +++ b/clc/ux/stage/errors.go @@ -0,0 +1,17 @@ +package stage + +type ignoreError struct { + Err error +} + +func (se ignoreError) Unwrap() error { + return se.Err +} + +func (se ignoreError) Error() string { + return se.Err.Error() +} + +func IgnoreError(wrappedErr error) error { + return ignoreError{Err: wrappedErr} +} diff --git a/clc/ux/stage/stage.go b/clc/ux/stage/stage.go index fa71255dc..44518fad7 100644 --- a/clc/ux/stage/stage.go +++ b/clc/ux/stage/stage.go @@ -2,6 +2,7 @@ package stage import ( "context" + "errors" "fmt" "time" @@ -35,7 +36,7 @@ func (s basicStatuser[T]) SetRemainingDuration(dur time.Duration) { if dur > 0 { text = fmt.Sprintf(s.textFmtWithRemaining, dur) } - s.sp.SetText(s.indexText + " " + text) + s.sp.SetText(" " + s.indexText + " " + text) } func (s basicStatuser[T]) Value() T { @@ -117,11 +118,19 @@ func Execute[T any](ctx context.Context, ec plug.ExecContext, value T, sp Provid return any(v), nil }) if err != nil { - ec.PrintlnUnnecessary(fmt.Sprintf("FAIL %s: %s", stg.FailureMsg, err.Error())) - return value, hzerrors.WrappedError{Err: err} + var ie ignoreError + if errors.As(err, &ie) { + // the error can be ignored + ec.PrintlnUnnecessary(fmt.Sprintf("ERROR %s %s: %s", ss.indexText, stg.FailureMsg, ie.Unwrap().Error())) + } else { + ec.PrintlnUnnecessary(fmt.Sprintf("ERROR %s: %s", stg.FailureMsg, err.Error())) + return value, hzerrors.WrappedError{Err: err} + } } stop() - ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg)) + if err == nil { + ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg)) + } if v == nil { var vv T value = vv diff --git a/clc/ux/stage/stage_test.go b/clc/ux/stage/stage_test.go index 7c271ceaa..dbf4c6ce4 100644 --- a/clc/ux/stage/stage_test.go +++ b/clc/ux/stage/stage_test.go @@ -65,14 +65,14 @@ func executeTest(t *testing.T) { _, err := stage.Execute[any](context.TODO(), ec, nil, 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", + " [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} @@ -103,8 +103,8 @@ func execute_WithFailureTest(t *testing.T) { ec := it.NewExecuteContext(nil) _, err := stage.Execute[any](context.TODO(), ec, nil, stage.NewFixedProvider(stages...)) assert.Error(t, err) - texts := []string{"[1/2] Progressing 1"} + texts := []string{" [1/2] Progressing 1"} assert.Equal(t, texts, ec.Spinner.Texts) - text := "FAIL Failure 1: some error\n" + text := "ERROR Failure 1: some error\n" assert.Equal(t, text, ec.StdoutText()) } diff --git a/cmd/clc/main.go b/cmd/clc/main.go index d9d06348b..31eae77ed 100644 --- a/cmd/clc/main.go +++ b/cmd/clc/main.go @@ -12,6 +12,8 @@ import ( cmd "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/config" hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" ) const ( @@ -39,7 +41,8 @@ func main() { bye(err) } _, name := filepath.Split(os.Args[0]) - m, err := cmd.NewMain(name, cfgPath, cp, logPath, logLevel, clc.StdIO()) + stdio := clc.StdIO() + m, err := cmd.NewMain(name, cfgPath, cp, logPath, logLevel, stdio) if err != nil { bye(err) } @@ -48,7 +51,7 @@ func main() { // print the error only if it wasn't printed before if _, ok := err.(hzerrors.WrappedError); !ok { if !hzerrors.IsUserCancelled(err) { - fmt.Println(cmd.MakeErrStr(err)) + check.I2(fmt.Fprintln(stdio.Stderr, str.Colorize(hzerrors.MakeString(err)))) } } } diff --git a/errors/error.go b/errors/error.go index 0c38dd6df..07b8ed288 100644 --- a/errors/error.go +++ b/errors/error.go @@ -17,10 +17,7 @@ package errors import ( - "context" "errors" - - "github.com/gohxs/readline" ) var ( @@ -46,11 +43,3 @@ type HTTPError interface { Text() string Code() int } - -func IsUserCancelled(err error) bool { - return errors.Is(err, context.Canceled) || errors.Is(err, ErrUserCancelled) || errors.Is(err, readline.ErrInterrupt) -} - -func IsTimeout(err error) bool { - return errors.Is(err, context.DeadlineExceeded) -} diff --git a/errors/utils.go b/errors/utils.go new file mode 100644 index 000000000..50423809b --- /dev/null +++ b/errors/utils.go @@ -0,0 +1,58 @@ +package errors + +import ( + "context" + "encoding/json" + "errors" + + "github.com/gohxs/readline" + "github.com/hazelcast/hazelcast-go-client/hzerrors" +) + +func IsUserCancelled(err error) bool { + return errors.Is(err, context.Canceled) || errors.Is(err, ErrUserCancelled) || errors.Is(err, readline.ErrInterrupt) +} + +func IsTimeout(err error) bool { + return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, hzerrors.ErrTimeout) +} + +func MakeString(err error) string { + if IsTimeout(err) { + return "TIMEOUT" + } + var httpErr HTTPError + var errStr string + if errors.As(err, &httpErr) { + errStr = makeErrorStringFromHTTPResponse(httpErr.Text()) + } else { + errStr = err.Error() + } + // convert the first character of the error string to upper case + if len(errStr) > 0 { + r := []rune(errStr) + if r[0] >= 'a' && r[0] <= 'z' { + r[0] -= 'a' - 'A' + } + errStr = string(r) + } + return "ERROR " + errStr +} + +func makeErrorStringFromHTTPResponse(text string) string { + m := map[string]any{} + if err := json.Unmarshal([]byte(text), &m); err != nil { + return text + } + if v, ok := m["errorCode"]; ok { + if v == "ClusterTokenNotFound" { + return "Discovery token is not valid for this cluster" + } + } + if v, ok := m["message"]; ok { + if vs, ok := v.(string); ok { + return vs + } + } + return text +} diff --git a/internal/client.go b/internal/client.go new file mode 100644 index 000000000..9af0483cf --- /dev/null +++ b/internal/client.go @@ -0,0 +1,20 @@ +package internal + +import ( + "strings" + + "github.com/hazelcast/hazelcast-go-client" +) + +func StringToPartitionID(ci *hazelcast.ClientInternal, name string) (int32, error) { + idx := strings.Index(name, "@") + keyData, err := ci.EncodeData(name[idx+1:]) + if err != nil { + return 0, err + } + partitionID, err := ci.GetPartitionID(keyData) + if err != nil { + return 0, err + } + return partitionID, nil +} diff --git a/internal/demo/wikimedia/generate.go b/internal/demo/wikimedia/generate.go index 7b11a063b..ef10bdde1 100644 --- a/internal/demo/wikimedia/generate.go +++ b/internal/demo/wikimedia/generate.go @@ -64,6 +64,6 @@ func handleEvents(ctx context.Context, client *sse.Client, itemCh chan demo.Stre }) } -func (StreamGenerator) MappingQuery(mapName string) (string, error) { +func (StreamGenerator) GenerateMappingQuery(mapName string) (string, error) { return demo.GenerateMappingQuery(mapName, event{}.KeyValues()) } diff --git a/internal/plug/command.go b/internal/plug/command.go index b5ddb6470..ffb1233fa 100644 --- a/internal/plug/command.go +++ b/internal/plug/command.go @@ -9,7 +9,3 @@ type Commander interface { type InteractiveCommander interface { ExecInteractive(ctx context.Context, ec ExecContext) error } - -type UnwrappableCommander interface { - Unwrappable() -} diff --git a/internal/plug/context.go b/internal/plug/context.go index 502558eef..bfa119f17 100644 --- a/internal/plug/context.go +++ b/internal/plug/context.go @@ -51,7 +51,3 @@ type ExecContext interface { ExecuteBlocking(ctx context.Context, f func(ctx context.Context, sp clc.Spinner) (any, error)) (value any, stop context.CancelFunc, err error) PrintlnUnnecessary(text string) } - -type ResultWrapper interface { - WrapResult(f func() error) error -} diff --git a/internal/str/str.go b/internal/str/str.go index f2613f4e1..4dd34e761 100644 --- a/internal/str/str.go +++ b/internal/str/str.go @@ -4,6 +4,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/fatih/color" ) // SplitByComma splits a string by commas, and optionally removes empty items. @@ -47,3 +49,13 @@ func SpacePaddedIntFormat(maxValue int) string { } return fmt.Sprintf("%%%dd", len(strconv.Itoa(maxValue))) } + +func Colorize(text string) string { + if strings.HasPrefix(text, "OK ") { + return fmt.Sprintf(" %s %s", color.GreenString("OK"), text[3:]) + } + if strings.HasPrefix(text, "ERROR ") { + return fmt.Sprintf(" %s %s", color.RedString("ERROR"), text[6:]) + } + return text +} diff --git a/internal/topic/topic.go b/internal/topic/topic.go deleted file mode 100644 index 5b97f4c5d..000000000 --- a/internal/topic/topic.go +++ /dev/null @@ -1,85 +0,0 @@ -package topic - -import ( - "context" - "strings" - "time" - - "github.com/hazelcast/hazelcast-go-client" - "github.com/hazelcast/hazelcast-go-client/cluster" - "github.com/hazelcast/hazelcast-go-client/types" - - "github.com/hazelcast/hazelcast-commandline-client/internal/log" - "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" -) - -type TopicEvent struct { - PublishTime time.Time - Value any - ValueType int32 - TopicName string - Member cluster.MemberInfo -} - -func newTopicEvent(name string, value any, valueType int32, publishTime time.Time, member cluster.MemberInfo) TopicEvent { - return TopicEvent{ - TopicName: name, - Value: value, - ValueType: valueType, - PublishTime: publishTime, - Member: member, - } -} - -func PublishAll(ctx context.Context, ci *hazelcast.ClientInternal, topic string, vals []hazelcast.Data) error { - pid, err := stringToPartitionID(ci, topic) - if err != nil { - return err - } - req := codec.EncodeTopicPublishAllRequest(topic, vals) - _, err = ci.InvokeOnPartition(ctx, req, pid, nil) - return err -} - -func stringToPartitionID(ci *hazelcast.ClientInternal, name string) (int32, error) { - idx := strings.Index(name, "@") - keyData, err := ci.EncodeData(name[idx+1:]) - if err != nil { - return 0, err - } - partitionID, err := ci.GetPartitionID(keyData) - if err != nil { - return 0, err - } - return partitionID, nil -} - -func AddListener(ctx context.Context, ci *hazelcast.ClientInternal, topic string, logger log.Logger, handler func(event TopicEvent)) (types.UUID, error) { - subscriptionID := types.NewUUID() - addRequest := codec.EncodeTopicAddMessageListenerRequest(topic, false) - removeRequest := codec.EncodeTopicRemoveMessageListenerRequest(topic, subscriptionID) - listenerHandler := func(msg *hazelcast.ClientMessage) { - codec.HandleTopicAddMessageListener(msg, func(itemData hazelcast.Data, publishTime int64, uuid types.UUID) { - itemType := itemData.Type() - item, err := ci.DecodeData(itemData) - if err != nil { - logger.Warn("The value was not decoded, due to error: %s", err.Error()) - item = serialization.NondecodedType(serialization.TypeToLabel(itemType)) - } - var member cluster.MemberInfo - if m := ci.ClusterService().GetMemberByUUID(uuid); m != nil { - member = *m - } - handler(newTopicEvent(topic, item, itemType, time.Unix(0, publishTime*1_000_000), member)) - }) - } - binder := ci.ListenerBinder() - err := binder.Add(ctx, subscriptionID, addRequest, removeRequest, listenerHandler) - return subscriptionID, err -} - -// RemoveListener removes the given subscription from this topic. -func RemoveListener(ctx context.Context, ci *hazelcast.ClientInternal, subscriptionID types.UUID) error { - return ci.ListenerBinder().Remove(ctx, subscriptionID) -} From f8151404be4926dab95af5bf8ffbefd8349d444a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 11 Sep 2023 13:45:59 +0300 Subject: [PATCH 55/79] Updated install docs (#377) Updated install docs --- README.md | 43 ++++++++++------- base/commands/project/help.go | 2 +- docs/modules/ROOT/pages/clc-project.adoc | 2 +- docs/modules/ROOT/pages/get-started.adoc | 2 +- docs/modules/ROOT/pages/install-clc.adoc | 61 +++++++----------------- 5 files changed, 46 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 29b1d2399..822af986d 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,40 @@ ## Installation -We provide binaries for the popular platforms at our [Releases](https://github.com/hazelcast/hazelcast-commandline-client/releases) page. -In order to install CLC: - -* Download the release package for your platform, -* Extract it, -* Optionally move the `clc` binary to somewhere in your *PATH*, so it can be run in any terminal without additional settings. - Currently we provide precompiled binaries of CLC for the following platforms and architectures: * Linux/amd64 * Windows/amd64 -* MacOS/amd64 -* MacOS/arm64 +* macOS/amd64 +* macOS/arm64 -Additionally, we provide an installer for Windows 10 and up. -The installer can install CLC for either system-wide or just for the user. -It adds the `clc` binary automatically to the `$PATH`, so it can be run in any terminal without additional settings. +### Linux / macOS + +You can run the following command to install the latest stable CLC on a computer running Linux x64 or macOS 10.15 (Catalina) x64/ARM 64 (M1/M2): +``` +curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash +``` -On MacOS, you may need to remove the CLC binary from quarantine, if you get a security warning: +On macOS, binaries downloaded outside of AppStore require your intervention to run. +The install script automatically handles this, but if you downloaded a release package you can do it manually: ``` $ xattr -d com.apple.quarantine CLC_FOLDER/clc ``` Use the correct path instead of `CLC_FOLDER` in the command above. +### Windows + +We provide an installer for Windows 10 and up. +The installer can install CLC either system-wide or just for the user. +It adds the `clc` binary automatically to the `$PATH`, so it can be run in any terminal without additional settings. + +Check out our [Releases](https://github.com/hazelcast/hazelcast-commandline-client/releases/latest) page for the download. + +### Building from Source + If your platform is not one of the above, you may want to compile CLC yourself. Our build process is very simple and doesn't have many dependencies. -In most cases just running `make` is sufficient to build CLC if you have the latest [Go](https://go.dev/) compiler installed. -See [Building from source](#building-from-source) section. +In most cases, running `make` is sufficient to build CLC if you have the latest [Go](https://go.dev/) compiler and GNU make installed. +See [Building from source](#building-from-source) section for detailed instructions. ## Usage Summary @@ -202,14 +209,14 @@ The following targets are tested and supported. The prior versions of the given targets would also work, but that's not tested. * Ubuntu 22.04 or better. -* MacOS 12 or better. +* macOS 15 or better. * Windows 10 or better. ### Requirements * Go 1.21 or better * Git -* GNU Make (on Linux and MacOS) +* GNU Make (on Linux and macOS) * Command Prompt or Powershell (on Windows) * go-winres: https://github.com/tc-hib/go-winres (on Windows) @@ -236,7 +243,7 @@ The `clc` or `clc.exe` binary is created in the `build` directory. CLC starts the in interactive mode by default. -On Linux and MacOS: +On Linux and macOS: ``` ./build/clc ``` diff --git a/base/commands/project/help.go b/base/commands/project/help.go index 7502b58e8..8c480bb40 100644 --- a/base/commands/project/help.go +++ b/base/commands/project/help.go @@ -44,7 +44,7 @@ You can use the placeholders in "defaults.yaml" and the following configuration * log_path * log_level -Example (Linux and MacOS): +Example (Linux and macOS): $ clc project create \ simple-streaming-pipeline\ diff --git a/docs/modules/ROOT/pages/clc-project.adoc b/docs/modules/ROOT/pages/clc-project.adoc index 6936b9306..f2dfe8c7f 100644 --- a/docs/modules/ROOT/pages/clc-project.adoc +++ b/docs/modules/ROOT/pages/clc-project.adoc @@ -83,7 +83,7 @@ You can use the placeholders in "defaults.yaml" and the following configuration * log_path * log_level -Example (Linux and MacOS): +Example (Linux and macOS): [source,bash] ---- diff --git a/docs/modules/ROOT/pages/get-started.adoc b/docs/modules/ROOT/pages/get-started.adoc index c18f42227..958041d1b 100644 --- a/docs/modules/ROOT/pages/get-started.adoc +++ b/docs/modules/ROOT/pages/get-started.adoc @@ -203,7 +203,7 @@ SELECT * FROM currencydata ORDER BY Code; [tabs] ==== -Linux and MacOS:: +Linux and macOS:: + -- . To run the script on your Test1 cluster, execute the following command. diff --git a/docs/modules/ROOT/pages/install-clc.adoc b/docs/modules/ROOT/pages/install-clc.adoc index d0bfe1eae..c20ea50a7 100644 --- a/docs/modules/ROOT/pages/install-clc.adoc +++ b/docs/modules/ROOT/pages/install-clc.adoc @@ -5,8 +5,9 @@ {description} You can install the Hazelcast CLC, using one of the following: -* Downloading a pre-built binary +* Using an install script on Linux or macOS * Windows installer +* Downloading a pre-built binary * Building from source ifdef::snapshot[] @@ -18,14 +19,14 @@ Pre-release versions can only be built from source. Supported OS: - Ubuntu 22.04 or later -- MacOS 12 or later +- macOS 15 or later - Windows 10 or later Requirements: -- Go 1.19 or later +- Go 1.21 or later - Git -- For Linux and MacOS, GNU Make +- For Linux and macOS, GNU Make - Command prompt or Windows Powershell - For Windows, https://github.com/tc-hib/go-winres[go-winres] @@ -56,7 +57,7 @@ The ``clc``, or ``clc.exe``, binary is created in the ``build`` directory. . Run the project + --- On Linux or MacOS: +-- On Linux or macOS: + [source,shell] ---- @@ -74,42 +75,26 @@ endif::[] ifndef::snapshot[] == Installing on macOS -The Hazelcast CLC is supported on macOS 13 or newer versions. +The Hazelcast CLC is supported on macOS 15 or newer versions. [tabs] ==== -ZIP (Intel):: +Install Script (Intel):: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the AMD compressed file (`hazelcast-clc_v{full-version}_darwin_amd64.zip`). -. Download and unzip the file. -. Remove the `clc` binary from quarantine if you get a security warning when running it: +. Run the following command to install CLC using the install script we provide: + [source,shell,subs="attributes"] ---- -xattr -d com.apple.quarantine hazelcast-clc_v{full-version}_darwin_amd64/clc ----- -. Optionally make `clc` available without using its full path. You can do that by moving `clc` to one of the directories in the `$PATH` environment variable. `/usr/local/bin` is a safe choice. -+ -[source,shell,subs="attributes"] ----- -sudo mv hazelcast-clc_v{full-version}_darwin_amd64/clc /usr/local/bin +curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash ---- -ZIP (Apple Silicon):: +Install Script (Apple Silicon):: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the ARM compressed file (`hazelcast-clc_v{full-version}_darwin_arm64.zip`). -. Download and unzip the file. -. Remove the `clc` binary from quarantine if you get a security warning when running it: +. Run the following command to install CLC using the install script we provide: + [source,shell,subs="attributes"] ---- -xattr -d com.apple.quarantine hazelcast-clc_v{full-version}_darwin_arm64/clc ----- -. Optionally make `clc` available without using its full path. You can do that by moving `clc` to one of the directories in the `$PATH` environment variable. `/usr/local/bin` is a safe choice. -+ -[source,shell,subs="attributes"] ----- -sudo mv hazelcast-clc_v{full-version}_darwin_arm64/clc /usr/local/bin +curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash ---- Build from Source:: @@ -162,15 +147,13 @@ The Hazelcast CLC runs on any recent Linux distribution. We test it on Ubuntu 22 [tabs] ==== -Tarball (AMD64):: +Install Script (AMD64):: + -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the tarball for Linux (`hazelcast-clc_v{full-version}_linux_amd64.tar.gz`). -. Download and unzip the file. -. Optionally make `clc` available without using its full path. You can do that by moving `clc` to one of the directories in the `$PATH` environment variable. `/usr/local/bin` is a safe choice. +. Run the following command to install CLC using the install script we provide: + [source,shell,subs="attributes"] ---- -sudo mv hazelcast-clc_v{full-version}_linux_amd64/clc /usr/local/bin +curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash ---- Build from Source:: @@ -235,14 +218,6 @@ Installer:: clc.exe ---- -ZIP:: -+ -. Go to the https://github.com/hazelcast/hazelcast-commandline-client/releases[releases page], and locate the Windows ZIP file (`hazelcast-clc_v{full-version}_windows_amd64.zip`). -. Download and unzip the file. -. Optionally make `clc.exe` available without using its full path. You can do that by adding the full path of the extracted directory to the `PATH` environment variable. - -==== - endif::[] == Verifying the Hazelcast CLC Installation @@ -268,9 +243,9 @@ Windows:: . Right-click on it and select *Uninstall*. . Press kbd:[Yes] on the uninstallation dialog. -Release Package:: +Install Script (Linux/macOS):: + -Delete the `hazelcast-commandline-client` directory. +Delete the `$HOME/.hazelcast` directory. ==== == Next Steps From 5677b87613d4ff627e8eacb1a7443335e803b482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 11 Sep 2023 15:31:06 +0300 Subject: [PATCH 56/79] Added 5.3.3 release notes (#378) Added 5.3.3 release notes --- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/release-notes-5.3.3.adoc | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 docs/modules/ROOT/pages/release-notes-5.3.3.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 873edaa70..472c3ce80 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -37,6 +37,7 @@ * xref:keyboard-shortcuts.adoc[] .Release Notes +* xref:release-notes-5.3.3.adoc[5.3.3] * xref:release-notes-5.3.2.adoc[5.3.2] * xref:release-notes-5.3.1.adoc[5.3.1] * xref:release-notes-5.3.0.adoc[5.3.0] diff --git a/docs/modules/ROOT/pages/release-notes-5.3.3.adoc b/docs/modules/ROOT/pages/release-notes-5.3.3.adoc new file mode 100644 index 000000000..f17019248 --- /dev/null +++ b/docs/modules/ROOT/pages/release-notes-5.3.3.adoc @@ -0,0 +1,26 @@ += 5.3.3 Release Notes + +== New Features + +* Added the `demo generate-data` command that creates data from a Wikipedia event stream. +* Added the `demo map-setmany` command that generates multiple Map entries. +* Added the `project list-templates` command that lists templates that can be used with the `project create` command. +* Added the `\di` shortcut that lists all or some indexes. This command is available only in the interactive mode. +* Added `--prelease` and `--development` flags to `viridian create-cluster` command. + +== Improvements + +* The Viridian "CLI" bundle is used with `config import` and `viridian import-config` commands. The configuration bundle includes certificates for all supported Hazelcast client libraries. +* CLC configuration is saved as both `config.yaml` and `config.json` when `config import`, `viridian import-config` or `config add` commands are used. +* An installation script may be used to install CLC on Linux and macOS. +* Configuration menu is displayed in the non-interactive mode when the `default` configuration doesn't exist. +* `snapshot list` command provides more information. +* Configuration name is used in the CLC interactive prompt. +* "Invalid number of arguments" error is improved to display which positional arguments are missing. +* `project create` command updates the corresponding template if necessary when run. +* Updated command output. + +== Fixes + +* Full stack trace is shown on `job submit` errors. +* link:https://github.com/hazelcast/hazelcast-commandline-client/pull/288[#288] Fix streaming timeout. From 019320adb300708650eb0c99ec3114ee33c974e8 Mon Sep 17 00:00:00 2001 From: Serdar Ozmen Date: Tue, 12 Sep 2023 16:38:46 +0200 Subject: [PATCH 57/79] Update antora.yml to the correct snapshot version. (#384) --- docs/antora.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 426a1d43b..965ace4a7 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -2,15 +2,15 @@ name: clc title: Hazelcast CLC start_page: overview.adoc # Version in the URL -version: '5.3.3-snapshot' +version: '5.3.4-snapshot' # Version in the version selector (we display only the latest major.minor version) -display_version: '5.3.3-SNAPSHOT' +display_version: '5.3.4-SNAPSHOT' # Displays a banner to inform users that this is a prerelease version prerelease: true asciidoc: attributes: # The full major.minor.patch version, which is used as a variable in the docs for things like download links - full-version: '5.3.3-SNAPSHOT' + full-version: '5.3.4-SNAPSHOT' # Allows us to use UI macros. See https://docs.asciidoctor.org/asciidoc/latest/macros/ui-macros/ experimental: true snapshot: true From 9f90c355ea0da08c70e04bb30a544a1e08bde3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Wed, 13 Sep 2023 09:27:41 +0300 Subject: [PATCH 58/79] Fixes "connecting to cluster" prompt for object list (#385) --- base/objects/objects.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/base/objects/objects.go b/base/objects/objects.go index cd0b2263d..bff671d40 100644 --- a/base/objects/objects.go +++ b/base/objects/objects.go @@ -8,15 +8,16 @@ import ( "github.com/hazelcast/hazelcast-go-client/types" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) func GetAll(ctx context.Context, ec plug.ExecContext, typeFilter string, showHidden bool) ([]types.DistributedObjectInfo, error) { - ci, err := ec.ClientInternal(ctx) - if err != nil { - return nil, err - } objs, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + ci, err := cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } sp.SetText("Getting distributed objects") return ci.Client().GetDistributedObjectsInfo(ctx) }) From 7ad4c6c75f2192d9136c730c5229853e42875edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Wed, 13 Sep 2023 18:04:49 +0300 Subject: [PATCH 59/79] Adds support for updating the PATH when .bashrc is not sourced, e.g., tmux (#387) --- extras/unix/install.sh | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/extras/unix/install.sh b/extras/unix/install.sh index 3bfa695e4..a8cec90e9 100644 --- a/extras/unix/install.sh +++ b/extras/unix/install.sh @@ -11,6 +11,7 @@ check_ok () { which "$what" > /dev/null && e=yes case "$what" in awk*) state_awk_ok=$e;; + bash*) state_bash_ok=$e;; curl*) state_curl_ok=$e;; tar*) state_tar_ok=$e;; unzip*) state_unzip_ok=$e;; @@ -55,7 +56,7 @@ bye () { } print_usage () { - echo "This script installs Hazelcast CLC to a system or user directory." + echo "This script installs Hazelcast CLC to a user directory." echo echo "Usage: $0 [--beta | --debug | --help]" echo @@ -79,7 +80,7 @@ detect_tmpdir () { } do_curl () { - curl -Ls "$1" + curl -LSs "$1" } do_wget () { @@ -155,20 +156,22 @@ remove_from_quarantine () { } update_config_files () { - update_rc "BASH" "$HOME/.bashrc" + if [[ "$state_bash_ok" == "yes" ]]; then + update_rc "$HOME/.bashrc" + update_rc "$HOME/.profile" + fi if [[ "$state_zsh_ok" == "yes" ]]; then - update_rc "ZSH" "$HOME/.zshenv" + update_rc "$HOME/.zshenv" fi } update_rc () { - local prefix="$1" - local path="$2" - local installed="CLC_INSTALLED_$prefix=1" + local path="$1" + local set_path="PATH=\$PATH:${state_bin_dir}" local code=" -if [[ \"\$CLC_INSTALLED_$prefix\" != \"1\" ]]; then - export CLC_INSTALLED_$prefix=1 - export PATH=\$PATH:${state_bin_dir} +echo \"\$PATH\" | grep \"${state_bin_dir}\" > /dev/null +if [[ \$? == 1 ]]; then + export $set_path fi " if [[ -e "$path" ]]; then @@ -181,7 +184,7 @@ fi fi local text set +e - text=$(cat "$path" | grep "$installed") + text=$(cat "$path" | grep "$set_path") set -e if [[ "$text" != "" ]]; then # CLC PATH is already exported in this file @@ -346,7 +349,7 @@ process_flags () { done } -DEPENDENCIES="awk wget curl unzip tar xattr zsh" +DEPENDENCIES="awk bash curl tar unzip wget xattr zsh" CLC_HOME="${CLC_HOME:-$HOME/.hazelcast}" state_arch= @@ -369,6 +372,7 @@ state_tar_ok=no state_unzip_ok=no state_wget_ok=no state_xattr_ok=no +state_bash_ok=no state_zsh_ok=no process_flags "$@" From 3f9d477c75bc9fd9b477bfce8e6dc8ae7aed395d Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:51:33 +0300 Subject: [PATCH 60/79] [CLC-334] Fix list add command with index (#386) --- base/commands/list/list_add.go | 7 +++++-- base/commands/list/list_it_test.go | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/base/commands/list/list_add.go b/base/commands/list/list_add.go index 1b3dfa5c5..9481ef9cb 100644 --- a/base/commands/list/list_add.go +++ b/base/commands/list/list_add.go @@ -65,7 +65,10 @@ func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } - return codec.DecodeListAddResponse(resp), err + if index >= 0 { + return true, nil + } + return codec.DecodeListAddResponse(resp), nil }) if err != nil { return err @@ -75,7 +78,7 @@ func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { ec.PrintlnUnnecessary(msg) row := output.Row{ output.Column{ - Name: "Value", + Name: "Value Changed", Type: serialization.TypeBool, Value: val, }, diff --git a/base/commands/list/list_it_test.go b/base/commands/list/list_it_test.go index 5ceb4107e..520b8b634 100644 --- a/base/commands/list/list_it_test.go +++ b/base/commands/list/list_it_test.go @@ -22,6 +22,7 @@ func TestList(t *testing.T) { f func(t *testing.T) }{ {name: "Add_NonInteractive", f: add_NonInteractiveTest}, + {name: "Add_WithIndex_NonInteractive", f: add_WithIndex_NonInteractiveTest}, {name: "Clear_NonInteractive", f: clear_NonInteractiveTest}, {name: "Contains_NonInteractive", f: contains_NonInteractiveTest}, {name: "RemoveIndex_Noninteractive", f: removeIndex_NonInteractiveTest}, @@ -50,6 +51,23 @@ func add_NonInteractiveTest(t *testing.T) { }) } +func add_WithIndex_NonInteractiveTest(t *testing.T) { + it.ListTester(t, func(tcx it.TestContext, l *hz.List) { + t := tcx.T + ctx := context.Background() + tcx.WithReset(func() { + tcx.CLCExecute(ctx, "list", "-n", l.Name(), "--index", "0", "add", "foo") + tcx.CLCExecute(ctx, "list", "-n", l.Name(), "--index", "1", "add", "bar") + require.Equal(t, "foo", check.MustValue(l.Get(context.Background(), 0))) + require.Equal(t, "bar", check.MustValue(l.Get(context.Background(), 1))) + tcx.CLCExecute(ctx, "list", "-n", l.Name(), "--index", "1", "add", "second") + require.Equal(t, "foo", check.MustValue(l.Get(context.Background(), 0))) + require.Equal(t, "second", check.MustValue(l.Get(context.Background(), 1))) + require.Equal(t, "bar", check.MustValue(l.Get(context.Background(), 2))) + }) + }) +} + func clear_NonInteractiveTest(t *testing.T) { it.ListTester(t, func(tcx it.TestContext, l *hz.List) { t := tcx.T From c343e6fb9ad9885e2196aa94e953e1b453a4e0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 15 Sep 2023 16:12:06 +0300 Subject: [PATCH 61/79] Fixed Viridian test outputs (#389) --- base/commands/viridian/viridian_it_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index 0f982fbc8..ee7292dec 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -90,7 +90,7 @@ func loginWithParams_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { 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.") + tcx.AssertStdoutContains("Saved the access token") }) }) }) @@ -106,7 +106,7 @@ func loginWithEnvVariables_NonInteractiveTest(t *testing.T) { it.WithEnv(viridian.EnvAPIKey, it.ViridianAPIKey(), func() { it.WithEnv(viridian.EnvAPISecret, it.ViridianAPISecret(), func() { tcx.CLCExecute(ctx, "viridian", "login") - tcx.AssertStdoutContains("Viridian token was fetched and saved.") + tcx.AssertStdoutContains("Saved the access token") }) }) }) @@ -122,7 +122,6 @@ func listClusters_NonInteractiveTest(t *testing.T) { */ c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.CLCExecute(ctx, "viridian", "list-clusters") - tcx.AssertStderrContains("OK") tcx.AssertStdoutContains(c.ID) }) } @@ -204,7 +203,7 @@ func resumeCluster_NonInteractiveTest(t *testing.T) { viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "STOPPED") tcx.CLCExecute(ctx, "viridian", "resume-cluster", c.ID) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") check.Must(waitState(ctx, tcx, c.ID, "RUNNING")) }) } From 61a54c4688b121438ac0dec75765095196933b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 15 Sep 2023 17:12:56 +0300 Subject: [PATCH 62/79] Fix project command tags (#390) --- base/commands/project/help.go | 2 ++ base/commands/project/project_list_it_test.go | 2 ++ base/commands/project/project_list_templates.go | 2 ++ 3 files changed, 6 insertions(+) diff --git a/base/commands/project/help.go b/base/commands/project/help.go index 8c480bb40..e78871d92 100644 --- a/base/commands/project/help.go +++ b/base/commands/project/help.go @@ -1,3 +1,5 @@ +//go:build std || project + package project import "fmt" diff --git a/base/commands/project/project_list_it_test.go b/base/commands/project/project_list_it_test.go index 8fa68a524..3e51a1d9a 100644 --- a/base/commands/project/project_list_it_test.go +++ b/base/commands/project/project_list_it_test.go @@ -1,3 +1,5 @@ +//go:build std || project + package project import ( diff --git a/base/commands/project/project_list_templates.go b/base/commands/project/project_list_templates.go index 5074ba620..1ff6dc698 100644 --- a/base/commands/project/project_list_templates.go +++ b/base/commands/project/project_list_templates.go @@ -1,3 +1,5 @@ +//go:build std || project + package project import ( From 2f18078aa2dd717db003708748f7f2418b0c02f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 18 Sep 2023 19:18:24 +0300 Subject: [PATCH 63/79] Refactored max cols getter; Set the gohxs width using max cols getter (#392) --- base/printers.go | 29 +++-------------------------- clc/shell/linereader.go | 5 +++++ internal/terminal/term.go | 23 ++++++++++++++++++++++- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/base/printers.go b/base/printers.go index e017571e9..0fd2a1a19 100644 --- a/base/printers.go +++ b/base/printers.go @@ -3,14 +3,10 @@ package base import ( "context" "io" - "os" - "strconv" - "github.com/nathan-fiscaletti/consolesize-go" - - "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" ) const ( @@ -53,23 +49,14 @@ func (pr JSONPrinter) PrintRows(ctx context.Context, w io.Writer, rows []output. type TablePrinter struct{} func (pr *TablePrinter) PrintStream(ctx context.Context, w io.Writer, rp output.RowProducer) error { - mc, _ := consolesize.GetConsoleSize() - if mc <= 0 { - mc = tableMaxcols() - } - if mc < 1 { - mc = 1000 - } + mc := terminal.ConsoleWidth() tr := output.NewTableResult(nil, rp, mc) _, err := tr.Serialize(ctx, w) return err } func (pr *TablePrinter) PrintRows(ctx context.Context, w io.Writer, rows []output.Row) error { - mc := tableMaxcols() - if mc <= 0 { - mc, _ = consolesize.GetConsoleSize() - } + mc := terminal.ConsoleWidth() header, rows := output.MakeTableFromRows(rows, mc) rp := output.NewSimpleRows(rows) tr := output.NewTableResult(header, rp, mc) @@ -92,16 +79,6 @@ func (pr *CSVPrinter) PrintRows(ctx context.Context, w io.Writer, rows []output. return err } -func tableMaxcols() int { - if s, ok := os.LookupEnv(clc.EnvMaxCols); ok { - v, err := strconv.Atoi(s) - if err == nil { - return v - } - } - return 0 -} - func init() { plug.Registry.RegisterPrinter(PrinterDelimited, &DelimitedPrinter{}) plug.Registry.RegisterPrinter(PrinterJSON, &JSONPrinter{}) diff --git a/clc/shell/linereader.go b/clc/shell/linereader.go index 627fe646e..b00aecb0e 100644 --- a/clc/shell/linereader.go +++ b/clc/shell/linereader.go @@ -13,6 +13,8 @@ import ( gohxs "github.com/gohxs/readline" ny "github.com/nyaosorg/go-readline-ny" "github.com/nyaosorg/go-readline-ny/simplehistory" + + "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" ) type LineReader interface { @@ -100,6 +102,9 @@ func (sh *Shell) createGohxsLineReader(prompt string) error { Stdout: sh.stdout, Stderr: sh.stderr, Stdin: sh.stdin, + FuncGetWidth: func() int { + return terminal.ConsoleWidth() + }, } if sh.historyPath != "" { cfg.HistoryFile = sh.historyPath diff --git a/internal/terminal/term.go b/internal/terminal/term.go index 8bff8ee85..67baef35b 100644 --- a/internal/terminal/term.go +++ b/internal/terminal/term.go @@ -1,6 +1,13 @@ package terminal -import "os" +import ( + "os" + "strconv" + + "github.com/nathan-fiscaletti/consolesize-go" + + "github.com/hazelcast/hazelcast-commandline-client/clc" +) func IsPipe(v any) bool { s, ok := v.(Stater) @@ -18,3 +25,17 @@ func IsPipe(v any) bool { type Stater interface { Stat() (os.FileInfo, error) } + +func ConsoleWidth() int { + if s, ok := os.LookupEnv(clc.EnvMaxCols); ok { + v, err := strconv.Atoi(s) + if err == nil { + return v + } + } + s, _ := consolesize.GetConsoleSize() + if s == 0 { + return 1000 + } + return s +} From c5277af28fba3b674392a8100f45d571cbb833e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Wed, 20 Sep 2023 11:25:08 +0300 Subject: [PATCH 64/79] [CLC-263] Configuration file format reference (#393) Added the configuration format reference --- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/configuration-format.adoc | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/modules/ROOT/pages/configuration-format.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 472c3ce80..462a283e7 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -33,6 +33,7 @@ ** xref:clc-version.adoc[] ** xref:clc-viridian.adoc[] ** xref:clc-project.adoc[] +* xref:configuration-format.adoc[] * xref:environment-variables.adoc[] * xref:keyboard-shortcuts.adoc[] diff --git a/docs/modules/ROOT/pages/configuration-format.adoc b/docs/modules/ROOT/pages/configuration-format.adoc new file mode 100644 index 000000000..c57fb3ca9 --- /dev/null +++ b/docs/modules/ROOT/pages/configuration-format.adoc @@ -0,0 +1,94 @@ += Configuration Format +:description: The Hazelcast CLC recognizes the following items in the configuration file. + +{description} + +* The configuration is in YAML format. +* It has the `cluster` and `ssl` sections. +* A typical configuration file looks as follows: + +```yaml +cluster: + discovery-token: "XXXXXXXDOr9CWUNhzoyXXXXXXwK9nfRV8Ro4XUc6XXXXX" + name: "pr-abcd123" +ssl: + ca-path: "ca.pem" + cert-path: "cert.pem" + key-path: "key.pem" + key-password: "766391XXXXX" +``` + +== cluster section + +[cols="1a,2a,1a"] +|=== +|Key|Description|Default + +|address +|Address of one of the members in the cluster. +|`localhost:5701` + +|name +|Cluster name. +|`dev` + +|discovery-token +|Viridian discovery token. +| + +|api-base +|Viridian API base URL or name. +| + +|viridian-id +|Viridian cluster ID. +| + +|user +|User name. +| + +|password +|Password. +| + +|=== + +== ssl section + +[cols="1a,2a,1a"] +|=== +|Key|Description|Default + +|enabled +|Set to `true` if SSL is enableed for this cluster. Automatically set to `true` if `cluster.discovery-token` is defined. +|`false` + +|ca-path +|CA path. +| + +|cert-path +|Certificate path. +| + +|key-path +|Key certificate path. +| + +|key-password +|Key certificate password. +| + +|server +|Server name if different from the host name in the certificate. +| + +|skip-verify +|Disables certificate verification. Never use in a production environment. +|`false` + +|=== + + + From f95e8b9f03d94b774c12fa5895a99a9c43efa880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Thu, 21 Sep 2023 09:57:56 +0300 Subject: [PATCH 65/79] Fix install.sh for ash (#397) --- extras/unix/install.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extras/unix/install.sh b/extras/unix/install.sh index a8cec90e9..640b9b543 100644 --- a/extras/unix/install.sh +++ b/extras/unix/install.sh @@ -266,9 +266,13 @@ detect_last_release () { re='$1 ~ /tag_name/ { gsub(/[",]/, "", $2); print($2) }' text="$(httpget https://api.github.com/repos/hazelcast/hazelcast-commandline-client/releases)" if [[ "$state_beta" == "yes" ]]; then + set +e v=$(echo "$text" | awk "$re" | head -1) + set -e else + set +e v=$(echo "$text" | awk "$re" | grep -vi preview | grep -vi beta | head -1) + set -e fi if [[ "$v" == "" ]]; then bye "could not determine the latest version" From 5f2ca097f8e9a7a3d62a7a2f7c3320000e880ef5 Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Thu, 21 Sep 2023 13:13:27 +0300 Subject: [PATCH 66/79] [CLC-219]: Add Check Updates (#294) --- Makefile | 2 + base/commands/shell.go | 3 + base/commands/update_check.go | 110 ++++++++++++++++++ base/commands/update_check_test.go | 34 ++++++ .../ROOT/pages/environment-variables.adoc | 4 + internal/github.go | 60 ++++++++++ internal/version.go | 8 +- 7 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 base/commands/update_check.go create mode 100644 base/commands/update_check_test.go create mode 100644 internal/github.go diff --git a/Makefile b/Makefile index 3e636a0e3..28b53c01d 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ GIT_COMMIT = $(shell git rev-parse HEAD 2> /dev/null || echo unknown) CLC_VERSION ?= v0.0.0-CUSTOMBUILD +CLC_SKIP_UPDATE_CHECK ?= 0 +LDFLAGS = "-s -w -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)' -X 'github.com/hazelcast/hazelcast-commandline-client/internal.SkipUpdateCheck=$(CLC_SKIP_UPDATE_CHECK)'" 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 ?= -count 1 -timeout 30m -race diff --git a/base/commands/shell.go b/base/commands/shell.go index 692464f9b..f9c62a8b2 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -75,6 +75,9 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext logText = fmt.Sprintf("Log %9s : %s", logLevel, logPath) } check.I2(fmt.Fprintf(ec.Stdout(), banner, internal.Version, cfgText, logText)) + if err = MaybePrintNewVersionNotification(ctx, ec); err != nil { + ec.Logger().Error(err) + } } endLineFn := makeEndLineFunc() textFn := makeTextFunc(m, ec, func(shortcut string) bool { diff --git a/base/commands/update_check.go b/base/commands/update_check.go new file mode 100644 index 000000000..af0af1097 --- /dev/null +++ b/base/commands/update_check.go @@ -0,0 +1,110 @@ +package commands + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/store" + "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" +) + +const skipUpdateCheck = "CLC_SKIP_UPDATE_CHECK" + +const newVersionWarning = `A newer version of CLC is available. +Visit the following link for release notes and to download: +https://github.com/hazelcast/hazelcast-commandline-client/releases/%s +` + +const ( + updateCheckKey = "update.nextCheckTime" + updateVersionKey = "update.latestVersion" + checkInterval = time.Hour * 24 * 7 +) + +func MaybePrintNewVersionNotification(ctx context.Context, ec plug.ExecContext) error { + sa := store.NewStoreAccessor(filepath.Join(paths.Caches(), "update"), ec.Logger()) + shouldSkip, err := shouldSkipNewerVersion(sa) + if err != nil { + return err + } + var latest string + if shouldSkip { + v, err := sa.WithLock(func(s *store.Store) (any, error) { + return s.GetEntry([]byte(updateVersionKey)) + }) + if err != nil { + return err + } + latest = string(v.([]byte)) + } else { + latest, err = internal.LatestReleaseVersion(ctx) + if err != nil { + return err + } + if err = UpdateVersionAndNextCheckTime(sa, latest); err != nil { + return err + } + } + if latest != "" && internal.CheckVersion(trimVersion(latest), ">", trimVersion(internal.Version)) { + ec.PrintlnUnnecessary(fmt.Sprintf(newVersionWarning, latest)) + } + return nil +} + +func shouldSkipNewerVersion(sa *store.StoreAccessor) (bool, error) { + if internal.Version == internal.UnknownVersion { + return true, nil + } + if strings.Contains(internal.Version, internal.CustomBuildSuffix) { + return true, nil + } + if internal.SkipUpdateCheck == "1" { + return true, nil + } + if os.Getenv(skipUpdateCheck) == "1" { + return true, nil + } + nextCheck, err := sa.WithLock(func(s *store.Store) (any, error) { + return s.GetEntry([]byte(updateCheckKey)) + }) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return false, nil + } + return true, err + } + var nextCheckTS time.Time + t, err := strconv.ParseInt(string(nextCheck.([]byte)), 10, 64) + if err != nil { + return true, err + } + nextCheckTS = time.Unix(t, 0) + if time.Now().Before(nextCheckTS) { + return true, nil + } + return false, nil +} + +func trimVersion(v string) string { + return strings.TrimPrefix(strings.Split(v, "-")[0], "v") +} + +func UpdateVersionAndNextCheckTime(sa *store.StoreAccessor, v string) error { + _, err := sa.WithLock(func(s *store.Store) (any, error) { + err := s.SetEntry([]byte(updateCheckKey), + []byte(strconv.FormatInt(time.Now().Add(checkInterval).Unix(), 10))) + if err != nil { + return nil, err + } + return nil, s.SetEntry([]byte(updateVersionKey), []byte(v)) + }) + return err +} diff --git a/base/commands/update_check_test.go b/base/commands/update_check_test.go new file mode 100644 index 000000000..63d6890e2 --- /dev/null +++ b/base/commands/update_check_test.go @@ -0,0 +1,34 @@ +package commands_test + +import ( + "context" + "path/filepath" + "testing" + + "github.com/hazelcast/hazelcast-commandline-client/base/commands" + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" + "github.com/hazelcast/hazelcast-commandline-client/clc/store" + "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/it" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/stretchr/testify/assert" +) + +func Test_maybePrintNewVersionNotification(t *testing.T) { + tcx := it.TestContext{T: t} + tcx.Tester(func(tcx it.TestContext) { + ec := it.NewExecuteContext(nil) + sa := store.NewStoreAccessor(filepath.Join(paths.Caches(), "update"), log.NopLogger{}) + check.Must(commands.UpdateVersionAndNextCheckTime(sa, "v5.3.2")) + internal.Version = "v5.3.0" + check.Must(commands.MaybePrintNewVersionNotification(context.TODO(), ec)) + o := ec.StdoutText() + expected := `A newer version of CLC is available. +Visit the following link for release notes and to download: +https://github.com/hazelcast/hazelcast-commandline-client/releases/v5.3.2 + +` + assert.Equal(t, expected, o) + }) +} diff --git a/docs/modules/ROOT/pages/environment-variables.adoc b/docs/modules/ROOT/pages/environment-variables.adoc index 24fefaeb2..7c966613e 100644 --- a/docs/modules/ROOT/pages/environment-variables.adoc +++ b/docs/modules/ROOT/pages/environment-variables.adoc @@ -63,6 +63,10 @@ The following environment variables are experimental and may be removed in a fut |Disables color output if set to `1`. |`0` (false) +|CLC_SKIP_UPDATE_CHECK +|Disables checking new version of CLC. +|`0` (false) + |=== diff --git a/internal/github.go b/internal/github.go new file mode 100644 index 000000000..9f253a914 --- /dev/null +++ b/internal/github.go @@ -0,0 +1,60 @@ +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" +) + +const latestReleaseURL = "https://api.github.com/repos/hazelcast/hazelcast-commandline-client/releases" + +// LatestReleaseVersion returns the latest release version, except beta ones +func LatestReleaseVersion(ctx context.Context) (string, error) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + req, err := http.NewRequest(http.MethodGet, latestReleaseURL, nil) + if err != nil { + return "", err + } + req = req.WithContext(ctx) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + respData, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + var data []map[string]any + err = json.Unmarshal(respData, &data) + if err != nil { + return "", err + } + var release map[string]any + for _, d := range data { + prs, ok := d["prerelease"] + if !ok { + continue + } + pr, ok := prs.(bool) + if !ok { + continue + } + if !pr { + release = d + break + } + } + if release == nil { + return "", fmt.Errorf("no stable release") + } + t, ok := release["tag_name"].(string) + if !ok { + return "", errors.New("fetching tag_name") + } + return t, nil +} diff --git a/internal/version.go b/internal/version.go index 544497b49..4ec6e9dd5 100644 --- a/internal/version.go +++ b/internal/version.go @@ -6,10 +6,14 @@ import ( "strings" ) +const UnknownVersion = "UNKNOWN" +const CustomBuildSuffix = "CUSTOMBUILD" + // being initialized at compile-time. var ( - GitCommit string - Version = "UNKNOWN" + GitCommit string + Version = UnknownVersion + SkipUpdateCheck = "0" ) // CheckVersion checks whether left OP right condition holds. From 39c2caa0c85fec6d5b71a816b50ca61e0344c6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 22 Sep 2023 15:08:59 +0300 Subject: [PATCH 67/79] Small fixes (#391) Small fixes --- base/commands/config/config_list.go | 2 +- base/commands/demo/demo_generate_data.go | 15 +--- base/commands/job/common.go | 3 +- base/commands/map_common.go | 2 +- base/commands/sql/testdata/sql_help.txt | 14 +-- .../viridian/viridian_cluster_delete.go | 4 +- clc/config/wizard/list.go | 34 +++++--- clc/config/wizard_provider.go | 7 +- clc/shell/common.go | 14 +-- clc/ux/stage/stage.go | 86 ++++++++++++++----- clc/ux/stage/stage_test.go | 12 +-- docs/modules/ROOT/pages/clc-project.adoc | 2 +- 12 files changed, 122 insertions(+), 73 deletions(-) diff --git a/base/commands/config/config_list.go b/base/commands/config/config_list.go index ce34b1f54..9c0f8fc49 100644 --- a/base/commands/config/config_list.go +++ b/base/commands/config/config_list.go @@ -56,7 +56,7 @@ func (ListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { ec.PrintlnUnnecessary("OK No configurations found.") return nil } - msg := fmt.Sprintf("OK Found %d configurations.", len(rows)) + msg := fmt.Sprintf("OK Found %d configuration(s).", len(rows)) defer ec.PrintlnUnnecessary(msg) return ec.AddOutputRows(ctx, rows...) } diff --git a/base/commands/demo/demo_generate_data.go b/base/commands/demo/demo_generate_data.go index ba425e4b3..50a26ed5b 100644 --- a/base/commands/demo/demo_generate_data.go +++ b/base/commands/demo/demo_generate_data.go @@ -133,7 +133,7 @@ func generateResult(ctx context.Context, ec plug.ExecContext, generator dataStre errCh := make(chan error) itemCh, stopStream := generator.Stream(ctx) defer stopStream() - ci, err := ec.ClientInternal(ctx) + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return 0, err } @@ -232,19 +232,6 @@ var supportedEventStreams = map[string]dataStreamGenerator{ "wikipedia-event-stream": wikimedia.StreamGenerator{}, } -func getMap(ctx context.Context, ec plug.ExecContext, sp clc.Spinner, mapName string) (*hazelcast.Map, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } - sp.SetText(fmt.Sprintf("Getting Map '%s'", mapName)) - m, err := ci.Client().GetMap(ctx, mapName) - if err != nil { - return nil, err - } - return m, nil -} - func init() { check.Must(plug.Registry.RegisterCommand("demo:generate-data", &GenerateDataCommand{})) } diff --git a/base/commands/job/common.go b/base/commands/job/common.go index bfaae82ab..0d07d76a0 100644 --- a/base/commands/job/common.go +++ b/base/commands/job/common.go @@ -12,6 +12,7 @@ import ( "github.com/hazelcast/hazelcast-go-client/types" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -64,7 +65,7 @@ func terminateJob(ctx context.Context, ec plug.ExecContext, tm int32, cm Termina SuccessMsg: fmt.Sprintf(cm.successMsg, nameOrID), FailureMsg: cm.failureMsg, Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { - ci, err := ec.ClientInternal(ctx) + ci, err := cmd.ClientInternal(ctx, ec, status) if err != nil { return nil, err } diff --git a/base/commands/map_common.go b/base/commands/map_common.go index cdf9182a9..e8997e8ca 100644 --- a/base/commands/map_common.go +++ b/base/commands/map_common.go @@ -411,7 +411,7 @@ This command is only available in the interactive mode.`, cm.typeName) func (cm MapUnlockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := ec.ClientInternal(ctx) + ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err } diff --git a/base/commands/sql/testdata/sql_help.txt b/base/commands/sql/testdata/sql_help.txt index 15eb86459..90c607e26 100644 --- a/base/commands/sql/testdata/sql_help.txt +++ b/base/commands/sql/testdata/sql_help.txt @@ -1,8 +1,8 @@ $Shortcut Commands:$ -$\di List Indexes$ -$\di MAPPING List Indexes for a specific mapping$ -$\dm List mappings$ -$\dm MAPPING Display information about a mapping$ -$\dm+ MAPPING Describe a mapping$ -$\exit Exit the shell$ -$\help Display help for CLC commands$ \ No newline at end of file +$\di List indexes$ +$\di MAPPING List indexes for a specific mapping$ +$\dm List mappings$ +$\dm MAPPING Display information about a mapping$ +$\dm+ MAPPING Describe a mapping$ +$\exit Exit the shell$ +$\help Display help for CLC commands$ \ No newline at end of file diff --git a/base/commands/viridian/viridian_cluster_delete.go b/base/commands/viridian/viridian_cluster_delete.go index ee7ca67f3..4cc818dc8 100644 --- a/base/commands/viridian/viridian_cluster_delete.go +++ b/base/commands/viridian/viridian_cluster_delete.go @@ -76,9 +76,9 @@ func (ClusterDeleteCommand) Exec(ctx context.Context, ec plug.ExecContext) error } if ec.Props().GetBool(clc.PropertyVerbose) { row = append(row, output.Column{ - Name: "ID", + Name: "Name", Type: serialization.TypeString, - Value: cluster.ID, + Value: cluster.Name, }) } return ec.AddOutputRows(ctx, row) diff --git a/clc/config/wizard/list.go b/clc/config/wizard/list.go index dfe42c46e..054f69480 100644 --- a/clc/config/wizard/list.go +++ b/clc/config/wizard/list.go @@ -7,6 +7,8 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + + "github.com/hazelcast/hazelcast-commandline-client/internal/check" ) const listHeight = 14 @@ -18,26 +20,36 @@ var ( type item string -func (i item) FilterValue() string { return "" } +func (i item) FilterValue() string { + return "" +} type itemDelegate struct{} -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Height() int { + return 1 +} + +func (d itemDelegate) Spacing() int { + return 0 +} + +func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { + return nil +} + func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(item) + v, ok := listItem.(item) if !ok { return } - str := fmt.Sprintf("%d. %s", index+1, i) - fn := itemStyle.Render + var text string if index == m.Index() { - fn = func(s ...string) string { - return selectedItemStyle.Render(s[0]) - } + text = selectedItemStyle.Render(string(v)) + } else { + text = itemStyle.Render(string(v)) } - fmt.Fprint(w, fn(str)) + check.I2(fmt.Fprint(w, " "+text)) } type model struct { diff --git a/clc/config/wizard_provider.go b/clc/config/wizard_provider.go index 98bd0d048..71b395f4f 100644 --- a/clc/config/wizard_provider.go +++ b/clc/config/wizard_provider.go @@ -3,7 +3,6 @@ package config import ( "context" "errors" - "fmt" "os" "sync/atomic" @@ -21,6 +20,10 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) +var ( + errNoConfig = errors.New("no configuration was provided and cannot display the configuration wizard; use the --config flag") +) + type WizardProvider struct { fp *atomic.Pointer[FileProvider] cfg hazelcast.Config @@ -63,7 +66,7 @@ func (p *WizardProvider) ClientConfig(ctx context.Context, ec plug.ExecContext) 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`) + return hazelcast.Config{}, errNoConfig } // ask the config to the user name, err := p.runWizard(ctx, ec) diff --git a/clc/shell/common.go b/clc/shell/common.go index 3549cc632..beaab903f 100644 --- a/clc/shell/common.go +++ b/clc/shell/common.go @@ -101,12 +101,12 @@ func ConvertStatement(ctx context.Context, ec plug.ExecContext, stmt string) (fu func InteractiveHelp() string { return ` Shortcut Commands: - \di List Indexes - \di MAPPING List Indexes for a specific mapping - \dm List mappings - \dm MAPPING Display information about a mapping - \dm+ MAPPING Describe a mapping - \exit Exit the shell - \help Display help for CLC commands + \di List indexes + \di MAPPING List indexes for a specific mapping + \dm List mappings + \dm MAPPING Display information about a mapping + \dm+ MAPPING Describe a mapping + \exit Exit the shell + \help Display help for CLC commands ` } diff --git a/clc/ux/stage/stage.go b/clc/ux/stage/stage.go index 44518fad7..44c2291f0 100644 --- a/clc/ux/stage/stage.go +++ b/clc/ux/stage/stage.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sync" "time" "github.com/hazelcast/hazelcast-commandline-client/clc" @@ -14,35 +15,83 @@ import ( ) type Statuser[T any] interface { + SetText(text string) SetProgress(progress float32) SetRemainingDuration(dur time.Duration) Value() T } type basicStatuser[T any] struct { - text string - textFmtWithRemaining string - indexText string - sp clc.Spinner - value T + sp clc.Spinner + value T + mu *sync.RWMutex + index int + count int + text string } -func (s basicStatuser[T]) SetProgress(progress float32) { +func newBasicStatuser[T any](value T) *basicStatuser[T] { + return &basicStatuser[T]{ + value: value, + mu: &sync.RWMutex{}, + } +} + +func (s *basicStatuser[T]) SetText(text string) { + s.mu.Lock() + s.text = text + sp := s.sp + s.mu.Unlock() + if sp != nil { + s.sp.SetText(" " + s.IndexText() + " " + text) + } +} + +func (s *basicStatuser[T]) SetProgress(progress float32) { s.sp.SetProgress(progress) } -func (s basicStatuser[T]) SetRemainingDuration(dur time.Duration) { +func (s *basicStatuser[T]) SetRemainingDuration(dur time.Duration) { + s.mu.RLock() text := s.text if dur > 0 { - text = fmt.Sprintf(s.textFmtWithRemaining, dur) + text = fmt.Sprintf(s.textFmtWithRemaining(), dur) } - s.sp.SetText(" " + s.indexText + " " + text) + s.mu.RUnlock() + s.sp.SetText(" " + s.IndexText() + " " + text) } -func (s basicStatuser[T]) Value() T { +func (s *basicStatuser[T]) Value() T { return s.value } +func (s *basicStatuser[T]) SetIndex(index, count int) { + s.mu.Lock() + s.index = index + s.count = count + s.mu.Unlock() +} + +func (s *basicStatuser[T]) IndexText() string { + s.mu.RLock() + defer s.mu.RUnlock() + if s.index == 0 || s.count < 2 { + return "" + } + d := str.SpacePaddedIntFormat(s.count) + return fmt.Sprintf("["+d+"/%d]", s.index, s.count) +} + +func (s *basicStatuser[T]) SetSpinner(sp clc.Spinner) { + s.mu.Lock() + s.sp = sp + s.mu.Unlock() +} + +func (s *basicStatuser[T]) textFmtWithRemaining() string { + return s.text + " (%s left)" +} + type Stage[T any] struct { ProgressMsg string SuccessMsg string @@ -100,16 +149,11 @@ func Execute[T any](ctx context.Context, ec plug.ExecContext, value T, sp Provid } stg := sp.Value() index++ - ss := basicStatuser[T]{value: value} - ss.text = stg.ProgressMsg - ss.textFmtWithRemaining = stg.ProgressMsg + " (%s left)" - ss.indexText = "" - if stageCount > 1 { - d := str.SpacePaddedIntFormat(stageCount) - ss.indexText = fmt.Sprintf("["+d+"/%d]", index, stageCount) - } + ss := newBasicStatuser(value) + ss.SetText(stg.ProgressMsg) + ss.SetIndex(index, stageCount) v, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, spinner clc.Spinner) (any, error) { - ss.sp = spinner + ss.SetSpinner(spinner) ss.SetRemainingDuration(0) v, err := stg.Func(ctx, ss) if err != nil { @@ -121,7 +165,7 @@ func Execute[T any](ctx context.Context, ec plug.ExecContext, value T, sp Provid var ie ignoreError if errors.As(err, &ie) { // the error can be ignored - ec.PrintlnUnnecessary(fmt.Sprintf("ERROR %s %s: %s", ss.indexText, stg.FailureMsg, ie.Unwrap().Error())) + ec.PrintlnUnnecessary(fmt.Sprintf("ERROR %s %s: %s", ss.IndexText(), stg.FailureMsg, ie.Unwrap().Error())) } else { ec.PrintlnUnnecessary(fmt.Sprintf("ERROR %s: %s", stg.FailureMsg, err.Error())) return value, hzerrors.WrappedError{Err: err} @@ -129,7 +173,7 @@ func Execute[T any](ctx context.Context, ec plug.ExecContext, value T, sp Provid } stop() if err == nil { - ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg)) + ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.IndexText(), stg.SuccessMsg)) } if v == nil { var vv T diff --git a/clc/ux/stage/stage_test.go b/clc/ux/stage/stage_test.go index dbf4c6ce4..daacbda47 100644 --- a/clc/ux/stage/stage_test.go +++ b/clc/ux/stage/stage_test.go @@ -53,6 +53,7 @@ func executeTest(t *testing.T) { SuccessMsg: "Success 3", FailureMsg: "Failure 3", Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + status.SetText("Custom text") for i := 0; i < 5; i++ { status.SetRemainingDuration(5*time.Second - time.Duration(i+1)*time.Second) } @@ -68,11 +69,12 @@ func executeTest(t *testing.T) { " [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", + " [3/3] Custom text", + " [3/3] Custom text (4s left)", + " [3/3] Custom text (3s left)", + " [3/3] Custom text (2s left)", + " [3/3] Custom text (1s left)", + " [3/3] Custom text", } assert.Equal(t, texts, ec.Spinner.Texts) progresses := []float32{0.2, 0.4, 0.6, 0.8, 1} diff --git a/docs/modules/ROOT/pages/clc-project.adoc b/docs/modules/ROOT/pages/clc-project.adoc index f2dfe8c7f..536e35469 100644 --- a/docs/modules/ROOT/pages/clc-project.adoc +++ b/docs/modules/ROOT/pages/clc-project.adoc @@ -54,7 +54,7 @@ Templates are located in https://github.com/hazelcast-templates. You can overrid ==== Rules while creating your own templates: -* Templates are in link:.https://pkg.go.dev/text/template[Go template] format. +* Templates are in link:https://pkg.go.dev/text/template[Go template] format. * You can create a "defaults.yaml" file for default values in template's root directory. * Template files must have the ".template" extension. * Files with "." and "_" prefixes are ignored unless they have the ".keep" extension. From 28f48fe189e49c357cd144d0b5447d904ed7b4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 22 Sep 2023 15:23:42 +0300 Subject: [PATCH 68/79] Set the default log path in tests (#399) --- internal/it/test_context.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/it/test_context.go b/internal/it/test_context.go index 2140ac657..440b3aef0 100644 --- a/internal/it/test_context.go +++ b/internal/it/test_context.go @@ -169,6 +169,12 @@ func (tcx TestContext) Tester(f func(tcx TestContext)) { d, _ := filepath.Split(p) check.Must(os.MkdirAll(d, 0700)) home.WithFile(p, bytesConfig, func(_ string) { + if tcx.LogPath == "" { + tcx.LogPath = paths.ResolveLogPath("test") + } + if tcx.LogLevel == "" { + tcx.LogLevel = "info" + } tcx.main = check.MustValue(tcx.createMain()) tcx.T.Logf("created CLC main") defer func() { From 0484632aaa26a6037618ff2def82e5ea9f9fce35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Fri, 22 Sep 2023 18:22:16 +0300 Subject: [PATCH 69/79] [CLC-340] Single config is the default config (#396) If the config was not provided, and there's a single config, return it --- README.md | 11 +-- .../{config_test.go => config_it_test.go} | 14 ++++ clc/config/file_provider.go | 8 +-- clc/config/wizard_provider.go | 72 +++++++++---------- docs/modules/ROOT/pages/configuration.adoc | 14 +--- 5 files changed, 56 insertions(+), 63 deletions(-) rename clc/config/{config_test.go => config_it_test.go} (95%) diff --git a/README.md b/README.md index 822af986d..f37caafcd 100644 --- a/README.md +++ b/README.md @@ -58,14 +58,6 @@ This file can exist anywhere in the file system, and can be used with the `--con $ clc -c test/config.yaml ``` -If there is a `config.yaml` in the same directory with the CLC binary and the configuration was not explicitly set, CLC tries to load that configuration file: -``` -$ ls -lh -total 17M --rwxrwxr-x 1 yuce yuce 17M Nov 26 23:11 clc* --rw------- 1 yuce yuce 200 Nov 26 23:12 config.yaml -``` - `configs` directory in `$CLC_HOME` is special, it contains all the configurations known to CLC. Known configurations can be directly specified by their names, instead of the full path. `clc config list` command lists the configurations known to CLC: @@ -80,6 +72,9 @@ $ clc -c pr-3066 ``` If no configuration is specified, the `default` configuration is used if it exists. +The name of the default configuration may be overriden using the `CLC_CONFIG` environment variable. + +If there's only a single named configuration, then it is used if the configuration is not specified with `-c`/`--config`. #### Configuration format diff --git a/clc/config/config_test.go b/clc/config/config_it_test.go similarity index 95% rename from clc/config/config_test.go rename to clc/config/config_it_test.go index 26fa5c358..043ebb423 100644 --- a/clc/config/config_test.go +++ b/clc/config/config_it_test.go @@ -2,6 +2,7 @@ package config_test import ( "bytes" + "context" "crypto/tls" "fmt" "os" @@ -14,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/hazelcast/hazelcast-commandline-client/internal/it" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-commandline-client/internal/types" @@ -298,6 +300,18 @@ func TestConvertKeyValuesToMap(t *testing.T) { assert.Equal(t, target, m) } +func TestSingleConfig(t *testing.T) { + tcx := it.TestContext{T: t} + tcx.Tester(func(tcx it.TestContext) { + p := MustValue(config.NewWizardProvider("")) + ctx := context.Background() + ec := it.NewExecuteContext(nil) + cfg, err := p.ClientConfig(ctx, ec) + assert.NoError(t, err) + assert.Equal(t, []string{"localhost:10000"}, cfg.Cluster.Network.Addresses) + }) +} + func userHostName() string { u := MustValue(user.Current()) host := MustValue(os.Hostname()) diff --git a/clc/config/file_provider.go b/clc/config/file_provider.go index 4fe8628c6..4371f8e8b 100644 --- a/clc/config/file_provider.go +++ b/clc/config/file_provider.go @@ -53,11 +53,11 @@ func NewFileProvider(path string) (*FileProvider, error) { func (p *FileProvider) load(path string) error { path = paths.ResolveConfigPath(path) + if path == "" { + // there is no default config, user will be prompted for config later + return nil + } if !paths.Exists(path) { - if path == "" { - // there is no default config, user will be prompted for config later - return nil - } return fmt.Errorf("configuration does not exist %s: %w", path, os.ErrNotExist) } p.path = path diff --git a/clc/config/wizard_provider.go b/clc/config/wizard_provider.go index 71b395f4f..06f82c6b7 100644 --- a/clc/config/wizard_provider.go +++ b/clc/config/wizard_provider.go @@ -11,7 +11,6 @@ import ( "github.com/spf13/pflag" "github.com/hazelcast/hazelcast-commandline-client/clc" - "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" "github.com/hazelcast/hazelcast-commandline-client/clc/config/wizard" @@ -64,57 +63,50 @@ func maybeUnwrapStdout(ec plug.ExecContext) any { func (p *WizardProvider) ClientConfig(ctx context.Context, ec plug.ExecContext) (hazelcast.Config, error) { cfg, err := p.fp.Load().ClientConfig(ctx, ec) + if err == nil { + // note that comparing err to nil + return cfg, nil + } + var configName string + if !errors.Is(err, clcerrors.ErrNoClusterConfig) { + return hazelcast.Config{}, err + } + cs, err := FindAll(paths.Configs()) if err != nil { + if errors.Is(err, os.ErrNotExist) { + return hazelcast.Config{}, clcerrors.ErrNoClusterConfig + } + } + if len(cs) == 0 { + return hazelcast.Config{}, clcerrors.ErrNoClusterConfig + } + if len(cs) == 1 { + configName = cs[0] + } + if configName == "" { if terminal.IsPipe(maybeUnwrapStdout(ec)) { return hazelcast.Config{}, errNoConfig } // ask the config to the user - name, err := p.runWizard(ctx, ec) + configName, err = p.runWizard(cs) if err != nil { return hazelcast.Config{}, err } - fp, err := NewFileProvider(name) - if err != nil { - return cfg, err - } - config, err := fp.ClientConfig(ctx, ec) - if err != nil { - return hazelcast.Config{}, err - } - p.fp.Store(fp) - return config, nil } - return cfg, nil -} - -func (p *WizardProvider) runWizard(ctx context.Context, ec plug.ExecContext) (string, error) { - cs, err := FindAll(paths.Configs()) + fp, err := NewFileProvider(configName) if err != nil { - if errors.Is(err, os.ErrNotExist) { - err = os.MkdirAll(paths.Configs(), 0700) - } - if err != nil { - return "", err - } + return cfg, err } - if len(cs) == 0 { - m := wizard.InitialModel() - mv, err := tea.NewProgram(m).Run() - if err != nil { - return "", err - } - if mv.View() == "" { - return "", clcerrors.ErrNoClusterConfig - } - args := m.GetInputs() - stages := MakeImportStages(ec, args[0]) - _, err = stage.Execute(ctx, ec, args[1], stage.NewFixedProvider(stages...)) - if err != nil { - return "", err - } - return args[0], nil + config, err := fp.ClientConfig(ctx, ec) + if err != nil { + return hazelcast.Config{}, err } - m := wizard.InitializeList(cs) + p.fp.Store(fp) + return config, nil +} + +func (p *WizardProvider) runWizard(configNames []string) (string, error) { + m := wizard.InitializeList(configNames) model, err := tea.NewProgram(m).Run() if err != nil { return "", err diff --git a/docs/modules/ROOT/pages/configuration.adoc b/docs/modules/ROOT/pages/configuration.adoc index 3601d5fdc..2077147a0 100644 --- a/docs/modules/ROOT/pages/configuration.adoc +++ b/docs/modules/ROOT/pages/configuration.adoc @@ -10,17 +10,6 @@ This file can exist anywhere in the file system, and can be used with the `--con clc -c test/config.yaml ---- -//TIP: If you try to run an operation that requires a client connection before you have added any configuration, CLC will prompt the configuration wizard to import a {hazelcast-cloud} configuration. For details, see xref:config-wizard.adoc[CLC Configuration Wizard]. - -If there is a `config.yaml` in the same directory with the CLC binary and the configuration was not explicitly set, CLC tries to load that configuration file: -[source, bash] ----- -ls -lh -total 17M --rwxrwxr-x 1 yuce yuce 17M Nov 26 23:11 clc* --rw------- 1 yuce yuce 200 Nov 26 23:12 config.yaml ----- - `configs` directory in `$CLC_HOME` is special, it contains all the configurations known to CLC. You can use the `clc home` command in order to see where `$CLC_HOME` is: [source, bash] ---- @@ -42,6 +31,9 @@ $ clc -c pr-3066 ---- If no configuration is specified, the `default` configuration is used. +The name of the default configuration may be overriden using the `CLC_CONFIG` environment variable. + +If there's only a single named configuration, then it is used if the configuration is not specified with `-c`/`--config`. == CLC Configuration with Command-Line Parameters From 28cc0fb287b6e982e1d777b3e31f8d94cd3570d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 25 Sep 2023 15:30:14 +0300 Subject: [PATCH 70/79] [CLC-366] Updates job submit to deduce the job ID after submission (#388) * Updates job submit to deduce the job ID after submission * Updated * Updates --- base/commands/job/common.go | 42 +++++++------ base/commands/job/job_resume.go | 25 ++++---- base/commands/job/job_submit.go | 104 +++++++++++++++++++++++++++----- internal/jet/job.go | 15 +++-- internal/types/types.go | 29 ++++++++- internal/types/types_test.go | 17 +++++- 6 files changed, 173 insertions(+), 59 deletions(-) diff --git a/base/commands/job/common.go b/base/commands/job/common.go index 0d07d76a0..097ac5270 100644 --- a/base/commands/job/common.go +++ b/base/commands/job/common.go @@ -12,14 +12,14 @@ import ( "github.com/hazelcast/hazelcast-go-client/types" - "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec/control" ) -func WaitJobState(ctx context.Context, ec plug.ExecContext, sp stage.Statuser[any], jobNameOrID string, state int32, duration time.Duration) error { +func WaitJobState(ctx context.Context, ec plug.ExecContext, sp stage.Statuser[int64], state int32, duration time.Duration) error { + jobID := sp.Value() ci, err := ec.ClientInternal(ctx) if err != nil { return err @@ -30,14 +30,15 @@ func WaitJobState(ctx context.Context, ec plug.ExecContext, sp stage.Statuser[an if err != nil { return err } - ok, err := jet.EnsureJobState(jl, jobNameOrID, state) + ok, state, err := jet.EnsureJobState(jl, jobID, state) if err != nil { return err } if ok { return nil } - ec.Logger().Debugf("Waiting %s for job %s to transition to state %s", duration.String(), jobNameOrID, jet.StatusToString(state)) + s := idToString(jobID) + ec.Logger().Debugf("Waiting %s for job %s to transition to state %s", duration.String(), s, jet.StatusToString(state)) time.Sleep(duration) } } @@ -58,61 +59,62 @@ func idToString(id int64) string { func terminateJob(ctx context.Context, ec plug.ExecContext, tm int32, cm TerminateCommand) error { nameOrID := ec.GetStringArg(argJobID) - stages := []stage.Stage[any]{ - stage.MakeConnectStage[any](ec), + stages := []stage.Stage[int64]{ + stage.MakeConnectStage[int64](ec), { ProgressMsg: fmt.Sprintf(cm.inProgressMsg, nameOrID), SuccessMsg: fmt.Sprintf(cm.successMsg, nameOrID), FailureMsg: cm.failureMsg, - Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, status) + Func: func(ctx context.Context, status stage.Statuser[int64]) (int64, error) { + ci, err := ec.ClientInternal(ctx) if err != nil { - return nil, err + return 0, err } j := jet.New(ci, status, ec.Logger()) jis, err := j.GetJobList(ctx) if err != nil { - return nil, err + return 0, err } jm, err := NewJobNameToIDMap(jis) if err != nil { - return nil, err + return 0, err } jid, ok := jm.GetIDForName(nameOrID) if !ok { - return nil, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) + return 0, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) } ec.Logger().Info("%s %s (%s)", cm.inProgressMsg, nameOrID, idToString(jid)) ji, ok := jm.GetInfoForID(jid) if !ok { - return nil, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) + return 0, fmt.Errorf("%w: %s", jet.ErrInvalidJobID, nameOrID) } var coord types.UUID if ji.LightJob { conns := ci.ConnectionManager().ActiveConnections() if len(conns) == 0 { - return nil, errors.New("not connected") + return 0, errors.New("not connected") } coord = conns[0].MemberUUID() } - return nil, j.TerminateJob(ctx, jid, cm.terminateMode, coord) + return jid, j.TerminateJob(ctx, jid, cm.terminateMode, coord) }, }, } wait := ec.Props().GetBool(flagWait) if wait { - stages = append(stages, stage.Stage[any]{ + stages = append(stages, stage.Stage[int64]{ ProgressMsg: fmt.Sprintf("Waiting for job to be %sed", cm.name), SuccessMsg: fmt.Sprintf("Job %s is %sed", nameOrID, cm.name), FailureMsg: fmt.Sprintf("Failed to %s %s", cm.name, nameOrID), - Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { - msg := fmt.Sprintf("Waiting for job %s to be %sed", nameOrID, cm.name) + Func: func(ctx context.Context, status stage.Statuser[int64]) (int64, error) { + s := idToString(status.Value()) + msg := fmt.Sprintf("Waiting for job %s to be %sed", s, cm.name) ec.Logger().Info(msg) - return nil, WaitJobState(ctx, ec, status, nameOrID, cm.waitState, 1*time.Second) + return 0, WaitJobState(ctx, ec, status, cm.waitState, 1*time.Second) }, }) } - _, err := stage.Execute[any](ctx, ec, nil, stage.NewFixedProvider(stages...)) + _, err := stage.Execute(ctx, ec, 0, stage.NewFixedProvider(stages...)) if err != nil { return err } diff --git a/base/commands/job/job_resume.go b/base/commands/job/job_resume.go index b8c7e9e5d..e740b886e 100644 --- a/base/commands/job/job_resume.go +++ b/base/commands/job/job_resume.go @@ -26,45 +26,46 @@ func (ResumeCommand) Init(cc plug.InitContext) error { func (ResumeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { nameOrID := ec.GetStringArg(argJobID) - stages := []stage.Stage[any]{ - stage.MakeConnectStage[any](ec), + stages := []stage.Stage[int64]{ + stage.MakeConnectStage[int64](ec), { ProgressMsg: fmt.Sprintf("Initiating resume of job: %s", nameOrID), SuccessMsg: fmt.Sprintf("Initiated resume of job %s", nameOrID), FailureMsg: fmt.Sprintf("Failed initiating job resume %s", nameOrID), - Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + Func: func(ctx context.Context, status stage.Statuser[int64]) (int64, error) { ci, err := ec.ClientInternal(ctx) if err != nil { - return nil, err + return 0, err } j := jet.New(ci, status, ec.Logger()) jis, err := j.GetJobList(ctx) if err != nil { - return nil, err + return 0, err } jm, err := NewJobNameToIDMap(jis) if err != nil { - return nil, err + return 0, err } jid, ok := jm.GetIDForName(nameOrID) if !ok { - return nil, jet.ErrInvalidJobID + return 0, jet.ErrInvalidJobID } - return nil, j.ResumeJob(ctx, jid) + return jid, j.ResumeJob(ctx, jid) }, }, } if ec.Props().GetBool(flagWait) { - stages = append(stages, stage.Stage[any]{ + stages = append(stages, stage.Stage[int64]{ ProgressMsg: fmt.Sprintf("Waiting for job %s to resume", nameOrID), SuccessMsg: fmt.Sprintf("Job %s is resumed", nameOrID), FailureMsg: fmt.Sprintf("Job %s failed to resume", nameOrID), - Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { - return nil, WaitJobState(ctx, ec, status, nameOrID, jet.JobStatusRunning, 2*time.Second) + Func: func(ctx context.Context, status stage.Statuser[int64]) (int64, error) { + jobID := status.Value() + return jobID, WaitJobState(ctx, ec, status, jet.JobStatusRunning, 2*time.Second) }, }) } - _, err := stage.Execute[any](ctx, ec, nil, stage.NewFixedProvider(stages...)) + _, err := stage.Execute(ctx, ec, 0, stage.NewFixedProvider(stages...)) if err != nil { return err } diff --git a/base/commands/job/job_submit.go b/base/commands/job/job_submit.go index 845483f3b..c89a61171 100644 --- a/base/commands/job/job_submit.go +++ b/base/commands/job/job_submit.go @@ -16,7 +16,10 @@ import ( . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) const ( @@ -55,16 +58,32 @@ func (SubmitCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if !strings.HasSuffix(path, ".jar") { return fmt.Errorf("submitted file is not a jar file: %s", path) } - return submitJar(ctx, ec, path) + jobID, err := submitJar(ctx, ec, path) + if err != nil { + return err + } + jobName := ec.Props().GetString(flagName) + if jobName == "" { + return nil + } + ec.PrintlnUnnecessary("") + return ec.AddOutputRows(ctx, output.Row{ + output.Column{ + Name: "Job ID", + Type: serialization.TypeString, + Value: idToString(jobID), + }, + }) + } -func submitJar(ctx context.Context, ec plug.ExecContext, path string) error { +func submitJar(ctx context.Context, ec plug.ExecContext, path string) (int64, error) { wait := ec.Props().GetBool(flagWait) jobName := ec.Props().GetString(flagName) snapshot := ec.Props().GetString(flagSnapshot) className := ec.Props().GetString(flagClass) if wait && jobName == "" { - return fmt.Errorf("--wait requires the --name to be set") + return 0, fmt.Errorf("--wait requires the --name to be set") } tries := int(ec.Props().GetInt(flagRetries)) if tries < 0 { @@ -74,50 +93,89 @@ func submitJar(ctx context.Context, ec plug.ExecContext, path string) error { _, fn := filepath.Split(path) fn = strings.TrimSuffix(fn, ".jar") args := ec.GetStringSliceArg(argArg) - stages := []stage.Stage[any]{ - stage.MakeConnectStage[any](ec), + stages := []stage.Stage[int64]{ + stage.MakeConnectStage[int64](ec), { ProgressMsg: "Submitting the job", SuccessMsg: "Submitted the job", FailureMsg: "Failed submitting the job", - Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { + Func: func(ctx context.Context, status stage.Statuser[int64]) (int64, error) { ci, err := ec.ClientInternal(ctx) if err != nil { - return nil, err + return 0, err } if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { err := fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) - return nil, err + return 0, err } j := jet.New(ci, status, ec.Logger()) + var jobIDs []int64 err = retry(tries, ec.Logger(), func(try int) error { if try == 0 { ec.Logger().Info("Submitting %s", jobName) } else { ec.Logger().Info("Submitting %s, retry %d", jobName, try) } + // try to deduce the job ID + var before, after types.Set[int64] + if jobName != "" { + before, err = getJobIDs(ctx, j, jobName) + if err != nil { + return err + } + } br := jet.CreateBinaryReaderForPath(path) - return j.SubmitJob(ctx, path, jobName, className, snapshot, args, br) + if err := j.SubmitJob(ctx, path, jobName, className, snapshot, args, br); err != nil { + return err + } + if jobName != "" { + after, err = getJobIDs(ctx, j, jobName) + if err != nil { + return err + } + } + diff := after.Diff(before) + jobIDs = diff.Items() + return nil }) - return nil, err + if jobName == "" { + return 0, nil + } + // at this point we may have 0, 1 or more jobIDs, + // deal with that + if len(jobIDs) == 0 { + // couldn't find any job, + // this is unlikely to happen if the job name was specified + return 0, fmt.Errorf("could not find the job with name: %s", jobName) + } + if len(jobIDs) > 1 { + // there are more than one jobs with the same name, + // this is a problem! + ec.Logger().Warn("Multiple job IDs returned for job with name: %s", jobName) + return 0, fmt.Errorf("could not determine the job ID") + } + // ideal case, there's only job with this name. + // it must be the one we submitted. + return jobIDs[0], err }, }, } if wait { - stages = append(stages, stage.Stage[any]{ + stages = append(stages, stage.Stage[int64]{ ProgressMsg: fmt.Sprintf("Waiting for job %s to start", jobName), SuccessMsg: fmt.Sprintf("Job %s started", jobName), FailureMsg: fmt.Sprintf("Job %s failed to start", jobName), - Func: func(ctx context.Context, status stage.Statuser[any]) (any, error) { - return nil, WaitJobState(ctx, ec, status, jobName, jet.JobStatusRunning, 2*time.Second) + Func: func(ctx context.Context, status stage.Statuser[int64]) (int64, error) { + jobID := status.Value() + return jobID, WaitJobState(ctx, ec, status, jet.JobStatusRunning, 2*time.Second) }, }) } - _, err := stage.Execute[any](ctx, ec, nil, stage.NewFixedProvider(stages...)) + jobID, err := stage.Execute[int64](ctx, ec, 0, stage.NewFixedProvider(stages...)) if err != nil { - return err + return 0, err } - return nil + return jobID, nil } func retry(times int, lg log.Logger, f func(try int) error) error { @@ -134,6 +192,20 @@ func retry(times int, lg log.Logger, f func(try int) error) error { return fmt.Errorf("failed after %d tries: %w", times, err) } +func getJobIDs(ctx context.Context, j *jet.Jet, jobName string) (types.Set[int64], error) { + jl, err := j.GetJobList(ctx) + if err != nil { + return types.Set[int64]{}, err + } + ids := types.MakeSet[int64]() + for _, item := range jl { + if item.NameOrId == jobName { + ids.Add(item.JobId) + } + } + return ids, nil +} + func init() { Must(plug.Registry.RegisterCommand("job:submit", &SubmitCommand{})) } diff --git a/internal/jet/job.go b/internal/jet/job.go index e0796f070..9aa0fae38 100644 --- a/internal/jet/job.go +++ b/internal/jet/job.go @@ -140,17 +140,20 @@ func (j Jet) ResumeJob(ctx context.Context, jobID int64) error { return nil } -func EnsureJobState(jobs []control.JobAndSqlSummary, jobNameOrID string, state int32) (bool, error) { +func EnsureJobState(jobs []control.JobAndSqlSummary, jobID int64, state int32) (bool, int32, error) { for _, j := range jobs { - if j.NameOrId == jobNameOrID { + if j.JobId == jobID { if j.Status == state { - return true, nil + return true, j.Status, nil + } + if j.Status == JobStatusCompleted { + return false, j.Status, nil } if j.Status == JobStatusFailed { - return false, ErrJobFailed + return false, j.Status, ErrJobFailed } - return false, nil + return false, j.Status, nil } } - return false, ErrJobNotFound + return false, -1, ErrJobNotFound } diff --git a/internal/types/types.go b/internal/types/types.go index 4c208bad2..2e57e52b6 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -16,25 +16,50 @@ type Set[K comparable] struct { m map[K]struct{} } -func NewSet[K comparable](items ...K) *Set[K] { +// MakeSet creates a set with the given items. +func MakeSet[K comparable](items ...K) Set[K] { s := Set[K]{ m: map[K]struct{}{}, } for _, v := range items { s.Add(v) } - return &s + return s } +// Add adds the given item to the set. func (s *Set[K]) Add(item K) { s.m[item] = struct{}{} } +// Has returns true if the given item is in the set. func (s *Set[K]) Has(item K) bool { _, ok := s.m[item] return ok } +// Len returns the number of items in the set. func (s *Set[K]) Len() int { return len(s.m) } + +// Diff returns a set with items which are not in other. +func (s Set[K]) Diff(other Set[K]) Set[K] { + r := MakeSet[K]() + for my := range s.m { + if !other.Has(my) { + r.Add(my) + } + } + return r +} + +// Items returns the items in the set. +// Returned items are not sorted. +func (s Set[K]) Items() []K { + r := make([]K, 0, len(s.m)) + for item := range s.m { + r = append(r, item) + } + return r +} diff --git a/internal/types/types_test.go b/internal/types/types_test.go index 14b2df575..0fd9f55f8 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) @@ -14,6 +15,7 @@ func TestTypes(t *testing.T) { f func(t *testing.T) }{ {name: "setAdd", f: setAddTest}, + {name: "setDiff", f: setDiffTest}, {name: "setEmpty", f: setEmptyTest}, {name: "setNew", f: setNewTest}, } @@ -23,20 +25,29 @@ func TestTypes(t *testing.T) { } func setEmptyTest(t *testing.T) { - s := types.NewSet[string]() + s := types.MakeSet[string]() assert.Equal(t, 0, s.Len()) } func setNewTest(t *testing.T) { - s := types.NewSet[string]("foo", "bar") + s := types.MakeSet[string]("foo", "bar") assert.Equal(t, 2, s.Len()) assert.True(t, s.Has("foo")) assert.True(t, s.Has("bar")) } func setAddTest(t *testing.T) { - s := types.NewSet[string]() + s := types.MakeSet[string]() s.Add("foo") assert.Equal(t, 1, s.Len()) assert.True(t, s.Has("foo")) } + +func setDiffTest(t *testing.T) { + s1 := types.MakeSet[string]("foo", "bar", "baz") + s2 := types.MakeSet[string]("bar") + s3 := s1.Diff(s2) + items := s3.Items() + slices.Sort(items) + assert.Equal(t, []string{"baz", "foo"}, items) +} From 71746a35517230391aaf47754549e081e696c3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 26 Sep 2023 09:22:10 +0300 Subject: [PATCH 71/79] [CLC-365] Makes sure map proxy is created in map commands (#400) Makes sure map proxy is created in map commands --- base/commands/common.go | 22 +++++ base/commands/map/map_entry_set.go | 2 +- base/commands/map/map_get.go | 4 +- base/commands/map/map_key_set.go | 2 +- base/commands/map/map_load_all.go | 3 +- base/commands/map/map_size.go | 4 +- base/commands/map/map_values.go | 2 +- base/commands/map_common.go | 96 ++++++++++---------- base/commands/multimap/multimap_entry_set.go | 2 +- base/commands/multimap/multimap_get.go | 3 +- base/commands/multimap/multimap_key_set.go | 2 +- base/commands/multimap/multimap_values.go | 2 +- 12 files changed, 87 insertions(+), 57 deletions(-) diff --git a/base/commands/common.go b/base/commands/common.go index 1653af99f..a0f8959f5 100644 --- a/base/commands/common.go +++ b/base/commands/common.go @@ -207,6 +207,28 @@ func AddValueTypeFlag(cc plug.InitContext) { cc.AddStringFlag(FlagValueType, "v", "string", false, help) } +func MakeValueFromString(s string, typeStr string) (any, error) { + if typeStr == "" { + typeStr = "string" + } + return mk.ValueFromString(s, typeStr) +} + +func MakeValuesFromStrings(typeStr string, items []string) ([]any, error) { + if typeStr == "" { + typeStr = "string" + } + vs := make([]any, len(items)) + for i, s := range items { + v, err := mk.ValueFromString(s, typeStr) + if err != nil { + return nil, fmt.Errorf("converting string to key: %w", err) + } + vs[i] = v + } + return vs, nil +} + func MakeKeyData(ec plug.ExecContext, ci *hazelcast.ClientInternal, keyStr string) (hazelcast.Data, error) { kt := ec.Props().GetString(FlagKeyType) if kt == "" { diff --git a/base/commands/map/map_entry_set.go b/base/commands/map/map_entry_set.go index 21610a6c9..66df367db 100644 --- a/base/commands/map/map_entry_set.go +++ b/base/commands/map/map_entry_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapEntrySetCommand("Map", codec.EncodeMapEntrySetRequest, codec.DecodeMapEntrySetResponse) + c := commands.NewMapEntrySetCommand("Map", codec.EncodeMapEntrySetRequest, codec.DecodeMapEntrySetResponse, getMap) check.Must(plug.Registry.RegisterCommand("map:entry-set", c)) } diff --git a/base/commands/map/map_get.go b/base/commands/map/map_get.go index 2eaba89d0..61a257ead 100644 --- a/base/commands/map/map_get.go +++ b/base/commands/map/map_get.go @@ -3,6 +3,8 @@ package _map import ( + "github.com/hazelcast/hazelcast-go-client" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -10,6 +12,6 @@ import ( ) func init() { - c := commands.NewMapGetCommand("Map", codec.EncodeMapGetRequest, makeDecodeResponseRowsFunc(codec.DecodeMapGetResponse)) + c := commands.NewMapGetCommand[*hazelcast.Map]("Map", codec.EncodeMapGetRequest, makeDecodeResponseRowsFunc(codec.DecodeMapGetResponse), getMap) check.Must(plug.Registry.RegisterCommand("map:get", c)) } diff --git a/base/commands/map/map_key_set.go b/base/commands/map/map_key_set.go index bcdb67fbe..d08fc8052 100644 --- a/base/commands/map/map_key_set.go +++ b/base/commands/map/map_key_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapKeySetCommand("Map", codec.EncodeMapKeySetRequest, codec.DecodeMapKeySetResponse) + c := commands.NewMapKeySetCommand("Map", codec.EncodeMapKeySetRequest, codec.DecodeMapKeySetResponse, getMap) check.Must(plug.Registry.RegisterCommand("map:key-set", c)) } diff --git a/base/commands/map/map_load_all.go b/base/commands/map/map_load_all.go index 41c9ca06b..a3a059119 100644 --- a/base/commands/map/map_load_all.go +++ b/base/commands/map/map_load_all.go @@ -35,6 +35,8 @@ If no key is given, all keys are loaded.` func (MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + replace := ec.Props().GetBool(mapFlagReplace) + // TODO: use Map.LoadAllX methods in the Go client when they are fixed. --YT ci, err := cmd.ClientInternal(ctx, ec, sp) if err != nil { return nil, err @@ -47,7 +49,6 @@ func (MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } keys = append(keys, keyData) } - replace := ec.Props().GetBool(mapFlagReplace) var req *hazelcast.ClientMessage if len(keys) == 0 { req = codec.EncodeMapLoadAllRequest(name, replace) diff --git a/base/commands/map/map_size.go b/base/commands/map/map_size.go index 26c822ec4..d933f3f38 100644 --- a/base/commands/map/map_size.go +++ b/base/commands/map/map_size.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewSizeCommand("Map", getMap) - check.Must(plug.Registry.RegisterCommand("map:size", cmd)) + c := commands.NewSizeCommand("Map", getMap) + check.Must(plug.Registry.RegisterCommand("map:size", c)) } diff --git a/base/commands/map/map_values.go b/base/commands/map/map_values.go index b0bd093c3..59639160d 100644 --- a/base/commands/map/map_values.go +++ b/base/commands/map/map_values.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapValuesCommand("Map", codec.EncodeMapValuesRequest, codec.DecodeMapValuesResponse) + c := commands.NewMapValuesCommand("Map", codec.EncodeMapValuesRequest, codec.DecodeMapValuesResponse, getMap) check.Must(plug.Registry.RegisterCommand("map:values", c)) } diff --git a/base/commands/map_common.go b/base/commands/map_common.go index e8997e8ca..9f4dde55d 100644 --- a/base/commands/map_common.go +++ b/base/commands/map_common.go @@ -15,31 +15,35 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) +type mapPrefixFunc[T any] func(ctx context.Context, ec plug.ExecContext, sp clc.Spinner) (T, error) + type nameRequestEncodeFunc func(name string) *hazelcast.ClientMessage type pairsResponseDecodeFunc func(message *hazelcast.ClientMessage) []hazelcast.Pair -type MapEntrySetCommand struct { +type MapEntrySetCommand[T any] struct { typeName string encoder nameRequestEncodeFunc decoder pairsResponseDecodeFunc + prefixer mapPrefixFunc[T] } -func NewMapEntrySetCommand(typeName string, encoder nameRequestEncodeFunc, decoder pairsResponseDecodeFunc) *MapEntrySetCommand { - return &MapEntrySetCommand{ +func NewMapEntrySetCommand[T any](typeName string, encoder nameRequestEncodeFunc, decoder pairsResponseDecodeFunc, prefixer mapPrefixFunc[T]) *MapEntrySetCommand[T] { + return &MapEntrySetCommand[T]{ typeName: typeName, encoder: encoder, decoder: decoder, + prefixer: prefixer, } } -func (cm MapEntrySetCommand) Init(cc plug.InitContext) error { +func (cm MapEntrySetCommand[T]) Init(cc plug.InitContext) error { cc.SetCommandUsage("entry-set") help := fmt.Sprintf("Get all entries of a %s", cm.typeName) cc.SetCommandHelp(help, help) return nil } -func (cm MapEntrySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm MapEntrySetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) showType := ec.Props().GetBool(base.FlagShowType) rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { @@ -47,6 +51,9 @@ func (cm MapEntrySetCommand) Exec(ctx context.Context, ec plug.ExecContext) erro if err != nil { return nil, err } + if _, err = cm.prefixer(ctx, ec, sp); err != nil { + return nil, err + } req := cm.encoder(name) sp.SetText(fmt.Sprintf("Getting entries of %s", name)) resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) @@ -67,21 +74,23 @@ func (cm MapEntrySetCommand) Exec(ctx context.Context, ec plug.ExecContext) erro type getRequestEncodeFunc func(name string, keyData hazelcast.Data, threadID int64) *hazelcast.ClientMessage type getResponseDecodeFunc func(ctx context.Context, ec plug.ExecContext, res *hazelcast.ClientMessage) ([]output.Row, error) -type MapGetCommand struct { +type MapGetCommand[T any] struct { typeName string encoder getRequestEncodeFunc decoder getResponseDecodeFunc + prefixer mapPrefixFunc[T] } -func NewMapGetCommand(typeName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc) *MapGetCommand { - return &MapGetCommand{ +func NewMapGetCommand[T any](typeName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc, prefixer mapPrefixFunc[T]) *MapGetCommand[T] { + return &MapGetCommand[T]{ typeName: typeName, encoder: encoder, decoder: decoder, + prefixer: prefixer, } } -func (cm MapGetCommand) Init(cc plug.InitContext) error { +func (cm MapGetCommand[T]) Init(cc plug.InitContext) error { cc.SetCommandUsage("get") AddKeyTypeFlag(cc) help := fmt.Sprintf("Get a value from the given %s", cm.typeName) @@ -90,7 +99,7 @@ func (cm MapGetCommand) Init(cc plug.InitContext) error { return nil } -func (cm MapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm MapGetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) keyStr := ec.GetStringArg(ArgKey) rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { @@ -98,6 +107,9 @@ func (cm MapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + if _, err = cm.prefixer(ctx, ec, sp); err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Getting from %s '%s'", cm.typeName, name)) keyData, err := MakeKeyData(ec, ci, keyStr) if err != nil { @@ -124,28 +136,30 @@ func (cm MapGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { type dataSliceDecoderFunc func(message *hazelcast.ClientMessage) []*hazelcast.Data -type MapKeySetCommand struct { +type MapKeySetCommand[T any] struct { typeName string encoder nameRequestEncodeFunc decoder dataSliceDecoderFunc + prefixer mapPrefixFunc[T] } -func NewMapKeySetCommand(typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc) *MapKeySetCommand { - return &MapKeySetCommand{ +func NewMapKeySetCommand[T any](typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc, prefixer mapPrefixFunc[T]) *MapKeySetCommand[T] { + return &MapKeySetCommand[T]{ typeName: typeName, encoder: encoder, decoder: decoder, + prefixer: prefixer, } } -func (cm MapKeySetCommand) Init(cc plug.InitContext) error { +func (cm MapKeySetCommand[T]) Init(cc plug.InitContext) error { cc.SetCommandUsage("key-set") help := fmt.Sprintf("Get all keys of a %s", cm.typeName) cc.SetCommandHelp(help, help) return nil } -func (cm MapKeySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm MapKeySetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) showType := ec.Props().GetBool(base.FlagShowType) rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { @@ -153,6 +167,9 @@ func (cm MapKeySetCommand) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return nil, err } + if _, err = cm.prefixer(ctx, ec, sp); err != nil { + return nil, err + } req := cm.encoder(name) sp.SetText(fmt.Sprintf("Getting keys of %s '%s'", cm.typeName, name)) resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) @@ -270,7 +287,7 @@ This command is only available in the interactive mode.`, cm.typeName) func (cm LockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) + key, err := MakeValueFromString(ec.GetStringArg(ArgKey), ec.Props().GetString(FlagKeyType)) if err != nil { return nil, err } @@ -278,16 +295,11 @@ func (cm LockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } - keyStr := ec.GetStringArg(ArgKey) - keyData, err := MakeKeyData(ec, ci, keyStr) - if err != nil { - return nil, err - } sp.SetText(fmt.Sprintf("Locking the key in %s '%s'", cm.typeName, name)) if ttl := GetTTL(ec); ttl != clc.TTLUnset { - return nil, m.LockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) + return nil, m.LockWithLease(ctx, key, time.Duration(GetTTL(ec))) } - return nil, m.Lock(ctx, keyData) + return nil, m.Lock(ctx, key) }) if err != nil { return err @@ -335,25 +347,20 @@ This command is only available in the interactive mode.`, cm.typeName) func (cm MapTryLockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { mapName := ec.Props().GetString(base.FlagName) - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } sp.SetText(fmt.Sprintf("Locking key in map %s", mapName)) m, err := cm.getFn(ctx, ec, sp) if err != nil { return nil, err } - keyStr := ec.GetStringArg(ArgKey) - keyData, err := MakeKeyData(ec, ci, keyStr) + key, err := MakeValueFromString(ec.GetStringArg(ArgKey), ec.Props().GetString(FlagKeyType)) if err != nil { return nil, err } var locked bool if ttl := GetTTL(ec); ttl != clc.TTLUnset { - locked, err = m.TryLockWithLease(ctx, keyData, time.Duration(GetTTL(ec))) + locked, err = m.TryLockWithLease(ctx, key, time.Duration(GetTTL(ec))) } else { - locked, err = m.TryLock(ctx, keyData) + locked, err = m.TryLock(ctx, key) } row := output.Row{ { @@ -411,21 +418,13 @@ This command is only available in the interactive mode.`, cm.typeName) func (cm MapUnlockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - ci, err := cmd.ClientInternal(ctx, ec, sp) - if err != nil { - return nil, err - } + key, err := MakeValueFromString(ec.GetStringArg(ArgKey), ec.Props().GetString(FlagKeyType)) sp.SetText(fmt.Sprintf("Unlocking key in %s '%s'", cm.typeName, name)) m, err := cm.getFn(ctx, ec, sp) if err != nil { return nil, err } - keyStr := ec.GetStringArg(ArgKey) - keyData, err := MakeKeyData(ec, ci, keyStr) - if err != nil { - return nil, err - } - return nil, m.Unlock(ctx, keyData) + return nil, m.Unlock(ctx, key) }) if err != nil { return err @@ -436,28 +435,30 @@ func (cm MapUnlockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) err return nil } -type MapValuesCommand struct { +type MapValuesCommand[T any] struct { typeName string encoder nameRequestEncodeFunc decoder dataSliceDecoderFunc + prefixer mapPrefixFunc[T] } -func NewMapValuesCommand(typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc) *MapValuesCommand { - return &MapValuesCommand{ +func NewMapValuesCommand[T any](typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc, prefixer mapPrefixFunc[T]) *MapValuesCommand[T] { + return &MapValuesCommand[T]{ typeName: typeName, encoder: encoder, decoder: decoder, + prefixer: prefixer, } } -func (cm MapValuesCommand) Init(cc plug.InitContext) error { +func (cm MapValuesCommand[T]) Init(cc plug.InitContext) error { cc.SetCommandUsage("values") help := fmt.Sprintf("Get all values of a %s", cm.typeName) cc.SetCommandHelp(help, help) return nil } -func (cm *MapValuesCommand) Exec(ctx context.Context, ec plug.ExecContext) error { +func (cm *MapValuesCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) showType := ec.Props().GetBool(base.FlagShowType) rowsV, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { @@ -465,6 +466,9 @@ func (cm *MapValuesCommand) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return nil, err } + if _, err := cm.prefixer(ctx, ec, sp); err != nil { + return nil, err + } sp.SetText(fmt.Sprintf("Getting values of %s", name)) req := cm.encoder(name) resp, err := ci.InvokeOnRandomTarget(ctx, req, nil) diff --git a/base/commands/multimap/multimap_entry_set.go b/base/commands/multimap/multimap_entry_set.go index b919d8858..a943bb5b2 100644 --- a/base/commands/multimap/multimap_entry_set.go +++ b/base/commands/multimap/multimap_entry_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapEntrySetCommand("MultiMap", codec.EncodeMultiMapEntrySetRequest, codec.DecodeMultiMapEntrySetResponse) + c := commands.NewMapEntrySetCommand("MultiMap", codec.EncodeMultiMapEntrySetRequest, codec.DecodeMultiMapEntrySetResponse, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:entry-set", c)) } diff --git a/base/commands/multimap/multimap_get.go b/base/commands/multimap/multimap_get.go index 46b7acb5b..0a1f950f6 100644 --- a/base/commands/multimap/multimap_get.go +++ b/base/commands/multimap/multimap_get.go @@ -10,6 +10,7 @@ import ( ) func init() { - c := commands.NewMapGetCommand("MultiMap", codec.EncodeMultiMapGetRequest, makeDecodeResponseRowsFunc(codec.DecodeMultiMapGetResponse)) + d := makeDecodeResponseRowsFunc(codec.DecodeMultiMapGetResponse) + c := commands.NewMapGetCommand("MultiMap", codec.EncodeMultiMapGetRequest, d, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:get", c)) } diff --git a/base/commands/multimap/multimap_key_set.go b/base/commands/multimap/multimap_key_set.go index 659dbb4e8..4f1eab86a 100644 --- a/base/commands/multimap/multimap_key_set.go +++ b/base/commands/multimap/multimap_key_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapKeySetCommand("MultiMap", codec.EncodeMultiMapKeySetRequest, codec.DecodeMultiMapKeySetResponse) + c := commands.NewMapKeySetCommand("MultiMap", codec.EncodeMultiMapKeySetRequest, codec.DecodeMultiMapKeySetResponse, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:key-set", c)) } diff --git a/base/commands/multimap/multimap_values.go b/base/commands/multimap/multimap_values.go index c3b370e8e..7da9b1243 100644 --- a/base/commands/multimap/multimap_values.go +++ b/base/commands/multimap/multimap_values.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapValuesCommand("MultiMap", codec.EncodeMultiMapValuesRequest, codec.DecodeMultiMapValuesResponse) + c := commands.NewMapValuesCommand("MultiMap", codec.EncodeMultiMapValuesRequest, codec.DecodeMultiMapValuesResponse, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:values", c)) } From 695372463920453390abe588de216b09377e0955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 26 Sep 2023 10:36:28 +0300 Subject: [PATCH 72/79] [CLC-361] [CLC-341] [CLC-338] Small improvements (#402) * Require confirmation from the user for demo generate * Add SQL text to prompt * Updated docs with the new install script link --- README.md | 2 +- base/commands/common.go | 9 +++++++++ base/commands/demo/demo_generate_data.go | 17 +++++++++++++++++ base/commands/demo/demo_it_test.go | 4 ++-- base/commands/shell.go | 9 ++++++--- clc/const.go | 1 + docs/modules/ROOT/pages/clc-demo.adoc | 3 +++ docs/modules/ROOT/pages/install-clc.adoc | 6 +++--- 8 files changed, 42 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f37caafcd..e7840b880 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Currently we provide precompiled binaries of CLC for the following platforms and You can run the following command to install the latest stable CLC on a computer running Linux x64 or macOS 10.15 (Catalina) x64/ARM 64 (M1/M2): ``` -curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash +curl https://hazelcast.com/clc/install.sh | bash ``` On macOS, binaries downloaded outside of AppStore require your intervention to run. diff --git a/base/commands/common.go b/base/commands/common.go index a0f8959f5..9b286bcde 100644 --- a/base/commands/common.go +++ b/base/commands/common.go @@ -3,6 +3,7 @@ package commands import ( "context" "fmt" + "os" "strings" "github.com/hazelcast/hazelcast-go-client" @@ -282,3 +283,11 @@ func GetTTL(ec plug.ExecContext) int64 { } return clc.TTLUnset } + +func IsYes(ec plug.ExecContext) bool { + yes := ec.Props().GetBool(clc.FlagAutoYes) + if yes { + return true + } + return os.Getenv(clc.EnvYes) == "1" +} diff --git a/base/commands/demo/demo_generate_data.go b/base/commands/demo/demo_generate_data.go index 50a26ed5b..d72e0d05c 100644 --- a/base/commands/demo/demo_generate_data.go +++ b/base/commands/demo/demo_generate_data.go @@ -12,6 +12,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-go-client/serialization" + "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/sql" @@ -21,6 +22,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/demo/wikimedia" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/prompt" ) const ( @@ -50,6 +52,7 @@ Generate data for given name, supported names are: cc.SetCommandHelp(long, short) cc.AddIntFlag(flagMaxValues, "", 0, false, "number of events to create (default: 0, no limits)") cc.AddBoolFlag(flagPreview, "", false, false, "print the generated data without interacting with the cluster") + cc.AddBoolFlag(clc.FlagAutoYes, "", false, false, "skip confirming the data generation") cc.AddStringArg(argGeneratorName, argTitleGeneratorName) cc.AddKeyValueSliceArg(argKeyValues, argTitleKeyValues, 0, clc.MaxArgs) return nil @@ -63,6 +66,20 @@ func (GenerateDataCommand) Exec(ctx context.Context, ec plug.ExecContext) error } kvs := ec.GetKeyValuesArg(argKeyValues) preview := ec.Props().GetBool(flagPreview) + yes := commands.IsYes(ec) + if !yes { + p := prompt.New(ec.Stdin(), ec.Stdout()) + ec.PrintlnUnnecessary("The data is streamed from Wikipedia changes.") + ec.PrintlnUnnecessary("Hazelcast has no control over the incoming data,") + yes, err := p.YesNo("Proceed?") + if err != nil { + ec.Logger().Info("User input could not be processed due to error: %s", err.Error()) + return hzerrors.ErrUserCancelled + } + if !yes { + return hzerrors.ErrUserCancelled + } + } if preview { return generatePreviewResult(ctx, ec, generator, kvs.Map()) } diff --git a/base/commands/demo/demo_it_test.go b/base/commands/demo/demo_it_test.go index b0e7fd3ae..8cd84bbf9 100644 --- a/base/commands/demo/demo_it_test.go +++ b/base/commands/demo/demo_it_test.go @@ -34,7 +34,7 @@ func generateData_WikipediaTest(t *testing.T) { t := tcx.T ctx := context.Background() tcx.WithReset(func() { - err := tcx.CLCExecuteErr(ctx, "demo", "generate-data", "wikipedia-event-stream", "map="+m.Name(), "--timeout", "2s") + err := tcx.CLCExecuteErr(ctx, "demo", "generate-data", "wikipedia-event-stream", "map="+m.Name(), "--timeout", "2s", "--yes") require.Error(t, err) size := check.MustValue(m.Size(context.Background())) require.Greater(t, size, 0) @@ -48,7 +48,7 @@ func generateData_Wikipedia_MaxValues_Test(t *testing.T) { ctx := context.Background() count := 10 tcx.WithReset(func() { - tcx.CLCExecute(ctx, "demo", "generate-data", "wikipedia-event-stream", "map="+m.Name(), fmt.Sprintf("--max-values=%d", count)) + tcx.CLCExecute(ctx, "demo", "generate-data", "wikipedia-event-stream", "map="+m.Name(), fmt.Sprintf("--max-values=%d", count), "--yes") size := check.MustValue(m.Size(context.Background())) require.Equal(t, count, size) }) diff --git a/base/commands/shell.go b/base/commands/shell.go index f9c62a8b2..e93b15f5c 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -21,7 +21,8 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" ) -const banner = `Hazelcast CLC %s (c) 2023 Hazelcast Inc. +const ( + banner = `Hazelcast CLC %s (c) 2023 Hazelcast Inc. * Participate in our survey at: https://forms.gle/rPFywdQjvib1QCe49 * Type 'help' for help information. Prefix non-SQL commands with \ @@ -29,6 +30,8 @@ const banner = `Hazelcast CLC %s (c) 2023 Hazelcast Inc. %s%s ` + defaultPrompt = "SQL> " +) type ShellCommand struct { shortcuts map[string]struct{} @@ -100,7 +103,7 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext promptFn := func() string { cfgPath := ec.ConfigPath() if cfgPath == "" { - return "> " + return defaultPrompt } // Best effort for absolute path p, err := filepath.Abs(cfgPath) @@ -108,7 +111,7 @@ func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext cfgPath = p } pd := paths.ParentDir(cfgPath) - return fmt.Sprintf("%s> ", str.MaybeShorten(pd, 12)) + return fmt.Sprintf("%s %s ", str.MaybeShorten(pd, 12), defaultPrompt) } sh, err := shell.New(promptFn, " ... ", path, ec.Stdout(), ec.Stderr(), ec.Stdin(), endLineFn, textFn) if err != nil { diff --git a/clc/const.go b/clc/const.go index fbb3a2f37..7eee85f8b 100644 --- a/clc/const.go +++ b/clc/const.go @@ -33,6 +33,7 @@ const ( EnvMaxCols = "CLC_MAX_COLS" EnvConfig = "CLC_CONFIG" EnvSkipServerVersionCheck = "CLC_SKIP_SERVER_VERSION_CHECK" + EnvYes = "CLC_YES" FlagAutoYes = "yes" MaxArgs = 65535 TTLUnset = -1 diff --git a/docs/modules/ROOT/pages/clc-demo.adoc b/docs/modules/ROOT/pages/clc-demo.adoc index cdacc2ac8..9e4257b20 100644 --- a/docs/modules/ROOT/pages/clc-demo.adoc +++ b/docs/modules/ROOT/pages/clc-demo.adoc @@ -23,6 +23,9 @@ Generate data for given name, supported names are: - wikipedia-event-stream: Real-time Wikipedia event stream. Following key-value pairs can be set - map=: generated stream items are written into the map +Note that the data is streamed from Wikipedia changes. +Hazelcast has no control over the incoming data, + Usage: [source,bash] diff --git a/docs/modules/ROOT/pages/install-clc.adoc b/docs/modules/ROOT/pages/install-clc.adoc index c20ea50a7..5c8b5e3ff 100644 --- a/docs/modules/ROOT/pages/install-clc.adoc +++ b/docs/modules/ROOT/pages/install-clc.adoc @@ -85,7 +85,7 @@ Install Script (Intel):: + [source,shell,subs="attributes"] ---- -curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash +curl https://hazelcast.com/clc/install.sh | bash ---- Install Script (Apple Silicon):: @@ -94,7 +94,7 @@ Install Script (Apple Silicon):: + [source,shell,subs="attributes"] ---- -curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash +curl https://hazelcast.com/clc/install.sh | bash ---- Build from Source:: @@ -153,7 +153,7 @@ Install Script (AMD64):: + [source,shell,subs="attributes"] ---- -curl -sL https://raw.githubusercontent.com/hazelcast/hazelcast-commandline-client/main/extras/unix/install.sh | bash +curl https://hazelcast.com/clc/install.sh | bash ---- Build from Source:: From a1477c0e6a2679b483c7f214bc982dbbe19a0e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 26 Sep 2023 11:02:13 +0300 Subject: [PATCH 73/79] [CLC-370] Refactored the Config List (#403) Refactored the config list --- base/commands/sql/sql.go | 6 +- clc/cmd/exec_context.go | 54 ++++++++---- clc/common.go | 4 + clc/config/wizard/dummy.go | 3 - clc/config/wizard/input.go | 156 ---------------------------------- clc/config/wizard/list.go | 109 ------------------------ clc/config/wizard_provider.go | 43 +++++++--- clc/ux/selector/selector.go | 21 +++++ clc/ux/stage/stage.go | 4 +- clc/ux/stage/stage_test.go | 4 +- errors/error.go | 2 +- go.mod | 33 +++---- go.sum | 115 +++++++++++++------------ 13 files changed, 173 insertions(+), 381 deletions(-) delete mode 100644 clc/config/wizard/dummy.go delete mode 100644 clc/config/wizard/input.go delete mode 100644 clc/config/wizard/list.go create mode 100644 clc/ux/selector/selector.go diff --git a/base/commands/sql/sql.go b/base/commands/sql/sql.go index f6b85d8c7..f8eed94bf 100644 --- a/base/commands/sql/sql.go +++ b/base/commands/sql/sql.go @@ -23,15 +23,11 @@ const ( argTitleQuery = "query" ) -type arg0er interface { - Arg0() string -} - type SQLCommand struct{} func (SQLCommand) Augment(ec plug.ExecContext, props *plug.Properties) error { // set the default format to table in the interactive mode - if ecc, ok := ec.(arg0er); ok { + if ecc, ok := ec.(clc.Arg0er); ok { if ec.CommandName() == ecc.Arg0()+" shell" && len(ec.Args()) == 0 { props.Set(clc.PropertyFormat, base.PrinterTable) } diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index 4038ab555..b25af0b7d 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "strconv" + "sync/atomic" "time" "github.com/hazelcast/hazelcast-go-client" @@ -34,19 +35,20 @@ const ( type ClientFn func(ctx context.Context, cfg hazelcast.Config) (*hazelcast.ClientInternal, error) type ExecContext struct { - lg log.Logger - stdout io.Writer - stderr io.Writer - stdin io.Reader - args []string - kwargs map[string]any - props *plug.Properties - mode Mode - cmd *cobra.Command - main *Main - spinnerWait time.Duration - printer plug.Printer - cp config.Provider + lg log.Logger + stdout io.Writer + stderr io.Writer + stdin io.Reader + args []string + kwargs map[string]any + props *plug.Properties + mode Mode + cmd *cobra.Command + main *Main + spinnerWait time.Duration + printer plug.Printer + cp config.Provider + spinnerPaused atomic.Bool } func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, mode Mode) (*ExecContext, error) { @@ -141,10 +143,14 @@ func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInt if ci != nil { return ci, nil } + ec.pauseSpinner() cfg, err := ec.cp.ClientConfig(ctx, ec) if err != nil { + // unpausing here, since can't use defer + ec.unpauseSpinner() return nil, err } + ec.unpauseSpinner() if err := ec.main.ensureClient(ctx, cfg); err != nil { return nil, err } @@ -221,8 +227,9 @@ func (ec *ExecContext) ExecuteBlocking(ctx context.Context, f func(context.Conte } ch <- v }() - timer := time.NewTimer(ec.spinnerWait) - defer timer.Stop() + var started bool + ticker := time.NewTicker(ec.spinnerWait) + defer ticker.Stop() for { select { case <-ctx.Done(): @@ -240,10 +247,15 @@ func (ec *ExecContext) ExecuteBlocking(ctx context.Context, f func(context.Conte return nil, func() {}, err } return v, stop, nil - case <-timer.C: + case <-ticker.C: if !ec.Quiet() { if s, ok := sp.(clc.SpinnerStarter); ok { - s.Start() + if !ec.spinnerPaused.Load() { + if !started { + started = true + s.Start() + } + } } } } @@ -256,6 +268,14 @@ func (ec *ExecContext) PrintlnUnnecessary(text string) { } } +func (ec *ExecContext) pauseSpinner() { + ec.spinnerPaused.Store(true) +} + +func (ec *ExecContext) unpauseSpinner() { + ec.spinnerPaused.Store(false) +} + func (ec *ExecContext) Quiet() bool { return ec.Props().GetBool(clc.PropertyQuiet) || terminal.IsPipe(ec.Stdin()) || terminal.IsPipe(ec.Stdout()) } diff --git a/clc/common.go b/clc/common.go index 717dfdef8..9aa5f2a9e 100644 --- a/clc/common.go +++ b/clc/common.go @@ -26,3 +26,7 @@ type Spinner interface { type SpinnerStarter interface { Start() } + +type Arg0er interface { + Arg0() string +} diff --git a/clc/config/wizard/dummy.go b/clc/config/wizard/dummy.go deleted file mode 100644 index 13cdeff48..000000000 --- a/clc/config/wizard/dummy.go +++ /dev/null @@ -1,3 +0,0 @@ -package wizard - -// This file exists only for compilation diff --git a/clc/config/wizard/input.go b/clc/config/wizard/input.go deleted file mode 100644 index 38136a527..000000000 --- a/clc/config/wizard/input.go +++ /dev/null @@ -1,156 +0,0 @@ -package wizard - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -var ( - focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00E1E1")) - blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - noStyle = lipgloss.NewStyle() - - focusedButton = focusedStyle.Copy().Render("[ Submit ]") - blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit")) -) - -type textModel struct { - focusIndex int - quitting bool - choice string - inputs []textinput.Model -} - -func InitialModel() textModel { - m := textModel{ - inputs: make([]textinput.Model, 2), - quitting: false, - choice: "", - } - var t textinput.Model - for i := range m.inputs { - t = textinput.New() - switch i { - case 0: - t.Prompt = "Configuration Name: " - t.Placeholder = "default" - t.PromptStyle = focusedStyle - t.TextStyle = focusedStyle - t.Focus() - case 1: - t.Prompt = "Source: " - t.Placeholder = "" - } - m.inputs[i] = t - } - return m -} - -func (m textModel) Init() tea.Cmd { - return nil -} - -func (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC, tea.KeyEsc: - m.quitting = true - return m, tea.Quit - case tea.KeyTab, tea.KeyShiftTab, tea.KeyEnter, tea.KeyUp, tea.KeyDown: - s := msg.String() - if s == "enter" && m.focusIndex == len(m.inputs) { - m.quitting = true - if m.inputs[0].Value() == "" { - m.inputs[0].SetValue("default") - } - m.choice = "enter" - return m, tea.Quit - } - if s == "up" || s == "shift+tab" { - m.focusIndex-- - } else { - m.focusIndex++ - } - if m.focusIndex > len(m.inputs) { - m.focusIndex = 0 - } else if m.focusIndex < 0 { - m.focusIndex = len(m.inputs) - } - cmds := make([]tea.Cmd, len(m.inputs)) - for i := 0; i <= len(m.inputs)-1; i++ { - if i == m.focusIndex { - cmds[i] = m.inputs[i].Focus() - m.inputs[i].PromptStyle = focusedStyle - m.inputs[i].TextStyle = focusedStyle - continue - } - m.inputs[i].Blur() - m.inputs[i].PromptStyle = noStyle - m.inputs[i].TextStyle = noStyle - } - return m, tea.Batch(cmds...) - } - } - cmd := m.updateInputs(msg) - return m, cmd -} - -func (m *textModel) updateInputs(msg tea.Msg) tea.Cmd { - cmds := make([]tea.Cmd, len(m.inputs)) - for i := range m.inputs { - m.inputs[i], cmds[i] = m.inputs[i].Update(msg) - } - return tea.Batch(cmds...) -} - -func (m textModel) GetInputs() []string { - return []string{m.inputs[0].Value(), m.inputs[1].Value()} -} - -func (m textModel) View() string { - if m.choice != "" { - return m.choice - } - if m.quitting { - return "" - } - var b strings.Builder - b.WriteString(`There is no configuration detected. - -This screen helps you create a new connection configuration. -Note that this screen supports only Viridian clusters. -For other clusters use the following command: - - clc config add --help - -1. Enter the desired name in the "Configuration Name" field. -2. On Viridian console, visit: - - Dashboard -> Connect Client -> CLI - -3. Copy the URL in second box and pass it to "Source" field. -4. Navigate to the [Submit] button and press enter. - -Alternatively, you can use the following command: - - clc config import --help - -`) - for i := range m.inputs { - b.WriteString(m.inputs[i].View()) - if i < len(m.inputs)-1 { - b.WriteRune('\n') - } - } - button := &blurredButton - if m.focusIndex == len(m.inputs) { - button = &focusedButton - } - fmt.Fprintf(&b, "\n\n%s\n", *button) - return b.String() -} diff --git a/clc/config/wizard/list.go b/clc/config/wizard/list.go deleted file mode 100644 index 054f69480..000000000 --- a/clc/config/wizard/list.go +++ /dev/null @@ -1,109 +0,0 @@ -package wizard - -import ( - "fmt" - "io" - - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - - "github.com/hazelcast/hazelcast-commandline-client/internal/check" -) - -const listHeight = 14 - -var ( - itemStyle = lipgloss.NewStyle() - selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00E1E1")) -) - -type item string - -func (i item) FilterValue() string { - return "" -} - -type itemDelegate struct{} - -func (d itemDelegate) Height() int { - return 1 -} - -func (d itemDelegate) Spacing() int { - return 0 -} - -func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { - return nil -} - -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - v, ok := listItem.(item) - if !ok { - return - } - var text string - if index == m.Index() { - text = selectedItemStyle.Render(string(v)) - } else { - text = itemStyle.Render(string(v)) - } - check.I2(fmt.Fprint(w, " "+text)) -} - -type model struct { - list list.Model - choice string - quit bool -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.list.SetWidth(msg.Width) - return m, nil - case tea.KeyMsg: - switch keypress := msg.String(); keypress { - case "enter": - i, ok := m.list.SelectedItem().(item) - if ok { - m.choice = string(i) - } - return m, tea.Quit - case "ctrl+c", "esc": - m.quit = true - return m, tea.Quit - } - } - var cmd tea.Cmd - m.list, cmd = m.list.Update(msg) - return m, cmd -} - -func (m model) View() string { - if m.choice != "" { - return m.choice - } - if m.quit { - return "" - } - return m.list.View() -} - -func InitializeList(dirs []string) model { - var items []list.Item - for _, k := range dirs { - items = append(items, item(k)) - } - l := list.New(items, itemDelegate{}, 20, listHeight) - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.SetShowTitle(false) - l.SetShowHelp(false) - return model{list: l} -} diff --git a/clc/config/wizard_provider.go b/clc/config/wizard_provider.go index 06f82c6b7..722866ffe 100644 --- a/clc/config/wizard_provider.go +++ b/clc/config/wizard_provider.go @@ -3,25 +3,32 @@ package config import ( "context" "errors" + "fmt" "os" "sync/atomic" - tea "github.com/charmbracelet/bubbletea" "github.com/hazelcast/hazelcast-go-client" "github.com/spf13/pflag" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/selector" "github.com/hazelcast/hazelcast-commandline-client/internal/terminal" - "github.com/hazelcast/hazelcast-commandline-client/clc/config/wizard" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" clcerrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) -var ( - errNoConfig = errors.New("no configuration was provided and cannot display the configuration wizard; use the --config flag") -) +const fmtConfigurationHelp = `No configurations found. + +Run the following command to learn more about adding a configuration: + + %[1]s config add --help + +Or run the following command to import a Viridian cluster configuration: + + %[1]s config import --help +` type WizardProvider struct { fp *atomic.Pointer[FileProvider] @@ -74,10 +81,12 @@ func (p *WizardProvider) ClientConfig(ctx context.Context, ec plug.ExecContext) cs, err := FindAll(paths.Configs()) if err != nil { if errors.Is(err, os.ErrNotExist) { + printNoConfigHelp(ec) return hazelcast.Config{}, clcerrors.ErrNoClusterConfig } } if len(cs) == 0 { + printNoConfigHelp(ec) return hazelcast.Config{}, clcerrors.ErrNoClusterConfig } if len(cs) == 1 { @@ -85,10 +94,10 @@ func (p *WizardProvider) ClientConfig(ctx context.Context, ec plug.ExecContext) } if configName == "" { if terminal.IsPipe(maybeUnwrapStdout(ec)) { - return hazelcast.Config{}, errNoConfig + 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 - configName, err = p.runWizard(cs) + configName, err = p.runWizard(ctx, cs) if err != nil { return hazelcast.Config{}, err } @@ -105,14 +114,22 @@ func (p *WizardProvider) ClientConfig(ctx context.Context, ec plug.ExecContext) return config, nil } -func (p *WizardProvider) runWizard(configNames []string) (string, error) { - m := wizard.InitializeList(configNames) - model, err := tea.NewProgram(m).Run() +func (p *WizardProvider) runWizard(ctx context.Context, cs []string) (string, error) { + cfg, canceled, err := selector.Show(ctx, "Select a configuration", cs...) if err != nil { return "", err } - if model.View() == "" { - return "", clcerrors.ErrNoClusterConfig + if canceled { + return "", clcerrors.ErrUserCancelled + } + return cfg, nil +} + +func printNoConfigHelp(ec plug.ExecContext) { + var arg0 = "clc" + if c, ok := ec.(clc.Arg0er); ok { + arg0 = c.Arg0() } - return model.View(), nil + text := fmt.Sprintf(fmtConfigurationHelp, arg0) + ec.PrintlnUnnecessary(text) } diff --git a/clc/ux/selector/selector.go b/clc/ux/selector/selector.go new file mode 100644 index 000000000..7425db822 --- /dev/null +++ b/clc/ux/selector/selector.go @@ -0,0 +1,21 @@ +package selector + +import ( + "context" + + "github.com/pterm/pterm" +) + +func Show(ctx context.Context, text string, options ...string) (result string, canceled bool, err error) { + widget := pterm.DefaultInteractiveSelect.WithOptions(options) + widget.TextStyle = &pterm.ThemeDefault.DefaultText + widget.SelectorStyle = pterm.NewStyle(pterm.Bold) + widget = widget.WithOnInterruptFunc(func() { + canceled = true + }) + option, err := widget.Show(text) + if err != nil { + return "", false, err + } + return option, canceled, nil +} diff --git a/clc/ux/stage/stage.go b/clc/ux/stage/stage.go index 44c2291f0..2e68e177e 100644 --- a/clc/ux/stage/stage.go +++ b/clc/ux/stage/stage.go @@ -8,7 +8,6 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" - hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/str" @@ -167,8 +166,7 @@ func Execute[T any](ctx context.Context, ec plug.ExecContext, value T, sp Provid // the error can be ignored ec.PrintlnUnnecessary(fmt.Sprintf("ERROR %s %s: %s", ss.IndexText(), stg.FailureMsg, ie.Unwrap().Error())) } else { - ec.PrintlnUnnecessary(fmt.Sprintf("ERROR %s: %s", stg.FailureMsg, err.Error())) - return value, hzerrors.WrappedError{Err: err} + return value, fmt.Errorf("%s: %w", stg.FailureMsg, err) } } stop() diff --git a/clc/ux/stage/stage_test.go b/clc/ux/stage/stage_test.go index daacbda47..efd4d2e43 100644 --- a/clc/ux/stage/stage_test.go +++ b/clc/ux/stage/stage_test.go @@ -107,6 +107,6 @@ func execute_WithFailureTest(t *testing.T) { assert.Error(t, err) texts := []string{" [1/2] Progressing 1"} assert.Equal(t, texts, ec.Spinner.Texts) - text := "ERROR Failure 1: some error\n" - assert.Equal(t, text, ec.StdoutText()) + text := "Failure 1: some error" + assert.Equal(t, text, err.Error()) } diff --git a/errors/error.go b/errors/error.go index 07b8ed288..1cb23be99 100644 --- a/errors/error.go +++ b/errors/error.go @@ -21,7 +21,7 @@ import ( ) var ( - ErrUserCancelled = errors.New("cancelled") + ErrUserCancelled = errors.New("canceled") ErrNotDecoded = errors.New("not decoded") ErrNotAvailable = errors.New("not available") ErrNoClusterConfig = errors.New("no configuration was specified") diff --git a/go.mod b/go.mod index e1e2ca309..ddf477395 100644 --- a/go.mod +++ b/go.mod @@ -8,17 +8,20 @@ require ( 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.2-0.20230908105658-19ade8678cb0 - github.com/mattn/go-runewidth v0.0.14 + github.com/mattn/go-runewidth v0.0.15 github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.4 github.com/theckman/yacspin v0.13.12 go.uber.org/zap v1.23.0 golang.org/x/exp v0.0.0-20221108223516-5d533826c662 ) require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect @@ -27,7 +30,6 @@ require ( github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect @@ -47,51 +49,44 @@ require ( github.com/golang/protobuf v1.3.1 // indirect github.com/golang/snappy v0.0.3 // indirect github.com/google/flatbuffers v1.12.1 // indirect + github.com/gookit/color v1.5.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.12.3 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.1 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/sahilm/fuzzy v0.1.0 // indirect - github.com/sergi/go-diff v1.1.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/shirou/gopsutil/v3 v3.21.5 // indirect github.com/skeema/knownhosts v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.4 // indirect github.com/tklauser/numcpus v0.2.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opencensus.io v0.22.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.11.0 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.12.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/term v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect golang.org/x/tools v0.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) require ( github.com/apache/thrift v0.14.1 - github.com/charmbracelet/bubbles v0.15.0 - github.com/charmbracelet/bubbletea v0.23.2 - github.com/charmbracelet/lipgloss v0.7.1 github.com/dgraph-io/badger/v4 v4.1.0 github.com/fatih/color v1.13.0 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 + github.com/pterm/pterm v0.12.69 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 9aeca604b..3ee724059 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,24 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -25,27 +42,15 @@ github.com/apache/thrift v0.14.1 h1:Yh8v0hpCj63p5edXOLaqTJW0IJ1p+eMW6+YSOqw1d6s= github.com/apache/thrift v0.14.1/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= -github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI= -github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74= -github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= -github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps= -github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= @@ -110,6 +115,10 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/hazelcast/hazelcast-go-client v1.4.2-0.20230908105658-19ade8678cb0 h1:NIl9B/ckHJ07RpwhvefQKPPe8ASC3cT5KyPcLvad4gg= github.com/hazelcast/hazelcast-go-client v1.4.2-0.20230908105658-19ade8678cb0/go.mod h1:PJ38lqXJ18S0YpkrRznPDlUH8GnnMAQCx3jpQtBPZ6Q= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -122,6 +131,11 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -130,10 +144,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -141,28 +153,11 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= -github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d h1:PQW4Aqovdqc9efHl9EVA+bhKmuZ4ME1HvSYYDvaDiK0= github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d/go.mod h1:cxIIfNMTwff8f/ZvRouvWYF6wOoO7nj99neWSx2q/Es= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -174,16 +169,23 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.69 h1:fBCKnB8dSLAl8FlYRQAWYGp2WTI/Xm/tKJ21Hyo9USw= +github.com/pterm/pterm v0.12.69/go.mod h1:wl06ko9MHnqxz4oDV++IORDpjCzw6+mfrvf0MPj6fdk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= -github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil/v3 v3.21.5 h1:YUBf0w/KPLk7w1803AYBnH7BmA+1Z/Q5MEZxpREUaB4= github.com/shirou/gopsutil/v3 v3.21.5/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -194,15 +196,13 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= github.com/tklauser/go-sysconf v0.3.4 h1:HT8SVixZd3IzLdfs/xlpq0jeSfTX57g1v6wB1EuzV7M= @@ -211,6 +211,9 @@ github.com/tklauser/numcpus v0.2.1 h1:ct88eFm+Q7m2ZfXJdan1xYoXKlmwsfP+k88q05KvlZ github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -281,11 +284,13 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -295,15 +300,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -311,8 +318,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -342,6 +350,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From ff92790826bb36864f98cc50981b6c909728e85e Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:30:23 +0300 Subject: [PATCH 74/79] Implement phone home [CLC-205] (#298) Add metric collection system --- .../atomic_long/atomic_long_destroy.go | 2 +- base/commands/atomic_long/atomic_long_get.go | 1 + base/commands/atomic_long/atomic_long_set.go | 1 + base/commands/atomic_long/common.go | 1 + base/commands/common.go | 16 +- base/commands/demo/demo_generate_data.go | 10 +- base/commands/demo/demo_map_set_many.go | 5 +- base/commands/job/common.go | 2 + base/commands/job/job.go | 4 +- base/commands/job/job_export_snapshot.go | 5 +- base/commands/job/job_list.go | 5 +- base/commands/job/job_resume.go | 6 +- base/commands/job/job_submit.go | 5 +- base/commands/job/job_terminate.go | 8 +- base/commands/list/common.go | 1 + base/commands/list/list_add.go | 7 +- base/commands/list/list_clear.go | 2 +- base/commands/list/list_contains.go | 5 +- base/commands/list/list_destroy.go | 2 +- base/commands/list/list_remove_index.go | 4 +- base/commands/list/list_remove_value.go | 4 +- base/commands/list/list_set.go | 5 +- base/commands/list/list_size.go | 2 +- base/commands/map/map.go | 4 +- base/commands/map/map_clear.go | 2 +- base/commands/map/map_destroy.go | 2 +- base/commands/map/map_entry_set.go | 2 +- base/commands/map/map_get.go | 2 +- base/commands/map/map_key_set.go | 2 +- base/commands/map/map_load_all.go | 5 +- base/commands/map/map_lock.go | 2 +- base/commands/map/map_remove.go | 2 +- base/commands/map/map_set.go | 5 +- base/commands/map/map_size.go | 2 +- base/commands/map/map_try_lock.go | 2 +- base/commands/map/map_unlock.go | 2 +- base/commands/map/map_values.go | 2 +- base/commands/map_common.go | 140 ++++---- base/commands/multimap/multimap_clear.go | 2 +- base/commands/multimap/multimap_destroy.go | 2 +- base/commands/multimap/multimap_entry_set.go | 2 +- base/commands/multimap/multimap_get.go | 2 +- base/commands/multimap/multimap_key_set.go | 2 +- base/commands/multimap/multimap_lock.go | 2 +- base/commands/multimap/multimap_put.go | 2 + base/commands/multimap/multimap_remove.go | 2 +- base/commands/multimap/multimap_size.go | 2 +- base/commands/multimap/multimap_try_lock.go | 2 +- base/commands/multimap/multimap_unlock.go | 2 +- base/commands/multimap/multimap_values.go | 2 +- base/commands/object/object.go | 4 +- base/commands/object/object_list.go | 4 +- base/commands/project/project.go | 4 +- base/commands/project/project_create.go | 6 +- .../project/project_list_templates.go | 8 +- base/commands/queue/queue_clear.go | 2 +- base/commands/queue/queue_destroy.go | 2 +- base/commands/queue/queue_offer.go | 5 +- base/commands/queue/queue_poll.go | 5 +- base/commands/queue/queue_size.go | 2 +- base/commands/script.go | 3 +- base/commands/set/set_add.go | 1 + base/commands/set/set_clear.go | 2 +- base/commands/set/set_destroy.go | 2 +- base/commands/set/set_get_all.go | 5 +- base/commands/set/set_remove.go | 5 +- base/commands/set/set_size.go | 2 +- base/commands/shell.go | 3 +- base/commands/sql/sql.go | 5 +- base/commands/topic/topic.go | 4 +- base/commands/topic/topic_destroy.go | 2 +- base/commands/topic/topic_publish.go | 5 +- base/commands/topic/topic_subscribe.go | 16 +- base/commands/viridian/custom_class_delete.go | 2 + .../viridian/custom_class_download.go | 2 + base/commands/viridian/custom_class_list.go | 2 + base/commands/viridian/custom_class_upload.go | 2 + base/commands/viridian/download_logs.go | 3 + .../viridian/viridian_cluster_create.go | 3 + .../viridian/viridian_cluster_delete.go | 2 + .../commands/viridian/viridian_cluster_get.go | 2 + .../viridian/viridian_cluster_list.go | 2 + .../viridian/viridian_cluster_resume.go | 2 + .../viridian/viridian_cluster_stop.go | 2 + .../viridian/viridian_import_config.go | 2 + base/commands/viridian/viridian_log_stream.go | 2 + base/commands/viridian/viridian_login.go | 2 + clc/cmd/clc.go | 38 +-- clc/cmd/command_context.go | 7 +- clc/cmd/exec_context.go | 19 +- clc/cmd/utils.go | 55 ++++ clc/metrics/environment.go | 81 +++++ clc/metrics/interfaces.go | 18 ++ clc/metrics/key.go | 94 ++++++ clc/metrics/metric_store.go | 299 ++++++++++++++++++ clc/metrics/metric_store_test.go | 225 +++++++++++++ clc/metrics/nop_metric_store.go | 6 + clc/metrics/query.go | 31 ++ clc/metrics/query_generator.go | 84 +++++ clc/paths/paths.go | 5 + clc/sql/sql.go | 2 +- clc/store/store.go | 44 ++- clc/store/store_test.go | 23 +- cmd/clc/main.go | 48 ++- .../ROOT/pages/environment-variables.adoc | 4 + go.mod | 9 +- go.sum | 20 +- internal/http/http.go | 14 + internal/it/context.go | 11 +- internal/it/test_context.go | 3 +- internal/plug/context.go | 12 +- internal/types/types.go | 9 +- 112 files changed, 1359 insertions(+), 232 deletions(-) create mode 100644 clc/metrics/environment.go create mode 100644 clc/metrics/interfaces.go create mode 100644 clc/metrics/key.go create mode 100644 clc/metrics/metric_store.go create mode 100644 clc/metrics/metric_store_test.go create mode 100644 clc/metrics/nop_metric_store.go create mode 100644 clc/metrics/query.go create mode 100644 clc/metrics/query_generator.go diff --git a/base/commands/atomic_long/atomic_long_destroy.go b/base/commands/atomic_long/atomic_long_destroy.go index 1918dc5e9..61ffeec85 100644 --- a/base/commands/atomic_long/atomic_long_destroy.go +++ b/base/commands/atomic_long/atomic_long_destroy.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewDestroyCommand("AtomicLong", getAtomicLong) + c := commands.NewDestroyCommand("AtomicLong", "atomiclong", getAtomicLong) check.Must(plug.Registry.RegisterCommand("atomic-long:destroy", c)) } diff --git a/base/commands/atomic_long/atomic_long_get.go b/base/commands/atomic_long/atomic_long_get.go index 8a0389868..f8d55fcc6 100644 --- a/base/commands/atomic_long/atomic_long_get.go +++ b/base/commands/atomic_long/atomic_long_get.go @@ -29,6 +29,7 @@ func (GetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.atomiclong") sp.SetText(fmt.Sprintf("Getting value of AtomicLong %s", ali.Name())) val, err := ali.Get(ctx) if err != nil { diff --git a/base/commands/atomic_long/atomic_long_set.go b/base/commands/atomic_long/atomic_long_set.go index b131b7c63..cf46dcd0d 100644 --- a/base/commands/atomic_long/atomic_long_set.go +++ b/base/commands/atomic_long/atomic_long_set.go @@ -32,6 +32,7 @@ func (mc *SetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.atomiclong") sp.SetText(fmt.Sprintf("Setting value of AtomicLong %s", name)) v := ec.GetInt64Arg(base.ArgValue) err = ali.Set(ctx, v) diff --git a/base/commands/atomic_long/common.go b/base/commands/atomic_long/common.go index d6b6df56d..f62c42768 100644 --- a/base/commands/atomic_long/common.go +++ b/base/commands/atomic_long/common.go @@ -29,6 +29,7 @@ func atomicLongChangeValue(ctx context.Context, ec plug.ExecContext, verb string if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.atomiclong") sp.SetText(fmt.Sprintf("%sing the AtomicLong %s", verb, name)) val, err := ali.AddAndGet(ctx, change(by)) if err != nil { diff --git a/base/commands/common.go b/base/commands/common.go index 9b286bcde..2d8d389f7 100644 --- a/base/commands/common.go +++ b/base/commands/common.go @@ -11,6 +11,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base" _ "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/mk" @@ -35,12 +36,14 @@ type getDestroyerFunc[T Destroyer] func(context.Context, plug.ExecContext, clc.S type DestroyCommand[T Destroyer] struct { typeName string + metricName string getDestroyerFn getDestroyerFunc[T] } -func NewDestroyCommand[T Destroyer](typeName string, getFn getDestroyerFunc[T]) *DestroyCommand[T] { +func NewDestroyCommand[T Destroyer](typeName string, metricName string, getFn getDestroyerFunc[T]) *DestroyCommand[T] { return &DestroyCommand[T]{ typeName: typeName, + metricName: metricName, getDestroyerFn: getFn, } } @@ -75,6 +78,7 @@ func (cm DestroyCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) sp.SetText(fmt.Sprintf("Destroying %s '%s'", cm.typeName, name)) if err := m.Destroy(ctx); err != nil { return nil, err @@ -98,12 +102,14 @@ type getClearerFunc[T Clearer] func(context.Context, plug.ExecContext, clc.Spinn type ClearCommand[T Clearer] struct { typeName string + metricName string getClearerFn getClearerFunc[T] } -func NewClearCommand[T Clearer](typeName string, getFn getClearerFunc[T]) *ClearCommand[T] { +func NewClearCommand[T Clearer](typeName, metricName string, getFn getClearerFunc[T]) *ClearCommand[T] { return &ClearCommand[T]{ typeName: typeName, + metricName: metricName, getClearerFn: getFn, } } @@ -135,6 +141,7 @@ func (cm ClearCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) sp.SetText(fmt.Sprintf("Clearing %s '%s'", cm.typeName, name)) if err := m.Clear(ctx); err != nil { return nil, err @@ -158,12 +165,14 @@ type getSizerFunc[T Sizer] func(context.Context, plug.ExecContext, clc.Spinner) type SizeCommand[T Sizer] struct { typeName string + metricName string getSizerFn getSizerFunc[T] } -func NewSizeCommand[T Sizer](typeName string, getFn getSizerFunc[T]) *SizeCommand[T] { +func NewSizeCommand[T Sizer](typeName, metricName string, getFn getSizerFunc[T]) *SizeCommand[T] { return &SizeCommand[T]{ typeName: typeName, + metricName: metricName, getSizerFn: getFn, } } @@ -182,6 +191,7 @@ func (cm SizeCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) sp.SetText(fmt.Sprintf("Getting the size of %s '%s'", cm.typeName, name)) return m.Size(ctx) }) diff --git a/base/commands/demo/demo_generate_data.go b/base/commands/demo/demo_generate_data.go index d72e0d05c..6930cd7f2 100644 --- a/base/commands/demo/demo_generate_data.go +++ b/base/commands/demo/demo_generate_data.go @@ -87,6 +87,7 @@ func (GenerateDataCommand) Exec(ctx context.Context, ec plug.ExecContext) error } func generatePreviewResult(ctx context.Context, ec plug.ExecContext, generator dataStreamGenerator, keyVals map[string]string) error { + cmd.IncrementMetric(ctx, ec, "total.demo") maxCount := ec.Props().GetInt(flagMaxValues) if maxCount < 1 { maxCount = 10 @@ -121,15 +122,16 @@ func generateResult(ctx context.Context, ec plug.ExecContext, generator dataStre return fmt.Errorf("either %s key-value pair must be given or --preview must be used", pairMapName) } maxCount := ec.Props().GetInt(flagMaxValues) + query, err := generator.GenerateMappingQuery(mapName) + if err != nil { + return err + } query, stop, err := cmd.ExecuteBlocking(ctx, ec, func(ctx context.Context, sp clc.Spinner) (string, error) { sp.SetText("Creating the mapping") - query, err := generator.GenerateMappingQuery(mapName) - if err != nil { - return "", err - } if _, err := sql.ExecSQL(ctx, ec, query); err != nil { return "", err } + cmd.IncrementClusterMetric(ctx, ec, "total.demo") return query, nil }) if err != nil { diff --git a/base/commands/demo/demo_map_set_many.go b/base/commands/demo/demo_map_set_many.go index 5e7eb1be1..2eb571a7d 100644 --- a/base/commands/demo/demo_map_set_many.go +++ b/base/commands/demo/demo_map_set_many.go @@ -12,7 +12,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -48,6 +48,7 @@ func (m MapSetManyCmd) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.demo") sp.SetText(fmt.Sprintf("Creating entries in map %s with %d entries", mapName, count)) mm, err := ci.Client().GetMap(ctx, mapName) if err != nil { @@ -113,5 +114,5 @@ func getValueSize(sizeStr string) (int64, error) { } func init() { - Must(plug.Registry.RegisterCommand("demo:map-setmany", &MapSetManyCmd{})) + check.Must(plug.Registry.RegisterCommand("demo:map-setmany", &MapSetManyCmd{})) } diff --git a/base/commands/job/common.go b/base/commands/job/common.go index 097ac5270..c45b42ef7 100644 --- a/base/commands/job/common.go +++ b/base/commands/job/common.go @@ -12,6 +12,7 @@ import ( "github.com/hazelcast/hazelcast-go-client/types" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -70,6 +71,7 @@ func terminateJob(ctx context.Context, ec plug.ExecContext, tm int32, cm Termina if err != nil { return 0, err } + cmd.IncrementClusterMetric(ctx, ec, "total.job") j := jet.New(ci, status, ec.Logger()) jis, err := j.GetJobList(ctx) if err != nil { diff --git a/base/commands/job/job.go b/base/commands/job/job.go index e34b988a1..2d97d851e 100644 --- a/base/commands/job/job.go +++ b/base/commands/job/job.go @@ -6,7 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -27,5 +27,5 @@ func (Command) Exec(context.Context, plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("job", &Command{})) + check.Must(plug.Registry.RegisterCommand("job", &Command{})) } diff --git a/base/commands/job/job_export_snapshot.go b/base/commands/job/job_export_snapshot.go index 55cd0b153..50361b999 100644 --- a/base/commands/job/job_export_snapshot.go +++ b/base/commands/job/job_export_snapshot.go @@ -10,7 +10,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -42,6 +42,7 @@ func (ExportSnapshotCommand) Exec(ctx context.Context, ec plug.ExecContext) erro if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.job") j := jet.New(ci, sp, ec.Logger()) jis, err := j.GetJobList(ctx) if err != nil { @@ -104,5 +105,5 @@ func autoGenerateSnapshotName(jobName string) string { } func init() { - Must(plug.Registry.RegisterCommand("job:export-snapshot", &ExportSnapshotCommand{})) + check.Must(plug.Registry.RegisterCommand("job:export-snapshot", &ExportSnapshotCommand{})) } diff --git a/base/commands/job/job_list.go b/base/commands/job/job_list.go index 8e6185a5f..32f96c305 100644 --- a/base/commands/job/job_list.go +++ b/base/commands/job/job_list.go @@ -10,7 +10,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -35,6 +35,7 @@ func (ListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.job") sp.SetText("Getting the job list") j := jet.New(ci, sp, ec.Logger()) jl, err := j.GetJobList(ctx) @@ -139,5 +140,5 @@ func msToOffsetDateTimeColumn(ms int64, name string) output.Column { } func init() { - Must(plug.Registry.RegisterCommand("job:list", &ListCommand{})) + check.Must(plug.Registry.RegisterCommand("job:list", &ListCommand{})) } diff --git a/base/commands/job/job_resume.go b/base/commands/job/job_resume.go index e740b886e..168603164 100644 --- a/base/commands/job/job_resume.go +++ b/base/commands/job/job_resume.go @@ -7,8 +7,9 @@ import ( "fmt" "time" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -37,6 +38,7 @@ func (ResumeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return 0, err } + cmd.IncrementClusterMetric(ctx, ec, "total.job") j := jet.New(ci, status, ec.Logger()) jis, err := j.GetJobList(ctx) if err != nil { @@ -73,5 +75,5 @@ func (ResumeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("job:resume", &ResumeCommand{})) + check.Must(plug.Registry.RegisterCommand("job:resume", &ResumeCommand{})) } diff --git a/base/commands/job/job_submit.go b/base/commands/job/job_submit.go index c89a61171..f50ad1ff1 100644 --- a/base/commands/job/job_submit.go +++ b/base/commands/job/job_submit.go @@ -13,7 +13,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -104,6 +104,7 @@ func submitJar(ctx context.Context, ec plug.ExecContext, path string) (int64, er if err != nil { return 0, err } + cmd.IncrementClusterMetric(ctx, ec, "total.job") if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { err := fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) return 0, err @@ -207,5 +208,5 @@ func getJobIDs(ctx context.Context, j *jet.Jet, jobName string) (types.Set[int64 } func init() { - Must(plug.Registry.RegisterCommand("job:submit", &SubmitCommand{})) + check.Must(plug.Registry.RegisterCommand("job:submit", &SubmitCommand{})) } diff --git a/base/commands/job/job_terminate.go b/base/commands/job/job_terminate.go index 61169e987..12e940e13 100644 --- a/base/commands/job/job_terminate.go +++ b/base/commands/job/job_terminate.go @@ -6,7 +6,7 @@ import ( "context" "fmt" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/jet" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -44,7 +44,7 @@ func (cm TerminateCommand) Exec(ctx context.Context, ec plug.ExecContext) error } func init() { - Must(plug.Registry.RegisterCommand("job:cancel", &TerminateCommand{ + check.Must(plug.Registry.RegisterCommand("job:cancel", &TerminateCommand{ name: "cancel", longHelp: "Cancels the job with the given ID or name", shortHelp: "Cancels the job with the given ID or name", @@ -55,7 +55,7 @@ func init() { successMsg: "Started cancellation of '%s'", failureMsg: "Failed to start job cancellation", })) - Must(plug.Registry.RegisterCommand("job:suspend", &TerminateCommand{ + check.Must(plug.Registry.RegisterCommand("job:suspend", &TerminateCommand{ name: "suspend", longHelp: "Suspends the job with the given ID or name", shortHelp: "Suspends the job with the given ID or name", @@ -66,7 +66,7 @@ func init() { successMsg: "Started suspension of '%s'", failureMsg: "Failed to start job suspension", })) - Must(plug.Registry.RegisterCommand("job:restart", &TerminateCommand{ + check.Must(plug.Registry.RegisterCommand("job:restart", &TerminateCommand{ name: "restart", longHelp: "Restarts the job with the given ID or name", shortHelp: "Restarts the job with the given ID or name", diff --git a/base/commands/list/common.go b/base/commands/list/common.go index 8af5b295f..7b5a3b2ec 100644 --- a/base/commands/list/common.go +++ b/base/commands/list/common.go @@ -36,6 +36,7 @@ func removeFromList(ctx context.Context, ec plug.ExecContext, name string, index if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.list") pid, err := internal.StringToPartitionID(ci, name) if err != nil { return nil, err diff --git a/base/commands/list/list_add.go b/base/commands/list/list_add.go index 9481ef9cb..ed8a2146f 100644 --- a/base/commands/list/list_add.go +++ b/base/commands/list/list_add.go @@ -13,7 +13,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -39,6 +39,7 @@ func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.list") // get the list just to ensure the corresponding proxy is created _, err = getList(ctx, ec, sp) if err != nil { @@ -67,7 +68,7 @@ func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } if index >= 0 { return true, nil - } + } return codec.DecodeListAddResponse(resp), nil }) if err != nil { @@ -87,5 +88,5 @@ func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("list:add", &AddCommand{})) + check.Must(plug.Registry.RegisterCommand("list:add", &AddCommand{})) } diff --git a/base/commands/list/list_clear.go b/base/commands/list/list_clear.go index 7ee632bb4..3e83fd0c0 100644 --- a/base/commands/list/list_clear.go +++ b/base/commands/list/list_clear.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewClearCommand("List", getList) + cmd := commands.NewClearCommand("List", "list", getList) check.Must(plug.Registry.RegisterCommand("list:clear", cmd)) } diff --git a/base/commands/list/list_contains.go b/base/commands/list/list_contains.go index 2a2f3314f..f5d05fb8a 100644 --- a/base/commands/list/list_contains.go +++ b/base/commands/list/list_contains.go @@ -11,7 +11,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -36,6 +36,7 @@ func (mc *ListContainsCommand) Exec(ctx context.Context, ec plug.ExecContext) er if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.list") // get the list just to ensure the corresponding proxy is created _, err = getList(ctx, ec, sp) if err != nil { @@ -73,5 +74,5 @@ func (mc *ListContainsCommand) Exec(ctx context.Context, ec plug.ExecContext) er } func init() { - Must(plug.Registry.RegisterCommand("list:contains", &ListContainsCommand{})) + check.Must(plug.Registry.RegisterCommand("list:contains", &ListContainsCommand{})) } diff --git a/base/commands/list/list_destroy.go b/base/commands/list/list_destroy.go index 1fd6b75a7..6a692a4a9 100644 --- a/base/commands/list/list_destroy.go +++ b/base/commands/list/list_destroy.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewDestroyCommand("List", getList) + cmd := commands.NewDestroyCommand("List", "list", getList) check.Must(plug.Registry.RegisterCommand("list:destroy", cmd)) } diff --git a/base/commands/list/list_remove_index.go b/base/commands/list/list_remove_index.go index 7ee03f43b..8ed5791ec 100644 --- a/base/commands/list/list_remove_index.go +++ b/base/commands/list/list_remove_index.go @@ -8,7 +8,7 @@ import ( "math" "github.com/hazelcast/hazelcast-commandline-client/base" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -35,5 +35,5 @@ func (mc *ListRemoveIndexCommand) Exec(ctx context.Context, ec plug.ExecContext) } func init() { - Must(plug.Registry.RegisterCommand("list:remove-index", &ListRemoveIndexCommand{})) + check.Must(plug.Registry.RegisterCommand("list:remove-index", &ListRemoveIndexCommand{})) } diff --git a/base/commands/list/list_remove_value.go b/base/commands/list/list_remove_value.go index 3276fe9dd..07aac7bb9 100644 --- a/base/commands/list/list_remove_value.go +++ b/base/commands/list/list_remove_value.go @@ -7,7 +7,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/base/commands" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -29,5 +29,5 @@ func (mc *ListRemoveValueCommand) Exec(ctx context.Context, ec plug.ExecContext) } func init() { - Must(plug.Registry.RegisterCommand("list:remove-value", &ListRemoveValueCommand{})) + check.Must(plug.Registry.RegisterCommand("list:remove-value", &ListRemoveValueCommand{})) } diff --git a/base/commands/list/list_set.go b/base/commands/list/list_set.go index d58c8ab38..a3b66bd99 100644 --- a/base/commands/list/list_set.go +++ b/base/commands/list/list_set.go @@ -11,7 +11,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -38,6 +38,7 @@ func (mc *ListSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.list") // get the list just to ensure the corresponding proxy is created _, err = getList(ctx, ec, sp) if err != nil { @@ -69,5 +70,5 @@ func (mc *ListSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("list:set", &ListSetCommand{})) + check.Must(plug.Registry.RegisterCommand("list:set", &ListSetCommand{})) } diff --git a/base/commands/list/list_size.go b/base/commands/list/list_size.go index f2b0ee2b8..606c9d395 100644 --- a/base/commands/list/list_size.go +++ b/base/commands/list/list_size.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewSizeCommand("List", getList) + cmd := commands.NewSizeCommand("List", "list", getList) check.Must(plug.Registry.RegisterCommand("list:size", cmd)) } diff --git a/base/commands/map/map.go b/base/commands/map/map.go index 83bf34856..ce1059392 100644 --- a/base/commands/map/map.go +++ b/base/commands/map/map.go @@ -7,7 +7,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -30,5 +30,5 @@ func (Command) Exec(context.Context, plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("map", &Command{})) + check.Must(plug.Registry.RegisterCommand("map", &Command{})) } diff --git a/base/commands/map/map_clear.go b/base/commands/map/map_clear.go index e9f900b9a..b12fe008d 100644 --- a/base/commands/map/map_clear.go +++ b/base/commands/map/map_clear.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewClearCommand("Map", getMap) + cmd := commands.NewClearCommand("Map", "map", getMap) check.Must(plug.Registry.RegisterCommand("map:clear", cmd)) } diff --git a/base/commands/map/map_destroy.go b/base/commands/map/map_destroy.go index 0824758dc..aa94bd329 100644 --- a/base/commands/map/map_destroy.go +++ b/base/commands/map/map_destroy.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewDestroyCommand("Map", getMap) + cmd := commands.NewDestroyCommand("Map", "map", getMap) check.Must(plug.Registry.RegisterCommand("map:destroy", cmd)) } diff --git a/base/commands/map/map_entry_set.go b/base/commands/map/map_entry_set.go index 66df367db..1889068ce 100644 --- a/base/commands/map/map_entry_set.go +++ b/base/commands/map/map_entry_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapEntrySetCommand("Map", codec.EncodeMapEntrySetRequest, codec.DecodeMapEntrySetResponse, getMap) + c := commands.NewMapEntrySetCommand("Map", "map", codec.EncodeMapEntrySetRequest, codec.DecodeMapEntrySetResponse, getMap) check.Must(plug.Registry.RegisterCommand("map:entry-set", c)) } diff --git a/base/commands/map/map_get.go b/base/commands/map/map_get.go index 61a257ead..87c299030 100644 --- a/base/commands/map/map_get.go +++ b/base/commands/map/map_get.go @@ -12,6 +12,6 @@ import ( ) func init() { - c := commands.NewMapGetCommand[*hazelcast.Map]("Map", codec.EncodeMapGetRequest, makeDecodeResponseRowsFunc(codec.DecodeMapGetResponse), getMap) + c := commands.NewMapGetCommand[*hazelcast.Map]("Map", "map", codec.EncodeMapGetRequest, makeDecodeResponseRowsFunc(codec.DecodeMapGetResponse), getMap) check.Must(plug.Registry.RegisterCommand("map:get", c)) } diff --git a/base/commands/map/map_key_set.go b/base/commands/map/map_key_set.go index d08fc8052..57db717f8 100644 --- a/base/commands/map/map_key_set.go +++ b/base/commands/map/map_key_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapKeySetCommand("Map", codec.EncodeMapKeySetRequest, codec.DecodeMapKeySetResponse, getMap) + c := commands.NewMapKeySetCommand("Map", "map", codec.EncodeMapKeySetRequest, codec.DecodeMapKeySetResponse, getMap) check.Must(plug.Registry.RegisterCommand("map:key-set", c)) } diff --git a/base/commands/map/map_load_all.go b/base/commands/map/map_load_all.go index a3a059119..e4449c9b9 100644 --- a/base/commands/map/map_load_all.go +++ b/base/commands/map/map_load_all.go @@ -12,7 +12,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) @@ -41,6 +41,7 @@ func (MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.map") var keys []hazelcast.Data for _, keyStr := range ec.GetStringSliceArg(commands.ArgKey) { keyData, err := commands.MakeKeyData(ec, ci, keyStr) @@ -71,5 +72,5 @@ func (MapLoadAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("map:load-all", &MapLoadAllCommand{})) + check.Must(plug.Registry.RegisterCommand("map:load-all", &MapLoadAllCommand{})) } diff --git a/base/commands/map/map_lock.go b/base/commands/map/map_lock.go index 8ccb3838e..7a2d7311f 100644 --- a/base/commands/map/map_lock.go +++ b/base/commands/map/map_lock.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewLockCommand("Map", getMap) + c := commands.NewLockCommand("Map", "map", getMap) check.Must(plug.Registry.RegisterCommand("map:lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/map/map_remove.go b/base/commands/map/map_remove.go index f0bb5a86a..6ee6a0960 100644 --- a/base/commands/map/map_remove.go +++ b/base/commands/map/map_remove.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapRemoveCommand("Map", codec.EncodeMapRemoveRequest, makeDecodeResponseRowsFunc(codec.DecodeMapRemoveResponse)) + c := commands.NewMapRemoveCommand("Map", "map", codec.EncodeMapRemoveRequest, makeDecodeResponseRowsFunc(codec.DecodeMapRemoveResponse)) check.Must(plug.Registry.RegisterCommand("map:remove", c)) } diff --git a/base/commands/map/map_set.go b/base/commands/map/map_set.go index d00331ec5..703658ebf 100644 --- a/base/commands/map/map_set.go +++ b/base/commands/map/map_set.go @@ -12,7 +12,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" ) @@ -39,6 +39,7 @@ func (MapSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.map") sp.SetText(fmt.Sprintf("Setting value into Map '%s'", mapName)) _, err = getMap(ctx, ec, sp) if err != nil { @@ -73,5 +74,5 @@ func (MapSetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("map:set", &MapSetCommand{})) + check.Must(plug.Registry.RegisterCommand("map:set", &MapSetCommand{})) } diff --git a/base/commands/map/map_size.go b/base/commands/map/map_size.go index d933f3f38..a117873f0 100644 --- a/base/commands/map/map_size.go +++ b/base/commands/map/map_size.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewSizeCommand("Map", getMap) + c := commands.NewSizeCommand("Map", "map", getMap) check.Must(plug.Registry.RegisterCommand("map:size", c)) } diff --git a/base/commands/map/map_try_lock.go b/base/commands/map/map_try_lock.go index c3d9bc95d..ec154ba5b 100644 --- a/base/commands/map/map_try_lock.go +++ b/base/commands/map/map_try_lock.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewTryLockCommand("Map", getMap) + c := commands.NewTryLockCommand("Map", "map", getMap) check.Must(plug.Registry.RegisterCommand("map:try-lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/map/map_unlock.go b/base/commands/map/map_unlock.go index 1cc8d0318..c1694dfef 100644 --- a/base/commands/map/map_unlock.go +++ b/base/commands/map/map_unlock.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewMapUnlockCommand("Map", getMap) + c := commands.NewMapUnlockCommand("Map", "map", getMap) check.Must(plug.Registry.RegisterCommand("map:unlock", c)) } diff --git a/base/commands/map/map_values.go b/base/commands/map/map_values.go index 59639160d..d3db7ac90 100644 --- a/base/commands/map/map_values.go +++ b/base/commands/map/map_values.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapValuesCommand("Map", codec.EncodeMapValuesRequest, codec.DecodeMapValuesResponse, getMap) + c := commands.NewMapValuesCommand("Map", "map", codec.EncodeMapValuesRequest, codec.DecodeMapValuesResponse, getMap) check.Must(plug.Registry.RegisterCommand("map:values", c)) } diff --git a/base/commands/map_common.go b/base/commands/map_common.go index 9f4dde55d..38cacec3f 100644 --- a/base/commands/map_common.go +++ b/base/commands/map_common.go @@ -21,18 +21,20 @@ type nameRequestEncodeFunc func(name string) *hazelcast.ClientMessage type pairsResponseDecodeFunc func(message *hazelcast.ClientMessage) []hazelcast.Pair type MapEntrySetCommand[T any] struct { - typeName string - encoder nameRequestEncodeFunc - decoder pairsResponseDecodeFunc - prefixer mapPrefixFunc[T] + typeName string + metricName string + encoder nameRequestEncodeFunc + decoder pairsResponseDecodeFunc + prefixer mapPrefixFunc[T] } -func NewMapEntrySetCommand[T any](typeName string, encoder nameRequestEncodeFunc, decoder pairsResponseDecodeFunc, prefixer mapPrefixFunc[T]) *MapEntrySetCommand[T] { +func NewMapEntrySetCommand[T any](typeName, metricName string, encoder nameRequestEncodeFunc, decoder pairsResponseDecodeFunc, prefixer mapPrefixFunc[T]) *MapEntrySetCommand[T] { return &MapEntrySetCommand[T]{ - typeName: typeName, - encoder: encoder, - decoder: decoder, - prefixer: prefixer, + typeName: typeName, + metricName: metricName, + encoder: encoder, + decoder: decoder, + prefixer: prefixer, } } @@ -51,6 +53,7 @@ func (cm MapEntrySetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) e if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) if _, err = cm.prefixer(ctx, ec, sp); err != nil { return nil, err } @@ -75,18 +78,20 @@ type getRequestEncodeFunc func(name string, keyData hazelcast.Data, threadID int type getResponseDecodeFunc func(ctx context.Context, ec plug.ExecContext, res *hazelcast.ClientMessage) ([]output.Row, error) type MapGetCommand[T any] struct { - typeName string - encoder getRequestEncodeFunc - decoder getResponseDecodeFunc - prefixer mapPrefixFunc[T] + typeName string + metricName string + encoder getRequestEncodeFunc + decoder getResponseDecodeFunc + prefixer mapPrefixFunc[T] } -func NewMapGetCommand[T any](typeName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc, prefixer mapPrefixFunc[T]) *MapGetCommand[T] { +func NewMapGetCommand[T any](typeName, metricName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc, prefixer mapPrefixFunc[T]) *MapGetCommand[T] { return &MapGetCommand[T]{ - typeName: typeName, - encoder: encoder, - decoder: decoder, - prefixer: prefixer, + typeName: typeName, + metricName: metricName, + encoder: encoder, + decoder: decoder, + prefixer: prefixer, } } @@ -107,6 +112,7 @@ func (cm MapGetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) if _, err = cm.prefixer(ctx, ec, sp); err != nil { return nil, err } @@ -137,18 +143,20 @@ func (cm MapGetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error type dataSliceDecoderFunc func(message *hazelcast.ClientMessage) []*hazelcast.Data type MapKeySetCommand[T any] struct { - typeName string - encoder nameRequestEncodeFunc - decoder dataSliceDecoderFunc - prefixer mapPrefixFunc[T] + typeName string + metricName string + encoder nameRequestEncodeFunc + decoder dataSliceDecoderFunc + prefixer mapPrefixFunc[T] } -func NewMapKeySetCommand[T any](typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc, prefixer mapPrefixFunc[T]) *MapKeySetCommand[T] { +func NewMapKeySetCommand[T any](typeName, metricName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc, prefixer mapPrefixFunc[T]) *MapKeySetCommand[T] { return &MapKeySetCommand[T]{ - typeName: typeName, - encoder: encoder, - decoder: decoder, - prefixer: prefixer, + typeName: typeName, + metricName: metricName, + encoder: encoder, + decoder: decoder, + prefixer: prefixer, } } @@ -167,6 +175,7 @@ func (cm MapKeySetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) err if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) if _, err = cm.prefixer(ctx, ec, sp); err != nil { return nil, err } @@ -201,16 +210,18 @@ func (cm MapKeySetCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) err } type MapRemoveCommand struct { - typeName string - encoder getRequestEncodeFunc - decoder getResponseDecodeFunc + typeName string + metricName string + encoder getRequestEncodeFunc + decoder getResponseDecodeFunc } -func NewMapRemoveCommand(typeName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc) *MapRemoveCommand { +func NewMapRemoveCommand(typeName, metricName string, encoder getRequestEncodeFunc, decoder getResponseDecodeFunc) *MapRemoveCommand { return &MapRemoveCommand{ - typeName: typeName, - encoder: encoder, - decoder: decoder, + typeName: typeName, + metricName: metricName, + encoder: encoder, + decoder: decoder, } } @@ -231,6 +242,7 @@ func (cm MapRemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) sp.SetText(fmt.Sprintf("Removing from %s '%s'", cm.typeName, name)) keyData, err := MakeKeyData(ec, ci, keyStr) if err != nil { @@ -260,14 +272,16 @@ type Locker interface { type getLockerFunc[T Locker] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) type LockCommand[T Locker] struct { - typeName string - getFn getLockerFunc[T] + typeName string + metricName string + getFn getLockerFunc[T] } -func NewLockCommand[T Locker](typeName string, getFn getLockerFunc[T]) *LockCommand[T] { +func NewLockCommand[T Locker](typeName, metricName string, getFn getLockerFunc[T]) *LockCommand[T] { return &LockCommand[T]{ - typeName: typeName, - getFn: getFn, + typeName: typeName, + metricName: metricName, + getFn: getFn, } } @@ -295,6 +309,7 @@ func (cm LockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) sp.SetText(fmt.Sprintf("Locking the key in %s '%s'", cm.typeName, name)) if ttl := GetTTL(ec); ttl != clc.TTLUnset { return nil, m.LockWithLease(ctx, key, time.Duration(GetTTL(ec))) @@ -318,14 +333,16 @@ type LockTrier interface { type getLockTrierFunc[T LockTrier] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) type MapTryLockCommand[T LockTrier] struct { - typeName string - getFn getLockTrierFunc[T] + typeName string + metricName string + getFn getLockTrierFunc[T] } -func NewTryLockCommand[T LockTrier](typeName string, getFn getLockTrierFunc[T]) *MapTryLockCommand[T] { +func NewTryLockCommand[T LockTrier](typeName, metricName string, getFn getLockTrierFunc[T]) *MapTryLockCommand[T] { return &MapTryLockCommand[T]{ - typeName: typeName, - getFn: getFn, + typeName: typeName, + metricName: metricName, + getFn: getFn, } } @@ -352,6 +369,7 @@ func (cm MapTryLockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) er if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) key, err := MakeValueFromString(ec.GetStringArg(ArgKey), ec.Props().GetString(FlagKeyType)) if err != nil { return nil, err @@ -392,14 +410,16 @@ type Unlocker interface { type getUnlockerFunc[T Unlocker] func(context.Context, plug.ExecContext, clc.Spinner) (T, error) type MapUnlockCommand[T Unlocker] struct { - typeName string - getFn getUnlockerFunc[T] + typeName string + metricName string + getFn getUnlockerFunc[T] } -func NewMapUnlockCommand[T Unlocker](typeName string, getFn getUnlockerFunc[T]) *MapUnlockCommand[T] { +func NewMapUnlockCommand[T Unlocker](typeName, metricName string, getFn getUnlockerFunc[T]) *MapUnlockCommand[T] { return &MapUnlockCommand[T]{ - typeName: typeName, - getFn: getFn, + typeName: typeName, + metricName: metricName, + getFn: getFn, } } @@ -424,6 +444,7 @@ func (cm MapUnlockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) err if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) return nil, m.Unlock(ctx, key) }) if err != nil { @@ -436,18 +457,20 @@ func (cm MapUnlockCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) err } type MapValuesCommand[T any] struct { - typeName string - encoder nameRequestEncodeFunc - decoder dataSliceDecoderFunc - prefixer mapPrefixFunc[T] + typeName string + metricName string + encoder nameRequestEncodeFunc + decoder dataSliceDecoderFunc + prefixer mapPrefixFunc[T] } -func NewMapValuesCommand[T any](typeName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc, prefixer mapPrefixFunc[T]) *MapValuesCommand[T] { +func NewMapValuesCommand[T any](typeName, metricName string, encoder nameRequestEncodeFunc, decoder dataSliceDecoderFunc, prefixer mapPrefixFunc[T]) *MapValuesCommand[T] { return &MapValuesCommand[T]{ - typeName: typeName, - encoder: encoder, - decoder: decoder, - prefixer: prefixer, + typeName: typeName, + metricName: metricName, + encoder: encoder, + decoder: decoder, + prefixer: prefixer, } } @@ -466,6 +489,7 @@ func (cm *MapValuesCommand[T]) Exec(ctx context.Context, ec plug.ExecContext) er if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total."+cm.metricName) if _, err := cm.prefixer(ctx, ec, sp); err != nil { return nil, err } diff --git a/base/commands/multimap/multimap_clear.go b/base/commands/multimap/multimap_clear.go index 64d645adc..afab18409 100644 --- a/base/commands/multimap/multimap_clear.go +++ b/base/commands/multimap/multimap_clear.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewClearCommand("MultiMap", getMultiMap) + cmd := commands.NewClearCommand("MultiMap", "multimap", getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:clear", cmd)) } diff --git a/base/commands/multimap/multimap_destroy.go b/base/commands/multimap/multimap_destroy.go index 1af96c25f..3a165096a 100644 --- a/base/commands/multimap/multimap_destroy.go +++ b/base/commands/multimap/multimap_destroy.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewDestroyCommand("MultiMap", getMultiMap) + cmd := commands.NewDestroyCommand("MultiMap", "multimap", getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:destroy", cmd)) } diff --git a/base/commands/multimap/multimap_entry_set.go b/base/commands/multimap/multimap_entry_set.go index a943bb5b2..2edbc852f 100644 --- a/base/commands/multimap/multimap_entry_set.go +++ b/base/commands/multimap/multimap_entry_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapEntrySetCommand("MultiMap", codec.EncodeMultiMapEntrySetRequest, codec.DecodeMultiMapEntrySetResponse, getMultiMap) + c := commands.NewMapEntrySetCommand("MultiMap", "multimap", codec.EncodeMultiMapEntrySetRequest, codec.DecodeMultiMapEntrySetResponse, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:entry-set", c)) } diff --git a/base/commands/multimap/multimap_get.go b/base/commands/multimap/multimap_get.go index 0a1f950f6..1e7b22338 100644 --- a/base/commands/multimap/multimap_get.go +++ b/base/commands/multimap/multimap_get.go @@ -11,6 +11,6 @@ import ( func init() { d := makeDecodeResponseRowsFunc(codec.DecodeMultiMapGetResponse) - c := commands.NewMapGetCommand("MultiMap", codec.EncodeMultiMapGetRequest, d, getMultiMap) + c := commands.NewMapGetCommand("MultiMap", "multimap", codec.EncodeMultiMapGetRequest, d, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:get", c)) } diff --git a/base/commands/multimap/multimap_key_set.go b/base/commands/multimap/multimap_key_set.go index 4f1eab86a..8b36cfe37 100644 --- a/base/commands/multimap/multimap_key_set.go +++ b/base/commands/multimap/multimap_key_set.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapKeySetCommand("MultiMap", codec.EncodeMultiMapKeySetRequest, codec.DecodeMultiMapKeySetResponse, getMultiMap) + c := commands.NewMapKeySetCommand("MultiMap", "multimap", codec.EncodeMultiMapKeySetRequest, codec.DecodeMultiMapKeySetResponse, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:key-set", c)) } diff --git a/base/commands/multimap/multimap_lock.go b/base/commands/multimap/multimap_lock.go index ca12da29f..e8b06e256 100644 --- a/base/commands/multimap/multimap_lock.go +++ b/base/commands/multimap/multimap_lock.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewLockCommand("MultiMap", getMultiMap) + c := commands.NewLockCommand("MultiMap", "multimap", getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/multimap/multimap_put.go b/base/commands/multimap/multimap_put.go index 6d5ffb700..4b8be53b1 100644 --- a/base/commands/multimap/multimap_put.go +++ b/base/commands/multimap/multimap_put.go @@ -9,6 +9,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -38,6 +39,7 @@ func (MultiMapPutCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.multimap") sp.SetText(fmt.Sprintf("Putting value into MultiMap '%s'", name)) kd, vd, err := commands.MakeKeyValueData(ec, ci, keyStr, valueStr) if err != nil { diff --git a/base/commands/multimap/multimap_remove.go b/base/commands/multimap/multimap_remove.go index c741259d0..d73550c3a 100644 --- a/base/commands/multimap/multimap_remove.go +++ b/base/commands/multimap/multimap_remove.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapRemoveCommand("MultiMap", codec.EncodeMultiMapRemoveRequest, makeDecodeResponseRowsFunc(codec.DecodeMultiMapRemoveResponse)) + c := commands.NewMapRemoveCommand("MultiMap", "multimap", codec.EncodeMultiMapRemoveRequest, makeDecodeResponseRowsFunc(codec.DecodeMultiMapRemoveResponse)) check.Must(plug.Registry.RegisterCommand("multi-map:remove", c)) } diff --git a/base/commands/multimap/multimap_size.go b/base/commands/multimap/multimap_size.go index bcf0d902b..cbb37d0c5 100644 --- a/base/commands/multimap/multimap_size.go +++ b/base/commands/multimap/multimap_size.go @@ -9,6 +9,6 @@ import ( ) func init() { - cmd := commands.NewSizeCommand("MultiMap", getMultiMap) + cmd := commands.NewSizeCommand("MultiMap", "multimap", getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:size", cmd)) } diff --git a/base/commands/multimap/multimap_try_lock.go b/base/commands/multimap/multimap_try_lock.go index 3df4ef224..d0949420b 100644 --- a/base/commands/multimap/multimap_try_lock.go +++ b/base/commands/multimap/multimap_try_lock.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewTryLockCommand("MultiMap", getMultiMap) + c := commands.NewTryLockCommand("MultiMap", "multimap", getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:try-lock", c, plug.OnlyInteractive{})) } diff --git a/base/commands/multimap/multimap_unlock.go b/base/commands/multimap/multimap_unlock.go index dd4a73de2..d3c71d33e 100644 --- a/base/commands/multimap/multimap_unlock.go +++ b/base/commands/multimap/multimap_unlock.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewMapUnlockCommand("MultiMap", getMultiMap) + c := commands.NewMapUnlockCommand("MultiMap", "multimap", getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:unlock", c)) } diff --git a/base/commands/multimap/multimap_values.go b/base/commands/multimap/multimap_values.go index 7da9b1243..af4d6e110 100644 --- a/base/commands/multimap/multimap_values.go +++ b/base/commands/multimap/multimap_values.go @@ -10,6 +10,6 @@ import ( ) func init() { - c := commands.NewMapValuesCommand("MultiMap", codec.EncodeMultiMapValuesRequest, codec.DecodeMultiMapValuesResponse, getMultiMap) + c := commands.NewMapValuesCommand("MultiMap", "multimap", codec.EncodeMultiMapValuesRequest, codec.DecodeMultiMapValuesResponse, getMultiMap) check.Must(plug.Registry.RegisterCommand("multi-map:values", c)) } diff --git a/base/commands/object/object.go b/base/commands/object/object.go index d1d143b9e..ba31b52ad 100644 --- a/base/commands/object/object.go +++ b/base/commands/object/object.go @@ -6,7 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -27,5 +27,5 @@ func (Command) Exec(context.Context, plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("object", &Command{})) + check.Must(plug.Registry.RegisterCommand("object", &Command{})) } diff --git a/base/commands/object/object_list.go b/base/commands/object/object_list.go index 3d01a36e4..0025cde4b 100644 --- a/base/commands/object/object_list.go +++ b/base/commands/object/object_list.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/hazelcast/hazelcast-commandline-client/base/objects" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" @@ -126,5 +126,5 @@ func init() { sort.Slice(objTypes, func(i, j int) bool { return objTypes[i] < objTypes[j] }) - Must(plug.Registry.RegisterCommand("object:list", &ListCommand{})) + check.Must(plug.Registry.RegisterCommand("object:list", &ListCommand{})) } diff --git a/base/commands/project/project.go b/base/commands/project/project.go index d064b4ff8..fe6436dcc 100644 --- a/base/commands/project/project.go +++ b/base/commands/project/project.go @@ -5,7 +5,7 @@ package project import ( "context" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -27,5 +27,5 @@ func (gc Command) Exec(ctx context.Context, ec plug.ExecContext) error { func init() { cmd := &Command{} - Must(plug.Registry.RegisterCommand("project", cmd)) + check.Must(plug.Registry.RegisterCommand("project", cmd)) } diff --git a/base/commands/project/project_create.go b/base/commands/project/project_create.go index 879068ed7..5175a7aa1 100644 --- a/base/commands/project/project_create.go +++ b/base/commands/project/project_create.go @@ -13,9 +13,10 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/mk" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -45,6 +46,7 @@ func (pc CreateCommand) Init(cc plug.InitContext) error { } func (pc CreateCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.project") templateName := ec.GetStringArg(argTemplateName) outputDir := ec.Props().GetString(commands.FlagOutputDir) if outputDir == "" { @@ -193,5 +195,5 @@ func isDefaultPropertiesFile(d fs.DirEntry) bool { } func init() { - Must(plug.Registry.RegisterCommand("project:create", &CreateCommand{})) + check.Must(plug.Registry.RegisterCommand("project:create", &CreateCommand{})) } diff --git a/base/commands/project/project_list_templates.go b/base/commands/project/project_list_templates.go index 1ff6dc698..0a9154c67 100644 --- a/base/commands/project/project_list_templates.go +++ b/base/commands/project/project_list_templates.go @@ -16,9 +16,10 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/clc/store" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -50,6 +51,7 @@ func (ListTemplatesCommand) Init(cc plug.InitContext) error { } func (ListTemplatesCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.project") isLocal := ec.Props().GetBool(flagLocal) isRefresh := ec.Props().GetBool(flagRefresh) if isLocal && isRefresh { @@ -196,7 +198,7 @@ func updateCache(sa *store.StoreAccessor, templates []Template) error { return err } _, err = sa.WithLock(func(s *store.Store) (any, error) { - err = s.DeleteEntriesWithPrefix(templatesKey) + err = s.DeleteEntriesWithPrefixes(templatesKey) if err != nil { return nil, err } @@ -228,5 +230,5 @@ func listFromCache(sa *store.StoreAccessor) ([]Template, error) { } func init() { - Must(plug.Registry.RegisterCommand("project:list-templates", &ListTemplatesCommand{})) + check.Must(plug.Registry.RegisterCommand("project:list-templates", &ListTemplatesCommand{})) } diff --git a/base/commands/queue/queue_clear.go b/base/commands/queue/queue_clear.go index 022e8f5e4..97e59438f 100644 --- a/base/commands/queue/queue_clear.go +++ b/base/commands/queue/queue_clear.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewClearCommand("Queue", getQueue) + c := commands.NewClearCommand("Queue", "queue", getQueue) check.Must(plug.Registry.RegisterCommand("queue:clear", c)) } diff --git a/base/commands/queue/queue_destroy.go b/base/commands/queue/queue_destroy.go index f33b02621..9b12d2c1b 100644 --- a/base/commands/queue/queue_destroy.go +++ b/base/commands/queue/queue_destroy.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewDestroyCommand("Queue", getQueue) + c := commands.NewDestroyCommand("Queue", "queue", getQueue) check.Must(plug.Registry.RegisterCommand("queue:destroy", c)) } diff --git a/base/commands/queue/queue_offer.go b/base/commands/queue/queue_offer.go index 50fbcd75a..39c643424 100644 --- a/base/commands/queue/queue_offer.go +++ b/base/commands/queue/queue_offer.go @@ -10,7 +10,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" @@ -34,6 +34,7 @@ func (QueueOfferCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.queue") q, err := ci.Client().GetQueue(ctx, name) if err != nil { return nil, err @@ -72,5 +73,5 @@ func (QueueOfferCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("queue:offer", &QueueOfferCommand{})) + check.Must(plug.Registry.RegisterCommand("queue:offer", &QueueOfferCommand{})) } diff --git a/base/commands/queue/queue_poll.go b/base/commands/queue/queue_poll.go index b076bf434..88f0abea4 100644 --- a/base/commands/queue/queue_poll.go +++ b/base/commands/queue/queue_poll.go @@ -11,7 +11,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -42,6 +42,7 @@ func (PollCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.queue") sp.SetText(fmt.Sprintf("Polling from Queue '%s'", queueName)) req := codec.EncodeQueuePollRequest(queueName, 0) pID, err := internal.StringToPartitionID(ci, queueName) @@ -87,5 +88,5 @@ func (PollCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("queue:poll", &PollCommand{})) + check.Must(plug.Registry.RegisterCommand("queue:poll", &PollCommand{})) } diff --git a/base/commands/queue/queue_size.go b/base/commands/queue/queue_size.go index 4c3554789..8d1c115c9 100644 --- a/base/commands/queue/queue_size.go +++ b/base/commands/queue/queue_size.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewSizeCommand("Queue", getQueue) + c := commands.NewSizeCommand("Queue", "queue", getQueue) check.Must(plug.Registry.RegisterCommand("queue:size", c)) } diff --git a/base/commands/script.go b/base/commands/script.go index a8a13b53d..c23081af4 100644 --- a/base/commands/script.go +++ b/base/commands/script.go @@ -52,6 +52,7 @@ See examples/sql/dessert.sql for a sample script. } func (ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.script") args := ec.GetStringSliceArg(argPath) in := ec.Stdin() if len(args) > 0 { @@ -67,7 +68,7 @@ func (ScriptCommand) Exec(ctx context.Context, ec plug.ExecContext) error { Stderr: ec.Stderr(), Stdout: ec.Stdout(), } - m, err := ec.(*cmd.ExecContext).Main().Clone(cmd.ModeScripting) + m, err := ec.(*cmd.ExecContext).Main().Clone(plug.ModeScripting) if err != nil { return fmt.Errorf("cloning Main: %w", err) } diff --git a/base/commands/set/set_add.go b/base/commands/set/set_add.go index e58d53f23..534eaf4d1 100644 --- a/base/commands/set/set_add.go +++ b/base/commands/set/set_add.go @@ -34,6 +34,7 @@ func (AddCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.set") s, err := ci.Client().GetSet(ctx, name) if err != nil { return nil, err diff --git a/base/commands/set/set_clear.go b/base/commands/set/set_clear.go index 68d0eeecc..d0108f9e9 100644 --- a/base/commands/set/set_clear.go +++ b/base/commands/set/set_clear.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewClearCommand("Set", getSet) + c := commands.NewClearCommand("Set", "set", getSet) check.Must(plug.Registry.RegisterCommand("set:clear", c)) } diff --git a/base/commands/set/set_destroy.go b/base/commands/set/set_destroy.go index c70ed31b8..a6be89f2b 100644 --- a/base/commands/set/set_destroy.go +++ b/base/commands/set/set_destroy.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewDestroyCommand("Set", getSet) + c := commands.NewDestroyCommand("Set", "set", getSet) check.Must(plug.Registry.RegisterCommand("set:destroy", c)) } diff --git a/base/commands/set/set_get_all.go b/base/commands/set/set_get_all.go index f71c77db9..b2ddc22d2 100644 --- a/base/commands/set/set_get_all.go +++ b/base/commands/set/set_get_all.go @@ -10,7 +10,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -33,6 +33,7 @@ func (GetAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.set") req := codec.EncodeSetGetAllRequest(name) pID, err := internal.StringToPartitionID(ci, name) if err != nil { @@ -83,5 +84,5 @@ func (GetAllCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("set:get-all", &GetAllCommand{})) + check.Must(plug.Registry.RegisterCommand("set:get-all", &GetAllCommand{})) } diff --git a/base/commands/set/set_remove.go b/base/commands/set/set_remove.go index 8ee576978..0b3d024bf 100644 --- a/base/commands/set/set_remove.go +++ b/base/commands/set/set_remove.go @@ -11,7 +11,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/proto/codec" @@ -36,6 +36,7 @@ func (sc *RemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.set") sp.SetText(fmt.Sprintf("Removing from Set '%s'", name)) showType := ec.Props().GetBool(base.FlagShowType) var rows []output.Row @@ -80,5 +81,5 @@ func (sc *RemoveCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("set:remove", &RemoveCommand{})) + check.Must(plug.Registry.RegisterCommand("set:remove", &RemoveCommand{})) } diff --git a/base/commands/set/set_size.go b/base/commands/set/set_size.go index 086b52673..009240936 100644 --- a/base/commands/set/set_size.go +++ b/base/commands/set/set_size.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewSizeCommand("Set", getSet) + c := commands.NewSizeCommand("Set", "set", getSet) check.Must(plug.Registry.RegisterCommand("set:size", c)) } diff --git a/base/commands/shell.go b/base/commands/shell.go index e93b15f5c..a462549db 100644 --- a/base/commands/shell.go +++ b/base/commands/shell.go @@ -59,10 +59,11 @@ func (cm *ShellCommand) Exec(context.Context, plug.ExecContext) error { } func (cm *ShellCommand) ExecInteractive(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.shell") if len(ec.Args()) > 0 { return puberrors.ErrNotAvailable } - m, err := ec.(*cmd.ExecContext).Main().Clone(cmd.ModeInteractive) + m, err := ec.(*cmd.ExecContext).Main().Clone(plug.ModeInteractive) if err != nil { return fmt.Errorf("cloning Main: %w", err) } diff --git a/base/commands/sql/sql.go b/base/commands/sql/sql.go index f8eed94bf..7658c77d5 100644 --- a/base/commands/sql/sql.go +++ b/base/commands/sql/sql.go @@ -13,7 +13,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" clcsql "github.com/hazelcast/hazelcast-commandline-client/clc/sql" "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -66,6 +66,7 @@ func (SQLCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.sql") if sv, ok := cmd.CheckServerCompatible(ci, minServerVersion); !ok { return nil, fmt.Errorf("server (%s) does not support this command, at least %s is expected", sv, minServerVersion) } @@ -83,5 +84,5 @@ func (SQLCommand) Exec(ctx context.Context, ec plug.ExecContext) error { func init() { plug.Registry.RegisterAugmentor("20-sql", &SQLCommand{}) - Must(plug.Registry.RegisterCommand("sql", &SQLCommand{})) + check.Must(plug.Registry.RegisterCommand("sql", &SQLCommand{})) } diff --git a/base/commands/topic/topic.go b/base/commands/topic/topic.go index e8a180242..bea964d42 100644 --- a/base/commands/topic/topic.go +++ b/base/commands/topic/topic.go @@ -7,7 +7,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -30,5 +30,5 @@ func (Command) Exec(context.Context, plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("topic", &Command{})) + check.Must(plug.Registry.RegisterCommand("topic", &Command{})) } diff --git a/base/commands/topic/topic_destroy.go b/base/commands/topic/topic_destroy.go index 4a0735f68..15fad4094 100644 --- a/base/commands/topic/topic_destroy.go +++ b/base/commands/topic/topic_destroy.go @@ -9,6 +9,6 @@ import ( ) func init() { - c := commands.NewDestroyCommand("Topic", getTopic) + c := commands.NewDestroyCommand("Topic", "topic", getTopic) check.Must(plug.Registry.RegisterCommand("topic:destroy", c)) } diff --git a/base/commands/topic/topic_publish.go b/base/commands/topic/topic_publish.go index 45cc6f9ef..f1ad12711 100644 --- a/base/commands/topic/topic_publish.go +++ b/base/commands/topic/topic_publish.go @@ -10,7 +10,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base/commands" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/mk" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -34,6 +34,7 @@ func (PublishCommand) Exec(ctx context.Context, ec plug.ExecContext) error { if err != nil { return nil, err } + cmd.IncrementClusterMetric(ctx, ec, "total.topic") // get the topic just to ensure the corresponding proxy is created t, err := ci.Client().GetTopic(ctx, name) if err != nil { @@ -64,5 +65,5 @@ func (PublishCommand) Exec(ctx context.Context, ec plug.ExecContext) error { } func init() { - Must(plug.Registry.RegisterCommand("topic:publish", &PublishCommand{})) + check.Must(plug.Registry.RegisterCommand("topic:publish", &PublishCommand{})) } diff --git a/base/commands/topic/topic_subscribe.go b/base/commands/topic/topic_subscribe.go index 86140d4c3..bf519359d 100644 --- a/base/commands/topic/topic_subscribe.go +++ b/base/commands/topic/topic_subscribe.go @@ -15,7 +15,8 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/base" "github.com/hazelcast/hazelcast-commandline-client/clc" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -35,13 +36,16 @@ func (SubscribeCommand) Init(cc plug.InitContext) error { func (SubscribeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { name := ec.Props().GetString(base.FlagName) - ci, err := ec.ClientInternal(ctx) - if err != nil { - return err - } events := make(chan TopicEvent, 1) + var ci *hazelcast.ClientInternal // Channel is not closed intentionally sid, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + var err error + ci, err = cmd.ClientInternal(ctx, ec, sp) + if err != nil { + return nil, err + } + cmd.IncrementClusterMetric(ctx, ec, "total.topic") sp.SetText(fmt.Sprintf("Listening to messages of topic %s", name)) sid, err := addListener(ctx, ci, name, ec.Logger(), func(event TopicEvent) { select { @@ -203,5 +207,5 @@ func eventRow(e TopicEvent, ec plug.ExecContext) (row output.Row) { } func init() { - Must(plug.Registry.RegisterCommand("topic:subscribe", &SubscribeCommand{})) + check.Must(plug.Registry.RegisterCommand("topic:subscribe", &SubscribeCommand{})) } diff --git a/base/commands/viridian/custom_class_delete.go b/base/commands/viridian/custom_class_delete.go index f15db97d4..5308aabb6 100644 --- a/base/commands/viridian/custom_class_delete.go +++ b/base/commands/viridian/custom_class_delete.go @@ -6,6 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -30,6 +31,7 @@ Make sure you login before running this command. } func (CustomClassDeleteCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/custom_class_download.go b/base/commands/viridian/custom_class_download.go index 4e3d643d0..b1da7efa8 100644 --- a/base/commands/viridian/custom_class_download.go +++ b/base/commands/viridian/custom_class_download.go @@ -6,6 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -36,6 +37,7 @@ Make sure you login before running this command. } func (CustomClassDownloadCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/custom_class_list.go b/base/commands/viridian/custom_class_list.go index 1c21b6012..5d4a92b38 100644 --- a/base/commands/viridian/custom_class_list.go +++ b/base/commands/viridian/custom_class_list.go @@ -6,6 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -29,6 +30,7 @@ Make sure you login before running this command. } func (CustomClassListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/custom_class_upload.go b/base/commands/viridian/custom_class_upload.go index 1685be52d..455bbbe79 100644 --- a/base/commands/viridian/custom_class_upload.go +++ b/base/commands/viridian/custom_class_upload.go @@ -6,6 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -32,6 +33,7 @@ Make sure you login before running this command. } func (CustomClassUploadCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/download_logs.go b/base/commands/viridian/download_logs.go index bce1607dc..0f4e0aa8d 100644 --- a/base/commands/viridian/download_logs.go +++ b/base/commands/viridian/download_logs.go @@ -8,8 +8,10 @@ import ( "os" "path/filepath" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" @@ -32,6 +34,7 @@ Make sure you login before running this command. } func (DownloadLogsCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_cluster_create.go b/base/commands/viridian/viridian_cluster_create.go index be8dfdb12..6abf97dd0 100644 --- a/base/commands/viridian/viridian_cluster_create.go +++ b/base/commands/viridian/viridian_cluster_create.go @@ -7,8 +7,10 @@ import ( "errors" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" @@ -33,6 +35,7 @@ Make sure you login before running this command. } func (ClusterCreateCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_cluster_delete.go b/base/commands/viridian/viridian_cluster_delete.go index 4cc818dc8..b2436154a 100644 --- a/base/commands/viridian/viridian_cluster_delete.go +++ b/base/commands/viridian/viridian_cluster_delete.go @@ -6,6 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -33,6 +34,7 @@ Make sure you login before running this command. } func (ClusterDeleteCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_cluster_get.go b/base/commands/viridian/viridian_cluster_get.go index 20fd3241b..3157b8111 100644 --- a/base/commands/viridian/viridian_cluster_get.go +++ b/base/commands/viridian/viridian_cluster_get.go @@ -7,6 +7,7 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -30,6 +31,7 @@ Make sure you login before running this command. } func (ClusterGetCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_cluster_list.go b/base/commands/viridian/viridian_cluster_list.go index 4104abd75..28aa600de 100644 --- a/base/commands/viridian/viridian_cluster_list.go +++ b/base/commands/viridian/viridian_cluster_list.go @@ -6,6 +6,7 @@ import ( "context" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" @@ -28,6 +29,7 @@ Make sure you login before running this command. } func (ClusterListCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_cluster_resume.go b/base/commands/viridian/viridian_cluster_resume.go index e93a7447c..dcfc39b85 100644 --- a/base/commands/viridian/viridian_cluster_resume.go +++ b/base/commands/viridian/viridian_cluster_resume.go @@ -5,6 +5,7 @@ package viridian import ( "context" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -29,6 +30,7 @@ Make sure you login before running this command. } func (ClusterResumeCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_cluster_stop.go b/base/commands/viridian/viridian_cluster_stop.go index 8f242186c..1ab61ed6a 100644 --- a/base/commands/viridian/viridian_cluster_stop.go +++ b/base/commands/viridian/viridian_cluster_stop.go @@ -5,6 +5,7 @@ package viridian import ( "context" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -29,6 +30,7 @@ Make sure you login before running this command. } func (ClusterStopCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_import_config.go b/base/commands/viridian/viridian_import_config.go index 320d79fcc..686329804 100644 --- a/base/commands/viridian/viridian_import_config.go +++ b/base/commands/viridian/viridian_import_config.go @@ -6,6 +6,7 @@ import ( "context" "fmt" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -30,6 +31,7 @@ Make sure you login before running this command. } func (cm ImportConfigCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") if err := cm.exec(ctx, ec); err != nil { err = handleErrorResponse(ec, err) return fmt.Errorf("could not import cluster configuration: %w", err) diff --git a/base/commands/viridian/viridian_log_stream.go b/base/commands/viridian/viridian_log_stream.go index 8a4c989bf..ac50eb867 100644 --- a/base/commands/viridian/viridian_log_stream.go +++ b/base/commands/viridian/viridian_log_stream.go @@ -14,6 +14,7 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -47,6 +48,7 @@ The log format may be one of: } func (StreamLogCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") api, err := getAPI(ec) if err != nil { return err diff --git a/base/commands/viridian/viridian_login.go b/base/commands/viridian/viridian_login.go index 38d4acaaa..39dc44e93 100644 --- a/base/commands/viridian/viridian_login.go +++ b/base/commands/viridian/viridian_login.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/secrets" "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -51,6 +52,7 @@ Alternatively, you can use the following environment variables: } func (cm LoginCommand) Exec(ctx context.Context, ec plug.ExecContext) error { + cmd.IncrementMetric(ctx, ec, "total.viridian") key, secret, err := getAPIKeySecret(ec) if err != nil { return err diff --git a/clc/cmd/clc.go b/clc/cmd/clc.go index 18531ea23..7f3ce7538 100644 --- a/clc/cmd/clc.go +++ b/clc/cmd/clc.go @@ -19,9 +19,11 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/clc/logger" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" puberrors "github.com/hazelcast/hazelcast-commandline-client/errors" - . "github.com/hazelcast/hazelcast-commandline-client/internal/check" + + "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" ) @@ -30,14 +32,6 @@ var ( MainCommandShortHelp = "Hazelcast CLC" ) -type Mode int - -const ( - ModeNonInteractive Mode = iota - ModeInteractive - ModeScripting -) - type Main struct { root *cobra.Command cmds map[string]*cobra.Command @@ -45,18 +39,19 @@ type Main struct { stderr io.WriteCloser stdout io.WriteCloser stdin io.Reader - mode Mode + mode plug.Mode outputFormat string configLoaded bool props *plug.Properties cc *CommandContext cp config.Provider + ms metrics.MetricStorer arg0 string ciMu *sync.Mutex ci *atomic.Pointer[hazelcast.ClientInternal] } -func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLevel string, sio clc.IO) (*Main, error) { +func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLevel string, sio clc.IO, ms metrics.MetricStorer) (*Main, error) { rc := &cobra.Command{ Use: arg0, Short: MainCommandShortHelp, @@ -75,6 +70,7 @@ func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLeve stdin: sio.Stdin, props: plug.NewProperties(), cp: cfgProvider, + ms: ms, arg0: arg0, ciMu: &sync.Mutex{}, ci: &atomic.Pointer[hazelcast.ClientInternal]{}, @@ -109,7 +105,7 @@ func NewMain(arg0, cfgPath string, cfgProvider config.Provider, logPath, logLeve return m, nil } -func (m *Main) Clone(mode Mode) (*Main, error) { +func (m *Main) Clone(mode plug.Mode) (*Main, error) { mc := *m mc.mode = mode rc := &cobra.Command{ @@ -146,7 +142,7 @@ func (m *Main) Execute(ctx context.Context, args ...string) error { var cm *cobra.Command var cmdArgs []string var err error - if m.mode == ModeNonInteractive { + if m.mode == plug.ModeNonInteractive { cm, cmdArgs, err = m.root.Find(args) if err != nil { return err @@ -263,11 +259,11 @@ func (m *Main) createCommands() error { for _, c := range plug.Registry.Commands() { c := c // check if current command available in current mode - if !plug.Registry.IsAvailable(m.mode != ModeNonInteractive, c.Name) { + if !plug.Registry.IsAvailable(m.mode != plug.ModeNonInteractive, c.Name) { continue } // skip interactive commands in interactive mode - if m.mode == ModeInteractive { + if m.mode == plug.ModeInteractive { if _, ok := c.Item.(plug.InteractiveCommander); ok { continue } @@ -326,7 +322,7 @@ func (m *Main) createCommands() error { Stderr: m.stderr, Stdout: m.stdout, } - ec, err := NewExecContext(m.lg, sio, m.props, m.mode) + ec, err := NewExecContext(m.lg, sio, m.props, m.mode, m.ms) if err != nil { return err } @@ -353,7 +349,7 @@ func (m *Main) createCommands() error { return err } if ic, ok := c.Item.(plug.InteractiveCommander); ok { - ec.SetMode(ModeInteractive) + ec.SetMode(plug.ModeInteractive) err = ic.ExecInteractive(ctx, ec) if errors.Is(err, puberrors.ErrNotAvailable) { return nil @@ -364,7 +360,7 @@ func (m *Main) createCommands() error { } } // add the backslash prefix for top-level commands in the interactive mode - if m.mode != ModeNonInteractive && parent == m.root { + if m.mode != plug.ModeNonInteractive && parent == m.root { cmd.Use = fmt.Sprintf("\\%s", cmd.Use) } parent.AddCommand(cmd) @@ -408,11 +404,11 @@ func (m *Main) clientInternal() *hazelcast.ClientInternal { func convertFlagValue(fs *pflag.FlagSet, name string, v pflag.Value) any { switch v.Type() { case "string": - return MustValue(fs.GetString(name)) + return check.MustValue(fs.GetString(name)) case "bool": - return MustValue(fs.GetBool(name)) + return check.MustValue(fs.GetBool(name)) case "int64": - return MustValue(fs.GetInt64(name)) + return check.MustValue(fs.GetInt64(name)) } panic(fmt.Errorf("cannot convert type: %s", v.Type())) } diff --git a/clc/cmd/command_context.go b/clc/cmd/command_context.go index cb88d8b24..a6e68052a 100644 --- a/clc/cmd/command_context.go +++ b/clc/cmd/command_context.go @@ -9,6 +9,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) type ArgType int @@ -36,14 +37,14 @@ type CommandContext struct { stringValues map[string]*string boolValues map[string]*bool intValues map[string]*int64 - mode Mode + mode plug.Mode isTopLevel bool group *cobra.Group argSpecs []ArgSpec usage string } -func NewCommandContext(cmd *cobra.Command, cfgProvider config.Provider, mode Mode) *CommandContext { +func NewCommandContext(cmd *cobra.Command, cfgProvider config.Provider, mode plug.Mode) *CommandContext { return &CommandContext{ Cmd: cmd, CP: cfgProvider, @@ -137,7 +138,7 @@ func (cc *CommandContext) Hide() { } func (cc *CommandContext) Interactive() bool { - return cc.mode == ModeInteractive + return cc.mode == plug.ModeInteractive } func (cc *CommandContext) SetCommandHelp(long, short string) { diff --git a/clc/cmd/exec_context.go b/clc/cmd/exec_context.go index b25af0b7d..377b9caa0 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -17,6 +17,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" cmderrors "github.com/hazelcast/hazelcast-commandline-client/errors" . "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/log" @@ -42,16 +43,17 @@ type ExecContext struct { args []string kwargs map[string]any props *plug.Properties - mode Mode + mode plug.Mode cmd *cobra.Command main *Main spinnerWait time.Duration printer plug.Printer cp config.Provider spinnerPaused atomic.Bool + ms metrics.MetricStorer } -func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, mode Mode) (*ExecContext, error) { +func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, mode plug.Mode, ms metrics.MetricStorer) (*ExecContext, error) { return &ExecContext{ lg: lg, stdout: sio.Stdout, @@ -61,6 +63,7 @@ func NewExecContext(lg log.Logger, sio clc.IO, props *plug.Properties, mode Mode mode: mode, spinnerWait: 1 * time.Second, kwargs: map[string]any{}, + ms: ms, }, nil } @@ -157,8 +160,8 @@ func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInt return ec.main.clientInternal(), nil } -func (ec *ExecContext) Interactive() bool { - return ec.mode == ModeInteractive +func (ec *ExecContext) Mode() plug.Mode { + return ec.mode } func (ec *ExecContext) AddOutputRows(ctx context.Context, rows ...output.Row) error { @@ -178,9 +181,13 @@ func (ec *ExecContext) AddOutputStream(ctx context.Context, ch <-chan output.Row return ec.printer.PrintStream(ctx, ec.stdout, output.NewChanRows(ch)) } +func (ec *ExecContext) Metrics() metrics.MetricStorer { + return ec.ms +} + func (ec *ExecContext) ShowHelpAndExit() { Must(ec.cmd.Help()) - if !ec.Interactive() { + if ec.Mode() != plug.ModeInteractive { os.Exit(0) } } @@ -189,7 +196,7 @@ func (ec *ExecContext) CommandName() string { return ec.cmd.CommandPath() } -func (ec *ExecContext) SetMode(mode Mode) { +func (ec *ExecContext) SetMode(mode plug.Mode) { ec.mode = mode } diff --git a/clc/cmd/utils.go b/clc/cmd/utils.go index ef928188d..16f119c68 100644 --- a/clc/cmd/utils.go +++ b/clc/cmd/utils.go @@ -11,6 +11,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" "github.com/hazelcast/hazelcast-commandline-client/internal" "github.com/hazelcast/hazelcast-commandline-client/internal/plug" ) @@ -96,3 +97,57 @@ func parseDuration(duration string) (time.Duration, error) { } return pd, nil } + +func IncrementClusterMetric(ctx context.Context, ec plug.ExecContext, metric string) { + // Must not be called before ec.ClusterInternal function. + cid, vid := FindClusterIDs(ctx, ec) + ec.Metrics().Increment(metrics.NewKey(cid, vid), fmt.Sprintf("%s.%s", metric, RunningModeString(ec))) +} + +func IncrementMetric(ctx context.Context, ec plug.ExecContext, metric string) { + // Must not be called before ec.ClusterInternal function. + ec.Metrics().Increment(metrics.NewSimpleKey(), fmt.Sprintf("%s.%s", metric, RunningModeString(ec))) +} + +func FindClusterIDs(ctx context.Context, ec plug.ExecContext) (clusterID string, viridianID string) { + // Must not be called before ec.ClusterInternal function. + if !metrics.PhoneHomeEnabled() { + return "", "" + } + ctx, cancel := context.WithTimeout(ctx, 1000*time.Millisecond) + defer cancel() + ci, err := ec.ClientInternal(ctx) + if err != nil { + return + } + clusterID = ci.ClusterID().String() + if vtoken := ec.Props().GetString(clc.PropertyClusterDiscoveryToken); vtoken != "" { + cluster := ci.ClusterService().FailoverService().Current() + if cluster != nil { + clusterName := ci.ClusterService().FailoverService().Current().ClusterName + viridianID = parseViridianClusterID(clusterName) + } + } + return +} + +func parseViridianClusterID(cid string) string { + s := strings.Split(cid, "-") + if len(s) != 2 { + return "" + } + return s[1] +} + +func RunningModeString(ec plug.ExecContext) string { + switch ec.Mode() { + case plug.ModeNonInteractive: + return "noninteractive-mode" + case plug.ModeInteractive: + return "interactive-mode" + case plug.ModeScripting: + return "script-mode" + default: + return "unknown" + } +} diff --git a/clc/metrics/environment.go b/clc/metrics/environment.go new file mode 100644 index 000000000..8796c85a9 --- /dev/null +++ b/clc/metrics/environment.go @@ -0,0 +1,81 @@ +package metrics + +import ( + "encoding/json" + "os" + "runtime" + + "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-go-client/types" +) + +func PhoneHomeEnabled() bool { + return os.Getenv(EnvPhoneHomeEnabled) != "false" +} + +type GlobalAttributes struct { + ID string `json:"id"` + Architecture string `json:"architecture"` + OS string `json:"os"` +} + +func NewGlobalAttributes() GlobalAttributes { + return GlobalAttributes{ + ID: types.NewUUID().String(), + Architecture: runtime.GOARCH, + OS: runtime.GOOS, + } +} + +func (g *GlobalAttributes) Marshal() ([]byte, error) { + return json.Marshal(g) +} + +func (g *GlobalAttributes) Unmarshal(b []byte) error { + if err := json.Unmarshal(b, g); err != nil { + return err + } + return nil +} + +type SessionAttributes struct { + CLCVersion string + AcquisitionSource AcquisitionSource +} + +func NewSessionMetrics() SessionAttributes { + return SessionAttributes{ + CLCVersion: internal.Version, + AcquisitionSource: FindAcquisionSource(), + } +} + +type AcquisitionSource string + +const ( + EnvAcquisionSource = "CLC_INTERNAL_ACQUISITION_SOURCE" + AcquisionSourceMC AcquisitionSource = "MC" + AcquisionSourceVSCode AcquisitionSource = "VSCode" + AcquisionSourceDocker AcquisitionSource = "Docker" + AcquisionSourceK8S AcquisitionSource = "Kubernetes" + AcquisionSourceUnknown AcquisitionSource = "Unknown" +) + +func FindAcquisionSource() AcquisitionSource { + src := AcquisitionSource(os.Getenv(EnvAcquisionSource)) + if src == "" { + return AcquisionSourceUnknown + } + return AcquisitionSource(src) +} + +func (g *SessionAttributes) Marshal() ([]byte, error) { + return json.Marshal(g) +} + +func (g *SessionAttributes) Unmarshal(b []byte) error { + if err := json.Unmarshal(b, g); err != nil { + return err + } + return nil +} diff --git a/clc/metrics/interfaces.go b/clc/metrics/interfaces.go new file mode 100644 index 000000000..5fe2d5e0a --- /dev/null +++ b/clc/metrics/interfaces.go @@ -0,0 +1,18 @@ +package metrics + +import ( + "context" +) + +type MetricStorer interface { + // Store stores the value in the specified key/metric + // metric is in the form of "metric1.metric2.metric3". + Store(key Key, metric string, val int) + // Increment increments the value in the specified key/metric by one. + // metric is in the form of "metric1.metric2.metric3" + Increment(key Key, metric string) +} + +type metricSender interface { + Send(ctx context.Context) error +} diff --git a/clc/metrics/key.go b/clc/metrics/key.go new file mode 100644 index 000000000..24326a7d7 --- /dev/null +++ b/clc/metrics/key.go @@ -0,0 +1,94 @@ +package metrics + +import ( + "errors" + "strings" + "time" +) + +const ( + PhonehomeKeyPrefix = "ph" + DateFormat = "2006-01-02" + KeyFieldSeparator = "^" +) + +type Key struct { + Datetime time.Time + ClusterID string + ViridianClusterID string +} + +func NewKey(cid, vid string) Key { + date := time.Now().UTC().Truncate(time.Hour * 24) + return Key{ + Datetime: date, + ClusterID: cid, + ViridianClusterID: vid, + } +} + +func NewSimpleKey() Key { + return NewKey("", "") +} + +func datePrefix(date string) string { + return PhonehomeKeyPrefix + KeyFieldSeparator + date +} + +type storageKey struct { + Key + KeyPrefix string + AcquisitionSource AcquisitionSource + CLCVersion string + MetricName string +} + +func newStorageKey(k Key, as AcquisitionSource, version string, metric string) storageKey { + return storageKey{ + Key: k, + KeyPrefix: PhonehomeKeyPrefix, + AcquisitionSource: as, + CLCVersion: version, + MetricName: metric, + } +} + +func (c *storageKey) Marshal() ([]byte, error) { + str := strings.Join([]string{ + c.KeyPrefix, + c.Date(), + c.ClusterID, + c.ViridianClusterID, + string(c.AcquisitionSource), + c.CLCVersion, + c.MetricName, + }, KeyFieldSeparator) + return []byte(str), nil +} + +func (c *storageKey) Date() string { + return c.Datetime.Format(DateFormat) +} + +func (c *storageKey) Unmarshal(b []byte) error { + s := strings.Split(string(b), KeyFieldSeparator) + if len(s) != 7 { + return errors.New("key is in an incorrect format") + } + date, err := time.Parse(DateFormat, s[1]) + if err != nil { + return err + } + *c = storageKey{ + KeyPrefix: s[0], + Key: Key{ + Datetime: date, + ClusterID: s[2], + ViridianClusterID: s[3], + }, + AcquisitionSource: AcquisitionSource(s[4]), + CLCVersion: s[5], + MetricName: s[6], + } + return nil +} diff --git a/clc/metrics/metric_store.go b/clc/metrics/metric_store.go new file mode 100644 index 000000000..83a90724f --- /dev/null +++ b/clc/metrics/metric_store.go @@ -0,0 +1,299 @@ +package metrics + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc/store" + "github.com/hazelcast/hazelcast-commandline-client/internal/http" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" +) + +var ( + defaultStore atomic.Value + defaultNopStore = &NopMetricStore{} +) + +func DefaultStore() MetricStorer { + ds := defaultStore.Load() + if ds != nil { + return ds.(MetricStorer) + } + return defaultNopStore +} + +func Send(ctx context.Context) { + if sender, ok := DefaultStore().(metricSender); ok { + // ignore errors about metrics + _ = sender.Send(ctx) + } +} + +func CreateMetricStore(dir string) { + if PhoneHomeEnabled() { + store, err := newMetricStore(dir) + if err != nil { + defaultStore.Store(defaultNopStore) + return + } + defaultStore.Store(store) + } +} + +const ( + GlobalAttributesKeyName = "metrics-global-attributes" + NextPingTryTimeKey = "metrics-try-next-time" + MetricsVersion = "v1" + EnvPhoneHomeEnabled = "HZ_PHONE_HOME_ENABLED" + StoreDuration = time.Duration(30 * 24 * time.Hour) +) + +type metricStore struct { + mu *sync.Mutex + increments map[storageKey]int + updates map[storageKey]int + globalAttrs GlobalAttributes + sessionAttrs SessionAttributes + sa *store.StoreAccessor + serverURL string + // for test purposes + sendQueriesFn func(ctx context.Context, url string, q ...Query) error +} + +func newMetricStore(dir string) (*metricStore, error) { + ms := metricStore{ + serverURL: "http://phonehome.hazelcast.com/pingCLC", + mu: &sync.Mutex{}, + increments: make(map[storageKey]int), + updates: make(map[storageKey]int), + sessionAttrs: NewSessionMetrics(), + sa: store.NewStoreAccessor(dir, log.NopLogger{}), + sendQueriesFn: sendQueries, + } + if err := ms.ensureGlobalMetrics(); err != nil { + return nil, err + } + return &ms, nil +} + +func (ms *metricStore) ensureGlobalMetrics() error { + gasKey := []byte(GlobalAttributesKeyName) + _, err := ms.sa.WithLock(func(s *store.Store) (any, error) { + var gas GlobalAttributes + firstTime := false + valb, err := s.GetEntry(gasKey) + if err != nil { + if !errors.Is(err, store.ErrKeyNotFound) { + return nil, err + } + firstTime = true + } + if !firstTime { + if err := gas.Unmarshal(valb); err != nil { + return nil, err + } + ms.globalAttrs = gas + return nil, nil + } + gas = NewGlobalAttributes() + gasb, err := gas.Marshal() + if err != nil { + return nil, err + } + if err := s.SetEntry(gasKey, gasb); err != nil { + return nil, err + } + ms.globalAttrs = gas + return nil, nil + }) + return err +} + +func (ms *metricStore) Store(key Key, metric string, val int) { + metrics := strings.Split(metric, ".") + ms.mu.Lock() + for _, m := range metrics { + sk := newStorageKey(key, ms.sessionAttrs.AcquisitionSource, ms.sessionAttrs.CLCVersion, m) + ms.updates[sk] = val + } + ms.mu.Unlock() +} + +func (ms *metricStore) Increment(key Key, metric string) { + metrics := strings.Split(metric, ".") + ms.mu.Lock() + for _, m := range metrics { + sk := newStorageKey(key, ms.sessionAttrs.AcquisitionSource, ms.sessionAttrs.CLCVersion, m) + ms.increments[sk]++ + } + ms.mu.Unlock() +} + +func (ms *metricStore) Send(ctx context.Context) error { + now := time.Now().UTC() + _, err := ms.sa.WithLock(func(s *store.Store) (any, error) { + defer func() { + // set next try time irregardless of send function result + // this stops CLC from trying to send metrics again at every command call + // after failure at sending metrics + _ = storeNextTryTime(s, now) + }() + ms.persistMetrics(s) + nextTime, err := getTryNextTime(s) + sendFirstQuery := false + if err != nil { + if !errors.Is(err, store.ErrKeyNotFound) { + return nil, err + } + sendFirstQuery = true + } + if now.Before(nextTime) { + // enough time still hasn't passed + return nil, nil + } + dates, err := findDatesToSend(s, now) + if err != nil { + return nil, err + } + q := GenerateQueries(s, ms.globalAttrs, dates) + if len(q) == 0 { + if !sendFirstQuery { + return nil, nil + } + q = []Query{GenerateFirstPingQuery(ms.globalAttrs, ms.sessionAttrs, now)} + } + if err := ms.sendQueriesFn(ctx, ms.serverURL, q...); err != nil { + return nil, err + } + return nil, deleteSentDates(s, dates) + }) + return err +} + +func (ms *metricStore) persistMetrics(s *store.Store) { + ms.mu.Lock() + defer ms.mu.Unlock() + ms.persistIncrementMetrics(s) + ms.persistStoreMetrics(s) +} + +func getTryNextTime(s *store.Store) (time.Time, error) { + tb, err := s.GetEntry([]byte(NextPingTryTimeKey)) + if err != nil { + return time.Time{}, err + } + var t time.Time + err = json.Unmarshal(tb, &t) + if err != nil { + return time.Time{}, err + } + return t, nil +} + +func (ms *metricStore) persistIncrementMetrics(s *store.Store) { + for key, val := range ms.increments { + var newVal int + keyb, err := key.Marshal() + if err != nil { + continue + } + err = s.UpdateEntry(keyb, func(current []byte, found bool) []byte { + if found { + var existing int + err := json.Unmarshal(current, &existing) + if err != nil { + // stored value is incorrect format, override it + newVal = val + } else { + newVal = val + existing + } + } else { + // value is not found + newVal = val + } + // int marshalling should not return an error + b, _ := json.Marshal(&newVal) + return b + }, store.OptionWithTTL(StoreDuration)) + if err != nil { + continue + } + } + // delete the metrics from the memory + ms.increments = make(map[storageKey]int) +} + +func (ms *metricStore) persistStoreMetrics(s *store.Store) { + for key, val := range ms.updates { + // int marshalling should not return an error + valb, _ := json.Marshal(val) + keyb, err := key.Marshal() + if err != nil { + continue + } + err = s.SetEntry(keyb, valb, store.OptionWithTTL(StoreDuration)) + if err != nil { + continue + } + } + // delete the metrics from the memory + ms.updates = make(map[storageKey]int) +} + +func findDatesToSend(s *store.Store, now time.Time) (types.Set[string], error) { + keys, err := s.GetKeysWithPrefix(PhonehomeKeyPrefix) + if err != nil { + return types.Set[string]{}, err + } + dates := findDatesFromKeys(keys) + today := now.Format(DateFormat) + dates.Delete(today) + return dates, nil +} + +func deleteSentDates(s *store.Store, dates types.Set[string]) error { + var datePrefixes []string + for date := range dates.Map() { + datePrefixes = append(datePrefixes, datePrefix(date)) + } + return s.DeleteEntriesWithPrefixes(datePrefixes...) + +} + +func storeNextTryTime(s *store.Store, now time.Time) error { + nt := now.Add(12 * time.Hour) + valb, err := json.Marshal(nt) + if err != nil { + return err + } + return s.SetEntry([]byte(NextPingTryTimeKey), valb) +} + +func sendQueries(ctx context.Context, url string, q ...Query) error { + b, err := json.Marshal(q) + if err != nil { + return err + } + cl := http.NewClient() + _, err = cl.Post(ctx, url, bytes.NewReader(b)) + return err +} + +func findDatesFromKeys(keys [][]byte) types.Set[string] { + dates := types.MakeSet[string]() + for _, keyb := range keys { + var k storageKey + if err := k.Unmarshal(keyb); err != nil { + continue + } + dates.Add(k.Date()) + } + return dates +} diff --git a/clc/metrics/metric_store_test.go b/clc/metrics/metric_store_test.go new file mode 100644 index 000000000..347bb9628 --- /dev/null +++ b/clc/metrics/metric_store_test.go @@ -0,0 +1,225 @@ +package metrics + +import ( + "context" + "encoding/json" + "fmt" + "os" + "runtime" + "sync" + "testing" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc/store" + "github.com/hazelcast/hazelcast-commandline-client/internal/check" + "github.com/hazelcast/hazelcast-commandline-client/internal/log" + "github.com/stretchr/testify/require" +) + +func newStorageTestKey(ms *metricStore, m string) storageKey { + return newStorageKey(NewSimpleKey(), ms.sessionAttrs.AcquisitionSource, ms.sessionAttrs.CLCVersion, m) +} + +func newSimpleTestKey(m string, t time.Time, cid string) storageKey { + return storageKey{ + KeyPrefix: PhonehomeKeyPrefix, + Key: Key{ + Datetime: t.Truncate(24 * time.Hour), + ClusterID: cid, + }, + MetricName: m, + } +} + +func TestMetricStore_GlobalMetrics(t *testing.T) { + WithMetricStore(func(ms *metricStore, _ *[]Query) { + gmb := WithStore(ms, func(s *store.Store) []byte { + return check.MustValue(s.GetEntry([]byte(GlobalAttributesKeyName))) + }) + var gm GlobalAttributes + check.Must(gm.Unmarshal(gmb)) + require.Equal(t, runtime.GOARCH, gm.Architecture) + require.Equal(t, runtime.GOOS, gm.OS) + require.NotEqual(t, "", gm.ID) + }) +} + +func TestMetricStore_SessionMetrics(t *testing.T) { + WithMetricStore(func(ms *metricStore, _ *[]Query) { + ms.sessionAttrs = SessionAttributes{ + CLCVersion: "test-version", + AcquisitionSource: "test-as", + } + ms.Increment(NewSimpleKey(), "metric1.metric1") + expected := map[storageKey]int{ + newStorageKey(NewSimpleKey(), "test-as", "test-version", "metric1"): 2, + } + require.EqualValues(t, expected, ms.increments) + }) +} + +func TestMetricStore_Increment(t *testing.T) { + WithMetricStore(func(ms *metricStore, _ *[]Query) { + ms.Increment(NewSimpleKey(), "metric1.metric1.metric2") + ms.Increment(NewSimpleKey(), "metric1") + expected := map[storageKey]int{ + newStorageTestKey(ms, "metric1"): 3, + newStorageTestKey(ms, "metric2"): 1, + } + require.EqualValues(t, expected, ms.increments) + }) +} + +func TestMetricStore_Store(t *testing.T) { + WithMetricStore(func(ms *metricStore, _ *[]Query) { + ms.Store(NewSimpleKey(), "metric1.metric2", 6) + ms.Store(NewSimpleKey(), "metric1", 5) + expected := map[storageKey]int{ + newStorageTestKey(ms, "metric1"): 5, + newStorageTestKey(ms, "metric2"): 6, + } + require.EqualValues(t, expected, ms.updates) + }) +} + +func TestMetricStore_PersistMetrics(t *testing.T) { + WithMetricStore(func(ms *metricStore, _ *[]Query) { + ms.Increment(NewSimpleKey(), "metric1.metric1.metric2") + ms.Increment(NewSimpleKey(), "metric1") + ms.Store(NewSimpleKey(), "metric3.metric4", 6) + ms.Store(NewSimpleKey(), "metric2", 22) + expectedEntries := map[storageKey]int{ + newStorageTestKey(ms, "metric1"): 3, + newStorageTestKey(ms, "metric2"): 22, + newStorageTestKey(ms, "metric3"): 6, + newStorageTestKey(ms, "metric4"): 6, + } + ok := WithStore(ms, func(s *store.Store) bool { + ms.persistMetrics(s) + entries := make(map[storageKey]int) + check.Must(s.RunForeachWithPrefix(PhonehomeKeyPrefix, func(keyb, valb []byte) (ok bool, err error) { + var k storageKey + var v int + check.Must(k.Unmarshal(keyb)) + check.Must(json.Unmarshal(valb, &v)) + entries[k] = v + return true, nil + })) + require.EqualValues(t, expectedEntries, entries) + return true + }) + require.True(t, ok) + }) +} + +func TestMetricStore_Send_Today(t *testing.T) { + now := time.Now() + cid := "cid" + todayKey := newSimpleTestKey("map", now, cid) + todayValue := 5 + WithMetricStore(func(ms *metricStore, sentQueries *[]Query) { + // write the entry to database + kb := check.MustValue(todayKey.Marshal()) + vb := check.MustValue(json.Marshal(todayValue)) + ok := WithStore(ms, func(s *store.Store) bool { + check.Must(s.SetEntry(kb, vb)) + return true + }) + require.True(t, ok) + // send the entries + check.Must(ms.Send(context.Background())) + // check that today's data is not sent and data exists in the database + _, ft := findQuery(sentQueries, todayKey.Date(), cid) + require.Equal(t, false, ft) + keysToday := WithStore(ms, func(s *store.Store) [][]byte { + return check.MustValue(s.GetKeysWithPrefix(datePrefix(todayKey.Date()))) + }) + require.Len(t, keysToday, 1) + }) +} + +func TestMetricStore_Send_Yesterday(t *testing.T) { + now := time.Now() + cid := "cid" + yesterdayCID := newSimpleTestKey("map", now.Add(-24*time.Hour), cid) + yesterday := newSimpleTestKey("map", now.Add(-24*time.Hour), "") + WithMetricStore(func(ms *metricStore, sentQueries *[]Query) { + entries := map[storageKey]int{ + yesterdayCID: 10, + yesterday: 20, + } + ok := WithStore(ms, func(s *store.Store) bool { + for k, v := range entries { + kb := check.MustValue(k.Marshal()) + vb := check.MustValue(json.Marshal(v)) + check.Must(s.SetEntry(kb, vb)) + } + return true + }) + require.True(t, ok) + check.Must(ms.Send(context.Background())) + // check that yesterday's data is sent and data is deleted from the database + queryWithCID, ok := findQuery(sentQueries, yesterday.Date(), cid) + require.Equal(t, true, ok) + query, ok := findQuery(sentQueries, yesterday.Date(), "") + require.Equal(t, true, ok) + keysYesterday := WithStore(ms, func(s *store.Store) [][]byte { + return check.MustValue(s.GetKeysWithPrefix(datePrefix(yesterday.Date()))) + }) + require.Empty(t, keysYesterday) + // check that queries are formed correctly + require.Equal(t, 10, queryWithCID.MapRunCount) + require.Equal(t, 20, query.MapRunCount) + + }) +} + +func findQuery(queries *[]Query, date, cid string) (Query, bool) { + for _, q := range *queries { + if q.Date == date && q.ClusterUUID == cid { + return q, true + } + } + return Query{}, false +} + +func WithTempDir(fn func(string)) { + dir, err := os.MkdirTemp("", "clc-metric-store-*") + if err != nil { + panic(fmt.Errorf("creating temp dir: %w", err)) + } + defer func() { + // errors are ignored + os.RemoveAll(dir) + }() + fmt.Println(dir) + fn(dir) +} + +func WithMetricStore(fn func(ms *metricStore, queries *[]Query)) { + WithTempDir(func(dir string) { + queries := make([]Query, 0) + sendQueriesFn := func(ctx context.Context, url string, q ...Query) error { + queries = q + return nil + } + ms := metricStore{ + increments: make(map[storageKey]int), + updates: make(map[storageKey]int), + mu: &sync.Mutex{}, + sa: store.NewStoreAccessor(dir, log.NopLogger{}), + sendQueriesFn: sendQueriesFn, + } + if err := ms.ensureGlobalMetrics(); err != nil { + panic(fmt.Errorf("setting global metrics: %w", err)) + } + fn(&ms, &queries) + }) +} + +func WithStore[T any](ms *metricStore, fn func(s *store.Store) T) T { + val := check.MustValue(ms.sa.WithLock(func(s *store.Store) (any, error) { + return fn(s), nil + })) + return val.(T) +} diff --git a/clc/metrics/nop_metric_store.go b/clc/metrics/nop_metric_store.go new file mode 100644 index 000000000..1890f5b26 --- /dev/null +++ b/clc/metrics/nop_metric_store.go @@ -0,0 +1,6 @@ +package metrics + +type NopMetricStore struct{} + +func (ms *NopMetricStore) Store(key Key, metric string, val int) {} +func (ms *NopMetricStore) Increment(key Key, metric string) {} diff --git a/clc/metrics/query.go b/clc/metrics/query.go new file mode 100644 index 000000000..2e706d128 --- /dev/null +++ b/clc/metrics/query.go @@ -0,0 +1,31 @@ +package metrics + +type Query struct { + Date string `json:"date"` + ID string `json:"id"` + Architecture string `json:"architecture"` + OS string `json:"os"` + Version string `json:"version"` + AcquisitionSource string `json:"acquisitionSource"` + ClusterUUID string `json:"clusterUUID"` + ViridianClusterID string `json:"viridianClusterID"` + ClusterConfigCount int `json:"clusterConfigCount"` + SQLRunCount int `json:"sqlRunCount"` + MapRunCount int `json:"mapRunCount"` + TopicRunCount int `json:"topicRunCount"` + QueueRunCount int `json:"queueRunCount"` + MultiMapRunCont int `json:"multiMapRunCount"` + ListRunCount int `json:"listRunCount"` + DemoRunCount int `json:"demoRunCount"` + ProjectRunCount int `json:"projectRunCount"` + JobRunCount int `json:"jobRunCount"` + ViridianRunCount int `json:"viridianRunCount"` + SetRunCount int `json:"setRunCount"` + ShellRunCount int `json:"shellRunCount"` + ScriptRunCount int `json:"scriptRunCount"` + TotalRunCount int `json:"totalRunCount"` + AtomicLongRunCount int `json:"atomicLongRunCount"` + InteractiveModeRunCount int `json:"interactiveModeRunCount"` + NoninteractiveModeRunCount int `json:"noninteractiveModeRunCount"` + ScriptModeRunCount int `json:"scriptModeRunCount"` +} diff --git a/clc/metrics/query_generator.go b/clc/metrics/query_generator.go new file mode 100644 index 000000000..de94969b6 --- /dev/null +++ b/clc/metrics/query_generator.go @@ -0,0 +1,84 @@ +package metrics + +import ( + "encoding/json" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc/store" + "github.com/hazelcast/hazelcast-commandline-client/internal/types" +) + +func GenerateFirstPingQuery(ga GlobalAttributes, sa SessionAttributes, t time.Time) Query { + return Query{ + Date: t.Format(DateFormat), + ID: ga.ID, + Architecture: ga.Architecture, + OS: ga.OS, + Version: sa.CLCVersion, + AcquisitionSource: string(sa.AcquisitionSource), + } +} + +type MetricValues map[string]int + +func GenerateQueries(db *store.Store, ga GlobalAttributes, dates types.Set[string]) []Query { + qs := make([]Query, 0, dates.Len()) + for date := range dates.Map() { + entries := make(map[[4]string]MetricValues) + prefix := datePrefix(date) + db.RunForeachWithPrefix(prefix, func(keyb, valb []byte) (bool, error) { + var k storageKey + if err := k.Unmarshal(keyb); err != nil { + return false, err + } + var v int + if err := json.Unmarshal(valb, &v); err != nil { + return false, err + } + attributes := [4]string{k.ClusterID, k.ViridianClusterID, + string(k.AcquisitionSource), k.CLCVersion} + if _, ok := entries[attributes]; !ok { + entries[attributes] = make(MetricValues) + } + entries[attributes][k.MetricName] = v + return true, nil + }) + for attribs, metrics := range entries { + cid := attribs[0] + vid := attribs[1] + acqSource := attribs[2] + version := attribs[3] + d := Query{ + Date: date, + ID: ga.ID, + Architecture: ga.Architecture, + OS: ga.OS, + Version: version, + AcquisitionSource: acqSource, + ClusterUUID: cid, + ViridianClusterID: vid, + ClusterConfigCount: metrics["cluster-config-count"], + SQLRunCount: metrics["sql"], + MapRunCount: metrics["map"], + TopicRunCount: metrics["topic"], + QueueRunCount: metrics["queue"], + MultiMapRunCont: metrics["multimap"], + ListRunCount: metrics["list"], + DemoRunCount: metrics["demo"], + ProjectRunCount: metrics["project"], + JobRunCount: metrics["job"], + ViridianRunCount: metrics["viridian"], + SetRunCount: metrics["set"], + ShellRunCount: metrics["shell"], + ScriptRunCount: metrics["script"], + AtomicLongRunCount: metrics["atomiclong"], + TotalRunCount: metrics["total"], + InteractiveModeRunCount: metrics["interactive-mode"], + NoninteractiveModeRunCount: metrics["noninteractive-mode"], + ScriptModeRunCount: metrics["script-mode"], + } + qs = append(qs, d) + } + } + return qs +} diff --git a/clc/paths/paths.go b/clc/paths/paths.go index 8b5320ca6..a4f4bd56c 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -10,6 +10,7 @@ import ( "time" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" ) const ( @@ -35,6 +36,10 @@ func Configs() string { return filepath.Join(Home(), "configs") } +func Metrics() string { + return filepath.Join(Caches(), fmt.Sprintf("metrics-%s", metrics.MetricsVersion)) +} + func Schemas() string { return filepath.Join(Home(), "schemas") } diff --git a/clc/sql/sql.go b/clc/sql/sql.go index 52b405a9f..75b122bfd 100644 --- a/clc/sql/sql.go +++ b/clc/sql/sql.go @@ -34,7 +34,7 @@ func ExecSQL(ctx context.Context, ec plug.ExecContext, query string) (sql.Result // Once that is removed from the Go client, the code below may be removed. err = adaptSQLError(err) if !as { - if serr.Suggestion != "" && !ec.Interactive() { + if serr.Suggestion != "" && ec.Mode() != plug.ModeInteractive { return nil, fmt.Errorf("%w\n\nUse --%s to automatically apply the suggestion", err, PropertyUseMappingSuggestion) } return nil, err diff --git a/clc/store/store.go b/clc/store/store.go index ce6c3e2ec..ec3341d3d 100644 --- a/clc/store/store.go +++ b/clc/store/store.go @@ -3,6 +3,7 @@ package store import ( "errors" "sync" + "time" "github.com/dgraph-io/badger/v4" @@ -58,6 +59,10 @@ func (s *Store) open() error { return nil } +func (s *Store) close() error { + return s.db.Close() +} + type badgerLogger struct { logger log.Logger } @@ -79,13 +84,22 @@ func (l badgerLogger) Debugf(log string, args ...any) { l.logger.Debugf(log, args...) } -func (s *Store) close() error { - return s.db.Close() +type Option interface { + ModifyEntry(*badger.Entry) } -func (s *Store) SetEntry(key, val []byte) error { +type OptionWithTTL time.Duration + +func (s OptionWithTTL) ModifyEntry(e *badger.Entry) { + e.WithTTL(time.Duration(s)) +} + +func (s *Store) SetEntry(key, val []byte, opts ...Option) error { err := s.db.Update(func(txn *badger.Txn) error { e := badger.NewEntry(key, val) + for _, opt := range opts { + opt.ModifyEntry(e) + } return txn.SetEntry(e) }) return err @@ -132,7 +146,7 @@ func (s *Store) GetKeysWithPrefix(prefix string) ([][]byte, error) { type UpdateFunc = func(current []byte, found bool) []byte -func (s *Store) UpdateEntry(key []byte, f UpdateFunc) error { +func (s *Store) UpdateEntry(key []byte, f UpdateFunc, opts ...Option) error { var item []byte err := s.db.Update(func(txn *badger.Txn) error { it, err := txn.Get(key) @@ -149,6 +163,10 @@ func (s *Store) UpdateEntry(key []byte, f UpdateFunc) error { } } newVal := f(item, found) + e := badger.NewEntry(key, newVal) + for _, opt := range opts { + opt.ModifyEntry(e) + } return txn.Set(key, newVal) }) return err @@ -182,18 +200,10 @@ func (s *Store) RunForeachWithPrefix(prefix string, f ForeachFunc) error { return err } -func (s *Store) DeleteEntriesWithPrefix(prefix string) error { - keys, err := s.GetKeysWithPrefix(prefix) - if err != nil { - return err +func (s *Store) DeleteEntriesWithPrefixes(prefixes ...string) error { + var ps [][]byte + for _, p := range prefixes { + ps = append(ps, []byte(p)) } - err = s.db.Update(func(txn *badger.Txn) error { - for _, key := range keys { - if err := txn.Delete(key); err != nil { - return err - } - } - return nil - }) - return err + return s.db.DropPrefix(ps...) } diff --git a/clc/store/store_test.go b/clc/store/store_test.go index 0e4cd237d..a2ba0b46c 100644 --- a/clc/store/store_test.go +++ b/clc/store/store_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "testing" + "time" badger "github.com/dgraph-io/badger/v4" "github.com/stretchr/testify/require" @@ -25,6 +26,19 @@ func TestStore_GetSetEntry(t *testing.T) { }) } +func TestStore_SettEntryWithTTL(t *testing.T) { + withStore(func(s *Store) { + check.Must(s.SetEntry([]byte("key1"), []byte("val1"))) + check.Must(s.SetEntry([]byte("key2"), []byte("val2"), OptionWithTTL(100*time.Millisecond))) + check.Must(s.SetEntry([]byte("key3"), []byte("val3"), OptionWithTTL(5*time.Second))) + time.Sleep(200 * time.Millisecond) + entries := check.MustValue(getAllEntries(s.db)) + require.Equal(t, entries["key1"], []byte("val1")) + require.NotContains(t, entries, "key2") + require.Equal(t, entries["key3"], []byte("val3")) + }) +} + func TestStore_GetKeysWithPrefix(t *testing.T) { withStore(func(s *Store) { check.Must(insertValues(s.db, map[string][]byte{ @@ -83,11 +97,12 @@ func TestStore_RunForeachWithPrefix(t *testing.T) { func TestStore_DeleteEntriesWithPrefix(t *testing.T) { withStore(func(s *Store) { check.Must(insertValues(s.db, map[string][]byte{ - "prefix.key1": []byte(""), - "prefix.key2": []byte(""), - "noprefix": []byte(""), + "1.prefix.key1": []byte(""), + "1.prefix.key2": []byte(""), + "2.prefix.key3": []byte(""), + "noprefix": []byte(""), })) - check.Must(s.DeleteEntriesWithPrefix("prefix")) + check.Must(s.DeleteEntriesWithPrefixes("1.prefix", "2.prefix")) entries := check.MustValue(getAllEntries(s.db)) expected := map[string][]byte{ "noprefix": nil, diff --git a/cmd/clc/main.go b/cmd/clc/main.go index 31eae77ed..821c0bb47 100644 --- a/cmd/clc/main.go +++ b/cmd/clc/main.go @@ -5,12 +5,15 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/spf13/cobra" clc "github.com/hazelcast/hazelcast-commandline-client/clc" cmd "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/config" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" + "github.com/hazelcast/hazelcast-commandline-client/clc/paths" hzerrors "github.com/hazelcast/hazelcast-commandline-client/errors" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/str" @@ -42,7 +45,10 @@ func main() { } _, name := filepath.Split(os.Args[0]) stdio := clc.StdIO() - m, err := cmd.NewMain(name, cfgPath, cp, logPath, logLevel, stdio) + metrics.CreateMetricStore(paths.Metrics()) + ctx, cancel := context.WithCancel(context.Background()) + startMetricsTicker(ctx) + m, err := cmd.NewMain(name, cfgPath, cp, logPath, logLevel, stdio, metrics.DefaultStore()) if err != nil { bye(err) } @@ -55,6 +61,8 @@ func main() { } } } + cancel() + trySendingMetrics(context.Background()) // ignoring the error here _ = m.Exit() if err != nil { @@ -68,3 +76,41 @@ func main() { } os.Exit(ExitCodeSuccess) } + +func startMetricsTicker(ctx context.Context) { + if !metrics.PhoneHomeEnabled() { + return + } + go func() { + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + metrics.Send(ctx) + cancel() + } + } + }() +} + +func trySendingMetrics(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + storeOtherMetrics() + // try to send the locally stored metrics + // if there is no metric to send, do nothing + metrics.Send(ctx) +} + +func storeOtherMetrics() { + // store cluster config count before sending + cd := paths.Configs() + cs, err := config.FindAll(cd) + if err == nil { + metrics.DefaultStore().Store(metrics.NewSimpleKey(), "cluster-config-count", len(cs)) + } +} diff --git a/docs/modules/ROOT/pages/environment-variables.adoc b/docs/modules/ROOT/pages/environment-variables.adoc index 7c966613e..d60130935 100644 --- a/docs/modules/ROOT/pages/environment-variables.adoc +++ b/docs/modules/ROOT/pages/environment-variables.adoc @@ -39,6 +39,10 @@ |Sets the configuration CLC will use if `--config` flag is not specified. | +|HZ_PHONE_HOME_ENABLED +|Enables phone home collection for CLC usage statistics. +|true + |=== The following environment variables are experimental and may be removed in a future version. diff --git a/go.mod b/go.mod index ddf477395..af1a57b2f 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 // indirect github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cloudflare/circl v1.3.3 // indirect @@ -44,9 +44,9 @@ require ( github.com/go-git/go-billy/v5 v5.4.1 // indirect github.com/go-ole/go-ole v1.2.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.3.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 // indirect github.com/google/flatbuffers v1.12.1 // indirect github.com/gookit/color v1.5.4 // indirect @@ -77,12 +77,13 @@ require ( golang.org/x/term v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect golang.org/x/tools v0.6.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) require ( github.com/apache/thrift v0.14.1 - github.com/dgraph-io/badger/v4 v4.1.0 + github.com/dgraph-io/badger/v4 v4.2.0 github.com/fatih/color v1.13.0 github.com/go-git/go-git/v5 v5.8.1 github.com/mattn/go-colorable v0.1.12 diff --git a/go.sum b/go.sum index 3ee724059..968ab8678 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= @@ -65,8 +65,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v4 v4.1.0 h1:E38jc0f+RATYrycSUf9LMv/t47XAy+3CApyYSq4APOQ= -github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= @@ -97,20 +97,24 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c h1:yE35fKFwcelIte3q5q1/cPiY7pI7vvf5/j/0ddxNCKs= github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c/go.mod h1:9S/fKAutQ6wVHqm1jnp9D9sc5hu689s9AaTWFS92LaU= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -341,6 +345,10 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/http/http.go b/internal/http/http.go index a8a6a1556..22173fb8d 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -3,6 +3,7 @@ package http import ( "bytes" "context" + "io" "net/http" ) @@ -28,3 +29,16 @@ func (c *Client) Get(ctx context.Context, url string) (*http.Response, error) { } return resp, nil } + +func (c *Client) Post(ctx context.Context, url string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, "POST", url, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/internal/it/context.go b/internal/it/context.go index 8c308821e..8ab7b4d0d 100644 --- a/internal/it/context.go +++ b/internal/it/context.go @@ -9,6 +9,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" "github.com/hazelcast/hazelcast-commandline-client/internal/check" "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/output" @@ -98,6 +99,7 @@ type ExecContext struct { props *plug.Properties Rows []output.Row Spinner *Spinner + ms metrics.MetricStorer } func NewExecuteContext(args []string) *ExecContext { @@ -109,6 +111,7 @@ func NewExecuteContext(args []string) *ExecContext { args: args, props: plug.NewProperties(), Spinner: NewSpinner(), + ms: &metrics.NopMetricStore{}, } } @@ -130,8 +133,8 @@ func (ec *ExecContext) ClientInternal(ctx context.Context) (*hazelcast.ClientInt panic("implement me") } -func (ec *ExecContext) Interactive() bool { - return false +func (ec *ExecContext) Mode() plug.Mode { + return plug.ModeNonInteractive } func (ec *ExecContext) AddOutputStream(ctx context.Context, ch <-chan output.Row) error { @@ -143,6 +146,10 @@ func (ec *ExecContext) AddOutputRows(ctx context.Context, rows ...output.Row) er return nil } +func (ec *ExecContext) Metrics() metrics.MetricStorer { + return ec.ms +} + func (ec *ExecContext) Logger() log.Logger { return ec.lg } diff --git a/internal/it/test_context.go b/internal/it/test_context.go index 440b3aef0..0d435db9d 100644 --- a/internal/it/test_context.go +++ b/internal/it/test_context.go @@ -35,6 +35,7 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" "github.com/hazelcast/hazelcast-commandline-client/clc/config" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/clc/shell" "github.com/hazelcast/hazelcast-commandline-client/internal/check" @@ -329,7 +330,7 @@ func (tcx TestContext) createMain() (*cmd.Main, error) { if err != nil { panic(err) } - return cmd.NewMain("clctest", tcx.ConfigPath, fp, tcx.LogPath, tcx.LogLevel, tcx.IO()) + return cmd.NewMain("clctest", tcx.ConfigPath, fp, tcx.LogPath, tcx.LogLevel, tcx.IO(), &metrics.NopMetricStore{}) } func WithEnv(name, value string, f func()) { diff --git a/internal/plug/context.go b/internal/plug/context.go index bfa119f17..bd13e100f 100644 --- a/internal/plug/context.go +++ b/internal/plug/context.go @@ -7,11 +7,20 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/clc/metrics" "github.com/hazelcast/hazelcast-commandline-client/internal/log" "github.com/hazelcast/hazelcast-commandline-client/internal/output" "github.com/hazelcast/hazelcast-commandline-client/internal/types" ) +type Mode int + +const ( + ModeNonInteractive Mode = iota + ModeInteractive + ModeScripting +) + type InitContext interface { AddBoolFlag(long, short string, value bool, required bool, help string) AddCommandGroup(id, title string) @@ -33,6 +42,7 @@ type InitContext interface { type ExecContext interface { AddOutputRows(ctx context.Context, rows ...output.Row) error AddOutputStream(ctx context.Context, ch <-chan output.Row) error + Metrics() metrics.MetricStorer Args() []string GetStringArg(key string) string GetStringSliceArg(key string) []string @@ -41,7 +51,7 @@ type ExecContext interface { ConfigPath() string ClientInternal(ctx context.Context) (*hazelcast.ClientInternal, error) CommandName() string - Interactive() bool + Mode() Mode Logger() log.Logger Props() ReadOnlyProperties ShowHelpAndExit() diff --git a/internal/types/types.go b/internal/types/types.go index 2e57e52b6..12c7ed0cb 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -32,7 +32,14 @@ func (s *Set[K]) Add(item K) { s.m[item] = struct{}{} } -// Has returns true if the given item is in the set. +func (s *Set[K]) Delete(item K) { + delete(s.m, item) +} + +func (s *Set[K]) Map() map[K]struct{} { + return s.m +} + func (s *Set[K]) Has(item K) bool { _, ok := s.m[item] return ok From 3dc90502274e9381f23b7096b58eb69e7f2feedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 26 Sep 2023 14:35:20 +0300 Subject: [PATCH 75/79] 5.3.4 Pre-release updates (#404) 5.3.4 Pre-release updates --- clc/config/config.go | 16 ++++++++-------- extras/windows/hazelcast-icon-flat-sm.png | Bin 1937 -> 4832 bytes .../installer/hazelcast-clc-installer.iss | 2 +- extras/windows/installer/hazelcast_64x64.ico | Bin 5694 -> 5694 bytes extras/windows/installer/post.txt | 3 ++- extras/windows/installer/pre.txt | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/clc/config/config.go b/clc/config/config.go index bf81bb432..d49b51b1d 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -104,14 +104,6 @@ func MakeHzConfig(props plug.ReadOnlyProperties, lg log.Logger) (hazelcast.Confi cfg.Logger.CustomLogger = lg cfg.Cluster.Unisocket = true cfg.Stats.Enabled = true - if ca := props.GetString(clc.PropertyClusterAddress); ca != "" { - lg.Debugf("Cluster address: %s", ca) - cfg.Cluster.Network.SetAddresses(ca) - } - if cn := props.GetString(clc.PropertyClusterName); cn != "" { - lg.Debugf("Cluster name: %s", cn) - cfg.Cluster.Name = cn - } var viridianEnabled bool if vt := props.GetString(clc.PropertyClusterDiscoveryToken); vt != "" { lg.Debugf("Viridan token: XXX") @@ -119,6 +111,14 @@ func MakeHzConfig(props plug.ReadOnlyProperties, lg log.Logger) (hazelcast.Confi cfg.Cluster.Cloud.Token = vt viridianEnabled = true } + if ca := props.GetString(clc.PropertyClusterAddress); ca != "" && !viridianEnabled { + lg.Debugf("Cluster address: %s", ca) + cfg.Cluster.Network.SetAddresses(ca) + } + if cn := props.GetString(clc.PropertyClusterName); cn != "" { + lg.Debugf("Cluster name: %s", cn) + cfg.Cluster.Name = cn + } if props.GetBool(clc.PropertySSLEnabled) || viridianEnabled { sn := "hazelcast.cloud" if !viridianEnabled { diff --git a/extras/windows/hazelcast-icon-flat-sm.png b/extras/windows/hazelcast-icon-flat-sm.png index 73b9346f7b7d0d5bc8619d9769d332598582e912..2d56beb4ce31efca143fa94ee00a61a05e257917 100644 GIT binary patch literal 4832 zcmYj#1ymGVxAwr0Lk);BGzvIFNC?t7ATS^!DS~wK(kb0DbO=h<2uOpJbcd2s(k(Hh zbc2`o|NeFFx6az5!Nkg9je=0bHIZB(5#>#+EA7AzW*!`;qG5S*Z#z@1Oa;L*tdU3&Lz6BVn zfUnqkCk?iKWbsZbr~S<90^qF{)kjAar(6v5%fXmoyXc*@gT-I#stN?Pa@g(LMr zWSDgP=^{(kObyxab+^fz1npJVRpLY*rJEn_J->yAUnb*c(FzG{d9CtQzTGv>57F(L z2rr1pE&ig}8h`qBy}3uXWa8~k)(r~N+*&p0p2n(VN~sdh0+%JT&@yIKC_zS|1oxng z7p&z*PmQrTKy`NB)STW45oDf#r+hB0ico(a(DfQ^*I&L?z(EaazozxmB!@KO2iqDtmic8! z0cf{5-GK}YxMnZ=4gVbN-wiL`Nse#-Aq?4cDWl*=Uf*8}fvJk3JmBxYEvKa*@m@ph zq@?Q#0FXTRS8xDn84v&fe1MRb)$*L(%k=WpK281JwwckCm5vI4JR5GfU)KlyA!-&LkL~?6_e{1#>+e!It!xz*DOw6avQ;T+^J4MO8<2I+XNMDyY}DrSz7Y&?<6-1$AORGePG7HjD)grd>ils zrdj5gW=G}{x@$2`sJY5_Mf^jyr~t%i3vz0gf&GwS2RhI?^#{It^dX)1bKzTg=ZMh# z7B+paPoZP2RPyTerROmHSh}kVell?(*mMhPaErM1MVfAJ0dR&9PybquzWn+FP)=c1 zU5h)vVnnJU)_#ZD?rpj8R1#5cHqHqXi%B;G>d-O}L4IlKO|+m@VYWptd$ai&+{w-O zoT>>}zvlpDx}p&VH7OTyoD@BvTl8WNKUL{cOO5aIGL@DvP>}tQLI*FQ1P&~LRx+jW zz@&WPYu7(?aeF1VdVhXzGO1>-9Jzk4$hS!>9m_BRc`?Sr?#Diul8%xBu3EXzAKmp+ zz&&@7ycSQt6wOzxjH(Y^IGKRal-bR6uIMu__Z$CWin*yuwWp)5w|b8xL@U>) zQ_3BRrw%U+tT+TBQp;>vQ}p+HiXK$nb}D9KY~zYk{Qu}?6pryCpzs99$w0&x92}$7 z0`A5$@ZnK{om0*~G#0&$LlbPJgo9QI$@2xjonx6jLuRf388y69;(xO(s2o z=&vqNwk>EIME2{G7-y?`rEODWao6FF_&N>f?bqi}&Z1`|3u*;sN0~#;eS?04Gkh-5 z4}{~Ya?PyD6O}FKw%zRX%8a|7<1g)N|4t|}GD?kVMw+gM-tX+yEtHqDHR5g#ri|;r zQD?SmF06cA8LOm79*}E#7z`FF)5!TAf8KvJ$LHwvEkE|N4Th|%z52J=4uHJ%S5RNI zxxRX*T?Vn1j&+K(Kp?MF)M9G7qA1^W+3rQ03wMA3b?qRdc$DYLPhHQ$Tkif0S?XkE z&&w{mL4Ph1FVyHM>RYM$LEmRJUxAam1xg8z0^#Bpw9*jEu4q`m=frkb)d+8Ecw9To{ypbRs0g7olB+4X{y2?sUil}t`c{h zlZLhZ#&vR32Tv-f#>ctXw6b{RjYR(dg=7e`@U45%wq5Gb2KO-NG#GV7zU-I|btoJ^ z+%K7=;eYL`(Ce3|AI;JLGb^a)Td|ora(DbmTqB-1{beNT4*XR@-0i14&HaPPV-n~{ zgUU)D+|3)i!L!B6O>W1&<_Cya7c#~Tx(VoYQzvF6I^Pa?s7(1bvdvp2IcQ_!?jThY zl!IK8*(+air zj{$XWURQYY5@qWM^PYV9WTHZ^=Lm859_Qf7C&tE!j`LXVWllebQsW6#Xbh6HjKcT0 zm2tncE}Lj6y^^>RugibD7&Fkd5n3);yjt>CpiOT($wfR;G*i_yPm!0nOCkN|n2$$s zVaez$jrXR4t!A(p)Kx2_JB;wlTl(I)26Y=F?1K0p$-?{i7!?`BJgjLJy*QO}ZgcKY zYma#`)V`RxF%a{?EF&5>f{XQ!(4UDcMoMhsXyeGk>p^J|b#P$1;tVYgl}_oz=;Bl0 zr|a5D)VC_4BA?W5XDh1rpr-I70@_$6YdLIM*T(q(DV^!>g4a}?Ihc)r%M#96aAV_` zFPdlXH3ne{Yng3+_u`@V4xCxq60-(d!4e=y7gUCl8EU^zOrp+w?nevWRsK&SRwA=D z67!eldSPKpowJ*z4CTeF5gle7WdZsX0YM|OZ4J^-hyuq|?@66}@W$WO;9AXrXLmv3 zVVcW^vI%_hl|B3dH@}H()NMG3Bx)nslB^jA3M+*T2)~v6)7=|4y!@k;<>S}b*i1%< zuaA&I9Iw3p=0wJ&gr9!Lwsdyq-|}P3p*bFpuKd;uhv<4~3gN}cjwAA&k9SWp{^q2v z7|^{kSaSjHa2;zFV-(Nu`F~r5fkFN83_9&A!)REf+9z;lkS5XPKZgfx?j6%xFF8)} z{e0d$lQLAJc#q^~*vFmTMPeJ#PVFQw^h0*ZK^vtoIp>Or*?NhpywQ`Ol60ioU@7VC zUb6oyMlW#IP05FaqUFxJzy;zkC{0!~)5XE3mO}AIy*2of39YJVoSU;&(Cz|Hj@dZ| z%sz+jo;Sn+guzGN#Sw>`o2?jK<=6#goK+&P%MC^fOlGkmKl+&g$xvo=xzP!GIF*YS zMRx%2so6MG6|q$QH$^8*@*SBujXqEo{LVg^s|4WUA0Xj;EgOm}hC;ead6$t3DO!oF zF+k9x8zf+s_w}IT0&S>!dZ#SopJpdqmql&9K##fgkK!`%ps#Kn2vFv>@0B5|or?|@ z?XYd(>IFWEDinh#&kF6$uAwxCgN&=w9g~~5CR>(dFq+<4asne-6*5%~##r=LGWJJD zkU?vn{nTCC6{WiCv)aCmc;_lkJk}va{qaW4>G_<_m4Et7->7Mer)~erv)A!c#S6w( z&hs>l6D-j_obeI@Wk$+V1A}4hUm>1d;GEFJI$(ZiOxr?1tq-;QVUsO~8_4YRqXsL! ze{xLWzQC=Or+~cm!ZtplqS^#IIrj<2Jbc6^BsE?$5e%5AG+jE#Hh#rzsoZ<~J~Wt|-PZ*rL6e~jFd zQ>TGJs`uEZie}hJLMyT=I(teA{C_>s|MLl?`XBs{PyglPf74t^*m>Zf9sJT0SO6YA zM@$rs?-kxVq8E=+)v(m;b-854pLJV@SJXZ%&1BW=ZBGo5V~f{!Cc+~#eCbh0ZMO74 za(oYnUbmj5B9UMDF`IqU)#7rHBKf<4;o{&<2PDx}X3lsUy7d-=J3VUm_`sxIQs!K0 zKhJ*rv~I0aEtrqn+$L-!TAhOCD;nwEi{uK{Jn0C>Q=PVJFPbu&yhS!Ce}3Q6cqcCuWSc{@^8NbGPcax$U|DIRlS!cc|gWuOH# zP)iaz;YAg0uOozrart&aG$cfxQIM}g%_<=c*%HEETc1m_)8Aa81*Q+wS;_avWQgft*nhsOt}{Aahv0LF<2r3< z;n^;U$L!grIbVh1Ccl&aBg+)sEX^!^;q0LN&Bh$T3YT^Lv*7mGYy+e^_6}R9Ox^{) zh)4{jDl?-Y+7lGj$PXpMib=aAK-^k-_^W@xOW;lB9cy)jM1+@XDhZHaf1o9EDW?@l z8A+za3inC%5Oo?GLEBXGJC`@~a37b!E^I$#OCX3IKAPv`c`yfVO!mUO|C2Mq#_!v(aI^hmRinrq z<6~%Hh%N4-quYM#5r3g!RFZoZ?p5J^<{*c?Pr|EQhA6BZ4Ih}`De2ENEAZbgW=P;j zUVnRB0|P-kb1zRxcal;6#@)#PJ439j+#a|`NI`VCrH^}QU)}kFesYXWis44N>oEyq zXOLWLwr?BsB%YF7MYz&aHv9vNv&rF2&ek~ym(M)3+7Huo63@3BwlNT^TEbdbzRX4v z5yMI-Xs2u2I$!Vcl`OL@ux`OQXTKl!p2Cui^|FsNrEse(%zOG1ZnUx4_?H-Op7E zNC&gG>fBT>yUO28G1fNHZQUUpL3}*FIS5_f1#q{o#w1<7Ikf`vsKNJ)xzXJ!h*F!BltIF(A zx@VR|QJb!?xSFBsWB7&nhuR2HQI+-(&AG*v#V;e5nJ;f#8c(4_EtjT&8i{W%jSYz$ z+sysUcc%o*-MFc8Y3Km{Qj0MOtqJ)7c93@ZHVWeoM__4D-;%B6Q2V^qjpuPs;MTJ@ zo3I_|%CPfZa{C_0z8Xhh!?I)l>tdjVBbslIW-`)0Axm&#%X4+VI%$9u;3N`Mu;I;u zfbYVAGTAsn63+!13!L|7A9Y;3`)NG>PpqaD=lv=2-9Zg^XJ{81vlacJx#ur7CLsm~ zIjLtUmt4Hx%47Wduw-3(DM!uG`xaEW88{4*g(i1{fYy1c7J;sS?RHnkl%*DXgms(F z_lc(A-kMuw>L8O`Mo;5N5(}L<$f@AkvJTy&pA@D8dhA~|>rNWpbe0Ubtn^V)u?hkN zf9<4bEk^(TZkqjYS3ssSoF7R_zb9it!*rg|Kfydz=~Wwi%FLCN6!=Y{jtie#e+I`3 zHQD@?TM#h8q<#3dfhMM4b!vu-hm!w3eYUu@hw)IV>vMMOQO1Aj`X0;vK&m1r@TZI_ z3SF$s_|T|ix|TIKO+*Txr7bZK#pU`;62I+{v^wC!?kfLCwe~8$1IYL%9sm z1Q8J{((cClNAoG>v7&O>KO6HOH;Z*qpad4XaXkq64s~hGo_24>f2k*XHt!KfLUPA7 zbtEX-r{8~t@bq&>&YQ>DqNT!$ak(DhS7O(f*|+55C%O`!E$h7Qt9Q*9pKv>~czDG? z<}SAA_bl*rawz3Wsj%!oTD17S>ni5E#oBx1V6$9jsFdP32P4-a*~)KeR7#})buxXu zSt-f6LNS}fDM7%q2e zLgxNUtXY*V_%7*KDA;#bQDZp?r%o|&4!E|4fN?%j*MExfNe#_vVL#dB^82IfN(&Wy kd>E=Se;+B?bT0#Tx?omuL>Uk7KSThCmq__?xN-3R0Fh}i1poj5 literal 1937 zcmZWqc~nzp7H1KutQHGFZ2%9H9Lrv|Ne}}Hln4Y0C@7YQYb^>TtPPECs7@IM6eP8w z5QvrtWk6KQCR@PDK-e;b*g#pHN=SGiB!v*tge1%h&hgAS^T+qTd+xp8y}$drzjIzu z4E|GdlLIDtdV1!#@Q_&e9@d@rcEjICiOn_ewdbqw)0ujDduh6(?>vt2(bF?NjtdEn z&!er*7i7$a?KO(|3&nt2K(i>f5Swt0%>|zgcD8mxPP-3SCn6J(PU3!~ljE`PcU3ZW ziS-lbdi`tnd|D2f=dzuK4R0iH(-fDsjZ!O8v^QMU%(bpRGy}GSLbkyJ=Ru*g@VugR zR`boepKD5;(xoG)OEUiB?2}uk1&{d#O8MCJ#OJOu#XpC8#W}f;HCnVv5T4pucv4Gv_62N#Q z>72HX`9cJyYE{oc?ZfWAzG`*uzzLcE>VAet+E@+Mstq91?k%77j^}?vZcS&;(``%)af0;DAQdk!UE-hb*6XN93k4Rod0r=~ ztR}$5t+)>es3S8FsNu=wv%A_nseD-iu{$7sD8h9;TxqrPp)u5Scqv$QctLO}>MAG* z{0-<_Pk8WVA(zJk5M3T;o^aGNUp~)i@&x=`THG4^UF=FcIB)LLjD+kxao6HR`|2bc z!0Ro9T999S^Nmd<(<;GNNqp_rc`zxo;^mku;d~>jy9j&J>T)&nPXh@1g);9A%AWk* zu=sKfmTGrti$fr_dhi9T+%#7U8}CK+y*Y_i`F2?e#DY26GxLb)e)Ve3_A2v!xzeTj zv*pg4zSeZC>8;8S=*;#B9Cgud>eV9ej9F`is6wDhEr^*~M4`x^pe0_nH#;8EvS1_X ze~jDX|L}|Zm(Lt{ipW{&9P*h3Rhc`+0%-HrGY@v{c$U)#shW(_KiO*L8@*AV-^nl0 z9{pL$sdc}7pq6Qv{dv#tgKZZ!6ajKRV>F|55~Q@--*nNyY=F5dT7*~6kZaa9c zb}`?1+yh58?XhSm7_QpR1C!9uA}gbGCKkZn>zevuXX@qj#LXS9%r`sQY`+*ndUXtp zO2(q_q%tEb*{asX1r42~EY{qyT|S$P+D4q&3Dox4q9ICyM;M+|LEEsE4-VrjV+kd+ z4c4_&C7VIgV(SGi!Vwi1MD`NRQYZ^?V4Nub6{oh8%WIHU z6en0VM3W7PoI-AWpT0guxKsa7cG!~!X%L=tXx4Hqo{%kP;plj>7m<^)((@vgu5#xJ z9W-Byitkd6-t9R2u0D~2vbo%NOI1gqB>z=)p6l;`>aFjq=PLU5#0AQcabOKCOI}^q z7l?*L-7w5|W9*1xtMNEA~CURskgfUmRHT$^E`|HjWk!D$un<_rTcz=I| zSnf-uB$EpE%E}+@a}77ka!wULJ0Sd68Q}`kKeICGPEgCrV18P7)M}U=G?-1H*f|Ni z(_o;#%rvL@xqQb`HdnzJ{BL+L`xP{47d&a?ot*g%-X%cdMj+&2a!j%zvv8B*=Y zt>Txazi>c(#ACZJ^Y1Q8bNLL*@dP+6zg(|cHo~P=Ye#UFm8Wzb2Ne0Ks$~jRB$CK^ zY*k~Hxy4rtd*qMEYXaf2G=58EVGQk$uI<5O1{9Cg*Vs*gYn#4b7oBYB6K~{Zku5lM zp+g|N!;l5Rco49}h>q!lBmb{dFEkWbym(kces!y%W?$VMNLoYNXcLX%0R^9N1^l?h z4-KUd`&OJ$fguyQ^Lz#Z-hXx`H0c0SGMfyfFEL7~y?x?#E*Sk#5|78U9k~G${$JX> zMs>7ZEa1WnE%D2Sd=!7SSsPwwpN!NFMdLAN9Z<+0XiQky4OO(PM3VOR>85bF$FCS! z0)WTBZ!K2F3G-+OevfGi0I~&)IDa%$c0nuZ%JbEQp9#{p^f9{f58uD?Vi>nQv3dNH zB3N4^e#GG-+)*Hjg{y~mM(B@d`i+IdvVt)ng(A70oE(J5@T;*&QPUQ1!BnkiY$xx~ zkosj61+T-PlJSnc#gIBqk0@<{Rh7b3vv7r`G878g?hg=$Pg7`Fo#F5tF*(|<<}Lfb Ym+1|S&>vvnf2P2!!QgpfC36)hFfOhmRVMcOYTA@ZY`RaDNBml zYMCMoDq<=MCW?+Jm+@nr)-Uy;I&B|PJE!cq!$I%GINZ@w=XmmkAJ04gdH?Ud_n8a# zc>sFR*N73Ip8+sL0Q3z5K@v6)qTbz5_Xae50CiuABK-g@!Te}W=m@PpkVr7vMn~w0 z#Qqcm0ZwEf9Zw_(;DdXCAKV+}A$?&E?FTNr53FH+u!j3XA4#u?#7IAy`_Y_;BBK3Z ziwvab4+CL~8UQXP07gO=Lwalg(H|xvH6{pVRUkB~AXrobkQzG(NpSySE>2g@|Ukj2B0O>wvh_FNl`c~%%l zCBvAXLfB!PGz`YcnJ^Y)g7=aIeo7XMQ?g;4nhoB|aPZTJIb&d&I~IK5SeTbjhIs|C zvH+}P7TDU^(3UQMwrmM>gsFTfEQGFN87!5{px;#l-L7Joh?L#M;OaIZ`OrorAKnPt zVVWP=46eQu$@QCI^HK`iQKDfBY{$01d}14nC%3~)7#k@zmce|g9QJKO2`0*qhKQ+Sf^C{vq(tja-6={O=w`QdMeimuh&LV?IyVin?>omXKg7oX> zkbdJFGKlmW=aK%$d1U-~5gC77#L$}*Z@MYEDPHRCSL?pe1`TciY!7yRh#CHB&y8esDpyLqkcFa>BUt&2K%LLCl#(0-6zRtBSx)0WSJc}61Y3|f~iA_0-E%tckF*ZiQ zZFA?&ZDSal@9`8eHnGc_?meaNU6yN?-z|Nsi_|`mvBH1yUwI{-#ch%MUyf(-uU?UB z-TBY@QD*0UEM0u^Ns1|SEq>&;Ti@(jq;)($?zMiQ`YHEb z>Zi5$R=)|`sIu50%ay_xm1Havgo6}TT~g?gEOQr8{aQoy^YXi|pI1`-EbJBQ_d}AM z6-^I`_4{72foNS#SMZ#V8+iMN67leYaO7dyV8$*Va^S9-PL}-67Hq%RKdDyjaGg<5 z2)8x6DvPOiVEWGfNe~W*XF&Hsu9E&Ox1)c{-|s#7jvq%?``SO29G2KAiTjFu=wDS! w_I9LBvPbK|ZR-}z>#=<01+56vlrD6hSO*6suNop(|C8QHW|A-y==1b52a@}q_dWM!a_*UPC*URjmMufR4`s%4 zApe;mUy`Cwz7&2S^YVfrL%`b`e0;#y7ySId-yenzgTO!-J{*FAV8jR*H3~+LhB0Fx zI9Pt3{J@Y97&{imjf3&yVZsEMGzmgOVagPkIu)i(gRn4|K3!g)yug_=Av_#r&4Ssp zVa^Z5wRg4m)aokeUir^CU6kdXnInQ-V396k(LS#abC zWM{+Cqj2mP96t^xPC!l$C44gd+=g!GJErj#u;lc$d zDuRm_p|}`IO5pNkC@qC6SHNU~vNE`O6|P^08#kc59B$r(TeqO10&d?1vl%KY;m#eX zs)FijxO*3BYM{0j?%jj?_o1#1>g%Dg5gt5%rY3m!5FR~($B&`88CqJPwH2N`fwney z`V=}lp{onJyTM|Co*wA!1*;WoHV^{de;+>h06zQ>KKckg{un;_1U~%~KKl$l{~W&f z0>1nbzWNHj{u;jd2EP3kzWWZo{~mt$0e<`ue)&qj`a(Zn;V3%RYdyv z$5hJ&?ynf;l-xnS?Y-CReqygB$z70-Yz^Y7uR;ai7I(+>)tsJpO|FWs7-}rOS)CQp z9^;bo&dQ&#Jxk!t$oQ;}YB`5Qu2ym7t?0`{oDH(3R2(|~a--J)_iS|f@xsm%1Z zRq#ypXyZf&6Wvi6Xh#ztHchmHfXWWFk}p}+j2^XWNzyM9EjOtwShADlCf%7LL2#Mu zNXi2K`Msijt5QFEJ4JW;Gy190KUnsjQX7qh*~P62XerJvlncs=GG!IiD79rNKbe19 zX;L=xDdQFfY%oiyt2lh<;%hRe;fov;P*IyVMcPK5dg{7plzI0u^vc1IBUP3n~|<^--T?D&v!&g>Tk?aXce`Y}oB^ znqHhFpUoY1Lj#5}tmUeM^SxL5tWM48S?*wxEU7w4b0?XFHBWH@O+sAiyTHy)(HNm< zueFNywF=PNoNU>(gw?!xPTGA5L~UxQ7BYP0HF-3Ow$o8diaI6eDvt5fH0Kz5qDe}& zihb)3-IGAnW~h}9&!?JXYZC3JwkBTakf5t5A;>w;1twPtoBiJKmDg`_6o96tbc=>4 z#aZ=4w8v_!Zk*BT%-_3?8_^YzZ*jCX5-odNtPY*iGLyMir!d*Mf8}0!&FRNF?t1wP zb*ht7f%giHiPwN}%zM?y8JJQ7qY=T*`$uCujH^52Jg|1kBzm+1w7(Eut``sNg`|&o zA-+;JnV$2ndfHkw*gHJ)k=+00tl7o~%~d5zX^6(WnfjGoeqfSWDWu-2w7%8th*;Pl zZ1(=?nqWk!UBZ%Xr0vYYjywG*ZgiF{$Ia^tO9t3mXJpk#C)9)0TK~}t?*1lsXAlo& NU@!xN8Tj8B_!r0W4Fv!I diff --git a/extras/windows/installer/post.txt b/extras/windows/installer/post.txt index 2826408d2..a260547a4 100644 --- a/extras/windows/installer/post.txt +++ b/extras/windows/installer/post.txt @@ -3,7 +3,8 @@ You can launch the CLC from the Hazelcast group in the Start menu. Useful links: -* Hazelcast CLC documentation: https://docs.hazelcast.com/hazelcast/latest-dev/clients/clc +* Hazelcast CLC documentation: https://docs.hazelcast.com/hazelcast/latest/clients/clc +* Get started with Viridian: https://docs.hazelcast.com/cloud/get-started * Get Started with Hazelcast: https://docs.hazelcast.com/hazelcast/latest/getting-started/get-started-cli * Hazelcast CLC Survey - please complete the survey to share your feedback: https://forms.gle/rPFywdQjvib1QCe49 diff --git a/extras/windows/installer/pre.txt b/extras/windows/installer/pre.txt index d3dc5eed9..6ac983563 100644 --- a/extras/windows/installer/pre.txt +++ b/extras/windows/installer/pre.txt @@ -1,4 +1,4 @@ -Welcome to the Hazelcast CLC Install Wizard. +Welcome to the Hazelcast CLC Installation Wizard. The wizard will guide you through installing Hazelcast CLC on your computer. You can cancel the installation on any screen. -Click on Next to continue with the installation. +Click on "Next" to continue with the installation. From 14c0c4ebd3e5ec48005275e5349335f4f211ff13 Mon Sep 17 00:00:00 2001 From: Muhammet Yazici <44066377+mtyazici@users.noreply.github.com> Date: Tue, 26 Sep 2023 14:37:46 +0300 Subject: [PATCH 76/79] Add phone home documentation (#405) --- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/environment-variables.adoc | 2 +- docs/modules/ROOT/pages/phone-homes.adoc | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 docs/modules/ROOT/pages/phone-homes.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 462a283e7..6fe04c131 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -36,6 +36,7 @@ * xref:configuration-format.adoc[] * xref:environment-variables.adoc[] * xref:keyboard-shortcuts.adoc[] +* xref:phone-homes.adoc[] .Release Notes * xref:release-notes-5.3.3.adoc[5.3.3] diff --git a/docs/modules/ROOT/pages/environment-variables.adoc b/docs/modules/ROOT/pages/environment-variables.adoc index d60130935..ebd2f7bcc 100644 --- a/docs/modules/ROOT/pages/environment-variables.adoc +++ b/docs/modules/ROOT/pages/environment-variables.adoc @@ -40,7 +40,7 @@ | |HZ_PHONE_HOME_ENABLED -|Enables phone home collection for CLC usage statistics. +|Enables phone home collection for CLC usage statistics. Set 'false' to disable phone homes. |true |=== diff --git a/docs/modules/ROOT/pages/phone-homes.adoc b/docs/modules/ROOT/pages/phone-homes.adoc new file mode 100644 index 000000000..239fa0964 --- /dev/null +++ b/docs/modules/ROOT/pages/phone-homes.adoc @@ -0,0 +1,25 @@ += Phone Homes + +CLC uses phone home data to learn about the usage of CLC. + +CLC instances collect usage metrics over a 24-hour period and send phone home data every 24 hours. + +== What is sent in? + +- ID for CLC Home installation +- Name of the operating system +- Name of the architecture +- CLC version +- Acquisition source (e.g Docker, Management Center) +- Connected Hazelcast Cluster ID +- Connected Viridian Cluster ID +- Number of times commands are run. Command runs are stored separately for each command type. +- Number of created CLC configurations + +== Disabling Phone Homes + +Phone homes can be disabled by setting the following environment variable. + +``` +export HZ_PHONE_HOME_ENABLED=false +``` From 9360752e96fe9e57769e440e88bb44a8aeb6829f Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Tue, 26 Sep 2023 16:32:25 +0300 Subject: [PATCH 77/79] [CLC-215]: Serializer Generator Documentation (#406) --- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/clc-serializer-generator.adoc | 139 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 docs/modules/ROOT/pages/clc-serializer-generator.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 6fe04c131..4b435f114 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -33,6 +33,7 @@ ** xref:clc-version.adoc[] ** xref:clc-viridian.adoc[] ** xref:clc-project.adoc[] +** xref:clc-serializer-generator.adoc[] * xref:configuration-format.adoc[] * xref:environment-variables.adoc[] * xref:keyboard-shortcuts.adoc[] diff --git a/docs/modules/ROOT/pages/clc-serializer-generator.adoc b/docs/modules/ROOT/pages/clc-serializer-generator.adoc new file mode 100644 index 000000000..3e2435ef5 --- /dev/null +++ b/docs/modules/ROOT/pages/clc-serializer-generator.adoc @@ -0,0 +1,139 @@ += clc serializer generator (Beta) + +Serializer commands are a group of serializer generator operations. + +Check out the documentation for https://docs.hazelcast.com/hazelcast/latest/serialization/compact-serialization[Compact Serialization] for more information. + +Usage: + +[source,bash] +---- +clc serializer [command] [flags] +---- + +== Commands + +* <> + +== clc serializer generate + +Generates compact serialization code from the given schema for the target language. See https://docs.hazelcast.com/hazelcast/latest/serialization/compact-serialization#implementing-compactserializer[Compact Serialization] documentation for more information. + +Usage: + +[source, bash] +---- +clc serializer generate [schema] [flags] +---- + +Parameters: + +[cols="1m,1a,2a,1a"] +|=== +|Parameter|Required|Description|Default + +|`output-dir`, `-o` +|Optional +|Output directory for the serialization files. +|Current working directory. + +|`language`, `-l` +|Required +|Programming language that the serializer created for. +| +|=== + +Example: + +[source,bash] +---- +clc serializer generate my-schema.yaml --language java --output-dir my-dir +---- + +=== Schema Creation + +A schema allows you to: + +* describe the contents of a compact class using supported field types +* import other schema +* specify a namespaces for schema files and reference other namespaces +* define cyclic references between classes +* reference classes that are not present in the given schemas + +A schema is written in YAML. Schema format is given below: + +[source,yaml] +---- +namespace: +# note that other schema files can be imported with relative path to this yaml file +imports: + - someOtherSchema.yaml +# All objects in this file will share the same namespace. +classes: + - name: + fields: + - name: + type: + external: bool # to mark external types (external to this yaml file) +---- + +==== namespace + +Used for logical grouping of classes. Typically, for every namespace, you will have a schema file. Namespace is optional. If not provided, the classes will be generated at global namespace (no namespace). The user should provide the language specific best practice when using the namespace. The tool will use the namespace while generating code if provided. + +==== imports + +Used to import other schema files. The type definitions in the imported yaml schemas can be used within this yaml file. Cyclic imports will be checked and handled properly. For this version of the tool, an import can only be a single file name and the tool will assume all yaml files imported will be in the same directory as the importing schema file. + +==== classes + +Used to define classes in the schema. + +* name: Name of the class +* fields: Field specification of the class +** name: Name of the field +** type: Type of the field. Normally you should refer to another class as namespace.classname. You can use a class without namespace when the class is defined in the same schema yaml file. type can be one of the following: +*** `boolean` +*** `boolean[]` +*** `int8` +*** `int8[]` +*** `int16` +*** `int16[]` +*** `int32` +*** `int32[]` +*** `int64` +*** `int64[]` +*** `float32` +*** `float32[]` +*** `float64` +*** `float64[]` +*** `string` +*** `string[]` +*** `date` +*** `date[]` +*** `time` +*** `time[]` +*** `timestamp` +*** `timestamp[]` +*** `timestampWithTimezone` +*** `timestampWithTimezone[]` +*** `nullableBoolean` +*** `nullableBoolean[]` +*** `nullableInt8` +*** `nullableInt8[]` +*** `nullableInt16` +*** `nullableInt16[]` +*** `nullableInt32` +*** `nullableInt32[]` +*** `nullableInt64` +*** `nullableInt64[]` +*** `nullableFloat32` +*** `nullableFloat32[]` +*** `nullableFloat64` +*** `nullableFloat64[]` +*** `` +** external: +*** Used to mark if the type is external. If a field is external, the tool will not check if it is imported and available. External types are managed by the user and not generated by the tool. +*** The serializer of an external field can be a custom serializer which is handwritten, the zero-config serializer for Java and .NET, or previously genereated using the tool. This flag will enable such mixed use cases. +*** In generated code, external types are imported exactly what as the "type" of the field, hence for languages like Java the user should enter the full package name together with the class. E.g. type: `com.app1.dto.Address`. + From d15e441e9d0066cbd751c89f771da304462fdd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 26 Sep 2023 17:10:18 +0300 Subject: [PATCH 78/79] Fixed TestViridian/deleteCluster_NonInteractive (#407) --- base/commands/viridian/viridian_it_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index ee7292dec..526b36f05 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -75,7 +75,7 @@ func loginWithParams_NonInteractiveTest(t *testing.T) { tcx.Tester(func(tcx it.TestContext) { ctx := context.Background() tcx.CLCExecute(ctx, "viridian", "login", "--api-base", "dev2", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) - tcx.AssertStdoutContains("Viridian token was fetched and saved.") + tcx.AssertStdoutContains("Saved the access token") }) } From 63940d8e7a226e372857e88362cbc4f2e2637539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Tue, 26 Sep 2023 18:28:22 +0300 Subject: [PATCH 79/79] Added 5.3.4 release notes (#408) * Added release notes for 5.3.4 * Fixed Viridian tests --- base/commands/viridian/viridian_it_test.go | 21 +++++++-------- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/release-notes-5.3.4.adoc | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 docs/modules/ROOT/pages/release-notes-5.3.4.adoc diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index 526b36f05..d33528016 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -132,12 +132,12 @@ func listClusters_InteractiveTest(t *testing.T) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { tcx.WriteStdin([]byte("\\viridian list-clusters\n")) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") }) c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.WithReset(func() { tcx.WriteStdin([]byte("\\viridian list-clusters\n")) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") tcx.AssertStdoutContains(c.ID) }) }) @@ -166,8 +166,7 @@ func createCluster_InteractiveTest(t *testing.T) { tcx.WriteStdinf("\\viridian create-cluster --development --verbose --name %s \n", clusterName) time.Sleep(10 * time.Second) check.Must(waitState(ctx, tcx, "", "RUNNING")) - tcx.AssertStdoutContains(fmt.Sprintf("Imported configuration: %s", clusterName)) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") require.True(t, paths.Exists(paths.ResolveConfigDir(clusterName))) _ = check.MustValue(tcx.Viridian.GetClusterWithName(ctx, clusterName)) @@ -180,7 +179,7 @@ func stopCluster_NonInteractiveTest(t *testing.T) { viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.CLCExecute(ctx, "viridian", "stop-cluster", c.ID) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") check.Must(waitState(ctx, tcx, c.ID, "STOPPED")) }) } @@ -192,7 +191,7 @@ func stopCluster_InteractiveTest(t *testing.T) { tcx.WithReset(func() { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.WriteStdinf("\\viridian stop-cluster %s\n", c.Name) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") check.Must(waitState(ctx, tcx, c.ID, "STOPPED")) }) }) @@ -215,7 +214,7 @@ func resumeCluster_InteractiveTest(t *testing.T) { tcx.WithReset(func() { c := createOrGetClusterWithState(ctx, tcx, "STOPPED") tcx.WriteStdinf("\\viridian resume-cluster %s\n", c.Name) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") check.Must(waitState(ctx, tcx, c.ID, "RUNNING")) }) }) @@ -240,7 +239,7 @@ func getCluster_InteractiveTest(t *testing.T) { tcx.WithReset(func() { c := createOrGetClusterWithState(ctx, tcx, "") tcx.WriteStdinf("\\viridian get-cluster %s\n", c.Name) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") tcx.AssertStdoutContains(c.Name) tcx.AssertStdoutContains(c.ID) }) @@ -269,7 +268,7 @@ func deleteCluster_InteractiveTest(t *testing.T) { tcx.WriteStdinf("\\viridian delete-cluster %s\n", c.Name) tcx.AssertStdoutContains("(y/n)") tcx.WriteStdin([]byte("y")) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") require.Eventually(t, func() bool { _, err := tcx.Viridian.GetCluster(ctx, c.ID) return err == nil @@ -287,7 +286,7 @@ func downloadLogs_NonInteractiveTest(t *testing.T) { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.WithReset(func() { tcx.CLCExecute(ctx, "viridian", "download-logs", c.ID, "--output-dir", dir) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") require.FileExists(t, paths.Join(dir, "node-1.log")) }) }) @@ -303,7 +302,7 @@ func downloadLogs_InteractiveTest(t *testing.T) { tcx.WithReset(func() { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") tcx.WriteStdinf("\\viridian download-logs %s -o %s\n", c.Name, dir) - tcx.AssertStderrContains("OK") + tcx.AssertStdoutContains("OK") require.FileExists(t, paths.Join(dir, "node-1.log")) }) }) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 4b435f114..e1f65fe28 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -40,6 +40,7 @@ * xref:phone-homes.adoc[] .Release Notes +* xref:release-notes-5.3.4.adoc[5.3.4] * xref:release-notes-5.3.3.adoc[5.3.3] * xref:release-notes-5.3.2.adoc[5.3.2] * xref:release-notes-5.3.1.adoc[5.3.1] diff --git a/docs/modules/ROOT/pages/release-notes-5.3.4.adoc b/docs/modules/ROOT/pages/release-notes-5.3.4.adoc new file mode 100644 index 000000000..d24343941 --- /dev/null +++ b/docs/modules/ROOT/pages/release-notes-5.3.4.adoc @@ -0,0 +1,26 @@ += 5.3.4 Release Notes + +== New Features + +* Added the `serializer generate` command that creates Java Compact Serialization classess from the given schema. +* Added an update checker which displays a notification in the interactive mode. You can disable it by setting the `CLC_SKIP_SERVER_VERSION_CHECK` to `1`. +* In order to make an informed decision about CLC improvements and features, we started to collect non-identifiable telemetry. You can disable this by setting `HZ_PHONE_HOME_ENABLED` environment variable to `false`. You can find more information in the xref:phone-homes.adoc[documentation]. +* CLC can now be installed using an install script on Linux and macOS systems. Just run `curl https://hazelcast.com/clc/install.sh | bash`. + +== Improvements + +* Improved configuration selector. +* If there's a single configuration, do not display the configuration selector and use that configuration automatically. +* Updated `job submit` command to deduce the job ID. + +== Changes + +* Confirmation is required from the user when `demo generate-data` command runs. + +== Fixes + +* CLC interactive prompt doesn't change after config wizard. +* Readline panic in the interactive mode if terminal width cannot be determined and left arrow is pressed. +* Map proxy is created when a `map` command is used. +* "connecting to cluster" prompt is not shown when `object list` command runs. +* Fixed `list add` command.