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 }