mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
Add CLI and API components for creating node introduction tokens via ACL endpoint. (#26332)
This commit is contained in:
@@ -644,6 +644,16 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node intro": func() (cli.Command, error) {
|
||||
return &NodeIntroCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node intro create": func() (cli.Command, error) {
|
||||
return &NodeIntroCreateCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node meta": func() (cli.Command, error) {
|
||||
return &NodeMetaCommand{
|
||||
Meta: meta,
|
||||
|
||||
34
command/node_intro.go
Normal file
34
command/node_intro.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/cli"
|
||||
)
|
||||
|
||||
type NodeIntroCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (n *NodeIntroCommand) Name() string { return "node intro" }
|
||||
|
||||
func (n *NodeIntroCommand) Run(_ []string) int { return cli.RunResultHelp }
|
||||
|
||||
func (n *NodeIntroCommand) Synopsis() string {
|
||||
return "Tooling for managing node introduction tokens"
|
||||
}
|
||||
|
||||
func (n *NodeIntroCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node intro <subcommand> [options]
|
||||
|
||||
This command groups subcommands for managing node introduction tokens. These
|
||||
tokens are used to authenticate new Nomad client nodes to the cluster.
|
||||
|
||||
Please see the individual subcommand help for detailed usage information.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
145
command/node_intro_create.go
Normal file
145
command/node_intro_create.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type NodeIntroCreateCommand struct {
|
||||
Meta
|
||||
|
||||
// Fields for the command flags.
|
||||
json bool
|
||||
tmpl string
|
||||
ttl string
|
||||
nodeName string
|
||||
nodePool string
|
||||
}
|
||||
|
||||
func (n *NodeIntroCreateCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node intro create [options]
|
||||
|
||||
Generates a new node introduction token. This token is used to authenticate
|
||||
a new Nomad client node to the cluster.
|
||||
|
||||
If ACLs are enabled, this command requires a token with the 'node:write'
|
||||
capability.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
Create Options:
|
||||
|
||||
-node-name
|
||||
The name of the node to which the introduction token will be scoped. If not
|
||||
specified, the value will be left empty.
|
||||
|
||||
-node-pool
|
||||
The node pool to which the introduction token will be scoped. If not
|
||||
specified, the value "default" will be used.
|
||||
|
||||
-ttl
|
||||
The TTL to apply to the introduction token. If not specified, the server
|
||||
configured default value will be used.
|
||||
|
||||
-json
|
||||
Output the response object in JSON format.
|
||||
|
||||
-t
|
||||
Format and display the response object using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (n *NodeIntroCreateCommand) Synopsis() string {
|
||||
return "Generate a new node introduction token"
|
||||
}
|
||||
|
||||
func (n *NodeIntroCreateCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(n.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-node-pool": nodePoolPredictor(n.Client, nil),
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (n *NodeIntroCreateCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (n *NodeIntroCreateCommand) Name() string { return "node intro create" }
|
||||
|
||||
func (n *NodeIntroCreateCommand) Run(args []string) int {
|
||||
|
||||
flags := n.Meta.FlagSet(n.Name(), FlagSetClient)
|
||||
flags.Usage = func() { n.Ui.Output(n.Help()) }
|
||||
flags.StringVar(&n.ttl, "ttl", "", "")
|
||||
flags.StringVar(&n.nodeName, "node-name", "", "")
|
||||
flags.StringVar(&n.nodePool, "node-pool", "", "")
|
||||
flags.StringVar(&n.tmpl, "t", "", "")
|
||||
flags.BoolVar(&n.json, "json", false, "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
args = flags.Args()
|
||||
if len(args) != 0 {
|
||||
n.Ui.Error(uiMessageNoArguments)
|
||||
n.Ui.Error(commandErrorText(n))
|
||||
return 1
|
||||
}
|
||||
|
||||
var ttlTime time.Duration
|
||||
|
||||
if n.ttl != "" {
|
||||
parsedTTL, err := time.ParseDuration(n.ttl)
|
||||
if err != nil {
|
||||
n.Ui.Error(fmt.Sprintf("Error parsing TTL: %s", err))
|
||||
return 1
|
||||
}
|
||||
ttlTime = parsedTTL
|
||||
}
|
||||
|
||||
client, err := n.Meta.Client()
|
||||
if err != nil {
|
||||
n.Ui.Error(fmt.Sprintf("Error creating Nomad client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
req := api.ACLIdentityClientIntroductionTokenRequest{
|
||||
TTL: ttlTime,
|
||||
NodeName: n.nodeName,
|
||||
NodePool: n.nodePool,
|
||||
}
|
||||
|
||||
resp, _, err := client.ACLIdentity().CreateClientIntroductionToken(&req, nil)
|
||||
if err != nil {
|
||||
n.Ui.Error(fmt.Sprintf("Error generating introduction token: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if n.json || n.tmpl != "" {
|
||||
out, err := Format(n.json, n.tmpl, resp)
|
||||
if err != nil {
|
||||
n.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
n.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
n.Ui.Output(fmt.Sprintf("Successfully generated client introduction token:\n\n%s", resp.JWT))
|
||||
return 0
|
||||
}
|
||||
115
command/node_intro_create_test.go
Normal file
115
command/node_intro_create_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/cli"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNodeIntroCreateCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodeIntroCreateCommand{}
|
||||
}
|
||||
|
||||
func TestNodeIntroCreateCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
srv, _, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait until our test node is ready.
|
||||
testutil.WaitForClient(
|
||||
t,
|
||||
srv.Agent.Client().RPC,
|
||||
srv.Agent.Client().NodeID(),
|
||||
srv.Agent.Client().Region(),
|
||||
)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
|
||||
cmd := &NodeIntroCreateCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Run the command with some random arguments to ensure we are performing
|
||||
// this check.
|
||||
t.Run("with command argument", func(t *testing.T) {
|
||||
t.Cleanup(func() { resetUI(ui) })
|
||||
|
||||
must.One(t, cmd.Run([]string{"pretty-please"}))
|
||||
must.StrContains(t, ui.ErrorWriter.String(), "This command takes no arguments")
|
||||
ui.ErrorWriter.Reset()
|
||||
})
|
||||
|
||||
// Run the command with an invalid TTL, which should return an error as we
|
||||
// parse this within the CLI.
|
||||
t.Run("incorrect TTL", func(t *testing.T) {
|
||||
t.Cleanup(func() { resetUI(ui) })
|
||||
|
||||
must.One(t, cmd.Run([]string{"-ttl=1millennium"}))
|
||||
must.StrContains(t, ui.ErrorWriter.String(), "Error parsing TTL")
|
||||
ui.ErrorWriter.Reset()
|
||||
})
|
||||
|
||||
// Run the command with no flags supplied, which should write the JWT to the
|
||||
// console.
|
||||
t.Run("standard format output", func(t *testing.T) {
|
||||
must.Zero(t, cmd.Run([]string{"--address=" + url}))
|
||||
|
||||
t.Cleanup(func() { resetUI(ui) })
|
||||
must.StrContains(t, ui.OutputWriter.String(),
|
||||
"Successfully generated client introduction token")
|
||||
ui.OutputWriter.Reset()
|
||||
})
|
||||
|
||||
// Run the command with all claim specific flags supplied, which should
|
||||
// write the JWT to the console.
|
||||
t.Run("standard format output all flags", func(t *testing.T) {
|
||||
must.Zero(t, cmd.Run([]string{
|
||||
"--address=" + url,
|
||||
"-ttl=1h",
|
||||
"-node-name=test-node",
|
||||
"-node-pool=test-pool",
|
||||
}))
|
||||
|
||||
t.Cleanup(func() { resetUI(ui) })
|
||||
must.StrContains(t, ui.OutputWriter.String(),
|
||||
"Successfully generated client introduction token")
|
||||
ui.OutputWriter.Reset()
|
||||
})
|
||||
|
||||
// Run the command with the JSON flag supplied and ensure the output looks
|
||||
// like valid JSON.
|
||||
t.Run("json format output", func(t *testing.T) {
|
||||
t.Cleanup(func() { resetUI(ui) })
|
||||
|
||||
must.Zero(t, cmd.Run([]string{"--address=" + url, "-json"}))
|
||||
|
||||
var jsonObj api.ACLIdentityClientIntroductionTokenResponse
|
||||
must.NoError(t, json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonObj))
|
||||
must.NotEq(t, "", jsonObj.JWT)
|
||||
})
|
||||
|
||||
t.Run("template format output", func(t *testing.T) {
|
||||
t.Cleanup(func() { resetUI(ui) })
|
||||
|
||||
must.Zero(t, cmd.Run([]string{"--address=" + url, "-t={{.JWT}}"}))
|
||||
must.NotEq(t, "", ui.OutputWriter.String())
|
||||
})
|
||||
}
|
||||
|
||||
func resetUI(ui *cli.MockUi) {
|
||||
ui.ErrorWriter.Reset()
|
||||
ui.OutputWriter.Reset()
|
||||
}
|
||||
16
command/node_intro_test.go
Normal file
16
command/node_intro_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/cli"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
)
|
||||
|
||||
func TestNodeIntroCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodeIntroCommand{}
|
||||
}
|
||||
Reference in New Issue
Block a user