Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,43 @@ For TLS-only clusters (typically port `9440`), set `CLICKHOUSE_SECURE=true`
— or pass `-secure` on the CLI, or `?secure=true` on the diff URL form.
See **[TLS / secure connections](#tls--secure-connections)** below.

## CLI comparison defaults

`diff`, `drift`, `plan`, and dump-mode `web` read comparison defaults from up
to two HCL files, in this order:

1. `$HOME/.hclexp.config` — user-wide defaults.
2. `<working-directory>/.hclexp.config` — project defaults.

Only those exact locations are inspected; parent directories of the working
directory are not searched. If both paths identify the same file, it is read
once. Each file can set a `global` default and override it per subcommand:

```hcl
global {
ignore_column_order = true
}

plan {
ignore_column_order = false
}

web {
ignore_column_order = true
}
```

The supported command blocks are `diff`, `drift`, `plan`, and `web`.
Precedence, from lowest to highest, is the built-in default, home `global`,
home command block, working-directory `global`, working-directory command
block, and an explicit CLI flag. Both directions are explicit: for example,
`-ignore-column-order=false` overrides a configured `true`.

Malformed files, unknown settings, and unknown command blocks stop the command
with the config path in the error. When stderr is a terminal, hclexp prints one
`hclexp: loaded config <path>` line per file read. The status goes to stderr;
JSON and redirected stdout remain machine-clean.

## Introspect a live database

```bash
Expand Down
148 changes: 148 additions & 0 deletions cmd/hclexp/cli_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package main

import (
"flag"
"fmt"
"io"
"os"
"path/filepath"

"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
"golang.org/x/term"
)

const hclexpConfigFilename = ".hclexp.config"

// hclexpConfigFile separates defaults shared by comparison commands from
// command-specific overrides. Pointer values distinguish an explicit false
// from an omitted setting that should inherit from the preceding layer.
type hclexpConfigFile struct {
Global *hclexpConfigSection `hcl:"global,block"`
Diff *hclexpConfigSection `hcl:"diff,block"`
Drift *hclexpConfigSection `hcl:"drift,block"`
Plan *hclexpConfigSection `hcl:"plan,block"`
Web *hclexpConfigSection `hcl:"web,block"`
}

type hclexpConfigSection struct {
IgnoreColumnOrder *bool `hcl:"ignore_column_order,optional"`
}

type hclexpConfigResolution struct {
IgnoreColumnOrder *bool
Files []string
}

// configuredIgnoreColumnOrder applies, from lowest to highest precedence:
// built-in flag default, $HOME config, working-directory config, explicit CLI
// flag. Inside each file the subcommand section overrides the global section.
func configuredIgnoreColumnOrder(fs *flag.FlagSet, command string, flagValue bool) (bool, error) {
workdir, err := os.Getwd()
if err != nil {
return false, fmt.Errorf("get working directory: %w", err)
}
resolved, err := resolveHCLExpConfig(os.Getenv("HOME"), workdir, command)
if err != nil {
return false, err
}
renderHCLExpConfigInfoIfTerminal(os.Stderr, resolved.Files, term.IsTerminal(int(os.Stderr.Fd())))
return effectiveIgnoreColumnOrder(resolved, flagWasSet(fs, "ignore-column-order"), flagValue), nil
}

func resolveHCLExpConfig(home, workdir, command string) (hclexpConfigResolution, error) {
if !isComparisonCommand(command) {
return hclexpConfigResolution{}, fmt.Errorf("unsupported config subcommand %q", command)
}

var resolved hclexpConfigResolution
seen := map[string]bool{}
for _, dir := range []string{home, workdir} {
if dir == "" {
continue
}
path, err := filepath.Abs(filepath.Join(dir, hclexpConfigFilename))
if err != nil {
return hclexpConfigResolution{}, fmt.Errorf("resolve config path in %q: %w", dir, err)
}
path = filepath.Clean(path)
if seen[path] {
continue
}
seen[path] = true
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
continue
}
return hclexpConfigResolution{}, fmt.Errorf("stat %s: %w", path, err)
}

parser := hclparse.NewParser()
file, diagnostics := parser.ParseHCLFile(path)
if diagnostics.HasErrors() {
return hclexpConfigResolution{}, fmt.Errorf("parse %s: %s", path, diagnostics)
}
var config hclexpConfigFile
if diagnostics := gohcl.DecodeBody(file.Body, nil, &config); diagnostics.HasErrors() {
return hclexpConfigResolution{}, fmt.Errorf("parse %s: %s", path, diagnostics)
}
resolved.Files = append(resolved.Files, path)
applyHCLExpConfigSection(&resolved, config.Global)
applyHCLExpConfigSection(&resolved, config.section(command))
}
return resolved, nil
}

func (c hclexpConfigFile) section(command string) *hclexpConfigSection {
switch command {
case "diff":
return c.Diff
case "drift":
return c.Drift
case "plan":
return c.Plan
case "web":
return c.Web
default:
return nil
}
}

func isComparisonCommand(command string) bool {
switch command {
case "diff", "drift", "plan", "web":
return true
default:
return false
}
}

func applyHCLExpConfigSection(resolved *hclexpConfigResolution, section *hclexpConfigSection) {
if section == nil || section.IgnoreColumnOrder == nil {
return
}
value := *section.IgnoreColumnOrder
resolved.IgnoreColumnOrder = &value
}

func effectiveIgnoreColumnOrder(resolved hclexpConfigResolution, explicit bool, flagValue bool) bool {
if explicit {
return flagValue
}
if resolved.IgnoreColumnOrder != nil {
return *resolved.IgnoreColumnOrder
}
return flagValue
}

func renderHCLExpConfigInfo(w io.Writer, files []string) {
for _, file := range files {
fmt.Fprintf(w, "hclexp: loaded config %s\n", file)
}
}

func renderHCLExpConfigInfoIfTerminal(w io.Writer, files []string, terminal bool) {
if terminal {
renderHCLExpConfigInfo(w, files)
}
}
Loading