executors/virtualbox/virtualbox.go (333 lines of code) (raw):
package virtualbox
import (
"context"
"errors"
"fmt"
"time"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/common/buildlogger"
"gitlab.com/gitlab-org/gitlab-runner/executors"
"gitlab.com/gitlab-org/gitlab-runner/executors/vm"
"gitlab.com/gitlab-org/gitlab-runner/helpers/ssh"
vbox "gitlab.com/gitlab-org/gitlab-runner/helpers/virtualbox"
)
const virtualboxCleanupTimeout = 5 * time.Minute
type executor struct {
vm.Executor
vmName string
sshCommand ssh.Client
sshPort string
provisioned bool
machineVerified bool
}
func (s *executor) verifyMachine(sshPort string) error {
if s.machineVerified {
return nil
}
// Create SSH command
sshCommand := ssh.Client{
SshConfig: *s.Config.SSH,
ConnectRetries: 30,
}
sshCommand.Port = sshPort
sshCommand.Host = "localhost"
s.BuildLogger.Debugln("Connecting to SSH...")
err := sshCommand.Connect()
if err != nil {
return err
}
defer sshCommand.Cleanup()
err = sshCommand.Run(s.Context, ssh.Command{Command: "exit"})
if err != nil {
return err
}
s.machineVerified = true
return nil
}
func (s *executor) restoreFromSnapshot() error {
s.BuildLogger.Debugln("Reverting VM to current snapshot...")
err := vbox.RevertToSnapshot(s.Context, s.vmName)
if err != nil {
return err
}
return nil
}
func (s *executor) determineBaseSnapshot(baseImage string) string {
var err error
baseSnapshot := s.Config.VirtualBox.BaseSnapshot
if baseSnapshot == "" {
baseSnapshot, err = vbox.GetCurrentSnapshot(s.Context, baseImage)
if err != nil {
if s.Config.VirtualBox.DisableSnapshots {
s.BuildLogger.Debugln("No snapshots found for base VM", baseImage)
return ""
}
baseSnapshot = "Base State"
}
}
if baseSnapshot != "" && !vbox.HasSnapshot(s.Context, baseImage, baseSnapshot) {
if s.Config.VirtualBox.DisableSnapshots {
s.BuildLogger.Warningln("Snapshot", baseSnapshot, "not found in base VM", baseImage)
return ""
}
s.BuildLogger.Debugln("Creating snapshot", baseSnapshot, "from current base VM", baseImage, "state...")
err = vbox.CreateSnapshot(s.Context, baseImage, baseSnapshot)
if err != nil {
s.BuildLogger.Warningln("Failed to create snapshot", baseSnapshot, "from base VM", baseImage)
return ""
}
}
return baseSnapshot
}
// virtualbox doesn't support templates
func (s *executor) createVM(baseImage string) (err error) {
_, err = vbox.Status(s.Context, s.vmName)
if err != nil {
_ = vbox.Unregister(s.Context, s.vmName)
}
if !vbox.Exist(s.Context, s.vmName) {
baseSnapshot := s.determineBaseSnapshot(baseImage)
if baseSnapshot == "" {
s.BuildLogger.Debugln("Creating testing VM from VM", baseImage, "...")
} else {
s.BuildLogger.Debugln("Creating testing VM from VM", baseImage, "snapshot", baseSnapshot, "...")
}
err = vbox.CreateOsVM(s.Context, baseImage, s.vmName, baseSnapshot, s.Config.VirtualBox.BaseFolder)
if err != nil {
return err
}
}
s.BuildLogger.Debugln("Identify SSH Port...")
s.sshPort, err = vbox.FindSSHPort(s.Context, s.vmName)
if err != nil {
s.BuildLogger.Debugln("Creating localhost ssh forwarding...")
vmSSHPort := s.Config.SSH.Port
if vmSSHPort == "" {
vmSSHPort = "22"
}
s.sshPort, err = vbox.ConfigureSSH(s.Context, s.vmName, vmSSHPort)
if err != nil {
return err
}
}
s.BuildLogger.Debugln("Using local", s.sshPort, "SSH port to connect to VM...")
s.BuildLogger.Debugln("Bootstraping VM...")
err = s.startVM()
if err != nil {
return err
}
s.BuildLogger.Debugln("Waiting for VM to become responsive...")
time.Sleep(10 * time.Second)
err = s.verifyMachine(s.sshPort)
if err != nil {
return err
}
return nil
}
func (s *executor) Prepare(options common.ExecutorPrepareOptions) error {
err := s.AbstractExecutor.Prepare(options)
if err != nil {
return err
}
err = s.validateConfig()
if err != nil {
return err
}
err = s.printVersion()
if err != nil {
return err
}
var baseName string
baseName, err = s.Executor.GetBaseName(s.Config.VirtualBox.BaseName)
if err != nil {
return err
}
s.vmName = s.getVMName(baseName)
if s.Config.VirtualBox.DisableSnapshots && vbox.Exist(s.Context, s.vmName) {
s.BuildLogger.Debugln("Deleting old VM...")
killAndUnregisterVM(s.Context, s.vmName)
}
s.tryRestoreFromSnapshot()
if !vbox.Exist(s.Context, s.vmName) {
s.BuildLogger.Println("Creating new VM...")
err = s.createVM(baseName)
if err != nil {
return err
}
if !s.Config.VirtualBox.DisableSnapshots {
s.BuildLogger.Println("Creating default snapshot...")
err = vbox.CreateSnapshot(s.Context, s.vmName, "Started")
if err != nil {
return err
}
}
}
err = s.ensureVMStarted()
if err != nil {
return err
}
return s.sshConnect()
}
func (s *executor) printVersion() error {
version, err := vbox.Version(s.Context)
if err != nil {
return err
}
s.BuildLogger.Println("Using VirtualBox version", version, "executor...")
return nil
}
func (s *executor) validateConfig() error {
if s.Config.VirtualBox.BaseName == "" {
return errors.New("missing BaseName setting from VirtualBox configuration")
}
if s.BuildShell.PassFile {
return errors.New("virtualbox doesn't support shells that require script file")
}
if s.Config.SSH == nil {
return errors.New("missing SSH config")
}
if s.Config.VirtualBox == nil {
return errors.New("missing VirtualBox configuration")
}
return s.ValidateAllowedImages(s.Config.VirtualBox.AllowedImages)
}
func (s *executor) getVMName(baseName string) string {
if s.Config.VirtualBox.DisableSnapshots {
return s.Config.VirtualBox.BaseName + "-" + s.Build.ProjectUniqueName()
}
return fmt.Sprintf(
"%s-runner-%s-concurrent-%d",
baseName,
s.Build.Runner.ShortDescription(),
s.Build.RunnerID,
)
}
func (s *executor) startVM() error {
s.BuildLogger.Debugln("Starting VM...")
startType := s.Config.VirtualBox.StartType
if startType == "" {
startType = "headless"
}
err := vbox.Start(s.Context, s.vmName, startType)
if err != nil {
return err
}
return nil
}
func (s *executor) tryRestoreFromSnapshot() {
if !vbox.Exist(s.Context, s.vmName) {
return
}
s.BuildLogger.Println("Restoring VM from snapshot...")
err := s.restoreFromSnapshot()
if err != nil {
s.BuildLogger.Println("Previous VM failed. Deleting, because", err)
killAndUnregisterVM(s.Context, s.vmName)
}
}
func killAndUnregisterVM(ctx context.Context, vmName string) {
_ = vbox.Kill(ctx, vmName)
_ = vbox.Delete(ctx, vmName)
_ = vbox.Unregister(ctx, vmName)
}
func (s *executor) ensureVMStarted() error {
s.BuildLogger.Debugln("Checking VM status...")
status, err := vbox.Status(s.Context, s.vmName)
if err != nil {
return err
}
if !vbox.IsStatusOnlineOrTransient(status) {
err = s.startVM()
if err != nil {
return err
}
}
if status != vbox.Running {
s.BuildLogger.Debugln("Waiting for VM to run...")
err = vbox.WaitForStatus(s.Context, s.vmName, vbox.Running, 60)
if err != nil {
return err
}
}
s.BuildLogger.Debugln("Identify SSH Port...")
sshPort, err := vbox.FindSSHPort(s.Context, s.vmName)
s.sshPort = sshPort
if err != nil {
return err
}
s.BuildLogger.Println("Waiting for VM to become responsive...")
err = s.verifyMachine(s.sshPort)
if err != nil {
return err
}
s.provisioned = true
return nil
}
func (s *executor) sshConnect() error {
s.BuildLogger.Println("Starting SSH command...")
s.sshCommand = ssh.Client{
SshConfig: *s.Config.SSH,
}
s.sshCommand.Port = s.sshPort
s.sshCommand.Host = "localhost"
s.BuildLogger.Debugln("Connecting to SSH server...")
return s.sshCommand.Connect()
}
func (s *executor) Run(cmd common.ExecutorCommand) error {
stdout := s.BuildLogger.Stream(buildlogger.StreamWorkLevel, buildlogger.Stdout)
defer stdout.Close()
stderr := s.BuildLogger.Stream(buildlogger.StreamWorkLevel, buildlogger.Stderr)
defer stderr.Close()
err := s.sshCommand.Run(cmd.Context, ssh.Command{
Command: s.BuildShell.CmdLine,
Stdin: cmd.Script,
Stdout: stdout,
Stderr: stderr,
})
if exitError, ok := err.(*ssh.ExitError); ok {
exitCode := exitError.ExitCode()
err = &common.BuildError{Inner: err, ExitCode: exitCode}
}
return err
}
func (s *executor) Cleanup() {
s.sshCommand.Cleanup()
if s.vmName != "" {
ctx, cancel := context.WithTimeout(context.Background(), virtualboxCleanupTimeout)
defer cancel()
_ = vbox.Kill(ctx, s.vmName)
if s.Config.VirtualBox.DisableSnapshots || !s.provisioned {
_ = vbox.Delete(ctx, s.vmName)
}
}
}
func init() {
options := executors.ExecutorOptions{
DefaultCustomBuildsDirEnabled: false,
DefaultSafeDirectoryCheckout: true,
DefaultBuildsDir: "builds",
DefaultCacheDir: "cache",
SharedBuildsDir: false,
Shell: common.ShellScriptInfo{
Shell: "bash",
Type: common.LoginShell,
RunnerCommand: "gitlab-runner",
},
ShowHostname: true,
}
creator := func() common.Executor {
return &executor{
Executor: vm.Executor{
AbstractExecutor: executors.AbstractExecutor{
ExecutorOptions: options,
},
},
}
}
featuresUpdater := func(features *common.FeaturesInfo) {
features.Variables = true
}
common.RegisterExecutorProvider("virtualbox", executors.DefaultExecutorProvider{
Creator: creator,
FeaturesUpdater: featuresUpdater,
DefaultShellName: options.Shell.Shell,
})
}