cli: add `nomad fmt` (#14779)
This commit is contained in:
parent
4b93a30225
commit
95f969c4bf
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
cli: add nomad fmt to the CLI
|
||||
```
|
|
@ -243,6 +243,7 @@ hclfmt: ## Format HCL files with hclfmt
|
|||
-o -name '.next' -prune \
|
||||
-o -path './ui/dist' -prune \
|
||||
-o -path './website/out' -prune \
|
||||
-o -path './command/testdata' -prune \
|
||||
-o \( -name '*.nomad' -o -name '*.hcl' -o -name '*.tf' \) \
|
||||
-print0 | xargs -0 hclfmt -w
|
||||
|
||||
|
|
|
@ -325,6 +325,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
|||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"fmt": func() (cli.Command, error) {
|
||||
return &FormatCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"fs": func() (cli.Command, error) {
|
||||
return &AllocFSCommand{
|
||||
Meta: meta,
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
"github.com/posener/complete"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
const (
|
||||
stdinArg = "-"
|
||||
stdinPath = "<stdin>"
|
||||
)
|
||||
|
||||
type FormatCommand struct {
|
||||
Meta
|
||||
|
||||
diagWr hcl.DiagnosticWriter
|
||||
|
||||
parser *hclparse.Parser
|
||||
hclDiags hcl.Diagnostics
|
||||
|
||||
errs *multierror.Error
|
||||
|
||||
list bool
|
||||
check bool
|
||||
recursive bool
|
||||
write bool
|
||||
paths []string
|
||||
|
||||
stdin io.Reader
|
||||
}
|
||||
|
||||
func (*FormatCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad fmt [flags] paths ...
|
||||
|
||||
Formats Nomad agent configuration and job file to a canonical format.
|
||||
If a path is a directory, it will recursively format all files
|
||||
with .nomad and .hcl extensions in the directory.
|
||||
|
||||
If you provide a single dash (-) as argument, fmt will read from standard
|
||||
input (STDIN) and output the processed output to standard output (STDOUT).
|
||||
|
||||
Format Options:
|
||||
|
||||
-list=false
|
||||
Don't list the files, which contain formatting inconsistencies.
|
||||
|
||||
-check
|
||||
Check if the files are valid HCL files. If not, exit status of the command
|
||||
will be 1 and the incorrect files will not be formatted.
|
||||
|
||||
-write=false
|
||||
Don't overwrite the input files.
|
||||
|
||||
-recursive
|
||||
Process also files in subdirectories. By default only the given (or current) directory is processed.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (*FormatCommand) Synopsis() string {
|
||||
return "Rewrites Nomad config and job files to canonical format"
|
||||
}
|
||||
|
||||
func (*FormatCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictFiles("*")
|
||||
}
|
||||
|
||||
func (*FormatCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-check": complete.PredictNothing,
|
||||
"-write": complete.PredictNothing,
|
||||
"-list": complete.PredictNothing,
|
||||
"-recursive": complete.PredictNothing,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FormatCommand) Name() string { return "fmt" }
|
||||
|
||||
func (f *FormatCommand) Run(args []string) int {
|
||||
if f.stdin == nil {
|
||||
f.stdin = os.Stdin
|
||||
}
|
||||
|
||||
flags := f.Meta.FlagSet(f.Name(), FlagSetClient)
|
||||
flags.Usage = func() { f.Ui.Output(f.Help()) }
|
||||
flags.BoolVar(&f.check, "check", false, "")
|
||||
flags.BoolVar(&f.write, "write", true, "")
|
||||
flags.BoolVar(&f.list, "list", true, "")
|
||||
flags.BoolVar(&f.recursive, "recursive", false, "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
f.parser = hclparse.NewParser()
|
||||
|
||||
color := terminal.IsTerminal(int(os.Stderr.Fd()))
|
||||
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil {
|
||||
w = 80
|
||||
}
|
||||
|
||||
f.diagWr = hcl.NewDiagnosticTextWriter(os.Stderr, f.parser.Files(), uint(w), color)
|
||||
|
||||
if len(flags.Args()) == 0 {
|
||||
f.paths = []string{"."}
|
||||
} else if flags.Args()[0] == stdinArg {
|
||||
f.write = false
|
||||
f.list = false
|
||||
} else {
|
||||
f.paths = flags.Args()
|
||||
}
|
||||
|
||||
f.fmt()
|
||||
|
||||
if f.hclDiags.HasErrors() {
|
||||
f.diagWr.WriteDiagnostics(f.hclDiags)
|
||||
}
|
||||
|
||||
if f.errs != nil {
|
||||
f.Ui.Error(f.errs.Error())
|
||||
f.Ui.Error(commandErrorText(f))
|
||||
}
|
||||
|
||||
if f.hclDiags.HasErrors() || f.errs != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *FormatCommand) fmt() {
|
||||
if len(f.paths) == 0 {
|
||||
f.processFile(stdinPath, f.stdin)
|
||||
return
|
||||
}
|
||||
|
||||
for _, path := range f.paths {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
f.appendError(fmt.Errorf("No file or directory at %s", path))
|
||||
continue
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
f.processDir(path)
|
||||
} else {
|
||||
if isNomadFile(info) {
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
f.appendError(fmt.Errorf("Failed to open file %s: %w", path, err))
|
||||
continue
|
||||
}
|
||||
|
||||
f.processFile(path, fp)
|
||||
|
||||
fp.Close()
|
||||
} else {
|
||||
f.appendError(fmt.Errorf("Only .nomad and .hcl files can be processed using nomad fmt"))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FormatCommand) processDir(path string) {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
f.appendError(fmt.Errorf("Failed to list directory %s", path))
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
subpath := filepath.Join(path, name)
|
||||
|
||||
if entry.IsDir() {
|
||||
if f.recursive {
|
||||
f.processDir(subpath)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
f.appendError(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if isNomadFile(info) {
|
||||
fp, err := os.Open(subpath)
|
||||
if err != nil {
|
||||
f.appendError(fmt.Errorf("Failed to open file %s: %w", path, err))
|
||||
continue
|
||||
}
|
||||
|
||||
f.processFile(subpath, fp)
|
||||
|
||||
fp.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FormatCommand) processFile(path string, r io.Reader) {
|
||||
src, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
f.appendError(fmt.Errorf("Failed to read file %s: %w", path, err))
|
||||
return
|
||||
}
|
||||
|
||||
f.parser.AddFile(path, &hcl.File{
|
||||
Body: hcl.EmptyBody(),
|
||||
Bytes: src,
|
||||
})
|
||||
|
||||
_, syntaxDiags := hclsyntax.ParseConfig(src, path, hcl.InitialPos)
|
||||
if syntaxDiags.HasErrors() {
|
||||
f.hclDiags = append(f.hclDiags, syntaxDiags...)
|
||||
return
|
||||
}
|
||||
formattedFile, diags := hclwrite.ParseConfig(src, path, hcl.InitialPos)
|
||||
if diags.HasErrors() {
|
||||
f.hclDiags = append(f.hclDiags, diags...)
|
||||
return
|
||||
}
|
||||
|
||||
out := formattedFile.Bytes()
|
||||
|
||||
if !bytes.Equal(src, out) {
|
||||
if f.list {
|
||||
f.Ui.Output(path)
|
||||
}
|
||||
|
||||
if f.write {
|
||||
if err := os.WriteFile(path, out, 0644); err != nil {
|
||||
f.appendError(fmt.Errorf("Failed to write file %s: %w", path, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !f.list && !f.write {
|
||||
f.Ui.Output(string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func isNomadFile(file fs.FileInfo) bool {
|
||||
return !file.IsDir() && (filepath.Ext(file.Name()) == ".nomad" || filepath.Ext(file.Name()) == ".hcl")
|
||||
}
|
||||
|
||||
func (f *FormatCommand) appendError(err error) {
|
||||
f.errs = multierror.Append(f.errs, err)
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFmtCommand(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
const inSuffix = ".in.hcl"
|
||||
const expectedSuffix = ".out.hcl"
|
||||
tests := []string{"nomad", "job"}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
for _, testName := range tests {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
inFile := filepath.Join("testdata", "fmt", testName+inSuffix)
|
||||
expectedFile := filepath.Join("testdata", "fmt", testName+expectedSuffix)
|
||||
fmtFile := filepath.Join(tmpDir, testName+".hcl")
|
||||
|
||||
input, err := os.ReadFile(inFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile(expectedFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(fmtFile, input, 0644))
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &FormatCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
|
||||
code := cmd.Run([]string{fmtFile})
|
||||
assert.Equal(t, 0, code)
|
||||
|
||||
actual, err := os.ReadFile(fmtFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, string(expected), string(actual))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFmtCommand_FromStdin(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
stdinFake := bytes.NewBuffer(fmtFixture.input)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &FormatCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
stdin: stdinFake,
|
||||
}
|
||||
|
||||
if code := cmd.Run([]string{"-"}); code != 0 {
|
||||
t.Fatalf("expected code 0, got %d", code)
|
||||
}
|
||||
|
||||
assert.Contains(t, ui.OutputWriter.String(), string(fmtFixture.golden))
|
||||
}
|
||||
|
||||
func TestFmtCommand_FromWorkingDirectory(t *testing.T) {
|
||||
tmpDir := fmtFixtureWriteDir(t)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Chdir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
defer os.Chdir(cwd)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &FormatCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
|
||||
code := cmd.Run([]string{})
|
||||
|
||||
assert.Equal(t, 0, code)
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", fmtFixture.filename), ui.OutputWriter.String())
|
||||
}
|
||||
|
||||
func TestFmtCommand_FromDirectoryArgument(t *testing.T) {
|
||||
tmpDir := fmtFixtureWriteDir(t)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &FormatCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
|
||||
code := cmd.Run([]string{tmpDir})
|
||||
|
||||
assert.Equal(t, 0, code)
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", filepath.Join(tmpDir, fmtFixture.filename)), ui.OutputWriter.String())
|
||||
}
|
||||
|
||||
func TestFmtCommand_FromFileArgument(t *testing.T) {
|
||||
tmpDir := fmtFixtureWriteDir(t)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &FormatCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
|
||||
path := filepath.Join(tmpDir, fmtFixture.filename)
|
||||
|
||||
code := cmd.Run([]string{path})
|
||||
|
||||
assert.Equal(t, 0, code)
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", path), ui.OutputWriter.String())
|
||||
}
|
||||
|
||||
func TestFmtCommand_FileDoesNotExist(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &FormatCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
|
||||
code := cmd.Run([]string{"file/does/not/exist.hcl"})
|
||||
assert.Equal(t, 1, code)
|
||||
}
|
||||
|
||||
func TestFmtCommand_InvalidSyntax(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
stdinFake := bytes.NewBufferString(`client {enabled true }`)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &FormatCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
stdin: stdinFake,
|
||||
}
|
||||
|
||||
code := cmd.Run([]string{"-"})
|
||||
assert.Equal(t, 1, code)
|
||||
}
|
||||
|
||||
func fmtFixtureWriteDir(t *testing.T) string {
|
||||
dir := t.TempDir()
|
||||
|
||||
err := ioutil.WriteFile(filepath.Join(dir, fmtFixture.filename), fmtFixture.input, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
var fmtFixture = struct {
|
||||
filename string
|
||||
input []byte
|
||||
golden []byte
|
||||
}{
|
||||
filename: "nomad.hcl",
|
||||
input: []byte(`client {enabled = true}`),
|
||||
golden: []byte(`client { enabled = true }`),
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
job "job1" {
|
||||
type = "service"
|
||||
datacenters = [ "dc1" ]
|
||||
group "group1" {
|
||||
count = 1
|
||||
task "task1" {
|
||||
driver = "exec"
|
||||
config {
|
||||
command = "/bin/sleep"
|
||||
}
|
||||
resources {
|
||||
cpu = 1000
|
||||
memory = 512
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
job "job1" {
|
||||
type = "service"
|
||||
datacenters = ["dc1"]
|
||||
group "group1" {
|
||||
count = 1
|
||||
task "task1" {
|
||||
driver = "exec"
|
||||
config {
|
||||
command = "/bin/sleep"
|
||||
}
|
||||
resources {
|
||||
cpu = 1000
|
||||
memory = 512
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
server {
|
||||
enabled = true
|
||||
bootstrap_expect = 3
|
||||
}
|
||||
|
||||
consul {
|
||||
address = "1.2.3.4:8500"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
server {
|
||||
enabled = true
|
||||
bootstrap_expect = 3
|
||||
}
|
||||
|
||||
consul {
|
||||
address = "1.2.3.4:8500"
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: 'Commands: fmt'
|
||||
description: |
|
||||
Rewrite Nomad config and job files to canonical format
|
||||
---
|
||||
|
||||
# Command: fmt
|
||||
|
||||
The `fmt` commands check the syntax and rewrites Nomad configuration and jobspec
|
||||
files to canonical format. It can be used to improve readability and enforce
|
||||
consistency of style in Nomad files.
|
||||
|
||||
## Usage
|
||||
|
||||
```plaintext
|
||||
nomad fmt [flags] paths ...
|
||||
```
|
||||
|
||||
Formats Nomad agent configuration and job file to a canonical format. If a path
|
||||
is a directory, it will recursively format all files with .nomad and .hcl
|
||||
extensions in the directory.
|
||||
|
||||
If you provide a single dash (-) as argument, fmt will read from standard input
|
||||
(STDIN) and output the processed output to standard output (STDOUT).
|
||||
|
||||
## Format Options:
|
||||
|
||||
- `-list=false` : Don't list the files, which contain formatting inconsistencies.
|
||||
- `-check` : Check if the files are valid HCL files. If not, exit status of the command
|
||||
will be 1 and the incorrect files will not be formatted.
|
||||
- `-write=false` : Don't overwrite the input files.
|
||||
- `-recursive` : Process also files in subdirectories. By default only the given (or current) directory is processed.
|
||||
|
||||
## Examples
|
||||
|
||||
```shell-session
|
||||
$ cat agent.hcl
|
||||
server {
|
||||
enabled = true
|
||||
bootstrap_expect = 1
|
||||
}
|
||||
|
||||
client {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
$ nomad fmt
|
||||
|
||||
agent.hcl
|
||||
$ cat agent.hcl
|
||||
server {
|
||||
enabled = true
|
||||
bootstrap_expect = 1
|
||||
}
|
||||
|
||||
client {
|
||||
enabled = true
|
||||
}
|
||||
```
|
|
@ -429,6 +429,10 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "fmt",
|
||||
"path": "commands/fmt"
|
||||
},
|
||||
{
|
||||
"title": "job",
|
||||
"routes": [
|
||||
|
|
Loading…
Reference in New Issue