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

snapstate: do not allow classic mode for strict snaps #6039

Merged
merged 15 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ const (
ErrorKindSnapNeedsDevMode = "snap-needs-devmode"
ErrorKindSnapNeedsClassic = "snap-needs-classic"
ErrorKindSnapNeedsClassicSystem = "snap-needs-classic-system"
ErrorKindSnapNotClassic = "snap-not-classic"
ErrorKindNoUpdateAvailable = "snap-no-update-available"

ErrorKindRevisionNotAvailable = "snap-revision-not-available"
Expand Down
19 changes: 19 additions & 0 deletions cmd/snap/cmd_snap_op_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,25 @@ func (s *SnapOpSuite) TestTryMissingOpt(c *check.C) {
}
}

func (s *SnapOpSuite) TestInstallConfinedAsClassic(c *check.C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, check.Equals, "POST")
w.WriteHeader(400)
fmt.Fprintf(w, `{
"type": "error",
"result": {
"message":"error from server",
"value": "some-snap",
"kind": "snap-not-classic"
},
"status-code": 400
}`)
})

_, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--classic", "some-snap"})
c.Assert(err, check.ErrorMatches, `snap "some-snap" is not a classic confined snap`)
}

func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) {
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--edge", "--beta", "some-snap"})
c.Assert(err, check.ErrorMatches, "Please specify a single channel")
Expand Down
2 changes: 2 additions & 0 deletions cmd/snap/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ usually confined to, which may put your system at risk.

If you understand and want to proceed repeat the command including --classic.
`)
case client.ErrorKindSnapNotClassic:
msg = i18n.G(`snap %q is not a classic confined snap`)
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a blocker, but I think this is not a good error message for a user.

case client.ErrorKindLoginRequired:
usesSnapName = false
u, _ := user.Current()
Expand Down
2 changes: 2 additions & 0 deletions daemon/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7605,6 +7605,7 @@ func (s *apiSuite) TestErrToResponse(c *check.C) {
nie := &snap.NotInstalledError{Snap: "foo"}
cce := &snapstate.ChangeConflictError{Snap: "foo"}
ndme := &snapstate.SnapNeedsDevModeError{Snap: "foo"}
nc := &snapstate.SnapNotClassicError{Snap: "foo"}
nce := &snapstate.SnapNeedsClassicError{Snap: "foo"}
ncse := &snapstate.SnapNeedsClassicSystemError{Snap: "foo"}
netoe := fakeNetError{message: "other"}
Expand All @@ -7631,6 +7632,7 @@ func (s *apiSuite) TestErrToResponse(c *check.C) {
{aie, makeErrorRsp(errorKindSnapAlreadyInstalled, aie, "foo")},
{nie, makeErrorRsp(errorKindSnapNotInstalled, nie, "foo")},
{ndme, makeErrorRsp(errorKindSnapNeedsDevMode, ndme, "foo")},
{nc, makeErrorRsp(errorKindSnapNotClassic, nc, "foo")},
{nce, makeErrorRsp(errorKindSnapNeedsClassic, nce, "foo")},
{ncse, makeErrorRsp(errorKindSnapNeedsClassicSystem, ncse, "foo")},
{cce, SnapChangeConflict(cce)},
Expand Down
4 changes: 4 additions & 0 deletions daemon/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const (
errorKindSnapNeedsDevMode = errorKind("snap-needs-devmode")
errorKindSnapNeedsClassic = errorKind("snap-needs-classic")
errorKindSnapNeedsClassicSystem = errorKind("snap-needs-classic-system")
errorKindSnapNotClassic = errorKind("snap-not-classic")

errorKindBadQuery = errorKind("bad-query")

Expand Down Expand Up @@ -545,6 +546,9 @@ func errToResponse(err error, snaps []string, fallback func(format string, v ...
case *snapstate.SnapNeedsClassicSystemError:
kind = errorKindSnapNeedsClassicSystem
snapName = err.Snap
case *snapstate.SnapNotClassicError:
kind = errorKindSnapNotClassic
snapName = err.Snap
case net.Error:
if err.Timeout() {
kind = errorKindNetworkTimeout
Expand Down
26 changes: 21 additions & 5 deletions overlord/snapstate/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ func (f *fakeStore) snap(spec snapSpec, user *auth.UserState) (*snap.Info, error
typ = snap.TypeGadget
case "some-snapd":
typ = snap.TypeSnapd
case "some-snap-now-classic":
confinement = "classic"
}

if spec.Name == "snap-unknown" {
Expand Down Expand Up @@ -263,6 +265,10 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
name = "services-snap"
case "some-snap-id":
name = "some-snap"
case "some-snap-now-classic-id":
name = "some-snap-now-classic"
case "some-snap-was-classic-id":
name = "some-snap-was-classic"
case "core-snap-id":
name = "core"
typ = snap.TypeOS
Expand Down Expand Up @@ -305,6 +311,9 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
case "channel-for-devmode":
confinement = snap.DevModeConfinement
}
if name == "some-snap-now-classic" {
confinement = "classic"
}

info := &snap.Info{
Type: typ,
Expand Down Expand Up @@ -589,31 +598,38 @@ func (f *fakeSnappyBackend) OpenSnapFile(snapFilePath string, si *snap.SideInfo)
op.sinfo = *si
}

var name string
var info *snap.Info
if !osutil.IsDirectory(snapFilePath) {
name = filepath.Base(snapFilePath)
name := filepath.Base(snapFilePath)
split := strings.Split(name, "_")
if len(split) >= 2 {
// <snap>_<rev>.snap
// <snap>_<instance-key>_<rev>.snap
name = split[0]
}

info = &snap.Info{SuggestedName: name, Architectures: []string{"all"}}
if name == "some-snap-now-classic" {
info.Confinement = "classic"
}
} else {
// for snap try only
snapf, err := snap.Open(snapFilePath)
if err != nil {
return nil, nil, err
}

info, err := snap.ReadInfoFromSnapFile(snapf, si)
info, err = snap.ReadInfoFromSnapFile(snapf, si)
if err != nil {
return nil, nil, err
}
name = info.SuggestedName
}

if info == nil {
return nil, nil, fmt.Errorf("internal error: no mocked snap for %q", snapFilePath)
}
f.appendOp(&op)
return &snap.Info{SuggestedName: name, Architectures: []string{"all"}}, f.emptyContainer, nil
return info, f.emptyContainer, nil
}

func (f *fakeSnappyBackend) SetupSnap(snapFilePath, instanceName string, si *snap.SideInfo, p progress.Meter) (snap.Type, error) {
Expand Down
12 changes: 12 additions & 0 deletions overlord/snapstate/check_snap.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,21 @@ func (e *SnapNeedsClassicSystemError) Error() string {
return fmt.Sprintf("snap %q requires classic confinement which is only available on classic systems", e.Snap)
}

type SnapNotClassicError struct {
Snap string
}

func (e *SnapNotClassicError) Error() string {
return fmt.Sprintf("snap %q is not a classic confined snap", e.Snap)
}

// determine whether the flags (and system overrides thereof) are
// compatible with the given *snap.Info
func validateFlagsForInfo(info *snap.Info, snapst *SnapState, flags Flags) error {
if flags.Classic && !info.NeedsClassic() {
return &SnapNotClassicError{Snap: info.InstanceName()}
}

switch c := info.Confinement; c {
case snap.StrictConfinement, "":
// strict is always fine
Expand Down
21 changes: 21 additions & 0 deletions overlord/snapstate/check_snap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,27 @@ confinement: classic
c.Assert(err, ErrorMatches, ".* requires classic confinement which is only available on classic systems")
}

func (s *checkSnapSuite) TestCheckSnapErrorClassicModeForStrictOrDevmode(c *C) {
const yaml = `name: hello
version: 1.10
confinement: strict
`
info, err := snap.InfoFromSnapYaml([]byte(yaml))
c.Assert(err, IsNil)

var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
c.Check(path, Equals, "snap-path")
c.Check(si, IsNil)
return info, emptyContainer(c), nil
}
restore := snapstate.MockOpenSnapFile(openSnapFile)
defer restore()

err = snapstate.CheckSnap(s.st, "snap-path", "hello", nil, nil, snapstate.Flags{Classic: true})

c.Assert(err, ErrorMatches, `snap "hello" is not a classic confined snap`)
}

func (s *checkSnapSuite) TestCheckSnapKernelUpdate(c *C) {
reset := release.MockOnClassic(false)
defer reset()
Expand Down
15 changes: 11 additions & 4 deletions overlord/snapstate/snapstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,11 @@ func UpdateMany(ctx context.Context, st *state.State, names []string, userID int

params := func(update *snap.Info) (string, Flags, *SnapState) {
snapst := stateByInstanceName[update.InstanceName()]
updateFlags := snapst.Flags
if !update.NeedsClassic() && updateFlags.Classic {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

// allow updating from classic to strict
updateFlags.Classic = false
}
return snapst.Channel, snapst.Flags, snapst

}
Expand Down Expand Up @@ -1118,7 +1123,12 @@ func Update(st *state.State, name, channel string, revision snap.Revision, userI
}

params := func(update *snap.Info) (string, Flags, *SnapState) {
return channel, flags, &snapst
updateFlags := flags
if !update.NeedsClassic() && updateFlags.Classic {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Slightly sad that our code requires to duplicate this with the code above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually given that infoForUpdate is also changed (below) - do we need this change here or is the change in infoForUpdate sufficient?

Copy link
Contributor

Choose a reason for hiding this comment

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

Snap info is verified twice, once in infoForUpdate() and then once again in doUpdate(). I'm beginning to think that maybe verifying flags in infoForUpdate() is actually superfluous.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bboozzoo Thanks for checking this! Would be great if you could double check, the less code the better :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Dropped the part in infoForUpdate().

// allow updating from classic to strict
updateFlags.Classic = false
}
return channel, updateFlags, &snapst
}

_, tts, err := doUpdate(context.TODO(), st, []string{name}, updates, params, userID, &flags)
Expand Down Expand Up @@ -1192,9 +1202,6 @@ func infoForUpdate(st *state.State, snapst *SnapState, name, channel string, rev
if err != nil {
return nil, err
}
if err := validateInfoAndFlags(info, snapst, flags); err != nil {
return nil, err
}
if ValidateRefreshes != nil && !flags.IgnoreValidation {
_, err := ValidateRefreshes(st, []*snap.Info{info}, nil, userID)
if err != nil {
Expand Down
43 changes: 27 additions & 16 deletions overlord/snapstate/snapstate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package snapstate_test

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -510,9 +511,10 @@ func (s *snapmgrTestSuite) TestInstallClassicConfinementFiltering(c *C) {
_, err = snapstate.Install(s.state, "some-snap", "channel-for-classic", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, IsNil)

// if a snap is *not* classic, you can still install it with --classic
// if a snap is *not* classic, you cannot install it with --classic
_, err = snapstate.Install(s.state, "some-snap", "channel-for-strict", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, IsNil)
c.Assert(err, ErrorMatches, `snap "some-snap" is not a classic confined snap`)
c.Check(err, FitsTypeOf, &snapstate.SnapNotClassicError{})
}

func (s *snapmgrTestSuite) TestInstallFailsWhenClassicSnapsAreNotSupported(c *C) {
Expand Down Expand Up @@ -1832,21 +1834,20 @@ func (s *snapmgrTestSuite) TestUpdateClassicConfinementFiltering(c *C) {
s.state.Lock()
defer s.state.Unlock()

snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
snapstate.Set(s.state, "some-snap-now-classic", &snapstate.SnapState{
Active: true,
Channel: "channel-for-classic",
Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
Sequence: []*snap.SideInfo{{RealName: "some-snap-now-classic", SnapID: "some-snap-now-classic-id", Revision: snap.R(7)}},
Current: snap.R(7),
SnapType: "app",
})

// updated snap is classic, refresh without --classic, do nothing
// TODO: better error message here
_, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{})
_, err := snapstate.Update(s.state, "some-snap-now-classic", "", snap.R(0), s.user.ID, snapstate.Flags{})
c.Assert(err, ErrorMatches, `.* requires classic confinement`)

// updated snap is classic, refresh with --classic
ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
ts, err := snapstate.Update(s.state, "some-snap-now-classic", "", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, IsNil)

chg := s.state.NewChange("refresh", "refresh snap")
Expand All @@ -1857,9 +1858,12 @@ func (s *snapmgrTestSuite) TestUpdateClassicConfinementFiltering(c *C) {
s.settle(c)
s.state.Lock()

c.Assert(chg.Err(), IsNil)
c.Assert(chg.IsReady(), Equals, true)

// verify snap is in classic
var snapst snapstate.SnapState
err = snapstate.Get(s.state, "some-snap", &snapst)
err = snapstate.Get(s.state, "some-snap-now-classic", &snapst)
c.Assert(err, IsNil)
c.Check(snapst.Classic, Equals, true)
}
Expand Down Expand Up @@ -1942,21 +1946,21 @@ func (s *snapmgrTestSuite) TestUpdateStrictFromClassic(c *C) {
s.state.Lock()
defer s.state.Unlock()

snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
snapstate.Set(s.state, "some-snap-was-classic", &snapstate.SnapState{
Active: true,
Channel: "channel",
Sequence: []*snap.SideInfo{{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(7)}},
Sequence: []*snap.SideInfo{{RealName: "some-snap-was-classic", SnapID: "some-snap-was-classic-id", Revision: snap.R(7)}},
Current: snap.R(7),
SnapType: "app",
Flags: snapstate.Flags{Classic: true},
})

// snap installed with --classic, update does not need classic, refresh works without --classic
_, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{})
_, err := snapstate.Update(s.state, "some-snap-was-classic", "", snap.R(0), s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)

// snap installed with --classic, update does not need classic, refresh works with --classic
_, err = snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
_, err = snapstate.Update(s.state, "some-snap-was-classic", "", snap.R(0), s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, IsNil)
}

Expand Down Expand Up @@ -9177,10 +9181,10 @@ func (s *snapmgrTestSuite) TestTrySetsTryModeClassic(c *C) {
restore := maybeMockClassicSupport(c)
defer restore()

s.testTrySetsTryMode(snapstate.Flags{Classic: true}, c)
s.testTrySetsTryMode(snapstate.Flags{Classic: true}, c, "confinement: classic\n")
}

func (s *snapmgrTestSuite) testTrySetsTryMode(flags snapstate.Flags, c *C) {
func (s *snapmgrTestSuite) testTrySetsTryMode(flags snapstate.Flags, c *C, extraYaml ...string) {
s.state.Lock()
defer s.state.Unlock()

Expand All @@ -9190,7 +9194,14 @@ func (s *snapmgrTestSuite) testTrySetsTryMode(flags snapstate.Flags, c *C) {
tryYaml := filepath.Join(d, "meta", "snap.yaml")
err := os.MkdirAll(filepath.Dir(tryYaml), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(tryYaml, []byte("name: foo\nversion: 1.0"), 0644)
buf := bytes.Buffer{}
buf.WriteString("name: foo\nversion: 1.0\n")
if len(extraYaml) > 0 {
for _, extra := range extraYaml {
buf.WriteString(extra)
}
}
err = ioutil.WriteFile(tryYaml, buf.Bytes(), 0644)
c.Assert(err, IsNil)

chg := s.state.NewChange("try", "try snap")
Expand Down Expand Up @@ -9247,7 +9258,7 @@ func (s *snapmgrTestSuite) TestTryUndoRemovesTryFlagLeavesJailMode(c *C) {
func (s *snapmgrTestSuite) TestTryUndoRemovesTryFlagLeavesClassic(c *C) {
restore := maybeMockClassicSupport(c)
defer restore()
s.testTrySetsTryMode(snapstate.Flags{Classic: true}, c)
s.testTrySetsTryMode(snapstate.Flags{Classic: true}, c, "confinement: classic\n")
}

func (s *snapmgrTestSuite) testTryUndoRemovesTryFlag(flags snapstate.Flags, c *C) {
Expand Down
Loading