/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package upgradeagent import ( "context" "errors" "fmt" "io/fs" "net" "os" "os/exec" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/installer" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/versions/components" "github.com/edgelesssys/constellation/v2/upgrade-agent/upgradeproto" "golang.org/x/mod/semver" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var errInvalidKubernetesVersion = errors.New("invalid kubernetes version") // Server is the upgrade-agent server. type Server struct { file file.Handler grpcServer serveStopper log *logger.Logger upgradeproto.UnimplementedUpdateServer } // New creates a new upgrade-agent server. func New(log *logger.Logger, fileHandler file.Handler) (*Server, error) { log = log.Named("upgradeServer") server := &Server{ log: log, file: fileHandler, } grpcServer := grpc.NewServer( log.Named("gRPC").GetServerUnaryInterceptor(), ) upgradeproto.RegisterUpdateServer(grpcServer, server) server.grpcServer = grpcServer return server, nil } // Run starts the upgrade-agent server on the given port, using the provided protocol and socket address. func (s *Server) Run(protocol string, sockAddr string) error { grpcServer := grpc.NewServer() upgradeproto.RegisterUpdateServer(grpcServer, s) cleanup := func() error { err := os.RemoveAll(sockAddr) if errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { return err } return nil } if err := cleanup(); err != nil { return fmt.Errorf("failed to clean socket file: %s", err) } lis, err := net.Listen(protocol, sockAddr) if err != nil { return fmt.Errorf("failed to listen: %s", err) } s.log.Infof("Starting") return grpcServer.Serve(lis) } // Stop stops the upgrade-agent server gracefully. func (s *Server) Stop() { s.log.Infof("Stopping") s.grpcServer.GracefulStop() s.log.Infof("Stopped") } // ExecuteUpdate installs & verifies the provided kubeadm, then executes `kubeadm upgrade plan` & `kubeadm upgrade apply {wanted_Kubernetes_Version}` to upgrade to the specified version. func (s *Server) ExecuteUpdate(ctx context.Context, updateRequest *upgradeproto.ExecuteUpdateRequest) (*upgradeproto.ExecuteUpdateResponse, error) { s.log.Infof("Upgrade to Kubernetes version started: %s", updateRequest.WantedKubernetesVersion) installer := installer.NewOSInstaller() err := prepareUpdate(ctx, installer, updateRequest) if errors.Is(err, errInvalidKubernetesVersion) { return nil, status.Errorf(codes.Internal, "unable to verify the Kubernetes version %s: %s", updateRequest.WantedKubernetesVersion, err) } else if err != nil { return nil, status.Errorf(codes.Internal, "unable to install the kubeadm binary: %s", err) } upgradeCmd := exec.CommandContext(ctx, "kubeadm", "upgrade", "plan") if out, err := upgradeCmd.CombinedOutput(); err != nil { return nil, status.Errorf(codes.Internal, "unable to execute kubeadm upgrade plan: %s: %s", err, string(out)) } applyCmd := exec.CommandContext(ctx, "kubeadm", "upgrade", "apply", "--yes", updateRequest.WantedKubernetesVersion) if out, err := applyCmd.CombinedOutput(); err != nil { return nil, status.Errorf(codes.Internal, "unable to execute kubeadm upgrade apply: %s: %s", err, string(out)) } s.log.Infof("Upgrade to Kubernetes version succeeded: %s", updateRequest.WantedKubernetesVersion) return &upgradeproto.ExecuteUpdateResponse{}, nil } // prepareUpdate downloads & installs the specified kubeadm version and verifies the desired Kubernetes version. func prepareUpdate(ctx context.Context, installer osInstaller, updateRequest *upgradeproto.ExecuteUpdateRequest) error { // verify Kubernetes version err := verifyVersion(updateRequest.WantedKubernetesVersion) if err != nil { return err } // download & install the kubeadm binary return installer.Install(ctx, components.Component{ URL: updateRequest.KubeadmUrl, Hash: updateRequest.KubeadmHash, InstallPath: constants.KubeadmPath, Extract: false, }) } // verifyVersion verifies the provided Kubernetes version. func verifyVersion(version string) error { if !semver.IsValid(version) { return errInvalidKubernetesVersion } return nil } type osInstaller interface { // Install downloads, installs and verifies the kubernetes component. Install(ctx context.Context, kubernetesComponent components.Component) error } type serveStopper interface { // Serve starts the server. Serve(lis net.Listener) error // GracefulStop stops the server and blocks until all requests are done. GracefulStop() }