From 19db1da6ea3208ac5d953b2964330eabc988b6e8 Mon Sep 17 00:00:00 2001 From: Ramakrishna Pattnaik Date: Mon, 4 Oct 2021 20:24:46 +0530 Subject: [PATCH] feat(kafka acl): add base and list command (#1173) --- docs/commands/rhoas_kafka.adoc | 7 + docs/commands/rhoas_kafka_acl.adoc | 47 +++++ docs/commands/rhoas_kafka_acl_list.adoc | 52 ++++++ ...oas_kafka_consumer-group_reset-offset.adoc | 4 +- pkg/cmd/kafka/acl/acl.go | 24 +++ pkg/cmd/kafka/acl/list/list.go | 174 ++++++++++++++++++ pkg/cmd/kafka/kafka.go | 2 + pkg/localize/locales/en/cmd/acl.en.toml | 70 +++++++ 8 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 docs/commands/rhoas_kafka_acl.adoc create mode 100644 docs/commands/rhoas_kafka_acl_list.adoc create mode 100644 pkg/cmd/kafka/acl/acl.go create mode 100644 pkg/cmd/kafka/acl/list/list.go create mode 100644 pkg/localize/locales/en/cmd/acl.en.toml diff --git a/docs/commands/rhoas_kafka.adoc b/docs/commands/rhoas_kafka.adoc index 90d0fc321..82b1202e8 100644 --- a/docs/commands/rhoas_kafka.adoc +++ b/docs/commands/rhoas_kafka.adoc @@ -51,6 +51,13 @@ ifdef::pantheonenv[] * link:{path}#ref-rhoas_{context}[rhoas] - RHOAS CLI endif::[] +ifdef::env-github,env-browser[] +* link:rhoas_kafka_acl.adoc#rhoas-kafka-acl[rhoas kafka acl] - Kafka ACL management for users and service accounts +endif::[] +ifdef::pantheonenv[] +* link:{path}#ref-rhoas-kafka-acl_{context}[rhoas kafka acl] - Kafka ACL management for users and service accounts +endif::[] + ifdef::env-github,env-browser[] * link:rhoas_kafka_consumer-group.adoc#rhoas-kafka-consumer-group[rhoas kafka consumer-group] - Describe, list, and delete consumer groups for the current Apache Kafka instance endif::[] diff --git a/docs/commands/rhoas_kafka_acl.adoc b/docs/commands/rhoas_kafka_acl.adoc new file mode 100644 index 000000000..4ccb2701c --- /dev/null +++ b/docs/commands/rhoas_kafka_acl.adoc @@ -0,0 +1,47 @@ +ifdef::env-github,env-browser[:context: cmd] +[id='ref-rhoas-kafka-acl_{context}'] += rhoas kafka acl + +[role="_abstract"] +Kafka ACL management for users and service accounts + +[discrete] +== Synopsis + +Set of commands that will let you manage Kafka ACLs. +By default, every users and service account have limited access to their Kafka instance (Only DESCRIBE permission is enabled for TOPIC, ACL, and GROUP). + + +[discrete] +== Examples + +.... +## List ACL rules for a Kafka instance +rhoas kafka acl list + +.... + +[discrete] +== Options inherited from parent commands + + `-h`, `--help`:: Show help for a command + `-v`, `--verbose`:: Enable verbose mode + +[discrete] +== See also + + +ifdef::env-github,env-browser[] +* link:rhoas_kafka.adoc#rhoas-kafka[rhoas kafka] - Create, view, use, and manage your Kafka instances +endif::[] +ifdef::pantheonenv[] +* link:{path}#ref-rhoas-kafka_{context}[rhoas kafka] - Create, view, use, and manage your Kafka instances +endif::[] + +ifdef::env-github,env-browser[] +* link:rhoas_kafka_acl_list.adoc#rhoas-kafka-acl-list[rhoas kafka acl list] - List all Kafka ACL rules. +endif::[] +ifdef::pantheonenv[] +* link:{path}#ref-rhoas-kafka-acl-list_{context}[rhoas kafka acl list] - List all Kafka ACL rules. +endif::[] + diff --git a/docs/commands/rhoas_kafka_acl_list.adoc b/docs/commands/rhoas_kafka_acl_list.adoc new file mode 100644 index 000000000..869737261 --- /dev/null +++ b/docs/commands/rhoas_kafka_acl_list.adoc @@ -0,0 +1,52 @@ +ifdef::env-github,env-browser[:context: cmd] +[id='ref-rhoas-kafka-acl-list_{context}'] += rhoas kafka acl list + +[role="_abstract"] +List all Kafka ACL rules. + +[discrete] +== Synopsis + +This command will display list of Kafka ACL rules + +The instances are displayed by default in a table, but can also be displayed as JSON or YAML. + + +.... +rhoas kafka acl list [flags] +.... + +[discrete] +== Examples + +.... +## Display Kafka ACL rules for the instance +rhoas kafka acl list + +.... + +[discrete] +== Options + + `-o`, `--output` _string_:: Format in which to display the Kafka ACL rules (choose from: "json", "yml", "yaml") + `--page` _int32_:: Current page number for list of Kafka ACL rules (default 1) + `--size` _int32_:: Maximum number of items to be returned per page (default 10) + +[discrete] +== Options inherited from parent commands + + `-h`, `--help`:: Show help for a command + `-v`, `--verbose`:: Enable verbose mode + +[discrete] +== See also + + +ifdef::env-github,env-browser[] +* link:rhoas_kafka_acl.adoc#rhoas-kafka-acl[rhoas kafka acl] - Kafka ACL management for users and service accounts +endif::[] +ifdef::pantheonenv[] +* link:{path}#ref-rhoas-kafka-acl_{context}[rhoas kafka acl] - Kafka ACL management for users and service accounts +endif::[] + diff --git a/docs/commands/rhoas_kafka_consumer-group_reset-offset.adoc b/docs/commands/rhoas_kafka_consumer-group_reset-offset.adoc index 4444bddb1..529dfe2e0 100644 --- a/docs/commands/rhoas_kafka_consumer-group_reset-offset.adoc +++ b/docs/commands/rhoas_kafka_consumer-group_reset-offset.adoc @@ -11,7 +11,7 @@ Reset partition offsets for a consumer group Reset the offset for consumers in a consumer group reading from a given topic. Reset to the earliest or latest offset. Or base the reset on a specific offset value or timestamp. -You can also choose partitions to reset offsets from +You can also choose partitions to reset offsets from. .... @@ -34,7 +34,7 @@ $ rhoas kafka consumer-group reset-offset --id consumer_group_1 --offset absolut # reset partition offsets for a consumer group to a timestamp $ rhoas kafka consumer-group reset-offset --id consumer_group_1 --offset timestamp --value "2016-06-23T09:07:21-07:00" -# reset partition offsets by specifying partitions for a consumer group +# reset specific partition offsets for a consumer group $ rhoas kafka consumer-group reset-offset --id consumer_group_1 --offset latest --topic my-topic --partitions 0,1 .... diff --git a/pkg/cmd/kafka/acl/acl.go b/pkg/cmd/kafka/acl/acl.go new file mode 100644 index 000000000..ef6e46791 --- /dev/null +++ b/pkg/cmd/kafka/acl/acl.go @@ -0,0 +1,24 @@ +package acl + +import ( + "github.com/redhat-developer/app-services-cli/pkg/cmd/factory" + "github.com/redhat-developer/app-services-cli/pkg/cmd/kafka/acl/list" + "github.com/spf13/cobra" +) + +// NewAclCommand creates a new command sub-group for Kafka ACL operations +func NewAclCommand(f *factory.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "acl", + Short: f.Localizer.MustLocalize("kafka.acl.cmd.shortDescription"), + Long: f.Localizer.MustLocalize("kafka.acl.cmd.longDescription"), + Example: f.Localizer.MustLocalize("kafka.acl.cmd.example"), + Args: cobra.ExactArgs(1), + } + + cmd.AddCommand( + list.NewListACLCommand(f), + ) + + return cmd +} diff --git a/pkg/cmd/kafka/acl/list/list.go b/pkg/cmd/kafka/acl/list/list.go new file mode 100644 index 000000000..8871fc22a --- /dev/null +++ b/pkg/cmd/kafka/acl/list/list.go @@ -0,0 +1,174 @@ +package list + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/redhat-developer/app-services-cli/internal/build" + "github.com/redhat-developer/app-services-cli/internal/config" + "github.com/redhat-developer/app-services-cli/pkg/cmd/factory" + "github.com/redhat-developer/app-services-cli/pkg/cmdutil" + "github.com/redhat-developer/app-services-cli/pkg/connection" + "github.com/redhat-developer/app-services-cli/pkg/dump" + "github.com/redhat-developer/app-services-cli/pkg/iostreams" + "github.com/redhat-developer/app-services-cli/pkg/localize" + "github.com/redhat-developer/app-services-cli/pkg/logging" + "github.com/spf13/cobra" + + kafkainstanceclient "github.com/redhat-developer/app-services-sdk-go/kafkainstance/apiv1internal/client" +) + +type options struct { + Config config.IConfig + Connection factory.ConnectionFunc + Logger logging.Logger + IO *iostreams.IOStreams + localizer localize.Localizer + Context context.Context + + page int32 + size int32 + kafkaID string + output string +} + +type permissionsRow struct { + Principal string `json:"principal,omitempty" header:"Principal"` + Permission string `json:"permission,omitempty" header:"permission"` + Description string `json:"description,omitempty" header:"description"` +} + +// NewListACLCommand creates a new command to list Kafka ACL rules +func NewListACLCommand(f *factory.Factory) *cobra.Command { + + opts := &options{ + Config: f.Config, + Connection: f.Connection, + Logger: f.Logger, + IO: f.IOStreams, + localizer: f.Localizer, + Context: f.Context, + } + + cmd := &cobra.Command{ + Use: "list", + Short: f.Localizer.MustLocalize("kafka.acl.list.cmd.shortDescription"), + Long: f.Localizer.MustLocalize("kafka.acl.list.cmd.longDescription"), + Example: f.Localizer.MustLocalize("kafka.acl.list.cmd.example"), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + + cfg, err := opts.Config.Load() + if err != nil { + return err + } + + if !cfg.HasKafka() { + return opts.localizer.MustLocalizeError("kafka.acl.common.error.noKafkaSelected") + } + + opts.kafkaID = cfg.Services.Kafka.ClusterID + + return runList(opts) + }, + } + + cmd.Flags().Int32VarP(&opts.page, "page", "", cmdutil.ConvertPageValueToInt32(build.DefaultPageNumber), opts.localizer.MustLocalize("kafka.acl.list.flag.page.description")) + cmd.Flags().Int32VarP(&opts.size, "size", "", cmdutil.ConvertSizeValueToInt32(build.DefaultPageSize), opts.localizer.MustLocalize("kafka.acl.list.flag.size.description")) + cmd.Flags().StringVarP(&opts.output, "output", "o", "", opts.localizer.MustLocalize("kafka.acl.list.flag.output.description")) + + return cmd +} + +func runList(opts *options) (err error) { + conn, err := opts.Connection(connection.DefaultConfigRequireMasAuth) + if err != nil { + return err + } + + api, kafkaInstance, err := conn.API().KafkaAdmin(opts.kafkaID) + if err != nil { + return err + } + + req := api.AclsApi.GetAcls(opts.Context) + + req = req.Page(float32(opts.page)) + + req = req.Size(float32(opts.size)) + + permissionsData, httpRes, err := req.Execute() + if httpRes != nil { + defer httpRes.Body.Close() + } + + if err != nil { + if httpRes == nil { + return err + } + + operationTmplPair := localize.NewEntry("Operation", "list") + + switch httpRes.StatusCode { + case http.StatusUnauthorized: + return opts.localizer.MustLocalizeError("kafka.acl.common.error.unauthorized", operationTmplPair) + case http.StatusForbidden: + return opts.localizer.MustLocalizeError("kafka.acl.common.error.forbidden", operationTmplPair) + case http.StatusInternalServerError: + return opts.localizer.MustLocalizeError("kafka.acl.common.error.internalServerError") + case http.StatusServiceUnavailable: + return opts.localizer.MustLocalizeError("kafka.acl.common.error.unableToConnectToKafka", localize.NewEntry("Name", kafkaInstance.GetName())) + default: + return err + } + } + + switch opts.output { + case dump.EmptyFormat: + opts.Logger.Info("") + permissions := permissionsData.GetItems() + rows := mapPermissionListResultsToTableFormat(permissions, opts.localizer) + dump.Table(opts.IO.Out, rows) + default: + return dump.Formatted(opts.IO.Out, opts.output, permissionsData) + } + + return nil +} + +func mapPermissionListResultsToTableFormat(permissions []kafkainstanceclient.AclBinding, localizer localize.Localizer) []permissionsRow { + + rows := make([]permissionsRow, len(permissions)) + + for i, p := range permissions { + + description := buildDescription(p.PatternType, localizer) + row := permissionsRow{ + Principal: formatPrincipal(p.GetPrincipal(), localizer), + Permission: fmt.Sprintf("%s | %s", p.GetPermission(), p.GetOperation()), + Description: fmt.Sprintf("%s %s \"%s\"", p.GetResourceType(), description, p.GetResourceName()), + } + rows[i] = row + } + return rows +} + +func formatPrincipal(principal string, localizer localize.Localizer) string { + s := strings.Split(principal, ":")[1] + + if s == "*" { + return localizer.MustLocalize("kafka.acl.list.allAccounts") + } + + return s +} + +func buildDescription(patternType kafkainstanceclient.AclPatternType, localizer localize.Localizer) string { + if patternType == kafkainstanceclient.ACLPATTERNTYPE_LITERAL { + return localizer.MustLocalize("kafka.acl.list.is") + } + + return localizer.MustLocalize("kafka.acl.list.startsWith") +} diff --git a/pkg/cmd/kafka/kafka.go b/pkg/cmd/kafka/kafka.go index 0a09bd5b5..539154a44 100644 --- a/pkg/cmd/kafka/kafka.go +++ b/pkg/cmd/kafka/kafka.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/redhat-developer/app-services-cli/pkg/cmd/factory" + "github.com/redhat-developer/app-services-cli/pkg/cmd/kafka/acl" "github.com/redhat-developer/app-services-cli/pkg/cmd/kafka/create" "github.com/redhat-developer/app-services-cli/pkg/cmd/kafka/delete" "github.com/redhat-developer/app-services-cli/pkg/cmd/kafka/describe" @@ -35,6 +36,7 @@ func NewKafkaCommand(f *factory.Factory) *cobra.Command { topic.NewTopicCommand(f), consumergroup.NewConsumerGroupCommand(f), update.NewUpdateCommand(f), + acl.NewAclCommand(f), ) return cmd diff --git a/pkg/localize/locales/en/cmd/acl.en.toml b/pkg/localize/locales/en/cmd/acl.en.toml new file mode 100644 index 000000000..2c4eb9f67 --- /dev/null +++ b/pkg/localize/locales/en/cmd/acl.en.toml @@ -0,0 +1,70 @@ +[kafka.acl.cmd.shortDescription] +one = 'Kafka ACL management for users and service accounts' + +[kafka.acl.cmd.longDescription] +one = ''' +Set of commands that will let you manage Kafka ACLs. +By default, every users and service account have limited access to their Kafka instance (Only DESCRIBE permission is enabled for TOPIC, ACL, and GROUP). +''' + +[kafka.acl.cmd.example] +one = ''' +## List ACL rules for a Kafka instance +rhoas kafka acl list +''' + + +[kafka.acl.common.error.unauthorized] +one = 'you are unauthorized to {{.Operation}} these Kafka ACL rules' + +[kafka.acl.common.error.forbidden] +one = 'you are forbidden to {{.Operation}} this consumer group' + +[kafka.acl.common.error.internalServerError] +one = 'internal server error' + +[kafka.acl.common.error.unableToConnectToKafka] +one = 'unable to connect to Kafka instance "{{.Name}}"' + +[kafka.acl.common.error.noKafkaSelected] +one = 'no Kafka instance is currently selected, run "rhoas kafka use" to set the current instance' + + + +[kafka.acl.list.cmd.shortDescription] +one = 'List all Kafka ACL rules.' + +[kafka.acl.list.cmd.longDescription] +one = ''' +This command will display list of Kafka ACL rules + +The instances are displayed by default in a table, but can also be displayed as JSON or YAML. +''' + +[kafka.acl.list.cmd.example] +one = ''' +## Display Kafka ACL rules for the instance +rhoas kafka acl list +''' + + +[kafka.acl.list.flag.output.description] +description = "Description for --output flag" +one = 'Format in which to display the Kafka ACL rules (choose from: "json", "yml", "yaml")' + +[kafka.acl.list.flag.page.description] +description = 'Description for the --page flag' +one = 'Current page number for list of Kafka ACL rules' + +[kafka.acl.list.flag.size.description] +description = 'Description for the --size flag' +one = 'Maximum number of items to be returned per page' + +[kafka.acl.list.allAccounts] +one = 'All accounts' + +[kafka.acl.list.is] +one = 'is' + +[kafka.acl.list.startsWith] +one = 'starts with' \ No newline at end of file