debugd: implement upload of multiple binaries

This commit is contained in:
Malte Poll 2023-01-20 10:11:41 +01:00 committed by Malte Poll
parent e6ac8e2a91
commit 6f56ed69f8
21 changed files with 2040 additions and 661 deletions

View file

@ -9,20 +9,18 @@ package server
import (
"context"
"errors"
"fmt"
"io/fs"
"net"
"strconv"
"sync"
"time"
"github.com/edgelesssys/constellation/v2/debugd/internal/bootstrapper"
"github.com/edgelesssys/constellation/v2/debugd/internal/debugd"
"github.com/edgelesssys/constellation/v2/debugd/internal/debugd/deploy"
"github.com/edgelesssys/constellation/v2/debugd/internal/debugd/info"
"github.com/edgelesssys/constellation/v2/debugd/internal/filetransfer"
pb "github.com/edgelesssys/constellation/v2/debugd/service"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/logger"
"go.uber.org/multierr"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
@ -31,18 +29,18 @@ import (
type debugdServer struct {
log *logger.Logger
serviceManager serviceManager
streamer streamer
transfer fileTransferer
info *info.Map
pb.UnimplementedDebugdServer
}
// New creates a new debugdServer according to the gRPC spec.
func New(log *logger.Logger, serviceManager serviceManager, streamer streamer, infos *info.Map) pb.DebugdServer {
func New(log *logger.Logger, serviceManager serviceManager, transfer fileTransferer, infos *info.Map) pb.DebugdServer {
return &debugdServer{
log: log,
serviceManager: serviceManager,
streamer: streamer,
transfer: transfer,
info: infos,
}
}
@ -55,13 +53,23 @@ func (s *debugdServer) SetInfo(ctx context.Context, req *pb.SetInfoRequest) (*pb
s.log.Infof("Info is empty")
}
if err := s.info.SetProto(req.Info); err != nil {
s.log.With(zap.Error(err)).Errorf("Setting info failed")
return &pb.SetInfoResponse{}, err
setProtoErr := s.info.SetProto(req.Info)
if errors.Is(setProtoErr, info.ErrInfoAlreadySet) {
s.log.Warnf("Setting info failed (already set)")
return &pb.SetInfoResponse{
Status: pb.SetInfoStatus_SET_INFO_ALREADY_SET,
}, nil
}
if setProtoErr != nil {
s.log.With(zap.Error(setProtoErr)).Errorf("Setting info failed")
return nil, setProtoErr
}
s.log.Infof("Info set")
return &pb.SetInfoResponse{}, nil
return &pb.SetInfoResponse{
Status: pb.SetInfoStatus_SET_INFO_SUCCESS,
}, nil
}
// GetInfo returns the info of the debugd instance.
@ -76,46 +84,66 @@ func (s *debugdServer) GetInfo(ctx context.Context, req *pb.GetInfoRequest) (*pb
return &pb.GetInfoResponse{Info: info}, nil
}
// UploadBootstrapper receives a bootstrapper binary in a stream of chunks and writes to a file.
func (s *debugdServer) UploadBootstrapper(stream pb.Debugd_UploadBootstrapperServer) error {
startAction := deploy.ServiceManagerRequest{
Unit: debugd.BootstrapperSystemdUnitName,
Action: deploy.Start,
}
var responseStatus pb.UploadBootstrapperStatus
defer func() {
if err := s.serviceManager.SystemdAction(stream.Context(), startAction); err != nil {
s.log.With(zap.Error(err)).Errorf("Starting uploaded bootstrapper failed")
if responseStatus == pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_SUCCESS {
responseStatus = pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_START_FAILED
}
}
stream.SendAndClose(&pb.UploadBootstrapperResponse{
Status: responseStatus,
// UploadFiles receives a stream of files (each consisting of a header and a stream of chunks) and writes them to the filesystem.
func (s *debugdServer) UploadFiles(stream pb.Debugd_UploadFilesServer) error {
s.log.Infof("Received UploadFiles request")
err := s.transfer.RecvFiles(stream)
switch {
case err == nil:
s.log.Infof("Uploading files succeeded")
case errors.Is(err, filetransfer.ErrReceiveRunning):
s.log.Warnf("Upload already in progress")
return stream.SendAndClose(&pb.UploadFilesResponse{
Status: pb.UploadFilesStatus_UPLOAD_FILES_ALREADY_STARTED,
})
case errors.Is(err, filetransfer.ErrReceiveFinished):
s.log.Warnf("Upload already finished")
return stream.SendAndClose(&pb.UploadFilesResponse{
Status: pb.UploadFilesStatus_UPLOAD_FILES_ALREADY_FINISHED,
})
default:
s.log.With(zap.Error(err)).Errorf("Uploading files failed")
return stream.SendAndClose(&pb.UploadFilesResponse{
Status: pb.UploadFilesStatus_UPLOAD_FILES_UPLOAD_FAILED,
})
}()
s.log.Infof("Starting bootstrapper upload")
if err := s.streamer.WriteStream(debugd.BootstrapperDeployFilename, stream, true); err != nil {
if errors.Is(err, fs.ErrExist) {
// bootstrapper was already uploaded
s.log.Warnf("Bootstrapper already uploaded")
responseStatus = pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_FILE_EXISTS
return nil
}
s.log.With(zap.Error(err)).Errorf("Uploading bootstrapper failed")
responseStatus = pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_UPLOAD_FAILED
return fmt.Errorf("uploading bootstrapper: %w", err)
}
s.log.Infof("Successfully uploaded bootstrapper")
responseStatus = pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_SUCCESS
return nil
files := s.transfer.GetFiles()
var overrideUnitErr error
for _, file := range files {
if file.OverrideServiceUnit == "" {
continue
}
// continue on error to allow other units to be overridden
// TODO: switch to native go multierror once 1.20 is released
// err = s.serviceManager.OverrideServiceUnitExecStart(stream.Context(), file.OverrideServiceUnit, file.TargetPath)
// if err != nil {
// overrideUnitErr = errors.Join(overrideUnitErr, err)
// }
err = s.serviceManager.OverrideServiceUnitExecStart(stream.Context(), file.OverrideServiceUnit, file.TargetPath)
if err != nil {
overrideUnitErr = multierr.Append(overrideUnitErr, err)
}
}
if overrideUnitErr != nil {
s.log.With(zap.Error(overrideUnitErr)).Errorf("Overriding service units failed")
return stream.SendAndClose(&pb.UploadFilesResponse{
Status: pb.UploadFilesStatus_UPLOAD_FILES_START_FAILED,
})
}
return stream.SendAndClose(&pb.UploadFilesResponse{
Status: pb.UploadFilesStatus_UPLOAD_FILES_SUCCESS,
})
}
// DownloadBootstrapper streams the local bootstrapper binary to other instances.
func (s *debugdServer) DownloadBootstrapper(request *pb.DownloadBootstrapperRequest, stream pb.Debugd_DownloadBootstrapperServer) error {
s.log.Infof("Sending bootstrapper to other instance")
return s.streamer.ReadStream(debugd.BootstrapperDeployFilename, stream, debugd.Chunksize, true)
// DownloadFiles streams the previously received files to other instances.
func (s *debugdServer) DownloadFiles(request *pb.DownloadFilesRequest, stream pb.Debugd_DownloadFilesServer) error {
s.log.Infof("Sending files to other instance")
if !s.transfer.CanSend() {
return errors.New("cannot send files at this time")
}
return s.transfer.SendFiles(stream)
}
// UploadSystemServiceUnits receives systemd service units, writes them to a service file and schedules a daemon-reload.
@ -157,9 +185,12 @@ func Start(log *logger.Logger, wg *sync.WaitGroup, serv pb.DebugdServer) {
type serviceManager interface {
SystemdAction(ctx context.Context, request deploy.ServiceManagerRequest) error
WriteSystemdUnitFile(ctx context.Context, unit deploy.SystemdUnit) error
OverrideServiceUnitExecStart(ctx context.Context, unitName string, execStart string) error
}
type streamer interface {
WriteStream(filename string, stream bootstrapper.ReadChunkStream, showProgress bool) error
ReadStream(filename string, stream bootstrapper.WriteChunkStream, chunksize uint, showProgress bool) error
type fileTransferer interface {
RecvFiles(stream filetransfer.RecvFilesStream) error
SendFiles(stream filetransfer.SendFilesStream) error
GetFiles() []filetransfer.FileStat
CanSend() bool
}

View file

@ -9,15 +9,14 @@ package server
import (
"context"
"errors"
"fmt"
"io"
"net"
"strconv"
"testing"
"github.com/edgelesssys/constellation/v2/debugd/internal/bootstrapper"
"github.com/edgelesssys/constellation/v2/debugd/internal/debugd/deploy"
"github.com/edgelesssys/constellation/v2/debugd/internal/debugd/info"
"github.com/edgelesssys/constellation/v2/debugd/internal/filetransfer"
pb "github.com/edgelesssys/constellation/v2/debugd/service"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/grpc/testdialer"
@ -40,21 +39,23 @@ func TestSetInfo(t *testing.T) {
info *info.Map
infoReceived bool
setInfo []*pb.Info
wantErr bool
wantStatus pb.SetInfoStatus
}{
"set info works": {
setInfo: []*pb.Info{{Key: "foo", Value: "bar"}},
info: info.NewMap(),
setInfo: []*pb.Info{{Key: "foo", Value: "bar"}},
info: info.NewMap(),
wantStatus: pb.SetInfoStatus_SET_INFO_SUCCESS,
},
"set empty info works": {
setInfo: []*pb.Info{},
info: info.NewMap(),
setInfo: []*pb.Info{},
info: info.NewMap(),
wantStatus: pb.SetInfoStatus_SET_INFO_SUCCESS,
},
"set fails when info already set": {
info: info.NewMap(),
infoReceived: true,
setInfo: []*pb.Info{{Key: "foo", Value: "bar"}},
wantErr: true,
wantStatus: pb.SetInfoStatus_SET_INFO_ALREADY_SET,
},
}
@ -78,19 +79,16 @@ func TestSetInfo(t *testing.T) {
defer conn.Close()
client := pb.NewDebugdClient(conn)
_, err = client.SetInfo(context.Background(), &pb.SetInfoRequest{Info: tc.setInfo})
setInfoStatus, err := client.SetInfo(context.Background(), &pb.SetInfoRequest{Info: tc.setInfo})
grpcServ.GracefulStop()
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantStatus, setInfoStatus.Status)
for i := range tc.setInfo {
value, ok, err := tc.info.Get(tc.setInfo[i].Key)
assert.NoError(err)
for i := range tc.setInfo {
value, ok, err := tc.info.Get(tc.setInfo[i].Key)
assert.NoError(err)
assert.True(ok)
assert.Equal(tc.setInfo[i].Value, value)
}
assert.True(ok)
assert.Equal(tc.setInfo[i].Value, value)
}
})
}
@ -152,35 +150,36 @@ func TestGetInfo(t *testing.T) {
}
}
func TestUploadBootstrapper(t *testing.T) {
func TestUploadFiles(t *testing.T) {
endpoint := "192.0.2.1:" + strconv.Itoa(constants.DebugdPort)
testCases := map[string]struct {
serviceManager stubServiceManager
streamer fakeStreamer
uploadChunks [][]byte
wantErr bool
wantResponseStatus pb.UploadBootstrapperStatus
wantFile bool
wantChunks [][]byte
files []filetransfer.FileStat
recvFilesErr error
wantResponseStatus pb.UploadFilesStatus
wantOverrideCalls []struct{ UnitName, ExecStart string }
}{
"upload works": {
uploadChunks: [][]byte{[]byte("test")},
wantFile: true,
wantChunks: [][]byte{[]byte("test")},
wantResponseStatus: pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_SUCCESS,
files: []filetransfer.FileStat{
{SourcePath: "source/testA", TargetPath: "target/testA", Mode: 0o644, OverrideServiceUnit: "testA"},
{SourcePath: "source/testB", TargetPath: "target/testB", Mode: 0o644},
},
wantOverrideCalls: []struct{ UnitName, ExecStart string }{
{"testA", "target/testA"},
},
wantResponseStatus: pb.UploadFilesStatus_UPLOAD_FILES_SUCCESS,
},
"recv fails": {
streamer: fakeStreamer{writeStreamErr: errors.New("recv error")},
wantResponseStatus: pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_UPLOAD_FAILED,
wantErr: true,
recvFilesErr: errors.New("recv error"),
wantResponseStatus: pb.UploadFilesStatus_UPLOAD_FILES_UPLOAD_FAILED,
},
"starting bootstrapper fails": {
uploadChunks: [][]byte{[]byte("test")},
serviceManager: stubServiceManager{systemdActionErr: errors.New("starting bootstrapper error")},
wantFile: true,
wantChunks: [][]byte{[]byte("test")},
wantResponseStatus: pb.UploadBootstrapperStatus_UPLOAD_BOOTSTRAPPER_START_FAILED,
"upload in progress": {
recvFilesErr: filetransfer.ErrReceiveRunning,
wantResponseStatus: pb.UploadFilesStatus_UPLOAD_FILES_ALREADY_STARTED,
},
"upload already finished": {
recvFilesErr: filetransfer.ErrReceiveFinished,
wantResponseStatus: pb.UploadFilesStatus_UPLOAD_FILES_ALREADY_FINISHED,
},
}
@ -189,60 +188,49 @@ func TestUploadBootstrapper(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
serviceMgr := &stubServiceManager{}
transfer := &stubTransfer{files: tc.files, recvFilesErr: tc.recvFilesErr}
serv := debugdServer{
log: logger.NewTest(t),
serviceManager: &tc.serviceManager,
streamer: &tc.streamer,
serviceManager: serviceMgr,
transfer: transfer,
}
grpcServ, conn, err := setupServerWithConn(endpoint, &serv)
require.NoError(err)
defer conn.Close()
client := pb.NewDebugdClient(conn)
stream, err := client.UploadBootstrapper(context.Background())
stream, err := client.UploadFiles(context.Background())
require.NoError(err)
require.NoError(fakeWrite(stream, tc.uploadChunks))
resp, err := stream.CloseAndRecv()
grpcServ.GracefulStop()
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantResponseStatus, resp.Status)
if tc.wantFile {
assert.Equal(tc.wantChunks, tc.streamer.writeStreamChunks)
assert.Equal("/run/state/bin/bootstrapper", tc.streamer.writeStreamFilename)
} else {
assert.Empty(tc.streamer.writeStreamChunks)
assert.Empty(tc.streamer.writeStreamFilename)
}
assert.Equal(tc.wantOverrideCalls, serviceMgr.overrideCalls)
})
}
}
func TestDownloadBootstrapper(t *testing.T) {
func TestDownloadFiles(t *testing.T) {
endpoint := "192.0.2.1:" + strconv.Itoa(constants.DebugdPort)
testCases := map[string]struct {
serviceManager stubServiceManager
request *pb.DownloadBootstrapperRequest
streamer fakeStreamer
wantErr bool
wantChunks [][]byte
request *pb.DownloadFilesRequest
canSend bool
wantRecvErr bool
wantSendFileCalls int
}{
"download works": {
request: &pb.DownloadBootstrapperRequest{},
streamer: fakeStreamer{readStreamChunks: [][]byte{[]byte("test")}},
wantErr: false,
wantChunks: [][]byte{[]byte("test")},
request: &pb.DownloadFilesRequest{},
canSend: true,
wantSendFileCalls: 1,
},
"download fails": {
request: &pb.DownloadBootstrapperRequest{},
streamer: fakeStreamer{readStreamErr: errors.New("read bootstrapper fails")},
wantErr: true,
"transfer is not ready for sending": {
request: &pb.DownloadFilesRequest{},
wantRecvErr: true,
},
}
@ -251,28 +239,29 @@ func TestDownloadBootstrapper(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
transfer := &stubTransfer{canSend: tc.canSend}
serv := debugdServer{
log: logger.NewTest(t),
serviceManager: &tc.serviceManager,
streamer: &tc.streamer,
log: logger.NewTest(t),
transfer: transfer,
}
grpcServ, conn, err := setupServerWithConn(endpoint, &serv)
require.NoError(err)
defer conn.Close()
client := pb.NewDebugdClient(conn)
stream, err := client.DownloadBootstrapper(context.Background(), tc.request)
stream, err := client.DownloadFiles(context.Background(), tc.request)
require.NoError(err)
chunks, err := fakeRead(stream)
grpcServ.GracefulStop()
if tc.wantErr {
assert.Error(err)
return
_, recvErr := stream.Recv()
if tc.wantRecvErr {
require.Error(recvErr)
} else {
require.ErrorIs(recvErr, io.EOF)
}
require.NoError(stream.CloseSend())
grpcServ.GracefulStop()
require.NoError(err)
assert.Equal(tc.wantChunks, chunks)
assert.Equal("/run/state/bin/bootstrapper", tc.streamer.readStreamFilename)
assert.Equal(tc.wantSendFileCalls, transfer.sendFilesCount)
})
}
}
@ -334,7 +323,6 @@ func TestUploadSystemServiceUnits(t *testing.T) {
serv := debugdServer{
log: logger.NewTest(t),
serviceManager: &tc.serviceManager,
streamer: &fakeStreamer{},
}
grpcServ, conn, err := setupServerWithConn(endpoint, &serv)
require.NoError(err)
@ -357,10 +345,13 @@ func TestUploadSystemServiceUnits(t *testing.T) {
}
type stubServiceManager struct {
requests []deploy.ServiceManagerRequest
unitFiles []deploy.SystemdUnit
systemdActionErr error
writeSystemdUnitFileErr error
requests []deploy.ServiceManagerRequest
unitFiles []deploy.SystemdUnit
overrideCalls []struct{ UnitName, ExecStart string }
systemdActionErr error
writeSystemdUnitFileErr error
overrideServiceUnitExecStartErr error
}
func (s *stubServiceManager) SystemdAction(ctx context.Context, request deploy.ServiceManagerRequest) error {
@ -373,6 +364,13 @@ func (s *stubServiceManager) WriteSystemdUnitFile(ctx context.Context, unit depl
return s.writeSystemdUnitFileErr
}
func (s *stubServiceManager) OverrideServiceUnitExecStart(ctx context.Context, unitName string, execStart string) error {
s.overrideCalls = append(s.overrideCalls, struct {
UnitName, ExecStart string
}{UnitName: unitName, ExecStart: execStart})
return s.overrideServiceUnitExecStartErr
}
type netDialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
@ -386,37 +384,31 @@ func dial(ctx context.Context, dialer netDialer, target string) (*grpc.ClientCon
)
}
type fakeStreamer struct {
writeStreamChunks [][]byte
writeStreamFilename string
writeStreamErr error
readStreamChunks [][]byte
readStreamFilename string
readStreamErr error
type stubTransfer struct {
recvFilesCount int
sendFilesCount int
files []filetransfer.FileStat
canSend bool
recvFilesErr error
sendFilesErr error
}
func (f *fakeStreamer) WriteStream(filename string, stream bootstrapper.ReadChunkStream, showProgress bool) error {
f.writeStreamFilename = filename
for {
chunk, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
return f.writeStreamErr
}
return fmt.Errorf("reading stream: %w", err)
}
f.writeStreamChunks = append(f.writeStreamChunks, chunk.Content)
}
func (t *stubTransfer) RecvFiles(_ filetransfer.RecvFilesStream) error {
t.recvFilesCount++
return t.recvFilesErr
}
func (f *fakeStreamer) ReadStream(filename string, stream bootstrapper.WriteChunkStream, chunksize uint, showProgress bool) error {
f.readStreamFilename = filename
for _, chunk := range f.readStreamChunks {
if err := stream.Send(&pb.Chunk{Content: chunk}); err != nil {
panic(err)
}
}
return f.readStreamErr
func (t *stubTransfer) SendFiles(_ filetransfer.SendFilesStream) error {
t.sendFilesCount++
return t.sendFilesErr
}
func (t *stubTransfer) GetFiles() []filetransfer.FileStat {
return t.files
}
func (t *stubTransfer) CanSend() bool {
return t.canSend
}
func setupServerWithConn(endpoint string, serv *debugdServer) (*grpc.Server, *grpc.ClientConn, error) {
@ -433,29 +425,3 @@ func setupServerWithConn(endpoint string, serv *debugdServer) (*grpc.Server, *gr
return grpcServ, conn, nil
}
func fakeWrite(stream bootstrapper.WriteChunkStream, chunks [][]byte) error {
for _, chunk := range chunks {
err := stream.Send(&pb.Chunk{
Content: chunk,
})
if err != nil {
return err
}
}
return nil
}
func fakeRead(stream bootstrapper.ReadChunkStream) ([][]byte, error) {
var chunks [][]byte
for {
chunk, err := stream.Recv()
if err != nil {
if err == io.EOF {
return chunks, nil
}
return nil, err
}
chunks = append(chunks, chunk.Content)
}
}