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