github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/virtualbox/executor_virtualbox.go (about)

     1  package virtualbox
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"time"
     7  
     8  	"gitlab.com/gitlab-org/gitlab-runner/common"
     9  	"gitlab.com/gitlab-org/gitlab-runner/executors"
    10  	"gitlab.com/gitlab-org/gitlab-runner/helpers/ssh"
    11  	vbox "gitlab.com/gitlab-org/gitlab-runner/helpers/virtualbox"
    12  )
    13  
    14  type executor struct {
    15  	executors.AbstractExecutor
    16  	vmName          string
    17  	sshCommand      ssh.Client
    18  	sshPort         string
    19  	provisioned     bool
    20  	machineVerified bool
    21  }
    22  
    23  func (s *executor) verifyMachine(vmName string, sshPort string) error {
    24  	if s.machineVerified {
    25  		return nil
    26  	}
    27  
    28  	// Create SSH command
    29  	sshCommand := ssh.Client{
    30  		Config:         *s.Config.SSH,
    31  		Stdout:         s.Trace,
    32  		Stderr:         s.Trace,
    33  		ConnectRetries: 30,
    34  	}
    35  	sshCommand.Port = sshPort
    36  	sshCommand.Host = "localhost"
    37  
    38  	s.Debugln("Connecting to SSH...")
    39  	err := sshCommand.Connect()
    40  	if err != nil {
    41  		return err
    42  	}
    43  	defer sshCommand.Cleanup()
    44  	err = sshCommand.Run(s.Context, ssh.Command{Command: []string{"exit"}})
    45  	if err != nil {
    46  		return err
    47  	}
    48  	s.machineVerified = true
    49  	return nil
    50  }
    51  
    52  func (s *executor) restoreFromSnapshot() error {
    53  	s.Debugln("Reverting VM to current snapshot...")
    54  	err := vbox.RevertToSnapshot(s.vmName)
    55  	if err != nil {
    56  		return err
    57  	}
    58  
    59  	return nil
    60  }
    61  
    62  func (s *executor) determineBaseSnapshot(baseImage string) string {
    63  	var err error
    64  	baseSnapshot := s.Config.VirtualBox.BaseSnapshot
    65  	if baseSnapshot == "" {
    66  		baseSnapshot, err = vbox.GetCurrentSnapshot(baseImage)
    67  		if err != nil {
    68  			if s.Config.VirtualBox.DisableSnapshots {
    69  				s.Debugln("No snapshots found for base VM", baseImage)
    70  				return ""
    71  			}
    72  
    73  			baseSnapshot = "Base State"
    74  		}
    75  	}
    76  
    77  	if baseSnapshot != "" && !vbox.HasSnapshot(baseImage, baseSnapshot) {
    78  		if s.Config.VirtualBox.DisableSnapshots {
    79  			s.Warningln("Snapshot", baseSnapshot, "not found in base VM", baseImage)
    80  			return ""
    81  		}
    82  
    83  		s.Debugln("Creating snapshot", baseSnapshot, "from current base VM", baseImage, "state...")
    84  		err = vbox.CreateSnapshot(baseImage, baseSnapshot)
    85  		if err != nil {
    86  			s.Warningln("Failed to create snapshot", baseSnapshot, "from base VM", baseImage)
    87  			return ""
    88  		}
    89  	}
    90  
    91  	return baseSnapshot
    92  }
    93  
    94  // virtualbox doesn't support templates
    95  func (s *executor) createVM(vmName string) (err error) {
    96  	baseImage := s.Config.VirtualBox.BaseName
    97  	if baseImage == "" {
    98  		return errors.New("Missing Image setting from VirtualBox configuration")
    99  	}
   100  
   101  	_, err = vbox.Status(vmName)
   102  	if err != nil {
   103  		vbox.Unregister(vmName)
   104  	}
   105  
   106  	if !vbox.Exist(vmName) {
   107  		baseSnapshot := s.determineBaseSnapshot(baseImage)
   108  		if baseSnapshot == "" {
   109  			s.Debugln("Creating testing VM from VM", baseImage, "...")
   110  		} else {
   111  			s.Debugln("Creating testing VM from VM", baseImage, "snapshot", baseSnapshot, "...")
   112  		}
   113  
   114  		err = vbox.CreateOsVM(baseImage, vmName, baseSnapshot)
   115  		if err != nil {
   116  			return err
   117  		}
   118  	}
   119  
   120  	s.Debugln("Identify SSH Port...")
   121  	s.sshPort, err = vbox.FindSSHPort(s.vmName)
   122  	if err != nil {
   123  		s.Debugln("Creating localhost ssh forwarding...")
   124  		vmSSHPort := s.Config.SSH.Port
   125  		if vmSSHPort == "" {
   126  			vmSSHPort = "22"
   127  		}
   128  		s.sshPort, err = vbox.ConfigureSSH(vmName, vmSSHPort)
   129  		if err != nil {
   130  			return err
   131  		}
   132  	}
   133  	s.Debugln("Using local", s.sshPort, "SSH port to connect to VM...")
   134  
   135  	s.Debugln("Bootstraping VM...")
   136  	err = vbox.Start(s.vmName)
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	s.Debugln("Waiting for VM to become responsive...")
   142  	time.Sleep(10)
   143  	err = s.verifyMachine(s.vmName, s.sshPort)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  func (s *executor) Prepare(options common.ExecutorPrepareOptions) error {
   152  	err := s.AbstractExecutor.Prepare(options)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	if s.BuildShell.PassFile {
   158  		return errors.New("virtualbox doesn't support shells that require script file")
   159  	}
   160  
   161  	if s.Config.SSH == nil {
   162  		return errors.New("Missing SSH config")
   163  	}
   164  
   165  	if s.Config.VirtualBox == nil {
   166  		return errors.New("Missing VirtualBox configuration")
   167  	}
   168  
   169  	if s.Config.VirtualBox.BaseName == "" {
   170  		return errors.New("Missing BaseName setting from VirtualBox configuration")
   171  	}
   172  
   173  	version, err := vbox.Version()
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	s.Println("Using VirtualBox version", version, "executor...")
   179  
   180  	if s.Config.VirtualBox.DisableSnapshots {
   181  		s.vmName = s.Config.VirtualBox.BaseName + "-" + s.Build.ProjectUniqueName()
   182  		if vbox.Exist(s.vmName) {
   183  			s.Debugln("Deleting old VM...")
   184  			vbox.Kill(s.vmName)
   185  			vbox.Delete(s.vmName)
   186  			vbox.Unregister(s.vmName)
   187  		}
   188  	} else {
   189  		s.vmName = fmt.Sprintf("%s-runner-%s-concurrent-%d",
   190  			s.Config.VirtualBox.BaseName,
   191  			s.Build.Runner.ShortDescription(),
   192  			s.Build.RunnerID)
   193  	}
   194  
   195  	if vbox.Exist(s.vmName) {
   196  		s.Println("Restoring VM from snapshot...")
   197  		err := s.restoreFromSnapshot()
   198  		if err != nil {
   199  			s.Println("Previous VM failed. Deleting, because", err)
   200  			vbox.Kill(s.vmName)
   201  			vbox.Delete(s.vmName)
   202  			vbox.Unregister(s.vmName)
   203  		}
   204  	}
   205  
   206  	if !vbox.Exist(s.vmName) {
   207  		s.Println("Creating new VM...")
   208  		err := s.createVM(s.vmName)
   209  		if err != nil {
   210  			return err
   211  		}
   212  
   213  		if !s.Config.VirtualBox.DisableSnapshots {
   214  			s.Println("Creating default snapshot...")
   215  			err = vbox.CreateSnapshot(s.vmName, "Started")
   216  			if err != nil {
   217  				return err
   218  			}
   219  		}
   220  	}
   221  
   222  	s.Debugln("Checking VM status...")
   223  	status, err := vbox.Status(s.vmName)
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	if !vbox.IsStatusOnlineOrTransient(status) {
   229  		s.Println("Starting VM...")
   230  		err := vbox.Start(s.vmName)
   231  		if err != nil {
   232  			return err
   233  		}
   234  	}
   235  
   236  	if status != vbox.Running {
   237  		s.Debugln("Waiting for VM to run...")
   238  		err = vbox.WaitForStatus(s.vmName, vbox.Running, 60)
   239  		if err != nil {
   240  			return err
   241  		}
   242  	}
   243  
   244  	s.Debugln("Identify SSH Port...")
   245  	sshPort, err := vbox.FindSSHPort(s.vmName)
   246  	s.sshPort = sshPort
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	s.Println("Waiting VM to become responsive...")
   252  	err = s.verifyMachine(s.vmName, s.sshPort)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	s.provisioned = true
   258  
   259  	s.Println("Starting SSH command...")
   260  	s.sshCommand = ssh.Client{
   261  		Config: *s.Config.SSH,
   262  		Stdout: s.Trace,
   263  		Stderr: s.Trace,
   264  	}
   265  	s.sshCommand.Port = s.sshPort
   266  	s.sshCommand.Host = "localhost"
   267  
   268  	s.Debugln("Connecting to SSH server...")
   269  	err = s.sshCommand.Connect()
   270  	if err != nil {
   271  		return err
   272  	}
   273  	return nil
   274  }
   275  
   276  func (s *executor) Run(cmd common.ExecutorCommand) error {
   277  	err := s.sshCommand.Run(cmd.Context, ssh.Command{
   278  		Environment: s.BuildShell.Environment,
   279  		Command:     s.BuildShell.GetCommandWithArguments(),
   280  		Stdin:       cmd.Script,
   281  	})
   282  	if _, ok := err.(*ssh.ExitError); ok {
   283  		err = &common.BuildError{Inner: err}
   284  	}
   285  	return err
   286  }
   287  
   288  func (s *executor) Cleanup() {
   289  	s.sshCommand.Cleanup()
   290  
   291  	if s.vmName != "" {
   292  		vbox.Kill(s.vmName)
   293  
   294  		if s.Config.VirtualBox.DisableSnapshots || !s.provisioned {
   295  			vbox.Delete(s.vmName)
   296  		}
   297  	}
   298  }
   299  
   300  func init() {
   301  	options := executors.ExecutorOptions{
   302  		DefaultCustomBuildsDirEnabled: false,
   303  		DefaultBuildsDir:              "builds",
   304  		DefaultCacheDir:               "cache",
   305  		SharedBuildsDir:               false,
   306  		Shell: common.ShellScriptInfo{
   307  			Shell:         "bash",
   308  			Type:          common.LoginShell,
   309  			RunnerCommand: "gitlab-runner",
   310  		},
   311  		ShowHostname: true,
   312  	}
   313  
   314  	creator := func() common.Executor {
   315  		return &executor{
   316  			AbstractExecutor: executors.AbstractExecutor{
   317  				ExecutorOptions: options,
   318  			},
   319  		}
   320  	}
   321  
   322  	featuresUpdater := func(features *common.FeaturesInfo) {
   323  		features.Variables = true
   324  	}
   325  
   326  	common.RegisterExecutor("virtualbox", executors.DefaultExecutorProvider{
   327  		Creator:          creator,
   328  		FeaturesUpdater:  featuresUpdater,
   329  		DefaultShellName: options.Shell.Shell,
   330  	})
   331  }