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 01/48] 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 60a009cc..785a35e6 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 02/48] 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 b7c728bb..f9e562ed 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 03/48] [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 8b5e5bca..456272a7 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 4e6bf447..6f7fde6e 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 1a30a5e5..ba2476b5 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 00000000..265418ab --- /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 04/48] [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 85d58a9a..16eccb4c 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 05/48] 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 b1b2ca05..28d2f66e 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -17,6 +17,6 @@ asciidoc: page-toclevels: 3@ # Required Go version for build go-version: 1.19 - page-latest-supported-mc: '5.3.2-snapshot' + page-latest-supported-mc: '5.4-snapshot' nav: - modules/ROOT/nav.adoc \ No newline at end of file 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 06/48] 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 cc88fe9d..18e49ca1 100644 --- a/docs/check-links-playbook.yml +++ b/docs/check-links-playbook.yml @@ -32,7 +32,7 @@ asciidoc: # Separate anchor link names by dashes idseparator: '-' page-survey: https://www.surveymonkey.co.uk/r/NYGJNF9 - hazelcast-cloud: Hazelcast Viridian + hazelcast-cloud: Viridian Cloud extensions: - ./tabs-block.js - asciidoctor-kroki diff --git a/docs/modules/ROOT/pages/clc-job.adoc b/docs/modules/ROOT/pages/clc-job.adoc index c0923bc1..b40bcdf0 100644 --- a/docs/modules/ROOT/pages/clc-job.adoc +++ b/docs/modules/ROOT/pages/clc-job.adoc @@ -25,7 +25,7 @@ clc job [command] [options] Creates a Jet job using the provided Jar file. -This command requires a {hazelcast-cloud} or Hazelcast cluster of version 5.3.0 or above. +This command requires a Hazelcast {hazelcast-cloud} or Hazelcast Platform cluster of version 5.3.0 or above. Usage: @@ -250,7 +250,7 @@ Parameters: == clc job export-snapshot -Exports a snapshot from a Jet job. This feature requires a {hazelcast-cloud} or Hazelcast Enterprise cluster. +Exports a snapshot from a Jet job. This feature requires a Hazelcast {hazelcast-cloud} or Hazelcast Enterprise cluster. Usage: diff --git a/docs/modules/ROOT/pages/clc-multimap.adoc b/docs/modules/ROOT/pages/clc-multimap.adoc index 6ef3407b..9122d0f1 100644 --- a/docs/modules/ROOT/pages/clc-multimap.adoc +++ b/docs/modules/ROOT/pages/clc-multimap.adoc @@ -25,7 +25,7 @@ clc multi-map [command] [flags] == clc multi-map put -Put a value in the given MultiMap +Put a value in the given MultiMap. Usage: @@ -120,7 +120,7 @@ Parameters: == clc multi-map remove -Remove values from the given multi-map. +Remove values from the given MultiMap. Usage: diff --git a/docs/modules/ROOT/pages/clc-project.adoc b/docs/modules/ROOT/pages/clc-project.adoc index 8d1bd436..16466409 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 9a8df97a..e134e664 100644 --- a/docs/modules/ROOT/pages/clc-queue.adoc +++ b/docs/modules/ROOT/pages/clc-queue.adoc @@ -19,7 +19,7 @@ clc queue [command] [flags] == clc queue clear -Delete all entries of a Queue. +Delete all entries of a queue. Usage: @@ -49,7 +49,7 @@ clc queue clear --name my-queue == clc queue offer -Add a value to the given Queue. +Add a value to the given queue. Usage: @@ -88,7 +88,7 @@ clc queue offer --value-type f32 19.94 19.92 --name my-queue == clc queue poll -Remove the given number of elements from the given Queue. +Remove the given number of elements from the given queue. Usage: @@ -134,7 +134,7 @@ clc queue poll --count 2 --name my-queue 5 == clc queue size -Return the size of the given Queue. +Return the size of the given queue. Usage: diff --git a/docs/modules/ROOT/pages/clc-set.adoc b/docs/modules/ROOT/pages/clc-set.adoc index 6fc19d24..cc803f66 100644 --- a/docs/modules/ROOT/pages/clc-set.adoc +++ b/docs/modules/ROOT/pages/clc-set.adoc @@ -20,7 +20,7 @@ clc set [command] [flags] == clc set clear -Deletes all entries of a Set. +Deletes all entries of a set. Usage: @@ -56,7 +56,7 @@ clc set clear --name my-set == clc set add -Adds values to the given Set. +Adds values to the given set. Usage: @@ -123,7 +123,7 @@ clc set get-all --name my-set == clc set remove -Removes values from the given Set. +Removes values from the given set. Usage: @@ -159,7 +159,7 @@ clc set remove 1 2 3 4 --name my-set == clc set size -Returns the size of the given Set. +Returns the size of the given set. Usage: @@ -190,7 +190,7 @@ clc set size --name my-set == clc set destroy -Destroys a Set. This command will delete the Set and the data in it will not be available anymore. +Destroys a set. This command will delete the set and the data in it will not be available anymore. Usage: diff --git a/docs/modules/ROOT/pages/clc-snapshot.adoc b/docs/modules/ROOT/pages/clc-snapshot.adoc index d7cd049e..e76bb742 100644 --- a/docs/modules/ROOT/pages/clc-snapshot.adoc +++ b/docs/modules/ROOT/pages/clc-snapshot.adoc @@ -203,7 +203,7 @@ Parameters: == clc job export-snapshot -Exports a snapshot from a Jet job. Note that this feature requires Viridian or Hazelcast Enterprise. +Exports a snapshot from a Jet job. This feature requires Hazelcast {hazelcast-cloud} or Hazelcast Enterprise. Usage: diff --git a/docs/modules/ROOT/pages/clc-topic.adoc b/docs/modules/ROOT/pages/clc-topic.adoc index 3db5062f..36b9a6be 100644 --- a/docs/modules/ROOT/pages/clc-topic.adoc +++ b/docs/modules/ROOT/pages/clc-topic.adoc @@ -17,7 +17,7 @@ clc topic [command] [flags] == clc topic publish -Publish new messages for a Topic. +Publish new messages for a topic. Usage: @@ -57,7 +57,7 @@ clc topic publish -v string string1 string2 --name topic1 == clc topic subscribe -Subscribe to a Topic for new messages. +Subscribe to a topic for new messages. Usage: @@ -88,9 +88,7 @@ clc topic subscribe --name topic1 == clc topic destroy -Destroy a Topic - -This command will delete the Topic and the data in it will not be available anymore. +This command will delete the topic and the data in it will not be available anymore. Usage: diff --git a/docs/modules/ROOT/pages/clc-viridian.adoc b/docs/modules/ROOT/pages/clc-viridian.adoc index b1bf6383..a8c7def9 100644 --- a/docs/modules/ROOT/pages/clc-viridian.adoc +++ b/docs/modules/ROOT/pages/clc-viridian.adoc @@ -1,6 +1,6 @@ = clc viridian -This command group provides commands for doing various {hazelcast-cloud} operations, such as creating and managing clusters. +This command group provides commands for doing various operations on Hazelcast {hazelcast-cloud}, such as creating and managing clusters. All commands except `viridian login` require the generation of a token using an API key and secret, which you can retrieve from the {hazelcast-cloud} console. Running `viridian login` prompts you for this information, and creates and saves the token. @@ -233,7 +233,7 @@ Parameters: == clc viridian delete-cluster -Deletes the given {hazelcast-cloud} cluster. Note that, all data in the cluster is deleted irreversibly. +Deletes the given {hazelcast-cloud} cluster. All data in the cluster is deleted irreversibly. Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. @@ -333,7 +333,7 @@ Parameters: == clc viridian import-config -Imports connection configuration of the given {hazelcast-cloud} cluster. +Imports the connection configuration of the given {hazelcast-cloud} cluster. Make sure you authenticate to the {hazelcast-cloud} API using `viridian login` before running this command. diff --git a/docs/modules/ROOT/pages/connect-to-viridian.adoc b/docs/modules/ROOT/pages/connect-to-viridian.adoc index c7042a45..09ae1e94 100644 --- a/docs/modules/ROOT/pages/connect-to-viridian.adoc +++ b/docs/modules/ROOT/pages/connect-to-viridian.adoc @@ -1,5 +1,5 @@ -== Connecting to {hazelcast-cloud} with Hazelcast CLC -:description: To use the Hazelcast CLC with Hazelcast {hazelcast-cloud}, you need to authenticate with {hazelcast-cloud} using your API secret and key. You can then import the configuration of the {hazelcast-cloud} cluster that you want to connect to. No additional configuration is required. +== Connecting to Hazelcast {hazelcast-cloud} with Hazelcast CLC +:description: To use the Hazelcast CLC with Hazelcast {hazelcast-cloud}, you need to authenticate with {hazelcast-cloud} using your API secret and key. You can then import the configuration of the cluster that you want to connect to. No additional configuration is required. :page-product: cloud @@ -18,7 +18,7 @@ You need the following: To allow the Hazelcast CLC to do cluster operations, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + ```bash clc viridian login @@ -54,6 +54,6 @@ clc viridian import-config $CLUSTER-NAME --name dev clc -c dev ``` -CLC will start in the interactive mode, and you should see a command prompt. You're ready to start managing xref:clc-viridian.adoc[{hazelcast-cloud} clusters]. +The Hazelcast CLC will start in interactive mode, and you should see a command prompt. You're ready to start managing xref:clc-viridian.adoc[{hazelcast-cloud} clusters]. NOTE: The Hazelcast CLC connects to the cluster on demand, that is when you issue a command that requires the connection, such as running a SQL query. \ No newline at end of file diff --git a/docs/modules/ROOT/pages/environment-variables.adoc b/docs/modules/ROOT/pages/environment-variables.adoc index 1e93bb01..24fefaeb 100644 --- a/docs/modules/ROOT/pages/environment-variables.adoc +++ b/docs/modules/ROOT/pages/environment-variables.adoc @@ -8,7 +8,7 @@ |Environment Variable|Description|Default |CLC_CLIENT_LABELS -|Sets the client labels. The labels must be separated by commas. This value is used in {hazelcast-cloud} and Management Center. See xref:{page-latest-supported-mc}@management-center:clusters:clients.adoc[the Management Center documentation] for more information. +|Sets the client labels. The labels must be separated by commas. This value is used in {hazelcast-cloud} and Management Center. See xref:{page-latest-supported-mc}@management-center:clusters:clients.adoc[Management Center documentation] for more information. |`CLC,USER@HOST-TIMESTAMP` |CLC_CLIENT_NAME diff --git a/docs/modules/ROOT/pages/get-started.adoc b/docs/modules/ROOT/pages/get-started.adoc index c8b9321f..c18f4222 100644 --- a/docs/modules/ROOT/pages/get-started.adoc +++ b/docs/modules/ROOT/pages/get-started.adoc @@ -1,5 +1,5 @@ -= Get Started With the Hazelcast CLC -:description: In this tutorial, you'll learn how to use Hazelcast CLC commands to authenticate with Hazelcast {hazelcast-cloud} and create two production clusters. You'll connect to and switch between clusters from the command line. Finally, you'll perform some basic operations on a cluster from both the command line and by running a script to automate the same actions. += Get Started with the Hazelcast CLC +:description: In this tutorial, you'll learn how to use Hazelcast CLC commands to authenticate with Hazelcast {hazelcast-cloud} and create two production clusters. You'll connect to and switch between the clusters from the command line. Finally, you'll perform some basic operations on a cluster from both the command line and by running a script to automate the same actions. {description} @@ -8,13 +8,14 @@ You need the following: - xref:install-clc.adoc[Hazelcast CLC] installed on your local machine. +- xref:cloud:ROOT:create-account.adoc[{hazelcast-cloud} account] - xref:cloud:ROOT:developer.adoc[{hazelcast-cloud} API key and secret] == Step 1. Authenticating with {hazelcast-cloud} To allow the Hazelcast CLC to to interact with {hazelcast-cloud} clusters, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + [source,shell] ---- @@ -23,7 +24,7 @@ clc viridian login . When prompted, enter your API key and secret. If both are correct, the token is retrieved and saved. -== Step 2. Create Two {hazelcast-cloud} Clusters +== Step 2. Create Two Clusters on {hazelcast-cloud} In this step, you'll create two production clusters called Test1 and Test2 from the command line, and run some commands to check their status. @@ -51,7 +52,7 @@ clc viridian create-cluster --name Test2 clc viridian list-clusters ---- + -The details of all clusters linked to your Hazelcast {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. +The details of all clusters linked to your {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. [[step-2-prod-configure]] diff --git a/docs/modules/ROOT/pages/install-clc.adoc b/docs/modules/ROOT/pages/install-clc.adoc index 5cb1e31c..fffa57b6 100644 --- a/docs/modules/ROOT/pages/install-clc.adoc +++ b/docs/modules/ROOT/pages/install-clc.adoc @@ -204,7 +204,7 @@ Windows:: . Right-click on it and select *Uninstall*. . Press kbd:[Yes] on the uninstallation dialog. -Release Packagae:: +Release Package:: + Delete the `hazelcast-commandline-client` directory. ==== diff --git a/docs/modules/ROOT/pages/jet-job-management.adoc b/docs/modules/ROOT/pages/jet-job-management.adoc index 439cf072..cad6dc09 100644 --- a/docs/modules/ROOT/pages/jet-job-management.adoc +++ b/docs/modules/ROOT/pages/jet-job-management.adoc @@ -1,5 +1,5 @@ = Get Started With Hazelcast Jet Job Management -:description: In this tutorial, you'll learn the basics of how to manage stream-processing pipelines using the Hazelcast CLC with Hazelcast {hazelcast-cloud}. You'll see how to connect to a {hazelcast-cloud} cluster and submit a sample Jet job that generates a stream of numbers. You'll also learn how to use simple commands to monitor and cancel jobs. +:description: In this tutorial, you'll learn the basics of how to manage stream processing pipelines using the Hazelcast CLC with Hazelcast {hazelcast-cloud}. You'll see how to connect to a cluster on {hazelcast-cloud}, and submit a sample Jet job that generates a stream of numbers. You'll also learn how to use simple commands to monitor and cancel jobs. {description} @@ -8,7 +8,7 @@ You need the following: - xref:install-clc.adoc[Hazelcast CLC] installed on your local machine -- One running Hazelcast {hazelcast-cloud} cluster. You can create a xref:managing-viridian-clusters.adoc#creating-a-cluster-on-viridian[{hazelcast-cloud} development cluster] using the Hazelcast CLC. +- One running {hazelcast-cloud} cluster. You can create a xref:managing-viridian-clusters.adoc#creating-a-cluster-on-viridian[development cluster] using the Hazelcast CLC. - xref:cloud:ROOT:developer.adoc[{hazelcast-cloud} API key and secret] - JRE 8 or above. - Gradle 8 or above. @@ -18,7 +18,7 @@ You need the following: To allow the Hazelcast CLC to perform cluster operations, including submitting Jet jobs, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + [source,shell] ---- @@ -37,7 +37,7 @@ Next, check that the Hazelcast CLC can access your cluster by running the follow clc viridian list-clusters ---- -The details of all clusters linked to your Hazelcast {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. +The details of all clusters linked to your {hazelcast-cloud} account are returned, including the Cluster ID, Cluster Name, Current Status, Hazelcast Version. [[step-3-dev-configure]] == Step 3. Connect to Your Cluster @@ -182,7 +182,7 @@ clc -c dev job cancel simple-pipeline In this tutorial, you learned how to do the following: -* Connect to a Hazelcast cluster. +* Connect to a cluster on {hazelcast-cloud}. * Build and submit a Hazelcast Jet job to create a data pipeline. * Manage the lifecycle of a Jet job using list and cancel commands. diff --git a/docs/modules/ROOT/pages/managing-viridian-clusters.adoc b/docs/modules/ROOT/pages/managing-viridian-clusters.adoc index 0cb2dc3d..7840e3af 100644 --- a/docs/modules/ROOT/pages/managing-viridian-clusters.adoc +++ b/docs/modules/ROOT/pages/managing-viridian-clusters.adoc @@ -1,6 +1,6 @@ -= Managing Viridian Clusters Using the Hazelcast CLC += Managing Clusters on Hazelcast Viridian Cloud Using the Hazelcast CLC -:description: In this tutorial, you'll learn the basics of managing {hazelcast-cloud} clusters using the Hazelcast CLC. You'll see how to create, list, and delete clusters, and how to download their logs. You'll also learn how to perform pause/resume operations on {hazelcast-cloud} clusters using the Hazelcast CLC. +:description: In this tutorial, you'll learn the basics of managing clusters on Hazelcast {hazelcast-cloud} using the Hazelcast CLC. You'll see how to create, list, and delete clusters, and how to download their logs. You'll also learn how to perform pause/resume operations on the clusters using the Hazelcast CLC. {description} @@ -17,7 +17,7 @@ You need the following: To allow the Hazelcast CLC to perform cluster operations, you must generate a {hazelcast-cloud} token. -. Execute the following command to retrieve the {hazelcast-cloud} token. +. Execute the following command to retrieve the token. + [source, bash] ---- diff --git a/docs/modules/ROOT/pages/overview.adoc b/docs/modules/ROOT/pages/overview.adoc index a2dcb0e6..73b36cf9 100644 --- a/docs/modules/ROOT/pages/overview.adoc +++ b/docs/modules/ROOT/pages/overview.adoc @@ -1,10 +1,10 @@ = Hazelcast Command-Line Client (CLC) :url-github-clc: https://github.com/hazelcast/hazelcast-cloud-cli/blob/master/README.md -:description: You can use the Hazelcast Command Line Client (CLC) to connect to and interact with clusters on {hazelcast-cloud} and Hazelcast Platform direct from the command line or through scripts. +:description: You can use the Hazelcast Command Line Client (CLC) to connect to and interact with clusters on Hazelcast {hazelcast-cloud} and Hazelcast Platform direct from the command line or through scripts. {description} -The Hazelcast CLC is a single binary with no dependencies. Within minutes of installation, you can start to perform common tasks on {hazelcast-cloud} and Hazelcast clusters. +The Hazelcast CLC is a single binary with no dependencies. Within minutes of installation, you can start to perform common tasks on clusters. == Install @@ -24,7 +24,7 @@ xref:clc-viridian.adoc[Create and manage] {hazelcast-cloud} clusters, and the cu === Create Data Pipelines -xref:clc-job.adoc[Create and manage] data pipelines using the Hazelcast CLC. Check out xref:hazelcast:pipelines:overview.adoc[Platform documentation] for more information about data pipelines. +xref:clc-job.adoc[Create and manage] data pipelines using the Hazelcast CLC. Check out the xref:hazelcast:pipelines:overview.adoc[Platform documentation] for more information about data pipelines. === Access Data for Debugging diff --git a/docs/modules/ROOT/pages/release-notes-5.2.0.adoc b/docs/modules/ROOT/pages/release-notes-5.2.0.adoc index fe69b700..3f47a2a2 100644 --- a/docs/modules/ROOT/pages/release-notes-5.2.0.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.2.0.adoc @@ -2,13 +2,13 @@ == New Features -* CLC can now read data serialized using Compact Serialization and Portable automatically. +* Hazelcast CLC can now automatically read data serialized using compact and portable serialization. * Added the ability to select a configuration from a list or import a {hazelcast-cloud} configuration when a configuration is not provided in the shell mode. -* Added the `--quite` (shorthand `-q`) flag which suppresses unnecessary outputs. CLC outputs can be sometimes noisy, such as success message logs; you can use this flag for a more quiet output. +* Added the `--quite` (shorthand `-q`) flag which suppresses unnecessary outputs. Hazelcast CLC outputs can be sometimes noisy, such as success message logs; you can use this flag for a more quiet output. * Added the `CLC_CLIENT_NAME` environment variable which allows overriding the default client name. * Added the `CLC_CLIENT_LABELS` environment variable which allows overriding the default client labels with a comma separated list of labels. -* Added support for link:https://hazelcast.com/products/viridian/[{hazelcast-cloud} Serverless]. +* Added support for link:https://hazelcast.com/products/viridian/[{hazelcast-cloud} Standard]. * Added the following commands: @@ -42,7 +42,7 @@ * Removed `map get-all`, `map put` and `map put-all` commands. * Added the `map set` command. * Auto-completion is disabled in interactive mode. -* {hazelcast-cloud} Serverless is the default cloud platform. +* {hazelcast-cloud} Standard is the default cloud platform. * The shell connects to the cluster on demand. == Known issues diff --git a/docs/modules/ROOT/pages/release-notes-5.2.1.adoc b/docs/modules/ROOT/pages/release-notes-5.2.1.adoc index 347e1bbe..e22fe2e9 100644 --- a/docs/modules/ROOT/pages/release-notes-5.2.1.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.2.1.adoc @@ -2,9 +2,9 @@ == Changes -* Corrected the --quite flag to --quiet. -* Uses the experimental NY readline library on Windows by default. That fixes arrow key related issues but disables syntax highlight for SQL -* Use stderr for unnecessary output. +* Corrects the `--quite` flag to `--quiet`. +* Uses the experimental NY readline library on Windows by default. This update fixes arrow key related issues but disables syntax highlighting for SQL. +* Uses stderr for unnecessary output. == Improvements * More consistent success messages when a list doesn't have any items. @@ -12,5 +12,5 @@ == Fixes * Fixed a race in shell command. -* Fixed a bug that would cause a panic if the SQL command is interrupted.* -* Powershell completion is fixed +* Fixed a bug that would cause a panic if the SQL command was interrupted. +* Powershell completion is fixed. diff --git a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc index 240a856a..7847b744 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-1.adoc @@ -2,7 +2,7 @@ == New Features -* CLC can now submit Jet jobs and manage job snapshots. +* Hazelcast CLC can now submit Jet jobs and manage job snapshots. * Added the following `job` commands: ** `submit`: Creates a job from the given jar file. ** `cancel`: Cancels a job. @@ -11,6 +11,6 @@ ** `resume`: Resumes a suspended job. ** `restart`: Restarts a job. ** `export-snapshot`: Exports a snapshot for a job. This feature requires a {hazelcast-cloud} or Hazelcast Enterprise cluster. -* Added the following `snapshot` commandS: +* Added the following `snapshot` commands: ** `list`: Lists the snapshots of a job. ** `delete`: Deletes a snapshot. diff --git a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc index 230690ce..4008a7bb 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.0-BETA-2.adoc @@ -2,7 +2,7 @@ == New Features -* {hazelcast-cloud} support. The following commands were added: +* Support for Hazelcast {hazelcast-cloud}. The following commands were added: ** `viridian login` ** `viridian create-cluster` ** `viridian delete-cluster` diff --git a/docs/modules/ROOT/pages/release-notes-5.3.0.adoc b/docs/modules/ROOT/pages/release-notes-5.3.0.adoc index 5e768834..a3ea8283 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.0.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.0.adoc @@ -1,7 +1,7 @@ = 5.3.0 Release Notes == New Features -* {hazelcast-cloud} support. The following commands were added: +* Support for Hazelcast {hazelcast-cloud}. The following commands were added: ** `viridian login` ** `viridian create-cluster` ** `viridian delete-cluster` @@ -16,7 +16,7 @@ ** `viridian download-custom-class` ** `viridian list-custom-classes` ** `viridian upload-custom-class` -* CLC can now submit Jet jobs and manage job snapshots. +* Hazelcast CLC can now submit Jet jobs and manage job snapshots. * Added the following `job` commands: ** `submit`: Creates a job from the given jar file. ** `cancel`: Cancels a job. diff --git a/docs/modules/ROOT/pages/release-notes-5.3.1.adoc b/docs/modules/ROOT/pages/release-notes-5.3.1.adoc index c14a2edb..53de998b 100644 --- a/docs/modules/ROOT/pages/release-notes-5.3.1.adoc +++ b/docs/modules/ROOT/pages/release-notes-5.3.1.adoc @@ -2,7 +2,7 @@ == New Features -* Added the `--timeout` flag which causes CLC to exit with status code 2 if an operation cannot be completed in the given time. +* Added the `--timeout` flag which causes Hazelcast CLC to exit with status code 2 if an operation cannot be completed in the given time. == Improvements 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 07/48] Disabled verbose test output (#347) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7e3dc234..3e636a0e 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ GIT_COMMIT = $(shell git rev-parse HEAD 2> /dev/null || echo unknown) CLC_VERSION ?= v0.0.0-CUSTOMBUILD MAIN_CMD_HELP ?= Hazelcast CLC LDFLAGS = -s -w -X 'github.com/hazelcast/hazelcast-commandline-client/clc/cmd.MainCommandShortHelp=$(MAIN_CMD_HELP)' -X 'github.com/hazelcast/hazelcast-commandline-client/internal.GitCommit=$(GIT_COMMIT)' -X 'github.com/hazelcast/hazelcast-commandline-client/internal.Version=$(CLC_VERSION)' -X 'github.com/hazelcast/hazelcast-go-client/internal.ClientType=CLC' -X 'github.com/hazelcast/hazelcast-go-client/internal.ClientVersion=$(CLC_VERSION)' -TEST_FLAGS ?= -v -count 1 -timeout 30m -race +TEST_FLAGS ?= -count 1 -timeout 30m -race COVERAGE_OUT = coverage.out PACKAGES = $(shell go list ./... | grep -v internal/it | tr '\n' ',') BINARY_NAME ?= clc 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 08/48] 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 307aeade..18e55ec5 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 c6b5397c..6c02d583 100644 --- a/clc/config/provider.go +++ b/clc/config/provider.go @@ -9,7 +9,7 @@ import ( "github.com/hazelcast/hazelcast-go-client" "github.com/spf13/pflag" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" @@ -66,7 +66,7 @@ func (p *FileProvider) load(path string) error { if err != nil { return fmt.Errorf("reading configuration: %w", err) } - m := map[any]any{} + m := map[string]any{} if err := yaml.Unmarshal(b, m); err != nil { return fmt.Errorf("loading configuration: %w", err) } @@ -185,20 +185,15 @@ func (p *FileProvider) clientConfig() (hazelcast.Config, bool) { return hazelcast.Config{}, false } -func (p *FileProvider) traverseMap(root string, m map[any]any) { - for k, v := range m { - // skip if the key is not a string - ks, ok := k.(string) - if !ok { - continue - } +func (p *FileProvider) traverseMap(root string, m map[string]any) { + for ks, v := range m { var r string if root == "" { r = ks } else { r = strings.Join([]string{root, ks}, ".") } - if mm, ok := v.(map[any]any); ok { + if mm, ok := v.(map[string]any); ok { p.traverseMap(r, mm) continue } diff --git a/go.mod b/go.mod index 8e1dc3e4..f26031a6 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/alecthomas/chroma v0.10.0 github.com/gohxs/readline v0.0.0-20171011095936-a780388e6e7c github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230809052932-73bc747e32b9 + github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230818105346-0389b1c5849c github.com/mattn/go-runewidth v0.0.14 github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d github.com/spf13/cobra v1.7.0 @@ -81,7 +81,6 @@ require ( golang.org/x/text v0.11.0 // indirect golang.org/x/tools v0.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( @@ -94,5 +93,5 @@ require ( github.com/go-git/go-git/v5 v5.8.1 github.com/mattn/go-colorable v0.1.12 github.com/nyaosorg/go-readline-ny v0.9.1 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 5ff34d84..fa82f07d 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230809052932-73bc747e32b9 h1:V1jVTVLL6BXU+yv2zWrfhhTcQ5oXDH+Q06umd2Z0HB8= -github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230809052932-73bc747e32b9/go.mod h1:PJ38lqXJ18S0YpkrRznPDlUH8GnnMAQCx3jpQtBPZ6Q= +github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230818105346-0389b1c5849c h1:V3Hhid/bU2mrR51+FG/FfmwyAEmq8gEFNRxfY/UerEw= +github.com/hazelcast/hazelcast-go-client v1.4.1-0.20230818105346-0389b1c5849c/go.mod h1:PJ38lqXJ18S0YpkrRznPDlUH8GnnMAQCx3jpQtBPZ6Q= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -331,8 +331,6 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/it/test_context.go b/internal/it/test_context.go index 6e02f7c9..cdb2121b 100644 --- a/internal/it/test_context.go +++ b/internal/it/test_context.go @@ -30,7 +30,7 @@ import ( "time" hz "github.com/hazelcast/hazelcast-go-client" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/cmd" diff --git a/internal/it/util.go b/internal/it/util.go index 14746c49..5827ff30 100644 --- a/internal/it/util.go +++ b/internal/it/util.go @@ -34,7 +34,7 @@ import ( hz "github.com/hazelcast/hazelcast-go-client" "github.com/hazelcast/hazelcast-go-client/logger" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/check" 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 09/48] 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 df96f15d..21a9b883 100644 --- a/base/commands/config/config_add.go +++ b/base/commands/config/config_add.go @@ -66,6 +66,12 @@ func (cm AddCmd) Exec(_ context.Context, ec plug.ExecContext) error { if ec.Interactive() || ec.Props().GetBool(clc.PropertyVerbose) { I2(fmt.Fprintf(ec.Stdout(), "Created configuration at: %s\n", filepath.Join(dir, cfgPath))) } + mopt := config.ConvertKeyValuesToMap(opts) + // ignoring the JSON path for now + _, _, err = config.CreateJSON(target, mopt) + if err != nil { + ec.Logger().Warn("Failed creating the JSON configuration: %s", err.Error()) + } return nil } diff --git a/base/commands/config/config_import.go b/base/commands/config/config_import.go index 5f648a1d..a6047a99 100644 --- a/base/commands/config/config_import.go +++ b/base/commands/config/config_import.go @@ -19,20 +19,16 @@ func (cm ImportCmd) Init(cc plug.InitContext) error { short := "Imports configuration from an arbitrary source" long := `Imports configuration from an arbitrary source -Currently importing only Viridian connection configuration is supported. +Currently importing Viridian connection configuration is supported only. 1. On Viridian console, visit: - Dashboard -> Connect Client -> Quick connection guide -> Python + Dashboard -> Connect Client -> CLI -2. Copy the text in box 1 and pass it as the second parameter. +2. Copy the URL in box 2 and pass it as the second parameter. Make sure the text is quoted before running: - clc config import my-config "curl https://api.viridian.hazelcast.com ... default.zip" - -Alternatively, you can use an already downloaded Python client sample: - - clc config import my-config /home/me/Downloads/hazelcast-cloud-python-sample....zip + clc config import my-config "https://api.viridian.hazelcast.com/client_samples/download/..." ` cc.SetCommandHelp(long, short) diff --git a/base/commands/config/config_it_test.go b/base/commands/config/config_it_test.go index fa16e118..7a591775 100644 --- a/base/commands/config/config_it_test.go +++ b/base/commands/config/config_it_test.go @@ -32,7 +32,7 @@ func TestConfig(t *testing.T) { func importTest(t *testing.T) { tcx := it.TestContext{T: t} - const configURL = "https://rcd-download.s3.us-east-2.amazonaws.com/hazelcast-cloud-python-sample-client-pr-FOR_TESTING-default.zip" + const configURL = "https://rcd-download.s3.us-east-2.amazonaws.com/hazelcast-cloud-clc-sample-client-pr-FOR_TESTING-default.zip" tcx.Tester(func(tcx it.TestContext) { name := it.NewUniqueObjectName("cfg") ctx := context.Background() diff --git a/base/commands/demo/demo_it_test.go b/base/commands/demo/demo_it_test.go index 91a78a0b..3212d07d 100644 --- a/base/commands/demo/demo_it_test.go +++ b/base/commands/demo/demo_it_test.go @@ -29,6 +29,7 @@ func TestGenerateData(t *testing.T) { } func generateData_WikipediaTest(t *testing.T) { + it.MarkFlaky(t, "https://github.com/hazelcast/hazelcast-commandline-client/issues/350") it.MapTester(t, func(tcx it.TestContext, m *hz.Map) { t := tcx.T ctx := context.Background() diff --git a/base/commands/viridian/common.go b/base/commands/viridian/common.go index 546167b7..2fb231e3 100644 --- a/base/commands/viridian/common.go +++ b/base/commands/viridian/common.go @@ -137,18 +137,20 @@ func waitClusterState(ctx context.Context, ec plug.ExecContext, api *viridian.AP func tryImportConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName string) (configPath string, err error) { cpv, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Importing configuration") - zipPath, stop, err := api.DownloadConfig(ctx, clusterID, "python") + cfgPath, ok, err := importCLCConfig(ctx, ec, api, clusterID, cfgName) if err != nil { - return nil, err + ec.Logger().Error(err) + } else if ok { + return cfgPath, err } - defer stop() - cfgPath, err := config.CreateFromZip(ctx, ec, cfgName, zipPath) + ec.Logger().Debugf("could not download CLC configuration, trying the Python configuration.") + cfgPath, ok, err = importPythonConfig(ctx, ec, api, clusterID, cfgName) if err != nil { return nil, err } cfgDir, _ := filepath.Split(cfgPath) // import the Java/.Net certificates - zipPath, stop, err = api.DownloadConfig(ctx, clusterID, "java") + zipPath, stop, err := api.DownloadConfig(ctx, clusterID, "java") if err != nil { return nil, err } @@ -173,6 +175,28 @@ func tryImportConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API return cp, nil } +func importCLCConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName string) (configPath string, ok bool, err error) { + return importConfig(ctx, ec, api, clusterID, cfgName, "clc", config.CreateFromZip) +} + +func importPythonConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName string) (configPath string, ok bool, err error) { + return importConfig(ctx, ec, api, clusterID, cfgName, "python", config.CreateFromZipLegacy) +} + +func importConfig(ctx context.Context, ec plug.ExecContext, api *viridian.API, clusterID, cfgName, language string, f func(ctx context.Context, ec plug.ExecContext, target, path string) (string, bool, error)) (configPath string, ok bool, err error) { + zipPath, stop, err := api.DownloadConfig(ctx, clusterID, language) + if err != nil { + return "", false, err + } + defer stop() + cfgPath, ok, err := f(ctx, ec, cfgName, zipPath) + if err != nil { + return "", false, err + } + return cfgPath, ok, nil + +} + // importFileFromZip extracts files matching selectPaths to targetDir // Note that this function assumes a Viridian sample zip file. func importFileFromZip(ctx context.Context, ec plug.ExecContext, selectPaths *types.Set[string], zipPath, targetDir string) (imported *types.Set[string], err error) { diff --git a/base/commands/viridian/download_logs_it_test.go b/base/commands/viridian/download_logs_it_test.go index 7720caec..32576650 100644 --- a/base/commands/viridian/download_logs_it_test.go +++ b/base/commands/viridian/download_logs_it_test.go @@ -1,44 +1,3 @@ //go:build std || viridian package viridian_test - -import ( - "context" - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/hazelcast/hazelcast-commandline-client/clc/paths" - "github.com/hazelcast/hazelcast-commandline-client/internal/check" - "github.com/hazelcast/hazelcast-commandline-client/internal/it" -) - -func downloadLogs_NonInteractiveTest(t *testing.T) { - viridianTester(t, func(ctx context.Context, tcx it.TestContext) { - dir := check.MustValue(os.MkdirTemp("", "log")) - defer func() { check.Must(os.RemoveAll(dir)) }() - c := createOrGetClusterWithState(ctx, tcx, "RUNNING") - tcx.WithReset(func() { - tcx.CLCExecute(ctx, "viridian", "download-logs", c.ID, "--output-dir", dir) - tcx.AssertStderrContains("OK") - require.FileExists(t, paths.Join(dir, "node-1.log")) - }) - }) -} - -func downloadLogs_InteractiveTest(t *testing.T) { - viridianTester(t, func(ctx context.Context, tcx it.TestContext) { - dir := check.MustValue(os.MkdirTemp("", "log")) - defer func() { check.Must(os.RemoveAll(dir)) }() - t.Logf("Downloading to directory: %s", dir) - tcx.WithShell(ctx, func(tcx it.TestContext) { - tcx.WithReset(func() { - c := createOrGetClusterWithState(ctx, tcx, "RUNNING") - tcx.WriteStdinf("\\viridian download-logs %s -o %s\n", c.Name, dir) - tcx.AssertStderrContains("OK") - require.FileExists(t, paths.Join(dir, "node-1.log")) - }) - }) - }) -} diff --git a/base/commands/viridian/viridian_it_test.go b/base/commands/viridian/viridian_it_test.go index 07e2cddf..d8bab0ea 100644 --- a/base/commands/viridian/viridian_it_test.go +++ b/base/commands/viridian/viridian_it_test.go @@ -5,6 +5,7 @@ package viridian_test import ( "context" "fmt" + "os" "testing" "time" @@ -59,7 +60,7 @@ func TestViridian(t *testing.T) { {"resumeCluster_NonInteractive", resumeCluster_NonInteractiveTest}, {"stopCluster_Interactive", stopCluster_InteractiveTest}, {"stopCluster_NonInteractive", stopCluster_NonInteractiveTest}, - {"streamLogs_nonInteractive", streamLogs_nonInteractiveTest}, + {"streamLogs_NonInteractive", streamLogs_NonInteractiveTest}, } for _, tc := range testCases { t.Run(tc.name, tc.f) @@ -73,12 +74,13 @@ func loginWithParams_NonInteractiveTest(t *testing.T) { } tcx.Tester(func(tcx it.TestContext) { ctx := context.Background() - tcx.CLCExecute(ctx, "viridian", "login", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) + tcx.CLCExecute(ctx, "viridian", "login", "--api-base", "dev2", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) tcx.AssertStdoutContains("Viridian token was fetched and saved.") }) } func loginWithParams_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") tcx := it.TestContext{ T: t, UseViridian: true, @@ -87,7 +89,7 @@ func loginWithParams_InteractiveTest(t *testing.T) { ctx := context.Background() tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { - tcx.WriteStdinf("\\viridian login --api-key %s --api-secret %s\n", it.ViridianAPIKey(), it.ViridianAPISecret()) + tcx.WriteStdinf("\\viridian login --api-base dev2 --api-key %s --api-secret %s\n", it.ViridianAPIKey(), it.ViridianAPISecret()) tcx.AssertStdoutContains("Viridian token was fetched and saved.") }) }) @@ -122,6 +124,7 @@ func listClusters_NonInteractiveTest(t *testing.T) { } func listClusters_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -146,12 +149,12 @@ func createCluster_NonInteractiveTest(t *testing.T) { cs := check.MustValue(tcx.Viridian.ListClusters(ctx)) cid := cs[0].ID tcx.AssertStdoutDollar(fmt.Sprintf("$%s$%s$", cid, clusterName)) - check.Must(waitState(ctx, tcx, cid, "RUNNING")) require.True(t, paths.Exists(paths.ResolveConfigDir(clusterName))) }) } func createCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { ensureNoClusterRunning(ctx, tcx) @@ -180,6 +183,7 @@ func stopCluster_NonInteractiveTest(t *testing.T) { } func stopCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -202,6 +206,7 @@ func resumeCluster_NonInteractiveTest(t *testing.T) { } func resumeCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -226,6 +231,7 @@ func getCluster_NonInteractiveTest(t *testing.T) { } func getCluster_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -252,7 +258,7 @@ func deleteCluster_NonInteractiveTest(t *testing.T) { } func deleteCluster_InteractiveTest(t *testing.T) { - t.Skip() + t.Skipf("Skipping interactive Viridian tests") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { tcx.WithShell(ctx, func(tcx it.TestContext) { tcx.WithReset(func() { @@ -270,6 +276,37 @@ func deleteCluster_InteractiveTest(t *testing.T) { }) } +func downloadLogs_NonInteractiveTest(t *testing.T) { + t.Skipf("skipping this test until the reason of failure is determined") + viridianTester(t, func(ctx context.Context, tcx it.TestContext) { + dir := check.MustValue(os.MkdirTemp("", "log")) + defer func() { check.Must(os.RemoveAll(dir)) }() + c := createOrGetClusterWithState(ctx, tcx, "RUNNING") + tcx.WithReset(func() { + tcx.CLCExecute(ctx, "viridian", "download-logs", c.ID, "--output-dir", dir) + tcx.AssertStderrContains("OK") + require.FileExists(t, paths.Join(dir, "node-1.log")) + }) + }) +} + +func downloadLogs_InteractiveTest(t *testing.T) { + t.Skipf("Skipping interactive Viridian tests") + viridianTester(t, func(ctx context.Context, tcx it.TestContext) { + dir := check.MustValue(os.MkdirTemp("", "log")) + defer func() { check.Must(os.RemoveAll(dir)) }() + t.Logf("Downloading to directory: %s", dir) + tcx.WithShell(ctx, func(tcx it.TestContext) { + tcx.WithReset(func() { + c := createOrGetClusterWithState(ctx, tcx, "RUNNING") + tcx.WriteStdinf("\\viridian download-logs %s -o %s\n", c.Name, dir) + tcx.AssertStderrContains("OK") + require.FileExists(t, paths.Join(dir, "node-1.log")) + }) + }) + }) +} + func viridianTester(t *testing.T, f func(ctx context.Context, tcx it.TestContext)) { tcx := it.TestContext{ T: t, @@ -277,7 +314,7 @@ func viridianTester(t *testing.T, f func(ctx context.Context, tcx it.TestContext } tcx.Tester(func(tcx it.TestContext) { ctx := context.Background() - tcx.CLCExecute(ctx, "viridian", "login", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) + tcx.CLCExecute(ctx, "viridian", "--api-base", "dev2", "login", "--api-key", it.ViridianAPIKey(), "--api-secret", it.ViridianAPISecret()) tcx.AssertStdoutContains("Viridian token was fetched and saved.") tcx.WithReset(func() { f(ctx, tcx) diff --git a/base/commands/viridian/viridian_it_unix_test.go b/base/commands/viridian/viridian_it_unix_test.go index 51796f5c..94dfacff 100644 --- a/base/commands/viridian/viridian_it_unix_test.go +++ b/base/commands/viridian/viridian_it_unix_test.go @@ -11,7 +11,8 @@ import ( "github.com/hazelcast/hazelcast-commandline-client/internal/it" ) -func streamLogs_nonInteractiveTest(t *testing.T) { +func streamLogs_NonInteractiveTest(t *testing.T) { + t.Skipf("skipping this test until the reason of failure is determined") viridianTester(t, func(ctx context.Context, tcx it.TestContext) { c := createOrGetClusterWithState(ctx, tcx, "RUNNING") go func() { diff --git a/base/commands/viridian/viridian_it_windows_test.go b/base/commands/viridian/viridian_it_windows_test.go index e47021b3..e77da8c9 100644 --- a/base/commands/viridian/viridian_it_windows_test.go +++ b/base/commands/viridian/viridian_it_windows_test.go @@ -6,6 +6,6 @@ import ( "testing" ) -func streamLogs_nonInteractiveTest(t *testing.T) { +func streamLogs_NonInteractiveTest(t *testing.T) { t.Skipf("This test doesn't run on Windows") } diff --git a/clc/config/config.go b/clc/config/config.go index 6f7fde6e..9d3ab954 100644 --- a/clc/config/config.go +++ b/clc/config/config.go @@ -11,10 +11,11 @@ import ( "strings" "time" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-go-client" "golang.org/x/exp/slices" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/paths" "github.com/hazelcast/hazelcast-commandline-client/internal/log" @@ -45,6 +46,37 @@ func CreateJSON(path string, opts map[string]any) (dir, cfgPath string, err erro }) } +func ConvertKeyValuesToMap(kvs clc.KeyValues[string, string]) map[string]any { + m := map[string]any{} + for _, kv := range kvs { + mp := m + ps := strings.Split(kv.Key, ".") + var i int + var p string + for i, p = range ps { + if i >= len(ps)-1 { + // this is the leaf + break + } + v, ok := mp[p] + if ok { + // found the sub, set the map pointer + mp = v.(map[string]any) + } else { + // sub doesn't exist, create it + mm := map[string]any{} + mp[p] = mm + // set the map pointer + mp = mm + } + } + if p != "" { + mp[p] = kv.Value + } + } + return m +} + func createFile(path string, f func(string) (string, []byte, error)) (dir, cfgPath string, err error) { dir, cfgPath, err = DirAndFile(path) if err != nil { diff --git a/clc/config/config_test.go b/clc/config/config_test.go index ba2476b5..45b4e4f4 100644 --- a/clc/config/config_test.go +++ b/clc/config/config_test.go @@ -9,12 +9,13 @@ import ( "path/filepath" "testing" - "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" "github.com/hazelcast/hazelcast-go-client" hzlogger "github.com/hazelcast/hazelcast-go-client/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/hazelcast/hazelcast-commandline-client/internal/serialization" + "github.com/hazelcast/hazelcast-commandline-client/clc" "github.com/hazelcast/hazelcast-commandline-client/clc/config" "github.com/hazelcast/hazelcast-commandline-client/clc/logger" @@ -277,6 +278,25 @@ ssl: } } +func TestConvertKeyValuesToMap(t *testing.T) { + kvs := clc.KeyValues[string, string]{ + {Key: "cluster.name", Value: "de-foobar"}, + {Key: "ssl.ca-path", Value: "ca.pem"}, + {Key: "cluster.discovery-token", Value: "tok123"}, + } + m := config.ConvertKeyValuesToMap(kvs) + target := map[string]any{ + "cluster": map[string]any{ + "name": "de-foobar", + "discovery-token": "tok123", + }, + "ssl": map[string]any{ + "ca-path": "ca.pem", + }, + } + assert.Equal(t, target, m) +} + func userHostName() string { u := MustValue(user.Current()) host := MustValue(os.Hostname()) diff --git a/clc/config/import.go b/clc/config/import.go index b60c0047..5e201cad 100644 --- a/clc/config/import.go +++ b/clc/config/import.go @@ -20,17 +20,8 @@ import ( func ImportSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, error) { target = strings.TrimSpace(target) src = strings.TrimSpace(src) - // first assume the passed string is a CURL command line, and try to import it. - path, ok, err := tryImportViridianCurlSource(ctx, ec, target, src) - if err != nil { - return "", err - } - // import is successful - if ok { - return path, nil - } - // import is not successful, check whether this an HTTP source - path, ok, err = tryImportHTTPSource(ctx, ec, target, src) + // check whether this an HTTP source + path, ok, err := tryImportHTTPSource(ctx, ec, target, src) if err != nil { return "", err } @@ -49,21 +40,6 @@ func ImportSource(ctx context.Context, ec plug.ExecContext, target, src string) return path, nil } -// tryImportViridianCurlSource returns true if importing from a Viridian CURL command line is successful -func tryImportViridianCurlSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, bool, error) { - const reCurlSource = `curl (?P[^\s]+)\s+` - re, err := regexp.Compile(reCurlSource) - if err != nil { - return "", false, err - } - grps := re.FindStringSubmatch(src) - if len(grps) < 2 { - return "", false, nil - } - url := grps[1] - return tryImportHTTPSource(ctx, ec, target, url) -} - func tryImportHTTPSource(ctx context.Context, ec plug.ExecContext, target, url string) (string, bool, error) { if !strings.HasPrefix(url, "https://") && !strings.HasSuffix(url, "http://") { return "", false, nil @@ -72,40 +48,26 @@ func tryImportHTTPSource(ctx context.Context, ec plug.ExecContext, target, url s if err != nil { return "", false, err } - ec.Logger().Info("Downloaded sample to: %s", path) - path, err = CreateFromZip(ctx, ec, target, path) - if err != nil { - return "", false, err - } - return path, true, nil - + ec.Logger().Info("Downloaded the configuration at: %s", path) + return tryImportViridianZipSource(ctx, ec, target, path) } // tryImportViridianZipSource returns true if importing from a Viridian Go sample zip file is successful func tryImportViridianZipSource(ctx context.Context, ec plug.ExecContext, target, src string) (string, bool, error) { - const reSource = `hazelcast-cloud-(?P[a-z]+)-sample-client-(?P[a-zA-Z0-9_-]+)-default\.zip` - re, err := regexp.Compile(reSource) - if err != nil { - return "", false, err - } - grps := re.FindStringSubmatch(src) - if len(grps) != 3 { - return "", false, nil - } - language := grps[1] - if language != "go" { - return "", false, fmt.Errorf("%s is not usable as a configuration source, use Go sample", src) + path, ok, err := CreateFromZip(ctx, ec, target, src) + if ok { + return path, true, nil } - path, err := CreateFromZip(ctx, ec, target, src) + path, ok, err = CreateFromZipLegacy(ctx, ec, target, src) if err != nil { - return "", false, err + return "", ok, err } - return path, true, nil + return path, ok, nil } func download(ctx context.Context, ec plug.ExecContext, url string) (string, error) { p, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { - sp.SetText("Downloading the sample") + sp.SetText("Downloading the configuration") f, err := os.CreateTemp("", "clc-download-*") if err != nil { return "", err @@ -125,7 +87,53 @@ func download(ctx context.Context, ec plug.ExecContext, url string) (string, err return p.(string), nil } -func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string) (string, error) { +func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string) (string, bool, error) { + // TODO: refactor this function so it is not dependent on ec + p, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { + sp.SetText("Extracting configuration files") + reader, err := zip.OpenReader(path) + if err != nil { + return nil, err + } + defer reader.Close() + // check whether this is the new config zip + var newConfig bool + var files []*zip.File + for _, rf := range reader.File { + if strings.HasSuffix(rf.Name, "/config.json") { + newConfig = true + } + if !rf.FileInfo().IsDir() { + files = append(files, rf) + } + } + if !newConfig { + return false, nil + } + // this is the new config zip, just extract to target + outDir, cfgFileName, err := DirAndFile(target) + if err != nil { + return nil, err + } + if err = os.MkdirAll(outDir, 0700); err != nil { + return nil, err + } + if err = copyFiles(ec, files, outDir); err != nil { + return nil, err + } + return paths.Join(outDir, cfgFileName), nil + }) + if err != nil { + return "", false, err + } + stop() + if p == false { + return "", false, nil + } + return p.(string), true, nil +} + +func CreateFromZipLegacy(ctx context.Context, ec plug.ExecContext, target, path string) (string, bool, error) { // TODO: refactor this function so it is not dependent on ec p, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, sp clc.Spinner) (any, error) { sp.SetText("Extracting configuration files") @@ -172,10 +180,10 @@ func CreateFromZip(ctx context.Context, ec plug.ExecContext, target, path string return paths.Join(outDir, cfgPath), nil }) if err != nil { - return "", err + return "", false, err } stop() - return p.(string), nil + return p.(string), true, nil } func makeViridianOpts(clusterName, token, password, apiBaseURL string) clc.KeyValues[string, string] { 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 10/48] 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 159a7ffa..18b4622c 100644 --- a/.github/workflows/nightly_tests.yaml +++ b/.github/workflows/nightly_tests.yaml @@ -80,9 +80,4 @@ jobs: - name: "Run All Tests" run: | make test - - - name: "Run Coverage" - run: | - make test-cover - make view-cover 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 11/48] 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 35e17337..14f7ef15 100644 --- a/clc/cmd/exec_context.go +++ b/clc/cmd/exec_context.go @@ -8,6 +8,7 @@ import ( "io" "os" "os/signal" + "strings" "time" "github.com/fatih/color" @@ -265,7 +266,7 @@ func (ec *ExecContext) WrapResult(f func() error) error { func (ec *ExecContext) PrintlnUnnecessary(text string) { if !ec.Quiet() { - I2(fmt.Fprintln(ec.Stdout(), text)) + I2(fmt.Fprintln(ec.Stdout(), colorizeText(text))) } } @@ -286,6 +287,16 @@ func (ec *ExecContext) ensurePrinter() error { return nil } +func colorizeText(text string) string { + if strings.HasPrefix(text, "OK ") { + return fmt.Sprintf(" %s %s", color.GreenString("OK"), text[3:]) + } + if strings.HasPrefix(text, "FAIL ") { + return fmt.Sprintf(" %s %s", color.RedString("FAIL"), text[5:]) + } + return text +} + func makeErrorStringFromHTTPResponse(text string) string { m := map[string]any{} if err := json.Unmarshal([]byte(text), &m); err != nil { @@ -314,13 +325,18 @@ func (s *simpleSpinner) Start() { _ = s.sp.Start() } +func (s *simpleSpinner) Stop() { + // ignoring the error here + _ = s.sp.Stop() +} + func (s *simpleSpinner) SetText(text string) { s.text = text if text == "" { s.sp.Prefix("") return } - s.sp.Prefix(text + cancelMsg) + s.sp.Prefix(" " + text + cancelMsg) } func (s *simpleSpinner) SetProgress(progress float32) { diff --git a/clc/ux/stage/stage.go b/clc/ux/stage/stage.go new file mode 100644 index 00000000..9129b4b5 --- /dev/null +++ b/clc/ux/stage/stage.go @@ -0,0 +1,117 @@ +package stage + +import ( + "context" + "fmt" + "time" + + "github.com/hazelcast/hazelcast-commandline-client/clc" + "github.com/hazelcast/hazelcast-commandline-client/internal" + "github.com/hazelcast/hazelcast-commandline-client/internal/plug" + "github.com/hazelcast/hazelcast-commandline-client/internal/str" +) + +type Statuser interface { + SetProgress(progress float32) + SetRemainingDuration(dur time.Duration) +} + +type basicStatuser struct { + text string + textFmtWithRemaining string + indexText string + sp clc.Spinner +} + +func (s *basicStatuser) SetProgress(progress float32) { + s.sp.SetProgress(progress) +} + +func (s *basicStatuser) SetRemainingDuration(dur time.Duration) { + text := s.text + if dur > 0 { + text = fmt.Sprintf(s.textFmtWithRemaining, dur) + } + s.sp.SetText(s.indexText + " " + text) +} + +type Stage struct { + ProgressMsg string + SuccessMsg string + FailureMsg string + Func func(status Statuser) error +} + +type Provider internal.Iterator[Stage] + +type Counter interface { + StageCount() int +} + +type FixedProvider struct { + stages []Stage + offset int + current Stage + err error +} + +func NewFixedProvider(stages ...Stage) *FixedProvider { + return &FixedProvider{stages: stages} +} + +func (sp *FixedProvider) Next() bool { + if sp.offset >= len(sp.stages) { + return false + } + sp.current = sp.stages[sp.offset] + sp.offset++ + return true +} + +func (sp *FixedProvider) Value() Stage { + return sp.current +} + +func (sp *FixedProvider) Err() error { + return sp.err +} + +func (sp *FixedProvider) StageCount() int { + return len(sp.stages) +} + +func Execute(ctx context.Context, ec plug.ExecContext, sp Provider) error { + ss := &basicStatuser{} + var index int + var stageCount int + if sc, ok := sp.(Counter); ok { + stageCount = sc.StageCount() + } + for sp.Next() { + if sp.Err() != nil { + return sp.Err() + } + stg := sp.Value() + index++ + ss.text = stg.ProgressMsg + ss.textFmtWithRemaining = stg.ProgressMsg + " (%s left)" + if stageCount > 0 { + d := str.SpacePaddedIntFormat(stageCount) + ss.indexText = fmt.Sprintf("["+d+"/%d]", index, stageCount) + } else { + ss.indexText = "" + } + _, stop, err := ec.ExecuteBlocking(ctx, func(ctx context.Context, spinner clc.Spinner) (any, error) { + ss.sp = spinner + ss.SetRemainingDuration(0) + return nil, stg.Func(ss) + }) + if err != nil { + ec.PrintlnUnnecessary(fmt.Sprintf("FAIL %s: %s", stg.FailureMsg, err.Error())) + return err + } + stop() + ec.PrintlnUnnecessary(fmt.Sprintf("OK %s %s.", ss.indexText, stg.SuccessMsg)) + } + return nil +} diff --git a/clc/ux/stage/stage_test.go b/clc/ux/stage/stage_test.go new file mode 100644 index 00000000..837608f9 --- /dev/null +++ b/clc/ux/stage/stage_test.go @@ -0,0 +1,97 @@ +package stage_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/hazelcast/hazelcast-commandline-client/clc/ux/stage" + "github.com/hazelcast/hazelcast-commandline-client/internal/it" +) + +func TestExecute(t *testing.T) { + stages := []stage.Stage{ + { + ProgressMsg: "Progressing 1", + SuccessMsg: "Success 1", + FailureMsg: "Failure 1", + Func: func(status stage.Statuser) error { + time.Sleep(1 * time.Millisecond) + return nil + }, + }, + { + ProgressMsg: "Progressing 2", + SuccessMsg: "Success 2", + FailureMsg: "Failure 2", + Func: func(status stage.Statuser) error { + for i := 0; i < 5; i++ { + status.SetProgress(float32(i+1) / float32(5)) + } + time.Sleep(1 * time.Millisecond) + return nil + }, + }, + { + ProgressMsg: "Progressing 3", + SuccessMsg: "Success 3", + FailureMsg: "Failure 3", + Func: func(status stage.Statuser) error { + for i := 0; i < 5; i++ { + status.SetRemainingDuration(5*time.Second - time.Duration(i+1)*time.Second) + } + time.Sleep(1 * time.Millisecond) + return nil + }, + }, + } + ec := it.NewExecuteContext(nil) + err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...)) + assert.NoError(t, err) + texts := []string{ + "[1/3] Progressing 1", + "[2/3] Progressing 2", + "[3/3] Progressing 3", + "[3/3] Progressing 3 (4s left)", + "[3/3] Progressing 3 (3s left)", + "[3/3] Progressing 3 (2s left)", + "[3/3] Progressing 3 (1s left)", + "[3/3] Progressing 3", + } + assert.Equal(t, texts, ec.Spinner.Texts) + progresses := []float32{0.2, 0.4, 0.6, 0.8, 1} + assert.Equal(t, progresses, ec.Spinner.Progresses) + text := "OK [1/3] Success 1.\nOK [2/3] Success 2.\nOK [3/3] Success 3.\n" + assert.Equal(t, text, ec.StdoutText()) +} + +func TestExecute_WithFailure(t *testing.T) { + stages := []stage.Stage{ + { + ProgressMsg: "Progressing 1", + SuccessMsg: "Success 1", + FailureMsg: "Failure 1", + Func: func(status stage.Statuser) error { + return fmt.Errorf("some error") + }, + }, + { + ProgressMsg: "Progressing 2", + SuccessMsg: "Success 2", + FailureMsg: "Failure 2", + Func: func(status stage.Statuser) error { + return nil + }, + }, + } + ec := it.NewExecuteContext(nil) + err := stage.Execute(context.TODO(), ec, stage.NewFixedProvider(stages...)) + assert.Error(t, err) + texts := []string{"[1/2] Progressing 1"} + assert.Equal(t, texts, ec.Spinner.Texts) + text := "FAIL Failure 1: some error\n" + assert.Equal(t, text, ec.StdoutText()) +} diff --git a/internal/it/context.go b/internal/it/context.go index 1d41133d..0313b91e 100644 --- a/internal/it/context.go +++ b/internal/it/context.go @@ -73,28 +73,31 @@ func (c CommandContext) SetTopLevel(b bool) { } type ExecContext struct { - lg *Logger - stdout *bytes.Buffer - stderr *bytes.Buffer - stdin *bytes.Buffer - args []string - props *plug.Properties - Rows []output.Row + lg *Logger + stdout *bytes.Buffer + stderr *bytes.Buffer + stdin *bytes.Buffer + args []string + props *plug.Properties + Rows []output.Row + Spinner *Spinner } func NewExecuteContext(args []string) *ExecContext { return &ExecContext{ - lg: NewLogger(), - stdout: &bytes.Buffer{}, - stderr: &bytes.Buffer{}, - stdin: &bytes.Buffer{}, - args: args, - props: plug.NewProperties(), + lg: NewLogger(), + stdout: &bytes.Buffer{}, + stderr: &bytes.Buffer{}, + stdin: &bytes.Buffer{}, + args: args, + props: plug.NewProperties(), + Spinner: NewSpinner(), } } -func (ec *ExecContext) ExecuteBlocking(context.Context, func(context.Context, clc.Spinner) (any, error)) (any, context.CancelFunc, error) { - //TODO implement me - panic("implement me") +func (ec *ExecContext) ExecuteBlocking(ctx context.Context, f func(context.Context, clc.Spinner) (any, error)) (any, context.CancelFunc, error) { + v, err := f(ctx, ec.Spinner) + stop := func() {} + return v, stop, err } func (ec *ExecContext) Props() plug.ReadOnlyProperties { @@ -187,3 +190,25 @@ func (ec *ExecContext) PrintlnUnnecessary(text string) { func (ec *ExecContext) WrapResult(f func() error) error { return f() } + +type Spinner struct { + Texts []string + Progresses []float32 +} + +func NewSpinner() *Spinner { + return &Spinner{} +} + +func (s *Spinner) Reset() { + s.Texts = nil + s.Progresses = nil +} + +func (s *Spinner) SetText(text string) { + s.Texts = append(s.Texts, text) +} + +func (s *Spinner) SetProgress(progress float32) { + s.Progresses = append(s.Progresses, progress) +} diff --git a/internal/iterator.go b/internal/iterator.go new file mode 100644 index 00000000..c6acae3f --- /dev/null +++ b/internal/iterator.go @@ -0,0 +1,17 @@ +package internal + +// Iterator is a generic iterator interface. +// Non thread safe. +type Iterator[T any] interface { + // Next returns false if the iterator is exhausted. + // Otherwise advances the iterator and returns true. + Next() bool + // Value returns the current value in the iterator. + // Next should always be called before Value is called. + // Otherwise may panic. + Value() T + // Err contains the error after advancing the iterator. + // If it is nil, it is safe to call Next. + // Otherwise Next should not be called. + Err() error +} diff --git a/internal/str/str.go b/internal/str/str.go index 77332356..f2613f4e 100644 --- a/internal/str/str.go +++ b/internal/str/str.go @@ -2,6 +2,7 @@ package str import ( "fmt" + "strconv" "strings" ) @@ -37,3 +38,12 @@ func MaybeShorten(s string, l int) string { } return fmt.Sprintf("%s...", s[:l]) } + +// SpacePaddedIntFormat returns the fmt string that can fit the given integer. +// The padding uses spaces. +func SpacePaddedIntFormat(maxValue int) string { + if maxValue < 0 { + panic("SpacePaddedIntFormat: cannot be negative") + } + return fmt.Sprintf("%%%dd", len(strconv.Itoa(maxValue))) +} diff --git a/internal/str/str_test.go b/internal/str/str_test.go index ab2c0b58..b7ca05a3 100644 --- a/internal/str/str_test.go +++ b/internal/str/str_test.go @@ -1,6 +1,7 @@ package str_test import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -86,3 +87,24 @@ func TestSplitByComma(t *testing.T) { }) } } + +func TestSpacePaddedIntFormat(t *testing.T) { + testCases := []struct { + num int + out string + }{ + {num: 0, out: "%1d"}, + {num: 9, out: "%1d"}, + {num: 10, out: "%2d"}, + {num: 99, out: "%2d"}, + {num: 100, out: "%3d"}, + {num: 9999, out: "%4d"}, + } + for _, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("pad %d", tc.num), func(t *testing.T) { + s := str.SpacePaddedIntFormat(tc.num) + assert.Equal(t, tc.out, s) + }) + } +} From b4d1a70115f53b8034816bd6d6eec98c5f182ba1 Mon Sep 17 00:00:00 2001 From: Kutluhan Metin Date: Tue, 22 Aug 2023 17:36:27 +0300 Subject: [PATCH 12/48] 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 16eccb4c..f685c5c2 100644 --- a/clc/config/wizard/provider.go +++ b/clc/config/wizard/provider.go @@ -58,11 +58,11 @@ func maybeUnwrapStdout(ec plug.ExecContext) any { } func (p *Provider) ClientConfig(ctx context.Context, ec plug.ExecContext) (hazelcast.Config, error) { - if terminal.IsPipe(maybeUnwrapStdout(ec)) { - return hazelcast.Config{}, fmt.Errorf(`no configuration was provided and cannot display the configuration wizard; use the --config flag`) - } cfg, err := p.fp.Load().ClientConfig(ctx, ec) if err != nil { + if terminal.IsPipe(maybeUnwrapStdout(ec)) { + return hazelcast.Config{}, fmt.Errorf(`no configuration was provided and cannot display the configuration wizard; use the --config flag`) + } // ask the config to the user name, err := p.runWizard(ctx, ec) if err != nil { 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 13/48] 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 850237cc..53a24096 100644 --- a/base/commands/project/project_create_it_test.go +++ b/base/commands/project/project_create_it_test.go @@ -17,25 +17,22 @@ import ( ) func TestCreateCommand(t *testing.T) { - // TODO: create a temp home and copy the template into it - testDir := filepath.Join(check.MustValue(filepath.Abs("testdata"))) - home := filepath.Join(testDir, "home") tcx := it.TestContext{T: t} tcx.Tester(func(tcx it.TestContext) { - it.WithEnv(paths.EnvCLCHome, home, func() { - tempDir := check.MustValue(os.MkdirTemp("", "clc-")) - outDir := filepath.Join(tempDir, "my-project") - fixture := filepath.Join(testDir, "fixture", "simple") - defer func() { - // ignoring the error here - _ = os.RemoveAll(outDir) - }() - ctx := context.Background() - // logging to stderr in order to avoid creating the logs directory - cmd := []string{"project", "create", "simple", "-o", outDir, "--log.path", "stderr", "another_key=foo", "key1=bar"} - check.Must(tcx.CLC().Execute(ctx, cmd...)) - check.Must(compareDirectories(fixture, outDir)) - }) + testHomeDir := "testdata/home" + check.Must(paths.CopyDir(testHomeDir, tcx.HomePath())) + tempDir := check.MustValue(os.MkdirTemp("", "clc-")) + outDir := filepath.Join(tempDir, "my-project") + fixtureDir := "testdata/fixture/simple" + defer func() { + // ignoring the error here + _ = os.RemoveAll(outDir) + }() + ctx := context.Background() + // logging to stderr in order to avoid creating the logs directory + cmd := []string{"project", "create", "simple", "-o", outDir, "--log.path", "stderr", "another_key=foo", "key1=bar"} + check.Must(tcx.CLC().Execute(ctx, cmd...)) + check.Must(compareDirectories(fixtureDir, outDir)) }) } diff --git a/clc/paths/paths.go b/clc/paths/paths.go index 9685b567..ccfec662 100644 --- a/clc/paths/paths.go +++ b/clc/paths/paths.go @@ -2,6 +2,8 @@ package paths import ( "fmt" + "io" + "io/fs" "os" "path/filepath" "strings" @@ -160,6 +162,43 @@ func FindAll(cd string, fn FilterFn) ([]string, error) { return cs, nil } +// CopyDir copies directory src into target directory. +// src/dir/file is copied as target/dir/file +func CopyDir(src, target string) error { + l := len(src) + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) (errOut error) { + if err != nil { + return err + } + part := path[l:] + dest := filepath.Join(target, part) + if d.IsDir() { + if err := os.MkdirAll(dest, 0700); err != nil { + return err + } + return nil + } + in, err := os.Open(path) + if err != nil { + return err + } + // ignoring the error here + defer in.Close() + out, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer func() { + errOut = err + return + }() + if _, err := io.Copy(out, in); err != nil { + return err + } + return nil + }) +} + func nearbyConfigPath() string { // check whether there is config.yaml in the current directory wd, err := os.Getwd() 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 14/48] [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 00000000..4b59bed4 --- /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 0b4154b1..0c1ac9c8 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 6b5db621..c92208ad 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 210f64d2..38e918a7 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 55903e79..b1704792 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 14f7ef15..c834851e 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 04661cee..14b783d6 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 15/48] [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 877d6f64..4ba5c51b 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 00000000..8fa68a52 --- /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 00000000..d0624d50 --- /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 18e55ec5..076f81bb 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 ccfec662..8b5320ca 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 16466409..6936b930 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 16/48] [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 6f5c6798..c90402fe 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 7ae583a0..b38e571a 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 0c1ac9c8..5c745196 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 c92208ad..33acda46 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 29c9e1a0..d9e4f078 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 331ccf1c..dc9728af 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 00000000..92a1c851 --- /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 641d5073..15eb8645 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 00000000..e85aeba6 --- /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 00000000..1228df59 --- /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 c293e478..8d66261a 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 9d1bf209..d48a14d0 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 00000000..fe3fe8cd --- /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 9f012494..0482768c 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 00000000..8be4dffe --- /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 00000000..b6571434 --- /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 17/48] 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 dc9728af..60c2c426 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 18/48] [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 00000000..8d29e51d --- /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 19/48] 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 8d29e51d..a42842d6 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 20/48] [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 3212d07d..b0e7fd3a 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 00000000..3bb32bcc --- /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 d5e090bf..cdacc2ac 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 21/48] [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 2fb231e3..6450cf75 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 7372f851..2d99ddbb 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 787cf8b7..a6b4c02b 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 18e6e2f6..43df0ee6 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 e896ece9..aa3ab6d7 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 82020de3..00000000 --- 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 d8bab0ea..125b4680 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 a8c7def9..10ff22d0 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 c7a46df5..d82cf395 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 3b653271..b2038a6c 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 22/48] 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 36858176..f641a657 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 23/48] 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 21a9b883..f6ecc41f 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 9d3ab954..6244779c 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 45b4e4f4..26fa5c35 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 5e201cad..d991fcf0 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 c44bd84e..975cd718 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 24/48] 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 2b194a19..e93d894f 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 f1e89321..12d2dee9 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 c4978de0..a50e5ee4 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 82134db7..a3e08ce0 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 f6ecc41f..4ba3427c 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 a6047a99..04cdabd9 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 6a29de9e..f2ed0af5 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 00000000..11007d73 --- /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 fb05a574..eaf7043c 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 7db0262d..f9aa8bea 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 3bb32bcc..466016f8 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 63d16e1f..27c06449 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 69ec4470..0dc200b7 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 4f38a043..583f33de 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 7c9b0c1b..9bb7f9b7 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 977caa30..cfa9c8b4 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 26e55c25..5f6e94e1 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 ef94ee0a..7d6cfcf2 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 d194c8c3..b1ac797f 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 d38b9256..e1d8b30a 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 1285f282..8917a554 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 844751ad..1f7a1f76 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 6604d562..aadb7c69 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 3c13fec7..ccf1364d 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 03191648..70b3dfb3 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 8ad9298a..44d13e0d 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 c1a17dd6..29797888 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 972d00c7..fb7f7d07 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 937ce1cb..3923daa3 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 630c6cd7..f253df95 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 49b22272..40de412f 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 b48adde3..a48daf49 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 b6cd1afd..e4d7df35 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 e308db4c..2b749c7a 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 01991939..78741f59 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 e9d49e8e..27d27b7c 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 cf46e3c5..bf8cb0ed 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 7807b6b2..02e2c53f 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 adb97730..2261f377 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 c90402fe..717542de 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 21f20bd7..884bf626 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 6dc7813c..42686707 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 874171d4..a558b7e4 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 1ad14892..aa1cc939 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 b8ea7fee..6584a02b 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 061e1c5f..274b1eba 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 be7f0db8..6ff48cbb 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 11ac420e..958598be 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 2ab4f4a9..51bd4f9c 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 96251576..77efd230 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 857234d4..50f17855 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 dc62cef8..87f0880b 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 f58b1aa3..fe8249fd 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 d8963c7b..395bc76c 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 998a1ce4..08a47cfa 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 0c86e119..9af9406d 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 7f1a7709..0513eda5 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 b38e571a..d96caa80 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 00000000..be121be0 --- /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 e41297ec..a5f6c660 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 d0624d50..3d1a2565 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 076f81bb..90420263 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 4f1d8092..63a3a198 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 2207935e..9d39651c 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 9b4ceb20..3aed4797 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 978eb9b3..17e1f175 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 4c5822e6..8fc32fc1 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 5c745196..66223dee 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 645273f7..9e1fd13b 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 1947c351..31d06d6a 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 4886a8da..9ceddcc6 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 00daa649..6274d57c 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 c332ec26..7899a9ea 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 2c772c51..178c9afb 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 1c999cd3..5349eeda 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 33acda46..fa31b512 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 78b04a52..089a77d8 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 456272a7..e7624901 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 8181530e..571c584c 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 11a251ca..15e2756e 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 aa97c920..9a6455e2 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 f536d3a3..da9834f8 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 0251c688..5c282a78 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 bf72de73..b539e4e6 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 2d99ddbb..f447ad1b 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 fe20942c..ea109c13 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 3bc9eb26..b6f59aaf 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 755c1d92..8e990ef0 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 5e79bfdb..d64755aa 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 ca312449..b32e89cc 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 a6b4c02b..00cc4705 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 8e5a61c3..4245c971 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 43df0ee6..6f69aa0a 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 aa3ab6d7..014fc2f9 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 35a59bd4..586084a9 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 223f1a56..46232974 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 ee0d2634..4d934ee7 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 e04ee671..c44912a2 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 8b371a81..84880231 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 38e918a7..228d50bc 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 00000000..c78a2e6c --- /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 57820e81..946b81cc 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 b1704792..cb88d8b2 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 c834851e..a933f0f1 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 149f7415..75f883ab 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 0313b91e..a93a4bbf 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 00000000..cf9e771b --- /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 fd1bc618..e6f96143 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 975cd718..bf4d9c40 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 25/48] [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 fa31b512..73b0051a 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 026fb947..00000000 --- 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 a933f0f1..10122362 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 6c02d583..4fe8628c 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 f641a657..38136a52 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 d8c6bddc..dfe42c46 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 f685c5c2..d27296bb 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 baf65c47..bab8bc34 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 1c8ae503..3e7c3c0d 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 a93a4bbf..8c308821 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 e6f96143..502558ee 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 26/48] 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 9756e97d..ff450442 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 5f3c8659..69a4ef9e 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 f4d5e515..19a3701e 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 18b4622c..d0af0ed4 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 19e94093..f839be0b 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 c1a2b8be..1ecb75df 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 01ec7912..29b1d239 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 ca71dcf7..d6e2dca4 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 e93d894f..a4b92f81 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 12d2dee9..3d9be807 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 a50e5ee4..d2bff150 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 a3e08ce0..a653d0ca 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 ab3300a9..f6d6a662 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 4ba3427c..b3829022 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 04cdabd9..613932d8 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 7a591775..1bef5da9 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 f2ed0af5..45e7436b 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 f9aa8bea..df142b05 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 466016f8..fa76de7a 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 583f33de..1badd7cd 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 cfa9c8b4..04cfb798 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 9b50bbe0..49877946 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 5f6e94e1..9029416c 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 7d6cfcf2..9ca8fc35 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 b1ac797f..ba9d337c 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 e1d8b30a..51d6cc22 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 6450cf75..93892e1d 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 ea109c13..5c1b0800 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 b6f59aaf..fdf98c9b 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 8e990ef0..7483a598 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 d64755aa..9e5a7d18 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 b32e89cc..7cd3c724 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 00cc4705..3577bafc 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 4245c971..c5d02f71 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 6f69aa0a..38c0a90d 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 014fc2f9..c61583a2 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 586084a9..5ae9f995 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 46232974..fdbb04b4 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 4d934ee7..937b8bb2 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 125b4680..ecef81c8 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 c44912a2..eb9c5d75 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 84880231..b907cdd5 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 10122362..46875271 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 dbfcb840..25e5c430 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 d991fcf0..0e28a0bd 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 d27296bb..98bd0d04 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 00000000..e994c1c4 --- /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 9129b4b5..8c1fe91f 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 837608f9..7c271cea 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 3e7c3c0d..d9d06348 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 28d2f66e..426a1d43 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 139a7302..0c38dd6d 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 f26031a6..87491747 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 fa82f07d..9fdb714a 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 00000000..a8a6a155 --- /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 cdb2121b..2140ac65 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 d82cf395..b89f48fc 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 cedeee18..bc781527 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 27/48] 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 fa76de7a..6fa15c44 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 7595a88b..3cc4d982 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 8917a554..b9f916bb 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 1b703618..b7af813e 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 1f7a1f76..69daa9f7 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 aadb7c69..09175b47 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 ccf1364d..cecfefff 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 70b3dfb3..9dc612a7 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 5236c94f..5ceb4107 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 44d13e0d..4ccb827e 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 29797888..00bc5ef2 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 fb7f7d07..09ad4ef8 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 3923daa3..c785bedb 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 00000000..da513941 --- /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 87491747..e1e2ca30 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 9fdb714a..9aeca604 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 28/48] 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 a42842d6..f7a28d2a 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 29/48] 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 fffa57b6..d0bfe1ea 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 30/48] 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 4cc97eba..4edf5c9a 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 40de412f..b43256b5 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 a48daf49..baf33043 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 e4d7df35..3b286625 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 2b749c7a..ed7874dc 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 78741f59..b52a6130 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 ae5882ff..a9d36eec 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 27d27b7c..0c678c9c 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 bf8cb0ed..8afd19ba 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 02e2c53f..4023f666 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 2261f377..059577ca 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 717542de..13a1fdae 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 884bf626..044bf259 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 42686707..1d075d4f 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 a558b7e4..5ad0b4ee 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 aa1cc939..5b98b67c 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 8c1fe91f..fa71255d 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 31/48] 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 f7a28d2a..3bfa695e 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 32/48] 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 d6e2dca4..99dd1380 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 a4b92f81..da0115d0 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 00000000..1918dc5e --- /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 3d9be807..8a038986 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 d2bff150..a1db69da 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 a653d0ca..b131b7c6 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 f6d6a662..d6b6df56 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 500b9d25..62313e61 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 17f6c839..1653af99 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 47701dc9..823e9d4f 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 b3829022..a3609782 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 613932d8..1ed8ef37 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 1bef5da9..f1e1a752 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 45e7436b..ce34b1f5 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 00000000..4554246d --- /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 d9135f4a..fbcdf0ed 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 df142b05..ba425e4b 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 6fa15c44..5e7eb1be 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 27c06449..f789aea1 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 1badd7cd..bfaae82a 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 9a57dc02..e34b988a 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 04cfb798..55cd0b15 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 49877946..9de9f957 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 9029416c..8e6185a5 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 9ca8fc35..b8c7e9e5 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 ba9d337c..845483f3 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 51d6cc22..61169e98 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 3cc4d982..8af5b295 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 b7af813e..818c7a42 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 69daa9f7..1b3dfa5c 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 09175b47..7ee632bb 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 cecfefff..2a2f3314 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 9dc612a7..1fd6b75a 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 4ccb827e..7ee03f43 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 00bc5ef2..3276fe9d 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 09ad4ef8..d58c8ab3 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 c785bedb..f2b0ee2b 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 4edf5c9a..15d962d7 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 f253df95..ad7a0ccb 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 b43256b5..83bf3485 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 baf33043..e9f900b9 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 3b286625..0824758d 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 ed7874dc..21610a6c 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 b52a6130..2eaba89d 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 a9d36eec..7c4bb0ea 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 0c678c9c..bcdb67fb 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 8afd19ba..41c9ca06 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 4023f666..8ccb3838 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 059577ca..f0bb5a86 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 13a1fdae..d00331ec 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 044bf259..26c822ec 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 1d075d4f..c3d9bc95 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 5ad0b4ee..1cc8d031 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 5b98b67c..b0bd093c 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 e258cdf0..00000000 --- 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 00000000..cdf9182a --- /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 c86f3b73..8b3c694d 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 6584a02b..00000000 --- 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 6eaf9b38..59ca55e9 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 274b1eba..64d645ad 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 6ff48cbb..1af96c25 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 958598be..b919d885 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 51bd4f9c..46b7acb5 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 77efd230..659dbb4e 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 50f17855..ca12da29 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 87f0880b..6d5ffb70 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 fe8249fd..c741259d 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 395bc76c..bcf0d902 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 08a47cfa..3df4ef22 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 9af9406d..dd4a73de 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 0513eda5..c3b370e8 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 f58f5fb3..00000000 --- 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 d76a8bad..d1d143b9 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 d96caa80..3d01a36e 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 02a4994f..085da85c 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 be121be0..7502b58e 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 4ba5c51b..d064b4ff 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 a5f6c660..879068ed 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 3d1a2565..5074ba62 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 90420263..27cbd640 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 f630c222..6f9a7c46 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 7427d61f..00000000 --- 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 bc0ec04f..27551482 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 63a3a198..022e8f5e 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 9d39651c..f33b0262 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 3aed4797..50fbcd75 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 17e1f175..b076bf43 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 8fc32fc1..4c355478 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 66223dee..a8a13b53 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 3a63ba43..95d8603e 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 9e1fd13b..00000000 --- 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 6d504872..c1819c86 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 31d06d6a..e58d53f2 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 9ceddcc6..68d0eeec 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 6274d57c..c70ed31b 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 7899a9ea..f71c77db 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 178c9afb..8ee57697 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 5349eeda..086b5267 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 73b0051a..692464f9 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 d69aff6e..c5a51666 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 d9e4f078..4f97c824 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 ee65014c..87c404a9 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 089a77d8..8c5aebec 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 e7624901..bd9274e0 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 571c584c..f6b85d8c 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 60c2c426..753b20c4 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 8173104b..33786f53 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 dc49cd97..bbeeec22 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 ab8cc4cc..e8a18024 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 15e2756e..4a0735f6 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 5fa06c84..5abe9318 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 9a6455e2..45cc6f9e 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 da9834f8..86140d4c 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 5c282a78..255100c1 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 5c1b0800..f15db97d 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 fdf98c9b..4e3d643d 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 7483a598..1c21b601 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 9e5a7d18..1685be52 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 7cd3c724..bce1607d 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 04dc500b..9e894c92 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 3577bafc..be8dfdb1 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 c5d02f71..ee7ca67f 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 38c0a90d..20fd3241 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 c61583a2..4104abd7 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 5ae9f995..e93a7447 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 fdbb04b4..8f242186 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 937b8bb2..320d79fc 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 ecef81c8..0f982fbc 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 eb9c5d75..8a4c989b 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 b907cdd5..38d4acaa 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 2bd3e8a3..aa29ba28 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 1228df59..cd0b2263 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 228d50bc..18531ea2 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 46875271..4038ab55 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 25e5c430..ef928188 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 6244779c..bf81bb43 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 75f883ab..fbb3a2f3 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 8d66261a..3549cc63 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 bab8bc34..888b1b7c 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 da2383c5..52b405a9 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 00000000..f2736e56 --- /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 fa71255d..44518fad 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 7c271cea..dbf4c6ce 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 d9d06348..31eae77e 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 0c38dd6d..07b8ed28 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 00000000..50423809 --- /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 00000000..9af0483c --- /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 7b11a063..ef10bdde 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 b5ddb647..ffb1233f 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 502558ee..bfa119f1 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 f2613f4e..4dd34e76 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 5b97f4c5..00000000 --- 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 33/48] 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 29b1d239..822af986 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 7502b58e..8c480bb4 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 6936b930..f2dfe8c7 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 c18f4222..958041d1 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 d0bfe1ea..c20ea50a 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 34/48] 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 873edaa7..472c3ce8 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 00000000..f1701924 --- /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 35/48] 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 426a1d43..965ace4a 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 36/48] 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 cd0b2263..bff671d4 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 37/48] 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 3bfa695e..a8cec90e 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 38/48] [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 1b3dfa5c..9481ef9c 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 5ceb4107..520b8b63 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 39/48] 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 0f982fbc..ee7292de 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 40/48] 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 8c480bb4..e78871d9 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 8fa68a52..3e51a1d9 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 5074ba62..1ff6dc69 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 41/48] 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 e017571e..0fd2a1a1 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 627fe646..b00aecb0 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 8bff8ee8..67baef35 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 42/48] [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 472c3ce8..462a283e 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 00000000..c57fb3ca --- /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 43/48] 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 a8cec90e..640b9b54 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 44/48] [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 3e636a0e..28b53c01 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 692464f9..f9c62a8b 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 00000000..af0af109 --- /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 00000000..63d6890e --- /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 24fefaeb..7c966613 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 00000000..9f253a91 --- /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 544497b4..4ec6e9dd 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 45/48] 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 ce34b1f5..9c0f8fc4 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 ba425e4b..50a26ed5 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 bfaae82a..0d07d76a 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 cdf9182a..e8997e8c 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 15eb8645..90c607e2 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 ee7ca67f..4cc818dc 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 dfe42c46..054f6948 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 98bd0d04..71b395f4 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 3549cc63..beaab903 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 44518fad..44c2291f 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 dbf4c6ce..daacbda4 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 f2dfe8c7..536e3546 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 46/48] 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 2140ac65..440b3aef 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 47/48] [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 822af986..f37caafc 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 26fa5c35..043ebb42 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 4fe8628c..4371f8e8 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 71b395f4..06f82c6b 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 3601d5fd..2077147a 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 48/48] [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 0d07d76a..097ac527 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 b8c7e9e5..e740b886 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 845483f3..c89a6117 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 e0796f07..9aa0fae3 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 4c208bad..2e57e52b 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 14b2df57..0fd9f55f 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) +}