summaryrefslogtreecommitdiff
diff options
authorMiguel Pires <miguel.pires@canonical.com>2022-10-26 16:09:21 +0100
committerMiguel Pires <miguelpires94@gmail.com>2023-07-25 14:08:37 +0100
commit3355b78c215a1a3b1a6b438c1f191731372322e9 (patch)
treee42b2ee4a98fd87733398d9b4e2fc582d529ad94
parentbc759f4720c516e941b74f471ff68e0d2eeb0899 (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.go14
-rw-r--r--overlord/snapstate/autorefresh_gating_test.go54
-rw-r--r--overlord/snapstate/snapstate.go2
-rw-r--r--overlord/snapstate/snapstate_remove_test.go10
-rw-r--r--overlord/snapstate/snapstate_update_test.go10
-rw-r--r--overlord/snapstate/storehelpers.go65
-rw-r--r--overlord/snapstate/storehelpers_test.go73
-rw-r--r--store/store_action.go10
-rw-r--r--store/store_action_test.go90
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)