github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/hookstate/hookmgr.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package hookstate
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"regexp"
    28  	"strings"
    29  	"sync"
    30  	"sync/atomic"
    31  	"time"
    32  
    33  	"gopkg.in/tomb.v2"
    34  
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/errtracker"
    37  	"github.com/snapcore/snapd/logger"
    38  	"github.com/snapcore/snapd/osutil"
    39  	"github.com/snapcore/snapd/overlord/configstate/settings"
    40  	"github.com/snapcore/snapd/overlord/snapstate"
    41  	"github.com/snapcore/snapd/overlord/state"
    42  	"github.com/snapcore/snapd/snap"
    43  )
    44  
    45  type hijackFunc func(ctx *Context) error
    46  type hijackKey struct{ hook, snap string }
    47  
    48  // HookManager is responsible for the maintenance of hooks in the system state.
    49  // It runs hooks when they're requested, assuming they're present in the given
    50  // snap. Otherwise they're skipped with no error.
    51  type HookManager struct {
    52  	state      *state.State
    53  	repository *repository
    54  
    55  	contextsMutex sync.RWMutex
    56  	contexts      map[string]*Context
    57  
    58  	hijackMap map[hijackKey]hijackFunc
    59  
    60  	runningHooks int32
    61  	runner       *state.TaskRunner
    62  }
    63  
    64  // Handler is the interface a client must satify to handle hooks.
    65  type Handler interface {
    66  	// Before is called right before the hook is to be run.
    67  	Before() error
    68  
    69  	// Done is called right after the hook has finished successfully.
    70  	Done() error
    71  
    72  	// Error is called if the hook encounters an error while running.
    73  	// The returned bool flag indicates if the original hook error should be
    74  	// ignored by hook manager.
    75  	Error(hookErr error) (ignoreHookErr bool, err error)
    76  }
    77  
    78  // HandlerGenerator is the function signature required to register for hooks.
    79  type HandlerGenerator func(*Context) Handler
    80  
    81  // HookSetup is a reference to a hook within a specific snap.
    82  type HookSetup struct {
    83  	Snap        string        `json:"snap"`
    84  	Revision    snap.Revision `json:"revision"`
    85  	Hook        string        `json:"hook"`
    86  	Timeout     time.Duration `json:"timeout,omitempty"`
    87  	Optional    bool          `json:"optional,omitempty"`     // do not error if script is missing
    88  	Always      bool          `json:"always,omitempty"`       // run handler even if script is missing
    89  	IgnoreError bool          `json:"ignore-error,omitempty"` // do not run handler's Error() on error
    90  	TrackError  bool          `json:"track-error,omitempty"`  // report hook error to oopsie
    91  }
    92  
    93  // Manager returns a new HookManager.
    94  func Manager(s *state.State, runner *state.TaskRunner) (*HookManager, error) {
    95  	// Make sure we only run 1 hook task for given snap at a time
    96  	runner.AddBlocked(func(thisTask *state.Task, running []*state.Task) bool {
    97  		// check if we're a hook task, probably not needed but let's take extra care
    98  		if thisTask.Kind() != "run-hook" {
    99  			return false
   100  		}
   101  		var hooksup HookSetup
   102  		if thisTask.Get("hook-setup", &hooksup) != nil {
   103  			return false
   104  		}
   105  		thisSnapName := hooksup.Snap
   106  		// examine all hook tasks, block thisTask if we find any other hook task affecting same snap
   107  		for _, t := range running {
   108  			if t.Kind() != "run-hook" || t.Get("hook-setup", &hooksup) != nil {
   109  				continue // ignore errors and continue checking remaining tasks
   110  			}
   111  			if hooksup.Snap == thisSnapName {
   112  				// found hook task affecting same snap, block thisTask.
   113  				return true
   114  			}
   115  		}
   116  		return false
   117  	})
   118  
   119  	manager := &HookManager{
   120  		state:      s,
   121  		repository: newRepository(),
   122  		contexts:   make(map[string]*Context),
   123  		hijackMap:  make(map[hijackKey]hijackFunc),
   124  		runner:     runner,
   125  	}
   126  
   127  	runner.AddHandler("run-hook", manager.doRunHook, manager.undoRunHook)
   128  	// Compatibility with snapd between 2.29 and 2.30 in edge only.
   129  	// We generated a configure-snapd task on core refreshes and
   130  	// for compatibility we need to handle those.
   131  	runner.AddHandler("configure-snapd", func(*state.Task, *tomb.Tomb) error {
   132  		return nil
   133  	}, nil)
   134  
   135  	setupHooks(manager)
   136  
   137  	snapstate.AddAffectedSnapsByAttr("hook-setup", manager.hookAffectedSnaps)
   138  
   139  	return manager, nil
   140  }
   141  
   142  // Register registers a function to create Handler values whenever hooks
   143  // matching the provided pattern are run.
   144  func (m *HookManager) Register(pattern *regexp.Regexp, generator HandlerGenerator) {
   145  	m.repository.addHandlerGenerator(pattern, generator)
   146  }
   147  
   148  // Ensure implements StateManager.Ensure.
   149  func (m *HookManager) Ensure() error {
   150  	return nil
   151  }
   152  
   153  // StopHooks kills all currently running hooks and returns after
   154  // that's done.
   155  func (m *HookManager) StopHooks() {
   156  	m.runner.StopKinds("run-hook")
   157  }
   158  
   159  func (m *HookManager) hijacked(hookName, instanceName string) hijackFunc {
   160  	return m.hijackMap[hijackKey{hookName, instanceName}]
   161  }
   162  
   163  func (m *HookManager) RegisterHijack(hookName, instanceName string, f hijackFunc) {
   164  	if _, ok := m.hijackMap[hijackKey{hookName, instanceName}]; ok {
   165  		panic(fmt.Sprintf("hook %s for snap %s already hijacked", hookName, instanceName))
   166  	}
   167  	m.hijackMap[hijackKey{hookName, instanceName}] = f
   168  }
   169  
   170  func (m *HookManager) hookAffectedSnaps(t *state.Task) ([]string, error) {
   171  	var hooksup HookSetup
   172  	if err := t.Get("hook-setup", &hooksup); err != nil {
   173  		return nil, fmt.Errorf("internal error: cannot obtain hook data from task: %s", t.Summary())
   174  
   175  	}
   176  
   177  	if m.hijacked(hooksup.Hook, hooksup.Snap) != nil {
   178  		// assume being these internal they should not
   179  		// generate conflicts
   180  		return nil, nil
   181  	}
   182  
   183  	return []string{hooksup.Snap}, nil
   184  }
   185  
   186  func (m *HookManager) ephemeralContext(cookieID string) (context *Context, err error) {
   187  	var contexts map[string]string
   188  	m.state.Lock()
   189  	defer m.state.Unlock()
   190  	err = m.state.Get("snap-cookies", &contexts)
   191  	if err != nil {
   192  		return nil, fmt.Errorf("cannot get snap cookies: %v", err)
   193  	}
   194  	if instanceName, ok := contexts[cookieID]; ok {
   195  		// create new ephemeral cookie
   196  		context, err = NewContext(nil, m.state, &HookSetup{Snap: instanceName}, nil, cookieID)
   197  		return context, err
   198  	}
   199  	return nil, fmt.Errorf("invalid snap cookie requested")
   200  }
   201  
   202  // Context obtains the context for the given cookie ID.
   203  func (m *HookManager) Context(cookieID string) (*Context, error) {
   204  	m.contextsMutex.RLock()
   205  	defer m.contextsMutex.RUnlock()
   206  
   207  	var err error
   208  	context, ok := m.contexts[cookieID]
   209  	if !ok {
   210  		context, err = m.ephemeralContext(cookieID)
   211  		if err != nil {
   212  			return nil, err
   213  		}
   214  	}
   215  
   216  	return context, nil
   217  }
   218  
   219  func hookSetup(task *state.Task, key string) (*HookSetup, *snapstate.SnapState, error) {
   220  	var hooksup HookSetup
   221  	err := task.Get(key, &hooksup)
   222  	if err != nil {
   223  		return nil, nil, err
   224  	}
   225  
   226  	var snapst snapstate.SnapState
   227  	err = snapstate.Get(task.State(), hooksup.Snap, &snapst)
   228  	if err != nil && err != state.ErrNoState {
   229  		return nil, nil, fmt.Errorf("cannot handle %q snap: %v", hooksup.Snap, err)
   230  	}
   231  
   232  	return &hooksup, &snapst, nil
   233  }
   234  
   235  // NumRunningHooks returns the number of hooks running at the moment.
   236  func (m *HookManager) NumRunningHooks() int {
   237  	return int(atomic.LoadInt32(&m.runningHooks))
   238  }
   239  
   240  // GracefullyWaitRunningHooks waits for currently running hooks to finish up to the default hook timeout. Returns true if there are no more running hooks on exit.
   241  func (m *HookManager) GracefullyWaitRunningHooks() bool {
   242  	toutC := time.After(defaultHookTimeout)
   243  	doWait := true
   244  	for m.NumRunningHooks() > 0 && doWait {
   245  		select {
   246  		case <-time.After(1 * time.Second):
   247  		case <-toutC:
   248  			doWait = false
   249  		}
   250  	}
   251  	return m.NumRunningHooks() == 0
   252  }
   253  
   254  // doRunHook actually runs the hook that was requested.
   255  //
   256  // Note that this method is synchronous, as the task is already running in a
   257  // goroutine.
   258  func (m *HookManager) doRunHook(task *state.Task, tomb *tomb.Tomb) error {
   259  	task.State().Lock()
   260  	hooksup, snapst, err := hookSetup(task, "hook-setup")
   261  	task.State().Unlock()
   262  	if err != nil {
   263  		return fmt.Errorf("cannot extract hook setup from task: %s", err)
   264  	}
   265  
   266  	return m.runHookForTask(task, tomb, snapst, hooksup)
   267  }
   268  
   269  // undoRunHook runs the undo-hook that was requested.
   270  //
   271  // Note that this method is synchronous, as the task is already running in a
   272  // goroutine.
   273  func (m *HookManager) undoRunHook(task *state.Task, tomb *tomb.Tomb) error {
   274  	task.State().Lock()
   275  	hooksup, snapst, err := hookSetup(task, "undo-hook-setup")
   276  	task.State().Unlock()
   277  	if err != nil {
   278  		if err == state.ErrNoState {
   279  			// no undo hook setup
   280  			return nil
   281  		}
   282  		return fmt.Errorf("cannot extract undo hook setup from task: %s", err)
   283  	}
   284  
   285  	return m.runHookForTask(task, tomb, snapst, hooksup)
   286  }
   287  
   288  func (m *HookManager) EphemeralRunHook(ctx context.Context, hooksup *HookSetup, contextData map[string]interface{}) (*Context, error) {
   289  	var snapst snapstate.SnapState
   290  	m.state.Lock()
   291  	err := snapstate.Get(m.state, hooksup.Snap, &snapst)
   292  	m.state.Unlock()
   293  	if err != nil {
   294  		return nil, fmt.Errorf("cannot run ephemeral hook %q for snap %q: %v", hooksup.Hook, hooksup.Snap, err)
   295  	}
   296  
   297  	context, err := newEphemeralHookContextWithData(m.state, hooksup, contextData)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	tomb, _ := tomb.WithContext(ctx)
   303  	if err := m.runHook(context, &snapst, hooksup, tomb); err != nil {
   304  		return nil, err
   305  	}
   306  	return context, nil
   307  }
   308  
   309  func (m *HookManager) runHookForTask(task *state.Task, tomb *tomb.Tomb, snapst *snapstate.SnapState, hooksup *HookSetup) error {
   310  	context, err := NewContext(task, m.state, hooksup, nil, "")
   311  	if err != nil {
   312  		return err
   313  	}
   314  	return m.runHook(context, snapst, hooksup, tomb)
   315  }
   316  
   317  func (m *HookManager) runHook(context *Context, snapst *snapstate.SnapState, hooksup *HookSetup, tomb *tomb.Tomb) error {
   318  	mustHijack := m.hijacked(hooksup.Hook, hooksup.Snap) != nil
   319  	hookExists := false
   320  	if !mustHijack {
   321  		// not hijacked, snap must be installed
   322  		if !snapst.IsInstalled() {
   323  			return fmt.Errorf("cannot find %q snap", hooksup.Snap)
   324  		}
   325  
   326  		info, err := snapst.CurrentInfo()
   327  		if err != nil {
   328  			return fmt.Errorf("cannot read %q snap details: %v", hooksup.Snap, err)
   329  		}
   330  
   331  		hookExists = info.Hooks[hooksup.Hook] != nil
   332  		if !hookExists && !hooksup.Optional {
   333  			return fmt.Errorf("snap %q has no %q hook", hooksup.Snap, hooksup.Hook)
   334  		}
   335  	}
   336  
   337  	if hookExists || mustHijack {
   338  		// we will run something, not a noop
   339  		if ok, _ := m.state.Restarting(); ok {
   340  			// don't start running a hook if we are restarting
   341  			return &state.Retry{}
   342  		}
   343  
   344  		// keep count of running hooks
   345  		atomic.AddInt32(&m.runningHooks, 1)
   346  		defer atomic.AddInt32(&m.runningHooks, -1)
   347  	} else if !hooksup.Always {
   348  		// a noop with no 'always' flag: bail
   349  		return nil
   350  	}
   351  
   352  	// Obtain a handler for this hook. The repository returns a list since it's
   353  	// possible for regular expressions to overlap, but multiple handlers is an
   354  	// error (as is no handler).
   355  	handlers := m.repository.generateHandlers(context)
   356  	handlersCount := len(handlers)
   357  	if handlersCount == 0 {
   358  		// Do not report error if hook handler doesn't exist as long as the hook is optional.
   359  		// This is to avoid issues when downgrading to an old core snap that doesn't know about
   360  		// particular hook type and a task for it exists (e.g. "post-refresh" hook).
   361  		if hooksup.Optional {
   362  			return nil
   363  		}
   364  		return fmt.Errorf("internal error: no registered handlers for hook %q", hooksup.Hook)
   365  	}
   366  	if handlersCount > 1 {
   367  		return fmt.Errorf("internal error: %d handlers registered for hook %q, expected 1", handlersCount, hooksup.Hook)
   368  	}
   369  	context.handler = handlers[0]
   370  
   371  	contextID := context.ID()
   372  	m.contextsMutex.Lock()
   373  	m.contexts[contextID] = context
   374  	m.contextsMutex.Unlock()
   375  
   376  	defer func() {
   377  		m.contextsMutex.Lock()
   378  		delete(m.contexts, contextID)
   379  		m.contextsMutex.Unlock()
   380  	}()
   381  
   382  	if err := context.Handler().Before(); err != nil {
   383  		return err
   384  	}
   385  
   386  	// some hooks get hijacked, e.g. the core configuration
   387  	var err error
   388  	var output []byte
   389  	if f := m.hijacked(hooksup.Hook, hooksup.Snap); f != nil {
   390  		err = f(context)
   391  	} else if hookExists {
   392  		output, err = runHook(context, tomb)
   393  	}
   394  	if err != nil {
   395  		if hooksup.TrackError {
   396  			trackHookError(context, output, err)
   397  		}
   398  		err = osutil.OutputErr(output, err)
   399  		if hooksup.IgnoreError {
   400  			context.Lock()
   401  			context.Errorf("ignoring failure in hook %q: %v", hooksup.Hook, err)
   402  			context.Unlock()
   403  		} else {
   404  			ignoreOriginalErr, handlerErr := context.Handler().Error(err)
   405  			if handlerErr != nil {
   406  				return handlerErr
   407  			}
   408  			if ignoreOriginalErr {
   409  				return nil
   410  			}
   411  
   412  			return fmt.Errorf("run hook %q: %v", hooksup.Hook, err)
   413  		}
   414  	}
   415  
   416  	if err = context.Handler().Done(); err != nil {
   417  		return err
   418  	}
   419  
   420  	context.Lock()
   421  	defer context.Unlock()
   422  	if err = context.Done(); err != nil {
   423  		return err
   424  	}
   425  
   426  	return nil
   427  }
   428  
   429  func runHookImpl(c *Context, tomb *tomb.Tomb) ([]byte, error) {
   430  	return runHookAndWait(c.InstanceName(), c.SnapRevision(), c.HookName(), c.ID(), c.Timeout(), tomb)
   431  }
   432  
   433  var runHook = runHookImpl
   434  
   435  // MockRunHook mocks the actual invocation of hooks for tests.
   436  func MockRunHook(hookInvoke func(c *Context, tomb *tomb.Tomb) ([]byte, error)) (restore func()) {
   437  	oldRunHook := runHook
   438  	runHook = hookInvoke
   439  	return func() {
   440  		runHook = oldRunHook
   441  	}
   442  }
   443  
   444  var osReadlink = os.Readlink
   445  
   446  // snapCmd returns the "snap" command to run. If snapd is re-execed
   447  // it will be the snap command from the core snap, otherwise it will
   448  // be the system "snap" command (c.f. LP: #1668738).
   449  func snapCmd() string {
   450  	// sensible default, assume PATH is correct
   451  	snapCmd := "snap"
   452  
   453  	exe, err := osReadlink("/proc/self/exe")
   454  	if err != nil {
   455  		logger.Noticef("cannot read /proc/self/exe: %v, using default snap command", err)
   456  		return snapCmd
   457  	}
   458  	if !strings.HasPrefix(exe, dirs.SnapMountDir) {
   459  		return snapCmd
   460  	}
   461  
   462  	// snap is running from the core snap, we know the relative
   463  	// location of "snap" from "snapd"
   464  	return filepath.Join(filepath.Dir(exe), "../../bin/snap")
   465  }
   466  
   467  var defaultHookTimeout = 10 * time.Minute
   468  
   469  func runHookAndWait(snapName string, revision snap.Revision, hookName, hookContext string, timeout time.Duration, tomb *tomb.Tomb) ([]byte, error) {
   470  	argv := []string{snapCmd(), "run", "--hook", hookName, "-r", revision.String(), snapName}
   471  	if timeout == 0 {
   472  		timeout = defaultHookTimeout
   473  	}
   474  
   475  	env := []string{
   476  		// Make sure the hook has its context defined so it can
   477  		// communicate via the REST API.
   478  		fmt.Sprintf("SNAP_COOKIE=%s", hookContext),
   479  		// Set SNAP_CONTEXT too for compatibility with old snapctl
   480  		// binary when transitioning to a new core - otherwise configure
   481  		// hook would fail during transition.
   482  		fmt.Sprintf("SNAP_CONTEXT=%s", hookContext),
   483  	}
   484  
   485  	return osutil.RunAndWait(argv, env, timeout, tomb)
   486  }
   487  
   488  var errtrackerReport = errtracker.Report
   489  
   490  func trackHookError(context *Context, output []byte, err error) {
   491  	errmsg := fmt.Sprintf("hook %s in snap %q failed: %v", context.HookName(), context.InstanceName(), osutil.OutputErr(output, err))
   492  	dupSig := fmt.Sprintf("hook:%s:%s:%s\n%s", context.InstanceName(), context.HookName(), err, output)
   493  	extra := map[string]string{
   494  		"HookName": context.HookName(),
   495  	}
   496  	if context.setup.IgnoreError {
   497  		extra["IgnoreError"] = "1"
   498  	}
   499  
   500  	context.state.Lock()
   501  	problemReportsDisabled := settings.ProblemReportsDisabled(context.state)
   502  	context.state.Unlock()
   503  	if !problemReportsDisabled {
   504  		oopsid, err := errtrackerReport(context.InstanceName(), errmsg, dupSig, extra)
   505  		if err == nil {
   506  			logger.Noticef("Reported hook failure from %q for snap %q as %s", context.HookName(), context.InstanceName(), oopsid)
   507  		} else {
   508  			logger.Debugf("Cannot report hook failure: %s", err)
   509  		}
   510  	}
   511  }