fast-build-update-tool/internal/runner/instance_updater.go (76 lines of code) (raw):
package runner
import (
"context"
"fmt"
"log/slog"
"golang.org/x/crypto/ssh"
)
//go:generate moq -skip-ensure -out ./moq_remote_ssh_enabler_test.go . RemoteSSHEnabler
//go:generate moq -skip-ensure -out ./moq_remote_command_runner_test.go . CommandRunner
//go:generate moq -skip-ensure -out ./moq_file_uploader_test.go . FileUploader
//go:generate moq -skip-ensure -out ./moq_instance_updater_test.go . InstanceUpdater
// RemoteSSHEnabler is an abstraction around enabling access to an instance over SSH
type RemoteSSHEnabler interface {
// Enable SSH on the remote instance
Enable(ctx context.Context) (remotePublicKey ssh.PublicKey, err error)
}
// CommandRunner is an abstraction around running commands on a remote instance
type CommandRunner interface {
// Run the command provided on the remote instance
Run(ctx context.Context, remotePublicKey ssh.PublicKey) error
}
// FileUploader is an abstraction around copying files to a remote instance
type FileUploader interface {
// CopyFiles will copy files to the remote instance
CopyFiles(ctx context.Context, remotePublicKey ssh.PublicKey) error
}
// InstanceUpdater is used to update a single instance in a GameLift fleet
type InstanceUpdater interface {
// Update will trigger the update process for a single instance
Update(ctx context.Context) error
}
type instanceUpdater struct {
progressTracker *InstanceProgressWriter
sshEnabler RemoteSSHEnabler
fileUploader FileUploader
commandRunner CommandRunner
logger *slog.Logger
}
func (s *instanceUpdater) Update(ctx context.Context) error {
remotePublicKey, err := s.enableSSH(ctx)
if err != nil {
return s.processError(err)
}
err = s.copyFilesToRemoteInstance(ctx, remotePublicKey)
if err != nil {
return s.processError(err)
}
err = s.runUpdateScript(ctx, remotePublicKey)
if err != nil {
return s.processError(err)
}
s.progressTracker.UpdateState(UpdateStateCount)
return nil
}
func (s *instanceUpdater) processError(err error) error {
s.progressTracker.UpdateFailed(err)
return err
}
// enableSSH will enable SSH on the instance. This must happen first as the other Update steps all depend on it.
func (s *instanceUpdater) enableSSH(ctx context.Context) (ssh.PublicKey, error) {
s.logger.Debug("enabling ssh on remote instance")
s.progressTracker.UpdateState(UpdateStateEnableSSH)
remotePublicKey, err := s.sshEnabler.Enable(ctx)
if err != nil {
return nil, fmt.Errorf("error enabling ssh on remote instance %w", err)
}
s.logger.Debug("done enabling ssh on remote instance")
return remotePublicKey, nil
}
// copyFilesToRemoteInstance will copy the build and any relevant update scripts to the instance
func (s *instanceUpdater) copyFilesToRemoteInstance(ctx context.Context, remotePublicKey ssh.PublicKey) error {
s.logger.Debug("copying files to remote instance")
s.progressTracker.UpdateState(UpdateStateCopyBuild)
err := s.fileUploader.CopyFiles(ctx, remotePublicKey)
if err != nil {
return fmt.Errorf("error copying files to remote instance %w", err)
}
s.logger.Debug("done copying files to remote instance")
return nil
}
// runUpdateScript will actually run a script on the instance to perform the update
func (s *instanceUpdater) runUpdateScript(ctx context.Context, remotePublicKey ssh.PublicKey) error {
s.logger.Debug("running update script")
s.progressTracker.UpdateState(UpdateStateRunUpdateScript)
err := s.commandRunner.Run(ctx, remotePublicKey)
if err != nil {
return fmt.Errorf("error running remote command %w", err)
}
s.logger.Debug("done running update script")
return nil
}