diff options
| author | Paweł Stołowski <stolowski@gmail.com> | 2022-06-23 13:59:49 +0200 |
|---|---|---|
| committer | Paweł Stołowski <stolowski@gmail.com> | 2022-06-23 13:59:49 +0200 |
| commit | 48aaec2e15f64494c4d4ae1906e4e23fd6dffbe4 (patch) | |
| tree | eae1f360f8bd5d5f7fa7da07f6dcf984aa22281f | |
| parent | 5c35df06c8eacc7affd195f218075396a9c3b672 (diff) | |
| parent | 58826a919e2907cb185c60e2127fcbd1c397dcec (diff) | |
Merge branch 'validation-sets/try-enforce-validation-sets' into validation-sets/enforce-refresh
| -rw-r--r-- | asserts/snapasserts/validation_sets.go | 32 | ||||
| -rw-r--r-- | asserts/snapasserts/validation_sets_test.go | 52 | ||||
| -rw-r--r-- | cmd/snap/cmd_validate.go | 34 | ||||
| -rw-r--r-- | overlord/assertstate/assertstate.go | 92 | ||||
| -rw-r--r-- | overlord/assertstate/assertstate_test.go | 351 |
5 files changed, 479 insertions, 82 deletions
diff --git a/asserts/snapasserts/validation_sets.go b/asserts/snapasserts/validation_sets.go index c2d073311e..4b2a94e5a6 100644 --- a/asserts/snapasserts/validation_sets.go +++ b/asserts/snapasserts/validation_sets.go @@ -23,6 +23,7 @@ import ( "bytes" "fmt" "sort" + "strconv" "strings" "github.com/snapcore/snapd/asserts" @@ -584,3 +585,34 @@ func (v *ValidationSets) CheckPresenceInvalid(snapRef naming.SnapRef) ([]string, sort.Strings(keys) return keys, nil } + +// ParseValidationSet parses a validation set string (account/name or account/name=sequence) +// and returns its individual components, or an error. +func ParseValidationSet(arg string) (account, name string, seq int, err error) { + parts := strings.Split(arg, "=") + if len(parts) > 2 { + return "", "", 0, fmt.Errorf("cannot parse validation set, expected account/name=seq") + } + if len(parts) == 2 { + seq, err = strconv.Atoi(parts[1]) + if err != nil { + return "", "", 0, fmt.Errorf("cannot parse sequence: %v", err) + } + } + + parts = strings.Split(parts[0], "/") + if len(parts) != 2 { + return "", "", 0, fmt.Errorf("expected a single account/name") + } + + account = parts[0] + name = parts[1] + if !asserts.IsValidAccountID(account) { + return "", "", 0, fmt.Errorf("invalid account ID %q", account) + } + if !asserts.IsValidValidationSetName(name) { + return "", "", 0, fmt.Errorf("invalid validation set name %q", name) + } + + return account, name, seq, nil +} diff --git a/asserts/snapasserts/validation_sets_test.go b/asserts/snapasserts/validation_sets_test.go index 62ea0f9a7c..79bb2d15b2 100644 --- a/asserts/snapasserts/validation_sets_test.go +++ b/asserts/snapasserts/validation_sets_test.go @@ -1012,3 +1012,55 @@ func (s *validationSetsSuite) TestIsPresenceInvalid(c *C) { c.Assert(err, IsNil) c.Check(vsKeys, HasLen, 0) } + +func (s *validationSetsSuite) TestParseValidationSet(c *C) { + for _, tc := range []struct { + input string + errMsg string + account string + name string + sequence int + }{ + { + input: "foo/bar", + account: "foo", + name: "bar", + }, + { + input: "foo/bar=9", + account: "foo", + name: "bar", + sequence: 9, + }, + { + input: "foo", + errMsg: "expected a single account/name", + }, + { + input: "foo/bar/baz", + errMsg: "expected a single account/name", + }, + { + input: "", + errMsg: "expected a single account/name", + }, + { + input: "foo=1", + errMsg: "expected a single account/name", + }, + { + input: "foo/bar=x", + errMsg: `cannot parse sequence: strconv.Atoi: parsing "x": invalid syntax`, + }, + } { + account, name, seq, err := snapasserts.ParseValidationSet(tc.input) + if tc.errMsg != "" { + c.Assert(err, ErrorMatches, tc.errMsg) + } else { + c.Assert(err, IsNil) + } + c.Check(account, Equals, tc.account) + c.Check(name, Equals, tc.name) + c.Check(seq, Equals, tc.sequence) + } +} diff --git a/cmd/snap/cmd_validate.go b/cmd/snap/cmd_validate.go index 94d14ed303..b53d9e348d 100644 --- a/cmd/snap/cmd_validate.go +++ b/cmd/snap/cmd_validate.go @@ -21,12 +21,11 @@ package main import ( "fmt" - "strconv" "strings" "github.com/jessevdk/go-flags" - "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" ) @@ -69,35 +68,6 @@ func init() { cmd.hidden = true } -func splitValidationSetArg(arg string) (account, name string, seq int, err error) { - parts := strings.Split(arg, "=") - if len(parts) > 2 { - return "", "", 0, fmt.Errorf("cannot parse validation set, expected account/name=seq") - } - if len(parts) == 2 { - seq, err = strconv.Atoi(parts[1]) - if err != nil { - return "", "", 0, err - } - } - - parts = strings.Split(parts[0], "/") - if len(parts) != 2 { - return "", "", 0, fmt.Errorf("expected a single account/name") - } - - account = parts[0] - name = parts[1] - if !asserts.IsValidAccountID(account) { - return "", "", 0, fmt.Errorf("invalid account ID %q", account) - } - if !asserts.IsValidValidationSetName(name) { - return "", "", 0, fmt.Errorf("invalid validation set name %q", name) - } - - return account, name, seq, nil -} - func fmtValid(res *client.ValidationSetResult) string { if res.Valid { return "valid" @@ -139,7 +109,7 @@ func (cmd *cmdValidate) Execute(args []string) error { var seq int var err error if cmd.Positional.ValidationSet != "" { - accountID, name, seq, err = splitValidationSetArg(cmd.Positional.ValidationSet) + accountID, name, seq, err = snapasserts.ParseValidationSet(cmd.Positional.ValidationSet) if err != nil { return fmt.Errorf("cannot parse validation set %q: %v", cmd.Positional.ValidationSet, err) } diff --git a/overlord/assertstate/assertstate.go b/overlord/assertstate/assertstate.go index defd1af12d..2cc0bac83f 100644 --- a/overlord/assertstate/assertstate.go +++ b/overlord/assertstate/assertstate.go @@ -25,7 +25,6 @@ package assertstate import ( "errors" "fmt" - "strconv" "strings" "github.com/snapcore/snapd/asserts" @@ -774,50 +773,27 @@ func validationSetAssertionForEnforce(st *state.State, accountID, name string, s } // TryEnforceValidationSets tries to fetch the given validation sets and enforce them (together with currently tracked validation sets) against installed snaps, -// but doesn't update tracking information. It may return snapasserts.ValidationSetsValidationError which can be used to install/remove snaps as required -// to satisfy validation sets constraints. +// but doesn't update tracking information in case of an error. It may return snapasserts.ValidationSetsValidationError which can be used to install/remove snaps +// as required to satisfy validation sets constraints. func TryEnforceValidationSets(st *state.State, validationSets []string, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { deviceCtx, err := snapstate.DevicePastSeeding(st, nil) if err != nil { return err } - opts := &RefreshAssertionsOptions{IsAutoRefresh: false} - - // refresh all currently tracked validation set assertions (this may or may not - // include the one requested by the caller). - if err = RefreshValidationSetAssertions(st, userID, opts); err != nil { - return err - } - db := cachedDB(st) pool := asserts.NewPool(db, maxGroups) extraVsHeaders := make([]map[string]string, 0, len(validationSets)) + newTracking := make([]*ValidationSetTracking, 0, len(validationSets)) for _, vsstr := range validationSets { - // XXX: do we have a helper for this already? - parts := strings.Split(vsstr, "/") - if len(parts) != 2 { - return fmt.Errorf("cannot parse validation set: %s", vsstr) - } - accountID, name := parts[0], parts[1] - seqParts := strings.Split(name, "=") - var sequence int - if len(seqParts) > 2 { - return fmt.Errorf("cannot parse validation-set sequence %s", name) - } - if len(seqParts) == 2 { - sequence, err = strconv.Atoi(seqParts[1]) - if err != nil { - return fmt.Errorf("cannot parse validation-set sequence %s: %v", seqParts[1], err) - } + accountID, name, sequence, err := snapasserts.ParseValidationSet(vsstr) + if err != nil { + return err } - // try to get existing from the db. It will be the latest one if it was - // tracked already and thus refreshed via RefreshValidationSetAssertions. - // Otherwise, it may be a local assertion that was tracked in the past and - // then forgotten, in which case we need to refresh it explicitly. + // try to get existing from the db headers := map[string]string{ "series": release.Series, "account-id": accountID, @@ -834,28 +810,27 @@ func TryEnforceValidationSets(st *state.State, validationSets []string, userID i Revision: asserts.RevisionNotKnown, Pinned: sequence > 0, } + + // prepare tracking data, note current is not known yet + tr := &ValidationSetTracking{ + AccountID: headers["account-id"], + Name: headers["name"], + Mode: Enforce, + // may be 0 meaning no pinning + PinnedAt: sequence, + } + extraVsHeaders = append(extraVsHeaders, headers) + newTracking = append(newTracking, tr) vs, err := getSpecificSequenceOrLatest(db, headers) // found locally if err == nil { - // check if we were tracking it already; if not, that - // means we found an old assertion (it was very likely tracked in the - // past) and we need to update it as it wasn't covered - // by RefreshValidationSetAssertions. - var tr ValidationSetTracking - trerr := GetValidationSet(st, accountID, name, &tr) - if trerr != nil && !errors.Is(trerr, state.ErrNoState) { - return trerr - } - // not tracked, update the assertion - if errors.Is(trerr, state.ErrNoState) { - // update with pool - atSeq.Sequence = vs.Sequence() - atSeq.Revision = vs.Revision() - if err := pool.AddSequenceToUpdate(atSeq, atSeq.Unique()); err != nil { - return err - } + // update with pool + atSeq.Sequence = vs.Sequence() + atSeq.Revision = vs.Revision() + if err := pool.AddSequenceToUpdate(atSeq, atSeq.Unique()); err != nil { + return err } } else { if !asserts.IsNotFound(err) { @@ -883,6 +858,8 @@ func TryEnforceValidationSets(st *state.State, validationSets []string, userID i valsets, err := EnforcedValidationSets(st, extraVs...) if err != nil { + // the returned error may be ValidationSetsValidationError which is normal and means we cannot enforce + // the new validation sets - the caller should resolve the error and retry. return err } if err := valsets.Conflict(); err != nil { @@ -897,13 +874,28 @@ func TryEnforceValidationSets(st *state.State, validationSets []string, userID i return nil } + opts := &RefreshAssertionsOptions{} if err := resolvePoolNoFallback(st, pool, checkBeforeCommit, userID, deviceCtx, opts); err != nil { return err } - // TODO: update tracking for all vsets + // no error, all validation-sets can be enforced, update tracking for all vsets + for i := 0; i < len(extraVsHeaders); i++ { + // get latest assertion from the db to determine current + a, err := db.FindSequence(asserts.ValidationSetType, extraVsHeaders[i], -1, -1) + if err != nil { + // this is unexpected since all asserts should be resolved and commited at this point + return fmt.Errorf("internal error: cannot find validation set assertion: %v", err) + } + vs := a.(*asserts.ValidationSet) + tr := newTracking[i] + tr.Current = vs.Sequence() + } + for _, tr := range newTracking { + UpdateValidationSet(st, tr) + } - return nil + return addCurrentTrackingToValidationSetsHistory(st) } // EnforceValidationSet tries to fetch the given validation set and enforce it. diff --git a/overlord/assertstate/assertstate_test.go b/overlord/assertstate/assertstate_test.go index a52a5817d4..a96a2b82db 100644 --- a/overlord/assertstate/assertstate_test.go +++ b/overlord/assertstate/assertstate_test.go @@ -893,6 +893,10 @@ func (s *assertMgrSuite) validationSetAssert(c *C, name, sequence, revision stri if requiredRevision != "" { snaps[0].(map[string]interface{})["revision"] = requiredRevision } + return s.validationSetAssertForSnaps(c, name, sequence, revision, snaps) +} + +func (s *assertMgrSuite) validationSetAssertForSnaps(c *C, name, sequence, revision string, snaps []interface{}) *asserts.ValidationSet { headers := map[string]interface{}{ "series": "16", "account-id": s.dev1Acct.AccountID(), @@ -3686,6 +3690,353 @@ func (s *assertMgrSuite) TestEnforceValidationSetAssertionIgnoreValidation(c *C) c.Check(tr, DeepEquals, *tracking) } +func (s *assertMgrSuite) TestTryEnforceValidationSetsAssertionsValidationError(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // pretend we are already enforcing a validation set foo + snaps3 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "required", + }} + vsetAs3 := s.validationSetAssertForSnaps(c, "foo", "1", "1", snaps3) + c.Assert(assertstate.Add(st, vsetAs3), IsNil) + assertstate.UpdateValidationSet(st, &assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 1, + }) + + // add validation set assertions to the store + snaps1 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "required", + "revision": "3", + }, + map[string]interface{}{ + "id": "aAqKhntON3vR7kwEbVPsILm7bUViPDaa", + "name": "other-snap", + "presence": "required", + }} + vsetAs := s.validationSetAssertForSnaps(c, "bar", "2", "2", snaps1) + c.Assert(s.storeSigning.Add(vsetAs), IsNil) + snaps2 := []interface{}{ + map[string]interface{}{ + "id": "cccchntON3vR7kwEbVPsILm7bUViPDcc", + "name": "invalid-snap", + "presence": "invalid", + }} + vsetAs2 := s.validationSetAssertForSnaps(c, "baz", "1", "1", snaps2) + c.Assert(s.storeSigning.Add(vsetAs2), IsNil) + + // try to enforce extra validation sets bar and baz. some-snap is present (and required by foo at any revision), + // but needs to be at revision 3 to satisfy bar. invalid-snap is present but is invalid for baz. + installedSnaps := []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("some-snap", "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.Revision{N: 1}), + snapasserts.NewInstalledSnap("invalid-snap", "cccchntON3vR7kwEbVPsILm7bUViPDcc", snap.Revision{N: 1}), + } + err := assertstate.TryEnforceValidationSets(st, []string{fmt.Sprintf("%s/bar", s.dev1Acct.AccountID()), fmt.Sprintf("%s/baz", s.dev1Acct.AccountID())}, 0, installedSnaps, nil) + verr, ok := err.(*snapasserts.ValidationSetsValidationError) + c.Assert(ok, Equals, true) + c.Check(verr.WrongRevisionSnaps, DeepEquals, map[string]map[snap.Revision][]string{ + "some-snap": { + snap.R(3): []string{fmt.Sprintf("%s/bar", s.dev1Acct.AccountID())}, + }, + }) + c.Check(verr.MissingSnaps, DeepEquals, map[string]map[snap.Revision][]string{ + "other-snap": { + snap.R(0): []string{fmt.Sprintf("%s/bar", s.dev1Acct.AccountID())}, + }, + }) + c.Check(verr.InvalidSnaps, DeepEquals, map[string][]string{"invalid-snap": {fmt.Sprintf("%s/baz", s.dev1Acct.AccountID())}}) + + // new assertions were not commited + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "bar", + "sequence": "2", + }) + c.Assert(asserts.IsNotFound(err), Equals, true) + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "baz", + "sequence": "1", + }) + c.Assert(asserts.IsNotFound(err), Equals, true) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) +} + +func (s *assertMgrSuite) TestTryEnforceValidationSetsAssertionsOK(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // pretend we are already enforcing a validation set foo + snaps3 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "required", + }} + vsetAs3 := s.validationSetAssertForSnaps(c, "foo", "1", "1", snaps3) + c.Assert(assertstate.Add(st, vsetAs3), IsNil) + assertstate.UpdateValidationSet(st, &assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 1, + }) + + // add validation set assertions to the store + snaps1 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "required", + "revision": "3", + }} + vsetAs := s.validationSetAssertForSnaps(c, "bar", "2", "2", snaps1) + c.Assert(s.storeSigning.Add(vsetAs), IsNil) + snaps2 := []interface{}{ + map[string]interface{}{ + "id": "aAqKhntON3vR7kwEbVPsILm7bUViPDaa", + "name": "other-snap", + "presence": "optional", + }} + vsetAs2 := s.validationSetAssertForSnaps(c, "baz", "1", "1", snaps2) + c.Assert(s.storeSigning.Add(vsetAs2), IsNil) + + installedSnaps := []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("some-snap", "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.Revision{N: 3}), + } + err := assertstate.TryEnforceValidationSets(st, []string{fmt.Sprintf("%s/bar", s.dev1Acct.AccountID()), fmt.Sprintf("%s/baz=1", s.dev1Acct.AccountID())}, 0, installedSnaps, nil) + c.Assert(err, IsNil) + + // new assertions were commited + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "bar", + "sequence": "2", + }) + c.Assert(err, IsNil) + + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "baz", + "sequence": "1", + }) + c.Assert(err, IsNil) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) + + // tracking was updated + var tr assertstate.ValidationSetTracking + c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) + c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Enforce, + Current: 2, + }) + c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "baz", &tr), IsNil) + c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "baz", + Mode: assertstate.Enforce, + Current: 1, + PinnedAt: 1, + }) + + // and it was added to the history + // note, normally there would be a map with just "foo" as well, but there isn't one + // since we created the initial state in the test manually. + vshist, err := assertstate.ValidationSetsHistory(st) + c.Assert(err, IsNil) + c.Check(vshist, DeepEquals, []map[string]*assertstate.ValidationSetTracking{{ + fmt.Sprintf("%s/foo", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 1, + }, + fmt.Sprintf("%s/bar", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Enforce, + Current: 2, + }, + fmt.Sprintf("%s/baz", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "baz", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 1, + }, + }}) +} + +func (s *assertMgrSuite) TestTryEnforceValidationSetsAssertionsAlreadyTrackedUpdateOK(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // pretend we are already enforcing a validation set foo + snaps3 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "required", + }} + vsetAs1 := s.validationSetAssertForSnaps(c, "foo", "1", "1", snaps3) + c.Assert(assertstate.Add(st, vsetAs1), IsNil) + assertstate.UpdateValidationSet(st, &assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 1, + }) + + // add validation set assertions to the store + snaps1 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "required", + "revision": "3", + }} + vsetAs2 := s.validationSetAssertForSnaps(c, "foo", "2", "2", snaps1) + c.Assert(s.storeSigning.Add(vsetAs2), IsNil) + + installedSnaps := []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("some-snap", "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.Revision{N: 3}), + } + err := assertstate.TryEnforceValidationSets(st, []string{fmt.Sprintf("%s/foo", s.dev1Acct.AccountID())}, 0, installedSnaps, nil) + c.Assert(err, IsNil) + + // new assertion was commited + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "foo", + "sequence": "2", + }) + c.Assert(err, IsNil) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) + + // tracking was updated + var tr assertstate.ValidationSetTracking + c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "foo", &tr), IsNil) + c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 2, + }) + + // and it was added to the history + // note, normally there would be a map with just "foo" as well, but there isn't one + // since we created the initial state in the test manually. + vshist, err := assertstate.ValidationSetsHistory(st) + c.Assert(err, IsNil) + c.Check(vshist, DeepEquals, []map[string]*assertstate.ValidationSetTracking{{ + fmt.Sprintf("%s/foo", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 2, + }, + }}) +} + +func (s *assertMgrSuite) TestTryEnforceValidationSetsAssertionsConflictError(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // pretend we are already enforcing a validation set foo + snaps3 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "required", + }} + vsetAs3 := s.validationSetAssertForSnaps(c, "foo", "1", "1", snaps3) + c.Assert(assertstate.Add(st, vsetAs3), IsNil) + assertstate.UpdateValidationSet(st, &assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 1, + }) + + // add a validation set assertion to the store + snaps1 := []interface{}{ + map[string]interface{}{ + "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", + "name": "some-snap", + "presence": "invalid", + }} + vsetAs := s.validationSetAssertForSnaps(c, "bar", "2", "2", snaps1) + c.Assert(s.storeSigning.Add(vsetAs), IsNil) + + // try to enforce extra validation sets bar and baz. some-snap is present (and required by foo at any revision), + // but needs to be at revision 3 to satisfy bar. invalid-snap is present but is invalid for baz. + installedSnaps := []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("some-snap", "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.Revision{N: 1}), + } + err := assertstate.TryEnforceValidationSets(st, []string{fmt.Sprintf("%s/bar", s.dev1Acct.AccountID())}, 0, installedSnaps, nil) + _, ok := err.(*snapasserts.ValidationSetsConflictError) + c.Assert(ok, Equals, true) + c.Assert(err, ErrorMatches, `validation sets are in conflict:\n- cannot constrain snap "some-snap" as both invalid \(.*/bar\) and required at any revision \(.*/foo\).*`) + + // new assertion wasn't commited + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "bar", + "sequence": "2", + }) + c.Assert(asserts.IsNotFound(err), Equals, true) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) +} + func (s *assertMgrSuite) TestMonitorValidationSet(c *C) { st := s.state |
