Skip to content

Commit

Permalink
Merge pull request #190 from deviceinsight/feature/config-fallback
Browse files Browse the repository at this point in the history
project config files
  • Loading branch information
d-rk authored Mar 5, 2024
2 parents 71c5482 + a86ac6f commit a014192
Show file tree
Hide file tree
Showing 37 changed files with 421 additions and 273 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dist
kafkactl

### Configs
.kafkactl.yml
kafkactl.yml

### Snap
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- [#190](https://github.com/deviceinsight/kafkactl/pull/190) Improve handling of project config files

## 4.0.0 - 2024-01-18

### Added
Expand Down
29 changes: 28 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,48 @@ contexts:
# optional: isolationLevel (defaults to ReadCommitted)
isolationLevel: ReadUncommitted
# optional for project config files
current-context: default
----

[#_config_file_read_order]
The config file location is resolved by

. checking for a provided commandline argument: `--config-file=$PATH_TO_CONFIG`
. evaluating the environment variable: `export KAFKA_CTL_CONFIG=$PATH_TO_CONFIG`
. checking for a config file in the working directory i.e. `$PWD/kafkactl.yml`
. checking for a project config file in the working directory (see <<_project_config_files>>)
. as default the config file is looked up from one of the following locations:
** `$HOME/.config/kafkactl/config.yml`
** `$HOME/.kafkactl/config.yml`
** `$SNAP_REAL_HOME/.kafkactl/config.yml`
** `$SNAP_DATA/kafkactl/config.yml`
** `/etc/kafkactl/config.yml`

[#_project_config_files]
==== Project config files

In addition to the config file locations above, _kafkactl_ allows to create a config file on project level.
A project config file is meant to be placed at the root level of a git repo and declares the kafka configuration
for this repository/project.

In order to identify the config file as belonging to _kafkactl_ the following names can be used:

* `kafkactl.yml`
* `.kafkactl.yml`

During initialization _kafkactl_ starts from the current working directory and recursively looks for a project level
config file. The recursive lookup ends at the boundary of a git repository (i.e. if a `.git` folder is found).
This way, _kafkactl_ can be used conveniently anywhere in the git repository.

Additionally, project config files have a special feature to use them read-only. Topically, if you configure more than
one context in a config file, and you switch the context with `kafkactl config use-context xy` this will lead to a write
operation on the config file to save the _current context_.

In order to avoid this for project config files, one can just omit the `current-context` parameter from the config file.
In this case _kafkactl_ will delegate read and write operations for the _current context_ to the next configuration file
according to <<_config_file_read_order, the config file read order>>.


=== Auto completion

==== bash
Expand Down
4 changes: 2 additions & 2 deletions cmd/alter/alter-partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ func newAlterPartitionCmd() *cobra.Command {
}
}
},
PreRunE: func(cmd *cobra.Command, args []string) error {
PreRunE: func(cmd *cobra.Command, _ []string) error {
return validation.ValidateAtLeastOneRequiredFlag(cmd)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return topic.CompleteTopicNames(cmd, args, toComplete)
} else if len(args) == 1 {
return partition.CompletePartitionIds(cmd, args, toComplete)
return partition.CompletePartitionIDs(cmd, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/alter/alter-partition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestAlterPartitionReplicationFactorIntegration(t *testing.T) {
return
}

checkReplicas := func(attempt uint) error {
checkReplicas := func(_ uint) error {
_, err := kafkaCtl.Execute("describe", "topic", topicName, "-o", "yaml")

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/alter/alter-topic.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func newAlterTopicCmd() *cobra.Command {
}
}
},
PreRunE: func(cmd *cobra.Command, args []string) error {
PreRunE: func(cmd *cobra.Command, _ []string) error {
return validation.ValidateAtLeastOneRequiredFlag(cmd)
},
ValidArgsFunction: topic.CompleteTopicNames,
Expand Down
6 changes: 3 additions & 3 deletions cmd/alter/alter-topic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestAlterTopicPartitionsIntegration(t *testing.T) {
t.Fatalf("failed to execute command: %v", err)
}

getPartitions := func(attempt uint) error {
getPartitions := func(_ uint) error {
_, err := kafkaCtl.Execute("describe", "topic", topicName, "-o", "yaml")

if err != nil {
Expand Down Expand Up @@ -93,7 +93,7 @@ func TestAlterTopicIncreaseReplicationFactorIntegration(t *testing.T) {
return
}

checkReplicas := func(attempt uint) error {
checkReplicas := func(_ uint) error {
_, err := kafkaCtl.Execute("describe", "topic", topicName, "-o", "yaml")

if err != nil {
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestAlterTopicDecreaseReplicationFactorIntegration(t *testing.T) {
return
}

checkReplicas := func(attempt uint) error {
checkReplicas := func(_ uint) error {
_, err := kafkaCtl.Execute("describe", "topic", topicName, "-o", "yaml")

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/attach/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func NewAttachCmd() *cobra.Command {
Use: "attach",
Short: "run kafkactl pod in kubernetes and attach to it",
Args: cobra.NoArgs,
Run: func(cobraCmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
if err := k8s.NewOperation().Attach(); err != nil {
output.Fail(err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/clone/clone-topic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestCloneTopicIntegration(t *testing.T) {

testutil.AssertEquals(t, fmt.Sprintf("topic %s cloned to %s", srcTopic, targetTopic), kafkaCtl.GetStdOut())

getTopic := func(attempt uint) error {
getTopic := func(_ uint) error {
_, err := kafkaCtl.Execute("describe", "topic", targetTopic, "-o", "yaml")

if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions cmd/config/currentContext.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package config

import (
"github.com/deviceinsight/kafkactl/internal/global"
"github.com/deviceinsight/kafkactl/output"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func newCurrentContextCmd() *cobra.Command {
Expand All @@ -12,8 +12,8 @@ func newCurrentContextCmd() *cobra.Command {
Aliases: []string{"currentContext"},
Short: "show current context",
Long: `Displays the name of context that is currently active`,
Run: func(cmd *cobra.Command, args []string) {
context := viper.GetString("current-context")
Run: func(_ *cobra.Command, _ []string) {
context := global.GetCurrentContext()
output.Infof("%s", context)
},
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/config/getContexts.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"github.com/deviceinsight/kafkactl/internal/global"
"github.com/deviceinsight/kafkactl/output"

"github.com/spf13/cobra"
Expand All @@ -16,9 +17,9 @@ func newGetContextsCmd() *cobra.Command {
Aliases: []string{"getContexts"},
Short: "list configured contexts",
Long: `Output names of all configured contexts`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
contexts := viper.GetStringMap("contexts")
currentContext := viper.GetString("current-context")
currentContext := global.GetCurrentContext()

if outputFormat == "compact" {
for name := range contexts {
Expand Down
10 changes: 5 additions & 5 deletions cmd/config/useContext.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package config
import (
"sort"

"github.com/deviceinsight/kafkactl/internal/global"

"github.com/deviceinsight/kafkactl/output"
"github.com/pkg/errors"

Expand All @@ -20,7 +22,7 @@ func newUseContextCmd() *cobra.Command {
Short: "switch active context",
Long: `command to switch active context`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, args []string) {

context := strings.Join(args, " ")

Expand All @@ -31,13 +33,11 @@ func newUseContextCmd() *cobra.Command {
output.Fail(errors.Errorf("not a valid context: %s", context))
}

viper.Set("current-context", context)

if err := viper.WriteConfig(); err != nil {
if err := global.SetCurrentContext(context); err != nil {
output.Fail(errors.Wrap(err, "unable to write config"))
}
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/config/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func newViewCmd() *cobra.Command {
Use: "view",
Short: "show contents of config file",
Long: `Shows the contents of the config file that is currently used`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {

yamlFile, err := os.ReadFile(viper.ConfigFileUsed())
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions cmd/create/create-acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ func newCreateACLCmd() *cobra.Command {
_ = cmdCreateACL.MarkFlagRequired("principal")
_ = cmdCreateACL.MarkFlagRequired("operation")

_ = cmdCreateACL.RegisterFlagCompletionFunc("pattern", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmdCreateACL.RegisterFlagCompletionFunc("pattern", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"match", "prefixed", "literal"}, cobra.ShellCompDirectiveDefault
})

_ = cmdCreateACL.RegisterFlagCompletionFunc("operation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmdCreateACL.RegisterFlagCompletionFunc("operation", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"any", "all", "read", "write", "create", "delete", "alter", "describe", "clusteraction", "describeconfigs", "alterconfigs", "idempotentwrite"}, cobra.ShellCompDirectiveDefault
})

Expand Down
2 changes: 1 addition & 1 deletion cmd/create/create-topic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ partitions:
}

func describeTopic(t *testing.T, kafkaCtl testutil.KafkaCtlTestCommand, topicName string) {
describeTopic := func(attempt uint) error {
describeTopic := func(_ uint) error {
_, err := kafkaCtl.Execute("describe", "topic", topicName, "-o", "yaml")
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/deletion/delete-acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ func newDeleteACLCmd() *cobra.Command {
_ = cmdDeleteACL.MarkFlagRequired("operation")
_ = cmdDeleteACL.MarkFlagRequired("pattern")

_ = cmdDeleteACL.RegisterFlagCompletionFunc("operation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmdDeleteACL.RegisterFlagCompletionFunc("operation", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"any", "all", "read", "write", "create", "delete", "alter", "describe", "clusteraction", "describeconfigs", "alterconfigs", "idempotentwrite"}, cobra.ShellCompDirectiveDefault
})

_ = cmdDeleteACL.RegisterFlagCompletionFunc("pattern", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmdDeleteACL.RegisterFlagCompletionFunc("pattern", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"any", "match", "prefixed", "literal"}, cobra.ShellCompDirectiveDefault
})

Expand Down
2 changes: 1 addition & 1 deletion cmd/deletion/delete-consumer-group-offset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func failedToDeleteMessage(groupName string, topic string, partition int32) stri
}

func checkOffsetDeleted(kafkaCtl testutil.KafkaCtlTestCommand, groupName string, topic string, partition int32) error {
checkOffsetDeleted := func(attempt uint) error {
checkOffsetDeleted := func(_ uint) error {
_, err := kafkaCtl.Execute("describe", "consumer-group", groupName, "-o", "yaml")

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/deletion/delete-consumer-group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestDeleteConsumerGroupAutoCompletionIntegration(t *testing.T) {

func verifyConsumerGroupDeleted(t *testing.T, kafkaCtl testutil.KafkaCtlTestCommand, groupName string) {

checkConsumerGrouDeleted := func(attempt uint) error {
checkConsumerGrouDeleted := func(_ uint) error {
_, err := kafkaCtl.Execute("get", "consumer-groups", "-o", "compact")

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/deletion/delete-topic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestDeleteTopicAutoCompletionIntegration(t *testing.T) {

func verifyTopicDeleted(t *testing.T, kafkaCtl testutil.KafkaCtlTestCommand, topicName string) {

checkTopicDeleted := func(attempt uint) error {
checkTopicDeleted := func(_ uint) error {
_, err := kafkaCtl.Execute("get", "topics", "-o", "compact")

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/describe/describe-broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func newDescribeBrokerCmd() *cobra.Command {
}
}
},
ValidArgsFunction: broker.CompleteBrokerIds,
ValidArgsFunction: broker.CompleteBrokerIDs,
}

cmdDescribeBroker.Flags().StringVarP(&flags.OutputFormat, "output", "o", flags.OutputFormat, "output format. One of: json|yaml|wide")
Expand Down
2 changes: 1 addition & 1 deletion cmd/describe/describe-consumer-group.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func newDescribeConsumerGroupCmd() *cobra.Command {
}
}
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ValidArgsFunction: func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return consumergroups.CompleteConsumerGroupsFiltered(flags)
},
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func newDocsCmd() *cobra.Command {
Short: "Generate documentation as markdown or man pages",
Long: docsDesc,
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, _ []string) {
if err := (&internal.DocsOperation{}).GenerateDocs(cmd.Root(), flags); err != nil {
output.Fail(err)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/get/get-acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ func newGetACLCmd() *cobra.Command {

cmdGetAcls.Flags().StringVarP(&flags.OutputFormat, "output", "o", flags.OutputFormat, "output format. One of: json|yaml")

_ = cmdGetAcls.RegisterFlagCompletionFunc("operation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmdGetAcls.RegisterFlagCompletionFunc("operation", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"any", "all", "read", "write", "create", "delete", "alter", "describe", "clusteraction", "describeconfigs", "alterconfigs", "idempotentwrite"}, cobra.ShellCompDirectiveDefault
})

_ = cmdGetAcls.RegisterFlagCompletionFunc("pattern", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmdGetAcls.RegisterFlagCompletionFunc("pattern", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"any", "match", "prefixed", "literal"}, cobra.ShellCompDirectiveDefault
})

Expand Down
Loading

0 comments on commit a014192

Please sign in to comment.