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