Persist Node State to disk after node activation

Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
Malte Poll 2022-04-13 09:18:32 +02:00 committed by Malte Poll
parent 0501d07f4a
commit 55a1aa783f
10 changed files with 109 additions and 4 deletions

View File

@ -230,6 +230,21 @@ func (c *Core) Initialize() (nodeActivated bool, err error) {
return nodeActivated, nil return nodeActivated, nil
} }
// PersistNodeState persists node state to disk.
func (c *Core) PersistNodeState(role role.Role, ownerID []byte, clusterID []byte) error {
vpnPrivKey, err := c.vpn.GetPrivateKey()
if err != nil {
return fmt.Errorf("failed to retrieve VPN private key: %w", err)
}
nodeState := nodestate.NodeState{
Role: role,
VPNPrivKey: vpnPrivKey,
OwnerID: ownerID,
ClusterID: clusterID,
}
return nodeState.ToFile(c.fileHandler)
}
// SetUpKMS sets the Coordinators key management service and key encryption key ID. // SetUpKMS sets the Coordinators key management service and key encryption key ID.
// Creates a new key encryption key in the KMS, if requested. // Creates a new key encryption key in the KMS, if requested.
// Otherwise the KEK is assumed to already exist in the KMS. // Otherwise the KEK is assumed to already exist in the KMS.

View File

@ -245,6 +245,64 @@ func TestInitialize(t *testing.T) {
} }
} }
func TestPersistNodeState(t *testing.T) {
testCases := map[string]struct {
vpn VPN
touchStateFile bool
errExpected bool
}{
"persisting works": {
vpn: &stubVPN{
privateKey: []byte("private-key"),
},
},
"retrieving VPN key fails": {
vpn: &stubVPN{
getPrivateKeyErr: errors.New("error"),
},
errExpected: true,
},
"writing node state over existing file fails": {
vpn: &stubVPN{
privateKey: []byte("private-key"),
},
touchStateFile: true,
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
if tc.touchStateFile {
file, err := fs.Create("/run/state/constellation/node_state.json")
require.NoError(err)
require.NoError(file.Close())
}
core, err := NewCore(tc.vpn, nil, nil, nil, nil, nil, zaptest.NewLogger(t), nil, nil, fileHandler)
require.NoError(err)
err = core.PersistNodeState(role.Coordinator, []byte("owner-id"), []byte("cluster-id"))
if tc.errExpected {
assert.Error(err)
return
}
require.NoError(err)
nodeState, err := nodestate.FromFile(fileHandler)
assert.NoError(err)
assert.Equal(nodestate.NodeState{
Role: role.Coordinator,
VPNPrivKey: []byte("private-key"),
OwnerID: []byte("owner-id"),
ClusterID: []byte("cluster-id"),
}, *nodeState)
})
}
}
type fakeStoreFactory struct { type fakeStoreFactory struct {
store store.Store store store.Store
} }

View File

@ -14,6 +14,8 @@ const nodeStatePath = "/run/state/constellation/node_state.json"
type NodeState struct { type NodeState struct {
Role role.Role Role role.Role
VPNPrivKey []byte VPNPrivKey []byte
OwnerID []byte
ClusterID []byte
} }
// FromFile reads a NodeState from disk. // FromFile reads a NodeState from disk.
@ -27,5 +29,5 @@ func FromFile(fileHandler file.Handler) (*NodeState, error) {
// ToFile writes a NodeState to disk. // ToFile writes a NodeState to disk.
func (nodeState *NodeState) ToFile(fileHandler file.Handler) error { func (nodeState *NodeState) ToFile(fileHandler file.Handler) error {
return fileHandler.WriteJSON(nodeStatePath, nodeState, false, true) return fileHandler.WriteJSON(nodeStatePath, nodeState, file.OptMkdirAll)
} }

View File

@ -18,10 +18,12 @@ func TestFromFile(t *testing.T) {
errExpected bool errExpected bool
}{ }{
"nodestate exists": { "nodestate exists": {
fileContents: `{ "Role": "Coordinator", "VPNPrivKey": "dGVzdA==" }`, fileContents: `{ "Role": "Coordinator", "VPNPrivKey": "dGVzdA==", "OwnerID": "T3duZXJJRA==", "ClusterID": "Q2x1c3RlcklE" }`,
expectedState: &NodeState{ expectedState: &NodeState{
Role: role.Coordinator, Role: role.Coordinator,
VPNPrivKey: []byte("test"), VPNPrivKey: []byte("test"),
OwnerID: []byte("OwnerID"),
ClusterID: []byte("ClusterID"),
}, },
}, },
"nodestate file does not exist": { "nodestate file does not exist": {
@ -62,10 +64,14 @@ func TestToFile(t *testing.T) {
state: &NodeState{ state: &NodeState{
Role: role.Coordinator, Role: role.Coordinator,
VPNPrivKey: []byte("test"), VPNPrivKey: []byte("test"),
OwnerID: []byte("OwnerID"),
ClusterID: []byte("ClusterID"),
}, },
expectedFile: `{ expectedFile: `{
"Role": "Coordinator", "Role": "Coordinator",
"VPNPrivKey": "dGVzdA==" "VPNPrivKey": "dGVzdA==",
"OwnerID": "T3duZXJJRA==",
"ClusterID": "Q2x1c3RlcklE"
}`, }`,
}, },
"file exists already": { "file exists already": {
@ -80,8 +86,8 @@ func TestToFile(t *testing.T) {
require := require.New(t) require := require.New(t)
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
require.NoError(fs.MkdirAll(filepath.Dir(nodeStatePath), 0o755))
if tc.precreateFile { if tc.precreateFile {
require.NoError(fs.MkdirAll(filepath.Dir(nodeStatePath), 0o755))
require.NoError(afero.WriteFile(fs, nodeStatePath, []byte("pre-existing"), 0o644)) require.NoError(afero.WriteFile(fs, nodeStatePath, []byte("pre-existing"), 0o644))
} }
fileHandler := file.NewHandler(fs) fileHandler := file.NewHandler(fs)

View File

@ -111,6 +111,11 @@ func (a *API) ActivateAsCoordinator(in *pubproto.ActivateAsCoordinatorRequest, s
return status.Errorf(codes.Internal, "%v", err) return status.Errorf(codes.Internal, "%v", err)
} }
// persist node state on disk
if err := a.core.PersistNodeState(role.Coordinator, ownerID, clusterID); err != nil {
return status.Errorf(codes.Internal, "%v", err)
}
// This effectively gives code execution, so we do this last. // This effectively gives code execution, so we do this last.
adminVPNIP, err := a.core.AddAdmin(in.AdminVpnPubKey) adminVPNIP, err := a.core.AddAdmin(in.AdminVpnPubKey)
if err != nil { if err != nil {

View File

@ -169,6 +169,7 @@ func TestActivateAsCoordinator(t *testing.T) {
assert.Equal(tc.expectedPeers, core.peers) assert.Equal(tc.expectedPeers, core.peers)
assert.Equal(autoscalingNodeGroups, core.autoscalingNodeGroups) assert.Equal(autoscalingNodeGroups, core.autoscalingNodeGroups)
assert.Equal(keyEncryptionKeyID, core.kekID) assert.Equal(keyEncryptionKeyID, core.kekID)
assert.Equal([]role.Role{role.Coordinator}, core.persistNodeStateRoles)
}) })
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/edgelesssys/constellation/coordinator/peer" "github.com/edgelesssys/constellation/coordinator/peer"
"github.com/edgelesssys/constellation/coordinator/role"
"github.com/edgelesssys/constellation/coordinator/state" "github.com/edgelesssys/constellation/coordinator/state"
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
) )
@ -17,6 +18,7 @@ type Core interface {
GetNextNodeIP() (string, error) GetNextNodeIP() (string, error)
SwitchToPersistentStore() error SwitchToPersistentStore() error
GetIDs(masterSecret []byte) (ownerID []byte, clusterID []byte, err error) GetIDs(masterSecret []byte) (ownerID []byte, clusterID []byte, err error)
PersistNodeState(role role.Role, ownerID []byte, clusterID []byte) error
SetUpKMS(ctx context.Context, storageURI, kmsURI, kekID string, useExisting bool) error SetUpKMS(ctx context.Context, storageURI, kmsURI, kekID string, useExisting bool) error
GetDataKey(ctx context.Context, keyID string, length int) ([]byte, error) GetDataKey(ctx context.Context, keyID string, length int) ([]byte, error)

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/edgelesssys/constellation/coordinator/peer" "github.com/edgelesssys/constellation/coordinator/peer"
"github.com/edgelesssys/constellation/coordinator/role"
"github.com/edgelesssys/constellation/coordinator/state" "github.com/edgelesssys/constellation/coordinator/state"
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
) )
@ -26,6 +27,8 @@ type fakeCore struct {
autoscalingNodeGroups []string autoscalingNodeGroups []string
joinArgs []kubeadm.BootstrapTokenDiscovery joinArgs []kubeadm.BootstrapTokenDiscovery
joinClusterErr error joinClusterErr error
persistNodeStateRoles []role.Role
persistNodeStateErr error
kekID string kekID string
dataKey []byte dataKey []byte
getDataKeyErr error getDataKeyErr error
@ -108,6 +111,11 @@ func (c *fakeCore) JoinCluster(args kubeadm.BootstrapTokenDiscovery) error {
return c.joinClusterErr return c.joinClusterErr
} }
func (c *fakeCore) PersistNodeState(role role.Role, ownerID []byte, clusterID []byte) error {
c.persistNodeStateRoles = append(c.persistNodeStateRoles, role)
return c.persistNodeStateErr
}
func (c *fakeCore) SetUpKMS(ctx context.Context, storageURI, kmsURI, kekID string, useExisting bool) error { func (c *fakeCore) SetUpKMS(ctx context.Context, storageURI, kmsURI, kekID string, useExisting bool) error {
c.kekID = kekID c.kekID = kekID
return nil return nil

View File

@ -7,6 +7,7 @@ import (
"github.com/edgelesssys/constellation/coordinator/peer" "github.com/edgelesssys/constellation/coordinator/peer"
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto" "github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
"github.com/edgelesssys/constellation/coordinator/role"
"github.com/edgelesssys/constellation/coordinator/state" "github.com/edgelesssys/constellation/coordinator/state"
"github.com/edgelesssys/constellation/coordinator/vpnapi/vpnproto" "github.com/edgelesssys/constellation/coordinator/vpnapi/vpnproto"
"go.uber.org/zap" "go.uber.org/zap"
@ -58,6 +59,11 @@ func (a *API) ActivateAsNode(ctx context.Context, in *pubproto.ActivateAsNodeReq
return nil, status.Errorf(codes.Internal, "%v", err) return nil, status.Errorf(codes.Internal, "%v", err)
} }
// persist node state on disk
if err := a.core.PersistNodeState(role.Node, in.OwnerId, in.ClusterId); err != nil {
return nil, status.Errorf(codes.Internal, "%v", err)
}
// regularly get (peer) updates from Coordinator // regularly get (peer) updates from Coordinator
a.wgClose.Add(1) a.wgClose.Add(1)
go a.updateLoop() go a.updateLoop()

View File

@ -8,6 +8,7 @@ import (
"github.com/edgelesssys/constellation/coordinator/peer" "github.com/edgelesssys/constellation/coordinator/peer"
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto" "github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
"github.com/edgelesssys/constellation/coordinator/role"
"github.com/edgelesssys/constellation/coordinator/state" "github.com/edgelesssys/constellation/coordinator/state"
"github.com/edgelesssys/constellation/coordinator/util/testdialer" "github.com/edgelesssys/constellation/coordinator/util/testdialer"
"github.com/edgelesssys/constellation/coordinator/vpnapi/vpnproto" "github.com/edgelesssys/constellation/coordinator/vpnapi/vpnproto"
@ -121,6 +122,7 @@ func TestActivateAsNode(t *testing.T) {
require.Len(core.updatedPeers, 1) require.Len(core.updatedPeers, 1)
} }
assert.Equal(tc.initialPeers, core.updatedPeers[0]) assert.Equal(tc.initialPeers, core.updatedPeers[0])
assert.Equal([]role.Role{role.Node}, core.persistNodeStateRoles)
}) })
} }
} }