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