commands/service.go (226 lines of code) (raw):
package commands
import (
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
"github.com/kardianos/service"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/helpers/homedir"
service_helpers "gitlab.com/gitlab-org/gitlab-runner/helpers/service"
)
const (
defaultServiceName = "gitlab-runner"
defaultDescription = "GitLab Runner"
)
type NullService struct {
}
func (n *NullService) Start(s service.Service) error {
return nil
}
func (n *NullService) Stop(s service.Service) error {
return nil
}
func runServiceInstall(s service.Service, c *cli.Context) error {
if c.String("user") == "" && c.String("init-user") == "" && os.Getuid() == 0 {
logrus.Fatal("Please specify user that will run gitlab-runner service")
}
if configFile := c.String("config"); configFile != "" {
// try to load existing config
config := common.NewConfig()
err := config.LoadConfig(configFile)
if err != nil {
return err
}
// save config for the first time
if !config.Loaded {
err = config.SaveConfig(configFile)
if err != nil {
return err
}
}
}
return service.Control(s, "install")
}
func runServiceStatus(displayName string, s service.Service) {
status, err := s.Status()
description := ""
switch status {
case service.StatusRunning:
description = "Service is running"
case service.StatusStopped:
description = "Service has stopped"
default:
description = "Service status unknown"
if err != nil {
description = err.Error()
}
}
if status != service.StatusRunning {
fmt.Fprintf(os.Stderr, "%s: %s\n", displayName, description)
os.Exit(1)
}
fmt.Printf("%s: %s\n", displayName, description)
}
func getUserHomeDir(username string) string {
u, err := user.Lookup(username)
if err != nil {
panic(fmt.Sprintf("Failed to get home for user %q: %s", username, err.Error()))
}
return u.HomeDir
}
func GetServiceArguments(c *cli.Context) (arguments []string) {
// Update the default config-file path if it was not actually set and --init-user was specified...
config := c.String("config")
if !c.IsSet("config") && c.String("init-user") != "" {
config = filepath.Join(getUserHomeDir(c.String("init-user")), "config.toml")
}
arguments = append(arguments, "--config", config)
applyStrArg(c, "working-directory", false, func(val string) { arguments = append(arguments, "--working-directory", val) })
applyStrArg(c, "service", false, func(val string) { arguments = append(arguments, "--service", val) })
// syslogging doesn't make sense for systemd systems as those log straight to journald
syslog := !c.IsSet("syslog") || c.Bool("syslog")
if service.Platform() == "linux-systemd" && !c.IsSet("syslog") {
syslog = false
}
if syslog {
arguments = append(arguments, "--syslog")
}
return
}
func createServiceConfig(c *cli.Context) *service.Config {
config := &service.Config{
Name: c.String("service"),
DisplayName: c.String("service"),
Description: defaultDescription,
Arguments: append([]string{"run"}, GetServiceArguments(c)...),
}
// setup os specific service config
setupOSServiceConfig(c, config)
return config
}
func RunServiceControl(c *cli.Context) {
if c.String("user") != "" && c.String("init-user") != "" {
logrus.Fatal("Only one of 'user' or 'init-user' can be specified.")
}
svcConfig := createServiceConfig(c)
s, err := service_helpers.New(&NullService{}, svcConfig)
if err != nil {
logrus.Fatal(err)
}
switch c.Command.Name {
case "install":
err = runServiceInstall(s, c)
case "status":
runServiceStatus(svcConfig.DisplayName, s)
default:
err = service.Control(s, c.Command.Name)
}
if err != nil {
logrus.Fatal(err)
}
}
func GetFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "service, n",
Value: defaultServiceName,
Usage: "Specify service name to use",
},
}
}
func GetInstallFlags() []cli.Flag {
installFlags := GetFlags()
installFlags = append(
installFlags,
cli.StringFlag{
Name: "working-directory, d",
Value: homedir.New().GetWDOrEmpty(),
Usage: "Specify custom root directory where all data are stored",
},
cli.StringFlag{
Name: "config, c",
Value: GetDefaultConfigFile(),
Usage: "Specify custom config file",
},
cli.BoolFlag{
Name: "syslog",
Usage: "Setup system logging integration",
},
)
if runtime.GOOS == osTypeWindows {
installFlags = append(
installFlags,
cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "Specify user-name to secure the runner",
},
cli.StringFlag{
Name: "password, p",
Value: "",
Usage: "Specify user password to install service (required)",
})
} else if os.Getuid() == 0 {
installFlags = append(installFlags,
cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "Specify user-name to secure the runner",
},
cli.StringFlag{
Name: "init-user, i",
Value: "",
Usage: "Specify user-name to secure the runner in the init script or systemd unit file",
})
}
return installFlags
}
func init() {
flags := GetFlags()
installFlags := GetInstallFlags()
common.RegisterCommand(cli.Command{
Name: "install",
Usage: "install service",
Action: RunServiceControl,
Flags: installFlags,
})
common.RegisterCommand(cli.Command{
Name: "uninstall",
Usage: "uninstall service",
Action: RunServiceControl,
Flags: flags,
})
common.RegisterCommand(cli.Command{
Name: "start",
Usage: "start service",
Action: RunServiceControl,
Flags: flags,
})
common.RegisterCommand(cli.Command{
Name: "stop",
Usage: "stop service",
Action: RunServiceControl,
Flags: flags,
})
common.RegisterCommand(cli.Command{
Name: "restart",
Usage: "restart service",
Action: RunServiceControl,
Flags: flags,
})
common.RegisterCommand(cli.Command{
Name: "status",
Usage: "get status of a service",
Action: RunServiceControl,
Flags: flags,
})
}
// applyStrArg applies the named string-typed runtime argument to the service configuration in whatever way the `apply`
// function dictates.
func applyStrArg(c *cli.Context, argname string, rootonly bool, apply func(val string)) {
argval := c.String(argname)
if argval == "" {
return
}
if rootonly && os.Getuid() != 0 {
logrus.Fatalf("The --%s is not supported for non-root users", argname)
}
apply(argval)
}