mirror of
https://github.com/siderolabs/talos.git
synced 2026-05-05 04:16:21 +02:00
Add a test that covers all maintenance APIs in general. Add a test for transition from SideroLink. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
243 lines
7.4 KiB
Go
243 lines
7.4 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
// Package lifecycle implements machine.LifecycleService.
|
|
package lifecycle
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
|
"github.com/siderolabs/talos/internal/app/machined/pkg/system/pid"
|
|
"github.com/siderolabs/talos/internal/pkg/install"
|
|
"github.com/siderolabs/talos/pkg/machinery/api/machine"
|
|
blockres "github.com/siderolabs/talos/pkg/machinery/resources/block"
|
|
crires "github.com/siderolabs/talos/pkg/machinery/resources/cri"
|
|
)
|
|
|
|
// Service implements machine.LifecycleService.
|
|
type Service struct {
|
|
machine.UnimplementedLifecycleServiceServer
|
|
|
|
lock sync.Mutex
|
|
runtime runtime.Runtime
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewService creates a new instance of the lifecycle service.
|
|
func NewService(runtime runtime.Runtime, logger *zap.Logger) *Service {
|
|
return &Service{
|
|
lock: sync.Mutex{},
|
|
runtime: runtime,
|
|
logger: logger.With(zap.String("service", "lifecycle")),
|
|
}
|
|
}
|
|
|
|
// Install handles the installation of the machine.
|
|
// It ensures that only one installation or upgrade can occur at a time by using a mutex lock.
|
|
//
|
|
//nolint:gocyclo
|
|
func (s *Service) Install(req *machine.LifecycleServiceInstallRequest, ss grpc.ServerStreamingServer[machine.LifecycleServiceInstallResponse]) error {
|
|
if s.runtime.State().Platform().Mode().IsAgent() {
|
|
return status.Error(codes.Unimplemented, "API is not implemented in agent mode")
|
|
}
|
|
|
|
if err := s.checkSupported(runtime.Upgrade); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := ss.Context()
|
|
|
|
if !s.lock.TryLock() {
|
|
return status.Error(codes.FailedPrecondition, "another installation/upgrade is already in progress")
|
|
}
|
|
defer s.lock.Unlock()
|
|
|
|
if s.runtime.State().Platform().Mode().InContainer() {
|
|
return status.Error(codes.FailedPrecondition, "installation is not supported in container mode")
|
|
}
|
|
|
|
if s.runtime.State().Machine().Installed() {
|
|
return status.Error(codes.AlreadyExists, "machine is already installed")
|
|
}
|
|
|
|
if err := crires.WaitForImageCache(ctx, s.runtime.State().V1Alpha2().Resources()); err != nil {
|
|
return status.Error(codes.Internal, fmt.Sprintf("failed to wait for the image cache: %v", err))
|
|
}
|
|
|
|
installerImage := req.GetSource().GetImageName()
|
|
if installerImage == "" {
|
|
return status.Error(codes.InvalidArgument, "installer image name is required")
|
|
}
|
|
|
|
disk := req.GetDestination().GetDisk()
|
|
if disk == "" {
|
|
return status.Error(codes.InvalidArgument, "destination disk is required")
|
|
}
|
|
|
|
targetDisk, err := filepath.EvalSymlinks(disk)
|
|
if err != nil {
|
|
return status.Error(codes.InvalidArgument, fmt.Sprintf("invalid disk path: %v", err))
|
|
}
|
|
|
|
log.Printf("starting installation: installer_image=%s, disk=%s\n", installerImage, targetDisk)
|
|
|
|
//nolint:dupl
|
|
err = runInstallerContainer(ctx,
|
|
pid.NewStateRecorder(s.runtime.State().V1Alpha2().Resources()).Record,
|
|
&containerRunConfig{
|
|
containerdInst: req.GetContainerd(),
|
|
imageRef: installerImage,
|
|
disk: targetDisk,
|
|
platform: s.runtime.State().Platform().Name(),
|
|
cfgContainer: s.runtime.ConfigContainer(),
|
|
opts: []install.Option{
|
|
install.WithForce(true),
|
|
install.WithZero(false),
|
|
},
|
|
send: func(msg string) error {
|
|
s.logger.Info("installation progress", zap.String("message", msg))
|
|
|
|
return ss.Send(&machine.LifecycleServiceInstallResponse{
|
|
Progress: &machine.LifecycleServiceInstallProgress{
|
|
Response: &machine.LifecycleServiceInstallProgress_Message{
|
|
Message: msg,
|
|
},
|
|
},
|
|
})
|
|
},
|
|
sendExitCode: func(exitCode int32) error {
|
|
if exitCode == 0 {
|
|
log.Printf("installation completed successfully: exit_code=%d\n", exitCode)
|
|
} else {
|
|
log.Printf("installation failed: exit_code=%d\n", exitCode)
|
|
}
|
|
|
|
return ss.Send(&machine.LifecycleServiceInstallResponse{
|
|
Progress: &machine.LifecycleServiceInstallProgress{
|
|
Response: &machine.LifecycleServiceInstallProgress_ExitCode{
|
|
ExitCode: exitCode,
|
|
},
|
|
},
|
|
})
|
|
},
|
|
})
|
|
if err != nil {
|
|
return status.Error(codes.Internal, fmt.Sprintf("installation failed: %v", err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Upgrade handles the upgrade of the machine.
|
|
// It ensures that only one installation or upgrade can occur at a time by using a mutex lock.
|
|
//
|
|
//nolint:gocyclo
|
|
func (s *Service) Upgrade(req *machine.LifecycleServiceUpgradeRequest, ss grpc.ServerStreamingServer[machine.LifecycleServiceUpgradeResponse]) error {
|
|
if s.runtime.State().Platform().Mode().IsAgent() {
|
|
return status.Error(codes.Unimplemented, "API is not implemented in agent mode")
|
|
}
|
|
|
|
if err := s.checkSupported(runtime.Upgrade); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := ss.Context()
|
|
|
|
if !s.lock.TryLock() {
|
|
return status.Error(codes.FailedPrecondition, "another installation/upgrade is already in progress")
|
|
}
|
|
defer s.lock.Unlock()
|
|
|
|
if s.runtime.State().Platform().Mode().InContainer() {
|
|
return status.Error(codes.FailedPrecondition, "upgrade is not supported in container mode")
|
|
}
|
|
|
|
if !s.runtime.State().Machine().Installed() {
|
|
return status.Error(codes.FailedPrecondition, "machine is not installed")
|
|
}
|
|
|
|
installerImage := req.GetSource().GetImageName()
|
|
if installerImage == "" {
|
|
return status.Error(codes.InvalidArgument, "installer image name is required")
|
|
}
|
|
|
|
systemDisk, err := blockres.GetSystemDisk(ctx, s.runtime.State().V1Alpha2().Resources())
|
|
if err != nil {
|
|
return status.Error(codes.Internal, fmt.Sprintf("failed to get system disk: %v", err))
|
|
}
|
|
|
|
if systemDisk == nil {
|
|
return status.Error(codes.Internal, "system disk not found")
|
|
}
|
|
|
|
devname := systemDisk.DevPath
|
|
|
|
log.Printf("starting upgrade: installer_image=%s, disk=%s\n", installerImage, devname)
|
|
|
|
//nolint:dupl
|
|
err = runInstallerContainer(ctx,
|
|
pid.NewStateRecorder(s.runtime.State().V1Alpha2().Resources()).Record,
|
|
&containerRunConfig{
|
|
containerdInst: req.GetContainerd(),
|
|
imageRef: installerImage,
|
|
disk: devname,
|
|
platform: s.runtime.State().Platform().Name(),
|
|
cfgContainer: s.runtime.ConfigContainer(),
|
|
opts: []install.Option{
|
|
install.WithUpgrade(true),
|
|
install.WithForce(false),
|
|
},
|
|
send: func(msg string) error {
|
|
s.logger.Info("upgrade progress", zap.String("message", msg))
|
|
|
|
return ss.Send(&machine.LifecycleServiceUpgradeResponse{
|
|
Progress: &machine.LifecycleServiceInstallProgress{
|
|
Response: &machine.LifecycleServiceInstallProgress_Message{
|
|
Message: msg,
|
|
},
|
|
},
|
|
})
|
|
},
|
|
sendExitCode: func(exitCode int32) error {
|
|
if exitCode == 0 {
|
|
log.Printf("upgrade completed successfully: exit_code=%d\n", exitCode)
|
|
} else {
|
|
log.Printf("upgrade failed: exit_code=%d\n", exitCode)
|
|
}
|
|
|
|
return ss.Send(&machine.LifecycleServiceUpgradeResponse{
|
|
Progress: &machine.LifecycleServiceInstallProgress{
|
|
Response: &machine.LifecycleServiceInstallProgress_ExitCode{
|
|
ExitCode: exitCode,
|
|
},
|
|
},
|
|
})
|
|
},
|
|
})
|
|
if err != nil {
|
|
return status.Error(codes.Internal, fmt.Sprintf("upgrade failed: %v", err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) checkSupported(feature runtime.ModeCapability) error {
|
|
mode := s.runtime.State().Platform().Mode()
|
|
|
|
if !mode.Supports(feature) {
|
|
return status.Errorf(codes.FailedPrecondition, "method is not supported in %s mode", mode.String())
|
|
}
|
|
|
|
return nil
|
|
}
|