summaryrefslogtreecommitdiff
diff options
authorPaweł Stołowski <stolowski@gmail.com>2020-10-16 12:16:09 +0200
committerPaweł Stołowski <stolowski@gmail.com>2020-10-16 12:16:09 +0200
commitee1a4d724443ef38d9b53f324ad5ffb8a6592de0 (patch)
tree9576fe23917f1bfffb30e4de8582a48984bca507
parent86756459e1c9f0c2821d09436a4f80588405a2ba (diff)
parenta1aaa70950c0011a81d58cd06a823250234f0b91 (diff)
Merge branch 'master' into remove-current-lastremove-current-last
-rw-r--r--client/snap_op.go4
-rw-r--r--client/snap_op_test.go31
-rw-r--r--cmd/snap/cmd_snap_op.go37
-rw-r--r--cmd/snap/cmd_snap_op_test.go19
-rw-r--r--daemon/api.go9
-rw-r--r--daemon/api_test.go64
-rw-r--r--overlord/assertstate/validation_set_tracking.go127
-rw-r--r--overlord/assertstate/validation_set_tracking_test.go159
-rw-r--r--overlord/snapstate/flags.go3
-rw-r--r--overlord/snapstate/handlers.go2
-rw-r--r--overlord/snapstate/handlers_link_test.go62
-rw-r--r--overlord/snapstate/snapstate.go5
-rw-r--r--overlord/snapstate/snapstate_install_test.go57
13 files changed, 567 insertions, 12 deletions
diff --git a/client/snap_op.go b/client/snap_op.go
index 83b62ba747..3e239cb706 100644
--- a/client/snap_op.go
+++ b/client/snap_op.go
@@ -40,6 +40,7 @@ type SnapOptions struct {
Classic bool `json:"classic,omitempty"`
Dangerous bool `json:"dangerous,omitempty"`
IgnoreValidation bool `json:"ignore-validation,omitempty"`
+ IgnoreRunning bool `json:"ignore-running,omitempty"`
Unaliased bool `json:"unaliased,omitempty"`
Purge bool `json:"purge,omitempty"`
Amend bool `json:"amend,omitempty"`
@@ -74,6 +75,9 @@ func (opts *SnapOptions) writeModeFields(mw *multipart.Writer) error {
}
func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error {
+ if err := writeFieldBool(mw, "ignore-running", opts.IgnoreRunning); err != nil {
+ return err
+ }
return writeFieldBool(mw, "unaliased", opts.Unaliased)
}
diff --git a/client/snap_op_test.go b/client/snap_op_test.go
index 4790a3166b..86c7a18317 100644
--- a/client/snap_op_test.go
+++ b/client/snap_op_test.go
@@ -242,6 +242,37 @@ func (cs *clientSuite) TestClientOpInstallPath(c *check.C) {
c.Check(id, check.Equals, "66b3")
}
+func (cs *clientSuite) TestClientOpInstallPathIgnoreRunning(c *check.C) {
+ cs.status = 202
+ cs.rsp = `{
+ "change": "66b3",
+ "status-code": 202,
+ "type": "async"
+ }`
+ bodyData := []byte("snap-data")
+
+ snap := filepath.Join(c.MkDir(), "foo.snap")
+ err := ioutil.WriteFile(snap, bodyData, 0644)
+ c.Assert(err, check.IsNil)
+
+ id, err := cs.cli.InstallPath(snap, "", &client.SnapOptions{IgnoreRunning: true})
+ c.Assert(err, check.IsNil)
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
+ c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
+ c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"ignore-running\"\r\n\r\ntrue\r\n.*")
+
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"))
+ c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*")
+ _, ok := cs.req.Context().Deadline()
+ c.Assert(ok, check.Equals, false)
+ c.Check(id, check.Equals, "66b3")
+}
+
func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) {
cs.status = 202
cs.rsp = `{
diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go
index 1dd1b41c66..0d3085f862 100644
--- a/cmd/snap/cmd_snap_op.go
+++ b/cmd/snap/cmd_snap_op.go
@@ -474,8 +474,9 @@ type cmdInstall struct {
Name string `long:"name"`
- Cohort string `long:"cohort"`
- Positional struct {
+ Cohort string `long:"cohort"`
+ IgnoreRunning bool `long:"ignore-running" hidden:"yes"`
+ Positional struct {
Snaps []remoteSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes" required:"yes"`
}
@@ -591,11 +592,12 @@ func (x *cmdInstall) Execute([]string) error {
dangerous := x.Dangerous || x.ForceDangerous
opts := &client.SnapOptions{
- Channel: x.Channel,
- Revision: x.Revision,
- Dangerous: dangerous,
- Unaliased: x.Unaliased,
- CohortKey: x.Cohort,
+ Channel: x.Channel,
+ Revision: x.Revision,
+ Dangerous: dangerous,
+ Unaliased: x.Unaliased,
+ CohortKey: x.Cohort,
+ IgnoreRunning: x.IgnoreRunning,
}
x.setModes(opts)
@@ -637,6 +639,7 @@ type cmdRefresh struct {
List bool `long:"list"`
Time bool `long:"time"`
IgnoreValidation bool `long:"ignore-validation"`
+ IgnoreRunning bool `long:"ignore-running" hidden:"yes"`
Positional struct {
Snaps []installedSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes"`
@@ -802,6 +805,7 @@ func (x *cmdRefresh) Execute([]string) error {
Amend: x.Amend,
Channel: x.Channel,
IgnoreValidation: x.IgnoreValidation,
+ IgnoreRunning: x.IgnoreRunning,
Revision: x.Revision,
CohortKey: x.Cohort,
LeaveCohort: x.LeaveCohort,
@@ -817,6 +821,9 @@ func (x *cmdRefresh) Execute([]string) error {
if x.IgnoreValidation {
return errors.New(i18n.G("a single snap name must be specified when ignoring validation"))
}
+ if x.IgnoreRunning {
+ return errors.New(i18n.G("a single snap name must be specified when ignoring running apps and hooks"))
+ }
return x.refreshMany(names, nil)
}
@@ -970,8 +977,9 @@ type cmdRevert struct {
waitMixin
modeMixin
- Revision string `long:"revision"`
- Positional struct {
+ Revision string `long:"revision"`
+ IgnoreRunning bool `long:"ignore-running" hidden:"yes"`
+ Positional struct {
Snap installedSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes" required:"yes"`
}
@@ -996,7 +1004,10 @@ func (x *cmdRevert) Execute(args []string) error {
}
name := string(x.Positional.Snap)
- opts := &client.SnapOptions{Revision: x.Revision}
+ opts := &client.SnapOptions{
+ Revision: x.Revision,
+ IgnoreRunning: x.IgnoreRunning,
+ }
x.setModes(opts)
changeID, err := x.client.Revert(name, opts)
if err != nil {
@@ -1095,6 +1106,8 @@ func init() {
"name": i18n.G("Install the snap file under the given instance name"),
// TRANSLATORS: This should not start with a lowercase letter.
"cohort": i18n.G("Install the snap in the given cohort"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "ignore-running": i18n.G("Ignore running hooks or applications blocking the installation"),
}), nil)
addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} },
colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{
@@ -1109,6 +1122,8 @@ func init() {
// TRANSLATORS: This should not start with a lowercase letter.
"ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"),
// TRANSLATORS: This should not start with a lowercase letter.
+ "ignore-running": i18n.G("Ignore running hooks or applications blocking the refresh"),
+ // TRANSLATORS: This should not start with a lowercase letter.
"cohort": i18n.G("Refresh the snap into the given cohort"),
// TRANSLATORS: This should not start with a lowercase letter.
"leave-cohort": i18n.G("Refresh the snap out of its cohort"),
@@ -1119,6 +1134,8 @@ func init() {
addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"revision": i18n.G("Revert to the given revision"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "ignore-running": i18n.G("Ignore running hooks or applications blocking the revert"),
}), nil)
addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs).also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go
index 61d61d0bb5..13533a7a06 100644
--- a/cmd/snap/cmd_snap_op_test.go
+++ b/cmd/snap/cmd_snap_op_test.go
@@ -207,6 +207,25 @@ func (s *SnapOpSuite) TestInstall(c *check.C) {
c.Check(s.srv.n, check.Equals, s.srv.total)
}
+func (s *SnapOpSuite) TestInstallIgnoreRunning(c *check.C) {
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "install",
+ "ignore-running": true,
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--ignore-running", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`)
+ c.Check(s.Stderr(), check.Equals, "")
+ // ensure that the fake server api was actually hit
+ c.Check(s.srv.n, check.Equals, s.srv.total)
+}
+
func (s *SnapOpSuite) TestInstallNoPATH(c *check.C) {
// PATH restored by test tear down
os.Setenv("PATH", "/bin:/usr/bin:/sbin:/usr/sbin")
diff --git a/daemon/api.go b/daemon/api.go
index b5eb361351..541c01b5a9 100644
--- a/daemon/api.go
+++ b/daemon/api.go
@@ -796,6 +796,7 @@ type snapInstruction struct {
JailMode bool `json:"jailmode"`
Classic bool `json:"classic"`
IgnoreValidation bool `json:"ignore-validation"`
+ IgnoreRunning bool `json:"ignore-running"`
Unaliased bool `json:"unaliased"`
Purge bool `json:"purge,omitempty"`
// dropping support temporarely until flag confusion is sorted,
@@ -831,6 +832,10 @@ func (inst *snapInstruction) installFlags() (snapstate.Flags, error) {
if inst.Unaliased {
flags.Unaliased = true
}
+ if inst.IgnoreRunning {
+ flags.IgnoreRunning = true
+ }
+
return flags, nil
}
@@ -1026,6 +1031,9 @@ func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSe
if inst.IgnoreValidation {
flags.IgnoreValidation = true
}
+ if inst.IgnoreRunning {
+ flags.IgnoreRunning = true
+ }
if inst.Amend {
flags.Amend = true
}
@@ -1424,6 +1432,7 @@ func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response {
flags.RemoveSnapPath = true
flags.Unaliased = isTrue(form, "unaliased")
+ flags.IgnoreRunning = isTrue(form, "ignore-running")
// find the file for the "snap" form field
var snapBody multipart.File
diff --git a/daemon/api_test.go b/daemon/api_test.go
index cbc7ed87ca..b13af0c6f4 100644
--- a/daemon/api_test.go
+++ b/daemon/api_test.go
@@ -4053,6 +4053,43 @@ func (s *apiSuite) TestRefreshIgnoreValidation(c *check.C) {
c.Check(summary, check.Equals, `Refresh "some-snap" snap`)
}
+func (s *apiSuite) TestRefreshIgnoreRunning(c *check.C) {
+ var calledFlags snapstate.Flags
+ installQueue := []string{}
+
+ snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+ installQueue = append(installQueue, name)
+
+ t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ return nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "refresh",
+ IgnoreRunning: true,
+ Snaps: []string{"some-snap"},
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ summary, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ flags := snapstate.Flags{}
+ flags.IgnoreRunning = true
+
+ c.Check(calledFlags, check.DeepEquals, flags)
+ c.Check(err, check.IsNil)
+ c.Check(installQueue, check.DeepEquals, []string{"some-snap"})
+ c.Check(summary, check.Equals, `Refresh "some-snap" snap`)
+}
+
func (s *apiSuite) TestRefreshCohort(c *check.C) {
cohort := ""
@@ -6751,6 +6788,33 @@ func (s *apiSuite) TestInstallUnaliased(c *check.C) {
c.Check(calledFlags.Unaliased, check.Equals, true)
}
+func (s *apiSuite) TestInstallIgnoreRunning(c *check.C) {
+ var calledFlags snapstate.Flags
+
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ calledFlags = flags
+
+ t := s.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+
+ d := s.daemon(c)
+ inst := &snapInstruction{
+ Action: "install",
+ // Install the snap without enabled automatic aliases
+ IgnoreRunning: true,
+ Snaps: []string{"fake"},
+ }
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, _, err := inst.dispatch()(inst, st)
+ c.Check(err, check.IsNil)
+
+ c.Check(calledFlags.IgnoreRunning, check.Equals, true)
+}
+
func (s *apiSuite) TestInstallPathUnaliased(c *check.C) {
body := "" +
"----hello--\r\n" +
diff --git a/overlord/assertstate/validation_set_tracking.go b/overlord/assertstate/validation_set_tracking.go
new file mode 100644
index 0000000000..036b1de49a
--- /dev/null
+++ b/overlord/assertstate/validation_set_tracking.go
@@ -0,0 +1,127 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package assertstate
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+// ValidationSetMode reflects the mode of respective validation set, which is
+// either monitoring or enforcing.
+type ValidationSetMode int
+
+const (
+ Monitor ValidationSetMode = iota
+ Enforce
+)
+
+// ValidationSetTracking holds tracking parameters for associated validation set.
+type ValidationSetTracking struct {
+ AccountID string `json:"account-id"`
+ Name string `json:"name"`
+ Mode ValidationSetMode `json:"mode"`
+
+ // PinnedAt is an optional pinned sequence point, or 0 if not pinned.
+ PinnedAt int `json:"pinned-at,omitempty"`
+
+ // Current is the current sequence point.
+ Current int `json:"current,omitempty"`
+}
+
+// ValidationSetKey formats the given account id and name into a validation set key.
+func ValidationSetKey(accountID, name string) string {
+ return fmt.Sprintf("%s/%s", accountID, name)
+}
+
+// UpdateValidationSet updates ValidationSetTracking.
+// The method assumes valid tr fields.
+func UpdateValidationSet(st *state.State, tr *ValidationSetTracking) {
+ var vsmap map[string]*json.RawMessage
+ err := st.Get("validation-sets", &vsmap)
+ if err != nil && err != state.ErrNoState {
+ panic("internal error: cannot unmarshal validation set tracking state: " + err.Error())
+ }
+ if vsmap == nil {
+ vsmap = make(map[string]*json.RawMessage)
+ }
+ data, err := json.Marshal(tr)
+ if err != nil {
+ panic("internal error: cannot marshal validation set tracking state: " + err.Error())
+ }
+ raw := json.RawMessage(data)
+ key := ValidationSetKey(tr.AccountID, tr.Name)
+ vsmap[key] = &raw
+ st.Set("validation-sets", vsmap)
+}
+
+// DeleteValidationSet deletes a validation set for the given accoundID and name.
+// It is not an error to delete a non-existing one.
+func DeleteValidationSet(st *state.State, accountID, name string) {
+ var vsmap map[string]*json.RawMessage
+ err := st.Get("validation-sets", &vsmap)
+ if err != nil && err != state.ErrNoState {
+ panic("internal error: cannot unmarshal validation set tracking state: " + err.Error())
+ }
+ if len(vsmap) == 0 {
+ return
+ }
+ delete(vsmap, ValidationSetKey(accountID, name))
+ st.Set("validation-sets", vsmap)
+ return
+}
+
+// GetValidationSet retrieves the ValidationSetTracking for the given account and name.
+func GetValidationSet(st *state.State, accountID, name string, tr *ValidationSetTracking) error {
+ if tr == nil {
+ return fmt.Errorf("internal error: tr is nil")
+ }
+
+ *tr = ValidationSetTracking{}
+
+ var vset map[string]*json.RawMessage
+ err := st.Get("validation-sets", &vset)
+ if err != nil {
+ return err
+ }
+ key := ValidationSetKey(accountID, name)
+ raw, ok := vset[key]
+ if !ok {
+ return state.ErrNoState
+ }
+ // XXX: &tr pointer isn't needed here but it is likely historical (a bug in
+ // old JSON marshaling probably) and carried over from snapstate.Get.
+ err = json.Unmarshal([]byte(*raw), &tr)
+ if err != nil {
+ return fmt.Errorf("cannot unmarshal validation set tracking state: %v", err)
+ }
+ return nil
+}
+
+// ValidationSets retrieves all ValidationSetTracking data.
+func ValidationSets(st *state.State) (map[string]*ValidationSetTracking, error) {
+ var vsmap map[string]*ValidationSetTracking
+ if err := st.Get("validation-sets", &vsmap); err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ return vsmap, nil
+}
diff --git a/overlord/assertstate/validation_set_tracking_test.go b/overlord/assertstate/validation_set_tracking_test.go
new file mode 100644
index 0000000000..8422ec6bc3
--- /dev/null
+++ b/overlord/assertstate/validation_set_tracking_test.go
@@ -0,0 +1,159 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package assertstate_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type validationSetTrackingSuite struct {
+ st *state.State
+}
+
+var _ = Suite(&validationSetTrackingSuite{})
+
+func (s *validationSetTrackingSuite) SetUpTest(c *C) {
+ s.st = state.New(nil)
+}
+
+func (s *validationSetTrackingSuite) TestUpdate(c *C) {
+ s.st.Lock()
+ defer s.st.Unlock()
+
+ all, err := assertstate.ValidationSets(s.st)
+ c.Assert(err, IsNil)
+ c.Assert(all, HasLen, 0)
+
+ tr := assertstate.ValidationSetTracking{
+ AccountID: "foo",
+ Name: "bar",
+ Mode: assertstate.Enforce,
+ PinnedAt: 1,
+ Current: 2,
+ }
+ assertstate.UpdateValidationSet(s.st, &tr)
+
+ all, err = assertstate.ValidationSets(s.st)
+ c.Assert(err, IsNil)
+ c.Assert(all, HasLen, 1)
+ for k, v := range all {
+ c.Check(k, Equals, "foo/bar")
+ c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "bar", Mode: assertstate.Enforce, PinnedAt: 1, Current: 2})
+ }
+
+ tr = assertstate.ValidationSetTracking{
+ AccountID: "foo",
+ Name: "bar",
+ Mode: assertstate.Monitor,
+ PinnedAt: 2,
+ Current: 3,
+ }
+ assertstate.UpdateValidationSet(s.st, &tr)
+
+ all, err = assertstate.ValidationSets(s.st)
+ c.Assert(err, IsNil)
+ c.Assert(all, HasLen, 1)
+ for k, v := range all {
+ c.Check(k, Equals, "foo/bar")
+ c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "bar", Mode: assertstate.Monitor, PinnedAt: 2, Current: 3})
+ }
+
+ tr = assertstate.ValidationSetTracking{
+ AccountID: "foo",
+ Name: "baz",
+ Mode: assertstate.Enforce,
+ Current: 3,
+ }
+ assertstate.UpdateValidationSet(s.st, &tr)
+
+ all, err = assertstate.ValidationSets(s.st)
+ c.Assert(err, IsNil)
+ c.Assert(all, HasLen, 2)
+
+ var gotFirst, gotSecond bool
+ for k, v := range all {
+ if k == "foo/bar" {
+ gotFirst = true
+ c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "bar", Mode: assertstate.Monitor, PinnedAt: 2, Current: 3})
+ } else {
+ gotSecond = true
+ c.Check(k, Equals, "foo/baz")
+ c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "baz", Mode: assertstate.Enforce, PinnedAt: 0, Current: 3})
+ }
+ }
+ c.Check(gotFirst, Equals, true)
+ c.Check(gotSecond, Equals, true)
+}
+
+func (s *validationSetTrackingSuite) TestDelete(c *C) {
+ s.st.Lock()
+ defer s.st.Unlock()
+
+ // delete non-existing one is fine
+ assertstate.DeleteValidationSet(s.st, "foo", "bar")
+ all, err := assertstate.ValidationSets(s.st)
+ c.Assert(err, IsNil)
+ c.Assert(all, HasLen, 0)
+
+ tr := assertstate.ValidationSetTracking{
+ AccountID: "foo",
+ Name: "bar",
+ Mode: assertstate.Monitor,
+ }
+ assertstate.UpdateValidationSet(s.st, &tr)
+
+ all, err = assertstate.ValidationSets(s.st)
+ c.Assert(err, IsNil)
+ c.Assert(all, HasLen, 1)
+
+ // deletes existing one
+ assertstate.DeleteValidationSet(s.st, "foo", "bar")
+ all, err = assertstate.ValidationSets(s.st)
+ c.Assert(err, IsNil)
+ c.Assert(all, HasLen, 0)
+}
+
+func (s *validationSetTrackingSuite) TestGet(c *C) {
+ s.st.Lock()
+ defer s.st.Unlock()
+
+ err := assertstate.GetValidationSet(s.st, "foo", "bar", nil)
+ c.Assert(err, ErrorMatches, `internal error: tr is nil`)
+
+ tr := assertstate.ValidationSetTracking{
+ AccountID: "foo",
+ Name: "bar",
+ Mode: assertstate.Enforce,
+ Current: 3,
+ }
+ assertstate.UpdateValidationSet(s.st, &tr)
+
+ var res assertstate.ValidationSetTracking
+ err = assertstate.GetValidationSet(s.st, "foo", "bar", &res)
+ c.Assert(err, IsNil)
+ c.Check(res, DeepEquals, tr)
+
+ // non-existing
+ err = assertstate.GetValidationSet(s.st, "foo", "baz", &res)
+ c.Assert(err, Equals, state.ErrNoState)
+}
diff --git a/overlord/snapstate/flags.go b/overlord/snapstate/flags.go
index e63fe1e2ad..29fe91d3fe 100644
--- a/overlord/snapstate/flags.go
+++ b/overlord/snapstate/flags.go
@@ -42,6 +42,9 @@ type Flags struct {
// to ignore refresh control validation.
IgnoreValidation bool `json:"ignore-validation,omitempty"`
+ // IgnoreRunning is set to indicate that running apps or hooks should be ignored.
+ IgnoreRunning bool `json:"ignore-running,omitempty"`
+
// Required is set to mark that a snap is required
// and cannot be removed
Required bool `json:"required,omitempty"`
diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go
index b57b078620..8c59e9a01f 100644
--- a/overlord/snapstate/handlers.go
+++ b/overlord/snapstate/handlers.go
@@ -834,7 +834,7 @@ func (m *SnapManager) doUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) error {
return err
}
- if experimentalRefreshAppAwareness {
+ if experimentalRefreshAppAwareness && !snapsup.Flags.IgnoreRunning {
// Invoke the hard refresh flow. Upon success the returned lock will be
// held to prevent snap-run from advancing until UnlinkSnap, executed
// below, completes.
diff --git a/overlord/snapstate/handlers_link_test.go b/overlord/snapstate/handlers_link_test.go
index b2bbed5c69..4b74672cab 100644
--- a/overlord/snapstate/handlers_link_test.go
+++ b/overlord/snapstate/handlers_link_test.go
@@ -376,6 +376,68 @@ func (s *linkSnapSuite) TestDoUndoLinkSnap(c *C) {
c.Check(ok, Equals, true)
}
+func (s *linkSnapSuite) TestDoUnlinkCurrentSnapWithIgnoreRunning(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // With refresh-app-awareness enabled
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.refresh-app-awareness", true)
+ tr.Commit()
+
+ // With a snap "pkg" at revision 42
+ si := &snap.SideInfo{RealName: "pkg", Revision: snap.R(42)}
+ snapstate.Set(s.state, "pkg", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ Active: true,
+ })
+
+ // With an app belonging to the snap that is apparently running.
+ snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) {
+ c.Assert(name, Equals, "pkg")
+ info := &snap.Info{SuggestedName: name, SideInfo: *si, SnapType: snap.TypeApp}
+ info.Apps = map[string]*snap.AppInfo{
+ "app": {Snap: info, Name: "app"},
+ }
+ return info, nil
+ })
+ restore := snapstate.MockPidsOfSnap(func(instanceName string) (map[string][]int, error) {
+ c.Assert(instanceName, Equals, "pkg")
+ return map[string][]int{"snap.pkg.app": {1234}}, nil
+ })
+ defer restore()
+
+ // We can unlink the current revision of that snap, by setting IgnoreRunning flag.
+ task := s.state.NewTask("unlink-current-snap", "")
+ task.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ Flags: snapstate.Flags{IgnoreRunning: true},
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(task)
+
+ // Run the task we created
+ s.state.Unlock()
+ s.se.Ensure()
+ s.se.Wait()
+ s.state.Lock()
+
+ // And observe the results.
+ var snapst snapstate.SnapState
+ err := snapstate.Get(s.state, "pkg", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.Active, Equals, false)
+ c.Check(snapst.Sequence, HasLen, 1)
+ c.Check(snapst.Current, Equals, snap.R(42))
+ c.Check(task.Status(), Equals, state.DoneStatus)
+ expected := fakeOps{{
+ op: "unlink-snap",
+ path: filepath.Join(dirs.SnapMountDir, "pkg/42"),
+ }}
+ c.Check(s.fakeBackend.ops, DeepEquals, expected)
+}
+
func (s *linkSnapSuite) TestDoUndoUnlinkCurrentSnapWithVitalityScore(c *C) {
s.state.Lock()
defer s.state.Unlock()
diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go
index 294c8206b7..e61cd0653b 100644
--- a/overlord/snapstate/snapstate.go
+++ b/overlord/snapstate/snapstate.go
@@ -174,7 +174,7 @@ func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup, flags int
}
snapsup.PlugsOnly = snapsup.PlugsOnly && (len(info.Slots) == 0)
- if experimentalRefreshAppAwareness {
+ if experimentalRefreshAppAwareness && !snapsup.Flags.IgnoreRunning {
// Note that because we are modifying the snap state inside
// softCheckNothingRunningForRefresh, this block must be located
// after the conflict check done above.
@@ -2418,6 +2418,9 @@ func Get(st *state.State, name string, snapst *SnapState) error {
if !ok {
return state.ErrNoState
}
+
+ // XXX: &snapst pointer isn't needed here but it is likely historical
+ // (a bug in old JSON marshaling probably).
err = json.Unmarshal([]byte(*raw), &snapst)
if err != nil {
return fmt.Errorf("cannot unmarshal snap state: %v", err)
diff --git a/overlord/snapstate/snapstate_install_test.go b/overlord/snapstate/snapstate_install_test.go
index ef44a0362d..03f8b125e5 100644
--- a/overlord/snapstate/snapstate_install_test.go
+++ b/overlord/snapstate/snapstate_install_test.go
@@ -344,6 +344,63 @@ func (s *snapmgrTestSuite) TestInstallFailsOnBusySnap(c *C) {
c.Check(snapst.RefreshInhibitedTime, NotNil)
}
+func (s *snapmgrTestSuite) TestInstallWithIgnoreValidationProceedsOnBusySnap(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // With the refresh-app-awareness feature enabled.
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.refresh-app-awareness", true)
+ tr.Commit()
+
+ // With a snap state indicating a snap is already installed.
+ snapst := &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "pkg", SnapID: "pkg-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ }
+ snapstate.Set(s.state, "pkg", snapst)
+
+ // With a snap info indicating it has an application called "app"
+ snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) {
+ if name != "pkg" {
+ return s.fakeBackend.ReadInfo(name, si)
+ }
+ info := &snap.Info{SuggestedName: name, SideInfo: *si, SnapType: snap.TypeApp}
+ info.Apps = map[string]*snap.AppInfo{
+ "app": {Snap: info, Name: "app"},
+ }
+ return info, nil
+ })
+
+ // With an app belonging to the snap that is apparently running.
+ restore := snapstate.MockPidsOfSnap(func(instanceName string) (map[string][]int, error) {
+ c.Assert(instanceName, Equals, "pkg")
+ return map[string][]int{
+ "snap.pkg.app": {1234},
+ }, nil
+ })
+ defer restore()
+
+ // Attempt to install revision 2 of the snap, with the IgnoreRunning flag set.
+ snapsup := &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{RealName: "pkg", SnapID: "pkg-id", Revision: snap.R(2)},
+ Flags: snapstate.Flags{IgnoreRunning: true},
+ }
+
+ // And observe that we do so despite the running app.
+ _, err := snapstate.DoInstall(s.state, snapst, snapsup, 0, "", dummyInUseCheck)
+ c.Assert(err, IsNil)
+
+ // The state confirms that the refresh operation was not postponed.
+ err = snapstate.Get(s.state, "pkg", snapst)
+ c.Assert(err, IsNil)
+ c.Check(snapst.RefreshInhibitedTime, IsNil)
+}
+
func (s *snapmgrTestSuite) TestInstallDespiteBusySnap(c *C) {
s.state.Lock()
defer s.state.Unlock()