github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/service/snap/snap.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package snap
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/juju/clock"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/retry"
    20  	"github.com/juju/utils/v3"
    21  	"github.com/juju/utils/v3/shell"
    22  
    23  	"github.com/juju/juju/service/common"
    24  	"github.com/juju/juju/service/systemd"
    25  )
    26  
    27  const (
    28  	// Command is a path to the snap binary, or to one that can be detected by os.Exec
    29  	Command = "snap"
    30  )
    31  
    32  var (
    33  	logger = loggo.GetLogger("juju.service.snap")
    34  
    35  	// snapNameRe is derived from https://github.com/snapcore/snapcraft/blob/a2ef08109d86259a0748446f41bce5205d00a922/schema/snapcraft.yaml#L81-106
    36  	// but does not test for "--"
    37  	snapNameRe = regexp.MustCompile("^[a-z0-9][a-z0-9-]{0,39}[^-]$")
    38  )
    39  
    40  // Runnable expects to be able to run a given command with a series of arguments
    41  // and return the output and/or error from that executing command.
    42  type Runnable interface {
    43  	Execute(name string, args ...string) (string, error)
    44  }
    45  
    46  // BackgroundService represents the a service that snaps define.
    47  // For example, the multipass snap includes the libvirt-bin and multipassd background services.
    48  type BackgroundService struct {
    49  	// name is the name of the service, without the snap name.
    50  	// For example , for the`juju-db.daemon` service, use the name `daemon`.
    51  	Name string
    52  
    53  	// enableAtStartup determines whether services provided
    54  	// by the snap should be started with the `--enable` flag
    55  	EnableAtStartup bool
    56  }
    57  
    58  // Validate checks that the construction parameters of
    59  // backgroundService are valid. Successful validation
    60  // returns nil.
    61  func (backgroundService *BackgroundService) Validate() error {
    62  	name := backgroundService.Name
    63  	if name == "" {
    64  		return errors.NotValidf("empty background service name")
    65  	}
    66  
    67  	if !snapNameRe.MatchString(name) {
    68  		return errors.NotValidf("background service name %q", name)
    69  	}
    70  
    71  	return nil
    72  }
    73  
    74  // SetSnapConfig sets a snap's key to value.
    75  func SetSnapConfig(snap string, key string, value string) error {
    76  	if key == "" {
    77  		return errors.NotValidf("key must not be empty")
    78  	}
    79  
    80  	cmd := exec.Command(Command, "set", snap, fmt.Sprintf("%s=%s", key, value))
    81  	_, err := cmd.Output()
    82  	if err != nil {
    83  		return errors.Annotate(err, fmt.Sprintf("setting snap %s config %s to %s", snap, key, value))
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  // Installable represents an installable snap.
    90  type Installable interface {
    91  	// Name returns the name of the application
    92  	Name() string
    93  
    94  	// Install returns a way to install one application with all it's settings.
    95  	Install() []string
    96  
    97  	// Validate will validate a given application for any potential issues.
    98  	Validate() error
    99  
   100  	// StartCommands returns a list if shell commands that should be executed
   101  	// (in order) to start App and its background services.
   102  	StartCommands(executable string) []string
   103  
   104  	// Prerequisites defines a list of all the Prerequisites required before the
   105  	// application also needs to be installed.
   106  	Prerequisites() []Installable
   107  
   108  	// BackgroundServices returns a list of background services that are
   109  	// required to be installed for the main application to run.
   110  	BackgroundServices() []BackgroundService
   111  }
   112  
   113  // Service is a type for services that are being managed by snapd as snaps.
   114  type Service struct {
   115  	runnable       Runnable
   116  	clock          clock.Clock
   117  	name           string
   118  	scriptRenderer shell.Renderer
   119  	executable     string
   120  	app            Installable
   121  	conf           common.Conf
   122  	configDir      string
   123  }
   124  
   125  // NewService returns a new Service defined by `conf`, with the name `serviceName`.
   126  // The Service abstracts service(s) provided by a snap.
   127  //
   128  // `serviceName` defaults to `snapName`. These two parameters are distinct to allow
   129  // for a file path to provided as a `mainSnap`, implying that a local snap will be
   130  // installed by snapd.
   131  //
   132  // If no BackgroundServices are provided, Service will wrap all of the snap's
   133  // background services.
   134  func NewService(mainSnap, serviceName string, conf common.Conf, snapPath, configDir, channel string, confinementPolicy ConfinementPolicy, backgroundServices []BackgroundService, prerequisites []Installable) (Service, error) {
   135  	if serviceName == "" {
   136  		serviceName = mainSnap
   137  	}
   138  	if mainSnap == "" {
   139  		return Service{}, errors.New("mainSnap must be provided")
   140  	}
   141  	app := &App{
   142  		name:               mainSnap,
   143  		confinementPolicy:  confinementPolicy,
   144  		channel:            channel,
   145  		backgroundServices: backgroundServices,
   146  		prerequisites:      prerequisites,
   147  	}
   148  	err := app.Validate()
   149  	if err != nil {
   150  		return Service{}, errors.Trace(err)
   151  	}
   152  
   153  	return Service{
   154  		runnable:       defaultRunner{},
   155  		clock:          clock.WallClock,
   156  		name:           serviceName,
   157  		scriptRenderer: &shell.BashRenderer{},
   158  		executable:     snapPath,
   159  		app:            app,
   160  		conf:           conf,
   161  		configDir:      configDir,
   162  	}, nil
   163  }
   164  
   165  // Validate validates that snap.Service has been correctly configured.
   166  // Validate returns nil when successful and an error when successful.
   167  func (s Service) Validate() error {
   168  	if err := s.app.Validate(); err != nil {
   169  		return errors.Trace(err)
   170  	}
   171  
   172  	for _, prerequisite := range s.app.Prerequisites() {
   173  		if err := prerequisite.Validate(); err != nil {
   174  			return errors.Trace(err)
   175  		}
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  // Name returns the service's name. It should match snap's naming conventions,
   182  // e.g. <snap> for all services provided by <snap> and `<snap>.<app>` for a specific service
   183  // under the snap's control.For example, the `juju-db` snap provides a `daemon` service.
   184  // Its name is `juju-db.daemon`.
   185  func (s Service) Name() string {
   186  	if s.name != "" {
   187  		return s.name
   188  	}
   189  	return s.app.Name()
   190  }
   191  
   192  // Running returns (true, nil) when snap indicates that service is currently active.
   193  func (s Service) Running() (bool, error) {
   194  	_, _, running, err := s.status()
   195  	if err != nil {
   196  		return false, errors.Trace(err)
   197  	}
   198  	return running, nil
   199  }
   200  
   201  // Exists is not implemented for snaps.
   202  func (s Service) Exists() (bool, error) {
   203  	return false, errors.NotImplementedf("snap service Exists")
   204  }
   205  
   206  // Install installs the snap and its background services.
   207  func (s Service) Install() error {
   208  	for _, app := range s.app.Prerequisites() {
   209  		logger.Infof("command: %v", app)
   210  
   211  		out, err := s.runCommandWithRetry(app.Install()...)
   212  		if err != nil {
   213  			return errors.Annotatef(err, "output: %v", out)
   214  		}
   215  	}
   216  
   217  	out, err := s.runCommandWithRetry(s.app.Install()...)
   218  	if err != nil {
   219  		return errors.Annotatef(err, "output: %v", out)
   220  	}
   221  	return nil
   222  }
   223  
   224  // Installed returns true if the service has been successfully installed.
   225  func (s Service) Installed() (bool, error) {
   226  	installed, _, _, err := s.status()
   227  	if err != nil {
   228  		return false, errors.Trace(err)
   229  	}
   230  	return installed, nil
   231  }
   232  
   233  // ConfigOverride writes a systemd override to enable the
   234  // specified limits to be used by the snap.
   235  func (s Service) ConfigOverride() error {
   236  	if len(s.conf.Limit) == 0 {
   237  		return nil
   238  	}
   239  
   240  	unitOptions := systemd.ServiceLimits(s.conf)
   241  	data, err := io.ReadAll(systemd.UnitSerialize(unitOptions))
   242  	if err != nil {
   243  		return errors.Trace(err)
   244  	}
   245  
   246  	for _, backgroundService := range s.app.BackgroundServices() {
   247  		overridesDir := fmt.Sprintf("%s/snap.%s.%s.service.d", s.configDir, s.name, backgroundService.Name)
   248  		if err := os.MkdirAll(overridesDir, 0755); err != nil {
   249  			return errors.Trace(err)
   250  		}
   251  		if err := os.WriteFile(filepath.Join(overridesDir, "overrides.conf"), data, 0644); err != nil {
   252  			return errors.Trace(err)
   253  		}
   254  	}
   255  	return nil
   256  }
   257  
   258  // StartCommands returns a slice of strings. that are
   259  // shell commands to be executed by a shell which start the service.
   260  func (s Service) StartCommands() ([]string, error) {
   261  	deps := s.app.Prerequisites()
   262  	commands := make([]string, 0, 1+len(deps))
   263  	for _, prerequisite := range deps {
   264  		commands = append(commands, prerequisite.StartCommands(s.executable)...)
   265  	}
   266  	return append(commands, s.app.StartCommands(s.executable)...), nil
   267  }
   268  
   269  // status returns an interpreted output from the `snap services` command.
   270  // For example, this output from `snap services juju-db.daemon`
   271  //
   272  //	Service                                Startup  Current
   273  //	juju-db.daemon                         enabled  inactive
   274  //
   275  // returns this output from status
   276  //
   277  //	(true, true, false, nil)
   278  func (s *Service) status() (isInstalled, enabledAtStartup, isCurrentlyActive bool, err error) {
   279  	out, err := s.runCommand("services", s.Name())
   280  	if err != nil {
   281  		return false, false, false, errors.Trace(err)
   282  	}
   283  	for _, line := range strings.Split(out, "\n") {
   284  		if !strings.HasPrefix(line, s.Name()) {
   285  			continue
   286  		}
   287  
   288  		fields := strings.Fields(line)
   289  		return true, fields[1] == "enabled", fields[2] == "active", nil
   290  	}
   291  
   292  	return false, false, false, nil
   293  }
   294  
   295  // Start starts the service, returning nil when successful.
   296  // If the service is already running, Start does not restart it.
   297  func (s Service) Start() error {
   298  	running, err := s.Running()
   299  	if err != nil {
   300  		return errors.Trace(err)
   301  	}
   302  	if running {
   303  		return nil
   304  	}
   305  
   306  	commands, err := s.StartCommands()
   307  	if err != nil {
   308  		return errors.Trace(err)
   309  	}
   310  	for _, command := range commands {
   311  		commandParts := strings.Fields(command)
   312  		out, err := utils.RunCommand(commandParts[0], commandParts[1:]...)
   313  		if err != nil {
   314  			if strings.Contains(out, "has no services") {
   315  				continue
   316  			}
   317  			return errors.Annotatef(err, "%v -> %v", command, out)
   318  		}
   319  	}
   320  
   321  	return nil
   322  }
   323  
   324  // Stop stops a running service. Returns nil when the underlying
   325  // call to `snap stop <service-name>` exits with error code 0.
   326  func (s Service) Stop() error {
   327  	running, err := s.Running()
   328  	if err != nil {
   329  		return errors.Trace(err)
   330  	}
   331  	if !running {
   332  		return nil
   333  	}
   334  
   335  	args := []string{"stop", s.Name()}
   336  	return s.execThenExpect(args, "Stopped.")
   337  }
   338  
   339  // Restart restarts the service, or starts if it's not currently
   340  // running.
   341  //
   342  // Restart is part of the service.RestartableService interface
   343  func (s Service) Restart() error {
   344  	args := []string{"restart", s.Name()}
   345  	return s.execThenExpect(args, "Restarted.")
   346  }
   347  
   348  // execThenExpect calls `snap <commandArgs>...` and then checks
   349  // stdout against expectation and snap's exit code. When there's a
   350  // mismatch or non-0 exit code, execThenExpect returns an error.
   351  func (s Service) execThenExpect(commandArgs []string, expectation string) error {
   352  	out, err := s.runCommand(commandArgs...)
   353  	if err != nil {
   354  		return errors.Trace(err)
   355  	}
   356  	if !strings.Contains(out, expectation) {
   357  		return errors.Annotatef(err, `expected "%s", got "%s"`, expectation, out)
   358  	}
   359  	return nil
   360  }
   361  
   362  func (s Service) runCommand(args ...string) (string, error) {
   363  	return s.runnable.Execute(s.executable, args...)
   364  }
   365  
   366  func (s Service) runCommandWithRetry(args ...string) (res string, err error) {
   367  	if resErr := retry.Call(retry.CallArgs{
   368  		Clock: s.clock,
   369  		Func: func() error {
   370  			res, err = s.runCommand(args...)
   371  			return errors.Trace(err)
   372  		},
   373  		Delay:    5 * time.Second,
   374  		Attempts: 2,
   375  	}); resErr != nil {
   376  		return "", errors.Trace(resErr)
   377  	}
   378  
   379  	// Named args are set via the retry.
   380  	return
   381  }
   382  
   383  type defaultRunner struct{}
   384  
   385  func (defaultRunner) Execute(name string, args ...string) (string, error) {
   386  	return utils.RunCommand(name, args...)
   387  }