diff options
| author | Paweł Stołowski <stolowski@gmail.com> | 2020-10-16 12:16:09 +0200 |
|---|---|---|
| committer | Paweł Stołowski <stolowski@gmail.com> | 2020-10-16 12:16:09 +0200 |
| commit | ee1a4d724443ef38d9b53f324ad5ffb8a6592de0 (patch) | |
| tree | 9576fe23917f1bfffb30e4de8582a48984bca507 | |
| parent | 86756459e1c9f0c2821d09436a4f80588405a2ba (diff) | |
| parent | a1aaa70950c0011a81d58cd06a823250234f0b91 (diff) | |
Merge branch 'master' into remove-current-lastremove-current-last
| -rw-r--r-- | client/snap_op.go | 4 | ||||
| -rw-r--r-- | client/snap_op_test.go | 31 | ||||
| -rw-r--r-- | cmd/snap/cmd_snap_op.go | 37 | ||||
| -rw-r--r-- | cmd/snap/cmd_snap_op_test.go | 19 | ||||
| -rw-r--r-- | daemon/api.go | 9 | ||||
| -rw-r--r-- | daemon/api_test.go | 64 | ||||
| -rw-r--r-- | overlord/assertstate/validation_set_tracking.go | 127 | ||||
| -rw-r--r-- | overlord/assertstate/validation_set_tracking_test.go | 159 | ||||
| -rw-r--r-- | overlord/snapstate/flags.go | 3 | ||||
| -rw-r--r-- | overlord/snapstate/handlers.go | 2 | ||||
| -rw-r--r-- | overlord/snapstate/handlers_link_test.go | 62 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate.go | 5 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate_install_test.go | 57 |
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() |
