gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/snapstate/storehelpers.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2018 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 snapstate
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"sort"
    26  
    27  	"github.com/snapcore/snapd/asserts/snapasserts"
    28  	"github.com/snapcore/snapd/logger"
    29  	"github.com/snapcore/snapd/overlord/auth"
    30  	"github.com/snapcore/snapd/overlord/state"
    31  	"github.com/snapcore/snapd/snap"
    32  	"github.com/snapcore/snapd/store"
    33  	"github.com/snapcore/snapd/strutil"
    34  )
    35  
    36  var currentSnaps = currentSnapsImpl
    37  
    38  // EnforcedValidationSets allows to hook getting of validation sets in enforce
    39  // mode into installation/refresh/removal of snaps. It gets hooked from
    40  // assertstate.
    41  var EnforcedValidationSets func(st *state.State) (*snapasserts.ValidationSets, error)
    42  
    43  func userIDForSnap(st *state.State, snapst *SnapState, fallbackUserID int) (int, error) {
    44  	userID := snapst.UserID
    45  	_, err := auth.User(st, userID)
    46  	if err == nil {
    47  		return userID, nil
    48  	}
    49  	if err != auth.ErrInvalidUser {
    50  		return 0, err
    51  	}
    52  	return fallbackUserID, nil
    53  }
    54  
    55  // userFromUserID returns the first valid user from a series of userIDs
    56  // used as successive fallbacks.
    57  func userFromUserID(st *state.State, userIDs ...int) (*auth.UserState, error) {
    58  	var user *auth.UserState
    59  	var err error
    60  	for _, userID := range userIDs {
    61  		if userID == 0 {
    62  			err = nil
    63  			continue
    64  		}
    65  		user, err = auth.User(st, userID)
    66  		if err != auth.ErrInvalidUser {
    67  			break
    68  		}
    69  	}
    70  	return user, err
    71  }
    72  
    73  func refreshOptions(st *state.State, origOpts *store.RefreshOptions) (*store.RefreshOptions, error) {
    74  	var opts store.RefreshOptions
    75  
    76  	if origOpts != nil {
    77  		if origOpts.PrivacyKey != "" {
    78  			// nothing to add
    79  			return origOpts, nil
    80  		}
    81  		opts = *origOpts
    82  	}
    83  
    84  	if err := st.Get("refresh-privacy-key", &opts.PrivacyKey); err != nil && err != state.ErrNoState {
    85  		return nil, fmt.Errorf("cannot obtain store request salt: %v", err)
    86  	}
    87  	if opts.PrivacyKey == "" {
    88  		return nil, fmt.Errorf("internal error: request salt is unset")
    89  	}
    90  	return &opts, nil
    91  }
    92  
    93  // installSize returns total download size of snaps and their prerequisites
    94  // (bases and default content providers), querying the store as neccessarry,
    95  // potentially more than once. It assumes the initial list of snaps already has
    96  // download infos set.
    97  // The state must be locked by the caller.
    98  var installSize = func(st *state.State, snaps []minimalInstallInfo, userID int) (uint64, error) {
    99  	curSnaps, err := currentSnaps(st)
   100  	if err != nil {
   101  		return 0, err
   102  	}
   103  
   104  	user, err := userFromUserID(st, userID)
   105  	if err != nil {
   106  		return 0, err
   107  	}
   108  
   109  	accountedSnaps := map[string]bool{}
   110  	for _, snap := range curSnaps {
   111  		accountedSnaps[snap.InstanceName] = true
   112  	}
   113  
   114  	var prereqs []string
   115  
   116  	resolveBaseAndContentProviders := func(inst minimalInstallInfo) {
   117  		if inst.Type() != snap.TypeApp {
   118  			return
   119  		}
   120  		if inst.SnapBase() != "none" {
   121  			base := defaultCoreSnapName
   122  			if inst.SnapBase() != "" {
   123  				base = inst.SnapBase()
   124  			}
   125  			if !accountedSnaps[base] {
   126  				prereqs = append(prereqs, base)
   127  				accountedSnaps[base] = true
   128  			}
   129  		}
   130  		for _, snapName := range inst.Prereq(st) {
   131  			if !accountedSnaps[snapName] {
   132  				prereqs = append(prereqs, snapName)
   133  				accountedSnaps[snapName] = true
   134  			}
   135  		}
   136  	}
   137  
   138  	snapSizes := map[string]uint64{}
   139  	for _, inst := range snaps {
   140  		if inst.DownloadSize() == 0 {
   141  			return 0, fmt.Errorf("internal error: download info missing for %q", inst.InstanceName())
   142  		}
   143  		snapSizes[inst.InstanceName()] = uint64(inst.DownloadSize())
   144  		resolveBaseAndContentProviders(inst)
   145  	}
   146  
   147  	opts, err := refreshOptions(st, nil)
   148  	if err != nil {
   149  		return 0, err
   150  	}
   151  
   152  	theStore := Store(st, nil)
   153  	channel := defaultPrereqSnapsChannel()
   154  
   155  	// this can potentially be executed multiple times if we (recursively)
   156  	// find new prerequisites or bases.
   157  	for len(prereqs) > 0 {
   158  		actions := []*store.SnapAction{}
   159  		for _, prereq := range prereqs {
   160  			action := &store.SnapAction{
   161  				Action:       "install",
   162  				InstanceName: prereq,
   163  				Channel:      channel,
   164  			}
   165  			actions = append(actions, action)
   166  		}
   167  
   168  		// calls to the store should be done without holding the state lock
   169  		st.Unlock()
   170  		results, _, err := theStore.SnapAction(context.TODO(), curSnaps, actions, nil, user, opts)
   171  		st.Lock()
   172  		if err != nil {
   173  			return 0, err
   174  		}
   175  		prereqs = []string{}
   176  		for _, res := range results {
   177  			snapSizes[res.InstanceName()] = uint64(res.Size)
   178  			// results may have new base or content providers
   179  			resolveBaseAndContentProviders(installSnapInfo{res.Info})
   180  		}
   181  	}
   182  
   183  	// state is locked at this point
   184  
   185  	// since we unlock state above when querying store, other changes may affect
   186  	// same snaps, therefore obtain current snaps again and only compute total
   187  	// size of snaps that would actually need to be installed.
   188  	curSnaps, err = currentSnaps(st)
   189  	if err != nil {
   190  		return 0, err
   191  	}
   192  	for _, snap := range curSnaps {
   193  		delete(snapSizes, snap.InstanceName)
   194  	}
   195  
   196  	var total uint64
   197  	for _, sz := range snapSizes {
   198  		total += sz
   199  	}
   200  
   201  	return total, nil
   202  }
   203  
   204  func installInfo(ctx context.Context, st *state.State, name string, revOpts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (store.SnapActionResult, error) {
   205  	// TODO: support ignore-validation
   206  
   207  	curSnaps, err := currentSnaps(st)
   208  	if err != nil {
   209  		return store.SnapActionResult{}, err
   210  	}
   211  
   212  	user, err := userFromUserID(st, userID)
   213  	if err != nil {
   214  		return store.SnapActionResult{}, err
   215  	}
   216  
   217  	opts, err := refreshOptions(st, nil)
   218  	if err != nil {
   219  		return store.SnapActionResult{}, err
   220  	}
   221  
   222  	action := &store.SnapAction{
   223  		Action:       "install",
   224  		InstanceName: name,
   225  	}
   226  
   227  	// cannot specify both with the API
   228  	if revOpts.Revision.Unset() {
   229  		// the desired channel
   230  		action.Channel = revOpts.Channel
   231  		// the desired cohort key
   232  		action.CohortKey = revOpts.CohortKey
   233  	} else {
   234  		// the desired revision
   235  		action.Revision = revOpts.Revision
   236  	}
   237  
   238  	theStore := Store(st, deviceCtx)
   239  	st.Unlock() // calls to the store should be done without holding the state lock
   240  	res, _, err := theStore.SnapAction(ctx, curSnaps, []*store.SnapAction{action}, nil, user, opts)
   241  	st.Lock()
   242  
   243  	return singleActionResult(name, action.Action, res, err)
   244  }
   245  
   246  func updateInfo(st *state.State, snapst *SnapState, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*snap.Info, error) {
   247  	curSnaps, err := currentSnaps(st)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	refreshOpts, err := refreshOptions(st, nil)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	curInfo, user, err := preUpdateInfo(st, snapst, flags.Amend, userID)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	var storeFlags store.SnapActionFlags
   263  	if flags.IgnoreValidation {
   264  		storeFlags = store.SnapActionIgnoreValidation
   265  	} else {
   266  		storeFlags = store.SnapActionEnforceValidation
   267  	}
   268  
   269  	action := &store.SnapAction{
   270  		Action:       "refresh",
   271  		InstanceName: curInfo.InstanceName(),
   272  		SnapID:       curInfo.SnapID,
   273  		// the desired channel
   274  		Channel:   opts.Channel,
   275  		CohortKey: opts.CohortKey,
   276  		Flags:     storeFlags,
   277  	}
   278  
   279  	if curInfo.SnapID == "" { // amend
   280  		action.Action = "install"
   281  		action.Epoch = curInfo.Epoch
   282  	}
   283  
   284  	theStore := Store(st, deviceCtx)
   285  	st.Unlock() // calls to the store should be done without holding the state lock
   286  	res, _, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, nil, user, refreshOpts)
   287  	st.Lock()
   288  
   289  	sar, err := singleActionResult(curInfo.InstanceName(), action.Action, res, err)
   290  	return sar.Info, err
   291  }
   292  
   293  func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (*snap.Info, *auth.UserState, error) {
   294  	user, err := userFromUserID(st, snapst.UserID, userID)
   295  	if err != nil {
   296  		return nil, nil, err
   297  	}
   298  
   299  	curInfo, err := snapst.CurrentInfo()
   300  	if err != nil {
   301  		return nil, nil, err
   302  	}
   303  
   304  	if curInfo.SnapID == "" { // covers also trymode
   305  		if !amend {
   306  			return nil, nil, store.ErrLocalSnap
   307  		}
   308  	}
   309  
   310  	return curInfo, user, nil
   311  }
   312  
   313  var ErrMissingExpectedResult = fmt.Errorf("unexpectedly empty response from the server (try again later)")
   314  
   315  func singleActionResult(name, action string, results []store.SnapActionResult, e error) (store.SnapActionResult, error) {
   316  	if len(results) > 1 {
   317  		return store.SnapActionResult{}, fmt.Errorf("internal error: multiple store results for a single snap op")
   318  	}
   319  	if len(results) > 0 {
   320  		// TODO: if we also have an error log/warn about it
   321  		return results[0], nil
   322  	}
   323  
   324  	if saErr, ok := e.(*store.SnapActionError); ok {
   325  		if len(saErr.Other) != 0 {
   326  			return store.SnapActionResult{}, saErr
   327  		}
   328  
   329  		var snapErr error
   330  		switch action {
   331  		case "refresh":
   332  			snapErr = saErr.Refresh[name]
   333  		case "install":
   334  			snapErr = saErr.Install[name]
   335  		}
   336  		if snapErr != nil {
   337  			return store.SnapActionResult{}, snapErr
   338  		}
   339  
   340  		// no result, atypical case
   341  		if saErr.NoResults {
   342  			return store.SnapActionResult{}, ErrMissingExpectedResult
   343  		}
   344  	}
   345  
   346  	return store.SnapActionResult{}, e
   347  }
   348  
   349  func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int, deviceCtx DeviceContext) (*snap.Info, error) {
   350  	// TODO: support ignore-validation?
   351  
   352  	curSnaps, err := currentSnaps(st)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  
   357  	curInfo, user, err := preUpdateInfo(st, snapst, false, userID)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  
   362  	opts, err := refreshOptions(st, nil)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  
   367  	action := &store.SnapAction{
   368  		Action:       "refresh",
   369  		SnapID:       curInfo.SnapID,
   370  		InstanceName: curInfo.InstanceName(),
   371  		// the desired revision
   372  		Revision: revision,
   373  	}
   374  
   375  	theStore := Store(st, deviceCtx)
   376  	st.Unlock() // calls to the store should be done without holding the state lock
   377  	res, _, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, nil, user, opts)
   378  	st.Lock()
   379  
   380  	sar, err := singleActionResult(curInfo.InstanceName(), action.Action, res, err)
   381  	return sar.Info, err
   382  }
   383  
   384  func currentSnapsImpl(st *state.State) ([]*store.CurrentSnap, error) {
   385  	snapStates, err := All(st)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  
   390  	if len(snapStates) == 0 {
   391  		// no snaps installed, do not bother any further
   392  		return nil, nil
   393  	}
   394  
   395  	curSnaps := collectCurrentSnaps(snapStates, nil)
   396  	return curSnaps, nil
   397  }
   398  
   399  func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState)) (curSnaps []*store.CurrentSnap) {
   400  	curSnaps = make([]*store.CurrentSnap, 0, len(snapStates))
   401  
   402  	for _, snapst := range snapStates {
   403  		if snapst.TryMode {
   404  			// try mode snaps are completely local and
   405  			// irrelevant for the operation
   406  			continue
   407  		}
   408  
   409  		snapInfo, err := snapst.CurrentInfo()
   410  		if err != nil {
   411  			continue
   412  		}
   413  
   414  		if snapInfo.SnapID == "" {
   415  			// the store won't be able to tell what this
   416  			// is and so cannot include it in the
   417  			// operation
   418  			continue
   419  		}
   420  
   421  		installed := &store.CurrentSnap{
   422  			InstanceName: snapInfo.InstanceName(),
   423  			SnapID:       snapInfo.SnapID,
   424  			// the desired channel (not snapInfo.Channel!)
   425  			TrackingChannel:  snapst.TrackingChannel,
   426  			Revision:         snapInfo.Revision,
   427  			RefreshedDate:    revisionDate(snapInfo),
   428  			IgnoreValidation: snapst.IgnoreValidation,
   429  			Epoch:            snapInfo.Epoch,
   430  			CohortKey:        snapst.CohortKey,
   431  		}
   432  		curSnaps = append(curSnaps, installed)
   433  
   434  		if consider != nil {
   435  			consider(installed, snapst)
   436  		}
   437  	}
   438  
   439  	return curSnaps
   440  }
   441  
   442  func refreshCandidates(ctx context.Context, st *state.State, names []string, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, map[string]*SnapState, map[string]bool, error) {
   443  	snapStates, err := All(st)
   444  	if err != nil {
   445  		return nil, nil, nil, err
   446  	}
   447  
   448  	opts, err = refreshOptions(st, opts)
   449  	if err != nil {
   450  		return nil, nil, nil, err
   451  	}
   452  
   453  	// check if we have this name at all
   454  	for _, name := range names {
   455  		if _, ok := snapStates[name]; !ok {
   456  			return nil, nil, nil, snap.NotInstalledError{Snap: name}
   457  		}
   458  	}
   459  
   460  	sort.Strings(names)
   461  
   462  	var fallbackID int
   463  	// normalize fallback user
   464  	if !user.HasStoreAuth() {
   465  		user = nil
   466  	} else {
   467  		fallbackID = user.ID
   468  	}
   469  
   470  	actionsByUserID := make(map[int][]*store.SnapAction)
   471  	stateByInstanceName := make(map[string]*SnapState, len(snapStates))
   472  	ignoreValidationByInstanceName := make(map[string]bool)
   473  	nCands := 0
   474  
   475  	addCand := func(installed *store.CurrentSnap, snapst *SnapState) {
   476  		// FIXME: snaps that are not active are skipped for now
   477  		//        until we know what we want to do
   478  		if !snapst.Active {
   479  			return
   480  		}
   481  
   482  		if len(names) == 0 && snapst.DevMode {
   483  			// no auto-refresh for devmode
   484  			return
   485  		}
   486  
   487  		if len(names) > 0 && !strutil.SortedListContains(names, installed.InstanceName) {
   488  			return
   489  		}
   490  
   491  		stateByInstanceName[installed.InstanceName] = snapst
   492  
   493  		if len(names) == 0 {
   494  			installed.Block = snapst.Block()
   495  		}
   496  
   497  		userID := snapst.UserID
   498  		if userID == 0 {
   499  			userID = fallbackID
   500  		}
   501  		actionsByUserID[userID] = append(actionsByUserID[userID], &store.SnapAction{
   502  			Action:       "refresh",
   503  			SnapID:       installed.SnapID,
   504  			InstanceName: installed.InstanceName,
   505  		})
   506  		if snapst.IgnoreValidation {
   507  			ignoreValidationByInstanceName[installed.InstanceName] = true
   508  		}
   509  		nCands++
   510  	}
   511  	// determine current snaps and collect candidates for refresh
   512  	curSnaps := collectCurrentSnaps(snapStates, addCand)
   513  
   514  	actionsForUser := make(map[*auth.UserState][]*store.SnapAction, len(actionsByUserID))
   515  	noUserActions := actionsByUserID[0]
   516  	for userID, actions := range actionsByUserID {
   517  		if userID == 0 {
   518  			continue
   519  		}
   520  		u, err := userFromUserID(st, userID, 0)
   521  		if err != nil {
   522  			return nil, nil, nil, err
   523  		}
   524  		if u.HasStoreAuth() {
   525  			actionsForUser[u] = actions
   526  		} else {
   527  			noUserActions = append(noUserActions, actions...)
   528  		}
   529  	}
   530  	// coalesce if possible
   531  	if len(noUserActions) != 0 {
   532  		if len(actionsForUser) == 0 {
   533  			actionsForUser[nil] = noUserActions
   534  		} else {
   535  			// coalesce no user actions with one other user's
   536  			for u1, actions := range actionsForUser {
   537  				actionsForUser[u1] = append(actions, noUserActions...)
   538  				break
   539  			}
   540  		}
   541  	}
   542  
   543  	// TODO: possibly support a deviceCtx
   544  	theStore := Store(st, nil)
   545  
   546  	updates := make([]*snap.Info, 0, nCands)
   547  	for u, actions := range actionsForUser {
   548  		st.Unlock()
   549  		sarsForUser, _, err := theStore.SnapAction(ctx, curSnaps, actions, nil, u, opts)
   550  		st.Lock()
   551  		if err != nil {
   552  			saErr, ok := err.(*store.SnapActionError)
   553  			if !ok {
   554  				return nil, nil, nil, err
   555  			}
   556  			// TODO: use the warning infra here when we have it
   557  			logger.Noticef("%v", saErr)
   558  		}
   559  
   560  		for _, sar := range sarsForUser {
   561  			updates = append(updates, sar.Info)
   562  		}
   563  	}
   564  
   565  	return updates, stateByInstanceName, ignoreValidationByInstanceName, nil
   566  }
   567  
   568  func installCandidates(st *state.State, names []string, channel string, user *auth.UserState) ([]store.SnapActionResult, error) {
   569  	curSnaps, err := currentSnaps(st)
   570  	if err != nil {
   571  		return nil, err
   572  	}
   573  
   574  	opts, err := refreshOptions(st, nil)
   575  	if err != nil {
   576  		return nil, err
   577  	}
   578  
   579  	actions := make([]*store.SnapAction, len(names))
   580  	for i, name := range names {
   581  		actions[i] = &store.SnapAction{
   582  			Action:       "install",
   583  			InstanceName: name,
   584  			// the desired channel
   585  			Channel: channel,
   586  		}
   587  	}
   588  
   589  	// TODO: possibly support a deviceCtx
   590  	theStore := Store(st, nil)
   591  	st.Unlock() // calls to the store should be done without holding the state lock
   592  	defer st.Lock()
   593  	results, _, err := theStore.SnapAction(context.TODO(), curSnaps, actions, nil, user, opts)
   594  	return results, err
   595  }