github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  func userIDForSnap(st *state.State, snapst *SnapState, fallbackUserID int) (int, error) {
    36  	userID := snapst.UserID
    37  	_, err := auth.User(st, userID)
    38  	if err == nil {
    39  		return userID, nil
    40  	}
    41  	if err != auth.ErrInvalidUser {
    42  		return 0, err
    43  	}
    44  	return fallbackUserID, nil
    45  }
    46  
    47  // userFromUserID returns the first valid user from a series of userIDs
    48  // used as successive fallbacks.
    49  func userFromUserID(st *state.State, userIDs ...int) (*auth.UserState, error) {
    50  	var user *auth.UserState
    51  	var err error
    52  	for _, userID := range userIDs {
    53  		if userID == 0 {
    54  			err = nil
    55  			continue
    56  		}
    57  		user, err = auth.User(st, userID)
    58  		if err != auth.ErrInvalidUser {
    59  			break
    60  		}
    61  	}
    62  	return user, err
    63  }
    64  
    65  func refreshOptions(st *state.State, origOpts *store.RefreshOptions) (*store.RefreshOptions, error) {
    66  	var opts store.RefreshOptions
    67  
    68  	if origOpts != nil {
    69  		if origOpts.PrivacyKey != "" {
    70  			// nothing to add
    71  			return origOpts, nil
    72  		}
    73  		opts = *origOpts
    74  	}
    75  
    76  	if err := st.Get("refresh-privacy-key", &opts.PrivacyKey); err != nil && err != state.ErrNoState {
    77  		return nil, fmt.Errorf("cannot obtain store request salt: %v", err)
    78  	}
    79  	if opts.PrivacyKey == "" {
    80  		return nil, fmt.Errorf("internal error: request salt is unset")
    81  	}
    82  	return &opts, nil
    83  }
    84  
    85  func installInfo(ctx context.Context, st *state.State, name string, revOpts *RevisionOptions, userID int, deviceCtx DeviceContext) (*snap.Info, error) {
    86  	// TODO: support ignore-validation?
    87  
    88  	curSnaps, err := currentSnaps(st)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	user, err := userFromUserID(st, userID)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	opts, err := refreshOptions(st, nil)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	action := &store.SnapAction{
   104  		Action:       "install",
   105  		InstanceName: name,
   106  	}
   107  
   108  	// cannot specify both with the API
   109  	if revOpts.Revision.Unset() {
   110  		// the desired channel
   111  		action.Channel = revOpts.Channel
   112  		// the desired cohort key
   113  		action.CohortKey = revOpts.CohortKey
   114  	} else {
   115  		// the desired revision
   116  		action.Revision = revOpts.Revision
   117  	}
   118  
   119  	theStore := Store(st, deviceCtx)
   120  	st.Unlock() // calls to the store should be done without holding the state lock
   121  	res, err := theStore.SnapAction(ctx, curSnaps, []*store.SnapAction{action}, user, opts)
   122  	st.Lock()
   123  
   124  	return singleActionResult(name, action.Action, res, err)
   125  }
   126  
   127  func updateInfo(st *state.State, snapst *SnapState, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*snap.Info, error) {
   128  	curSnaps, err := currentSnaps(st)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	refreshOpts, err := refreshOptions(st, nil)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	curInfo, user, err := preUpdateInfo(st, snapst, flags.Amend, userID)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	var storeFlags store.SnapActionFlags
   144  	if flags.IgnoreValidation {
   145  		storeFlags = store.SnapActionIgnoreValidation
   146  	} else {
   147  		storeFlags = store.SnapActionEnforceValidation
   148  	}
   149  
   150  	action := &store.SnapAction{
   151  		Action:       "refresh",
   152  		InstanceName: curInfo.InstanceName(),
   153  		SnapID:       curInfo.SnapID,
   154  		// the desired channel
   155  		Channel:   opts.Channel,
   156  		CohortKey: opts.CohortKey,
   157  		Flags:     storeFlags,
   158  	}
   159  
   160  	if curInfo.SnapID == "" { // amend
   161  		action.Action = "install"
   162  		action.Epoch = curInfo.Epoch
   163  	}
   164  
   165  	theStore := Store(st, deviceCtx)
   166  	st.Unlock() // calls to the store should be done without holding the state lock
   167  	res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, refreshOpts)
   168  	st.Lock()
   169  
   170  	return singleActionResult(curInfo.InstanceName(), action.Action, res, err)
   171  }
   172  
   173  func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (*snap.Info, *auth.UserState, error) {
   174  	user, err := userFromUserID(st, snapst.UserID, userID)
   175  	if err != nil {
   176  		return nil, nil, err
   177  	}
   178  
   179  	curInfo, err := snapst.CurrentInfo()
   180  	if err != nil {
   181  		return nil, nil, err
   182  	}
   183  
   184  	if curInfo.SnapID == "" { // covers also trymode
   185  		if !amend {
   186  			return nil, nil, store.ErrLocalSnap
   187  		}
   188  	}
   189  
   190  	return curInfo, user, nil
   191  }
   192  
   193  var ErrMissingExpectedResult = fmt.Errorf("unexpectedly empty response from the server (try again later)")
   194  
   195  func singleActionResult(name, action string, results []*snap.Info, e error) (info *snap.Info, err error) {
   196  	if len(results) > 1 {
   197  		return nil, fmt.Errorf("internal error: multiple store results for a single snap op")
   198  	}
   199  	if len(results) > 0 {
   200  		// TODO: if we also have an error log/warn about it
   201  		return results[0], nil
   202  	}
   203  
   204  	if saErr, ok := e.(*store.SnapActionError); ok {
   205  		if len(saErr.Other) != 0 {
   206  			return nil, saErr
   207  		}
   208  
   209  		var snapErr error
   210  		switch action {
   211  		case "refresh":
   212  			snapErr = saErr.Refresh[name]
   213  		case "install":
   214  			snapErr = saErr.Install[name]
   215  		}
   216  		if snapErr != nil {
   217  			return nil, snapErr
   218  		}
   219  
   220  		// no result, atypical case
   221  		if saErr.NoResults {
   222  			return nil, ErrMissingExpectedResult
   223  		}
   224  	}
   225  
   226  	return nil, e
   227  }
   228  
   229  func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int, deviceCtx DeviceContext) (*snap.Info, error) {
   230  	// TODO: support ignore-validation?
   231  
   232  	curSnaps, err := currentSnaps(st)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	curInfo, user, err := preUpdateInfo(st, snapst, false, userID)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	opts, err := refreshOptions(st, nil)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	action := &store.SnapAction{
   248  		Action:       "refresh",
   249  		SnapID:       curInfo.SnapID,
   250  		InstanceName: curInfo.InstanceName(),
   251  		// the desired revision
   252  		Revision: revision,
   253  	}
   254  
   255  	theStore := Store(st, deviceCtx)
   256  	st.Unlock() // calls to the store should be done without holding the state lock
   257  	res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, opts)
   258  	st.Lock()
   259  
   260  	return singleActionResult(curInfo.InstanceName(), action.Action, res, err)
   261  }
   262  
   263  func currentSnaps(st *state.State) ([]*store.CurrentSnap, error) {
   264  	snapStates, err := All(st)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	if len(snapStates) == 0 {
   270  		// no snaps installed, do not bother any further
   271  		return nil, nil
   272  	}
   273  
   274  	curSnaps := collectCurrentSnaps(snapStates, nil)
   275  	return curSnaps, nil
   276  }
   277  
   278  func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState)) (curSnaps []*store.CurrentSnap) {
   279  	curSnaps = make([]*store.CurrentSnap, 0, len(snapStates))
   280  
   281  	for _, snapst := range snapStates {
   282  		if snapst.TryMode {
   283  			// try mode snaps are completely local and
   284  			// irrelevant for the operation
   285  			continue
   286  		}
   287  
   288  		snapInfo, err := snapst.CurrentInfo()
   289  		if err != nil {
   290  			continue
   291  		}
   292  
   293  		if snapInfo.SnapID == "" {
   294  			// the store won't be able to tell what this
   295  			// is and so cannot include it in the
   296  			// operation
   297  			continue
   298  		}
   299  
   300  		installed := &store.CurrentSnap{
   301  			InstanceName: snapInfo.InstanceName(),
   302  			SnapID:       snapInfo.SnapID,
   303  			// the desired channel (not snapInfo.Channel!)
   304  			TrackingChannel:  snapst.Channel,
   305  			Revision:         snapInfo.Revision,
   306  			RefreshedDate:    revisionDate(snapInfo),
   307  			IgnoreValidation: snapst.IgnoreValidation,
   308  			Epoch:            snapInfo.Epoch,
   309  			CohortKey:        snapst.CohortKey,
   310  		}
   311  		curSnaps = append(curSnaps, installed)
   312  
   313  		if consider != nil {
   314  			consider(installed, snapst)
   315  		}
   316  	}
   317  
   318  	return curSnaps
   319  }
   320  
   321  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) {
   322  	snapStates, err := All(st)
   323  	if err != nil {
   324  		return nil, nil, nil, err
   325  	}
   326  
   327  	opts, err = refreshOptions(st, opts)
   328  	if err != nil {
   329  		return nil, nil, nil, err
   330  	}
   331  
   332  	// check if we have this name at all
   333  	for _, name := range names {
   334  		if _, ok := snapStates[name]; !ok {
   335  			return nil, nil, nil, snap.NotInstalledError{Snap: name}
   336  		}
   337  	}
   338  
   339  	sort.Strings(names)
   340  
   341  	var fallbackID int
   342  	// normalize fallback user
   343  	if !user.HasStoreAuth() {
   344  		user = nil
   345  	} else {
   346  		fallbackID = user.ID
   347  	}
   348  
   349  	actionsByUserID := make(map[int][]*store.SnapAction)
   350  	stateByInstanceName := make(map[string]*SnapState, len(snapStates))
   351  	ignoreValidationByInstanceName := make(map[string]bool)
   352  	nCands := 0
   353  
   354  	addCand := func(installed *store.CurrentSnap, snapst *SnapState) {
   355  		// FIXME: snaps that are not active are skipped for now
   356  		//        until we know what we want to do
   357  		if !snapst.Active {
   358  			return
   359  		}
   360  
   361  		if len(names) == 0 && snapst.DevMode {
   362  			// no auto-refresh for devmode
   363  			return
   364  		}
   365  
   366  		if len(names) > 0 && !strutil.SortedListContains(names, installed.InstanceName) {
   367  			return
   368  		}
   369  
   370  		stateByInstanceName[installed.InstanceName] = snapst
   371  
   372  		if len(names) == 0 {
   373  			installed.Block = snapst.Block()
   374  		}
   375  
   376  		userID := snapst.UserID
   377  		if userID == 0 {
   378  			userID = fallbackID
   379  		}
   380  		actionsByUserID[userID] = append(actionsByUserID[userID], &store.SnapAction{
   381  			Action:       "refresh",
   382  			SnapID:       installed.SnapID,
   383  			InstanceName: installed.InstanceName,
   384  		})
   385  		if snapst.IgnoreValidation {
   386  			ignoreValidationByInstanceName[installed.InstanceName] = true
   387  		}
   388  		nCands++
   389  	}
   390  	// determine current snaps and collect candidates for refresh
   391  	curSnaps := collectCurrentSnaps(snapStates, addCand)
   392  
   393  	actionsForUser := make(map[*auth.UserState][]*store.SnapAction, len(actionsByUserID))
   394  	noUserActions := actionsByUserID[0]
   395  	for userID, actions := range actionsByUserID {
   396  		if userID == 0 {
   397  			continue
   398  		}
   399  		u, err := userFromUserID(st, userID, 0)
   400  		if err != nil {
   401  			return nil, nil, nil, err
   402  		}
   403  		if u.HasStoreAuth() {
   404  			actionsForUser[u] = actions
   405  		} else {
   406  			noUserActions = append(noUserActions, actions...)
   407  		}
   408  	}
   409  	// coalesce if possible
   410  	if len(noUserActions) != 0 {
   411  		if len(actionsForUser) == 0 {
   412  			actionsForUser[nil] = noUserActions
   413  		} else {
   414  			// coalesce no user actions with one other user's
   415  			for u1, actions := range actionsForUser {
   416  				actionsForUser[u1] = append(actions, noUserActions...)
   417  				break
   418  			}
   419  		}
   420  	}
   421  
   422  	// TODO: possibly support a deviceCtx
   423  	theStore := Store(st, nil)
   424  
   425  	updates := make([]*snap.Info, 0, nCands)
   426  	for u, actions := range actionsForUser {
   427  		st.Unlock()
   428  		updatesForUser, err := theStore.SnapAction(ctx, curSnaps, actions, u, opts)
   429  		st.Lock()
   430  		if err != nil {
   431  			saErr, ok := err.(*store.SnapActionError)
   432  			if !ok {
   433  				return nil, nil, nil, err
   434  			}
   435  			// TODO: use the warning infra here when we have it
   436  			logger.Noticef("%v", saErr)
   437  		}
   438  
   439  		updates = append(updates, updatesForUser...)
   440  	}
   441  
   442  	return updates, stateByInstanceName, ignoreValidationByInstanceName, nil
   443  }
   444  
   445  func installCandidates(st *state.State, names []string, channel string, user *auth.UserState) ([]*snap.Info, error) {
   446  	curSnaps, err := currentSnaps(st)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  
   451  	opts, err := refreshOptions(st, nil)
   452  	if err != nil {
   453  		return nil, err
   454  	}
   455  
   456  	actions := make([]*store.SnapAction, len(names))
   457  	for i, name := range names {
   458  		actions[i] = &store.SnapAction{
   459  			Action:       "install",
   460  			InstanceName: name,
   461  			// the desired channel
   462  			Channel: channel,
   463  		}
   464  	}
   465  
   466  	// TODO: possibly support a deviceCtx
   467  	theStore := Store(st, nil)
   468  	st.Unlock() // calls to the store should be done without holding the state lock
   469  	defer st.Lock()
   470  	return theStore.SnapAction(context.TODO(), curSnaps, actions, user, opts)
   471  }