Skip to content

Commit

Permalink
store: handle error-list in fetch-assertions results
Browse files Browse the repository at this point in the history
this also introduces AssertionQuery.Add*Error methods for
error reporting by SnapAction for assertions
  • Loading branch information
pedronis committed Apr 21, 2020
1 parent d7dda1d commit 35ca7fe
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 8 deletions.
7 changes: 7 additions & 0 deletions store/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,10 @@ func MockRatelimitReader(f func(r io.Reader, bucket *ratelimit.Bucket) io.Reader
ratelimitReader = oldRatelimitReader
}
}

type (
ErrorListEntryJSON = errorListEntry
SnapActionResultJSON = snapActionResult
)

var ReportFetchAssertionsError = reportFetchAssertionsError
75 changes: 67 additions & 8 deletions store/store_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type CurrentSnap struct {

type AssertionQuery interface {
ToResolve() (map[asserts.Grouping][]*asserts.AtRevision, error)

AddError(e error, ref *asserts.Ref) error
AddGroupingError(e error, grouping asserts.Grouping) error
}

type currentSnapV2JSON struct {
Expand Down Expand Up @@ -134,6 +137,14 @@ type snapRelease struct {
Channel string `json:"channel"`
}

type errorListEntry struct {
Code string `json:"code"`
Message string `json:"message"`
// for assertions
Type string `json:"type"`
PrimaryKey []string `json:"primary-key"`
}

type snapActionResult struct {
Result string `json:"result"`
// For snap
Expand All @@ -151,8 +162,9 @@ type snapActionResult struct {
} `json:"extra"`
} `json:"error"`
// For assertions
Key string `json:"key"`
AssertionStreamURLs []string `json:"assertion-stream-urls"`
Key string `json:"key"`
AssertionStreamURLs []string `json:"assertion-stream-urls"`
ErrorList []errorListEntry `json:"error-list"`
}

type snapActionRequest struct {
Expand All @@ -164,10 +176,7 @@ type snapActionRequest struct {

type snapActionResultList struct {
Results []*snapActionResult `json:"results"`
ErrorList []struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error-list"`
ErrorList []errorListEntry `json:"error-list"`
}

var snapActionFields = jsonutil.StructFields((*storeSnap)(nil))
Expand All @@ -179,7 +188,8 @@ var snapActionFields = jsonutil.StructFields((*storeSnap)(nil))
// the snap infos and an SnapActionError.
// Orthogonally and at the same time it can be used to fetch or update
// assertions by passing an AssertionQuery whose ToResolve specifies
// the assertions and revisions to consider.
// the assertions and revisions to consider. Assertion related errors
// are reported via the AssertionQuery Add*Error methods.
func (s *Store) SnapAction(ctx context.Context, currentSnaps []*CurrentSnap, actions []*SnapAction, assertQuery AssertionQuery, user *auth.UserState, opts *RefreshOptions) ([]SnapActionResult, []AssertionResult, error) {
if opts == nil {
opts = &RefreshOptions{}
Expand Down Expand Up @@ -456,11 +466,16 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act
var ars []AssertionResult
for _, res := range results.Results {
if res.Result == "fetch-assertions" {
if len(res.ErrorList) != 0 {
if err := reportFetchAssertionsError(res, assertQuery); err != nil {
return nil, nil, fmt.Errorf("internal error: %v", err)
}
continue
}
ars = append(ars, AssertionResult{
Grouping: asserts.Grouping(res.Key),
StreamURLs: res.AssertionStreamURLs,
})
// XXX handle error-list
continue
}
if res.Result == "error" {
Expand Down Expand Up @@ -564,3 +579,47 @@ func findRev(needle snap.Revision, haystack []snap.Revision) bool {
}
return false
}

func reportFetchAssertionsError(res *snapActionResult, assertq AssertionQuery) error {
// prefer to report the most unexpected error
e := -1
errl := res.ErrorList
carryingRef := func(i int) bool {
aType := asserts.Type(errl[i].Type)
return aType != nil && len(errl[i].PrimaryKey) == len(aType.PrimaryKey)
}
for i, ent := range errl {
withRef := carryingRef(i)
if withRef && ent.Code == "not-found" {
if e == -1 {
e = i
}
} else if withRef {
if e == -1 || errl[e].Code == "not-found" {
e = i
}
} else {
if e == -1 || carryingRef(e) {
e = i
}
}
}
rep := errl[e]
if carryingRef(e) {
ref := &asserts.Ref{Type: asserts.Type(rep.Type), PrimaryKey: rep.PrimaryKey}
var err error
if rep.Code == "not-found" {
headers, _ := asserts.HeadersFromPrimaryKey(ref.Type, ref.PrimaryKey)
err = &asserts.NotFoundError{
Type: ref.Type,
Headers: headers,
}

} else {
err = fmt.Errorf("%s", rep.Message)
}
return assertq.AddError(err, ref)
}

return assertq.AddGroupingError(fmt.Errorf("%s", rep.Message), asserts.Grouping(res.Key))
}
174 changes: 174 additions & 0 deletions store/store_action_fetch_assertions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,30 @@ var _ = Suite(&storeActionFetchAssertionsSuite{})

type testAssertQuery struct {
toResolve map[asserts.Grouping][]*asserts.AtRevision
errors map[string]error
}

func (q *testAssertQuery) ToResolve() (map[asserts.Grouping][]*asserts.AtRevision, error) {
return q.toResolve, nil
}

func (q *testAssertQuery) addError(e error, u string) {
if q.errors == nil {
q.errors = make(map[string]error)
}
q.errors[u] = e
}

func (q *testAssertQuery) AddError(e error, ref *asserts.Ref) error {
q.addError(e, ref.Unique())
return nil
}

func (q *testAssertQuery) AddGroupingError(e error, grouping asserts.Grouping) error {
q.addError(e, fmt.Sprintf("{%s}", grouping))
return nil
}

func (s *storeActionFetchAssertionsSuite) TestFetch(c *C) {
restore := release.MockOnClassic(false)
defer restore()
Expand Down Expand Up @@ -266,3 +284,159 @@ func (s *storeActionFetchAssertionsSuite) TestUpdateIfNewerThan(c *C) {
}
c.Check(seen, Equals, 2)
}

func (s *storeActionFetchAssertionsSuite) TestFetchNotFound(c *C) {
restore := release.MockOnClassic(false)
defer restore()

mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", snapActionPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)

jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var req struct {
Context []map[string]interface{} `json:"context"`
Actions []map[string]interface{} `json:"actions"`
}

err = json.Unmarshal(jsonReq, &req)
c.Assert(err, IsNil)

c.Assert(req.Context, HasLen, 0)
c.Assert(req.Actions, HasLen, 1)
expectedAction := map[string]interface{}{
"action": "fetch-assertions",
"key": "g1",
"assertions": []interface{}{
map[string]interface{}{
"type": "snap-declaration",
"primary-key": []interface{}{
"16",
"xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5",
},
},
},
}
c.Assert(req.Actions[0], DeepEquals, expectedAction)

fmt.Fprintf(w, `{
"results": [{
"result": "fetch-assertions",
"key": "g1",
"assertion-stream-urls": [],
"error-list": [
{
"code": "not-found",
"message": "not found: no assertion with type \"snap-declaration\" and key {\"series\":\"16\",\"snap-id\":\"xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5\"}",
"primary-key": [
"16",
"xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5"
],
"type": "snap-declaration"
}
]
}
]
}`)
}))

c.Assert(mockServer, NotNil)
defer mockServer.Close()

mockServerURL, _ := url.Parse(mockServer.URL)
cfg := store.Config{
StoreBaseURL: mockServerURL,
}
dauthCtx := &testDauthContext{c: c, device: s.device}
sto := store.New(&cfg, dauthCtx)

assertq := &testAssertQuery{
toResolve: map[asserts.Grouping][]*asserts.AtRevision{
asserts.Grouping("g1"): {{
Ref: asserts.Ref{
Type: asserts.SnapDeclarationType,
PrimaryKey: []string{
"16",
"xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5",
},
},
Revision: asserts.RevisionNotKnown,
}},
},
}

results, aresults, err := sto.SnapAction(s.ctx, nil,
nil, assertq, nil, nil)
c.Assert(err, IsNil)
c.Check(results, HasLen, 0)
c.Check(aresults, HasLen, 0)

c.Check(assertq.errors, DeepEquals, map[string]error{
"snap-declaration/16/xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5": &asserts.NotFoundError{
Type: asserts.SnapDeclarationType,
Headers: map[string]string{"series": "16", "snap-id": "xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5"}},
})
}

func (s *storeActionFetchAssertionsSuite) TestReportFetchAssertionsError(c *C) {
notFound := store.ErrorListEntryJSON{
Code: "not-found",
Type: "snap-declaration",
PrimaryKey: []string{
"16",
"xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5",
},
Message: `not found: no assertion with type "snap-declaration" and key {"series":"16","snap-id":"xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5"}`,
}
invalidRequest := store.ErrorListEntryJSON{
Code: "invalid-request",
Type: "snap-declaration",
PrimaryKey: []string{},
Message: `invalid request: invalid key, should be "{series}/{snap-id}"`,
}
// not a realistic error, but for completeness
otherRefError := store.ErrorListEntryJSON{
Code: "other-ref-error",
Type: "snap-declaration",
PrimaryKey: []string{
"16",
"xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5",
},
Message: "other ref error",
}

tests := []struct {
errorList []store.ErrorListEntryJSON
errkey string
err string
}{
{[]store.ErrorListEntryJSON{notFound}, "snap-declaration/16/xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5", "snap-declaration.*not found"},
{[]store.ErrorListEntryJSON{otherRefError}, "snap-declaration/16/xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5", "other ref error"},
{[]store.ErrorListEntryJSON{otherRefError, notFound}, "snap-declaration/16/xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5", "other ref error"},
{[]store.ErrorListEntryJSON{notFound, otherRefError}, "snap-declaration/16/xEr2EpvaIaqrXxoM2JyHOmuXQYvSzUt5", "other ref error"},
{[]store.ErrorListEntryJSON{invalidRequest}, "{g1}", "invalid request: invalid key.*"},
{[]store.ErrorListEntryJSON{invalidRequest, otherRefError}, "{g1}", "invalid request: invalid key.*"},
{[]store.ErrorListEntryJSON{invalidRequest, notFound}, "{g1}", "invalid request: invalid key.*"},
{[]store.ErrorListEntryJSON{notFound, invalidRequest, otherRefError}, "{g1}", "invalid request: invalid key.*"},
}

for _, t := range tests {
assertq := &testAssertQuery{}

res := &store.SnapActionResultJSON{
Key: "g1",
ErrorList: t.errorList,
}

err := store.ReportFetchAssertionsError(res, assertq)
c.Assert(err, IsNil)

c.Check(assertq.errors, HasLen, 1)
for k, e := range assertq.errors {
c.Check(k, Equals, t.errkey)
c.Check(e, ErrorMatches, t.err)
}
}
}

0 comments on commit 35ca7fe

Please sign in to comment.