Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

o/configstate, o/snapshotstate: fix handling of nil snap config on snapshot restore #10007

Merged
merged 4 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion overlord/configstate/config/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ func GetSnapConfig(st *state.State, snapName string) (*json.RawMessage, error) {
func SetSnapConfig(st *state.State, snapName string, snapcfg *json.RawMessage) error {
var config map[string]*json.RawMessage
err := st.Get("config", &config)
isNil := snapcfg == nil || len(*snapcfg) == 0
// empty nil snapcfg should be an empty message, but deal with "null" as well.
isNil := snapcfg == nil || len(*snapcfg) == 0 || bytes.Compare(*snapcfg, []byte("null")) == 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove the "bytes.Compare()" here and run the (unit and snapshot-basic spread) tests nothing changes:

index 737421270d..563c5c49ae 100644
--- a/overlord/configstate/config/helpers.go
+++ b/overlord/configstate/config/helpers.go
@@ -146,7 +146,7 @@ func SetSnapConfig(st *state.State, snapName string, snapcfg *json.RawMessage) e
        var config map[string]*json.RawMessage
        err := st.Get("config", &config)
        // empty nil snapcfg should be an empty message, but deal with "null" as well.
-       isNil := snapcfg == nil || len(*snapcfg) == 0 || bytes.Compare(*snapcfg, []byte("null")) == 0
+       isNil := snapcfg == nil || len(*snapcfg) == 0
        if err == state.ErrNoState {
                if isNil {
                        // bail out early

so either:
a) I did something wrong
b) this is not needed
c) our tests need to improve

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! The check in the test was not sufficient, added now. Thanks!

if err == state.ErrNoState {
if isNil {
// bail out early
Expand Down
7 changes: 6 additions & 1 deletion overlord/configstate/config/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,13 @@ func (s *configHelpersSuite) TestSnapConfig(c *C) {
defer s.state.Unlock()

empty1 := json.RawMessage(nil)
buf, err := json.Marshal(nil)
c.Assert(err, IsNil)
empty2 := (*json.RawMessage)(&buf)
// sanity check
c.Check(bytes.Compare(*empty2, []byte(`null`)), Equals, 0)

for _, emptyCfg := range []*json.RawMessage{nil, &empty1, {}} {
for _, emptyCfg := range []*json.RawMessage{nil, &empty1, empty2, {}} {
rawCfg, err := config.GetSnapConfig(s.state, "snap1")
c.Assert(err, IsNil)
c.Check(rawCfg, IsNil)
Expand Down
6 changes: 4 additions & 2 deletions overlord/configstate/config/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,10 @@ func (t *Transaction) Commit() {

// Iterate through the write cache and save each item.
for instanceName, snapChanges := range t.changes {
config, ok := t.pristine[instanceName]
if !ok {
config := t.pristine[instanceName]
// due to LP #1917870 we might have a hook configure task in flight
// that tries to apply config over nil map, create it if nil.
if config == nil {
config = make(map[string]*json.RawMessage)
}
applyChanges(config, snapChanges)
Expand Down
15 changes: 15 additions & 0 deletions overlord/configstate/config/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,21 @@ func (b *brokenType) UnmarshalJSON(data []byte) error {
return nil
}

func (s *transactionSuite) TestCommitOverNilSnapConfig(c *C) {
s.state.Lock()
defer s.state.Unlock()

// simulate invalid nil map created due to LP #1917870 by snap restore
s.state.Set("config", map[string]interface{}{"test-snap": nil})
t := config.NewTransaction(s.state)

c.Assert(t.Set("test-snap", "foo", "bar"), IsNil)
t.Commit()
var v string
t.Get("test-snap", "foo", &v)
c.Assert(v, Equals, "bar")
}

func (s *transactionSuite) TestGetUnmarshalError(c *C) {
s.state.Lock()
defer s.state.Unlock()
Expand Down
56 changes: 37 additions & 19 deletions overlord/snapshotstate/snapshotmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,10 @@ func prepareSave(task *state.Task) (snapshot *snapshotSetup, cur *snap.Info, cfg
snapshot.Filename = filename(snapshot.SetID, cur)
task.Set("snapshot-setup", &snapshot)

rawCfg, err := configGetSnapConfig(st, snapshot.Snap)
cfg, err = unmarshalSnapConfig(st, snapshot.Snap)
if err != nil {
return nil, nil, nil, err
}
if rawCfg != nil {
if err := json.Unmarshal(*rawCfg, &cfg); err != nil {
return nil, nil, nil, err
}
}

// this should be done last because of it modifies the state and the caller needs to undo this if other operation fails.
if snapshot.Auto {
Expand Down Expand Up @@ -247,17 +242,10 @@ func prepareRestore(task *state.Task) (snapshot *snapshotSetup, oldCfg map[strin
return nil, nil, nil, taskGetErrMsg(task, err, "snapshot")
}

rawCfg, err := configGetSnapConfig(st, snapshot.Snap)
oldCfg, err = unmarshalSnapConfig(st, snapshot.Snap)
if err != nil {
return nil, nil, nil, fmt.Errorf("internal error: cannot obtain current snap config for snapshot restore: %v", err)
}

if rawCfg != nil {
if err := json.Unmarshal(*rawCfg, &oldCfg); err != nil {
return nil, nil, nil, fmt.Errorf("internal error: cannot decode current snap config: %v", err)
}
return nil, nil, nil, err
}

reader, err = backendOpen(snapshot.Filename, backend.ExtractFnameSetID)
if err != nil {
return nil, nil, nil, fmt.Errorf("cannot open snapshot: %v", err)
Expand All @@ -267,6 +255,36 @@ func prepareRestore(task *state.Task) (snapshot *snapshotSetup, oldCfg map[strin
return snapshot, oldCfg, reader, nil
}

// marshalSnapConfig encodes cfg to JSON and returns raw JSON message, unless
// cfg is nil - in this case nil is returned.
func marshalSnapConfig(cfg map[string]interface{}) (*json.RawMessage, error) {
stolowski marked this conversation as resolved.
Show resolved Hide resolved
if cfg == nil {
// do not marshal nil - this would result in "null" raw message which
// we want to avoid.
return nil, nil
}
buf, err := json.Marshal(cfg)
if err != nil {
return nil, err
}
raw := (*json.RawMessage)(&buf)
return raw, err
}

func unmarshalSnapConfig(st *state.State, snapName string) (map[string]interface{}, error) {
rawCfg, err := configGetSnapConfig(st, snapName)
if err != nil {
return nil, fmt.Errorf("internal error: cannot obtain current snap config: %v", err)
}
var cfg map[string]interface{}
if rawCfg != nil {
if err := json.Unmarshal(*rawCfg, &cfg); err != nil {
return nil, fmt.Errorf("internal error: cannot decode current snap config: %v", err)
}
}
return cfg, nil
}

func doRestore(task *state.Task, tomb *tomb.Tomb) error {
snapshot, oldCfg, reader, err := prepareRestore(task)
if err != nil {
Expand All @@ -286,7 +304,7 @@ func doRestore(task *state.Task, tomb *tomb.Tomb) error {
return err
}

buf, err := json.Marshal(reader.Conf)
pedronis marked this conversation as resolved.
Show resolved Hide resolved
raw, err := marshalSnapConfig(reader.Conf)
if err != nil {
backendRevert(restoreState)
return fmt.Errorf("cannot marshal saved config: %v", err)
Expand All @@ -295,7 +313,7 @@ func doRestore(task *state.Task, tomb *tomb.Tomb) error {
st.Lock()
defer st.Unlock()

if err := configSetSnapConfig(st, snapshot.Snap, (*json.RawMessage)(&buf)); err != nil {
if err := configSetSnapConfig(st, snapshot.Snap, raw); err != nil {
backendRevert(restoreState)
return fmt.Errorf("cannot set snap config: %v", err)
}
Expand All @@ -321,12 +339,12 @@ func undoRestore(task *state.Task, _ *tomb.Tomb) error {
return taskGetErrMsg(task, err, "snapshot")
}

buf, err := json.Marshal(restoreState.Config)
raw, err := marshalSnapConfig(restoreState.Config)
if err != nil {
return fmt.Errorf("cannot marshal saved config: %v", err)
}

if err := configSetSnapConfig(st, snapshot.Snap, (*json.RawMessage)(&buf)); err != nil {
if err := configSetSnapConfig(st, snapshot.Snap, raw); err != nil {
return fmt.Errorf("cannot restore saved config: %v", err)
}

Expand Down
70 changes: 68 additions & 2 deletions overlord/snapshotstate/snapshotmgr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ func (snapshotSuite) TestDoSaveFailsConfigError(c *check.C) {
})
st.Unlock()
err := snapshotstate.DoSave(task, &tomb.Tomb{})
c.Assert(err, check.ErrorMatches, "bzzt")
c.Assert(err, check.ErrorMatches, "internal error: cannot obtain current snap config: bzzt")
}

func (snapshotSuite) TestDoSaveFailsBadConfig(c *check.C) {
Expand Down Expand Up @@ -581,6 +581,48 @@ func (rs *readerSuite) TestDoRestore(c *check.C) {
c.Check(v, check.DeepEquals, map[string]interface{}{"config": map[string]interface{}{"old": "conf"}})
}

func (rs *readerSuite) TestDoRestoreNoConfig(c *check.C) {
defer snapshotstate.MockConfigGetSnapConfig(func(_ *state.State, snapname string) (*json.RawMessage, error) {
rs.calls = append(rs.calls, "get config")
c.Check(snapname, check.Equals, "a-snap")
// simulate old config
raw := json.RawMessage(`{"foo": "bar"}`)
return &raw, nil
})()
defer snapshotstate.MockBackendOpen(func(filename string, setID uint64) (*backend.Reader, error) {
rs.calls = append(rs.calls, "open")
// set id 0 tells backend.Open to use set id from the filename
c.Check(setID, check.Equals, uint64(0))
c.Check(filename, check.Equals, "/some/1_file.zip")
return &backend.Reader{
// snapshot has no configuration to restore
Snapshot: client.Snapshot{Snap: "a-snap", Conf: nil},
}, nil
})()
defer snapshotstate.MockBackendRestore(func(_ *backend.Reader, _ context.Context, _ snap.Revision, users []string, _ backend.Logf) (*backend.RestoreState, error) {
rs.calls = append(rs.calls, "restore")
c.Check(users, check.DeepEquals, []string{"a-user", "b-user"})
return &backend.RestoreState{}, nil
})()
defer snapshotstate.MockConfigSetSnapConfig(func(_ *state.State, snapname string, conf *json.RawMessage) error {
rs.calls = append(rs.calls, "set config")
c.Check(snapname, check.Equals, "a-snap")
c.Check(conf, check.IsNil)
return nil
})()

st := rs.task.State()
err := snapshotstate.DoRestore(rs.task, &tomb.Tomb{})
c.Assert(err, check.IsNil)
c.Check(rs.calls, check.DeepEquals, []string{"get config", "open", "restore", "set config"})

st.Lock()
defer st.Unlock()
var v map[string]interface{}
rs.task.Get("restore-state", &v)
c.Check(v, check.DeepEquals, map[string]interface{}{"config": map[string]interface{}{"foo": "bar"}})
}

func (rs *readerSuite) TestDoRestoreFailsNoTaskSnapshot(c *check.C) {
rs.task.State().Lock()
rs.task.Clear("snapshot-setup")
Expand All @@ -599,7 +641,7 @@ func (rs *readerSuite) TestDoRestoreFailsOnGetConfigError(c *check.C) {
})()

err := snapshotstate.DoRestore(rs.task, &tomb.Tomb{})
c.Assert(err, check.ErrorMatches, "internal error: cannot obtain current snap config for snapshot restore: bzzt")
c.Assert(err, check.ErrorMatches, "internal error: cannot obtain current snap config: bzzt")
c.Check(rs.calls, check.DeepEquals, []string{"get config"})
}

Expand Down Expand Up @@ -662,6 +704,30 @@ func (rs *readerSuite) TestDoRestoreFailsAndRevertsOnSetConfigError(c *check.C)
}

func (rs *readerSuite) TestUndoRestore(c *check.C) {
defer snapshotstate.MockConfigSetSnapConfig(func(st *state.State, snapName string, raw *json.RawMessage) error {
rs.calls = append(rs.calls, "set config")
c.Check(string(*raw), check.Equals, `{"foo":"bar"}`)
return nil
})()

st := rs.task.State()
st.Lock()
v := map[string]interface{}{"config": map[string]interface{}{"foo": "bar"}}
rs.task.Set("restore-state", &v)
st.Unlock()

err := snapshotstate.UndoRestore(rs.task, &tomb.Tomb{})
c.Assert(err, check.IsNil)
c.Check(rs.calls, check.DeepEquals, []string{"set config", "revert"})
}

func (rs *readerSuite) TestUndoRestoreNoConfig(c *check.C) {
defer snapshotstate.MockConfigSetSnapConfig(func(st *state.State, snapName string, raw *json.RawMessage) error {
rs.calls = append(rs.calls, "set config")
c.Check(raw, check.IsNil)
return nil
})()

st := rs.task.State()
st.Lock()
var v map[string]interface{}
Expand Down
8 changes: 8 additions & 0 deletions tests/main/snapshot-basic/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,11 @@ execute: |
[[ $(snap saved | grep -c test-snapd-sh) == "1" ]]
snap saved --id=123 | MATCH "123 .+test-snapd-sh"
snap restore 123 test-snapd-sh

# snap configuration should work after restoring snapshots that didn't
# have config (LP #1917870).
"$TESTSTOOLS"/snaps-state install-local test-snap
SET_ID=$( snap save test-snap | cut -d\ -f1 | tail -n1 )
snap restore "$SET_ID"
snap set test-snap foo=baz
snap get test-snap foo | MATCH "baz"
1 change: 1 addition & 0 deletions tests/main/snapshot-basic/test-snap/bin/sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/bin/sh
4 changes: 4 additions & 0 deletions tests/main/snapshot-basic/test-snap/meta/hooks/configure
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

# it is important for the test that this configure hook doesn't set
# any snap config options via snapctl.
6 changes: 6 additions & 0 deletions tests/main/snapshot-basic/test-snap/meta/snap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: test-snap
version: 1.0

apps:
sh:
command: bin/sh