Skip to content

Commit

Permalink
node pools: implement node pool init command (hashicorp#17479)
Browse files Browse the repository at this point in the history
Implement a `nomad node pool init` command that generates an example spec file
in either HCL or JSON format.
  • Loading branch information
tgross committed Jun 13, 2023
1 parent 5db9e64 commit 0aeeaf1
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 0 deletions.
6 changes: 6 additions & 0 deletions command/asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions command/asset/pool.nomad.hcl
Original file line number Diff line number Diff line change
@@ -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"
# }
}
11 changes: 11 additions & 0 deletions command/asset/pool.nomad.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Name": "example",
"Description": "Example node pool",
"Meta": {
"environment": "prod",
"owner": "sre"
},
"SchedulerConfiguration": {
"SchedulerAlgorithm": "spread"
}
}
5 changes: 5 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
128 changes: 128 additions & 0 deletions command/node_pool_init.go
Original file line number Diff line number Diff line change
@@ -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
}
119 changes: 119 additions & 0 deletions command/node_pool_init_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
36 changes: 36 additions & 0 deletions website/content/docs/commands/node-pool/init.mdx
Original file line number Diff line number Diff line change
@@ -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
```
4 changes: 4 additions & 0 deletions website/data/docs-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 0aeeaf1

Please sign in to comment.