node pools: implement `node pool init` command (#17479)
Implement a `nomad node pool init` command that generates an example spec file in either HCL or JSON format.
This commit is contained in:
parent
bc17cffaef
commit
c1a01697c8
|
@ -16,3 +16,9 @@ var JobConnect []byte
|
|||
|
||||
//go:embed connect-short.nomad.hcl
|
||||
var JobConnectShort []byte
|
||||
|
||||
//go:embed pool.nomad.hcl
|
||||
var NodePoolSpec []byte
|
||||
|
||||
//go:embed pool.nomad.json
|
||||
var NodePoolSpecJSON []byte
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
node_pool "example" {
|
||||
|
||||
description = "Example node pool"
|
||||
|
||||
# meta is optional metadata on the node pool, defined as key-value pairs.
|
||||
# The scheduler does not use node pool metadata as part of scheduling.
|
||||
meta {
|
||||
environment = "prod"
|
||||
owner = "sre"
|
||||
}
|
||||
|
||||
# The scheduler configuration options specific to this node pool. This block
|
||||
# supports a subset of the fields supported in the global scheduler
|
||||
# configuration as described at:
|
||||
# https://developer.hashicorp.com/nomad/docs/commands/operator/scheduler/set-config
|
||||
#
|
||||
# * scheduler_algorithm is the scheduling algorithm to use for the pool.
|
||||
# If not defined, the global cluster scheduling algorithm is used.
|
||||
#
|
||||
# Available only in Nomad Enterprise.
|
||||
|
||||
# scheduler_configuration {
|
||||
# scheduler_algorithm = "spread"
|
||||
# }
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"Name": "example",
|
||||
"Description": "Example node pool",
|
||||
"Meta": {
|
||||
"environment": "prod",
|
||||
"owner": "sre"
|
||||
},
|
||||
"SchedulerConfiguration": {
|
||||
"SchedulerAlgorithm": "spread"
|
||||
}
|
||||
}
|
|
@ -636,6 +636,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
|||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node pool init": func() (cli.Command, error) {
|
||||
return &NodePoolInitCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node pool jobs": func() (cli.Command, error) {
|
||||
return &NodePoolJobsCommand{
|
||||
Meta: meta,
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/command/asset"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHclNodePoolInitName is the default name we use when initializing
|
||||
// the example node pool spec file in HCL format
|
||||
DefaultHclNodePoolInitName = "pool.nomad.hcl"
|
||||
|
||||
// DefaultJsonNodePoolInitName is the default name we use when initializing
|
||||
// the example node pool spec in JSON format
|
||||
DefaultJsonNodePoolInitName = "pool.nomad.json"
|
||||
)
|
||||
|
||||
// NodePoolInitCommand generates a new variable specification
|
||||
type NodePoolInitCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodePoolInitCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node pool init <filename>
|
||||
|
||||
Creates an example node pool specification file that can be used as a starting
|
||||
point to customize further. When no filename is supplied, a default filename
|
||||
of "pool.nomad.hcl" or "pool.nomad.json" will be used depending on the output
|
||||
format.
|
||||
|
||||
Init Options:
|
||||
|
||||
-out (hcl | json)
|
||||
Format of generated node pool specification. Defaults to "hcl".
|
||||
|
||||
-quiet
|
||||
Do not print success message.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodePoolInitCommand) Synopsis() string {
|
||||
return "Create an example node pool specification file"
|
||||
}
|
||||
|
||||
func (c *NodePoolInitCommand) AutocompleteFlags() complete.Flags {
|
||||
return complete.Flags{
|
||||
"-out": complete.PredictSet("hcl", "json"),
|
||||
"-quiet": complete.PredictNothing,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *NodePoolInitCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *NodePoolInitCommand) Name() string { return "node pool init" }
|
||||
|
||||
func (c *NodePoolInitCommand) Run(args []string) int {
|
||||
var outFmt string
|
||||
var quiet bool
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
flags.StringVar(&outFmt, "out", "hcl", "")
|
||||
flags.BoolVar(&quiet, "quiet", false, "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we get no arguments
|
||||
args = flags.Args()
|
||||
if l := len(args); l > 1 {
|
||||
c.Ui.Error("This command takes no arguments or one: <filename>")
|
||||
c.Ui.Error(commandErrorText(c))
|
||||
return 1
|
||||
}
|
||||
var fileName string
|
||||
var fileContent []byte
|
||||
switch outFmt {
|
||||
case "hcl":
|
||||
fileName = DefaultHclNodePoolInitName
|
||||
fileContent = asset.NodePoolSpec
|
||||
case "json":
|
||||
fileName = DefaultJsonNodePoolInitName
|
||||
fileContent = asset.NodePoolSpecJSON
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
fileName = args[0]
|
||||
}
|
||||
|
||||
// Check if the file already exists
|
||||
_, err := os.Stat(fileName)
|
||||
if err == nil {
|
||||
c.Ui.Error(fmt.Sprintf("File %q already exists", fileName))
|
||||
return 1
|
||||
}
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to stat %q: %v", fileName, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Write out the example
|
||||
err = os.WriteFile(fileName, fileContent, 0660)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to write %q: %v", fileName, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Success
|
||||
if !quiet {
|
||||
c.Ui.Output(fmt.Sprintf("Example node pool specification written to %s", fileName))
|
||||
}
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test/must"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/asset"
|
||||
)
|
||||
|
||||
func TestNodePoolInitCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodePoolInitCommand{}
|
||||
}
|
||||
|
||||
func TestNodePoolInitCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
dir := t.TempDir()
|
||||
origDir, err := os.Getwd()
|
||||
must.NoError(t, err)
|
||||
err = os.Chdir(dir)
|
||||
must.NoError(t, err)
|
||||
t.Cleanup(func() { os.Chdir(origDir) })
|
||||
|
||||
t.Run("hcl", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
dir := dir
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolInitCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Fails on misuse
|
||||
ec := cmd.Run([]string{"some", "bad", "args"})
|
||||
must.Eq(t, 1, ec)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), commandErrorText(cmd))
|
||||
must.Eq(t, "", ui.OutputWriter.String())
|
||||
reset(ui)
|
||||
|
||||
// Works if the file doesn't exist
|
||||
ec = cmd.Run([]string{"-out", "hcl"})
|
||||
must.Eq(t, "", ui.ErrorWriter.String())
|
||||
must.Eq(t, "Example node pool specification written to pool.nomad.hcl\n", ui.OutputWriter.String())
|
||||
must.Zero(t, ec)
|
||||
reset(ui)
|
||||
t.Cleanup(func() { os.Remove(path.Join(dir, "pool.nomad.hcl")) })
|
||||
|
||||
content, err := os.ReadFile(DefaultHclNodePoolInitName)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, asset.NodePoolSpec, content)
|
||||
|
||||
// Fails if the file exists
|
||||
ec = cmd.Run([]string{"-out", "hcl"})
|
||||
must.StrContains(t, ui.ErrorWriter.String(), "exists")
|
||||
must.Eq(t, "", ui.OutputWriter.String())
|
||||
must.Eq(t, 1, ec)
|
||||
reset(ui)
|
||||
|
||||
// Works if file is passed
|
||||
ec = cmd.Run([]string{"-out", "hcl", "myTest.hcl"})
|
||||
must.Eq(t, "", ui.ErrorWriter.String())
|
||||
must.Eq(t, "Example node pool specification written to myTest.hcl\n", ui.OutputWriter.String())
|
||||
must.Zero(t, ec)
|
||||
reset(ui)
|
||||
|
||||
t.Cleanup(func() { os.Remove(path.Join(dir, "myTest.hcl")) })
|
||||
content, err = os.ReadFile("myTest.hcl")
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, asset.NodePoolSpec, content)
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
dir := dir
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolInitCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Fails on misuse
|
||||
code := cmd.Run([]string{"some", "bad", "args"})
|
||||
must.Eq(t, 1, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), "This command takes no arguments or one")
|
||||
must.Eq(t, "", ui.OutputWriter.String())
|
||||
reset(ui)
|
||||
|
||||
// Works if the file doesn't exist
|
||||
code = cmd.Run([]string{"-out", "json"})
|
||||
must.StrContains(t, ui.OutputWriter.String(), "Example node pool specification written to pool.nomad.json\n")
|
||||
must.Zero(t, code)
|
||||
reset(ui)
|
||||
|
||||
t.Cleanup(func() { os.Remove(path.Join(dir, "pool.nomad.json")) })
|
||||
content, err := os.ReadFile(DefaultJsonNodePoolInitName)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, asset.NodePoolSpecJSON, content)
|
||||
|
||||
// Fails if the file exists
|
||||
code = cmd.Run([]string{"-out", "json"})
|
||||
must.StrContains(t, ui.ErrorWriter.String(), "exists")
|
||||
must.Eq(t, "", ui.OutputWriter.String())
|
||||
must.Eq(t, 1, code)
|
||||
reset(ui)
|
||||
|
||||
// Works if file is passed
|
||||
code = cmd.Run([]string{"-out", "json", "myTest.json"})
|
||||
must.StrContains(t, ui.OutputWriter.String(), "Example node pool specification written to myTest.json\n")
|
||||
must.Zero(t, code)
|
||||
reset(ui)
|
||||
|
||||
t.Cleanup(func() { os.Remove(path.Join(dir, "myTest.json")) })
|
||||
content, err = os.ReadFile("myTest.json")
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, asset.NodePoolSpecJSON, content)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: 'Commands: node pool init'
|
||||
description: |
|
||||
Generate an example node pool specification.
|
||||
---
|
||||
|
||||
# Command: node pool init
|
||||
|
||||
The `node pool init` creates an example node pool specification file that can be
|
||||
used as a starting point to customize further.
|
||||
|
||||
## Usage
|
||||
|
||||
```plaintext
|
||||
nomad node pool init <filename>
|
||||
```
|
||||
|
||||
When no filename is supplied, a default filename of "pool.nomad.hcl" or
|
||||
"pool.nomad.json" will be used depending on the output format.
|
||||
|
||||
## Init Options
|
||||
|
||||
- `-out` `(enum: hcl | json)`: Format of generated node pool
|
||||
specification. Defaults to `hcl`.
|
||||
|
||||
- `-quiet`: Do not print success message.
|
||||
|
||||
## Examples
|
||||
|
||||
Create an example node pool specification:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool init
|
||||
Example node pool specification written to pool.nomad.hcl
|
||||
```
|
|
@ -691,6 +691,10 @@
|
|||
"title": "info",
|
||||
"path": "commands/node-pool/info"
|
||||
},
|
||||
{
|
||||
"title": "init",
|
||||
"path": "commands/node-pool/init"
|
||||
},
|
||||
{
|
||||
"title": "jobs",
|
||||
"path": "commands/node-pool/jobs"
|
||||
|
|
Loading…
Reference in New Issue