diff --git a/cmd/start/start.go b/cmd/start/start.go index c19d8b25d3f6..c0cbebb789ad 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -20,9 +20,7 @@ import ( "fmt" "os" - "github.com/k0sproject/k0s/pkg/install" - - "github.com/kardianos/service" + "github.com/k0sproject/k0s/pkg/service" "github.com/spf13/cobra" ) @@ -34,16 +32,24 @@ func NewStartCmd() *cobra.Command { if os.Geteuid() != 0 { return fmt.Errorf("this command must be run as root") } - svc, err := install.InstalledService() + svc, err := service.InstalledK0sService() + if err != nil { + return err + } + + status, err := svc.Status() if err != nil { return err } - status, _ := svc.Status() if status == service.StatusRunning { return fmt.Errorf("already running") } - return svc.Start() + + if err := svc.Start(); err != nil { + return fmt.Errorf("failed to start the service: %w", err) + } + + return nil }, } - } diff --git a/cmd/stop/stop.go b/cmd/stop/stop.go index bcb00fc3e988..b599c7d2510b 100644 --- a/cmd/stop/stop.go +++ b/cmd/stop/stop.go @@ -20,9 +20,7 @@ import ( "fmt" "os" - "github.com/k0sproject/k0s/pkg/install" - - "github.com/kardianos/service" + "github.com/k0sproject/k0s/pkg/service" "github.com/spf13/cobra" ) @@ -34,10 +32,11 @@ func NewStopCmd() *cobra.Command { if os.Geteuid() != 0 { return fmt.Errorf("this command must be run as root") } - svc, err := install.InstalledService() + svc, err := service.InstalledK0sService() if err != nil { return err } + status, err := svc.Status() if err != nil { return err @@ -45,8 +44,12 @@ func NewStopCmd() *cobra.Command { if status == service.StatusStopped { return fmt.Errorf("already stopped") } - return svc.Stop() + + if err := svc.Stop(); err != nil { + return fmt.Errorf("failed to stop the service: %w", err) + } + + return nil }, } - } diff --git a/pkg/install/linux_systemd.go b/pkg/install/linux_systemd.go index b546e104cac8..635946a7553a 100644 --- a/pkg/install/linux_systemd.go +++ b/pkg/install/linux_systemd.go @@ -29,7 +29,8 @@ ConditionFileIsExecutable={{.Path|cmdEscape}} StartLimitInterval=5 StartLimitBurst=10 ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmdEscape}}{{end}} -Environment="{{- range $key, $value := .EnvVars}}{{$key}}={{$value}} {{- end}}" +{{- if .Option.Environment}}{{range .Option.Environment}} +Environment="{{.}}"{{end}}{{- end}} RestartSec=10 Delegate=yes diff --git a/pkg/install/service.go b/pkg/install/service.go index 8e25b1b50589..c960f4320b29 100644 --- a/pkg/install/service.go +++ b/pkg/install/service.go @@ -17,71 +17,28 @@ limitations under the License. package install import ( - "errors" "fmt" "strings" - "github.com/kardianos/service" + "github.com/k0sproject/k0s/pkg/service" "github.com/sirupsen/logrus" ) -var ( - k0sServiceName = "k0s" - k0sDescription = "k0s - Zero Friction Kubernetes" -) - -type Program struct{} - -func (p *Program) Start(service.Service) error { - // Start should not block. Do the actual work async. - return nil -} - -func (p *Program) Stop(service.Service) error { - // Stop should not block. Return with a few seconds. - return nil -} - -// InstalledService returns a k0s service if one has been installed on the host or an error otherwise. -func InstalledService() (service.Service, error) { - prg := &Program{} - for _, role := range []string{"controller", "worker"} { - c := GetServiceConfig(role) - s, err := service.New(prg, c) - if err != nil { - return nil, err - } - _, err = s.Status() - - if err != nil && errors.Is(err, service.ErrNotInstalled) { - continue - } - if err != nil { - return nil, err - } - return s, nil - } - - var s service.Service - return s, fmt.Errorf("k0s has not been installed as a service") -} - // EnsureService installs the k0s service, per the given arguments, and the detected platform func EnsureService(args []string, envVars []string, force bool) error { var deps []string var svcConfig *service.Config - prg := &Program{} for _, v := range args { if v == "controller" || v == "worker" { - svcConfig = GetServiceConfig(v) + svcConfig = service.K0sConfig(v) break } } - s, err := service.New(prg, svcConfig) + s, err := service.NewService(svcConfig) if err != nil { - return err + return fmt.Errorf("failed to create service: %w", err) } // fetch service type @@ -117,9 +74,14 @@ func EnsureService(args []string, envVars []string, force bool) error { svcConfig.Arguments = args if force { logrus.Infof("Uninstalling %s service", svcConfig.Name) - err = s.Uninstall() - if err != nil && !errors.Is(err, service.ErrNotInstalled) { - logrus.Warnf("failed to uninstall service: %v", err) + status, err := s.Status() + if err != nil { + logrus.Warnf("failed to get service status: %v", err) + } + if status != service.StatusNotInstalled { + if err := s.Uninstall(); err != nil { + logrus.Warnf("failed to uninstall service: %v", err) + } } } logrus.Infof("Installing %s service", svcConfig.Name) @@ -131,33 +93,16 @@ func EnsureService(args []string, envVars []string, force bool) error { } func UninstallService(role string) error { - prg := &Program{} - - if role == "controller+worker" { - role = "controller" - } - - svcConfig := GetServiceConfig(role) - s, err := service.New(prg, svcConfig) + s, err := service.InstalledK0sService() if err != nil { - return err + return fmt.Errorf("uninstall service: %w", err) } - return s.Uninstall() -} - -func GetServiceConfig(role string) *service.Config { - var k0sDisplayName string - - if role == "controller" || role == "worker" { - k0sDisplayName = "k0s " + role - k0sServiceName = "k0s" + role - } - return &service.Config{ - Name: k0sServiceName, - DisplayName: k0sDisplayName, - Description: k0sDescription, + if err := s.Uninstall(); err != nil { + return fmt.Errorf("uninstall service: %w", err) } + + return nil } func prepareEnvVars(envVars []string) map[string]string { diff --git a/pkg/service/config.go b/pkg/service/config.go new file mode 100644 index 000000000000..9741700962af --- /dev/null +++ b/pkg/service/config.go @@ -0,0 +1,11 @@ +package service + +// Config describes a configuration for a system service +type Config struct { + Name string + DisplayName string + Description string + Arguments []string + Option map[string]any + Dependencies []string +} diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 000000000000..655b33087b28 --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,142 @@ +package service + +import ( + "errors" + "fmt" + + "github.com/kardianos/service" +) + +// Status represents the status of a system service +type Status string + +const ( + StatusRunning Status = "running" + StatusStopped Status = "stopped" + StatusUnknown Status = "unknown" + StatusNotInstalled Status = "not installed" + + k0sServicePrefix = "k0s" + k0sDescription = "k0s - Zero Friction Kubernetes" +) + +var ErrK0sNotInstalled = errors.New("k0s has not been installed as a system service") + +type Service struct { + svc service.Service +} + +// dummy implementation for kardianos.Interface +type program struct{} + +func (p *program) Start(service.Service) error { + // Start should not block. Do the actual work async. + return nil +} + +func (p *program) Stop(service.Service) error { + // Stop should not block. Return with a few seconds. + return nil +} + +// NewService creates a new system service instance +func NewService(cfg *Config) (*Service, error) { + kardianosCfg := &service.Config{ + Name: cfg.Name, + Description: cfg.Description, + Arguments: cfg.Arguments, + Option: cfg.Option, + } + + kardSvc, err := service.New(&program{}, kardianosCfg) + if err != nil { + return nil, fmt.Errorf("get service: %w", err) + } + return &Service{svc: kardSvc}, nil +} + +// K0sConfig returns a Config for a k0s system service +func K0sConfig(role string) *Config { + var k0sDisplayName, k0sServiceName string + + if role == "controller" || role == "worker" { + k0sDisplayName = k0sServicePrefix + " " + role + k0sServiceName = k0sServicePrefix + role + } + return &Config{ + Name: k0sServiceName, + DisplayName: k0sDisplayName, + Description: k0sDescription, + } +} + +func InstalledK0sService() (*Service, error) { + for _, role := range []string{"controller", "worker"} { + c := K0sConfig(role) + s, err := NewService(c) + if err != nil { + return nil, err + } + status, err := s.Status() + if err != nil { + return nil, err + } + + if status != StatusNotInstalled { + return s, nil + } + } + return nil, ErrK0sNotInstalled +} + +// Start the system service +func (s *Service) Start() error { + if err := s.svc.Start(); err != nil { + return fmt.Errorf("start service: %w", err) + } + return nil +} + +// Stop the system service +func (s *Service) Stop() error { + if err := s.svc.Stop(); err != nil { + return fmt.Errorf("stop service: %w", err) + } + return nil +} + +// Status of the system service or an error if the status could not be determined +func (s *Service) Status() (Status, error) { + status, err := s.svc.Status() + if err != nil { + if errors.Is(err, service.ErrNotInstalled) { + return StatusNotInstalled, nil + } + return StatusUnknown, fmt.Errorf("get service status: %w", err) + } + + // Map kardianos status codes to our defined constants + switch status { + case service.StatusRunning: + return StatusRunning, nil + case service.StatusStopped: + return StatusStopped, nil + default: + return StatusUnknown, nil + } +} + +// Uninstall the system service +func (s *Service) Uninstall() error { + return s.svc.Uninstall() +} + +// Install the system service +func (s *Service) Install() error { + return s.svc.Install() +} + +// Platform returns the init system used by the host +func (s *Service) Platform() string { + return s.svc.Platform() +}