commands/register.go (487 lines of code) (raw):
package commands
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/signal"
"runtime"
"strings"
"time"
"dario.cat/mergo"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/network"
"gitlab.com/gitlab-org/gitlab-runner/shells"
// Force to load shell executor, executes init() on them
_ "gitlab.com/gitlab-org/gitlab-runner/executors/custom"
_ "gitlab.com/gitlab-org/gitlab-runner/executors/docker"
_ "gitlab.com/gitlab-org/gitlab-runner/executors/parallels"
_ "gitlab.com/gitlab-org/gitlab-runner/executors/shell"
_ "gitlab.com/gitlab-org/gitlab-runner/executors/ssh"
_ "gitlab.com/gitlab-org/gitlab-runner/executors/virtualbox"
)
type configTemplate struct {
*common.Config
ConfigFile string `long:"config" env:"TEMPLATE_CONFIG_FILE" description:"Path to the configuration template file"`
}
func (c *configTemplate) Enabled() bool {
return c.ConfigFile != ""
}
func (c *configTemplate) MergeTo(config *common.RunnerConfig) error {
err := c.loadConfigTemplate()
if err != nil {
return fmt.Errorf("couldn't load configuration template file: %w", err)
}
if len(c.Runners) != 1 {
return errors.New("configuration template must contain exactly one [[runners]] entry")
}
c.Runners[0].Token = ""
err = mergo.Merge(config, c.Runners[0])
if err != nil {
return fmt.Errorf("error while merging configuration with configuration template: %w", err)
}
return nil
}
func (c *configTemplate) loadConfigTemplate() error {
config := common.NewConfig()
err := config.LoadConfig(c.ConfigFile)
if err != nil {
return err
}
c.Config = config
return nil
}
type RegisterCommand struct {
context *cli.Context
network common.Network
reader *bufio.Reader
registered bool
timeNowFn func() time.Time
configOptions
ConfigTemplate configTemplate `namespace:"template"`
TagList string `long:"tag-list" env:"RUNNER_TAG_LIST" description:"Tag list"`
NonInteractive bool `short:"n" long:"non-interactive" env:"REGISTER_NON_INTERACTIVE" description:"Run registration unattended"`
LeaveRunner bool `long:"leave-runner" env:"REGISTER_LEAVE_RUNNER" description:"Don't remove runner if registration fails"`
RegistrationToken string `short:"r" long:"registration-token" env:"REGISTRATION_TOKEN" description:"Runner's registration token (deprecated, use --token)"`
RunUntagged bool `long:"run-untagged" env:"REGISTER_RUN_UNTAGGED" description:"Register to run untagged builds; defaults to 'true' when 'tag-list' is empty"`
Locked bool `long:"locked" env:"REGISTER_LOCKED" description:"Lock Runner for current project, defaults to 'true'"`
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"`
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"`
Paused bool `long:"paused" env:"REGISTER_PAUSED" description:"Set Runner to be paused, defaults to 'false'"`
MaintenanceNote string `long:"maintenance-note" env:"REGISTER_MAINTENANCE_NOTE" description:"Runner's maintenance note"`
common.RunnerConfig
}
type AccessLevel string
const (
NotProtected AccessLevel = "not_protected"
RefProtected AccessLevel = "ref_protected"
)
const (
defaultDockerWindowCacheDir = "c:\\cache"
)
func (s *RegisterCommand) askOnce(prompt string, result *string, allowEmpty bool) bool {
println(prompt)
if *result != "" {
print("["+*result, "]: ")
}
if s.reader == nil {
s.reader = bufio.NewReader(os.Stdin)
}
data, _, err := s.reader.ReadLine()
if err == io.EOF && !s.NonInteractive {
logrus.Panicln("Unexpected EOF. Did you mean to use --non-interactive?")
}
if err != nil {
panic(err)
}
newResult := string(data)
newResult = strings.TrimSpace(newResult)
if newResult != "" {
*result = newResult
return true
}
if allowEmpty || *result != "" {
return true
}
return false
}
func (s *RegisterCommand) ask(key, prompt string, allowEmptyOptional ...bool) string {
allowEmpty := len(allowEmptyOptional) > 0 && allowEmptyOptional[0]
result := s.context.String(key)
result = strings.TrimSpace(result)
if s.NonInteractive || prompt == "" {
if result == "" && !allowEmpty {
logrus.Panicln("The", key, "needs to be entered")
}
return result
}
for {
if s.askOnce(prompt, &result, allowEmpty) {
break
}
}
return result
}
func (s *RegisterCommand) askExecutor() {
for {
names := common.GetExecutorNames()
executors := strings.Join(names, ", ")
s.Executor = s.ask("executor", "Enter an executor: "+executors+":", true)
if common.GetExecutorProvider(s.Executor) != nil {
return
}
message := "Invalid executor specified"
if s.NonInteractive {
logrus.Panicln(message)
} else {
logrus.Errorln(message)
}
}
}
func (s *RegisterCommand) askDocker() {
s.askBasicDocker("ruby:2.7")
for _, volume := range s.Docker.Volumes {
parts := strings.Split(volume, ":")
if parts[len(parts)-1] == "/cache" {
return
}
}
if !s.Docker.DisableCache {
s.Docker.Volumes = append(s.Docker.Volumes, "/cache")
}
}
func (s *RegisterCommand) askDockerWindows() {
s.askBasicDocker("mcr.microsoft.com/windows/servercore:1809")
for _, volume := range s.Docker.Volumes {
// This does not cover all the possibilities since we don't have access
// to volume parsing package since it's internal.
if strings.Contains(volume, defaultDockerWindowCacheDir) {
return
}
}
s.Docker.Volumes = append(s.Docker.Volumes, defaultDockerWindowCacheDir)
}
func (s *RegisterCommand) askBasicDocker(exampleHelperImage string) {
if s.Docker == nil {
s.Docker = &common.DockerConfig{}
}
s.Docker.Image = s.ask(
"docker-image",
fmt.Sprintf("Enter the default Docker image (for example, %s):", exampleHelperImage),
)
}
func (s *RegisterCommand) askParallels() {
s.Parallels.BaseName = s.ask("parallels-base-name", "Enter the Parallels VM (for example, my-vm):")
}
func (s *RegisterCommand) askVirtualBox() {
s.VirtualBox.BaseName = s.ask("virtualbox-base-name", "Enter the VirtualBox VM (for example, my-vm):")
}
func (s *RegisterCommand) askSSHServer() {
s.SSH.Host = s.ask("ssh-host", "Enter the SSH server address (for example, my.server.com):")
s.SSH.Port = s.ask("ssh-port", "Enter the SSH server port (for example, 22):", true)
}
func (s *RegisterCommand) askSSHLogin() {
s.SSH.User = s.ask("ssh-user", "Enter the SSH user (for example, root):")
s.SSH.Password = s.ask(
"ssh-password",
"Enter the SSH password (for example, docker.io):",
true,
)
s.SSH.IdentityFile = s.ask(
"ssh-identity-file",
"Enter the path to the SSH identity file (for example, /home/user/.ssh/id_rsa):",
true,
)
}
func (s *RegisterCommand) verifyRunner() {
// If a runner authentication token is specified in place of a registration token, let's accept it and process it as
// an authentication token. This allows for an easier transition for users by simply replacing the
// registration token with the new authentication token.
result := s.network.VerifyRunner(s.RunnerCredentials, s.SystemIDState.GetSystemID())
if result == nil || result.ID == 0 {
logrus.Panicln("Failed to verify the runner.")
}
s.ID = result.ID
s.TokenObtainedAt = s.timeNowFn().UTC().Truncate(time.Second)
s.TokenExpiresAt = result.TokenExpiresAt
s.registered = true
}
func (s *RegisterCommand) addRunner(runner *common.RunnerConfig) {
s.configMutex.Lock()
defer s.configMutex.Unlock()
s.config.Runners = append(s.config.Runners, runner)
}
func (s *RegisterCommand) askRunner() {
s.URL = s.ask("url", "Enter the GitLab instance URL (for example, https://gitlab.com/):")
if s.Token != "" && !s.tokenIsRunnerToken() {
logrus.Infoln("Token specified trying to verify runner...")
logrus.Warningln("If you want to register use the '-r' instead of '-t'.")
if s.network.VerifyRunner(s.RunnerCredentials, s.SystemIDState.GetSystemID()) == nil {
logrus.Panicln("Failed to verify the runner. You may be having network problems.")
}
return
}
if s.Token == "" || !s.tokenIsRunnerToken() {
s.Token = s.ask("registration-token", "Enter the registration token:")
}
if !s.tokenIsRunnerToken() {
s.Name = s.ask("name", "Enter a description for the runner:")
s.doLegacyRegisterRunner()
return
}
if r, err := s.RunnerByToken(s.Token); err == nil && r != nil {
logrus.Warningln("A runner with this system ID and token has already been registered.")
}
// when a runner authentication token is specified as a registration token, certain arguments are reserved to the server
s.ensureServerConfigArgsEmpty()
s.verifyRunner()
s.Name = s.ask("name", "Enter a name for the runner. This is stored only in the local config.toml file:")
}
func (s *RegisterCommand) doLegacyRegisterRunner() {
s.TagList = s.ask("tag-list", "Enter tags for the runner (comma-separated):", true)
s.MaintenanceNote = s.ask("maintenance-note", "Enter optional maintenance note for the runner:", true)
if s.TagList == "" {
s.RunUntagged = true
}
parameters := common.RegisterRunnerParameters{
Description: s.Name,
MaintenanceNote: s.MaintenanceNote,
Tags: s.TagList,
Locked: s.Locked,
AccessLevel: s.AccessLevel,
RunUntagged: s.RunUntagged,
MaximumTimeout: s.MaximumTimeout,
Paused: s.Paused,
}
if s.Token != "" {
logrus.Warningf(
"Support for registration tokens and runner parameters in the 'register' command has been deprecated in " +
"GitLab Runner 15.6 and will be replaced with support for authentication tokens. " +
"For more information, see https://docs.gitlab.com/ci/runners/new_creation_workflow/",
)
}
result := s.network.RegisterRunner(s.RunnerCredentials, parameters)
// golangci-lint doesn't recognize logrus.Panicln() call as breaking the execution
// flow which causes the following assignment to throw false-positive report for
// 'SA5011: possible nil pointer dereference'
//nolint:staticcheck
if result == nil {
logrus.Panicln("Failed to register the runner.")
}
s.ID = result.ID
s.Token = result.Token
s.TokenObtainedAt = s.timeNowFn().UTC().Truncate(time.Second)
s.TokenExpiresAt = result.TokenExpiresAt
s.registered = true
}
func (s *RegisterCommand) askExecutorOptions() {
kubernetes := s.Kubernetes
machine := s.Machine
docker := s.Docker
ssh := s.SSH
parallels := s.Parallels
virtualbox := s.VirtualBox
custom := s.Custom
s.Kubernetes = nil
s.Machine = nil
s.Docker = nil
s.SSH = nil
s.Parallels = nil
s.VirtualBox = nil
s.Custom = nil
s.Referees = nil
executorFns := map[string]func(){
"kubernetes": func() {
s.Kubernetes = kubernetes
},
"docker+machine": func() {
s.Machine = machine
s.Docker = docker
s.askDocker()
},
"docker": func() {
s.Docker = docker
s.askDocker()
},
"docker-autoscaler": func() {
s.Docker = docker
s.askDocker()
},
"docker-windows": func() {
if s.RunnerConfig.Shell == "" {
s.Shell = shells.SNPwsh
}
s.Docker = docker
s.askDockerWindows()
},
"ssh": func() {
s.SSH = ssh
s.askSSHServer()
s.askSSHLogin()
},
"parallels": func() {
s.SSH = ssh
s.Parallels = parallels
s.askParallels()
s.askSSHServer()
},
"virtualbox": func() {
s.SSH = ssh
s.VirtualBox = virtualbox
s.askVirtualBox()
s.askSSHLogin()
},
"shell": func() {
if runtime.GOOS == osTypeWindows && s.RunnerConfig.Shell == "" {
s.Shell = shells.SNPwsh
}
},
"custom": func() {
s.Custom = custom
},
}
executorFn, ok := executorFns[s.Executor]
if ok {
executorFn()
}
}
func (s *RegisterCommand) Execute(context *cli.Context) {
userModeWarning(true)
s.context = context
err := s.loadConfig()
if err != nil {
logrus.Panicln(err)
}
s.SystemIDState = s.loadedSystemIDState
validAccessLevels := []AccessLevel{NotProtected, RefProtected}
if !accessLevelValid(validAccessLevels, AccessLevel(s.AccessLevel)) {
logrus.Panicln("Given access-level is not valid. " +
"Refer to gitlab-runner register -h for the correct options.")
}
s.mergeTemplate()
s.askRunner()
if !s.LeaveRunner {
defer s.unregisterRunnerFunc()()
}
config := s.getConfig()
if config.Concurrent < s.Limit {
logrus.Warningf(
"The specified runner job concurrency limit (%d) is larger than current global concurrency limit (%d). "+
"The global concurrent limit will not be increased and takes precedence.",
s.Limit,
config.Concurrent,
)
}
if config.Concurrent < s.RequestConcurrency {
logrus.Warningf(
"The specified runner request concurrency (%d) is larger than the current global concurrent limit (%d). "+
"The global concurrent limit will not be increased and takes precedence.",
s.RequestConcurrency,
config.Concurrent,
)
}
s.askExecutor()
s.askExecutorOptions()
s.addRunner(&s.RunnerConfig)
err = s.saveConfig()
if err != nil {
logrus.Panicln(err)
}
logrus.Printf(
"Runner registered successfully. " +
"Feel free to start it, but if it's running already the config should be automatically reloaded!\n")
logrus.Printf("Configuration (with the authentication token) was saved in %q", s.ConfigFile)
}
func (s *RegisterCommand) unregisterRunnerFunc() func() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt)
go func() {
signal := <-signals
s.unregisterRunner()
logrus.Fatalf("RECEIVED SIGNAL: %v", signal)
}()
return func() {
// De-register runner on panic
if r := recover(); r != nil {
if s.registered {
s.unregisterRunner()
}
// pass panic to next defer
panic(r)
}
}
}
func (s *RegisterCommand) unregisterRunner() {
if s.tokenIsRunnerToken() {
s.network.UnregisterRunnerManager(s.RunnerCredentials, s.SystemIDState.GetSystemID())
} else {
s.network.UnregisterRunner(s.RunnerCredentials)
}
}
func (s *RegisterCommand) mergeTemplate() {
if !s.ConfigTemplate.Enabled() {
return
}
logrus.Infof("Merging configuration from template file %q", s.ConfigTemplate.ConfigFile)
err := s.ConfigTemplate.MergeTo(&s.RunnerConfig)
if err != nil {
logrus.WithError(err).Fatal("Could not handle configuration merging from template file")
}
}
func (s *RegisterCommand) tokenIsRunnerToken() bool {
return network.TokenIsCreatedRunnerToken(s.Token)
}
func (s *RegisterCommand) ensureServerConfigArgsEmpty() {
if s.Locked && s.AccessLevel == "" && !s.RunUntagged && s.MaximumTimeout == 0 && !s.Paused &&
s.TagList == "" && s.MaintenanceNote == "" {
return
}
if s.RegistrationToken == s.Token {
logrus.Warningln(
"You have specified an authentication token in the legacy parameter --registration-token. " +
"This has triggered the 'legacy-compatible registration process' which has resulted in the " +
"following command line parameters being ignored: --locked, --access-level, --run-untagged, " +
"--maximum-timeout, --paused, --tag-list, and --maintenance-note. " +
"For more information, see https://docs.gitlab.com/ci/runners/new_creation_workflow/#changes-to-the-gitlab-runner-register-command-syntax" +
"These parameters and the legacy-compatible registration process will be removed " +
"in a future GitLab Runner release. ",
)
return
}
logrus.Fatalln(
"Runner configuration other than name and executor configuration is reserved (specifically --locked, " +
"--access-level, --run-untagged, --maximum-timeout, --paused, --tag-list, and --maintenance-note) " +
"and cannot be specified when registering with a runner authentication token. " +
"This configuration is specified on the GitLab server. " +
"Please try again without specifying any of those arguments. " +
"For more information, see https://docs.gitlab.com/ci/runners/new_creation_workflow/#changes-to-the-gitlab-runner-register-command-syntax",
)
}
func getHostname() string {
hostname, _ := os.Hostname()
return hostname
}
func newRegisterCommand() *RegisterCommand {
return &RegisterCommand{
RunnerConfig: common.RunnerConfig{
Name: getHostname(),
RunnerSettings: common.RunnerSettings{
Kubernetes: &common.KubernetesConfig{},
Cache: &common.CacheConfig{},
Machine: &common.DockerMachine{},
Docker: &common.DockerConfig{},
SSH: &common.SshConfig{},
Parallels: &common.ParallelsConfig{},
VirtualBox: &common.VirtualBoxConfig{},
},
},
Locked: true,
Paused: false,
network: network.NewGitLabClient(),
timeNowFn: time.Now,
}
}
func accessLevelValid(levels []AccessLevel, givenLevel AccessLevel) bool {
if givenLevel == "" {
return true
}
for _, level := range levels {
if givenLevel == level {
return true
}
}
return false
}
func init() {
common.RegisterCommand2("register", "register a new runner", newRegisterCommand())
}