fast-build-update-tool/internal/tools/file_uploader.go (103 lines of code) (raw):
package tools
import (
"context"
"fmt"
"log/slog"
"net"
"os"
"os/exec"
"path/filepath"
"github.com/aws/amazon-gamelift-toolkit/fast-build-update-tool/internal/config"
"github.com/aws/amazon-gamelift-toolkit/fast-build-update-tool/internal/gamelift"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
func runCommand(cmdName string, args ...string) error {
cmd := exec.Command(cmdName, args...)
cmd.Stderr = config.NewErrorLogger(cmdName)
err := cmd.Run()
if err != nil {
return err
}
return nil
}
// FileUploader is used to upload one or more files to a remote instance
type FileUploader struct {
logger *slog.Logger
remoteIpAddress string
privateKeyPath string
remoteUser config.RemoteUser
remoteUploadDirectory config.RemoteUploadDirectory
filesToUpload []string
sshPort int32
commandRunner func(cmdName string, args ...string) error
}
const (
scpCommand = "scp"
)
// NewFileUploader instantiates a new file uploader for the given GameLift instance
func NewFileUploader(logger *slog.Logger, instance *gamelift.Instance, privateKeyPath string, filesToUpload []string, sshPort int32) (*FileUploader, error) {
result := &FileUploader{
logger: logger.With("context", "FileUploader"),
remoteIpAddress: instance.IpAddress,
privateKeyPath: privateKeyPath,
remoteUser: config.RemoteUserForOperatingSystem(instance.OperatingSystem),
remoteUploadDirectory: config.RemoteUploadDirectoryForOperatingSystem(instance.OperatingSystem),
filesToUpload: filesToUpload,
sshPort: sshPort,
commandRunner: runCommand,
}
return result, result.Validate()
}
// Validate that the FileUploader can copy files to the remote instance
func (f *FileUploader) Validate() error {
return verifyExe(scpCommand)
}
// CopyFiles opens an connection to the remote instance, and copies files up to it
func (f *FileUploader) CopyFiles(ctx context.Context, remotePublicKey ssh.PublicKey) error {
tempKnownHostsFile, err := f.generateKnownHostsFile(ctx, remotePublicKey)
if err != nil {
return err
}
defer func() {
if err := os.Remove(tempKnownHostsFile); err != nil {
f.logger.Error("error removing temporary known hosts file", "file", tempKnownHostsFile, "error", err)
}
}()
for _, file := range f.filesToUpload {
if err := f.copyFile(ctx, file, tempKnownHostsFile); err != nil {
return fmt.Errorf("error uploading file %s to server %w", file, err)
}
}
return nil
}
// generateKnownHostsFile generates a temporary known hosts file with the provided public key, so we can safely SCP files to server
func (f *FileUploader) generateKnownHostsFile(ctx context.Context, remotePublicKey ssh.PublicKey) (string, error) {
khFile, err := os.CreateTemp("", "known_hosts")
if err != nil {
return "", err
}
defer func() {
if err = khFile.Close(); err != nil {
f.logger.Warn("error closing known hosts file", "error", err)
}
}()
hostPort := net.JoinHostPort(f.remoteIpAddress, fmt.Sprintf("%d", f.sshPort))
line := knownhosts.Line([]string{hostPort}, remotePublicKey)
_, err = khFile.Write([]byte(line))
if err != nil {
return "", err
}
return khFile.Name(), err
}
// copyFile actually runs the scp command to copy a file ot the server
func (f *FileUploader) copyFile(ctx context.Context, file, knownHostsFile string) error {
f.logger.Debug("copying file to remote instance", "file", file)
err := f.commandRunner(scpCommand,
"-o",
"UserKnownHostsFile="+knownHostsFile,
"-P",
fmt.Sprintf("%d", f.sshPort),
"-i",
f.privateKeyPath,
file,
fmt.Sprintf("%s@%s:%s", f.remoteUser, f.remoteIpAddress, string(f.remoteUploadDirectory)+filepath.Base(file)))
if err != nil {
return fmt.Errorf("error copying file to remote instance: %s %w", f.remoteIpAddress, err)
}
return nil
}