github.com/secure-build/gitlab-runner@v12.5.0+incompatible/commands/register.go (about)

     1  package commands
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"os/signal"
     8  	"runtime"
     9  	"strings"
    10  
    11  	"github.com/imdario/mergo"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  	"github.com/urfave/cli"
    15  
    16  	"gitlab.com/gitlab-org/gitlab-runner/common"
    17  	"gitlab.com/gitlab-org/gitlab-runner/helpers/ssh"
    18  	"gitlab.com/gitlab-org/gitlab-runner/network"
    19  )
    20  
    21  type configTemplate struct {
    22  	*common.Config
    23  
    24  	ConfigFile string `long:"config" env:"TEMPLATE_CONFIG_FILE" description:"Path to the configuration template file"`
    25  }
    26  
    27  func (c *configTemplate) Enabled() bool {
    28  	return c.ConfigFile != ""
    29  }
    30  
    31  func (c *configTemplate) MergeTo(config *common.RunnerConfig) error {
    32  	err := c.loadConfigTemplate()
    33  	if err != nil {
    34  		return errors.Wrap(err, "couldn't load configuration template file")
    35  	}
    36  
    37  	if len(c.Runners) != 1 {
    38  		return errors.New("configuration template must contain exactly one [[runners]] entry")
    39  	}
    40  
    41  	err = mergo.Merge(config, c.Runners[0])
    42  	if err != nil {
    43  		return errors.Wrap(err, "error while merging configuration with configuration template")
    44  	}
    45  
    46  	return nil
    47  }
    48  
    49  func (c *configTemplate) loadConfigTemplate() error {
    50  	config := common.NewConfig()
    51  
    52  	err := config.LoadConfig(c.ConfigFile)
    53  	if err != nil {
    54  		return err
    55  	}
    56  
    57  	c.Config = config
    58  
    59  	return nil
    60  }
    61  
    62  type RegisterCommand struct {
    63  	context    *cli.Context
    64  	network    common.Network
    65  	reader     *bufio.Reader
    66  	registered bool
    67  
    68  	configOptions
    69  
    70  	ConfigTemplate configTemplate `namespace:"template"`
    71  
    72  	TagList           string `long:"tag-list" env:"RUNNER_TAG_LIST" description:"Tag list"`
    73  	NonInteractive    bool   `short:"n" long:"non-interactive" env:"REGISTER_NON_INTERACTIVE" description:"Run registration unattended"`
    74  	LeaveRunner       bool   `long:"leave-runner" env:"REGISTER_LEAVE_RUNNER" description:"Don't remove runner if registration fails"`
    75  	RegistrationToken string `short:"r" long:"registration-token" env:"REGISTRATION_TOKEN" description:"Runner's registration token"`
    76  	RunUntagged       bool   `long:"run-untagged" env:"REGISTER_RUN_UNTAGGED" description:"Register to run untagged builds; defaults to 'true' when 'tag-list' is empty"`
    77  	Locked            bool   `long:"locked" env:"REGISTER_LOCKED" description:"Lock Runner for current project, defaults to 'true'"`
    78  	AccessLevel       string `long:"access-level" env:"REGISTER_ACCESS_LEVEL" description:"Set access_level of the runner to not_protected or ref_protected; defaults to not_protected"`
    79  	MaximumTimeout    int    `long:"maximum-timeout" env:"REGISTER_MAXIMUM_TIMEOUT" description:"What is the maximum timeout (in seconds) that will be set for job when using this Runner"`
    80  	Paused            bool   `long:"paused" env:"REGISTER_PAUSED" description:"Set Runner to be paused, defaults to 'false'"`
    81  
    82  	common.RunnerConfig
    83  }
    84  
    85  type AccessLevel string
    86  
    87  const (
    88  	NotProtected AccessLevel = "not_protected"
    89  	RefProtected AccessLevel = "ref_protected"
    90  )
    91  
    92  const (
    93  	defaultDockerWindowCacheDir = "c:\\cache"
    94  )
    95  
    96  func (s *RegisterCommand) askOnce(prompt string, result *string, allowEmpty bool) bool {
    97  	println(prompt)
    98  	if *result != "" {
    99  		print("["+*result, "]: ")
   100  	}
   101  
   102  	if s.reader == nil {
   103  		s.reader = bufio.NewReader(os.Stdin)
   104  	}
   105  
   106  	data, _, err := s.reader.ReadLine()
   107  	if err != nil {
   108  		panic(err)
   109  	}
   110  	newResult := string(data)
   111  	newResult = strings.TrimSpace(newResult)
   112  
   113  	if newResult != "" {
   114  		*result = newResult
   115  		return true
   116  	}
   117  
   118  	if allowEmpty || *result != "" {
   119  		return true
   120  	}
   121  	return false
   122  }
   123  
   124  func (s *RegisterCommand) ask(key, prompt string, allowEmptyOptional ...bool) string {
   125  	allowEmpty := len(allowEmptyOptional) > 0 && allowEmptyOptional[0]
   126  
   127  	result := s.context.String(key)
   128  	result = strings.TrimSpace(result)
   129  
   130  	if s.NonInteractive || prompt == "" {
   131  		if result == "" && !allowEmpty {
   132  			logrus.Panicln("The", key, "needs to be entered")
   133  		}
   134  		return result
   135  	}
   136  
   137  	for {
   138  		if s.askOnce(prompt, &result, allowEmpty) {
   139  			break
   140  		}
   141  	}
   142  
   143  	return result
   144  }
   145  
   146  func (s *RegisterCommand) askExecutor() {
   147  	for {
   148  		names := common.GetExecutors()
   149  		executors := strings.Join(names, ", ")
   150  		s.Executor = s.ask("executor", "Please enter the executor: "+executors+":", true)
   151  		if common.GetExecutor(s.Executor) != nil {
   152  			return
   153  		}
   154  
   155  		message := "Invalid executor specified"
   156  		if s.NonInteractive {
   157  			logrus.Panicln(message)
   158  		} else {
   159  			logrus.Errorln(message)
   160  		}
   161  	}
   162  }
   163  
   164  func (s *RegisterCommand) askDocker() {
   165  	s.askBasicDocker("ruby:2.6")
   166  
   167  	for _, volume := range s.Docker.Volumes {
   168  		parts := strings.Split(volume, ":")
   169  		if parts[len(parts)-1] == "/cache" {
   170  			return
   171  		}
   172  	}
   173  	s.Docker.Volumes = append(s.Docker.Volumes, "/cache")
   174  }
   175  
   176  func (s *RegisterCommand) askDockerWindows() {
   177  	s.askBasicDocker("mcr.microsoft.com/windows/servercore:1809")
   178  
   179  	for _, volume := range s.Docker.Volumes {
   180  		// This does not cover all the possibilities since we don't have access
   181  		// to volume parsing package since it's internal.
   182  		if strings.Contains(volume, defaultDockerWindowCacheDir) {
   183  			return
   184  		}
   185  	}
   186  	s.Docker.Volumes = append(s.Docker.Volumes, defaultDockerWindowCacheDir)
   187  }
   188  
   189  func (s *RegisterCommand) askBasicDocker(exampleHelperImage string) {
   190  	if s.Docker == nil {
   191  		s.Docker = &common.DockerConfig{}
   192  	}
   193  
   194  	s.Docker.Image = s.ask("docker-image", fmt.Sprintf("Please enter the default Docker image (e.g. %s):", exampleHelperImage))
   195  }
   196  
   197  func (s *RegisterCommand) askParallels() {
   198  	s.Parallels.BaseName = s.ask("parallels-base-name", "Please enter the Parallels VM (e.g. my-vm):")
   199  }
   200  
   201  func (s *RegisterCommand) askVirtualBox() {
   202  	s.VirtualBox.BaseName = s.ask("virtualbox-base-name", "Please enter the VirtualBox VM (e.g. my-vm):")
   203  }
   204  
   205  func (s *RegisterCommand) askSSHServer() {
   206  	s.SSH.Host = s.ask("ssh-host", "Please enter the SSH server address (e.g. my.server.com):")
   207  	s.SSH.Port = s.ask("ssh-port", "Please enter the SSH server port (e.g. 22):", true)
   208  }
   209  
   210  func (s *RegisterCommand) askSSHLogin() {
   211  	s.SSH.User = s.ask("ssh-user", "Please enter the SSH user (e.g. root):")
   212  	s.SSH.Password = s.ask("ssh-password", "Please enter the SSH password (e.g. docker.io):", true)
   213  	s.SSH.IdentityFile = s.ask("ssh-identity-file", "Please enter path to SSH identity file (e.g. /home/user/.ssh/id_rsa):", true)
   214  }
   215  
   216  func (s *RegisterCommand) addRunner(runner *common.RunnerConfig) {
   217  	s.config.Runners = append(s.config.Runners, runner)
   218  }
   219  
   220  func (s *RegisterCommand) askRunner() {
   221  	s.URL = s.ask("url", "Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):")
   222  
   223  	if s.Token != "" {
   224  		logrus.Infoln("Token specified trying to verify runner...")
   225  		logrus.Warningln("If you want to register use the '-r' instead of '-t'.")
   226  		if !s.network.VerifyRunner(s.RunnerCredentials) {
   227  			logrus.Panicln("Failed to verify this runner. Perhaps you are having network problems")
   228  		}
   229  	} else {
   230  		// we store registration token as token, since we pass that to RunnerCredentials
   231  		s.Token = s.ask("registration-token", "Please enter the gitlab-ci token for this runner:")
   232  		s.Name = s.ask("name", "Please enter the gitlab-ci description for this runner:")
   233  		s.TagList = s.ask("tag-list", "Please enter the gitlab-ci tags for this runner (comma separated):", true)
   234  
   235  		if s.TagList == "" {
   236  			s.RunUntagged = true
   237  		}
   238  
   239  		parameters := common.RegisterRunnerParameters{
   240  			Description:    s.Name,
   241  			Tags:           s.TagList,
   242  			Locked:         s.Locked,
   243  			AccessLevel:    s.AccessLevel,
   244  			RunUntagged:    s.RunUntagged,
   245  			MaximumTimeout: s.MaximumTimeout,
   246  			Active:         !s.Paused,
   247  		}
   248  
   249  		result := s.network.RegisterRunner(s.RunnerCredentials, parameters)
   250  		if result == nil {
   251  			logrus.Panicln("Failed to register this runner. Perhaps you are having network problems")
   252  		}
   253  
   254  		s.Token = result.Token
   255  		s.registered = true
   256  	}
   257  }
   258  
   259  func (s *RegisterCommand) askExecutorOptions() {
   260  	kubernetes := s.Kubernetes
   261  	machine := s.Machine
   262  	docker := s.Docker
   263  	ssh := s.SSH
   264  	parallels := s.Parallels
   265  	virtualbox := s.VirtualBox
   266  	custom := s.Custom
   267  
   268  	s.Kubernetes = nil
   269  	s.Machine = nil
   270  	s.Docker = nil
   271  	s.SSH = nil
   272  	s.Parallels = nil
   273  	s.VirtualBox = nil
   274  	s.Custom = nil
   275  
   276  	executorFns := map[string]func(){
   277  		"kubernetes": func() {
   278  			s.Kubernetes = kubernetes
   279  		},
   280  		"docker+machine": func() {
   281  			s.Machine = machine
   282  			s.Docker = docker
   283  			s.askDocker()
   284  		},
   285  		"docker-ssh+machine": func() {
   286  			s.Machine = machine
   287  			s.Docker = docker
   288  			s.SSH = ssh
   289  			s.askDocker()
   290  			s.askSSHLogin()
   291  		},
   292  		"docker": func() {
   293  			s.Docker = docker
   294  			s.askDocker()
   295  		},
   296  		"docker-windows": func() {
   297  			s.Docker = docker
   298  			s.askDockerWindows()
   299  		},
   300  		"docker-ssh": func() {
   301  			s.Docker = docker
   302  			s.SSH = ssh
   303  			s.askDocker()
   304  			s.askSSHLogin()
   305  		},
   306  		"ssh": func() {
   307  			s.SSH = ssh
   308  			s.askSSHServer()
   309  			s.askSSHLogin()
   310  		},
   311  		"parallels": func() {
   312  			s.SSH = ssh
   313  			s.Parallels = parallels
   314  			s.askParallels()
   315  			s.askSSHServer()
   316  		},
   317  		"virtualbox": func() {
   318  			s.SSH = ssh
   319  			s.VirtualBox = virtualbox
   320  			s.askVirtualBox()
   321  			s.askSSHLogin()
   322  		},
   323  		"shell": func() {
   324  			if runtime.GOOS == "windows" && s.RunnerConfig.Shell == "" {
   325  				s.Shell = "powershell"
   326  			}
   327  		},
   328  		"custom": func() {
   329  			s.Custom = custom
   330  		},
   331  	}
   332  
   333  	executorFn, ok := executorFns[s.Executor]
   334  	if ok {
   335  		executorFn()
   336  	}
   337  }
   338  
   339  func (s *RegisterCommand) Execute(context *cli.Context) {
   340  	userModeWarning(true)
   341  
   342  	s.context = context
   343  	err := s.loadConfig()
   344  	if err != nil {
   345  		logrus.Panicln(err)
   346  	}
   347  
   348  	validAccessLevels := []AccessLevel{NotProtected, RefProtected}
   349  	if !accessLevelValid(validAccessLevels, AccessLevel(s.AccessLevel)) {
   350  		logrus.Panicln("Given access-level is not valid. " +
   351  			"Please refer to gitlab-runner register -h for the correct options.")
   352  	}
   353  
   354  	s.askRunner()
   355  
   356  	if !s.LeaveRunner {
   357  		defer func() {
   358  			// De-register runner on panic
   359  			if r := recover(); r != nil {
   360  				if s.registered {
   361  					s.network.UnregisterRunner(s.RunnerCredentials)
   362  				}
   363  
   364  				// pass panic to next defer
   365  				panic(r)
   366  			}
   367  		}()
   368  
   369  		signals := make(chan os.Signal, 1)
   370  		signal.Notify(signals, os.Interrupt)
   371  
   372  		go func() {
   373  			signal := <-signals
   374  			s.network.UnregisterRunner(s.RunnerCredentials)
   375  			logrus.Fatalf("RECEIVED SIGNAL: %v", signal)
   376  		}()
   377  	}
   378  
   379  	if s.config.Concurrent < s.Limit {
   380  		logrus.Warningf("Specified limit (%d) larger then current concurrent limit (%d). Concurrent limit will not be enlarged.", s.Limit, s.config.Concurrent)
   381  	}
   382  
   383  	s.askExecutor()
   384  	s.askExecutorOptions()
   385  
   386  	s.mergeTemplate()
   387  
   388  	s.addRunner(&s.RunnerConfig)
   389  	err = s.saveConfig()
   390  	if err != nil {
   391  		logrus.Panicln(err)
   392  	}
   393  
   394  	logrus.Printf("Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!")
   395  }
   396  
   397  func (s *RegisterCommand) mergeTemplate() {
   398  	if !s.ConfigTemplate.Enabled() {
   399  		return
   400  	}
   401  
   402  	logrus.Infof("Merging configuration from template file %q", s.ConfigTemplate.ConfigFile)
   403  
   404  	err := s.ConfigTemplate.MergeTo(&s.RunnerConfig)
   405  	if err != nil {
   406  		logrus.WithError(err).Fatal("Could not handle configuration merging from template file")
   407  	}
   408  }
   409  
   410  func getHostname() string {
   411  	hostname, _ := os.Hostname()
   412  	return hostname
   413  }
   414  
   415  func newRegisterCommand() *RegisterCommand {
   416  	return &RegisterCommand{
   417  		RunnerConfig: common.RunnerConfig{
   418  			Name: getHostname(),
   419  			RunnerSettings: common.RunnerSettings{
   420  				Kubernetes: &common.KubernetesConfig{},
   421  				Cache:      &common.CacheConfig{},
   422  				Machine:    &common.DockerMachine{},
   423  				Docker:     &common.DockerConfig{},
   424  				SSH:        &ssh.Config{},
   425  				Parallels:  &common.ParallelsConfig{},
   426  				VirtualBox: &common.VirtualBoxConfig{},
   427  			},
   428  		},
   429  		Locked:  true,
   430  		Paused:  false,
   431  		network: network.NewGitLabClient(),
   432  	}
   433  }
   434  
   435  func accessLevelValid(levels []AccessLevel, givenLevel AccessLevel) bool {
   436  	if givenLevel == "" {
   437  		return true
   438  	}
   439  
   440  	for _, level := range levels {
   441  		if givenLevel == level {
   442  			return true
   443  		}
   444  	}
   445  
   446  	return false
   447  }
   448  
   449  func init() {
   450  	common.RegisterCommand2("register", "register a new runner", newRegisterCommand())
   451  }