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:
Tim Gross 2023-06-13 14:51:29 -04:00 committed by GitHub
parent bc17cffaef
commit c1a01697c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 334 additions and 0 deletions

View File

@ -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

View File

@ -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"
# }
}

View File

@ -0,0 +1,11 @@
{
"Name": "example",
"Description": "Example node pool",
"Meta": {
"environment": "prod",
"owner": "sre"
},
"SchedulerConfiguration": {
"SchedulerAlgorithm": "spread"
}
}

View File

@ -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,

128
command/node_pool_init.go Normal file
View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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
```

View File

@ -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"