fast-build-update-tool/internal/runner/fleet_updater.go (175 lines of code) (raw):

package runner import ( "context" "errors" "fmt" "log/slog" "github.com/aws/amazon-gamelift-toolkit/fast-build-update-tool/internal/config" "github.com/aws/amazon-gamelift-toolkit/fast-build-update-tool/internal/gamelift" "github.com/aws/amazon-gamelift-toolkit/fast-build-update-tool/internal/tools" "golang.org/x/crypto/ssh" ) var UpdateFailedError error = errors.New("failed to update one or more instances") // FleetUpdater coordinates applying updates to instances in a specific GameLift fleet type FleetUpdater struct { args config.CLIArgs logger *slog.Logger gameLiftClient GameLiftClient updateScriptGenerator *tools.InstanceUpdateScriptGenerator sshConfigManager *tools.SSHConfigManager zipValidator *tools.ZipValidator instanceUpdaterFactory InstanceUpdaterFactory reportWriter *FleetUpdateReportWriter } // NewFleetUpdater will build a new FleetUpdater using command line arguments func NewFleetUpdater(ctx context.Context, logger *config.ApplicationLogger, args config.CLIArgs) (*FleetUpdater, error) { slogger := logger.Logger.With("fleetId", args.FleetId) gameLift, err := gamelift.NewGameLiftClient(ctx, logger.AwsLogger) if err != nil { return nil, err } return &FleetUpdater{ args: args, gameLiftClient: gameLift, logger: slogger, updateScriptGenerator: tools.NewInstanceUpdateScriptGenerator(args.GetUpdateOperation(), args.BuildZipPath, args.LockName), sshConfigManager: tools.NewSSHConfigManager(slogger, args.PrivateKeyPath, args.SSHPort), zipValidator: tools.NewZipValidator(args.BuildZipPath), instanceUpdaterFactory: NewInstanceUpdaterFactory(ctx, slogger, gameLift, args), reportWriter: NewFleetUpdateReportWriter(args.FleetId, args.Verbose), }, nil } // UpdateInstances will perform any actions necessary to update instances in a GameLift fleet func (f *FleetUpdater) UpdateInstances(ctx context.Context) (*FleetUpdateResults, error) { f.logger.Info("starting fleet update process") f.reportWriter.Preparing() fleet, err := f.lookupFleet(ctx) if err != nil { return nil, err } err = f.validateZipFile(ctx, fleet) if err != nil { return nil, err } sshPort, err := f.ensureSSHPortIsSet(ctx, fleet.OperatingSystem) if err != nil { return nil, err } err = f.ensureSSHPortIsOpenForFleet(ctx, sshPort) if err != nil { return nil, err } updateScript, err := f.generateUpdateScript(ctx, fleet) if err != nil { return nil, err } sshKey, err := f.loadSSHKey(ctx) if err != nil { return nil, err } instances, err := f.getInstances(ctx) if err != nil { return nil, err } return f.updateInstances(ctx, instances, sshKey, sshPort, fleet.OperatingSystem, updateScript) } // lookupFleet will verify the fleet exists, and fetch any relevant data we need to perform an update func (f *FleetUpdater) lookupFleet(ctx context.Context) (*gamelift.Fleet, error) { fleet, err := f.gameLiftClient.GetFleet(ctx, f.args.FleetId) if err != nil { return fleet, fmt.Errorf("error looking up fleet: %w", err) } f.logger.Debug("looking up fleet attributes", "os", fleet.OperatingSystem, "executables", fleet.ExecutablePaths) return fleet, nil } // validateZipFile will validate that the zip file provided by the user is valid for the given fleet func (f *FleetUpdater) validateZipFile(ctx context.Context, fleet *gamelift.Fleet) error { // If the user is restarting server processes, we don't have a zip file to validate if f.args.RestartProcess { f.logger.Debug("running as a restart process update, skipping zip file validation") return nil } err := f.zipValidator.ValidateZip(ctx, fleet) if err != nil { return fmt.Errorf("error validating zip file: %w", err) } f.logger.Debug("done validating zip file") return nil } // ensureSSHPortIsSet will make sure we have a valid SSH port set for the operating system running in this fleet func (f *FleetUpdater) ensureSSHPortIsSet(ctx context.Context, os config.OperatingSystem) (int32, error) { port, err := f.sshConfigManager.DeterminePort(os) if err != nil { return port, fmt.Errorf("error determining ssh port %w", err) } f.logger.Debug("done determining SSH port", "port", port) return port, nil } // ensureSSHPortIsOpenForFleet will update GameLift configuration to verify the ssh port is open for the IP range provided by the user func (f *FleetUpdater) ensureSSHPortIsOpenForFleet(ctx context.Context, sshPort int32) error { err := f.gameLiftClient.OpenPortForFleet(ctx, f.args.FleetId, sshPort, f.args.IpRange) if err != nil { return fmt.Errorf("error opening port for fleet %w", err) } f.logger.Debug("done ensuring SSH port is open for fleet") return nil } // loadSSHKey will load the SSH key provided by the user func (f *FleetUpdater) loadSSHKey(ctx context.Context) (ssh.Signer, error) { signer, err := f.sshConfigManager.LoadKey(ctx) if err != nil { return signer, fmt.Errorf("error loading private ssh key %w", err) } f.logger.Debug("done loading ssh key") return signer, nil } // generateUpdateScript will generate the script we use to update individual instances in the fleet. // This script will be uploaded and run on each individual instance later in this process. func (f *FleetUpdater) generateUpdateScript(ctx context.Context, fleet *gamelift.Fleet) (string, error) { scriptPath, err := f.updateScriptGenerator.GenerateScript(ctx, fleet.OperatingSystem, fleet.ExecutablePaths) if err != nil { return scriptPath, fmt.Errorf("error generating update script: %w", err) } f.logger.Debug("done generating update script", "scriptPath", scriptPath) return scriptPath, nil } // getInstances will load any relevant instances for this update operation func (f *FleetUpdater) getInstances(ctx context.Context) ([]*gamelift.Instance, error) { instances, err := f.gameLiftClient.GetInstances(ctx, f.args.FleetId, f.args.InstanceIds) if err != nil { return instances, fmt.Errorf("error fetching instances for fleet: %w", err) } f.logger.Debug("done loading instances in GameLift fleet", "instanceCount", len(instances)) return instances, nil } // updateInstances will actually run through the process of updating each instance in the fleet func (f *FleetUpdater) updateInstances(ctx context.Context, instances []*gamelift.Instance, sshKey ssh.Signer, sshPort int32, os config.OperatingSystem, updateScript string) (*FleetUpdateResults, error) { f.logger.Debug("updating instances in GameLift fleet") f.reportWriter.StartUpdatingInstances(len(instances)) results := &FleetUpdateResults{ InstancesFound: len(instances), InstancesFailedUpdate: make([]string, 0, len(instances)), } for _, instance := range instances { err := f.updateInstance(ctx, sshKey, sshPort, updateScript, instance) if err != nil { // If we fail to update an instance, log the error and continue. We may still be able to update other instances in the fleet slog.Error("Error updating remote instance", "error", err, "instanceId", instance.InstanceId) results.InstancesFailedUpdate = append(results.InstancesFailedUpdate, instance.InstanceId) continue } results.InstancesUpdated = results.InstancesUpdated + 1 } // We're done updating instances, write the report out for the user f.reportWriter.ReportResults(results) // If any instances failed to update, ensure that we return an error if len(results.InstancesFailedUpdate) > 0 { return results, UpdateFailedError } return results, nil } // updateInstance update an individual instance in the fleet func (f *FleetUpdater) updateInstance(ctx context.Context, sshKey ssh.Signer, sshPort int32, updateScript string, instance *gamelift.Instance) error { // create a fleet updater instanceUpdater, err := f.instanceUpdaterFactory.Create(ctx, f.args.Verbose, sshKey, updateScript, sshPort, instance) if err != nil { return fmt.Errorf("error setting up instance updater: %w", err) } // update the instance err = instanceUpdater.Update(ctx) if err != nil { return fmt.Errorf("error updating instance: %w", err) } return nil } func (f *FleetUpdater) Cleanup() { f.logger.Debug("cleaning up fleet updater resources") if f.updateScriptGenerator != nil { err := f.updateScriptGenerator.Cleanup() if err != nil { f.logger.Warn("error cleaning up local update script", "err", err) } } f.logger.Debug("done cleaning up fleet updater resources") }