github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/store/store_action.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 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 store has support to use the Ubuntu Store for querying and downloading of snaps, and the related services.
    21  package store
    22  
    23  import (
    24  	"context"
    25  	"crypto"
    26  	"encoding/base64"
    27  	"encoding/json"
    28  	"fmt"
    29  	"time"
    30  
    31  	"github.com/snapcore/snapd/asserts"
    32  	"github.com/snapcore/snapd/jsonutil"
    33  	"github.com/snapcore/snapd/logger"
    34  	"github.com/snapcore/snapd/overlord/auth"
    35  	"github.com/snapcore/snapd/snap"
    36  )
    37  
    38  type RefreshOptions struct {
    39  	// RefreshManaged indicates to the store that the refresh is
    40  	// managed via snapd-control.
    41  	RefreshManaged bool
    42  	IsAutoRefresh  bool
    43  
    44  	PrivacyKey string
    45  }
    46  
    47  // snap action: install/refresh
    48  
    49  type CurrentSnap struct {
    50  	InstanceName     string
    51  	SnapID           string
    52  	Revision         snap.Revision
    53  	TrackingChannel  string
    54  	RefreshedDate    time.Time
    55  	IgnoreValidation bool
    56  	Block            []snap.Revision
    57  	Epoch            snap.Epoch
    58  	CohortKey        string
    59  	// ValidationSets is an optional array of validation sets primary keys.
    60  	ValidationSets [][]string
    61  }
    62  
    63  type AssertionQuery interface {
    64  	ToResolve() (map[asserts.Grouping][]*asserts.AtRevision, map[asserts.Grouping][]*asserts.AtSequence, error)
    65  
    66  	AddError(e error, ref *asserts.Ref) error
    67  	AddSequenceError(e error, atSeq *asserts.AtSequence) error
    68  	AddGroupingError(e error, grouping asserts.Grouping) error
    69  }
    70  
    71  type currentSnapV2JSON struct {
    72  	SnapID           string     `json:"snap-id"`
    73  	InstanceKey      string     `json:"instance-key"`
    74  	Revision         int        `json:"revision"`
    75  	TrackingChannel  string     `json:"tracking-channel"`
    76  	Epoch            snap.Epoch `json:"epoch"`
    77  	RefreshedDate    *time.Time `json:"refreshed-date,omitempty"`
    78  	IgnoreValidation bool       `json:"ignore-validation,omitempty"`
    79  	CohortKey        string     `json:"cohort-key,omitempty"`
    80  	// ValidationSets is an optional array of validation sets primary keys.
    81  	ValidationSets [][]string `json:"validation-sets,omitempty"`
    82  }
    83  
    84  type SnapActionFlags int
    85  
    86  const (
    87  	SnapActionIgnoreValidation SnapActionFlags = 1 << iota
    88  	SnapActionEnforceValidation
    89  )
    90  
    91  type SnapAction struct {
    92  	Action       string
    93  	InstanceName string
    94  	SnapID       string
    95  	Channel      string
    96  	Revision     snap.Revision
    97  	CohortKey    string
    98  	Flags        SnapActionFlags
    99  	Epoch        snap.Epoch
   100  	// ValidationSets is an optional array of validation sets primary keys
   101  	// (relevant only for install action).
   102  	ValidationSets [][]string
   103  }
   104  
   105  func isValidAction(action string) bool {
   106  	switch action {
   107  	case "download", "install", "refresh":
   108  		return true
   109  	default:
   110  		return false
   111  	}
   112  }
   113  
   114  type snapActionJSON struct {
   115  	Action string `json:"action"`
   116  	// For snap
   117  	InstanceKey      string `json:"instance-key,omitempty"`
   118  	Name             string `json:"name,omitempty"`
   119  	SnapID           string `json:"snap-id,omitempty"`
   120  	Channel          string `json:"channel,omitempty"`
   121  	Revision         int    `json:"revision,omitempty"`
   122  	CohortKey        string `json:"cohort-key,omitempty"`
   123  	IgnoreValidation *bool  `json:"ignore-validation,omitempty"`
   124  
   125  	// NOTE the store needs an epoch (even if null) for the "install" and "download"
   126  	// actions, to know the client handles epochs at all.  "refresh" actions should
   127  	// send nothing, not even null -- the snap in the context should have the epoch
   128  	// already.  We achieve this by making Epoch be an `interface{}` with omitempty,
   129  	// and then setting it to a (possibly nil) epoch for install and download. As a
   130  	// nil epoch is not an empty interface{}, you'll get the null in the json.
   131  	Epoch interface{} `json:"epoch,omitempty"`
   132  	// For assertions
   133  	Key            string        `json:"key,omitempty"`
   134  	Assertions     []interface{} `json:"assertions,omitempty"`
   135  	ValidationSets [][]string    `json:"validation-sets,omitempty"`
   136  }
   137  
   138  type assertAtJSON struct {
   139  	Type        string   `json:"type"`
   140  	PrimaryKey  []string `json:"primary-key"`
   141  	IfNewerThan *int     `json:"if-newer-than,omitempty"`
   142  }
   143  
   144  type assertSeqAtJSON struct {
   145  	Type        string   `json:"type"`
   146  	SequenceKey []string `json:"sequence-key"`
   147  	Sequence    int      `json:"sequence,omitempty"`
   148  	// if-sequence-equal-or-newer-than and sequence are mutually exclusive
   149  	IfSequenceEqualOrNewerThan *int `json:"if-sequence-equal-or-newer-than,omitempty"`
   150  	IfSequenceNewerThan        *int `json:"if-sequence-newer-than,omitempty"`
   151  	IfNewerThan                *int `json:"if-newer-than,omitempty"`
   152  }
   153  
   154  type snapRelease struct {
   155  	Architecture string `json:"architecture"`
   156  	Channel      string `json:"channel"`
   157  }
   158  
   159  type errorListEntry struct {
   160  	Code    string `json:"code"`
   161  	Message string `json:"message"`
   162  	// for assertions
   163  	Type string `json:"type"`
   164  	// either primary-key or sequence-key is expected (but not both)
   165  	PrimaryKey  []string `json:"primary-key,omitempty"`
   166  	SequenceKey []string `json:"sequence-key,omitempty"`
   167  }
   168  
   169  type snapActionResult struct {
   170  	Result string `json:"result"`
   171  	// For snap
   172  	InstanceKey      string    `json:"instance-key"`
   173  	SnapID           string    `json:"snap-id,omitempty"`
   174  	Name             string    `json:"name,omitempty"`
   175  	Snap             storeSnap `json:"snap"`
   176  	EffectiveChannel string    `json:"effective-channel,omitempty"`
   177  	RedirectChannel  string    `json:"redirect-channel,omitempty"`
   178  	Error            struct {
   179  		Code    string `json:"code"`
   180  		Message string `json:"message"`
   181  		Extra   struct {
   182  			Releases []snapRelease `json:"releases"`
   183  		} `json:"extra"`
   184  	} `json:"error"`
   185  	// For assertions
   186  	Key                 string           `json:"key"`
   187  	AssertionStreamURLs []string         `json:"assertion-stream-urls"`
   188  	ErrorList           []errorListEntry `json:"error-list"`
   189  }
   190  
   191  type snapActionRequest struct {
   192  	Context             []*currentSnapV2JSON `json:"context"`
   193  	Actions             []*snapActionJSON    `json:"actions"`
   194  	Fields              []string             `json:"fields"`
   195  	AssertionMaxFormats map[string]int       `json:"assertion-max-formats,omitempty"`
   196  }
   197  
   198  type snapActionResultList struct {
   199  	Results   []*snapActionResult `json:"results"`
   200  	ErrorList []errorListEntry    `json:"error-list"`
   201  }
   202  
   203  var snapActionFields = jsonutil.StructFields((*storeSnap)(nil))
   204  
   205  // SnapAction queries the store for snap information for the given
   206  // install/refresh actions, given the context information about
   207  // current installed snaps in currentSnaps. If the request was overall
   208  // successul (200) but there were reported errors it will return both
   209  // the snap infos and an SnapActionError.
   210  // Orthogonally and at the same time it can be used to fetch or update
   211  // assertions by passing an AssertionQuery whose ToResolve specifies
   212  // the assertions and revisions to consider. Assertion related errors
   213  // are reported via the AssertionQuery Add*Error methods.
   214  func (s *Store) SnapAction(ctx context.Context, currentSnaps []*CurrentSnap, actions []*SnapAction, assertQuery AssertionQuery, user *auth.UserState, opts *RefreshOptions) ([]SnapActionResult, []AssertionResult, error) {
   215  	if opts == nil {
   216  		opts = &RefreshOptions{}
   217  	}
   218  
   219  	var toResolve map[asserts.Grouping][]*asserts.AtRevision
   220  	var toResolveSeq map[asserts.Grouping][]*asserts.AtSequence
   221  	if assertQuery != nil {
   222  		var err error
   223  		toResolve, toResolveSeq, err = assertQuery.ToResolve()
   224  		if err != nil {
   225  			return nil, nil, err
   226  		}
   227  	}
   228  
   229  	if len(currentSnaps) == 0 && len(actions) == 0 && len(toResolve) == 0 && len(toResolveSeq) == 0 {
   230  		// nothing to do
   231  		return nil, nil, &SnapActionError{NoResults: true}
   232  	}
   233  
   234  	authRefreshes := 0
   235  	for {
   236  		sars, ars, err := s.snapAction(ctx, currentSnaps, actions, assertQuery, toResolve, toResolveSeq, user, opts)
   237  
   238  		if saErr, ok := err.(*SnapActionError); ok && authRefreshes < 2 && len(saErr.Other) > 0 {
   239  			// do we need to try to refresh auths?, 2 tries
   240  			var refreshNeed authRefreshNeed
   241  			for _, otherErr := range saErr.Other {
   242  				switch otherErr {
   243  				case errUserAuthorizationNeedsRefresh:
   244  					refreshNeed.user = true
   245  				case errDeviceAuthorizationNeedsRefresh:
   246  					refreshNeed.device = true
   247  				}
   248  			}
   249  			if refreshNeed.needed() {
   250  				err := s.refreshAuth(user, refreshNeed)
   251  				if err != nil {
   252  					// best effort
   253  					logger.Noticef("cannot refresh soft-expired authorisation: %v", err)
   254  				}
   255  				authRefreshes++
   256  				// TODO: we could avoid retrying here
   257  				// if refreshAuth gave no error we got
   258  				// as many non-error results from the
   259  				// store as actions anyway
   260  				continue
   261  			}
   262  		}
   263  
   264  		return sars, ars, err
   265  	}
   266  }
   267  
   268  func genInstanceKey(curSnap *CurrentSnap, salt string) (string, error) {
   269  	_, snapInstanceKey := snap.SplitInstanceName(curSnap.InstanceName)
   270  
   271  	if snapInstanceKey == "" {
   272  		return curSnap.SnapID, nil
   273  	}
   274  
   275  	if salt == "" {
   276  		return "", fmt.Errorf("internal error: request salt not provided")
   277  	}
   278  
   279  	// due to privacy concerns, avoid sending the local names to the
   280  	// backend, instead hash the snap ID and instance key together
   281  	h := crypto.SHA256.New()
   282  	h.Write([]byte(curSnap.SnapID))
   283  	h.Write([]byte(snapInstanceKey))
   284  	h.Write([]byte(salt))
   285  	enc := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
   286  	return fmt.Sprintf("%s:%s", curSnap.SnapID, enc), nil
   287  }
   288  
   289  // SnapActionResult encapsulates the non-error result of a single
   290  // action of the SnapAction call.
   291  type SnapActionResult struct {
   292  	*snap.Info
   293  	RedirectChannel string
   294  }
   295  
   296  // AssertionResult encapsulates the non-error result for one assertion
   297  // grouping fetch action.
   298  type AssertionResult struct {
   299  	Grouping   asserts.Grouping
   300  	StreamURLs []string
   301  }
   302  
   303  func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, actions []*SnapAction, assertQuery AssertionQuery, toResolve map[asserts.Grouping][]*asserts.AtRevision, toResolveSeq map[asserts.Grouping][]*asserts.AtSequence, user *auth.UserState, opts *RefreshOptions) ([]SnapActionResult, []AssertionResult, error) {
   304  	requestSalt := ""
   305  	if opts != nil {
   306  		requestSalt = opts.PrivacyKey
   307  	}
   308  	curSnaps := make(map[string]*CurrentSnap, len(currentSnaps))
   309  	curSnapJSONs := make([]*currentSnapV2JSON, len(currentSnaps))
   310  	instanceNameToKey := make(map[string]string, len(currentSnaps))
   311  	for i, curSnap := range currentSnaps {
   312  		if curSnap.SnapID == "" || curSnap.InstanceName == "" || curSnap.Revision.Unset() {
   313  			return nil, nil, fmt.Errorf("internal error: invalid current snap information")
   314  		}
   315  		instanceKey, err := genInstanceKey(curSnap, requestSalt)
   316  		if err != nil {
   317  			return nil, nil, err
   318  		}
   319  		curSnaps[instanceKey] = curSnap
   320  		instanceNameToKey[curSnap.InstanceName] = instanceKey
   321  
   322  		channel := curSnap.TrackingChannel
   323  		if channel == "" {
   324  			channel = "stable"
   325  		}
   326  		var refreshedDate *time.Time
   327  		if !curSnap.RefreshedDate.IsZero() {
   328  			refreshedDate = &curSnap.RefreshedDate
   329  		}
   330  		curSnapJSONs[i] = &currentSnapV2JSON{
   331  			SnapID:           curSnap.SnapID,
   332  			InstanceKey:      instanceKey,
   333  			Revision:         curSnap.Revision.N,
   334  			TrackingChannel:  channel,
   335  			IgnoreValidation: curSnap.IgnoreValidation,
   336  			RefreshedDate:    refreshedDate,
   337  			Epoch:            curSnap.Epoch,
   338  			CohortKey:        curSnap.CohortKey,
   339  			ValidationSets:   curSnap.ValidationSets,
   340  		}
   341  	}
   342  
   343  	// do not include toResolveSeq len in the initial size since it may have
   344  	// group keys overlapping with toResolve; the loop over toResolveSeq simply
   345  	// appends to actionJSONs.
   346  	actionJSONs := make([]*snapActionJSON, len(actions)+len(toResolve))
   347  	actionIndex := 0
   348  
   349  	// snaps
   350  	downloadNum := 0
   351  	installNum := 0
   352  	installs := make(map[string]*SnapAction, len(actions))
   353  	downloads := make(map[string]*SnapAction, len(actions))
   354  	refreshes := make(map[string]*SnapAction, len(actions))
   355  	for _, a := range actions {
   356  		if !isValidAction(a.Action) {
   357  			return nil, nil, fmt.Errorf("internal error: unsupported action %q", a.Action)
   358  		}
   359  		if a.InstanceName == "" {
   360  			return nil, nil, fmt.Errorf("internal error: action without instance name")
   361  		}
   362  		var ignoreValidation *bool
   363  		if a.Flags&SnapActionIgnoreValidation != 0 {
   364  			var t = true
   365  			ignoreValidation = &t
   366  		} else if a.Flags&SnapActionEnforceValidation != 0 {
   367  			var f = false
   368  			ignoreValidation = &f
   369  		}
   370  
   371  		var instanceKey string
   372  		aJSON := &snapActionJSON{
   373  			Action:           a.Action,
   374  			SnapID:           a.SnapID,
   375  			Channel:          a.Channel,
   376  			Revision:         a.Revision.N,
   377  			CohortKey:        a.CohortKey,
   378  			IgnoreValidation: ignoreValidation,
   379  		}
   380  		if !a.Revision.Unset() {
   381  			a.Channel = ""
   382  		}
   383  
   384  		if a.Action == "install" {
   385  			installNum++
   386  			instanceKey = fmt.Sprintf("install-%d", installNum)
   387  			installs[instanceKey] = a
   388  			aJSON.ValidationSets = a.ValidationSets
   389  		} else if a.Action == "download" {
   390  			downloadNum++
   391  			instanceKey = fmt.Sprintf("download-%d", downloadNum)
   392  			downloads[instanceKey] = a
   393  			if _, key := snap.SplitInstanceName(a.InstanceName); key != "" {
   394  				return nil, nil, fmt.Errorf("internal error: unsupported download with instance name %q", a.InstanceName)
   395  			}
   396  		} else {
   397  			instanceKey = instanceNameToKey[a.InstanceName]
   398  			refreshes[instanceKey] = a
   399  		}
   400  
   401  		if a.Action != "refresh" {
   402  			aJSON.Name = snap.InstanceSnap(a.InstanceName)
   403  			if a.Epoch.IsZero() {
   404  				// Let the store know we can handle epochs, by sending the `epoch`
   405  				// field in the request.  A nil epoch is not an empty interface{},
   406  				// you'll get the null in the json. See comment in snapActionJSON.
   407  				aJSON.Epoch = (*snap.Epoch)(nil)
   408  			} else {
   409  				// this is the amend case
   410  				aJSON.Epoch = &a.Epoch
   411  			}
   412  		}
   413  
   414  		aJSON.InstanceKey = instanceKey
   415  
   416  		actionJSONs[actionIndex] = aJSON
   417  		actionIndex++
   418  	}
   419  
   420  	groupingsAssertions := make(map[string]*snapActionJSON)
   421  
   422  	// assertions
   423  	var assertMaxFormats map[string]int
   424  	if len(toResolve) > 0 {
   425  		for grp, ats := range toResolve {
   426  			aJSON := &snapActionJSON{
   427  				Action: "fetch-assertions",
   428  				Key:    string(grp),
   429  			}
   430  			aJSON.Assertions = make([]interface{}, len(ats))
   431  			groupingsAssertions[aJSON.Key] = aJSON
   432  
   433  			for j, at := range ats {
   434  				aj := &assertAtJSON{
   435  					Type:       at.Type.Name,
   436  					PrimaryKey: at.PrimaryKey,
   437  				}
   438  				rev := at.Revision
   439  				if rev != asserts.RevisionNotKnown {
   440  					aj.IfNewerThan = &rev
   441  				}
   442  				aJSON.Assertions[j] = aj
   443  			}
   444  			actionJSONs[actionIndex] = aJSON
   445  			actionIndex++
   446  		}
   447  	}
   448  
   449  	if len(toResolveSeq) > 0 {
   450  		for grp, ats := range toResolveSeq {
   451  			key := string(grp)
   452  			// append to existing grouping if applicable
   453  			aJSON := groupingsAssertions[key]
   454  			existingGroup := aJSON != nil
   455  			if !existingGroup {
   456  				aJSON = &snapActionJSON{
   457  					Action: "fetch-assertions",
   458  					Key:    key,
   459  				}
   460  				aJSON.Assertions = make([]interface{}, 0, len(ats))
   461  				actionJSONs = append(actionJSONs, aJSON)
   462  			}
   463  			for _, at := range ats {
   464  				aj := assertSeqAtJSON{
   465  					Type:        at.Type.Name,
   466  					SequenceKey: at.SequenceKey,
   467  				}
   468  				// for pinned we request the assertion ​by the sequence point <sequence-number>​, i.e.
   469  				// {"type": "validation-set",
   470  				//  "sequence-key": ["16", "account-id", "name"],
   471  				//  "sequence": <sequence-number>}
   472  				if at.Pinned {
   473  					if at.Sequence <= 0 {
   474  						return nil, nil, fmt.Errorf("internal error: sequence not set for pinned sequence %s, %v", at.Type.Name, at.SequenceKey)
   475  					}
   476  					aj.Sequence = at.Sequence
   477  				} else {
   478  					// for not pinned, if sequence is specified, then
   479  					// use it for "if-sequence-equal-or-newer-than": <sequence-number>
   480  					if at.Sequence > 0 {
   481  						aj.IfSequenceEqualOrNewerThan = &at.Sequence
   482  					} // else - get the latest
   483  				}
   484  				rev := at.Revision
   485  				// revision (if set) goes to "if-newer-than": <assert-revision>
   486  				if rev != asserts.RevisionNotKnown {
   487  					if at.Sequence <= 0 {
   488  						return nil, nil, fmt.Errorf("internal error: sequence not set while revision is known for %s, %v", at.Type.Name, at.SequenceKey)
   489  					}
   490  					aj.IfNewerThan = &rev
   491  				}
   492  				aJSON.Assertions = append(aJSON.Assertions, aj)
   493  			}
   494  		}
   495  	}
   496  
   497  	if len(toResolve) > 0 || len(toResolveSeq) > 0 {
   498  		assertMaxFormats = asserts.MaxSupportedFormats(1)
   499  	}
   500  
   501  	// build input for the install/refresh endpoint
   502  	jsonData, err := json.Marshal(snapActionRequest{
   503  		Context:             curSnapJSONs,
   504  		Actions:             actionJSONs,
   505  		Fields:              snapActionFields,
   506  		AssertionMaxFormats: assertMaxFormats,
   507  	})
   508  	if err != nil {
   509  		return nil, nil, err
   510  	}
   511  
   512  	reqOptions := &requestOptions{
   513  		Method:      "POST",
   514  		URL:         s.endpointURL(snapActionEndpPath, nil),
   515  		Accept:      jsonContentType,
   516  		ContentType: jsonContentType,
   517  		Data:        jsonData,
   518  		APILevel:    apiV2Endps,
   519  	}
   520  
   521  	if opts.IsAutoRefresh {
   522  		logger.Debugf("Auto-refresh; adding header Snap-Refresh-Reason: scheduled")
   523  		reqOptions.addHeader("Snap-Refresh-Reason", "scheduled")
   524  	}
   525  
   526  	if s.useDeltas() {
   527  		logger.Debugf("Deltas enabled. Adding header Snap-Accept-Delta-Format: %v", s.deltaFormat)
   528  		reqOptions.addHeader("Snap-Accept-Delta-Format", s.deltaFormat)
   529  	}
   530  	if opts.RefreshManaged {
   531  		reqOptions.addHeader("Snap-Refresh-Managed", "true")
   532  	}
   533  
   534  	var results snapActionResultList
   535  	resp, err := s.retryRequestDecodeJSON(ctx, reqOptions, user, &results, nil)
   536  	if err != nil {
   537  		return nil, nil, err
   538  	}
   539  
   540  	if resp.StatusCode != 200 {
   541  		return nil, nil, respToError(resp, "query the store for updates")
   542  	}
   543  
   544  	s.extractSuggestedCurrency(resp)
   545  
   546  	refreshErrors := make(map[string]error)
   547  	installErrors := make(map[string]error)
   548  	downloadErrors := make(map[string]error)
   549  	var otherErrors []error
   550  
   551  	var sars []SnapActionResult
   552  	var ars []AssertionResult
   553  	for _, res := range results.Results {
   554  		if res.Result == "fetch-assertions" {
   555  			if len(res.ErrorList) != 0 {
   556  				if err := reportFetchAssertionsError(res, assertQuery); err != nil {
   557  					return nil, nil, fmt.Errorf("internal error: %v", err)
   558  				}
   559  				continue
   560  			}
   561  			ars = append(ars, AssertionResult{
   562  				Grouping:   asserts.Grouping(res.Key),
   563  				StreamURLs: res.AssertionStreamURLs,
   564  			})
   565  			continue
   566  		}
   567  		if res.Result == "error" {
   568  			if a := installs[res.InstanceKey]; a != nil {
   569  				if res.Name != "" {
   570  					installErrors[a.InstanceName] = translateSnapActionError("install", a.Channel, res.Error.Code, res.Error.Message, res.Error.Extra.Releases)
   571  					continue
   572  				}
   573  			} else if a := downloads[res.InstanceKey]; a != nil {
   574  				if res.Name != "" {
   575  					downloadErrors[res.Name] = translateSnapActionError("download", a.Channel, res.Error.Code, res.Error.Message, res.Error.Extra.Releases)
   576  					continue
   577  				}
   578  			} else {
   579  				if cur := curSnaps[res.InstanceKey]; cur != nil {
   580  					a := refreshes[res.InstanceKey]
   581  					if a == nil {
   582  						// got an error for a snap that was not part of an 'action'
   583  						otherErrors = append(otherErrors, translateSnapActionError("", "", res.Error.Code, fmt.Sprintf("snap %q: %s", cur.InstanceName, res.Error.Message), nil))
   584  						logger.Debugf("Unexpected error for snap %q, instance key %v: [%v] %v", cur.InstanceName, res.InstanceKey, res.Error.Code, res.Error.Message)
   585  						continue
   586  					}
   587  					channel := a.Channel
   588  					if channel == "" && a.Revision.Unset() {
   589  						channel = cur.TrackingChannel
   590  					}
   591  					refreshErrors[cur.InstanceName] = translateSnapActionError("refresh", channel, res.Error.Code, res.Error.Message, res.Error.Extra.Releases)
   592  					continue
   593  				}
   594  			}
   595  			otherErrors = append(otherErrors, translateSnapActionError("", "", res.Error.Code, res.Error.Message, nil))
   596  			continue
   597  		}
   598  		snapInfo, err := infoFromStoreSnap(&res.Snap)
   599  		if err != nil {
   600  			return nil, nil, fmt.Errorf("unexpected invalid install/refresh API result: %v", err)
   601  		}
   602  
   603  		snapInfo.Channel = res.EffectiveChannel
   604  
   605  		var instanceName string
   606  		if res.Result == "refresh" {
   607  			cur := curSnaps[res.InstanceKey]
   608  			if cur == nil {
   609  				return nil, nil, fmt.Errorf("unexpected invalid install/refresh API result: unexpected refresh")
   610  			}
   611  			rrev := snap.R(res.Snap.Revision)
   612  			if rrev == cur.Revision || findRev(rrev, cur.Block) {
   613  				refreshErrors[cur.InstanceName] = ErrNoUpdateAvailable
   614  				continue
   615  			}
   616  			instanceName = cur.InstanceName
   617  		} else if res.Result == "install" {
   618  			if action := installs[res.InstanceKey]; action != nil {
   619  				instanceName = action.InstanceName
   620  			}
   621  		}
   622  
   623  		if res.Result != "download" && instanceName == "" {
   624  			return nil, nil, fmt.Errorf("unexpected invalid install/refresh API result: unexpected instance-key %q", res.InstanceKey)
   625  		}
   626  
   627  		_, instanceKey := snap.SplitInstanceName(instanceName)
   628  		snapInfo.InstanceKey = instanceKey
   629  
   630  		sars = append(sars, SnapActionResult{Info: snapInfo, RedirectChannel: res.RedirectChannel})
   631  	}
   632  
   633  	for _, errObj := range results.ErrorList {
   634  		otherErrors = append(otherErrors, translateSnapActionError("", "", errObj.Code, errObj.Message, nil))
   635  	}
   636  
   637  	if len(refreshErrors)+len(installErrors)+len(downloadErrors) != 0 || len(results.Results) == 0 || len(otherErrors) != 0 {
   638  		// normalize empty maps
   639  		if len(refreshErrors) == 0 {
   640  			refreshErrors = nil
   641  		}
   642  		if len(installErrors) == 0 {
   643  			installErrors = nil
   644  		}
   645  		if len(downloadErrors) == 0 {
   646  			downloadErrors = nil
   647  		}
   648  		return sars, ars, &SnapActionError{
   649  			NoResults: len(results.Results) == 0,
   650  			Refresh:   refreshErrors,
   651  			Install:   installErrors,
   652  			Download:  downloadErrors,
   653  			Other:     otherErrors,
   654  		}
   655  	}
   656  
   657  	return sars, ars, nil
   658  }
   659  
   660  func findRev(needle snap.Revision, haystack []snap.Revision) bool {
   661  	for _, r := range haystack {
   662  		if needle == r {
   663  			return true
   664  		}
   665  	}
   666  	return false
   667  }
   668  
   669  func reportFetchAssertionsError(res *snapActionResult, assertq AssertionQuery) error {
   670  	// prefer to report the most unexpected error:
   671  	// * errors not referring to an assertion (no valid type/primary-key)
   672  	// are more unexpected than
   673  	// * errors referring to a precise assertion that are not not-found
   674  	// themselves more unexpected than
   675  	// * not-found errors
   676  	errIdx := -1
   677  	errl := res.ErrorList
   678  	carryingRef := func(ent *errorListEntry) bool {
   679  		aType := asserts.Type(ent.Type)
   680  		return aType != nil && len(ent.PrimaryKey) == len(aType.PrimaryKey)
   681  	}
   682  	carryingSeqKey := func(ent *errorListEntry) bool {
   683  		aType := asserts.Type(ent.Type)
   684  		return aType != nil && aType.SequenceForming() && len(ent.SequenceKey) == len(aType.PrimaryKey)-1
   685  	}
   686  	prio := func(ent *errorListEntry) int {
   687  		if !carryingRef(ent) && !carryingSeqKey(ent) {
   688  			return 2
   689  		}
   690  		if ent.Code != "not-found" {
   691  			return 1
   692  		}
   693  		return 0
   694  	}
   695  	for i, ent := range errl {
   696  		if errIdx == -1 {
   697  			errIdx = i
   698  			continue
   699  		}
   700  		prioOther := prio(&errl[errIdx])
   701  		prioThis := prio(&ent)
   702  		if prioThis > prioOther {
   703  			errIdx = i
   704  		}
   705  	}
   706  	rep := errl[errIdx]
   707  	notFound := rep.Code == "not-found"
   708  	switch {
   709  	case carryingRef(&rep):
   710  		ref := &asserts.Ref{Type: asserts.Type(rep.Type), PrimaryKey: rep.PrimaryKey}
   711  		var err error
   712  		if notFound {
   713  			headers, _ := asserts.HeadersFromPrimaryKey(ref.Type, ref.PrimaryKey)
   714  			err = &asserts.NotFoundError{
   715  				Type:    ref.Type,
   716  				Headers: headers,
   717  			}
   718  		} else {
   719  			err = fmt.Errorf("%s", rep.Message)
   720  		}
   721  		return assertq.AddError(err, ref)
   722  	case carryingSeqKey(&rep):
   723  		var err error
   724  		atSeq := &asserts.AtSequence{Type: asserts.Type(rep.Type), SequenceKey: rep.SequenceKey}
   725  		if notFound {
   726  			headers, _ := asserts.HeadersFromSequenceKey(atSeq.Type, atSeq.SequenceKey)
   727  			err = &asserts.NotFoundError{
   728  				Type:    atSeq.Type,
   729  				Headers: headers,
   730  			}
   731  		} else {
   732  			err = fmt.Errorf("%s", rep.Message)
   733  		}
   734  		return assertq.AddSequenceError(err, atSeq)
   735  	}
   736  
   737  	return assertq.AddGroupingError(fmt.Errorf("%s", rep.Message), asserts.Grouping(res.Key))
   738  }