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] = ¤tSnapV2JSON{ 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 }