Merge pull request #26291 from hashicorp/f-NMD-763-identity

identity: The initial implementation code for node identity.
This commit is contained in:
James Rasell
2025-08-05 09:52:28 +02:00
committed by GitHub
73 changed files with 3961 additions and 439 deletions

View File

@@ -23,7 +23,7 @@ func TestACLBootstrapCommand(t *testing.T) {
c.ACL.PolicyTTL = 0
}
srv, _, url := testServer(t, true, config)
srv, _, url := testServer(t, false, config)
defer srv.Shutdown()
must.Nil(t, srv.RootToken)
@@ -101,7 +101,7 @@ func TestACLBootstrapCommand_WithOperatorFileBootstrapToken(t *testing.T) {
err := os.WriteFile(file, []byte(mockToken.SecretID), 0700)
must.NoError(t, err)
srv, _, url := testServer(t, true, config)
srv, _, url := testServer(t, false, config)
defer srv.Shutdown()
must.Nil(t, srv.RootToken)
@@ -139,7 +139,7 @@ func TestACLBootstrapCommand_WithBadOperatorFileBootstrapToken(t *testing.T) {
err := os.WriteFile(file, []byte(invalidToken), 0700)
must.NoError(t, err)
srv, _, url := testServer(t, true, config)
srv, _, url := testServer(t, false, config)
defer srv.Shutdown()
must.Nil(t, srv.RootToken)

View File

@@ -1120,7 +1120,7 @@ func TestServer_Reload_TLS_Shared_Keyloader(t *testing.T) {
TLSConfig: &config.TLSConfig{
EnableHTTP: true,
EnableRPC: true,
VerifyServerHostname: true,
VerifyServerHostname: false,
CAFile: foocafile,
CertFile: fooclientcert,
KeyFile: fooclientkey,

View File

@@ -450,6 +450,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest)))
s.mux.Handle("/v1/client/allocation/", wrapCORS(s.wrap(s.ClientAllocRequest)))
s.mux.Handle("/v1/client/metadata", wrapCORS(s.wrap(s.NodeMetaRequest)))
s.mux.Handle("/v1/client/identity/renew", wrapCORS(s.wrap(s.NodeIdentityRenewRequest)))
s.mux.HandleFunc("/v1/agent/self", s.wrap(s.AgentSelfRequest))
s.mux.HandleFunc("/v1/agent/join", s.wrap(s.AgentJoinRequest))

View File

@@ -0,0 +1,43 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package agent
import (
"net/http"
"github.com/hashicorp/nomad/nomad/structs"
)
func (s *HTTPServer) NodeIdentityRenewRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Build the request by parsing all common parameters and node id
args := structs.NodeIdentityRenewReq{}
s.parse(resp, req, &args.QueryOptions.Region, &args.QueryOptions)
parseNode(req, &args.NodeID)
// Determine the handler to use
useLocalClient, useClientRPC, useServerRPC := s.rpcHandlerForNode(args.NodeID)
// Make the RPC
var reply structs.NodeIdentityRenewResp
var rpcErr error
if useLocalClient {
rpcErr = s.agent.Client().ClientRPC(structs.NodeIdentityRenewRPCMethod, &args, &reply)
} else if useClientRPC {
rpcErr = s.agent.Client().RPC(structs.NodeIdentityRenewRPCMethod, &args, &reply)
} else if useServerRPC {
rpcErr = s.agent.Server().RPC(structs.NodeIdentityRenewRPCMethod, &args, &reply)
} else {
rpcErr = CodedError(400, "no local Node and node_id not provided")
}
if rpcErr != nil {
if structs.IsErrNoNodeConn(rpcErr) {
rpcErr = CodedError(404, rpcErr.Error())
}
return nil, rpcErr
}
return reply, nil
}

View File

@@ -639,6 +639,16 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"node identity": func() (cli.Command, error) {
return &NodeIdentityCommand{
Meta: meta,
}, nil
},
"node identity renew": func() (cli.Command, error) {
return &NodeIdentityRenewCommand{
Meta: meta,
}, nil
},
"node meta": func() (cli.Command, error) {
return &NodeMetaCommand{
Meta: meta,

34
command/node_identity.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"strings"
"github.com/hashicorp/cli"
)
type NodeIdentityCommand struct {
Meta
}
func (n *NodeIdentityCommand) Help() string {
helpText := `
Usage: nomad node identity [subcommand]
Interact with a node's identity. All commands interact directly with a client
and require setting the target node via its 36 character ID.
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (n *NodeIdentityCommand) Synopsis() string { return "Force renewal of a nodes identity" }
func (n *NodeIdentityCommand) Name() string { return "node identity" }
func (n *NodeIdentityCommand) Run(_ []string) int {
return cli.RunResultHelp
}

View File

@@ -0,0 +1,88 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete"
)
type NodeIdentityRenewCommand struct {
Meta
}
func (n *NodeIdentityRenewCommand) Help() string {
helpText := `
Usage: nomad node identity renew [options] <node_id>
Instruct a node to renew its identity at the next heartbeat. This command only
applies to client agents.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace)
return strings.TrimSpace(helpText)
}
func (n *NodeIdentityRenewCommand) Synopsis() string { return "Force a node to renew its identity" }
func (n *NodeIdentityRenewCommand) Name() string { return "node identity renew" }
func (n *NodeIdentityRenewCommand) Run(args []string) int {
flags := n.Meta.FlagSet(n.Name(), FlagSetClient)
flags.Usage = func() { n.Ui.Output(n.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
n.Ui.Error("This command takes one argument: <node_id>")
n.Ui.Error(commandErrorText(n))
return 1
}
// Get the HTTP client
client, err := n.Meta.Client()
if err != nil {
n.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
nodeID := args[0]
// Lookup nodeID
if nodeID != "" {
nodeID, err = lookupNodeID(client.Nodes(), nodeID)
if err != nil {
n.Ui.Error(err.Error())
return 1
}
}
req := api.NodeIdentityRenewRequest{
NodeID: nodeID,
}
if _, err := client.Nodes().Identity().Renew(&req, nil); err != nil {
n.Ui.Error(fmt.Sprintf("Error requesting node identity renewal: %s", err))
return 1
}
return 0
}
func (n *NodeIdentityRenewCommand) AutocompleteFlags() complete.Flags {
return n.Meta.AutocompleteFlags(FlagSetClient)
}
func (n *NodeIdentityRenewCommand) AutocompleteArgs() complete.Predictor {
return nodePredictor(n.Client, nil)
}

View File

@@ -132,8 +132,18 @@ func (c *OperatorClientStateCommand) Run(args []string) int {
Tasks: tasks,
}
}
// Get the node identity state, which is useful when debugging to see the
// real and current identity the node is using.
nodeIdentity, err := db.GetNodeIdentity()
if err != nil {
c.Ui.Error(fmt.Sprintf("failed to get node identity state: %v", err))
return 1
}
output := debugOutput{
Allocations: data,
Allocations: data,
NodeIdentity: nodeIdentity,
}
bytes, err := json.Marshal(output)
if err != nil {
@@ -146,7 +156,8 @@ func (c *OperatorClientStateCommand) Run(args []string) int {
}
type debugOutput struct {
Allocations map[string]*clientStateAlloc
Allocations map[string]*clientStateAlloc
NodeIdentity string
}
type clientStateAlloc struct {

View File

@@ -38,10 +38,16 @@ func TestOperatorClientStateCommand(t *testing.T) {
alloc := structs.MockAlloc()
err = db.PutAllocation(alloc)
must.NoError(t, err)
// Write a node identity to the DB, so we can test that the command reads
// this data.
must.NoError(t, db.PutNodeIdentity("mynodeidentity"))
must.NoError(t, db.Close())
// run against an incomplete client state directory
code = cmd.Run([]string{dir})
must.Eq(t, 0, code)
must.StrContains(t, ui.OutputWriter.String(), alloc.ID)
must.StrContains(t, ui.OutputWriter.String(), "NodeIdentity\":\"mynodeidentity")
}