From 17598c6e6423e17988fbb1981870c811efd6d860 Mon Sep 17 00:00:00 2001 From: Andrea Angiolillo Date: Thu, 14 Mar 2024 09:12:40 +0000 Subject: [PATCH] CLOUDP-164347: Add `--flag file` to the `atlas cluster index create` command (#2768) --- .../command/atlas-clusters-indexes-create.txt | 24 ++++++- internal/cli/atlas/clusters/indexes/create.go | 64 ++++++++++++++++--- .../cli/atlas/clusters/indexes/create_test.go | 50 ++++++++++++++- internal/usage/usage.go | 1 + test/e2e/atlas/clusters_file_test.go | 50 +++++++++++++-- test/e2e/atlas/data/create_2dspere_index.json | 24 +++++++ .../{ => data}/create_cluster_gov_test.json | 0 .../atlas/{ => data}/create_cluster_test.json | 0 test/e2e/atlas/data/create_partial_index.json | 27 ++++++++ test/e2e/atlas/data/create_sparse_index.json | 24 +++++++ .../create_streams_connection_test.json | 0 .../{ => data}/sample_embedded_movies.json | 0 .../{ => data}/sample_vector_search.json | 0 .../sample_vector_search_pipeline.json | 0 .../atlas/{ => data}/search_nodes_spec.json | 0 .../{ => data}/search_nodes_spec_update.json | 0 .../atlas/{ => data}/update_cluster_test.json | 0 .../update_streams_connection_test.json | 0 .../atlas/deployments_local_noauth_test.go | 4 +- test/e2e/atlas/search_nodes_test.go | 4 +- test/e2e/atlas/streams_test.go | 4 +- 21 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 test/e2e/atlas/data/create_2dspere_index.json rename test/e2e/atlas/{ => data}/create_cluster_gov_test.json (100%) rename test/e2e/atlas/{ => data}/create_cluster_test.json (100%) create mode 100644 test/e2e/atlas/data/create_partial_index.json create mode 100644 test/e2e/atlas/data/create_sparse_index.json rename test/e2e/atlas/{ => data}/create_streams_connection_test.json (100%) rename test/e2e/atlas/{ => data}/sample_embedded_movies.json (100%) rename test/e2e/atlas/{ => data}/sample_vector_search.json (100%) rename test/e2e/atlas/{ => data}/sample_vector_search_pipeline.json (100%) rename test/e2e/atlas/{ => data}/search_nodes_spec.json (100%) rename test/e2e/atlas/{ => data}/search_nodes_spec_update.json (100%) rename test/e2e/atlas/{ => data}/update_cluster_test.json (100%) rename test/e2e/atlas/{ => data}/update_streams_connection_test.json (100%) diff --git a/docs/command/atlas-clusters-indexes-create.txt b/docs/command/atlas-clusters-indexes-create.txt index b87e65dd8d..6a30abbeb8 100644 --- a/docs/command/atlas-clusters-indexes-create.txt +++ b/docs/command/atlas-clusters-indexes-create.txt @@ -59,20 +59,32 @@ Options - Name of the cluster. * - --collection - string - - true + - false - Name of the collection. + + Mutually exclusive with --file. * - --db - string - - true + - false - Name of the database. + + Mutually exclusive with --file. + * - -f, --file + - string + - false + - Path to an optional JSON configuration file that defines index settings. + + Mutually exclusive with --db, --collection, --key. * - -h, --help - - false - help for create * - --key - strings - - true + - false - Field to be indexed and the type of index in the following format: field:type. + + Mutually exclusive with --file. * - --projectId - string - false @@ -112,3 +124,9 @@ Examples # Create a compound index named property_room_bedrooms on the listings collection of the realestate database: atlas clusters indexes create property_room_bedrooms --clusterName Cluster0 --collection listings --db realestate --key property_type:1 --key room_type:1 --key bedrooms:1 + + +.. code-block:: + + # Create an index named my_index from a JSON configuration file named myfile.json: + atlas clusters indexes create my_index --clusterName Cluster0 --file file.json diff --git a/internal/cli/atlas/clusters/indexes/create.go b/internal/cli/atlas/clusters/indexes/create.go index f49a2c6d56..3abe72ee57 100644 --- a/internal/cli/atlas/clusters/indexes/create.go +++ b/internal/cli/atlas/clusters/indexes/create.go @@ -22,9 +22,11 @@ import ( "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/cli/require" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/config" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/file" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/store" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/usage" + "github.com/spf13/afero" "github.com/spf13/cobra" atlasv2 "go.mongodb.org/atlas-sdk/v20231115007/admin" ) @@ -35,9 +37,11 @@ type CreateOpts struct { name string db string collection string + filename string keys []string sparse bool store store.IndexCreator + fs afero.Fs } func (opts *CreateOpts) initStore(ctx context.Context) func() error { @@ -61,11 +65,43 @@ func (opts *CreateOpts) Run() error { } func (opts *CreateOpts) newIndex() (*atlasv2.DatabaseRollingIndexRequest, error) { + if opts.filename != "" { + return opts.newIndexViaFile() + } + + return opts.newIndexViaFlags() +} + +func (opts *CreateOpts) newIndexViaFile() (*atlasv2.DatabaseRollingIndexRequest, error) { + i := new(atlasv2.DatabaseRollingIndexRequest) + if err := file.Load(opts.fs, opts.filename, i); err != nil { + return nil, err + } + + if opts.name == "" { + return i, nil + } + + if i.Options == nil { + i.Options = &atlasv2.IndexOptions{ + Name: &opts.name, + } + + return i, nil + } + + i.Options.Name = &opts.name + + return i, nil +} + +func (opts *CreateOpts) newIndexViaFlags() (*atlasv2.DatabaseRollingIndexRequest, error) { + i := new(atlasv2.DatabaseRollingIndexRequest) keys, err := opts.indexKeys() if err != nil { return nil, err } - i := new(atlasv2.DatabaseRollingIndexRequest) + i.Db = opts.db i.Collection = opts.collection i.Keys = &keys @@ -100,9 +136,11 @@ func (opts *CreateOpts) indexKeys() ([]map[string]string, error) { } // CreateBuilder builds a cobra.Command that can run as: -// mcli atlas clusters index create [indexName] --clusterName clusterName --collection collection --dbName dbName [--key field:type]. +// mcli atlas clusters index create [indexName] --clusterName clusterName --collection collection --dbName dbName [--key field:type] --file filename. func CreateBuilder() *cobra.Command { - opts := &CreateOpts{} + opts := &CreateOpts{ + fs: afero.NewOsFs(), + } cmd := &cobra.Command{ Use: "create [indexName]", Short: "Create a rolling index for the specified cluster for your project.", @@ -116,8 +154,17 @@ func CreateBuilder() *cobra.Command { # Create a compound index named property_room_bedrooms on the listings collection of the realestate database: - atlas clusters indexes create property_room_bedrooms --clusterName Cluster0 --collection listings --db realestate --key property_type:1 --key room_type:1 --key bedrooms:1`, + atlas clusters indexes create property_room_bedrooms --clusterName Cluster0 --collection listings --db realestate --key property_type:1 --key room_type:1 --key bedrooms:1 + + # Create an index named my_index from a JSON configuration file named myfile.json: + atlas clusters indexes create my_index --clusterName Cluster0 --file file.json`, PreRunE: func(cmd *cobra.Command, _ []string) error { + if opts.filename == "" { + _ = cmd.MarkFlagRequired(flag.Database) + _ = cmd.MarkFlagRequired(flag.Collection) + _ = cmd.MarkFlagRequired(flag.Key) + } + return opts.PreRunE(opts.ValidateProjectID, opts.initStore(cmd.Context())) }, RunE: func(_ *cobra.Command, args []string) error { @@ -133,13 +180,14 @@ func CreateBuilder() *cobra.Command { cmd.Flags().StringVar(&opts.collection, flag.Collection, "", usage.Collection) cmd.Flags().StringSliceVar(&opts.keys, flag.Key, []string{}, usage.Key) cmd.Flags().BoolVar(&opts.sparse, flag.Sparse, false, usage.Sparse) + cmd.Flags().StringVarP(&opts.filename, flag.File, flag.FileShort, "", usage.IndexFilename) cmd.Flags().StringVar(&opts.ProjectID, flag.ProjectID, "", usage.ProjectID) - _ = cmd.MarkFlagRequired(flag.ClusterName) - _ = cmd.MarkFlagRequired(flag.Database) - _ = cmd.MarkFlagRequired(flag.Collection) - _ = cmd.MarkFlagRequired(flag.Key) + cmd.MarkFlagsMutuallyExclusive(flag.File, flag.Database) + cmd.MarkFlagsMutuallyExclusive(flag.File, flag.Collection) + cmd.MarkFlagsMutuallyExclusive(flag.File, flag.Key) + _ = cmd.MarkFlagRequired(flag.ClusterName) return cmd } diff --git a/internal/cli/atlas/clusters/indexes/create_test.go b/internal/cli/atlas/clusters/indexes/create_test.go index ff590105d6..a11920ea87 100644 --- a/internal/cli/atlas/clusters/indexes/create_test.go +++ b/internal/cli/atlas/clusters/indexes/create_test.go @@ -23,6 +23,7 @@ import ( "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/flag" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/mocks" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/test" + "github.com/spf13/afero" ) func TestCreate_Run(t *testing.T) { @@ -50,11 +51,58 @@ func TestCreate_Run(t *testing.T) { } } +func TestCreateWithFile_Run(t *testing.T) { + ctrl := gomock.NewController(t) + mockStore := mocks.NewMockIndexCreator(ctrl) + appFS := afero.NewMemMapFs() + fileJSON := ` +{ + "collection": "collectionName", + "db": "dbName", + "options":{ + "sparse": true, + "unique": true, + "textIndexVersion": 1, + "name": "myIndex", + "min": 1, + "max": 10, + "language_override": "test", + "hidden": true, + "expireAfterSeconds": 2, + "default_language": "test", + "default_language": "test", + "columnstoreProjection": {"key":1, "key2":2}, + "bucketSize": 2, + "bits": 222, + "background": false, + "2dsphereIndexVersion": 2 + } +}` + fileName := "atlas_cluster_index_create_test.json" + _ = afero.WriteFile(appFS, fileName, []byte(fileJSON), 0600) + createOpts := &CreateOpts{ + filename: fileName, + store: mockStore, + fs: appFS, + } + + index, _ := createOpts.newIndex() + mockStore. + EXPECT(). + CreateIndex(createOpts.ProjectID, createOpts.clusterName, index). + Return(nil). + Times(1) + + if err := createOpts.Run(); err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } +} + func TestCreateBuilder(t *testing.T) { test.CmdValidator( t, CreateBuilder(), 0, - []string{flag.ClusterName, flag.Database, flag.Collection, flag.Key, flag.Sparse, flag.ProjectID}, + []string{flag.ClusterName, flag.Database, flag.Collection, flag.Key, flag.Sparse, flag.ProjectID, flag.File}, ) } diff --git a/internal/usage/usage.go b/internal/usage/usage.go index 0cac44a91b..a482fdc2ee 100644 --- a/internal/usage/usage.go +++ b/internal/usage/usage.go @@ -107,6 +107,7 @@ dbName and collection are required only for built-in roles.` MaxDate = "Maximum created date. This option returns events whose created date is less than or equal to the specified value." MinDate = "Minimum created date. This option returns events whose created date is greater than or equal to the specified value." ClusterFilename = "Path to an optional JSON configuration file that defines cluster settings. To learn more about cluster configuration files for the Atlas CLI, see https://dochub.mongodb.org/core/cluster-config-file-atlascli. To learn more about cluster configuration files for MongoCLI, see https://dochub.mongodb.org/core/mms-cluster-settings-file-mcli." + IndexFilename = "Path to an optional JSON configuration file that defines index settings." BackupFilename = "Path to an optional JSON configuration file that defines backup schedule settings. To learn about the cloud backup configuration file for the Atlas CLI, see https://dochub.mongodb.org/core/cloud-backup-config-file." SearchFilename = "Name of the JSON index configuration file to use. To learn about the Atlas Search index configuration file, see https://dochub.mongodb.org/core/search-index-config-file-atlascli. To learn about the Atlas Search index syntax and options that you can define in your configuration file, see https://dochub.mongodb.org/core/index-definitions-fts." SearchNodesFilename = "Name of the JSON index configuration file to use." diff --git a/test/e2e/atlas/clusters_file_test.go b/test/e2e/atlas/clusters_file_test.go index 2b2c6cc27d..44f69e6f6e 100644 --- a/test/e2e/atlas/clusters_file_test.go +++ b/test/e2e/atlas/clusters_file_test.go @@ -83,12 +83,54 @@ func TestClustersFile(t *testing.T) { assert.Contains(t, string(resp), "Cluster available") }) + t.Run("Create Partial Index", func(t *testing.T) { + cmd := exec.Command(cliPath, + clustersEntity, + "indexes", + "create", + "--clusterName", clusterFileName, + "--file=data/create_partial_index.json", + "--projectId", g.projectID, + ) + cmd.Env = os.Environ() + resp, err := cmd.CombinedOutput() + require.NoError(t, err, string(resp)) + }) + + t.Run("Create Sparse Index", func(t *testing.T) { + cmd := exec.Command(cliPath, + clustersEntity, + "indexes", + "create", + "--clusterName", clusterFileName, + "--file=data/create_sparse_index.json", + "--projectId", g.projectID, + ) + cmd.Env = os.Environ() + resp, err := cmd.CombinedOutput() + require.NoError(t, err, string(resp)) + }) + + t.Run("Create 2dspere Index", func(t *testing.T) { + cmd := exec.Command(cliPath, + clustersEntity, + "indexes", + "create", + "--clusterName", clusterFileName, + "--file=data/create_2dspere_index.json", + "--projectId", g.projectID, + ) + cmd.Env = os.Environ() + resp, err := cmd.CombinedOutput() + require.NoError(t, err, string(resp)) + }) + t.Run("Update via file", func(t *testing.T) { cmd := exec.Command(cliPath, clustersEntity, "update", clusterFileName, - "--file=update_cluster_test.json", + "--file=data/update_cluster_test.json", "--projectId", g.projectID, "-o=json") @@ -141,9 +183,9 @@ func generateClusterFile(mdbVersion string) (string, error) { MongoDBMajorVersion: mdbVersion, } - templateFile := "create_cluster_test.json" + templateFile := "data/create_cluster_test.json" if service := os.Getenv("MCLI_SERVICE"); service == config.CloudGovService { - templateFile = "create_cluster_gov_test.json" + templateFile = "data/create_cluster_gov_test.json" } tmpl, err := template.ParseFiles(templateFile) @@ -156,7 +198,7 @@ func generateClusterFile(mdbVersion string) (string, error) { return "", err } - const clusterFile = "create_cluster.json" + const clusterFile = "data/create_cluster.json" file, err := os.Create(clusterFile) if err != nil { return "", err diff --git a/test/e2e/atlas/data/create_2dspere_index.json b/test/e2e/atlas/data/create_2dspere_index.json new file mode 100644 index 0000000000..a8ff014834 --- /dev/null +++ b/test/e2e/atlas/data/create_2dspere_index.json @@ -0,0 +1,24 @@ +{ + "collation": { + "alternate": "non-ignorable", + "backwards": false, + "caseFirst": "lower", + "caseLevel": false, + "locale": "af", + "maxVariable": "punct", + "normalization": false, + "numericOrdering": false, + "strength": 3 + }, + "collection": "accounts", + "db": "sample_airbnb", + "keys": [ + { + "test_field": "2dsphere" + } + ], + "options": { + "name": "2dspereIndexTest", + "2dsphereIndexVersion": 2 + } +} diff --git a/test/e2e/atlas/create_cluster_gov_test.json b/test/e2e/atlas/data/create_cluster_gov_test.json similarity index 100% rename from test/e2e/atlas/create_cluster_gov_test.json rename to test/e2e/atlas/data/create_cluster_gov_test.json diff --git a/test/e2e/atlas/create_cluster_test.json b/test/e2e/atlas/data/create_cluster_test.json similarity index 100% rename from test/e2e/atlas/create_cluster_test.json rename to test/e2e/atlas/data/create_cluster_test.json diff --git a/test/e2e/atlas/data/create_partial_index.json b/test/e2e/atlas/data/create_partial_index.json new file mode 100644 index 0000000000..5bd582fe40 --- /dev/null +++ b/test/e2e/atlas/data/create_partial_index.json @@ -0,0 +1,27 @@ +{ + "collation": { + "alternate": "non-ignorable", + "backwards": false, + "caseFirst": "lower", + "caseLevel": false, + "locale": "af", + "maxVariable": "punct", + "normalization": false, + "numericOrdering": false, + "strength": 3 + }, + "collection": "accounts", + "db": "sample_airbnb", + "keys": [ + { + "property_type": "1", + "room_type": "1" + } + ], + "options": { + "name": "PartialIndexTest", + "partialFilterExpression":{ + "limit": {"$gt": 900} + } + } +} diff --git a/test/e2e/atlas/data/create_sparse_index.json b/test/e2e/atlas/data/create_sparse_index.json new file mode 100644 index 0000000000..b5614a23a1 --- /dev/null +++ b/test/e2e/atlas/data/create_sparse_index.json @@ -0,0 +1,24 @@ +{ + "collation": { + "alternate": "non-ignorable", + "backwards": false, + "caseFirst": "lower", + "caseLevel": false, + "locale": "af", + "maxVariable": "punct", + "normalization": false, + "numericOrdering": false, + "strength": 3 + }, + "collection": "accounts", + "db": "sample_airbnb", + "keys": [ + { + "test_field": "1" + } + ], + "options": { + "name": "SparseIndexTest", + "sparse": true + } +} diff --git a/test/e2e/atlas/create_streams_connection_test.json b/test/e2e/atlas/data/create_streams_connection_test.json similarity index 100% rename from test/e2e/atlas/create_streams_connection_test.json rename to test/e2e/atlas/data/create_streams_connection_test.json diff --git a/test/e2e/atlas/sample_embedded_movies.json b/test/e2e/atlas/data/sample_embedded_movies.json similarity index 100% rename from test/e2e/atlas/sample_embedded_movies.json rename to test/e2e/atlas/data/sample_embedded_movies.json diff --git a/test/e2e/atlas/sample_vector_search.json b/test/e2e/atlas/data/sample_vector_search.json similarity index 100% rename from test/e2e/atlas/sample_vector_search.json rename to test/e2e/atlas/data/sample_vector_search.json diff --git a/test/e2e/atlas/sample_vector_search_pipeline.json b/test/e2e/atlas/data/sample_vector_search_pipeline.json similarity index 100% rename from test/e2e/atlas/sample_vector_search_pipeline.json rename to test/e2e/atlas/data/sample_vector_search_pipeline.json diff --git a/test/e2e/atlas/search_nodes_spec.json b/test/e2e/atlas/data/search_nodes_spec.json similarity index 100% rename from test/e2e/atlas/search_nodes_spec.json rename to test/e2e/atlas/data/search_nodes_spec.json diff --git a/test/e2e/atlas/search_nodes_spec_update.json b/test/e2e/atlas/data/search_nodes_spec_update.json similarity index 100% rename from test/e2e/atlas/search_nodes_spec_update.json rename to test/e2e/atlas/data/search_nodes_spec_update.json diff --git a/test/e2e/atlas/update_cluster_test.json b/test/e2e/atlas/data/update_cluster_test.json similarity index 100% rename from test/e2e/atlas/update_cluster_test.json rename to test/e2e/atlas/data/update_cluster_test.json diff --git a/test/e2e/atlas/update_streams_connection_test.json b/test/e2e/atlas/data/update_streams_connection_test.json similarity index 100% rename from test/e2e/atlas/update_streams_connection_test.json rename to test/e2e/atlas/data/update_streams_connection_test.json diff --git a/test/e2e/atlas/deployments_local_noauth_test.go b/test/e2e/atlas/deployments_local_noauth_test.go index ab2202c23c..131c289eab 100644 --- a/test/e2e/atlas/deployments_local_noauth_test.go +++ b/test/e2e/atlas/deployments_local_noauth_test.go @@ -154,7 +154,7 @@ func TestDeploymentsLocal(t *testing.T) { require.NoError(t, err) t.Log(ids) - b, err := os.ReadFile("sample_embedded_movies.json") + b, err := os.ReadFile("data/sample_embedded_movies.json") require.NoError(t, err) var movies []interface{} @@ -292,7 +292,7 @@ func TestDeploymentsLocal(t *testing.T) { "--type", "local", "--file", - "sample_vector_search.json", + "data/sample_vector_search.json", "-w", ) diff --git a/test/e2e/atlas/search_nodes_test.go b/test/e2e/atlas/search_nodes_test.go index 5d119d772a..b000ceff45 100644 --- a/test/e2e/atlas/search_nodes_test.go +++ b/test/e2e/atlas/search_nodes_test.go @@ -70,7 +70,7 @@ func TestSearchNodes(t *testing.T) { "create", "--clusterName", g.clusterName, "--projectId", g.projectID, - "--file", "search_nodes_spec.json", + "--file", "data/search_nodes_spec.json", "-w", "-o=json", ) @@ -128,7 +128,7 @@ func TestSearchNodes(t *testing.T) { "update", "--clusterName", g.clusterName, "--projectId", g.projectID, - "--file", "search_nodes_spec_update.json", + "--file", "data/search_nodes_spec_update.json", "-w", "-o=json", ) diff --git a/test/e2e/atlas/streams_test.go b/test/e2e/atlas/streams_test.go index ed49c10c94..b9b66adff2 100644 --- a/test/e2e/atlas/streams_test.go +++ b/test/e2e/atlas/streams_test.go @@ -177,7 +177,7 @@ func TestStreams(t *testing.T) { "create", connectionName, "-f", - "create_streams_connection_test.json", + "data/create_streams_connection_test.json", "-i", instanceName, "-o=json", @@ -255,7 +255,7 @@ func TestStreams(t *testing.T) { "update", connectionName, "-f", - "update_streams_connection_test.json", + "data/update_streams_connection_test.json", "-i", instanceName, "-o=json",