From c1a01697c8528fd10b0a20a72c72e3e489fcab0b Mon Sep 17 00:00:00 2001 From: Tim Gross Date: Tue, 13 Jun 2023 14:51:29 -0400 Subject: [PATCH] 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. --- command/asset/asset.go | 6 + command/asset/pool.nomad.hcl | 25 ++++ command/asset/pool.nomad.json | 11 ++ command/commands.go | 5 + command/node_pool_init.go | 128 ++++++++++++++++++ command/node_pool_init_test.go | 119 ++++++++++++++++ .../content/docs/commands/node-pool/init.mdx | 36 +++++ website/data/docs-nav-data.json | 4 + 8 files changed, 334 insertions(+) create mode 100644 command/asset/pool.nomad.hcl create mode 100644 command/asset/pool.nomad.json create mode 100644 command/node_pool_init.go create mode 100644 command/node_pool_init_test.go create mode 100644 website/content/docs/commands/node-pool/init.mdx diff --git a/command/asset/asset.go b/command/asset/asset.go index 474623e5d..3d713d739 100644 --- a/command/asset/asset.go +++ b/command/asset/asset.go @@ -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 diff --git a/command/asset/pool.nomad.hcl b/command/asset/pool.nomad.hcl new file mode 100644 index 000000000..e4c3ca228 --- /dev/null +++ b/command/asset/pool.nomad.hcl @@ -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" + # } +} diff --git a/command/asset/pool.nomad.json b/command/asset/pool.nomad.json new file mode 100644 index 000000000..02622902a --- /dev/null +++ b/command/asset/pool.nomad.json @@ -0,0 +1,11 @@ +{ + "Name": "example", + "Description": "Example node pool", + "Meta": { + "environment": "prod", + "owner": "sre" + }, + "SchedulerConfiguration": { + "SchedulerAlgorithm": "spread" + } +} diff --git a/command/commands.go b/command/commands.go index cd4a00812..9395a62b1 100644 --- a/command/commands.go +++ b/command/commands.go @@ -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, diff --git a/command/node_pool_init.go b/command/node_pool_init.go new file mode 100644 index 000000000..a423221fa --- /dev/null +++ b/command/node_pool_init.go @@ -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 + + 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: ") + 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 +} diff --git a/command/node_pool_init_test.go b/command/node_pool_init_test.go new file mode 100644 index 000000000..d07d9a24f --- /dev/null +++ b/command/node_pool_init_test.go @@ -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) + }) +} diff --git a/website/content/docs/commands/node-pool/init.mdx b/website/content/docs/commands/node-pool/init.mdx new file mode 100644 index 000000000..6f76c045e --- /dev/null +++ b/website/content/docs/commands/node-pool/init.mdx @@ -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 +``` + +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 +``` diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 656e1b240..f0617a41c 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -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"