diff --git a/client/state/db_bolt.go b/client/state/db_bolt.go index bef111f6e..c952500b6 100644 --- a/client/state/db_bolt.go +++ b/client/state/db_bolt.go @@ -140,6 +140,12 @@ var ( nodeRegistrationKey = []byte("node_registration") hostVolBucket = []byte("host_volumes_to_create") + + // nodeIdentityBucket and nodeIdentityBucketStateKey are used to persist + // the client identity and its state. Each client will only have a single + // identity, so we use a single key value for the storage. + nodeIdentityBucket = []byte("node_identity") + nodeIdentityBucketStateKey = []byte("node_identity_state") ) // taskBucketName returns the bucket name for the given task name. @@ -1089,6 +1095,42 @@ func (s *BoltStateDB) DeleteDynamicHostVolume(id string) error { }) } +// clientIdentity wraps the signed client identity so we can safely add more +// state in the future without needing a new entry type. +type clientIdentity struct { + SignedIdentity string +} + +func (s *BoltStateDB) PutNodeIdentity(identity string) error { + return s.db.Update(func(tx *boltdd.Tx) error { + b, err := tx.CreateBucketIfNotExists(nodeIdentityBucket) + if err != nil { + return err + } + + identityWrapper := clientIdentity{SignedIdentity: identity} + + return b.Put(nodeIdentityBucketStateKey, &identityWrapper) + }) +} + +func (s *BoltStateDB) GetNodeIdentity() (string, error) { + var identityWrapper clientIdentity + err := s.db.View(func(tx *boltdd.Tx) error { + b := tx.Bucket(nodeIdentityBucket) + if b == nil { + return nil + } + return b.Get(nodeIdentityBucketStateKey, &identityWrapper) + }) + + if boltdd.IsErrNotFound(err) { + return "", nil + } + + return identityWrapper.SignedIdentity, err +} + // init initializes metadata entries in a newly created state database. func (s *BoltStateDB) init() error { return s.db.Update(func(tx *boltdd.Tx) error { diff --git a/client/state/db_error.go b/client/state/db_error.go index 6c99defa2..6edfbbdd9 100644 --- a/client/state/db_error.go +++ b/client/state/db_error.go @@ -172,3 +172,7 @@ func (m *ErrDB) DeleteDynamicHostVolume(_ string) error { func (m *ErrDB) Close() error { return fmt.Errorf("Error!") } + +func (m *ErrDB) PutNodeIdentity(_ string) error { return ErrDBError } + +func (m *ErrDB) GetNodeIdentity() (string, error) { return "", ErrDBError } diff --git a/client/state/db_mem.go b/client/state/db_mem.go index 32abd883e..4fd827852 100644 --- a/client/state/db_mem.go +++ b/client/state/db_mem.go @@ -6,6 +6,7 @@ package state import ( "maps" "sync" + "sync/atomic" "github.com/hashicorp/go-hclog" arstate "github.com/hashicorp/nomad/client/allocrunner/state" @@ -62,6 +63,9 @@ type MemDB struct { dynamicHostVolumes map[string]*cstructs.HostVolumeState + // clientIdentity is the persisted identity of the client. + clientIdentity atomic.Value + logger hclog.Logger mu sync.RWMutex @@ -79,6 +83,7 @@ func NewMemDB(logger hclog.Logger) *MemDB { checks: make(checks.ClientResults), identities: make(map[string][]*structs.SignedWorkloadIdentity), dynamicHostVolumes: make(map[string]*cstructs.HostVolumeState), + clientIdentity: atomic.Value{}, logger: logger, } } @@ -379,6 +384,19 @@ func (m *MemDB) DeleteDynamicHostVolume(s string) error { return nil } +func (m *MemDB) PutNodeIdentity(identity string) error { + m.clientIdentity.Store(identity) + return nil +} + +func (m *MemDB) GetNodeIdentity() (string, error) { + if obj := m.clientIdentity.Load(); obj == nil { + return "", nil + } else { + return obj.(string), nil + } +} + func (m *MemDB) Close() error { m.mu.Lock() defer m.mu.Unlock() diff --git a/client/state/db_noop.go b/client/state/db_noop.go index 09488c181..3c53ae57c 100644 --- a/client/state/db_noop.go +++ b/client/state/db_noop.go @@ -157,6 +157,10 @@ func (n NoopDB) DeleteDynamicHostVolume(_ string) error { return nil } +func (n NoopDB) PutNodeIdentity(_ string) error { return nil } + +func (n NoopDB) GetNodeIdentity() (string, error) { return "", nil } + func (n NoopDB) Close() error { return nil } diff --git a/client/state/db_test.go b/client/state/db_test.go index 3a03cf3a2..23f6fb55d 100644 --- a/client/state/db_test.go +++ b/client/state/db_test.go @@ -493,6 +493,24 @@ func TestStateDB_CheckResult(t *testing.T) { } +func TestStateDB_NodeIdentity(t *testing.T) { + ci.Parallel(t) + + testDB(t, func(t *testing.T, db StateDB) { + identity, err := db.GetNodeIdentity() + must.NoError(t, err) + must.Eq(t, "", identity) + + fakeIdentity := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" + + must.NoError(t, db.PutNodeIdentity(fakeIdentity)) + + identity, err = db.GetNodeIdentity() + must.NoError(t, err) + must.Eq(t, fakeIdentity, identity) + }) +} + // TestStateDB_Upgrade asserts calling Upgrade on new databases always // succeeds. func TestStateDB_Upgrade(t *testing.T) { diff --git a/client/state/interface.go b/client/state/interface.go index 0460a75e2..136466d95 100644 --- a/client/state/interface.go +++ b/client/state/interface.go @@ -141,6 +141,14 @@ type StateDB interface { GetDynamicHostVolumes() ([]*cstructs.HostVolumeState, error) DeleteDynamicHostVolume(string) error + // PutNodeIdentity stores the signed identity JWT for the client. + PutNodeIdentity(identity string) error + + // GetNodeIdentity retrieves the signed identity JWT for the client. If the + // client has not generated an identity, this will return an empty string + // and no error. + GetNodeIdentity() (string, error) + // Close the database. Unsafe for further use after calling regardless // of return value. Close() error diff --git a/command/operator_client_state.go b/command/operator_client_state.go index b761d4bdb..ef5c2d9cb 100644 --- a/command/operator_client_state.go +++ b/command/operator_client_state.go @@ -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 { diff --git a/command/operator_client_state_test.go b/command/operator_client_state_test.go index 2220d33cc..696e5784b 100644 --- a/command/operator_client_state_test.go +++ b/command/operator_client_state_test.go @@ -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") }