constellation/debugd/internal/cdbg/cmd/deploy.go
Moritz Sanft 005e865a13
cli: use state file on init and upgrade (#2395)
* [wip] use state file in CLI

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

tidy

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* use state file in CLI

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

take clusterConfig from IDFile for compat

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

various fixes

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

wip

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* add GCP-specific values in Helm loader test

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* remove unnecessary pointer

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* write ClusterValues in one step

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* move stub to test file

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* remove mention of id-file

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* move output to `migrateTerraform`

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* unconditional assignments converting from idFile

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* move require block in go modules file

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* fall back to id file on upgrade

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* tidy

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* fix linter check

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* add notice to remove Terraform state check on manual migration

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* add `name` field

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

fix name tests

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* return early if no Terraform diff

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* tidy

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* return infrastructure state even if no diff exists

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* add TODO to remove comment

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* use state-file in miniconstellation

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* cli: remove id-file (#2402)

* remove id-file from `constellation create`

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* add file renaming to handler

* rename id-file after upgrade

* use idFile on `constellation init`

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* remove id-file from `constellation verify`

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* linter fixes

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* remove id-file from `constellation mini`

* remove id-file from `constellation recover`

* linter fixes

* remove id-file from `constellation terminate`

* fix initSecret type

* fix recover argument precedence

* fix terminate test

* generate

* add TODO to remove id-file removal

* Update cli/internal/cmd/init.go

Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>

* fix verify arg parse logic

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* add version test

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* remove id-file from docs

* add file not found log

* use state-file in miniconstellation

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* remove id-file from `constellation iam destroy`

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* remove id-file from `cdbg deploy`

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

---------

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>
Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>

* use state-file in CI

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>

* update orchestration docs

---------

Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>
Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>
2023-10-09 13:04:29 +02:00

293 lines
9.6 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 stateFile clusterStateFile
if err := fileHandler.ReadYAML(constants.StateFilename, &stateFile); err != nil {
return fmt.Errorf("reading cluster state file: %w", err)
}
ips = []string{stateFile.Infrastructure.ClusterEndpoint}
}
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 clusterStateFile struct {
Infrastructure struct {
ClusterEndpoint string `yaml:"clusterEndpoint"`
} `yaml:"infrastructure"`
}