From a2ad64bf95a3a9a8e4ed7813b1c1deb13c38c63c Mon Sep 17 00:00:00 2001 From: Jesse Schmidt Date: Fri, 28 Jun 2024 15:26:07 -0700 Subject: [PATCH] WIP --- cmd/drop.go | 23 ----- cmd/dropIndex.go | 118 ---------------------- cmd/flags/client.go | 11 ++- cmd/flags/constants.go | 7 ++ cmd/{create.go => index.go} | 10 +- cmd/{createIndex.go => indexCreate.go} | 132 ++++++++++++------------- cmd/indexDrop.go | 114 +++++++++++++++++++++ cmd/{listIndex.go => indexList.go} | 44 ++++----- cmd/list.go | 23 ----- cmd/role.go | 21 ++++ cmd/rolesList.go | 94 ++++++++++++++++++ cmd/root.go | 4 + cmd/user.go | 21 ++++ cmd/userCreate.go | 110 +++++++++++++++++++++ cmd/userDrop.go | 93 +++++++++++++++++ cmd/userGrant.go | 99 +++++++++++++++++++ cmd/userList.go | 86 ++++++++++++++++ cmd/userNewPassword.go | 109 ++++++++++++++++++++ cmd/userRevoke.go | 108 ++++++++++++++++++++ cmd/utils.go | 26 +++-- cmd/view.go | 28 ++++++ cmd/writers/roleList.go | 35 +++++++ cmd/writers/userList.go | 36 +++++++ 23 files changed, 1082 insertions(+), 270 deletions(-) delete mode 100644 cmd/drop.go delete mode 100644 cmd/dropIndex.go rename cmd/{create.go => index.go} (60%) rename cmd/{createIndex.go => indexCreate.go} (71%) create mode 100644 cmd/indexDrop.go rename cmd/{listIndex.go => indexList.go} (63%) delete mode 100644 cmd/list.go create mode 100644 cmd/role.go create mode 100644 cmd/rolesList.go create mode 100644 cmd/user.go create mode 100644 cmd/userCreate.go create mode 100644 cmd/userDrop.go create mode 100644 cmd/userGrant.go create mode 100644 cmd/userList.go create mode 100644 cmd/userNewPassword.go create mode 100644 cmd/userRevoke.go create mode 100644 cmd/writers/roleList.go create mode 100644 cmd/writers/userList.go diff --git a/cmd/drop.go b/cmd/drop.go deleted file mode 100644 index a5648d9..0000000 --- a/cmd/drop.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "github.com/spf13/cobra" -) - -// dropCmd represents the drop command -var dropCmd = &cobra.Command{ - Use: "drop", - Short: "A parent command for dropping resources", - Long: `A parent command for dropping resources. It currently only supports dropping indexes. - For example: - export ASVEC_HOST=:5000 - asvec drop index -i myindex -n test - `, -} - -func init() { - rootCmd.AddCommand(dropCmd) -} diff --git a/cmd/dropIndex.go b/cmd/dropIndex.go deleted file mode 100644 index d0c6306..0000000 --- a/cmd/dropIndex.go +++ /dev/null @@ -1,118 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "asvec/cmd/flags" - "context" - "fmt" - "log/slog" - "time" - - commonFlags "github.com/aerospike/tools-common-go/flags" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -//nolint:govet // Padding not a concern for a CLI -var dropIndexFlags = &struct { - clientFlags flags.ClientFlags - namespace string - sets []string - indexName string - timeout time.Duration -}{ - clientFlags: *flags.NewClientFlags(), -} - -func newDropIndexFlagSet() *pflag.FlagSet { - flagSet := &pflag.FlagSet{} - flagSet.StringVarP(&dropIndexFlags.namespace, flags.Namespace, "n", "", commonFlags.DefaultWrapHelpString("The namespace for the index.")) //nolint:lll // For readability - flagSet.StringSliceVarP(&dropIndexFlags.sets, flags.Sets, "s", nil, commonFlags.DefaultWrapHelpString("The sets for the index.")) //nolint:lll // For readability - flagSet.StringVarP(&dropIndexFlags.indexName, flags.IndexName, "i", "", commonFlags.DefaultWrapHelpString("The name of the index.")) //nolint:lll // For readability - flagSet.DurationVar(&dropIndexFlags.timeout, flags.Timeout, time.Second*5, commonFlags.DefaultWrapHelpString("The distance metric for the index.")) //nolint:lll // For readability - flagSet.AddFlagSet(dropIndexFlags.clientFlags.NewClientFlagSet()) - - return flagSet -} - -var dropIndexRequiredFlags = []string{ - flags.Namespace, - flags.IndexName, -} - -// dropIndexCmd represents the dropIndex command -func newDropIndexCommand() *cobra.Command { - return &cobra.Command{ - Use: "index", - Short: "A command for dropping indexes", - Long: `A command for dropping indexes. Deleting an index will free up - storage but will also disable vector search on your data. - - For example: - export ASVEC_HOST=:5000 - asvec drop index -i myindex -n test - `, - PreRunE: func(_ *cobra.Command, _ []string) error { - if viper.IsSet(flags.Seeds) && viper.IsSet(flags.Host) { - return fmt.Errorf("only --%s or --%s allowed", flags.Seeds, flags.Host) - } - - return nil - }, - RunE: func(_ *cobra.Command, _ []string) error { - logger.Debug("parsed flags", - append(dropIndexFlags.clientFlags.NewSLogAttr(), - slog.String(flags.Namespace, dropIndexFlags.namespace), - slog.Any(flags.Sets, dropIndexFlags.sets), - slog.String(flags.IndexName, dropIndexFlags.indexName), - slog.Duration(flags.Timeout, dropIndexFlags.timeout), - )..., - ) - - adminClient, err := createClientFromFlags(&dropIndexFlags.clientFlags, dropIndexFlags.timeout) - if err != nil { - return err - } - defer adminClient.Close() - - if !confirm(fmt.Sprintf( - "Are you sure you want to drop the index %s on field %s?", - nsAndSetString( - createIndexFlags.namespace, - createIndexFlags.sets, - ), - createIndexFlags.vectorField, - )) { - return nil - } - - ctx, cancel := context.WithTimeout(context.Background(), dropIndexFlags.timeout) - defer cancel() - - err = adminClient.IndexDrop(ctx, dropIndexFlags.namespace, dropIndexFlags.indexName) - if err != nil { - logger.Error("unable to drop index", slog.Any("error", err)) - return err - } - - view.Printf("Successfully dropped index %s.%s", dropIndexFlags.namespace, dropIndexFlags.indexName) - return nil - }, - } -} - -func init() { - dropIndexCmd := newDropIndexCommand() - dropCmd.AddCommand(dropIndexCmd) - dropIndexCmd.Flags().AddFlagSet(newDropIndexFlagSet()) - - for _, flag := range dropIndexRequiredFlags { - err := dropIndexCmd.MarkFlagRequired(flag) - if err != nil { - panic(err) - } - } -} diff --git a/cmd/flags/client.go b/cmd/flags/client.go index 45d410e..bea7a98 100644 --- a/cmd/flags/client.go +++ b/cmd/flags/client.go @@ -3,6 +3,7 @@ package flags import ( "fmt" "log/slog" + "time" commonFlags "github.com/aerospike/tools-common-go/flags" "github.com/spf13/pflag" @@ -14,6 +15,7 @@ type ClientFlags struct { ListenerName StringOptionalFlag User StringOptionalFlag Password commonFlags.PasswordFlag + Timeout time.Duration TLSFlags } @@ -32,21 +34,28 @@ func (cf *ClientFlags) NewClientFlagSet() *pflag.FlagSet { flagSet.VarP(&cf.ListenerName, ListenerName, "l", commonFlags.DefaultWrapHelpString("The listener to ask the AVS server for as configured in the AVS server. Likely required for cloud deployments.")) //nolint:lll // For readability flagSet.VarP(&cf.User, User, "U", commonFlags.DefaultWrapHelpString("The AVS user to authenticate with.")) //nolint:lll // For readability flagSet.VarP(&cf.Password, Password, "P", commonFlags.DefaultWrapHelpString("The AVS password for the specified user.")) //nolint:lll // For readability + flagSet.DurationVar(&cf.Timeout, Timeout, time.Second*5, commonFlags.DefaultWrapHelpString("The timeout to use for each request to AVS")) //nolint:lll // For readability flagSet.AddFlagSet(cf.NewTLSFlagSet(commonFlags.DefaultWrapHelpString)) return flagSet } func (cf *ClientFlags) NewSLogAttr() []any { + logPass := "" + if cf.Password.String() != "" { + logPass = "*" + } + return []any{slog.String(Host, cf.Host.String()), slog.String(Seeds, cf.Seeds.String()), slog.String(ListenerName, cf.ListenerName.String()), slog.String(User, cf.User.String()), - slog.String(Password, cf.Password.String()), + slog.String(Password, logPass), slog.Bool(TLSCaFile, cf.TLSRootCAFile != nil), slog.Bool(TLSCaPath, cf.TLSRootCAPath != nil), slog.Bool(TLSCertFile, cf.TLSCertFile != nil), slog.Bool(TLSKeyFile, cf.TLSKeyFile != nil), slog.Bool(TLSKeyFilePass, cf.TLSKeyFilePass != nil), + slog.Duration(Timeout, cf.Timeout), } } diff --git a/cmd/flags/constants.go b/cmd/flags/constants.go index 888f67b..0b3161d 100644 --- a/cmd/flags/constants.go +++ b/cmd/flags/constants.go @@ -7,6 +7,13 @@ const ( ListenerName = "listener-name" User = "user" Password = "password" + Username = "name" + NewUser = "create-user" + DropUser = "drop-user" + GrantUser = "grant-user" + RevokeUser = "revoke-user" + NewPassword = "new-password" + Roles = "roles" Namespace = "namespace" Sets = "sets" IndexName = "index-name" diff --git a/cmd/create.go b/cmd/index.go similarity index 60% rename from cmd/create.go rename to cmd/index.go index aee73bc..2f900d2 100644 --- a/cmd/create.go +++ b/cmd/index.go @@ -7,17 +7,17 @@ import ( "github.com/spf13/cobra" ) -// createCmd represents the create command -var createCmd = &cobra.Command{ - Use: "create", +// indexCmd represents the create command +var indexCmd = &cobra.Command{ + Use: "index", Short: "A parent command for creating resources", Long: `A parent command for creating resources. It currently only supports creating indexes. For example: export ASVEC_HOST=:5000 - asvec create index -i myindex -n test -s testset -f vector-field -d 256 -m COSINE + asvec index ---help `, } func init() { - rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(indexCmd) } diff --git a/cmd/createIndex.go b/cmd/indexCreate.go similarity index 71% rename from cmd/createIndex.go rename to cmd/indexCreate.go index d178b20..a57c8c9 100644 --- a/cmd/createIndex.go +++ b/cmd/indexCreate.go @@ -9,7 +9,6 @@ import ( "fmt" "log/slog" "strings" - "time" "github.com/aerospike/avs-client-go/protos" commonFlags "github.com/aerospike/tools-common-go/flags" @@ -19,7 +18,7 @@ import ( ) //nolint:govet // Padding not a concern for a CLI -var createIndexFlags = &struct { +var indexCreateFlags = &struct { clientFlags flags.ClientFlags namespace string sets []string @@ -36,7 +35,6 @@ var createIndexFlags = &struct { hnswBatchMaxRecords flags.Uint32OptionalFlag hnswBatchInterval flags.Uint32OptionalFlag hnswBatchEnabled flags.BoolOptionalFlag - timeout time.Duration }{ clientFlags: *flags.NewClientFlags(), storageNamespace: flags.StringOptionalFlag{}, @@ -49,30 +47,29 @@ var createIndexFlags = &struct { hnswBatchEnabled: flags.BoolOptionalFlag{}, } -func newCreateIndexFlagSet() *pflag.FlagSet { +func newIndexCreateFlagSet() *pflag.FlagSet { flagSet := &pflag.FlagSet{} //nolint:lll // For readability - flagSet.StringVarP(&createIndexFlags.namespace, flags.Namespace, "n", "", commonFlags.DefaultWrapHelpString("The namespace for the index.")) //nolint:lll // For readability - flagSet.StringSliceVarP(&createIndexFlags.sets, flags.Sets, "s", nil, commonFlags.DefaultWrapHelpString("The sets for the index.")) //nolint:lll // For readability - flagSet.StringVarP(&createIndexFlags.indexName, flags.IndexName, "i", "", commonFlags.DefaultWrapHelpString("The name of the index.")) //nolint:lll // For readability - flagSet.StringVarP(&createIndexFlags.vectorField, flags.VectorField, "f", "", commonFlags.DefaultWrapHelpString("The name of the vector field.")) //nolint:lll // For readability - flagSet.Uint32VarP(&createIndexFlags.dimensions, flags.Dimension, "d", 0, commonFlags.DefaultWrapHelpString("The dimension of the vector field.")) //nolint:lll // For readability - flagSet.VarP(&createIndexFlags.distanceMetric, flags.DistanceMetric, "m", commonFlags.DefaultWrapHelpString(fmt.Sprintf("The distance metric for the index. Valid values: %s", strings.Join(flags.DistanceMetricEnum(), ", ")))) //nolint:lll // For readability - flagSet.StringToStringVar(&createIndexFlags.indexMeta, flags.IndexMeta, nil, commonFlags.DefaultWrapHelpString("The distance metric for the index.")) //nolint:lll // For readability - flagSet.DurationVar(&createIndexFlags.timeout, flags.Timeout, time.Second*5, commonFlags.DefaultWrapHelpString("The distance metric for the index.")) //nolint:lll // For readability - flagSet.Var(&createIndexFlags.storageNamespace, flags.StorageNamespace, commonFlags.DefaultWrapHelpString("Optional storage namespace where the index is stored. Defaults to the index namespace.")) //nolint:lll // For readability //nolint:lll // For readability - flagSet.Var(&createIndexFlags.storageSet, flags.StorageSet, commonFlags.DefaultWrapHelpString("Optional storage set where the index is stored. Defaults to the index name.")) //nolint:lll // For readability //nolint:lll // For readability - flagSet.Var(&createIndexFlags.hnswMaxEdges, flags.MaxEdges, commonFlags.DefaultWrapHelpString("Maximum number bi-directional links per HNSW vertex. Greater values of 'm' in general provide better recall for data with high dimensionality, while lower values work well for data with lower dimensionality. The storage space required for the index increases proportionally with 'm'")) //nolint:lll // For readability - flagSet.Var(&createIndexFlags.hnswConstructionEf, flags.ConstructionEf, commonFlags.DefaultWrapHelpString("The number of candidate nearest neighbors shortlisted during index creation. Larger values provide better recall at the cost of longer index update times. The default is 100.")) //nolint:lll // For readability - flagSet.Var(&createIndexFlags.hnswEf, flags.Ef, commonFlags.DefaultWrapHelpString("The default number of candidate nearest neighbors shortlisted during search. Larger values provide better recall at the cost of longer search times. The default is 100.")) //nolint:lll // For readability - flagSet.Var(&createIndexFlags.hnswBatchMaxRecords, flags.BatchMaxRecords, commonFlags.DefaultWrapHelpString("Maximum number of records to fit in a batch. The default value is 10000.")) //nolint:lll // For readability - flagSet.Var(&createIndexFlags.hnswBatchInterval, flags.BatchInterval, commonFlags.DefaultWrapHelpString("The maximum amount of time in milliseconds to wait before finalizing a batch. The default value is 10000.")) //nolint:lll // For readability - flagSet.Var(&createIndexFlags.hnswBatchEnabled, flags.BatchEnabled, commonFlags.DefaultWrapHelpString("Enables batching for index updates. Default is true meaning batching is enabled.")) //nolint:lll // For readability - flagSet.AddFlagSet(createIndexFlags.clientFlags.NewClientFlagSet()) + flagSet.StringVarP(&indexCreateFlags.namespace, flags.Namespace, "n", "", commonFlags.DefaultWrapHelpString("The namespace for the index.")) //nolint:lll // For readability + flagSet.StringSliceVarP(&indexCreateFlags.sets, flags.Sets, "s", nil, commonFlags.DefaultWrapHelpString("The sets for the index.")) //nolint:lll // For readability + flagSet.StringVarP(&indexCreateFlags.indexName, flags.IndexName, "i", "", commonFlags.DefaultWrapHelpString("The name of the index.")) //nolint:lll // For readability + flagSet.StringVarP(&indexCreateFlags.vectorField, flags.VectorField, "f", "", commonFlags.DefaultWrapHelpString("The name of the vector field.")) //nolint:lll // For readability + flagSet.Uint32VarP(&indexCreateFlags.dimensions, flags.Dimension, "d", 0, commonFlags.DefaultWrapHelpString("The dimension of the vector field.")) //nolint:lll // For readability + flagSet.VarP(&indexCreateFlags.distanceMetric, flags.DistanceMetric, "m", commonFlags.DefaultWrapHelpString(fmt.Sprintf("The distance metric for the index. Valid values: %s", strings.Join(flags.DistanceMetricEnum(), ", ")))) //nolint:lll // For readability + flagSet.StringToStringVar(&indexCreateFlags.indexMeta, flags.IndexMeta, nil, commonFlags.DefaultWrapHelpString("The distance metric for the index.")) //nolint:lll // For readability //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.storageNamespace, flags.StorageNamespace, commonFlags.DefaultWrapHelpString("Optional storage namespace where the index is stored. Defaults to the index namespace.")) //nolint:lll // For readability //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.storageSet, flags.StorageSet, commonFlags.DefaultWrapHelpString("Optional storage set where the index is stored. Defaults to the index name.")) //nolint:lll // For readability //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.hnswMaxEdges, flags.MaxEdges, commonFlags.DefaultWrapHelpString("Maximum number bi-directional links per HNSW vertex. Greater values of 'm' in general provide better recall for data with high dimensionality, while lower values work well for data with lower dimensionality. The storage space required for the index increases proportionally with 'm'")) //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.hnswConstructionEf, flags.ConstructionEf, commonFlags.DefaultWrapHelpString("The number of candidate nearest neighbors shortlisted during index creation. Larger values provide better recall at the cost of longer index update times. The default is 100.")) //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.hnswEf, flags.Ef, commonFlags.DefaultWrapHelpString("The default number of candidate nearest neighbors shortlisted during search. Larger values provide better recall at the cost of longer search times. The default is 100.")) //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.hnswBatchMaxRecords, flags.BatchMaxRecords, commonFlags.DefaultWrapHelpString("Maximum number of records to fit in a batch. The default value is 10000.")) //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.hnswBatchInterval, flags.BatchInterval, commonFlags.DefaultWrapHelpString("The maximum amount of time in milliseconds to wait before finalizing a batch. The default value is 10000.")) //nolint:lll // For readability + flagSet.Var(&indexCreateFlags.hnswBatchEnabled, flags.BatchEnabled, commonFlags.DefaultWrapHelpString("Enables batching for index updates. Default is true meaning batching is enabled.")) //nolint:lll // For readability + flagSet.AddFlagSet(indexCreateFlags.clientFlags.NewClientFlagSet()) return flagSet } -var createIndexRequiredFlags = []string{ +var indexCreateRequiredFlags = []string{ flags.Namespace, flags.IndexName, flags.VectorField, @@ -81,9 +78,9 @@ var createIndexRequiredFlags = []string{ } // createIndexCmd represents the createIndex command -func newCreateIndexCmd() *cobra.Command { +func newIndexCreateCmd() *cobra.Command { return &cobra.Command{ - Use: "index", + Use: "create", Short: "A command for creating indexes", Long: `A command for creating indexes. An index is required to enable vector search on your data. The index tells AVS where your data is located, @@ -93,7 +90,7 @@ func newCreateIndexCmd() *cobra.Command { For example: export ASVEC_HOST=:5000 - asvec create index -i myindex -n test -s testset -d 256 -m COSINE --vector-field vector \ + asvec index create -i myindex -n test -s testset -d 256 -m COSINE --vector-field vector \ --storage-namespace test --hnsw-batch-enabled false `, PreRunE: func(_ *cobra.Command, _ []string) error { @@ -105,27 +102,26 @@ func newCreateIndexCmd() *cobra.Command { }, RunE: func(_ *cobra.Command, _ []string) error { logger.Debug("parsed flags", - append(createIndexFlags.clientFlags.NewSLogAttr(), - slog.String(flags.Namespace, createIndexFlags.namespace), - slog.Any(flags.Sets, createIndexFlags.sets), - slog.String(flags.IndexName, createIndexFlags.indexName), - slog.String(flags.VectorField, createIndexFlags.vectorField), - slog.Uint64(flags.Dimension, uint64(createIndexFlags.dimensions)), - slog.Any(flags.IndexMeta, createIndexFlags.indexMeta), - slog.String(flags.DistanceMetric, createIndexFlags.distanceMetric.String()), - slog.Duration(flags.Timeout, createIndexFlags.timeout), - slog.Any(flags.StorageNamespace, createIndexFlags.storageNamespace.String()), - slog.Any(flags.StorageSet, createIndexFlags.storageSet.String()), - slog.Any(flags.MaxEdges, createIndexFlags.hnswMaxEdges.String()), - slog.Any(flags.Ef, createIndexFlags.hnswEf), - slog.Any(flags.ConstructionEf, createIndexFlags.hnswConstructionEf.String()), - slog.Any(flags.BatchMaxRecords, createIndexFlags.hnswBatchMaxRecords.String()), - slog.Any(flags.BatchInterval, createIndexFlags.hnswBatchInterval.String()), - slog.Any(flags.BatchEnabled, createIndexFlags.hnswBatchEnabled.String()), + append(indexCreateFlags.clientFlags.NewSLogAttr(), + slog.String(flags.Namespace, indexCreateFlags.namespace), + slog.Any(flags.Sets, indexCreateFlags.sets), + slog.String(flags.IndexName, indexCreateFlags.indexName), + slog.String(flags.VectorField, indexCreateFlags.vectorField), + slog.Uint64(flags.Dimension, uint64(indexCreateFlags.dimensions)), + slog.Any(flags.IndexMeta, indexCreateFlags.indexMeta), + slog.String(flags.DistanceMetric, indexCreateFlags.distanceMetric.String()), + slog.Any(flags.StorageNamespace, indexCreateFlags.storageNamespace.String()), + slog.Any(flags.StorageSet, indexCreateFlags.storageSet.String()), + slog.Any(flags.MaxEdges, indexCreateFlags.hnswMaxEdges.String()), + slog.Any(flags.Ef, indexCreateFlags.hnswEf), + slog.Any(flags.ConstructionEf, indexCreateFlags.hnswConstructionEf.String()), + slog.Any(flags.BatchMaxRecords, indexCreateFlags.hnswBatchMaxRecords.String()), + slog.Any(flags.BatchInterval, indexCreateFlags.hnswBatchInterval.String()), + slog.Any(flags.BatchEnabled, indexCreateFlags.hnswBatchEnabled.String()), )..., ) - adminClient, err := createClientFromFlags(&createIndexFlags.clientFlags, createIndexFlags.timeout) + adminClient, err := createClientFromFlags(&indexCreateFlags.clientFlags) if err != nil { return err } @@ -133,23 +129,23 @@ func newCreateIndexCmd() *cobra.Command { // Inverted to make it easier to understand var hnswBatchDisabled *bool - if createIndexFlags.hnswBatchEnabled.Val != nil { - bd := !(*createIndexFlags.hnswBatchEnabled.Val) + if indexCreateFlags.hnswBatchEnabled.Val != nil { + bd := !(*indexCreateFlags.hnswBatchEnabled.Val) hnswBatchDisabled = &bd } indexStorage := &protos.IndexStorage{ - Namespace: createIndexFlags.storageNamespace.Val, - Set: createIndexFlags.storageSet.Val, + Namespace: indexCreateFlags.storageNamespace.Val, + Set: indexCreateFlags.storageSet.Val, } hnswParams := &protos.HnswParams{ - M: createIndexFlags.hnswMaxEdges.Val, - Ef: createIndexFlags.hnswEf.Val, - EfConstruction: createIndexFlags.hnswConstructionEf.Val, + M: indexCreateFlags.hnswMaxEdges.Val, + Ef: indexCreateFlags.hnswEf.Val, + EfConstruction: indexCreateFlags.hnswConstructionEf.Val, BatchingParams: &protos.HnswBatchingParams{ - MaxRecords: createIndexFlags.hnswBatchMaxRecords.Val, - Interval: createIndexFlags.hnswBatchInterval.Val, + MaxRecords: indexCreateFlags.hnswBatchMaxRecords.Val, + Interval: indexCreateFlags.hnswBatchInterval.Val, Disabled: hnswBatchDisabled, }, } @@ -157,27 +153,27 @@ func newCreateIndexCmd() *cobra.Command { if !confirm(fmt.Sprintf( "Are you sure you want to create the index %s field %s?", nsAndSetString( - createIndexFlags.namespace, - createIndexFlags.sets, + indexCreateFlags.namespace, + indexCreateFlags.sets, ), - createIndexFlags.vectorField, + indexCreateFlags.vectorField, )) { return nil } - ctx, cancel := context.WithTimeout(context.Background(), createIndexFlags.timeout) + ctx, cancel := context.WithTimeout(context.Background(), indexCreateFlags.clientFlags.Timeout) defer cancel() err = adminClient.IndexCreate( ctx, - createIndexFlags.namespace, - createIndexFlags.sets, - createIndexFlags.indexName, - createIndexFlags.vectorField, - createIndexFlags.dimensions, - protos.VectorDistanceMetric(protos.VectorDistanceMetric_value[createIndexFlags.distanceMetric.String()]), + indexCreateFlags.namespace, + indexCreateFlags.sets, + indexCreateFlags.indexName, + indexCreateFlags.vectorField, + indexCreateFlags.dimensions, + protos.VectorDistanceMetric(protos.VectorDistanceMetric_value[indexCreateFlags.distanceMetric.String()]), hnswParams, - createIndexFlags.indexMeta, + indexCreateFlags.indexMeta, indexStorage, ) if err != nil { @@ -185,23 +181,23 @@ func newCreateIndexCmd() *cobra.Command { return err } - view.Printf("Successfully created index %s.%s", createIndexFlags.namespace, createIndexFlags.indexName) + view.Printf("Successfully created index %s.%s", indexCreateFlags.namespace, indexCreateFlags.indexName) return nil }, } } func init() { - createIndexCmd := newCreateIndexCmd() - createCmd.AddCommand(createIndexCmd) + createIndexCmd := newIndexCreateCmd() + indexCmd.AddCommand(createIndexCmd) // TODO: Add custom template for usage to take into account terminal width // Ex: https://github.com/sigstore/cosign/pull/3011/files - flagSet := newCreateIndexFlagSet() + flagSet := newIndexCreateFlagSet() createIndexCmd.Flags().AddFlagSet(flagSet) - for _, flag := range createIndexRequiredFlags { + for _, flag := range indexCreateRequiredFlags { err := createIndexCmd.MarkFlagRequired(flag) if err != nil { panic(err) diff --git a/cmd/indexDrop.go b/cmd/indexDrop.go new file mode 100644 index 0000000..f25c7e4 --- /dev/null +++ b/cmd/indexDrop.go @@ -0,0 +1,114 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "fmt" + "log/slog" + + commonFlags "github.com/aerospike/tools-common-go/flags" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +//nolint:govet // Padding not a concern for a CLI +var indexDropFlags = &struct { + clientFlags flags.ClientFlags + namespace string + sets []string + indexName string +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newIndexDropFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} + flagSet.StringVarP(&indexDropFlags.namespace, flags.Namespace, "n", "", commonFlags.DefaultWrapHelpString("The namespace for the index.")) //nolint:lll // For readability + flagSet.StringSliceVarP(&indexDropFlags.sets, flags.Sets, "s", nil, commonFlags.DefaultWrapHelpString("The sets for the index.")) //nolint:lll // For readability + flagSet.StringVarP(&indexDropFlags.indexName, flags.IndexName, "i", "", commonFlags.DefaultWrapHelpString("The name of the index.")) //nolint:lll // For readability + flagSet.AddFlagSet(indexDropFlags.clientFlags.NewClientFlagSet()) + + return flagSet +} + +var indexDropRequiredFlags = []string{ + flags.Namespace, + flags.IndexName, +} + +// dropIndexCmd represents the dropIndex command +func newIndexDropCommand() *cobra.Command { + return &cobra.Command{ + Use: "drop", + Short: "A command for dropping indexes", + Long: `A command for dropping indexes. Deleting an index will free up + storage but will also disable vector search on your data. + + For example: + export ASVEC_HOST=:5000 + asvec index drop -i myindex -n test + `, + PreRunE: func(_ *cobra.Command, _ []string) error { + if viper.IsSet(flags.Seeds) && viper.IsSet(flags.Host) { + return fmt.Errorf("only --%s or --%s allowed", flags.Seeds, flags.Host) + } + + return nil + }, + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + append(indexDropFlags.clientFlags.NewSLogAttr(), + slog.String(flags.Namespace, indexDropFlags.namespace), + slog.Any(flags.Sets, indexDropFlags.sets), + slog.String(flags.IndexName, indexDropFlags.indexName), + )..., + ) + + adminClient, err := createClientFromFlags(&indexDropFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + if !confirm(fmt.Sprintf( + "Are you sure you want to drop the index %s on field %s?", + nsAndSetString( + indexCreateFlags.namespace, + indexCreateFlags.sets, + ), + indexCreateFlags.vectorField, + )) { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), indexDropFlags.clientFlags.Timeout) + defer cancel() + + err = adminClient.IndexDrop(ctx, indexDropFlags.namespace, indexDropFlags.indexName) + if err != nil { + logger.Error("unable to drop index", slog.Any("error", err)) + return err + } + + view.Printf("Successfully dropped index %s.%s", indexDropFlags.namespace, indexDropFlags.indexName) + return nil + }, + } +} + +func init() { + indexDropCmd := newIndexDropCommand() + indexCmd.AddCommand(indexDropCmd) + indexDropCmd.Flags().AddFlagSet(newIndexDropFlagSet()) + + for _, flag := range indexDropRequiredFlags { + err := indexDropCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/listIndex.go b/cmd/indexList.go similarity index 63% rename from cmd/listIndex.go rename to cmd/indexList.go index 5bd334e..13ec41c 100644 --- a/cmd/listIndex.go +++ b/cmd/indexList.go @@ -9,7 +9,6 @@ import ( "fmt" "log/slog" "sync" - "time" "github.com/aerospike/avs-client-go/protos" commonFlags "github.com/aerospike/tools-common-go/flags" @@ -18,36 +17,34 @@ import ( "github.com/spf13/viper" ) -var listIndexFlags = &struct { +var indexListFlags = &struct { clientFlags flags.ClientFlags verbose bool - timeout time.Duration }{ clientFlags: *flags.NewClientFlags(), } -func newListIndexFlagSet() *pflag.FlagSet { +func newIndexListFlagSet() *pflag.FlagSet { flagSet := &pflag.FlagSet{} - flagSet.BoolVarP(&listIndexFlags.verbose, flags.Verbose, "v", false, commonFlags.DefaultWrapHelpString("Print detailed index information.")) //nolint:lll // For readability - flagSet.DurationVar(&listIndexFlags.timeout, flags.Timeout, time.Second*5, commonFlags.DefaultWrapHelpString("The distance metric for the index.")) //nolint:lll // For readability - flagSet.AddFlagSet(listIndexFlags.clientFlags.NewClientFlagSet()) + flagSet.BoolVarP(&indexListFlags.verbose, flags.Verbose, "v", false, commonFlags.DefaultWrapHelpString("Print detailed index information.")) //nolint:lll // For readability + flagSet.AddFlagSet(indexListFlags.clientFlags.NewClientFlagSet()) return flagSet } -var listIndexRequiredFlags = []string{} +var indexListRequiredFlags = []string{} // listIndexCmd represents the listIndex command -func newListIndexCmd() *cobra.Command { +func newIndexListCmd() *cobra.Command { return &cobra.Command{ - Use: "index", - Aliases: []string{"indexes"}, + Use: "list", + Aliases: []string{"ls"}, Short: "A command for listing indexes", Long: fmt.Sprintf(`A command for displaying useful information about AVS indexes. To display additional index information use the --%s flag. For example: export ASVEC_HOST=:5000 - asvec list index + asvec index list `, flags.Verbose), PreRunE: func(_ *cobra.Command, _ []string) error { if viper.IsSet(flags.Seeds) && viper.IsSet(flags.Host) { @@ -58,19 +55,18 @@ func newListIndexCmd() *cobra.Command { }, RunE: func(_ *cobra.Command, _ []string) error { logger.Debug("parsed flags", - append(listIndexFlags.clientFlags.NewSLogAttr(), - slog.Bool(flags.Verbose, listIndexFlags.verbose), - slog.Duration(flags.Timeout, listIndexFlags.timeout), + append(indexListFlags.clientFlags.NewSLogAttr(), + slog.Bool(flags.Verbose, indexListFlags.verbose), )..., ) - adminClient, err := createClientFromFlags(&listIndexFlags.clientFlags, listIndexFlags.timeout) + adminClient, err := createClientFromFlags(&indexListFlags.clientFlags) if err != nil { return err } defer adminClient.Close() - ctx, cancel := context.WithTimeout(context.Background(), listIndexFlags.timeout) + ctx, cancel := context.WithTimeout(context.Background(), indexListFlags.clientFlags.Timeout) defer cancel() indexList, err := adminClient.IndexList(ctx) @@ -83,7 +79,7 @@ func newListIndexCmd() *cobra.Command { cancel() - ctx, cancel = context.WithTimeout(context.Background(), listIndexFlags.timeout) + ctx, cancel = context.WithTimeout(context.Background(), indexListFlags.clientFlags.Timeout) defer cancel() wg := sync.WaitGroup{} @@ -110,7 +106,7 @@ func newListIndexCmd() *cobra.Command { logger.Debug("server index list", slog.String("response", indexList.String())) - view.PrintIndexes(indexList, indexStatusList, listIndexFlags.verbose) + view.PrintIndexes(indexList, indexStatusList, indexListFlags.verbose) return nil }, @@ -118,13 +114,13 @@ func newListIndexCmd() *cobra.Command { } func init() { - listIndexCmd := newListIndexCmd() + indexListCmd := newIndexListCmd() - listCmd.AddCommand(listIndexCmd) - listIndexCmd.Flags().AddFlagSet(newListIndexFlagSet()) + indexCmd.AddCommand(indexListCmd) + indexListCmd.Flags().AddFlagSet(newIndexListFlagSet()) - for _, flag := range listIndexRequiredFlags { - err := listIndexCmd.MarkFlagRequired(flag) + for _, flag := range indexListRequiredFlags { + err := indexListCmd.MarkFlagRequired(flag) if err != nil { panic(err) } diff --git a/cmd/list.go b/cmd/list.go deleted file mode 100644 index 7f649df..0000000 --- a/cmd/list.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package cmd - -import ( - "github.com/spf13/cobra" -) - -// listCmd represents the list command -var listCmd = &cobra.Command{ - Use: "list", - Short: "A parent command for listing resources", - Long: `A parent command for listings resources. It currently only supports listing indexes. - For example: - export ASVEC_HOST=:5000 - asvec list index - `, -} - -func init() { - rootCmd.AddCommand(listCmd) -} diff --git a/cmd/role.go b/cmd/role.go new file mode 100644 index 0000000..a2bfcc2 --- /dev/null +++ b/cmd/role.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// userCmd represents the create command +var roleCmd = &cobra.Command{ + Use: "role", + Aliases: []string{"roles"}, + Short: "A parent command for viewing roles.", + Long: `A parent command for listing, creating, dropping, and granting roles to users. + For example: + export ASVEC_HOST=:5000 + asvec user list + `, +} + +func init() { + rootCmd.AddCommand(roleCmd) +} diff --git a/cmd/rolesList.go b/cmd/rolesList.go new file mode 100644 index 0000000..5c0711b --- /dev/null +++ b/cmd/rolesList.go @@ -0,0 +1,94 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "fmt" + "log/slog" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var rolesListFlags = &struct { + clientFlags flags.ClientFlags +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newRoleListFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} + + flagSet.AddFlagSet(rolesListFlags.clientFlags.NewClientFlagSet()) + + return flagSet +} + +var roleListRequiredFlags = []string{} + +// listIndexCmd represents the listIndex command +func newRoleListCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "A command for listing users", + Long: `A command for displaying useful information about AVS users. + For example: + export ASVEC_HOST=:5000 + asvec user list + `, + PreRunE: func(_ *cobra.Command, _ []string) error { + if viper.IsSet(flags.Seeds) && viper.IsSet(flags.Host) { + return fmt.Errorf("only --%s or --%s allowed", flags.Seeds, flags.Host) + } + + return nil + }, + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + rolesListFlags.clientFlags.NewSLogAttr()..., + ) + + adminClient, err := createClientFromFlags(&rolesListFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), rolesListFlags.clientFlags.Timeout) + defer cancel() + + userList, err := adminClient.ListRoles(ctx) + if err != nil { + logger.Error("failed to list roles", slog.Any("error", err)) + return err + } + + cancel() + + logger.Debug("server role list", slog.String("response", userList.String())) + + view.PrintRoles(userList) + + return nil + }, + } +} + +func init() { + roleListCmd := newRoleListCmd() + + roleCmd.AddCommand(roleListCmd) + roleListCmd.Flags().AddFlagSet(newRoleListFlagSet()) + + for _, flag := range roleListRequiredFlags { + err := roleListCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/root.go b/cmd/root.go index c8b6668..c5e3674 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -48,6 +48,10 @@ var rootCmd = &cobra.Command{ handler.Enabled(context.Background(), lvl.Level()) } + if viper.IsSet(flags.Seeds) && viper.IsSet(flags.Host) { + return fmt.Errorf("only --%s or --%s allowed", flags.Seeds, flags.Host) + } + cmd.SilenceUsage = true if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { diff --git a/cmd/user.go b/cmd/user.go new file mode 100644 index 0000000..1cda286 --- /dev/null +++ b/cmd/user.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// userCmd represents the create command +var userCmd = &cobra.Command{ + Use: "user", + Aliases: []string{"users"}, + Short: "A parent command for viewing and configuring users.", + Long: `A parent command for listing, creating, dropping, and granting roles to users. + For example: + export ASVEC_HOST=:5000 + asvec user list + `, +} + +func init() { + rootCmd.AddCommand(userCmd) +} diff --git a/cmd/userCreate.go b/cmd/userCreate.go new file mode 100644 index 0000000..bf6b40f --- /dev/null +++ b/cmd/userCreate.go @@ -0,0 +1,110 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "log/slog" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +//nolint:govet // Padding not a concern for a CLI +var userCreateFlags = &struct { + clientFlags flags.ClientFlags + newUsername string + newPassword string + roles []string +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newUserCreateFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} //nolint:lll // For readability //nolint:lll // For readability + flagSet.AddFlagSet(userCreateFlags.clientFlags.NewClientFlagSet()) + flagSet.StringVar(&userCreateFlags.newUsername, flags.NewUser, "", "TODO") + flagSet.StringVar(&userCreateFlags.newPassword, flags.NewPassword, "", "TODO") + flagSet.StringSliceVar(&userCreateFlags.roles, flags.Roles, []string{}, "TODO") + + return flagSet +} + +var userCreateRequiredFlags = []string{ + flags.NewUser, + flags.Roles, +} + +// createUserCmd represents the createIndex command +func newUserCreateCmd() *cobra.Command { + return &cobra.Command{ + Use: "create", + Short: "A command for creating users", + Long: `A command for creating users. TODO + + For example: + export ASVEC_HOST=127.0.0.1:5000 ASVEC_USER=admin + asvec user create --new-user foo --roles read-write + `, + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + append( + userCreateFlags.clientFlags.NewSLogAttr(), + slog.String(flags.NewUser, userCreateFlags.newUsername), + slog.Any(flags.Roles, userCreateFlags.roles), + )..., + ) + + adminClient, err := createClientFromFlags(&userCreateFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + if userCreateFlags.newPassword == "" { + userCreateFlags.newPassword, err = passwordPrompt("New User Password: ") + if err != nil { + logger.Error("failed to read new password", slog.Any("error", err)) + return err + } + } + + ctx, cancel := context.WithTimeout(context.Background(), userCreateFlags.clientFlags.Timeout) + defer cancel() + + err = adminClient.CreateUser( + ctx, + userCreateFlags.newUsername, + userCreateFlags.newPassword, + userCreateFlags.roles, + ) + if err != nil { + logger.Error("unable to create user", slog.String("user", userCreateFlags.newUsername), slog.Any("error", err)) + return err + } + + view.Printf("Successfully created user %s", userCreateFlags.newUsername) + return nil + }, + } +} + +func init() { + userCreateCmd := newUserCreateCmd() + userCmd.AddCommand(userCreateCmd) + + // TODO: Add custom template for usage to take into account terminal width + // Ex: https://github.com/sigstore/cosign/pull/3011/files + + flagSet := newUserCreateFlagSet() + userCreateCmd.Flags().AddFlagSet(flagSet) + + for _, flag := range userCreateRequiredFlags { + err := userCreateCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/userDrop.go b/cmd/userDrop.go new file mode 100644 index 0000000..26e185c --- /dev/null +++ b/cmd/userDrop.go @@ -0,0 +1,93 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "log/slog" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +//nolint:govet // Padding not a concern for a CLI +var userDropFlags = &struct { + clientFlags flags.ClientFlags + dropUser string +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newUserDropFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} //nolint:lll // For readability //nolint:lll // For readability + flagSet.AddFlagSet(userDropFlags.clientFlags.NewClientFlagSet()) + flagSet.StringVar(&userDropFlags.dropUser, flags.DropUser, "", "TODO") + + return flagSet +} + +var userDropRequiredFlags = []string{ + flags.DropUser, +} + +func newUserDropCmd() *cobra.Command { + return &cobra.Command{ + Use: "drop", + Short: "A command for dropping users", + Long: `A command for dropping users. TODO + + For example: + export ASVEC_HOST=127.0.0.1:5000 ASVEC_USER=admin + asvec user drop --new-user foo --roles read-write + `, + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + append( + userDropFlags.clientFlags.NewSLogAttr(), + slog.String(flags.NewUser, userDropFlags.dropUser), + )..., + ) + + adminClient, err := createClientFromFlags(&userDropFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), userDropFlags.clientFlags.Timeout) + defer cancel() + + err = adminClient.DropUser( + ctx, + userDropFlags.dropUser, + ) + if err != nil { + logger.Error("unable to create user", slog.String("user", userDropFlags.dropUser), slog.Any("error", err)) + return err + } + + view.Printf("Successfully dropped user %s", userDropFlags.dropUser) + return nil + }, + } +} + +func init() { + userDropCmd := newUserDropCmd() + userCmd.AddCommand(userDropCmd) + + // TODO: Add custom template for usage to take into account terminal width + // Ex: https://github.com/sigstore/cosign/pull/3011/files + + flagSet := newUserDropFlagSet() + userDropCmd.Flags().AddFlagSet(flagSet) + + for _, flag := range userDropRequiredFlags { + err := userDropCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/userGrant.go b/cmd/userGrant.go new file mode 100644 index 0000000..10445fa --- /dev/null +++ b/cmd/userGrant.go @@ -0,0 +1,99 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "log/slog" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +//nolint:govet // Padding not a concern for a CLI +var userGrantFlags = &struct { + clientFlags flags.ClientFlags + grantUser string + roles []string +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newUserGrantFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} //nolint:lll // For readability //nolint:lll // For readability + flagSet.AddFlagSet(userGrantFlags.clientFlags.NewClientFlagSet()) + flagSet.StringVar(&userGrantFlags.grantUser, flags.GrantUser, "", "TODO") + flagSet.StringSliceVar(&userGrantFlags.roles, flags.Roles, []string{}, "TODO") + + return flagSet +} + +var userGrantRequiredFlags = []string{ + flags.GrantUser, + flags.Roles, +} + +func newUserGrantCmd() *cobra.Command { + return &cobra.Command{ + Use: "grant", + Short: "A command for granting users roles", + Long: `A command for creating users. TODO + + For example: + export ASVEC_HOST=127.0.0.1:5000 ASVEC_USER=admin + asvec user grant --grant-user foo --roles admin + `, + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + append( + userGrantFlags.clientFlags.NewSLogAttr(), + slog.String(flags.NewUser, userGrantFlags.grantUser), + slog.Any(flags.Roles, userGrantFlags.roles), + )..., + ) + + adminClient, err := createClientFromFlags(&userGrantFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), userGrantFlags.clientFlags.Timeout) + defer cancel() + + err = adminClient.GrantRoles( + ctx, + userGrantFlags.grantUser, + userGrantFlags.roles, + ) + if err != nil { + logger.Error("unable to grant user roles", slog.String("user", userGrantFlags.grantUser), slog.Any("roles", userGrantFlags.roles), slog.Any("error", err)) + return err + } + + view.Printf("Successfully granted user %s roles %s", userGrantFlags.grantUser, strings.Join(userGrantFlags.roles, ", ")) + return nil + }, + } +} + +func init() { + userGrantCmd := newUserGrantCmd() + userCmd.AddCommand(userGrantCmd) + + // TODO: Add custom template for usage to take into account terminal width + // Ex: https://github.com/sigstore/cosign/pull/3011/files + + flagSet := newUserGrantFlagSet() + userGrantCmd.Flags().AddFlagSet(flagSet) + + for _, flag := range userGrantRequiredFlags { + err := userGrantCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/userList.go b/cmd/userList.go new file mode 100644 index 0000000..b6e523e --- /dev/null +++ b/cmd/userList.go @@ -0,0 +1,86 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "log/slog" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var userListFlags = &struct { + clientFlags flags.ClientFlags +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newUserListFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} + + flagSet.AddFlagSet(userListFlags.clientFlags.NewClientFlagSet()) + + return flagSet +} + +var userListRequiredFlags = []string{} + +// listIndexCmd represents the listIndex command +func newUserListCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "A command for listing users", + Long: `A command for displaying useful information about AVS users. + For example: + export ASVEC_HOST=:5000 + asvec user list + `, + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + userListFlags.clientFlags.NewSLogAttr()..., + ) + + adminClient, err := createClientFromFlags(&userListFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), userListFlags.clientFlags.Timeout) + defer cancel() + + userList, err := adminClient.ListUsers(ctx) + if err != nil { + logger.Error("failed to list users", slog.Any("error", err)) + return err + } + + cancel() + + logger.Debug("server user list", slog.String("response", userList.String())) + + view.PrintUsers(userList) + view.Print("Use 'role list' to view available roles") + + return nil + }, + } +} + +func init() { + userListCmd := newUserListCmd() + + userCmd.AddCommand(userListCmd) + userListCmd.Flags().AddFlagSet(newUserListFlagSet()) + + for _, flag := range userListRequiredFlags { + err := userListCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/userNewPassword.go b/cmd/userNewPassword.go new file mode 100644 index 0000000..7e267c0 --- /dev/null +++ b/cmd/userNewPassword.go @@ -0,0 +1,109 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "log/slog" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +//nolint:govet // Padding not a concern for a CLI +var userNewPassFlags = &struct { + clientFlags flags.ClientFlags + username string + password string + roles []string +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newUserNewPassFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} //nolint:lll // For readability //nolint:lll // For readability + flagSet.AddFlagSet(userNewPassFlags.clientFlags.NewClientFlagSet()) + flagSet.StringVar(&userNewPassFlags.username, flags.Username, "", "TODO") + flagSet.StringVar(&userNewPassFlags.password, flags.NewPassword, "", "TODO") + + return flagSet +} + +var userNewPassRequiredFlags = []string{ + flags.Username, +} + +// createUserCmd represents the createIndex command +func newUserNewPasswordCmd() *cobra.Command { + return &cobra.Command{ + Use: "new-password", + Aliases: []string{"new-pass"}, + Short: "A command for creating users", + Long: `A command for creating users. TODO + + For example: + export ASVEC_HOST=127.0.0.1:5000 ASVEC_USER=admin + asvec user new-password --name + `, + + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + append( + userNewPassFlags.clientFlags.NewSLogAttr(), + slog.String(flags.NewUser, userNewPassFlags.username), + slog.Any(flags.Roles, userNewPassFlags.roles), + )..., + ) + + adminClient, err := createClientFromFlags(&userNewPassFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + if userNewPassFlags.password == "" { + userNewPassFlags.password, err = passwordPrompt("New Password: ") + if err != nil { + logger.Error("failed to read new password", slog.Any("error", err)) + return err + } + } + + ctx, cancel := context.WithTimeout(context.Background(), userNewPassFlags.clientFlags.Timeout) + defer cancel() + + err = adminClient.UpdateCredentials( + ctx, + userNewPassFlags.username, + userNewPassFlags.password, + ) + if err != nil { + logger.Error("unable to update user credentials", slog.String("user", userNewPassFlags.username), slog.Any("error", err)) + return err + } + + view.Printf("Successfully updated user %s's credentials", userNewPassFlags.username) + return nil + }, + } +} + +func init() { + userNewPassCmd := newUserNewPasswordCmd() + userCmd.AddCommand(userNewPassCmd) + + // TODO: Add custom template for usage to take into account terminal width + // Ex: https://github.com/sigstore/cosign/pull/3011/files + + flagSet := newUserNewPassFlagSet() + userNewPassCmd.Flags().AddFlagSet(flagSet) + + for _, flag := range userNewPassRequiredFlags { + err := userNewPassCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/userRevoke.go b/cmd/userRevoke.go new file mode 100644 index 0000000..586e4af --- /dev/null +++ b/cmd/userRevoke.go @@ -0,0 +1,108 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "asvec/cmd/flags" + "context" + "fmt" + "log/slog" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +//nolint:govet // Padding not a concern for a CLI +var userRevokeFlags = &struct { + clientFlags flags.ClientFlags + revokeUser string + roles []string +}{ + clientFlags: *flags.NewClientFlags(), +} + +func newUserRevokeFlagSet() *pflag.FlagSet { + flagSet := &pflag.FlagSet{} //nolint:lll // For readability //nolint:lll // For readability + flagSet.AddFlagSet(userRevokeFlags.clientFlags.NewClientFlagSet()) + flagSet.StringVar(&userRevokeFlags.revokeUser, flags.RevokeUser, "", "TODO") + flagSet.StringSliceVar(&userRevokeFlags.roles, flags.Roles, []string{}, "TODO") + + return flagSet +} + +var userRevokeRequiredFlags = []string{ + flags.RevokeUser, + flags.Roles, +} + +func newUserRevokeCmd() *cobra.Command { + return &cobra.Command{ + Use: "revoke", + Short: "A command for revoking users roles", + Long: `A command for revoking users roles. TODO + + For example: + export ASVEC_HOST=127.0.0.1:5000 ASVEC_USER=admin + asvec user revoke --revoke-user foo --roles admin + `, + PreRunE: func(_ *cobra.Command, _ []string) error { + if viper.IsSet(flags.Seeds) && viper.IsSet(flags.Host) { + return fmt.Errorf("only --%s or --%s allowed", flags.Seeds, flags.Host) + } + + return nil + }, + RunE: func(_ *cobra.Command, _ []string) error { + logger.Debug("parsed flags", + append( + userRevokeFlags.clientFlags.NewSLogAttr(), + slog.String(flags.NewUser, userRevokeFlags.revokeUser), + slog.Any(flags.Roles, userRevokeFlags.roles), + )..., + ) + + adminClient, err := createClientFromFlags(&userRevokeFlags.clientFlags) + if err != nil { + return err + } + defer adminClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), userRevokeFlags.clientFlags.Timeout) + defer cancel() + + err = adminClient.RevokeRoles( + ctx, + userRevokeFlags.revokeUser, + userRevokeFlags.roles, + ) + if err != nil { + logger.Error("unable to revoke user roles", slog.String("user", userRevokeFlags.revokeUser), slog.Any("roles", userRevokeFlags.roles), slog.Any("error", err)) + return err + } + + view.Printf("Successfully revoked user %s's roles %s", userRevokeFlags.revokeUser, strings.Join(userRevokeFlags.roles, ", ")) + return nil + }, + } +} + +func init() { + userRevokeCmd := newUserRevokeCmd() + userCmd.AddCommand(userRevokeCmd) + + // TODO: Add custom template for usage to take into account terminal width + // Ex: https://github.com/sigstore/cosign/pull/3011/files + + flagSet := newUserRevokeFlagSet() + userRevokeCmd.Flags().AddFlagSet(flagSet) + + for _, flag := range userRevokeRequiredFlags { + err := userRevokeCmd.MarkFlagRequired(flag) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/utils.go b/cmd/utils.go index 46ce4d1..a8d201a 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -10,17 +10,29 @@ import ( "log/slog" "os" "strings" - "time" "golang.org/x/term" avs "github.com/aerospike/avs-client-go" ) -func createClientFromFlags(clientFlags *flags.ClientFlags, connectTimeout time.Duration) (*avs.AdminClient, error) { +func passwordPrompt(prompt string) (string, error) { + fmt.Print(prompt) + + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + + fmt.Println() + + return string(bytePassword), nil +} + +func createClientFromFlags(clientFlags *flags.ClientFlags) (*avs.AdminClient, error) { hosts, isLoadBalancer := parseBothHostSeedsFlag(clientFlags.Seeds, clientFlags.Host) - ctx, cancel := context.WithTimeout(context.Background(), connectTimeout) + ctx, cancel := context.WithTimeout(context.Background(), clientFlags.Timeout) defer cancel() tlsConfig, err := clientFlags.NewTLSConfig() @@ -35,15 +47,13 @@ func createClientFromFlags(clientFlags *flags.ClientFlags, connectTimeout time.D strPass := clientFlags.Password.String() password = &strPass } else { - fmt.Print("Enter Password: ") - bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + pass, err := passwordPrompt("Enter Password: ") if err != nil { logger.Error("failed to read password", slog.Any("error", err)) return nil, err } - fmt.Println() // Print a newline after the password input - strPass := string(bytePassword) - password = &strPass + + password = &pass } } diff --git a/cmd/view.go b/cmd/view.go index c3ccc59..2520555 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -66,3 +66,31 @@ func (v *View) PrintIndexes( t.Render() } + +func (v *View) getUserListWriter() *writers.UserTableWriter { + return writers.NewUserTableWriter(v.writer, v.logger) +} + +func (v *View) PrintUsers(usersList *protos.ListUsersResponse) { + t := v.getUserListWriter() + + for _, user := range usersList.GetUsers() { + t.AppendUserRow(user) + } + + t.Render() +} + +func (v *View) getRoleListWriter() *writers.RoleTableWriter { + return writers.NewRoleTableWriter(v.writer, v.logger) +} + +func (v *View) PrintRoles(usersList *protos.ListRolesResponse) { + t := v.getRoleListWriter() + + for _, role := range usersList.GetRoles() { + t.AppendRoleRow(role) + } + + t.Render() +} diff --git a/cmd/writers/roleList.go b/cmd/writers/roleList.go new file mode 100644 index 0000000..3a46ce1 --- /dev/null +++ b/cmd/writers/roleList.go @@ -0,0 +1,35 @@ +package writers + +import ( + "io" + "log/slog" + + "github.com/aerospike/avs-client-go/protos" + "github.com/jedib0t/go-pretty/v6/table" +) + +type RoleTableWriter struct { + table.Writer + logger *slog.Logger +} + +func NewRoleTableWriter(writer io.Writer, logger *slog.Logger) *RoleTableWriter { + t := RoleTableWriter{NewDefaultWriter(writer), logger} + + t.AppendHeader(table.Row{"Roles"}, rowConfigAutoMerge) + + // t.SetTitle("Roles") + t.SetAutoIndex(true) + t.SortBy([]table.SortBy{ + {Name: "Roles", Mode: table.Asc}, + {Name: "User", Mode: table.Asc}, + }) + + // t.Style().Options.SeparateRows = true + + return &t +} + +func (itw *RoleTableWriter) AppendRoleRow(role *protos.Role) { + itw.AppendRow(table.Row{role.GetId()}) +} diff --git a/cmd/writers/userList.go b/cmd/writers/userList.go new file mode 100644 index 0000000..b5c9512 --- /dev/null +++ b/cmd/writers/userList.go @@ -0,0 +1,36 @@ +package writers + +import ( + "io" + "log/slog" + "strings" + + "github.com/aerospike/avs-client-go/protos" + "github.com/jedib0t/go-pretty/v6/table" +) + +type UserTableWriter struct { + table.Writer + logger *slog.Logger +} + +func NewUserTableWriter(writer io.Writer, logger *slog.Logger) *UserTableWriter { + t := UserTableWriter{NewDefaultWriter(writer), logger} + + t.AppendHeader(table.Row{"User", "Roles"}, rowConfigAutoMerge) + + t.SetTitle("Users") + t.SetAutoIndex(true) + t.SortBy([]table.SortBy{ + {Name: "Roles", Mode: table.Asc}, + {Name: "User", Mode: table.Asc}, + }) + + t.Style().Options.SeparateRows = true + + return &t +} + +func (itw *UserTableWriter) AppendUserRow(user *protos.User) { + itw.AppendRow(table.Row{user.GetUsername(), strings.Join(user.GetRoles(), ", ")}) +}