mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-29 09:26:17 -05:00
60bf770e62
* refactor `debugd` file structure * create `hack`-tool to deploy logcollection to non-debug clusters * integrate changes into CI * update fields * update workflow input names * use `working-directory` * add opensearch creds to upgrade workflow * make template func generic * make templating func generic * linebreaks * remove magic defaults * move `os.Exit` to main package * make logging index configurable * make templating generic * remove excess brace * update fields * copy fields * fix flag name * fix linter warnings Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com> * remove unused workflow inputs * remove makefiles * fix command * bazel: fix output paths of container This fixes the output paths of builds within the container by mounting directories to paths that exist on the host. We also explicitly set the output path in a .bazelrc to the user specific path. The rc file is mounted into the container and overrides the host rc. Also adding automatic stop in case start is called and a containers is already running. Sym links like bazel-out and paths bazel outputs should generally work with this change. Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com> * tabs -> spaces --------- Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com> Co-authored-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
293 lines
9.5 KiB
Go
293 lines
9.5 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"path/filepath"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/edgelesssys/constellation/v2/debugd/internal/debugd"
|
|
"github.com/edgelesssys/constellation/v2/debugd/internal/debugd/logcollector"
|
|
"github.com/edgelesssys/constellation/v2/debugd/internal/filetransfer"
|
|
"github.com/edgelesssys/constellation/v2/debugd/internal/filetransfer/streamer"
|
|
pb "github.com/edgelesssys/constellation/v2/debugd/service"
|
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
|
"github.com/edgelesssys/constellation/v2/internal/config"
|
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
|
"github.com/edgelesssys/constellation/v2/internal/grpc/grpclog"
|
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const deployEndpointTimeout = 20 * time.Minute
|
|
|
|
func newDeployCmd() *cobra.Command {
|
|
deployCmd := &cobra.Command{
|
|
Use: "deploy",
|
|
Short: "Deploys a self-compiled bootstrapper binary on the current constellation",
|
|
Long: `Deploys a self-compiled bootstrapper binary on the current constellation.
|
|
Uses config provided by --config and reads constellation config from its default location.
|
|
If required, you can override the IP addresses that are used for a deployment by specifying "--ips" and a list of IP addresses.
|
|
Specifying --bootstrapper will upload the bootstrapper from the specified path.`,
|
|
RunE: runDeploy,
|
|
Example: "cdbg deploy\ncdbg deploy -C /path/to/workspace --bindir $(pwd)\ncdbg deploy -C /path/to/workspace --bootstrapper /path/to/bootstrapper --ips 192.0.2.1,192.0.2.2,192.0.2.3",
|
|
}
|
|
deployCmd.Flags().StringSlice("ips", nil, "override the ips that the bootstrapper will be uploaded to (defaults to ips from constellation config)")
|
|
deployCmd.Flags().String("bindir", "", "override the base path that binaries are read from")
|
|
deployCmd.Flags().String("bootstrapper", "bootstrapper", "override the path to the bootstrapper binary uploaded to instances")
|
|
deployCmd.Flags().String("upgrade-agent", "upgrade-agent", "override the path to the upgrade-agent binary uploaded to instances")
|
|
deployCmd.Flags().StringToString("info", nil, "additional info to be passed to the debugd, in the form --info key1=value1,key2=value2")
|
|
deployCmd.Flags().Int("verbosity", 0, logger.CmdLineVerbosityDescription)
|
|
return deployCmd
|
|
}
|
|
|
|
func runDeploy(cmd *cobra.Command, _ []string) error {
|
|
verbosity, err := cmd.Flags().GetInt("verbosity")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log := logger.New(logger.PlainLog, logger.VerbosityFromInt(verbosity))
|
|
force, err := cmd.Flags().GetBool("force")
|
|
if err != nil {
|
|
return fmt.Errorf("getting force flag: %w", err)
|
|
}
|
|
|
|
fs := afero.NewOsFs()
|
|
fileHandler := file.NewHandler(fs)
|
|
streamer := streamer.New(fs)
|
|
transfer := filetransfer.New(log, streamer, filetransfer.ShowProgress)
|
|
constellationConfig, err := config.New(fileHandler, constants.ConfigFilename, attestationconfigapi.NewFetcher(), force)
|
|
var configValidationErr *config.ValidationError
|
|
if errors.As(err, &configValidationErr) {
|
|
cmd.PrintErrln(configValidationErr.LongMessage())
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return deploy(cmd, fileHandler, constellationConfig, transfer, log)
|
|
}
|
|
|
|
func deploy(cmd *cobra.Command, fileHandler file.Handler, constellationConfig *config.Config,
|
|
transfer fileTransferer,
|
|
log *logger.Logger,
|
|
) error {
|
|
binDir, err := cmd.Flags().GetString("bindir")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bootstrapperPath, err := cmd.Flags().GetString("bootstrapper")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
upgradeAgentPath, err := cmd.Flags().GetString("upgrade-agent")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if constellationConfig.IsReleaseImage() {
|
|
log.Infof("WARNING: Constellation image does not look like a debug image. Are you using a debug image?")
|
|
}
|
|
|
|
if !constellationConfig.IsDebugCluster() {
|
|
log.Infof("WARNING: The Constellation config has debugCluster set to false.")
|
|
log.Infof("cdbg will likely not work unless you manually adjust the firewall / load balancing rules.")
|
|
log.Infof("If you create the cluster with a debug image, you should also set debugCluster to true.")
|
|
}
|
|
|
|
ips, err := cmd.Flags().GetStringSlice("ips")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(ips) == 0 {
|
|
var idFile clusterIDsFile
|
|
if err := fileHandler.ReadJSON(constants.ClusterIDsFilename, &idFile); err != nil {
|
|
return fmt.Errorf("reading cluster IDs file: %w", err)
|
|
}
|
|
ips = []string{idFile.IP}
|
|
}
|
|
|
|
info, err := cmd.Flags().GetStringToString("info")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := logcollector.FieldsFromMap(info).Check(); err != nil {
|
|
return err
|
|
}
|
|
|
|
files := []filetransfer.FileStat{
|
|
{
|
|
SourcePath: prependBinDir(binDir, bootstrapperPath),
|
|
TargetPath: debugd.BootstrapperDeployFilename,
|
|
Mode: debugd.BinaryAccessMode,
|
|
OverrideServiceUnit: "constellation-bootstrapper",
|
|
},
|
|
{
|
|
SourcePath: prependBinDir(binDir, upgradeAgentPath),
|
|
TargetPath: debugd.UpgradeAgentDeployFilename,
|
|
Mode: debugd.BinaryAccessMode,
|
|
OverrideServiceUnit: "constellation-upgrade-agent",
|
|
},
|
|
}
|
|
|
|
for _, ip := range ips {
|
|
input := deployOnEndpointInput{
|
|
debugdEndpoint: ip,
|
|
infos: info,
|
|
files: files,
|
|
transfer: transfer,
|
|
log: log,
|
|
}
|
|
if err := deployOnEndpoint(cmd.Context(), input); err != nil {
|
|
return fmt.Errorf("deploying endpoint on %q: %w", ip, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func prependBinDir(bindDir, binary string) string {
|
|
if len(bindDir) == 0 || filepath.IsAbs(binary) {
|
|
return binary
|
|
}
|
|
return filepath.Join(bindDir, binary)
|
|
}
|
|
|
|
type deployOnEndpointInput struct {
|
|
debugdEndpoint string
|
|
files []filetransfer.FileStat
|
|
infos map[string]string
|
|
transfer fileTransferer
|
|
log *logger.Logger
|
|
}
|
|
|
|
// deployOnEndpoint deploys a custom built bootstrapper binary to a debugd endpoint.
|
|
func deployOnEndpoint(ctx context.Context, in deployOnEndpointInput) error {
|
|
ctx, cancel := context.WithTimeout(ctx, deployEndpointTimeout)
|
|
defer cancel()
|
|
in.log.Infof("Deploying on %v", in.debugdEndpoint)
|
|
|
|
client, closeAndWaitFn, err := newDebugdClient(ctx, in.debugdEndpoint, in.log)
|
|
if err != nil {
|
|
return fmt.Errorf("creating debugd client: %w", err)
|
|
}
|
|
|
|
defer closeAndWaitFn()
|
|
|
|
if err := setInfo(ctx, in.log, client, in.infos); err != nil {
|
|
return fmt.Errorf("sending info: %w", err)
|
|
}
|
|
|
|
if err := uploadFiles(ctx, client, in); err != nil {
|
|
return fmt.Errorf("uploading bootstrapper: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type closeAndWait func()
|
|
|
|
// newDebugdClient creates a new gRPC client for the debugd service and logs the connection state changes.
|
|
func newDebugdClient(ctx context.Context, ip string, log *logger.Logger) (pb.DebugdClient, closeAndWait, error) {
|
|
conn, err := grpc.DialContext(
|
|
ctx,
|
|
net.JoinHostPort(ip, strconv.Itoa(constants.DebugdPort)),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
log.GetClientUnaryInterceptor(),
|
|
log.GetClientStreamInterceptor(),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("connecting to other instance via gRPC: %w", err)
|
|
}
|
|
var wg sync.WaitGroup
|
|
grpclog.LogStateChangesUntilReady(ctx, conn, log, &wg, func() {})
|
|
closeAndWait := func() {
|
|
conn.Close()
|
|
wg.Wait()
|
|
}
|
|
return pb.NewDebugdClient(conn), closeAndWait, nil
|
|
}
|
|
|
|
func setInfo(ctx context.Context, log *logger.Logger, client pb.DebugdClient, infos map[string]string) error {
|
|
log.Infof("Setting info with length %d", len(infos))
|
|
|
|
var infosPb []*pb.Info
|
|
for key, value := range infos {
|
|
infosPb = append(infosPb, &pb.Info{Key: key, Value: value})
|
|
}
|
|
|
|
req := &pb.SetInfoRequest{Info: infosPb}
|
|
|
|
status, err := client.SetInfo(ctx, req, grpc.WaitForReady(true))
|
|
if err != nil {
|
|
return fmt.Errorf("setting info: %w", err)
|
|
}
|
|
|
|
switch status.Status {
|
|
case pb.SetInfoStatus_SET_INFO_SUCCESS:
|
|
log.Infof("Info set")
|
|
case pb.SetInfoStatus_SET_INFO_ALREADY_SET:
|
|
log.Infof("Info already set")
|
|
default:
|
|
log.Warnf("Unknown status %v", status.Status)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func uploadFiles(ctx context.Context, client pb.DebugdClient, in deployOnEndpointInput) error {
|
|
in.log.Infof("Uploading files")
|
|
|
|
stream, err := client.UploadFiles(ctx, grpc.WaitForReady(true))
|
|
if err != nil {
|
|
return fmt.Errorf("starting bootstrapper upload to instance %v: %w", in.debugdEndpoint, err)
|
|
}
|
|
|
|
in.transfer.SetFiles(in.files)
|
|
if err := in.transfer.SendFiles(stream); err != nil {
|
|
return fmt.Errorf("sending files to %v: %w", in.debugdEndpoint, err)
|
|
}
|
|
|
|
uploadResponse, closeErr := stream.CloseAndRecv()
|
|
if closeErr != nil {
|
|
return fmt.Errorf("closing upload stream after uploading files to %v: %w", in.debugdEndpoint, closeErr)
|
|
}
|
|
switch uploadResponse.Status {
|
|
case pb.UploadFilesStatus_UPLOAD_FILES_SUCCESS:
|
|
in.log.Infof("Upload successful")
|
|
case pb.UploadFilesStatus_UPLOAD_FILES_ALREADY_FINISHED:
|
|
in.log.Infof("Files already uploaded")
|
|
case pb.UploadFilesStatus_UPLOAD_FILES_UPLOAD_FAILED:
|
|
return fmt.Errorf("uploading files to %v failed: %v", in.debugdEndpoint, uploadResponse)
|
|
case pb.UploadFilesStatus_UPLOAD_FILES_ALREADY_STARTED:
|
|
return fmt.Errorf("upload already started on %v", in.debugdEndpoint)
|
|
default:
|
|
return fmt.Errorf("unknown upload status %v", uploadResponse.Status)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type fileTransferer interface {
|
|
SendFiles(stream filetransfer.SendFilesStream) error
|
|
SetFiles(files []filetransfer.FileStat)
|
|
}
|
|
|
|
type clusterIDsFile struct {
|
|
ClusterID string
|
|
OwnerID string
|
|
IP string
|
|
}
|