Files
snapd/daemon/api_apps_test.go
Philip Meulengracht 81309e59a9 daemon,cmd/snap: support for user services in snap services (#13381)
* daemon,cmd/snap: support for user services in snap services

* NEWS: update news to reflect this functionality

* cmd/snap: add missing unit tests

* many: use interface instead for StatusDecorator to allow for unit testing

* daemon: fix a static check for a range loop where a variable could be omitted

* daemon,cmd/snap: support user-service status of the root user with a --user switch

* t/main/services-user: add a case for root user

* t/main/services-user: fix wrong filename

* cmd/snap: fix TestAppStatus unit test failing

* cmd/snap: extend help for "snap services" to describe the new --global and --user switches

remove errors on redundant switches, remove unneeded argument, move validation of arguments closer to entry of Execute

* cmd/snap: refer directly to fields in the help docs
2024-04-11 12:45:48 +02:00

1161 lines
36 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-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 daemon_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"os/user"
"sort"
"strconv"
"strings"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/client/clientutil"
"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/servicestate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/systemd"
"github.com/snapcore/snapd/testutil"
)
var _ = check.Suite(&appsSuite{})
type appsSuiteDecoratorResult struct {
daemonType string
active bool
enabled bool
}
type appsSuite struct {
apiBaseSuite
journalctlRestorer func()
jctlSvcses [][]string
jctlNs []int
jctlFollows []bool
jctlNamespaces []bool
jctlRCs []io.ReadCloser
jctlErrs []error
decoratorResults map[string]appsSuiteDecoratorResult
serviceControlError error
serviceControlCalls []serviceControlArgs
infoA, infoB, infoC, infoD, infoE *snap.Info
}
func (s *appsSuite) journalctl(svcs []string, n int, follow, namespaces bool) (rc io.ReadCloser, err error) {
s.jctlSvcses = append(s.jctlSvcses, svcs)
s.jctlNs = append(s.jctlNs, n)
s.jctlFollows = append(s.jctlFollows, follow)
s.jctlNamespaces = append(s.jctlNamespaces, namespaces)
if len(s.jctlErrs) > 0 {
err, s.jctlErrs = s.jctlErrs[0], s.jctlErrs[1:]
}
if len(s.jctlRCs) > 0 {
rc, s.jctlRCs = s.jctlRCs[0], s.jctlRCs[1:]
}
return rc, err
}
type serviceControlArgs struct {
action string
options string
names []string
scope client.ScopeSelector
users []string
}
func (s *appsSuite) fakeServiceControl(st *state.State, appInfos []*snap.AppInfo, inst *servicestate.Instruction, cu *user.User, flags *servicestate.Flags, context *hookstate.Context) ([]*state.TaskSet, error) {
if flags != nil {
panic("flags are not expected")
}
if s.serviceControlError != nil {
return nil, s.serviceControlError
}
users, err := inst.Users.UserList(cu)
if err != nil {
return nil, err
}
serviceCommand := serviceControlArgs{action: inst.Action, scope: inst.Scope, users: users}
if inst.RestartOptions.Reload {
serviceCommand.options = "reload"
}
// only one flag should ever be set (depending on Action), but appending
// them below acts as an extra validity check.
if inst.StartOptions.Enable {
serviceCommand.options += "enable"
}
if inst.StopOptions.Disable {
serviceCommand.options += "disable"
}
for _, app := range appInfos {
serviceCommand.names = append(serviceCommand.names, fmt.Sprintf("%s.%s", app.Snap.InstanceName(), app.Name))
}
s.serviceControlCalls = append(s.serviceControlCalls, serviceCommand)
t := st.NewTask("sample", "")
ts := state.NewTaskSet(t)
return []*state.TaskSet{ts}, nil
}
func (s *appsSuite) SetUpSuite(c *check.C) {
s.apiBaseSuite.SetUpSuite(c)
s.journalctlRestorer = systemd.MockJournalctl(s.journalctl)
}
func (s *appsSuite) TearDownSuite(c *check.C) {
if s.journalctlRestorer != nil {
s.journalctlRestorer()
}
s.apiBaseSuite.TearDownSuite(c)
}
func (s *appsSuite) SetUpTest(c *check.C) {
s.apiBaseSuite.SetUpTest(c)
s.jctlSvcses = nil
s.jctlNs = nil
s.jctlFollows = nil
s.jctlNamespaces = nil
s.jctlRCs = nil
s.jctlErrs = nil
d := s.daemon(c)
s.serviceControlCalls = nil
s.serviceControlError = nil
restoreServicestateCtrl := daemon.MockServicestateControl(s.fakeServiceControl)
s.AddCleanup(restoreServicestateCtrl)
// turn off ensuring snap services which will call systemctl automatically
r := servicestate.MockEnsuredSnapServices(s.d.Overlord().ServiceManager(), true)
s.AddCleanup(r)
s.AddCleanup(snapstate.MockEnsuredMountsUpdated(d.Overlord().SnapManager(), true))
s.infoA = s.mkInstalledInState(c, s.d, "snap-a", "dev", "v1", snap.R(1), true, "apps: {svc1: {daemon: simple}, svc2: {daemon: simple, reload-command: x}}")
s.infoB = s.mkInstalledInState(c, s.d, "snap-b", "dev", "v1", snap.R(1), false, "apps: {svc3: {daemon: simple}, cmd1: {}}")
s.infoC = s.mkInstalledInState(c, s.d, "snap-c", "dev", "v1", snap.R(1), true, "")
s.infoD = s.mkInstalledInState(c, s.d, "snap-d", "dev", "v1", snap.R(1), true, "apps: {cmd2: {}, cmd3: {}}")
s.infoE = s.mkInstalledInState(c, s.d, "snap-e", "dev", "v1", snap.R(1), true, "apps: {svc4: {daemon: simple, daemon-scope: user}}")
d.Overlord().Loop()
s.AddCleanup(func() { d.Overlord().Stop() })
s.AddCleanup(systemd.MockSystemdVersion(237, nil))
s.expectAppsAccess()
}
func (s *appsSuite) expectAppsAccess() {
s.expectOpenAccess()
s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage"})
}
func (s *appsSuite) TestSplitAppName(c *check.C) {
type T struct {
name string
snap string
app string
}
for _, x := range []T{
{name: "foo.bar", snap: "foo", app: "bar"},
{name: "foo", snap: "foo", app: ""},
{name: "foo.bar.baz", snap: "foo", app: "bar.baz"},
{name: ".", snap: "", app: ""}, // SISO
} {
snap, app := daemon.SplitAppName(x.name)
c.Check(x.snap, check.Equals, snap, check.Commentf(x.name))
c.Check(x.app, check.Equals, app, check.Commentf(x.name))
}
}
func (s *appsSuite) TestGetAppsInfo(c *check.C) {
// System services from active snaps
svcNames := []string{"snap-a.svc1", "snap-a.svc2"}
for _, name := range svcNames {
s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(`
Id=snap.%s.service
Names=snap.%[1]s.service
Type=simple
ActiveState=active
UnitFileState=enabled
NeedDaemonReload=no
`[1:], name)))
}
// System services from inactive snaps
svcNames = append(svcNames, "snap-b.svc3")
// User services from active snaps
svcNames = append(svcNames, "snap-e.svc4")
s.SysctlBufs = append(s.SysctlBufs, []byte("enabled\n"))
req, err := http.NewRequest("GET", "/v2/apps?global=true", nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
apps := rsp.Result.([]client.AppInfo)
c.Assert(apps, check.HasLen, 7)
for _, name := range svcNames {
snapName, app := daemon.SplitAppName(name)
needle := client.AppInfo{
Snap: snapName,
Name: app,
Daemon: "simple",
DaemonScope: snap.SystemDaemon,
}
if snapName != "snap-b" {
// snap-b is not active (all the others are)
needle.Active = true
needle.Enabled = true
}
if snapName == "snap-e" {
// snap-e contains user services
needle.DaemonScope = snap.UserDaemon
needle.Active = false
}
c.Check(apps, testutil.DeepContains, needle)
}
for _, name := range []string{"snap-b.cmd1", "snap-d.cmd2", "snap-d.cmd3"} {
snap, app := daemon.SplitAppName(name)
c.Check(apps, testutil.DeepContains, client.AppInfo{
Snap: snap,
Name: app,
})
}
appNames := make([]string, len(apps))
for i, app := range apps {
appNames[i] = app.Snap + "." + app.Name
}
c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
}
func (s *appsSuite) TestGetAppsInfoNames(c *check.C) {
req, err := http.NewRequest("GET", "/v2/apps?names=snap-d", nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
apps := rsp.Result.([]client.AppInfo)
c.Assert(apps, check.HasLen, 2)
for _, name := range []string{"snap-d.cmd2", "snap-d.cmd3"} {
snap, app := daemon.SplitAppName(name)
c.Check(apps, testutil.DeepContains, client.AppInfo{
Snap: snap,
Name: app,
})
}
appNames := make([]string, len(apps))
for i, app := range apps {
appNames[i] = app.Snap + "." + app.Name
}
c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
}
func (s *appsSuite) TestGetAppsInfoServices(c *check.C) {
r := daemon.MockNewStatusDecorator(func(ctx context.Context, isGlobal bool, uid string) clientutil.StatusDecorator {
c.Check(isGlobal, check.Equals, false)
c.Check(uid, check.Equals, "0")
return s
})
defer r()
// System and user services from active snaps
s.decoratorResults = map[string]appsSuiteDecoratorResult{
"snap-a.svc1": {
daemonType: "simple",
active: true,
enabled: true,
},
"snap-a.svc2": {
daemonType: "simple",
active: true,
enabled: true,
},
"snap-e.svc4": {
daemonType: "simple",
active: false,
enabled: true,
},
}
// System services from active snaps
svcNames := []string{"snap-a.svc1", "snap-a.svc2"}
// System services from inactive snaps
svcNames = append(svcNames, "snap-b.svc3")
// User services from active snaps
svcNames = append(svcNames, "snap-e.svc4")
req, err := http.NewRequest("GET", "/v2/apps?select=service", nil)
c.Assert(err, check.IsNil)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
svcs := rsp.Result.([]client.AppInfo)
c.Assert(svcs, check.HasLen, 4)
for _, name := range svcNames {
snapName, app := daemon.SplitAppName(name)
needle := client.AppInfo{
Snap: snapName,
Name: app,
Daemon: "simple",
DaemonScope: snap.SystemDaemon,
}
if snapName != "snap-b" {
// snap-b is not active (all the others are)
needle.Active = true
needle.Enabled = true
}
if snapName == "snap-e" {
// snap-e contains user services
needle.DaemonScope = snap.UserDaemon
needle.Active = false
}
c.Check(svcs, testutil.DeepContains, needle)
}
appNames := make([]string, len(svcs))
for i, svc := range svcs {
appNames[i] = svc.Snap + "." + svc.Name
}
c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
}
func (s *appsSuite) TestGetAppsInfoServicesWithGlobal(c *check.C) {
// System services from active snaps
svcNames := []string{"snap-a.svc1", "snap-a.svc2"}
for _, name := range svcNames {
s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(`
Id=snap.%s.service
Names=snap.%[1]s.service
Type=simple
ActiveState=active
UnitFileState=enabled
NeedDaemonReload=no
`[1:], name)))
}
// System services from inactive snaps
svcNames = append(svcNames, "snap-b.svc3")
// User services from active snaps
svcNames = append(svcNames, "snap-e.svc4")
s.SysctlBufs = append(s.SysctlBufs, []byte("enabled\n"))
req, err := http.NewRequest("GET", "/v2/apps?select=service&global=true", nil)
c.Assert(err, check.IsNil)
s.asUserAuth(c, req)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
svcs := rsp.Result.([]client.AppInfo)
c.Assert(svcs, check.HasLen, 4)
for _, name := range svcNames {
snapName, app := daemon.SplitAppName(name)
needle := client.AppInfo{
Snap: snapName,
Name: app,
Daemon: "simple",
DaemonScope: snap.SystemDaemon,
}
if snapName != "snap-b" {
// snap-b is not active (all the others are)
needle.Active = true
needle.Enabled = true
}
if snapName == "snap-e" {
// snap-e contains user services
needle.DaemonScope = snap.UserDaemon
needle.Active = false
}
c.Check(svcs, testutil.DeepContains, needle)
}
appNames := make([]string, len(svcs))
for i, svc := range svcs {
appNames[i] = svc.Snap + "." + svc.Name
}
c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
}
func (s *appsSuite) DecorateWithStatus(appInfo *client.AppInfo, snapApp *snap.AppInfo) error {
name := snapApp.Snap.RealName + "." + appInfo.Name
dec, ok := s.decoratorResults[name]
if !ok {
return fmt.Errorf("%s not found in expected test decorator results", name)
}
appInfo.Daemon = dec.daemonType
appInfo.Enabled = dec.enabled
appInfo.Active = dec.active
return nil
}
func (s *appsSuite) TestGetUserAppsInfoServices(c *check.C) {
r := daemon.MockNewStatusDecorator(func(ctx context.Context, isGlobal bool, uid string) clientutil.StatusDecorator {
c.Check(isGlobal, check.Equals, false)
c.Check(uid, check.Equals, "1337")
return s
})
defer r()
// System and user services from active snaps
s.decoratorResults = map[string]appsSuiteDecoratorResult{
"snap-a.svc1": {
daemonType: "simple",
active: true,
enabled: true,
},
"snap-a.svc2": {
daemonType: "simple",
active: true,
enabled: true,
},
"snap-e.svc4": {
daemonType: "simple",
active: true,
enabled: true,
},
}
// Perform the request as a non-root uid
req, err := http.NewRequest("GET", "/v2/apps?select=service", nil)
c.Assert(err, check.IsNil)
s.asUserAuth(c, req)
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, check.Equals, 200)
c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
svcs := rsp.Result.([]client.AppInfo)
c.Assert(svcs, check.HasLen, 4)
for name := range s.decoratorResults {
snapName, app := daemon.SplitAppName(name)
needle := client.AppInfo{
Snap: snapName,
Name: app,
Daemon: "simple",
DaemonScope: snap.SystemDaemon,
}
if snapName != "snap-b" {
// snap-b is not active (all the others are)
needle.Active = true
needle.Enabled = true
}
if snapName == "snap-e" {
// snap-e contains user services
needle.DaemonScope = snap.UserDaemon
needle.Active = true
}
c.Check(svcs, testutil.DeepContains, needle)
}
appNames := make([]string, len(svcs))
for i, svc := range svcs {
appNames[i] = svc.Snap + "." + svc.Name
}
c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
}
func (s *appsSuite) TestGetAppsInfoBadSelect(c *check.C) {
req, err := http.NewRequest("GET", "/v2/apps?select=potato", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 400)
}
func (s *appsSuite) TestGetAppsInfoBadName(c *check.C) {
req, err := http.NewRequest("GET", "/v2/apps?names=potato", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 404)
}
func (s *appsSuite) TestAppInfosForOne(c *check.C) {
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, []string{"snap-a.svc1"}, daemon.AppInfoServiceTrue)
c.Assert(rspe, check.IsNil)
c.Assert(appInfos, check.HasLen, 1)
c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[0].Name, check.Equals, "svc1")
}
func (s *appsSuite) TestAppInfosForAll(c *check.C) {
type T struct {
opts daemon.AppInfoOptions
snaps []*snap.Info
names []string
}
for _, t := range []T{
{
opts: daemon.AppInfoServiceTrue,
names: []string{"svc1", "svc2", "svc3", "svc4"},
snaps: []*snap.Info{s.infoA, s.infoA, s.infoB, s.infoE},
},
{
opts: daemon.AppInfoServiceFalse,
names: []string{"svc1", "svc2", "cmd1", "svc3", "cmd2", "cmd3", "svc4"},
snaps: []*snap.Info{s.infoA, s.infoA, s.infoB, s.infoB, s.infoD, s.infoD, s.infoE},
},
} {
c.Assert(len(t.names), check.Equals, len(t.snaps), check.Commentf("%s", t.opts))
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, nil, t.opts)
c.Assert(rspe, check.IsNil, check.Commentf("%s", t.opts))
names := make([]string, len(appInfos))
for i, appInfo := range appInfos {
names[i] = appInfo.Name
}
c.Assert(names, check.DeepEquals, t.names, check.Commentf("%s", t.opts))
for i := range appInfos {
c.Check(appInfos[i].Snap, check.DeepEquals, t.snaps[i], check.Commentf("%s: %s", t.opts, t.names[i]))
}
}
}
func (s *appsSuite) TestAppInfosForOneSnap(c *check.C) {
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, []string{"snap-a"}, daemon.AppInfoServiceTrue)
c.Assert(rspe, check.IsNil)
c.Assert(appInfos, check.HasLen, 2)
sort.Sort(snap.AppInfoBySnapApp(appInfos))
c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[0].Name, check.Equals, "svc1")
c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[1].Name, check.Equals, "svc2")
}
func (s *appsSuite) TestAppInfosForMixedArgs(c *check.C) {
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, []string{"snap-a", "snap-a.svc1"}, daemon.AppInfoServiceTrue)
c.Assert(rspe, check.IsNil)
c.Assert(appInfos, check.HasLen, 2)
sort.Sort(snap.AppInfoBySnapApp(appInfos))
c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[0].Name, check.Equals, "svc1")
c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[1].Name, check.Equals, "svc2")
}
func (s *appsSuite) TestAppInfosCleanupAndSorted(c *check.C) {
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, []string{
"snap-b.svc3",
"snap-a.svc2",
"snap-a.svc1",
"snap-a.svc2",
"snap-b.svc3",
"snap-a.svc1",
"snap-b",
"snap-a",
}, daemon.AppInfoServiceTrue)
c.Assert(rspe, check.IsNil)
c.Assert(appInfos, check.HasLen, 3)
sort.Sort(snap.AppInfoBySnapApp(appInfos))
c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[0].Name, check.Equals, "svc1")
c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
c.Check(appInfos[1].Name, check.Equals, "svc2")
c.Check(appInfos[2].Snap, check.DeepEquals, s.infoB)
c.Check(appInfos[2].Name, check.Equals, "svc3")
}
func (s *appsSuite) TestAppInfosForAppless(c *check.C) {
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, []string{"snap-c"}, daemon.AppInfoServiceTrue)
c.Assert(rspe, check.NotNil)
c.Check(rspe.Status, check.Equals, 404)
c.Check(rspe.Kind, check.Equals, client.ErrorKindAppNotFound)
c.Assert(appInfos, check.IsNil)
}
func (s *appsSuite) TestAppInfosForMissingApp(c *check.C) {
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, []string{"snap-c.whatever"}, daemon.AppInfoServiceTrue)
c.Assert(rspe, check.NotNil)
c.Check(rspe.Status, check.Equals, 404)
c.Check(rspe.Kind, check.Equals, client.ErrorKindAppNotFound)
c.Assert(appInfos, check.IsNil)
}
func (s *appsSuite) TestAppInfosForMissingSnap(c *check.C) {
st := s.d.Overlord().State()
appInfos, rspe := daemon.AppInfosFor(st, []string{"snap-x"}, daemon.AppInfoServiceTrue)
c.Assert(rspe, check.NotNil)
c.Check(rspe.Status, check.Equals, 404)
c.Check(rspe.Kind, check.Equals, client.ErrorKindSnapNotFound)
c.Assert(appInfos, check.IsNil)
}
func (s *appsSuite) testPostApps(c *check.C, inst servicestate.Instruction, servicecmds []serviceControlArgs) *state.Change {
postBody, err := json.Marshal(inst)
c.Assert(err, check.IsNil)
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBuffer(postBody))
c.Assert(err, check.IsNil)
rsp := s.asyncReq(c, req, s.authUser)
c.Assert(rsp.Status, check.Equals, 202)
c.Check(rsp.Change, check.Matches, `[0-9]+`)
st := s.d.Overlord().State()
st.Lock()
defer st.Unlock()
chg := st.Change(rsp.Change)
c.Assert(chg, check.NotNil)
c.Check(chg.Tasks(), check.HasLen, len(servicecmds))
st.Unlock()
<-chg.Ready()
st.Lock()
c.Check(s.serviceControlCalls, check.DeepEquals, servicecmds)
return chg
}
func (s *appsSuite) testPostAppsUser(c *check.C, inst servicestate.Instruction, servicecmds []serviceControlArgs, expectedErr string) *state.Change {
postBody, err := json.Marshal(inst)
c.Assert(err, check.IsNil)
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBuffer(postBody))
c.Assert(err, check.IsNil)
s.asUserAuth(c, req)
if expectedErr != "" {
rspe := s.errorReq(c, req, s.authUser)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Matches, expectedErr)
return nil
}
rsp := s.asyncReq(c, req, s.authUser)
c.Assert(rsp.Status, check.Equals, 202)
c.Check(rsp.Change, check.Matches, `[0-9]+`)
st := s.d.Overlord().State()
st.Lock()
defer st.Unlock()
chg := st.Change(rsp.Change)
c.Assert(chg, check.NotNil)
c.Check(chg.Tasks(), check.HasLen, len(servicecmds))
st.Unlock()
<-chg.Ready()
st.Lock()
c.Check(s.serviceControlCalls, check.DeepEquals, servicecmds)
return chg
}
func (s *appsSuite) TestPostAppsStartOne(c *check.C) {
inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}}
expected := []serviceControlArgs{
{action: "start", names: []string{"snap-a.svc2"}, scope: client.ScopeSelector{"system", "user"}},
}
chg := s.testPostApps(c, inst, expected)
chg.State().Lock()
defer chg.State().Unlock()
var names []string
err := chg.Get("snap-names", &names)
c.Assert(err, check.IsNil)
c.Assert(names, check.DeepEquals, []string{"snap-a"})
}
func (s *appsSuite) TestPostAppsStartTwo(c *check.C) {
inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a"}}
expected := []serviceControlArgs{
{action: "start", names: []string{"snap-a.svc1", "snap-a.svc2"}, scope: client.ScopeSelector{"system", "user"}},
}
chg := s.testPostApps(c, inst, expected)
chg.State().Lock()
defer chg.State().Unlock()
var names []string
err := chg.Get("snap-names", &names)
c.Assert(err, check.IsNil)
c.Assert(names, check.DeepEquals, []string{"snap-a"})
}
func (s *appsSuite) TestPostAppsStartThree(c *check.C) {
inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a", "snap-b"}}
expected := []serviceControlArgs{
{action: "start", names: []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"}, scope: client.ScopeSelector{"system", "user"}},
}
chg := s.testPostApps(c, inst, expected)
// check the summary expands the snap into actual apps
c.Check(chg.Summary(), check.Equals, "Running service command")
chg.State().Lock()
defer chg.State().Unlock()
var names []string
err := chg.Get("snap-names", &names)
c.Assert(err, check.IsNil)
c.Assert(names, check.DeepEquals, []string{"snap-a", "snap-b"})
}
func (s *appsSuite) TestPostAppsStop(c *check.C) {
inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}}
expected := []serviceControlArgs{
{action: "stop", names: []string{"snap-a.svc2"}, scope: client.ScopeSelector{"system", "user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsRestart(c *check.C) {
inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}}
expected := []serviceControlArgs{
{action: "restart", names: []string{"snap-a.svc2"}, scope: client.ScopeSelector{"system", "user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsReload(c *check.C) {
inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}}
inst.Reload = true
expected := []serviceControlArgs{
{action: "restart", options: "reload", names: []string{"snap-a.svc2"}, scope: client.ScopeSelector{"system", "user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsEnableNow(c *check.C) {
inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}}
inst.Enable = true
expected := []serviceControlArgs{
{action: "start", options: "enable", names: []string{"snap-a.svc2"}, scope: client.ScopeSelector{"system", "user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsDisableNow(c *check.C) {
inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}}
inst.Disable = true
expected := []serviceControlArgs{
{action: "stop", options: "disable", names: []string{"snap-a.svc2"}, scope: client.ScopeSelector{"system", "user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsFailedToGetUser(c *check.C) {
r := daemon.MockSystemUserFromRequest(func(r *http.Request) (*user.User, error) {
return nil, fmt.Errorf("failed")
})
defer r()
req, err := http.NewRequest("POST", "/v2/apps", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Matches, "cannot perform operation on services: failed")
}
func (s *appsSuite) TestPostAppsScopesSelfAsRootNotAllowed(c *check.C) {
inst := servicestate.Instruction{
Action: "start",
Names: []string{"snap-a.svc1"},
Scope: client.ScopeSelector{"user"},
Users: client.UserSelector{
Selector: client.UserSelectionSelf,
},
}
postBody, err := json.Marshal(inst)
c.Assert(err, check.IsNil)
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBuffer(postBody))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, s.authUser)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Matches, `cannot use "self" for root user`)
}
func (s *appsSuite) TestPostAppsAllUsersAsRootHappy(c *check.C) {
inst := servicestate.Instruction{
Action: "start",
Names: []string{"snap-a.svc1"},
Scope: client.ScopeSelector{"user"},
Users: client.UserSelector{
Selector: client.UserSelectionAll,
},
}
expected := []serviceControlArgs{
// Expect no user to appear as we are not logged in
{action: "start", names: []string{"snap-a.svc1"}, scope: client.ScopeSelector{"user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsScopesNotSpecifiedForRoot(c *check.C) {
inst := servicestate.Instruction{Action: "start", Names: []string{"snap-e.svc4"}}
expected := []serviceControlArgs{
{action: "start", names: []string{"snap-e.svc4"}, scope: client.ScopeSelector{"system", "user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsUsersAsUserHappy(c *check.C) {
inst := servicestate.Instruction{
Action: "start",
Names: []string{"snap-a.svc1"},
Scope: client.ScopeSelector{"user"},
Users: client.UserSelector{
Selector: client.UserSelectionAll,
},
}
expected := []serviceControlArgs{
{action: "start", names: []string{"snap-a.svc1"}, scope: client.ScopeSelector{"user"}},
}
s.testPostAppsUser(c, inst, expected, "")
}
func (s *appsSuite) TestPostAppsScopesNotSpecifiedForUser(c *check.C) {
inst := servicestate.Instruction{
Action: "start",
Names: []string{"snap-e.svc4"},
Users: client.UserSelector{
Selector: client.UserSelectionSelf,
},
}
s.testPostAppsUser(c, inst, nil, "cannot perform operation on services: non-root users must specify service scope when targeting user services")
}
func (s *appsSuite) TestPostAppsUsersUser(c *check.C) {
inst := servicestate.Instruction{
Action: "start",
Names: []string{"snap-a.svc1"},
Users: client.UserSelector{
Selector: client.UserSelectionSelf,
},
}
expected := []serviceControlArgs{
{action: "start", names: []string{"snap-a.svc1"}, scope: client.ScopeSelector{"system"}, users: []string{"username"}},
}
s.testPostAppsUser(c, inst, expected, "")
}
func (s *appsSuite) TestPostAppsUsersWithUsernames(c *check.C) {
inst := servicestate.Instruction{
Action: "start",
Names: []string{"snap-a.svc1"},
Users: client.UserSelector{
Names: []string{"my-user", "other-user"},
},
}
expected := []serviceControlArgs{
// Expect no user to appear as we are not logged in
{action: "start", names: []string{"snap-a.svc1"}, scope: client.ScopeSelector{"system", "user"}, users: []string{"my-user", "other-user"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsUserNotSpecifiedForRoot(c *check.C) {
inst := servicestate.Instruction{Action: "start", Names: []string{"snap-e.svc4"}, Scope: client.ScopeSelector{"system"}}
expected := []serviceControlArgs{
{action: "start", names: []string{"snap-e.svc4"}, scope: client.ScopeSelector{"system"}},
}
s.testPostApps(c, inst, expected)
}
func (s *appsSuite) TestPostAppsUserNotSpecifiedForUser(c *check.C) {
inst := servicestate.Instruction{Action: "start", Names: []string{"snap-e.svc4"}, Scope: client.ScopeSelector{"user"}}
s.testPostAppsUser(c, inst, nil, "cannot perform operation on services: non-root users must specify users when targeting user services")
}
func (s *appsSuite) TestPostAppsBadJSON(c *check.C) {
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`'junk`))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Matches, ".*cannot decode request body.*")
}
func (s *appsSuite) TestPostAppsBadOp(c *check.C) {
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"random": "json"}`))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Matches, ".*cannot perform operation on services without a list of services.*")
}
func (s *appsSuite) TestPostAppsBadSnap(c *check.C) {
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-c"]}`))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 404)
c.Check(rspe.Message, check.Equals, `snap "snap-c" has no services`)
}
func (s *appsSuite) TestPostAppsBadApp(c *check.C) {
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-a.what"]}`))
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 404)
c.Check(rspe.Message, check.Equals, `snap "snap-a" has no service "what"`)
}
func (s *appsSuite) TestPostAppsServiceControlError(c *check.C) {
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`))
c.Assert(err, check.IsNil)
s.serviceControlError = fmt.Errorf("total failure")
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Equals, `total failure`)
}
func (s *appsSuite) TestPostAppsConflict(c *check.C) {
req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`))
c.Assert(err, check.IsNil)
s.serviceControlError = &snapstate.ChangeConflictError{Snap: "snap-a", ChangeKind: "enable"}
rspe := s.errorReq(c, req, nil)
c.Check(rspe.Status, check.Equals, 400)
c.Check(rspe.Message, check.Equals, `snap "snap-a" has "enable" change in progress`)
}
func (s *appsSuite) expectLogsAccess() {
s.expectReadAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage"})
}
func (s *appsSuite) TestLogs(c *check.C) {
s.expectLogsAccess()
s.jctlRCs = []io.ReadCloser{io.NopCloser(strings.NewReader(`
{"MESSAGE": "hello1", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "42"}
{"MESSAGE": "hello2", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "44"}
{"MESSAGE": "hello3", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "46"}
{"MESSAGE": "hello4", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "48"}
{"MESSAGE": "hello5", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "50"}
`))}
req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}})
c.Check(s.jctlNs, check.DeepEquals, []int{42})
c.Check(s.jctlFollows, check.DeepEquals, []bool{false})
c.Check(s.jctlNamespaces, check.DeepEquals, []bool{false})
c.Check(rec.Code, check.Equals, 200)
c.Check(rec.Header().Get("Content-Type"), check.Equals, "application/json-seq")
c.Check(rec.Body.String(), check.Equals, `
{"timestamp":"1970-01-01T00:00:00.000042Z","message":"hello1","sid":"xyzzy","pid":"42"}
{"timestamp":"1970-01-01T00:00:00.000044Z","message":"hello2","sid":"xyzzy","pid":"42"}
{"timestamp":"1970-01-01T00:00:00.000046Z","message":"hello3","sid":"xyzzy","pid":"42"}
{"timestamp":"1970-01-01T00:00:00.000048Z","message":"hello4","sid":"xyzzy","pid":"42"}
{"timestamp":"1970-01-01T00:00:00.00005Z","message":"hello5","sid":"xyzzy","pid":"42"}
`[1:])
}
func (s *appsSuite) TestLogsNoNamespaceOption(c *check.C) {
restore := systemd.MockSystemdVersion(237, nil)
defer restore()
s.expectLogsAccess()
s.jctlRCs = []io.ReadCloser{io.NopCloser(strings.NewReader(""))}
req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}})
c.Check(s.jctlNs, check.DeepEquals, []int{42})
c.Check(s.jctlFollows, check.DeepEquals, []bool{false})
c.Check(s.jctlNamespaces, check.DeepEquals, []bool{false})
c.Check(rec.Code, check.Equals, 200)
c.Check(rec.Header().Get("Content-Type"), check.Equals, "application/json-seq")
c.Check(rec.Body.String(), check.Equals, "")
}
func (s *appsSuite) TestLogsWithNamespaceOption(c *check.C) {
restore := systemd.MockSystemdVersion(245, nil)
defer restore()
s.expectLogsAccess()
s.jctlRCs = []io.ReadCloser{io.NopCloser(strings.NewReader(""))}
req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}})
c.Check(s.jctlNs, check.DeepEquals, []int{42})
c.Check(s.jctlFollows, check.DeepEquals, []bool{false})
c.Check(s.jctlNamespaces, check.DeepEquals, []bool{true})
c.Check(rec.Code, check.Equals, 200)
c.Check(rec.Header().Get("Content-Type"), check.Equals, "application/json-seq")
c.Check(rec.Body.String(), check.Equals, "")
}
func (s *appsSuite) TestLogsN(c *check.C) {
s.expectLogsAccess()
type T struct {
in string
out int
}
for _, t := range []T{
{in: "", out: 10},
{in: "0", out: 0},
{in: "-1", out: -1},
{in: strconv.Itoa(math.MinInt32), out: math.MinInt32},
{in: strconv.Itoa(math.MaxInt32), out: math.MaxInt32},
} {
s.jctlRCs = []io.ReadCloser{io.NopCloser(strings.NewReader(""))}
s.jctlNs = nil
req, err := http.NewRequest("GET", "/v2/logs?n="+t.in, nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(s.jctlNs, check.DeepEquals, []int{t.out})
}
}
func (s *appsSuite) TestLogsBadN(c *check.C) {
s.expectLogsAccess()
req, err := http.NewRequest("GET", "/v2/logs?n=hello", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 400)
}
func (s *appsSuite) TestLogsFollow(c *check.C) {
s.expectLogsAccess()
s.jctlRCs = []io.ReadCloser{
io.NopCloser(strings.NewReader("")),
io.NopCloser(strings.NewReader("")),
io.NopCloser(strings.NewReader("")),
}
reqT, err := http.NewRequest("GET", "/v2/logs?follow=true", nil)
c.Assert(err, check.IsNil)
reqF, err := http.NewRequest("GET", "/v2/logs?follow=false", nil)
c.Assert(err, check.IsNil)
reqN, err := http.NewRequest("GET", "/v2/logs", nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, reqT, nil).ServeHTTP(rec, reqT)
s.req(c, reqF, nil).ServeHTTP(rec, reqF)
s.req(c, reqN, nil).ServeHTTP(rec, reqN)
c.Check(s.jctlFollows, check.DeepEquals, []bool{true, false, false})
}
func (s *appsSuite) TestLogsBadFollow(c *check.C) {
s.expectLogsAccess()
req, err := http.NewRequest("GET", "/v2/logs?follow=hello", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 400)
}
func (s *appsSuite) TestLogsBadName(c *check.C) {
s.expectLogsAccess()
req, err := http.NewRequest("GET", "/v2/logs?names=hello", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 404)
}
func (s *appsSuite) TestLogsSad(c *check.C) {
s.expectLogsAccess()
s.jctlErrs = []error{errors.New("potato")}
req, err := http.NewRequest("GET", "/v2/logs", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 500)
}
func (s *appsSuite) TestLogsNoServices(c *check.C) {
s.expectLogsAccess()
// no installed snaps with services
st := s.d.Overlord().State()
st.Lock()
st.Set("snaps", nil)
st.Unlock()
req, err := http.NewRequest("GET", "/v2/logs", nil)
c.Assert(err, check.IsNil)
rspe := s.errorReq(c, req, nil)
c.Assert(rspe.Status, check.Equals, 404)
}