diff options
| author | Miguel Pires <miguel.pires@canonical.com> | 2022-10-26 16:09:21 +0100 |
|---|---|---|
| committer | Miguel Pires <miguelpires94@gmail.com> | 2023-07-25 14:08:37 +0100 |
| commit | 3355b78c215a1a3b1a6b438c1f191731372322e9 (patch) | |
| tree | e42b2ee4a98fd87733398d9b4e2fc582d529ad94 | |
| parent | bc759f4720c516e941b74f471ff68e0d2eeb0899 (diff) | |
o/snapstate: send refresh hold data in store requests
Signed-off-by: Miguel Pires <miguel.pires@canonical.com>
| -rw-r--r-- | overlord/snapstate/autorefresh_gating.go | 14 | ||||
| -rw-r--r-- | overlord/snapstate/autorefresh_gating_test.go | 54 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate.go | 2 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate_remove_test.go | 10 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate_update_test.go | 10 | ||||
| -rw-r--r-- | overlord/snapstate/storehelpers.go | 65 | ||||
| -rw-r--r-- | overlord/snapstate/storehelpers_test.go | 73 | ||||
| -rw-r--r-- | store/store_action.go | 10 | ||||
| -rw-r--r-- | store/store_action_test.go | 90 |
9 files changed, 280 insertions, 48 deletions
diff --git a/overlord/snapstate/autorefresh_gating.go b/overlord/snapstate/autorefresh_gating.go index cd72a7c54d..10ce65477a 100644 --- a/overlord/snapstate/autorefresh_gating.go +++ b/overlord/snapstate/autorefresh_gating.go @@ -444,8 +444,9 @@ func pruneSnapsHold(st *state.State, snapName string) error { } // HeldSnaps returns all snaps that are held at the given level or at more -// restricting ones and shouldn't be refreshed. -func HeldSnaps(st *state.State, level HoldLevel) (map[string]bool, error) { +// restricting ones and shouldn't be refreshed. The snaps are mapped to a list +// of snaps with currently effective holds on them. +func HeldSnaps(st *state.State, level HoldLevel) (map[string][]string, error) { gating, err := refreshGating(st) if err != nil { return nil, err @@ -456,8 +457,7 @@ func HeldSnaps(st *state.State, level HoldLevel) (map[string]bool, error) { now := timeNow() - held := make(map[string]bool) -Loop: + held := make(map[string][]string) for heldSnap, holds := range gating { lastRefresh, err := lastRefreshed(st, heldSnap) if err != nil { @@ -479,8 +479,8 @@ Loop: if hold.HoldUntil.Before(now) { continue } - held[heldSnap] = true - continue Loop + + held[heldSnap] = append(held[heldSnap], holdingSnap) } } return held, nil @@ -766,7 +766,7 @@ var snapsToRefresh = func(gatingTask *state.Task) ([]*refreshCandidate, error) { var skipped []string var candidates []*refreshCandidate for _, s := range snaps { - if !held[s.InstanceName()] { + if _, ok := held[s.InstanceName()]; !ok { candidates = append(candidates, s) } else { skipped = append(skipped, s.InstanceName()) diff --git a/overlord/snapstate/autorefresh_gating_test.go b/overlord/snapstate/autorefresh_gating_test.go index 49563f4d5f..a5d5f3f8c6 100644 --- a/overlord/snapstate/autorefresh_gating_test.go +++ b/overlord/snapstate/autorefresh_gating_test.go @@ -667,21 +667,21 @@ func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) { held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true, "snap-e": true, "snap-f": true}) + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{"snap-b": {"snap-a"}, "snap-c": {"snap-a", "snap-d"}, "snap-d": {"snap-d"}, "snap-e": {"snap-e"}, "snap-f": {"snap-e"}}) // check that specifying a subset of snaps held by snap-e only unblocks those snaps c.Assert(snapstate.ProceedWithRefresh(st, "snap-e", []string{"snap-f"}), IsNil) held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true, "snap-e": true}) + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{"snap-b": {"snap-a"}, "snap-c": {"snap-a", "snap-d"}, "snap-d": {"snap-d"}, "snap-e": {"snap-e"}}) // clear the rest of snap-e's held snaps c.Assert(snapstate.ProceedWithRefresh(st, "snap-e", nil), IsNil) c.Assert(snapstate.ProceedWithRefresh(st, "snap-a", nil), IsNil) held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-d": true}) + c.Check(held, DeepEquals, map[string][]string{"snap-c": {"snap-d"}, "snap-d": {"snap-d"}}) c.Assert(snapstate.ProceedWithRefresh(st, "snap-d", nil), IsNil) held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) @@ -734,9 +734,9 @@ func (s *autorefreshGatingSuite) TestDontHoldSomeSnapsIfSomeFail(c *C) { c.Assert(err, IsNil) // note, snap-b couldn't hold base-snap-b anymore so we didn't hold snap-b // and snap-c. base-snap-b was held by snap-bb. - c.Check(held, DeepEquals, map[string]bool{ - "snap-d": true, - "base-snap-b": true, + c.Check(held, DeepEquals, map[string][]string{ + "snap-d": {"snap-d"}, + "base-snap-b": {"snap-bb"}, }) } @@ -764,7 +764,7 @@ func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) { // validity held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true}) + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{"snap-c": {"snap-a", "snap-d"}, "snap-b": {"snap-a"}, "snap-d": {"snap-d"}}) candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}} @@ -780,7 +780,7 @@ func (s *autorefreshGatingSuite) TestPruneGatingHelper(c *C) { }) held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-c": true}) + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{"snap-c": {"snap-a", "snap-d"}}) } func (s *autorefreshGatingSuite) TestPruneGatingHelperWithSystemHeld(c *C) { @@ -810,7 +810,7 @@ func (s *autorefreshGatingSuite) TestPruneGatingHelperWithSystemHeld(c *C) { // validity held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true, "snap-d": true}) + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{"snap-c": {"snap-a", "snap-d"}, "snap-b": {"snap-a", "system"}, "snap-d": {"snap-d"}}) candidates := map[string]*snapstate.RefreshCandidate{"snap-c": {}} @@ -831,7 +831,7 @@ func (s *autorefreshGatingSuite) TestPruneGatingHelperWithSystemHeld(c *C) { }) held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-c": true, "snap-b": true}) + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{"snap-c": {"snap-d", "snap-a"}, "snap-b": {"system"}}) } func (s *autorefreshGatingSuite) TestPruneGatingHelperNoGating(c *C) { @@ -898,7 +898,7 @@ func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelper(c *C) { held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-d": true}) + c.Check(held, DeepEquals, map[string][]string{"snap-d": {"snap-d"}}) } func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelperWithSystemHeld(c *C) { @@ -943,7 +943,7 @@ func (s *autorefreshGatingSuite) TestResetGatingForRefreshedHelperWithSystemHeld held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-d": true}) + c.Check(held, DeepEquals, map[string][]string{"snap-b": {"system"}, "snap-d": {"snap-d"}}) } func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) { @@ -966,11 +966,11 @@ func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) { // validity check held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{ - "snap-a": true, - "snap-b": true, - "snap-c": true, - "snap-d": true, + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{ + "snap-a": {"snap-a"}, + "snap-b": {"snap-a"}, + "snap-c": {"snap-a", "snap-d"}, + "snap-d": {"snap-a"}, }) c.Check(snapstate.PruneSnapsHold(st, "snap-a"), IsNil) @@ -978,8 +978,8 @@ func (s *autorefreshGatingSuite) TestPruneSnapsHold(c *C) { // after pruning snap-a, snap-c is still held. held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{ - "snap-c": true, + c.Check(held, DeepEquals, map[string][]string{ + "snap-c": {"snap-d"}, }) var gating map[string]map[string]*snapstate.HoldState c.Assert(st.Get("snaps-hold", &gating), IsNil) @@ -1489,9 +1489,9 @@ func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) { // validity check heldSnaps, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(heldSnaps, DeepEquals, map[string]bool{ - "snap-a": true, - "snap-d": true, + c.Check(heldSnaps, DeepEquals, map[string][]string{ + "snap-a": {"gating-snap"}, + "snap-d": {"gating-snap"}, }) names, tss, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") @@ -1577,8 +1577,8 @@ func (s *autorefreshGatingSuite) TestAutoRefreshPhase1(c *C) { heldSnaps, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) // snap-d got removed from held snaps. - c.Check(heldSnaps, DeepEquals, map[string]bool{ - "snap-a": true, + c.Check(heldSnaps, DeepEquals, map[string][]string{ + "snap-a": {"gating-snap"}, }) } @@ -2900,11 +2900,11 @@ func (s *autorefreshGatingSuite) TestHoldLevels(c *C) { held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-b": true, "snap-c": true, "snap-d": true, "snap-e": true, "snap-f": true}) + c.Check(held, testutil.DeepUnsortedMatches, map[string][]string{"snap-b": {"system"}, "snap-c": {"snap-d", "system"}, "snap-d": {"snap-d"}, "snap-e": {"system"}, "snap-f": {"system"}}) held, err = snapstate.HeldSnaps(st, snapstate.HoldGeneral) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"snap-d": true, "snap-e": true, "snap-f": true}) + c.Check(held, DeepEquals, map[string][]string{"snap-d": {"snap-d"}, "snap-e": {"system"}, "snap-f": {"system"}}) } func (s *autorefreshGatingSuite) TestSnapsCanBeHeldForeverBySystem(c *C) { @@ -2929,7 +2929,7 @@ func (s *autorefreshGatingSuite) TestSnapsCanBeHeldForeverBySystem(c *C) { gatedSnaps, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(gatedSnaps, DeepEquals, map[string]bool{"snap-a": true}) + c.Check(gatedSnaps, DeepEquals, map[string][]string{"snap-a": {"system"}}) } func (s *autorefreshGatingSuite) TestSnapsNotHeldForeverBySnaps(c *C) { diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index de5cc62076..f3194d2228 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -1805,7 +1805,7 @@ func filterHeldSnaps(st *state.State, updates []minimalInstallInfo, flags *Flags filteredUpdates := make([]minimalInstallInfo, 0, len(updates)) for _, update := range updates { - if !heldSnaps[update.InstanceName()] { + if _, ok := heldSnaps[update.InstanceName()]; !ok { filteredUpdates = append(filteredUpdates, update) } } diff --git a/overlord/snapstate/snapstate_remove_test.go b/overlord/snapstate/snapstate_remove_test.go index 12758008b8..f4a125aefc 100644 --- a/overlord/snapstate/snapstate_remove_test.go +++ b/overlord/snapstate/snapstate_remove_test.go @@ -1716,9 +1716,9 @@ func (s *snapmgrTestSuite) TestRemovePrunesRefreshGatingDataOnLastRevision(c *C) held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{ - "foo-snap": true, - "some-snap": true, + c.Check(held, DeepEquals, map[string][]string{ + "foo-snap": {"some-snap"}, + "some-snap": {"another-snap"}, }) chg := st.NewChange("remove", "remove a snap") @@ -1771,7 +1771,7 @@ func (s *snapmgrTestSuite) TestRemoveKeepsGatingDataIfNotLastRevision(c *C) { held, err := snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"some-snap": true}) + c.Check(held, DeepEquals, map[string][]string{"some-snap": {"some-snap"}}) chg := st.NewChange("remove", "remove a snap") ts, err := snapstate.Remove(st, "some-snap", snap.R(12), nil) @@ -1788,7 +1788,7 @@ func (s *snapmgrTestSuite) TestRemoveKeepsGatingDataIfNotLastRevision(c *C) { // held snaps are intact held, err = snapstate.HeldSnaps(st, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{"some-snap": true}) + c.Check(held, DeepEquals, map[string][]string{"some-snap": {"some-snap"}}) // refresh-candidates are intact var candidates map[string]*snapstate.RefreshCandidate diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go index c95c4d80e1..d286227c39 100644 --- a/overlord/snapstate/snapstate_update_test.go +++ b/overlord/snapstate/snapstate_update_test.go @@ -1199,9 +1199,9 @@ func (s *snapmgrTestSuite) TestUpdateResetsHoldState(c *C) { // validity check held, err := snapstate.HeldSnaps(s.state, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{ - "some-snap": true, - "other-snap": true, + c.Check(held, DeepEquals, map[string][]string{ + "some-snap": {"gating-snap"}, + "other-snap": {"gating-snap"}, }) _, err = snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{}) @@ -1210,8 +1210,8 @@ func (s *snapmgrTestSuite) TestUpdateResetsHoldState(c *C) { // and it is not held anymore (but other-snap still is) held, err = snapstate.HeldSnaps(s.state, snapstate.HoldAutoRefresh) c.Assert(err, IsNil) - c.Check(held, DeepEquals, map[string]bool{ - "other-snap": true, + c.Check(held, DeepEquals, map[string][]string{ + "other-snap": {"gating-snap"}, }) } diff --git a/overlord/snapstate/storehelpers.go b/overlord/snapstate/storehelpers.go index e769655191..9d91293130 100644 --- a/overlord/snapstate/storehelpers.go +++ b/overlord/snapstate/storehelpers.go @@ -24,11 +24,13 @@ import ( "errors" "fmt" "sort" + "time" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/naming" @@ -554,10 +556,20 @@ func currentSnapsImpl(st *state.State) ([]*store.CurrentSnap, error) { return nil, nil } - return collectCurrentSnaps(snapStates, nil) + var names []string + for _, snapst := range snapStates { + names = append(names, snapst.InstanceName()) + } + + holds, err := SnapHolds(st, names) + if err != nil { + return nil, err + } + + return collectCurrentSnaps(snapStates, holds, nil) } -func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState) error) (curSnaps []*store.CurrentSnap, err error) { +func collectCurrentSnaps(snapStates map[string]*SnapState, holds map[string][]string, consider func(*store.CurrentSnap, *SnapState) error) (curSnaps []*store.CurrentSnap, err error) { curSnaps = make([]*store.CurrentSnap, 0, len(snapStates)) for _, snapst := range snapStates { @@ -589,6 +601,7 @@ func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store. IgnoreValidation: snapst.IgnoreValidation, Epoch: snapInfo.Epoch, CohortKey: snapst.CohortKey, + HeldBy: holds[snapInfo.InstanceName()], } curSnaps = append(curSnaps, installed) @@ -721,8 +734,14 @@ func refreshCandidates(ctx context.Context, st *state.State, names []string, rev nCands++ return nil } + + holds, err := SnapHolds(st, names) + if err != nil { + return nil, nil, nil, err + } + // determine current snaps and collect candidates for refresh - curSnaps, err := collectCurrentSnaps(snapStates, addCand) + curSnaps, err := collectCurrentSnaps(snapStates, holds, addCand) if err != nil { return nil, nil, nil, err } @@ -781,6 +800,46 @@ func refreshCandidates(ctx context.Context, st *state.State, names []string, rev return updates, stateByInstanceName, ignoreValidationByInstanceName, nil } +// SnapHolds returns a map of held snaps to lists of holding snaps (including +// "system" for user holds). +func SnapHolds(st *state.State, snaps []string) (map[string][]string, error) { + var allSnapsHold string + tr := config.NewTransaction(st) + err := tr.Get("core", "refresh.hold", &allSnapsHold) + if err != nil && !config.IsNoOption(err) { + return nil, err + } + + var allSnapsHoldTime time.Time + if allSnapsHold != "" { + if allSnapsHold == "forever" { + allSnapsHoldTime = timeNow().Add(maxDuration) + } else { + allSnapsHoldTime, err = time.Parse(time.RFC3339, allSnapsHold) + if err != nil { + return nil, err + } + } + } + + holds, err := HeldSnaps(st, HoldGeneral) + if err != nil { + return nil, err + } + + for _, snap := range snaps { + if !strutil.ListContains(holds[snap], "system") && allSnapsHoldTime.After(timeNow()) { + if holds == nil { + holds = make(map[string][]string) + } + + holds[snap] = append(holds[snap], "system") + } + } + + return holds, nil +} + func installCandidates(st *state.State, names []string, revOpts []*RevisionOptions, channel string, user *auth.UserState) ([]store.SnapActionResult, error) { curSnaps, err := currentSnaps(st) if err != nil { diff --git a/overlord/snapstate/storehelpers_test.go b/overlord/snapstate/storehelpers_test.go index 4b25da4424..33f899ab22 100644 --- a/overlord/snapstate/storehelpers_test.go +++ b/overlord/snapstate/storehelpers_test.go @@ -22,11 +22,13 @@ package snapstate_test import ( "context" "fmt" + "time" . "gopkg.in/check.v1" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -483,3 +485,74 @@ func (s *snapmgrTestSuite) TestInstallSizeRemotePrereq(c *C) { op := s.fakeStore.fakeBackend.ops.MustFindOp(c, "storesvc-snap-action:action") c.Assert(op.action.InstanceName, Equals, "snap-content-slot") } + +func (s *snapmgrTestSuite) TestSnapHoldsSnapsOnly(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + mockInstalledSnap(c, st, snapAyaml, false) + mockInstalledSnap(c, st, snapByaml, false) + mockInstalledSnap(c, st, snapCyaml, false) + + //mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c") + + //now, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") + //c.Assert(err, IsNil) + //restore := snapstate.MockTimeNow(func() time.Time { + // return now + //}) + //defer restore() + + _, err := snapstate.HoldRefresh(st, snapstate.HoldGeneral, "snap-c", 24*time.Hour, "snap-a", "snap-b") + c.Assert(err, IsNil) + + snapHolds, err := snapstate.SnapHolds(st, []string{"snap-a", "snap-b", "snap-c"}) + c.Assert(err, IsNil) + c.Check(snapHolds, DeepEquals, map[string][]string{ + "snap-a": {"snap-c"}, + "snap-b": {"snap-c"}, + }) +} + +func (s *snapmgrTestSuite) TestSnapHoldsSystemOnly(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + mockInstalledSnap(c, st, snapAyaml, false) + mockInstalledSnap(c, st, snapByaml, false) + + mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b") + + now, err := time.Parse(time.RFC3339, "2021-05-10T10:00:00Z") + c.Assert(err, IsNil) + restore := snapstate.MockTimeNow(func() time.Time { + return now + }) + defer restore() + + tr := config.NewTransaction(st) + err = tr.Set("core", "refresh.hold", "2021-05-10T11:00:00Z") + c.Assert(err, IsNil) + tr.Commit() + + snapHolds, err := snapstate.SnapHolds(st, []string{"snap-a", "snap-b"}) + c.Assert(err, IsNil) + c.Check(snapHolds, DeepEquals, map[string][]string{ + "snap-a": {"system"}, + "snap-b": {"system"}, + }) + + tr = config.NewTransaction(st) + err = tr.Set("core", "refresh.hold", "forever") + c.Assert(err, IsNil) + tr.Commit() + + snapHolds, err = snapstate.SnapHolds(st, []string{"snap-a", "snap-b"}) + c.Assert(err, IsNil) + c.Check(snapHolds, DeepEquals, map[string][]string{ + "snap-a": {"system"}, + "snap-b": {"system"}, + }) +} diff --git a/store/store_action.go b/store/store_action.go index 2e7582b574..4a5112c6a7 100644 --- a/store/store_action.go +++ b/store/store_action.go @@ -59,6 +59,9 @@ type CurrentSnap struct { CohortKey string // ValidationSets is an optional array of validation set primary keys. ValidationSets []snapasserts.ValidationSetKey + // HeldBy is an optional array of snaps with holds on the current snap's + // refreshes. The "system" snap represents a hold placed by the user. + HeldBy []string } type AssertionQuery interface { @@ -80,6 +83,9 @@ type currentSnapV2JSON struct { CohortKey string `json:"cohort-key,omitempty"` // ValidationSets is an optional array of validation set primary keys. ValidationSets [][]string `json:"validation-sets,omitempty"` + // Held is an optional map that can contain a "by" key mapping to a list of + // snaps with holds on the current snap (see CurrentSnap#Held). + Held map[string][]string `json:"held,omitempty"` } type SnapActionFlags int @@ -347,6 +353,10 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act CohortKey: curSnap.CohortKey, ValidationSets: valsetKeys, } + + if len(curSnap.HeldBy) > 0 { + curSnapJSONs[i].Held = map[string][]string{"by": curSnap.HeldBy} + } } // do not include toResolveSeq len in the initial size since it may have diff --git a/store/store_action_test.go b/store/store_action_test.go index 99ea539042..4520dab8bb 100644 --- a/store/store_action_test.go +++ b/store/store_action_test.go @@ -2509,6 +2509,96 @@ func (s *storeActionSuite) TestSnapActionRefreshStableInstanceKey(c *C) { c.Assert(resultsAgain, DeepEquals, results) } +func (s *storeActionSuite) TestSnapActionRefreshWithHeld(c *C) { + 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, 1) + c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ + "snap-id": helloWorldSnapID, + "instance-key": helloWorldSnapID, + "revision": float64(1), + "tracking-channel": "stable", + "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, + "held": map[string]interface{}{"by": []interface{}{"foo", "bar"}}, + }) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "refresh", + "instance-key": helloWorldSnapID, + "snap-id": helloWorldSnapID, + "channel": "stable", + }) + + io.WriteString(w, `{ + "results": [{ + "result": "refresh", + "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + 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) + + results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ + { + InstanceName: "hello-world", + SnapID: helloWorldSnapID, + TrackingChannel: "stable", + Revision: snap.R(1), + RefreshedDate: helloRefreshedDate, + HeldBy: []string{"foo", "bar"}, + }, + }, []*store.SnapAction{ + { + Action: "refresh", + SnapID: helloWorldSnapID, + Channel: "stable", + InstanceName: "hello-world", + }, + }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) + + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].SnapName(), Equals, "hello-world") + c.Assert(results[0].InstanceName(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) +} + func (s *storeActionSuite) TestSnapActionRefreshWithValidationSets(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assertRequest(c, r, "POST", snapActionPath) |
