gitee.com/mysnapcore/mysnapd@v0.1.0/store/store_action.go (about)

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