Skip to content

Commit

Permalink
feat(confix): copy confix from cosmos sdk (cometbft#3036)
Browse files Browse the repository at this point in the history
# Confix

`Confix` is a configuration management tool that allows you to manage
your configuration via CLI.

It is based on the [CometBFT RFC
019](https://github.com/cometbft/cometbft/blob/5013bc3f4a6d64dcc2bf02ccc002ebc9881c62e4/docs/rfc/rfc-019-config-version.md).

## Usage

### Get

Get a configuration value, e.g.:

```shell
cometbft config get pruning # gets the value pruning
cometbft config get chain-id # gets the value chain-id
```

### Set

Set a configuration value, e.g.:

```shell
cometbft config set pruning "enabled" # sets the value pruning
cometbft config set chain-id "foo-1" # sets the value chain-id
```
### Migrate

Migrate a configuration file to a new version:

```shell
cometbft config migrate v0.38 # migrates defaultHome/config/config.toml to the latest v0.38 config
```

### Diff

Get the diff between a given configuration file and the default
configuration
file, e.g.:

```shell
cometbft config diff v0.38 # gets the diff between defaultHome/config/config.toml and the latest v0.38 config
```

### View

View a configuration file, e.g:

```shell
cometbft config view # views the current config
```

## Credits

This project is based on the [CometBFT RFC
019](https://github.com/cometbft/cometbft/blob/5013bc3f4a6d64dcc2bf02ccc002ebc9881c62e4/docs/rfc/rfc-019-config-version.md)
and their own implementation of
[confix](https://github.com/cometbft/cometbft/blob/v0.36.x/scripts/confix/confix.go).
Most of the code is copied over from [Cosmos
SDK](https://github.com/cosmos/cosmos-sdk/tree/main/tools/confix).

---

#### PR checklist

- [ ] ~~Tests written/updated~~
- [ ] ~~Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)~~
- [ ] ~~Updated relevant documentation (`docs/` or `spec/`) and code
comments~~
- [x] Title follows the [Conventional
Commits](https://www.conventionalcommits.org/en/v1.0.0/) spec
  • Loading branch information
melekes authored Jul 28, 2024
1 parent 94d42a9 commit b058188
Show file tree
Hide file tree
Showing 23 changed files with 2,982 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .changelog/unreleased/improvements/3036-confix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- `[cmd/cometbft]` Add `cometbft config` cmd to view, modify and
upgrade configs across different versions
([\#3036](https://github.com/cometbft/cometbft/pull/3036))
7 changes: 6 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,13 @@ linters-settings:
- "!$test"
allow:
- $gostd
- github.com/BurntSushi/toml
- github.com/Masterminds/semver/v3
- github.com/btcsuite/btcd/btcec/v2
- github.com/cometbft
- github.com/cosmos
- github.com/creachadair/atomicfile
- github.com/creachadair/tomledit
- github.com/btcsuite/btcd/btcec/v2
- github.com/BurntSushi/toml
- github.com/dgraph-io/badger/v4
Expand All @@ -108,9 +113,9 @@ linters-settings:
- github.com/hashicorp/golang-lru/v2
- github.com/lib/pq
- github.com/libp2p/go-buffer-pool
- github.com/Masterminds/semver/v3
- github.com/minio/highwayhash
- github.com/oasisprotocol/curve25519-voi
- github.com/pelletier/go-toml/v2
- github.com/pkg/errors
- github.com/prometheus
- github.com/rcrowley/go-metrics
Expand Down
18 changes: 18 additions & 0 deletions cmd/cometbft/commands/config/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package config

import (
"path/filepath"

"github.com/spf13/cobra"

"github.com/cometbft/cometbft/cmd/cometbft/commands"
cfg "github.com/cometbft/cometbft/config"
)

func defaultConfigPath(cmd *cobra.Command) string {
home, err := commands.ConfigHome(cmd)
if err != nil {
return ""
}
return filepath.Join(home, cfg.DefaultConfigDir, cfg.DefaultConfigFileName)
}
24 changes: 24 additions & 0 deletions cmd/cometbft/commands/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package config

import (
"github.com/spf13/cobra"
)

// Command contains all the confix commands
// These command can be used to interactively update a config value.
func Command() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Utilities for managing configuration",
}

cmd.AddCommand(
MigrateCommand(),
DiffCommand(),
GetCommand(),
SetCommand(),
ViewCommand(),
)

return cmd
}
55 changes: 55 additions & 0 deletions cmd/cometbft/commands/config/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package config

import (
"fmt"

"github.com/spf13/cobra"
"golang.org/x/exp/maps"

"github.com/cometbft/cometbft/internal/confix"
)

// DiffCommand creates a new command for comparing configuration files.
func DiffCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "diff [target-version] <config-path>",
Short: "Outputs all config values that are different from the default.",
Long: "This command compares the configuration file with the defaults and outputs any differences.",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var configPath string
if len(args) > 1 {
configPath = args[1]
} else {
configPath = defaultConfigPath(cmd)
}

targetVersion := args[0]
if _, ok := confix.Migrations[targetVersion]; !ok {
return fmt.Errorf("unknown version %q, supported versions are: %q", targetVersion, maps.Keys(confix.Migrations))
}

targetVersionFile, err := confix.LoadLocalConfig(targetVersion + ".toml")
if err != nil {
return fmt.Errorf("failed to load internal config: %w", err)
}

rawFile, err := confix.LoadConfig(configPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

diff := confix.DiffValues(rawFile, targetVersionFile)
if len(diff) == 0 {
fmt.Print("All config values are the same as the defaults.\n")
}

fmt.Print("The following config values are different from the defaults:\n")

confix.PrintDiff(cmd.OutOrStdout(), diff)
return nil
},
}

return cmd
}
69 changes: 69 additions & 0 deletions cmd/cometbft/commands/config/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package config

import (
"context"
"fmt"

"github.com/spf13/cobra"
"golang.org/x/exp/maps"

"github.com/cometbft/cometbft/internal/confix"
)

var (
FlagStdOut bool
FlagVerbose bool
FlagSkipValidate bool
)

func MigrateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "migrate [target-version] <config-path>",
Short: "Migrate configuration file to the specified version",
Long: `Migrate the contents of the configuration to the specified version.
The output is written in-place unless --stdout is provided.
In case of any error in updating the file, no output is written.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var configPath string
if len(args) > 1 {
configPath = args[1]
} else {
configPath = defaultConfigPath(cmd)
}

targetVersion := args[0]
plan, ok := confix.Migrations[targetVersion]
if !ok {
return fmt.Errorf("unknown version %q, supported versions are: %q", targetVersion, maps.Keys(confix.Migrations))
}

rawFile, err := confix.LoadConfig(configPath)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

ctx := context.Background()
if FlagVerbose {
ctx = confix.WithLogWriter(ctx, cmd.ErrOrStderr())
}

outputPath := configPath
if FlagStdOut {
outputPath = ""
}

if err := confix.Upgrade(ctx, plan(rawFile, targetVersion), configPath, outputPath, FlagSkipValidate); err != nil {
return fmt.Errorf("failed to migrate config: %w", err)
}

return nil
},
}

cmd.Flags().BoolVar(&FlagStdOut, "stdout", false, "print the updated config to stdout")
cmd.Flags().BoolVar(&FlagVerbose, "verbose", false, "log changes to stderr")
cmd.Flags().BoolVar(&FlagSkipValidate, "skip-validate", false, "skip configuration validation (allows to migrate unknown configurations)")

return cmd
}
144 changes: 144 additions & 0 deletions cmd/cometbft/commands/config/mutate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package config

import (
"context"
"errors"
"fmt"
"strings"

"github.com/creachadair/tomledit"
"github.com/creachadair/tomledit/parser"
"github.com/creachadair/tomledit/transform"
"github.com/spf13/cobra"

"github.com/cometbft/cometbft/internal/confix"
)

// SetCommand returns a CLI command to interactively update an application config value.
func SetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "set [config] [key] [value]",
Short: "Set a config value",
Long: "Set a config value. The [config] is an optional absolute path to the config file (default: `~/.cometbft/config/config.toml`)",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
var (
filename, inputValue string
key []string
)
switch len(args) {
case 2:
{
filename = defaultConfigPath(cmd)
// parse key e.g mempool.size -> [mempool, size]
key = strings.Split(args[0], ".")
inputValue = args[1]
}
case 3:
{
filename, inputValue = args[0], args[2]
key = strings.Split(args[1], ".")
}
default:
return errors.New("expected 2 or 3 arguments")
}

plan := transform.Plan{
{
Desc: fmt.Sprintf("update %q=%q in %s", key, inputValue, filename),
T: transform.Func(func(_ context.Context, doc *tomledit.Document) error {
results := doc.Find(key...)
if len(results) == 0 {
return fmt.Errorf("key %q not found", key)
} else if len(results) > 1 {
return fmt.Errorf("key %q is ambiguous", key)
}

value, err := parser.ParseValue(inputValue)
if err != nil {
value = parser.MustValue(`"` + inputValue + `"`)
}

if ok := transform.InsertMapping(results[0].Section, &parser.KeyValue{
Block: results[0].Block,
Name: results[0].Name,
Value: value,
}, true); !ok {
return errors.New("failed to set value")
}

return nil
}),
},
}

outputPath := filename
if FlagStdOut {
outputPath = ""
}

ctx := cmd.Context()
if FlagVerbose {
ctx = confix.WithLogWriter(ctx, cmd.ErrOrStderr())
}

return confix.Upgrade(ctx, plan, filename, outputPath, FlagSkipValidate)
},
}

cmd.Flags().BoolVar(&FlagStdOut, "stdout", false, "print the updated config to stdout")
cmd.Flags().BoolVarP(&FlagVerbose, "verbose", "v", false, "log changes to stderr")
cmd.Flags().BoolVarP(&FlagSkipValidate, "skip-validate", "s", false, "skip configuration validation (allows to mutate unknown configurations)")

return cmd
}

// GetCommand returns a CLI command to interactively get an application config value.
func GetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "get [config] [key]",
Short: "Get a config value",
Long: "Get a config value. The [config] is an optional absolute path to the config file (default: `~/.cometbft/config/config.toml`)",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var (
filename, key string
keys []string
)
switch len(args) {
case 1:
{
filename = defaultConfigPath(cmd)
// parse key e.g mempool.size -> [mempool, size]
key = args[0]
keys = strings.Split(key, ".")
}
case 2:
{
filename = args[0]
key = args[1]
keys = strings.Split(key, ".")
}
default:
return errors.New("expected 1 or 2 arguments")
}

doc, err := confix.LoadConfig(filename)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

results := doc.Find(keys...)
if len(results) == 0 {
return fmt.Errorf("key %q not found", key)
} else if len(results) > 1 {
return fmt.Errorf("key %q is ambiguous", key)
}

fmt.Printf("%s\n", results[0].Value.String())
return nil
},
}

return cmd
}
52 changes: 52 additions & 0 deletions cmd/cometbft/commands/config/view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package config

import (
"encoding/json"
"fmt"
"os"

"github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra"
)

func ViewCommand() *cobra.Command {
flagOutputFormat := "output-format"

cmd := &cobra.Command{
Use: "view [config]",
Short: "View the config file",
Long: "View the config file. The [config] is an optional absolute path to the config file (default: `~/.cometbft/config/config.toml`)",
RunE: func(cmd *cobra.Command, args []string) error {
var filename string
if len(args) > 0 {
filename = args[0]
} else {
filename = defaultConfigPath(cmd)
}

file, err := os.ReadFile(filename)
if err != nil {
return err
}

if format, _ := cmd.Flags().GetString(flagOutputFormat); format == "toml" {
cmd.Println(string(file))
return nil
}

var v any
if err := toml.Unmarshal(file, &v); err != nil {
return fmt.Errorf("failed to decode config file: %w", err)
}

e := json.NewEncoder(cmd.OutOrStdout())
e.SetIndent("", " ")
return e.Encode(v)
},
}

// output flag
cmd.Flags().String(flagOutputFormat, "toml", "Output format (json|toml)")

return cmd
}
Loading

0 comments on commit b058188

Please sign in to comment.