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