Files
snapd/client/apps_test.go
Philip Meulengracht 261d1c2e90 client,cmd/snap: introduce --user, --system and --users switches for snap service operations (#13368)
* client,cmd/snap: introduce --user, --system and --users switches for snap service operations

* client,o/servicestate: move Scope/UserSelection to client for reuse in client

* client,cmd/snap: improve handling of user and scope args

* NEWS: update news to reflect that we now support user daemons in start/stop/restart

* cmd/snap: some review feedback on allowed input

* t/main/services-user: add additional user to verify services are correctly affected

* cmd/snap: do not allow --system --user together, do not allow --users with =all

* tests,cmd: use --users=all in test, dont mark --users optional, enforce a value for it, add case for --system --users=all in spread test

* cmd/snap: add a comment for unreachable code, and correct a couple of messages
2024-03-13 14:57:35 +01:00

670 lines
18 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) 2017 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 client_test
import (
"encoding/json"
"fmt"
"os/user"
"strconv"
"strings"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/client"
)
func mksvc(snap, app string) *client.AppInfo {
return &client.AppInfo{
Snap: snap,
Name: app,
Daemon: "simple",
Active: true,
Enabled: true,
}
}
func testClientApps(cs *clientSuite, c *check.C) ([]*client.AppInfo, error) {
services, err := cs.cli.Apps([]string{"foo", "bar"}, client.AppOptions{})
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps")
c.Check(cs.req.Method, check.Equals, "GET")
query := cs.req.URL.Query()
c.Check(query, check.HasLen, 1)
c.Check(query.Get("names"), check.Equals, "foo,bar")
return services, err
}
func testClientAppsService(cs *clientSuite, c *check.C) ([]*client.AppInfo, error) {
services, err := cs.cli.Apps([]string{"foo", "bar"}, client.AppOptions{Service: true})
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps")
c.Check(cs.req.Method, check.Equals, "GET")
query := cs.req.URL.Query()
c.Check(query, check.HasLen, 2)
c.Check(query.Get("names"), check.Equals, "foo,bar")
c.Check(query.Get("select"), check.Equals, "service")
return services, err
}
var appcheckers = []func(*clientSuite, *check.C) ([]*client.AppInfo, error){testClientApps, testClientAppsService}
func (cs *clientSuite) TestClientServiceGetHappy(c *check.C) {
expected := []*client.AppInfo{mksvc("foo", "foo"), mksvc("bar", "bar1")}
buf, err := json.Marshal(expected)
c.Assert(err, check.IsNil)
cs.rsp = fmt.Sprintf(`{"type": "sync", "result": %s}`, buf)
for _, chkr := range appcheckers {
actual, err := chkr(cs, c)
c.Assert(err, check.IsNil)
c.Check(actual, check.DeepEquals, expected)
}
}
func (cs *clientSuite) TestClientServiceGetSad(c *check.C) {
cs.err = fmt.Errorf("xyzzy")
for _, chkr := range appcheckers {
actual, err := chkr(cs, c)
c.Assert(err, check.ErrorMatches, ".* xyzzy")
c.Check(actual, check.HasLen, 0)
}
}
func (cs *clientSuite) TestClientAppCommonID(c *check.C) {
expected := []*client.AppInfo{{
Snap: "foo",
Name: "foo",
CommonID: "org.foo",
}}
buf, err := json.Marshal(expected)
c.Assert(err, check.IsNil)
cs.rsp = fmt.Sprintf(`{"type": "sync", "result": %s}`, buf)
for _, chkr := range appcheckers {
actual, err := chkr(cs, c)
c.Assert(err, check.IsNil)
c.Check(actual, check.DeepEquals, expected)
}
}
func testClientLogs(cs *clientSuite, c *check.C) ([]client.Log, error) {
ch, err := cs.cli.Logs([]string{"foo", "bar"}, client.LogOptions{N: -1, Follow: false})
c.Check(cs.req.URL.Path, check.Equals, "/v2/logs")
c.Check(cs.req.Method, check.Equals, "GET")
// logs cannot have a deadline because of "-f"
_, ok := cs.req.Context().Deadline()
c.Check(ok, check.Equals, false)
query := cs.req.URL.Query()
c.Check(query, check.HasLen, 2)
c.Check(query.Get("names"), check.Equals, "foo,bar")
c.Check(query.Get("n"), check.Equals, "-1")
var logs []client.Log
if ch != nil {
for log := range ch {
logs = append(logs, log)
}
}
return logs, err
}
func (cs *clientSuite) TestClientLogsHappy(c *check.C) {
cs.rsp = `
{"message":"hello"}
{"message":"bye"}
`[1:] // remove the first \n
logs, err := testClientLogs(cs, c)
c.Assert(err, check.IsNil)
c.Check(logs, check.DeepEquals, []client.Log{{Message: "hello"}, {Message: "bye"}})
}
func (cs *clientSuite) TestClientLogsDealsWithIt(c *check.C) {
cs.rsp = `this is a line with no RS on it
this is a line with a RS after some junk{"message": "hello"}
{"message": "bye"}
and that was a regular line. The next one is empty, despite having a RS (and the one after is entirely empty):

`
logs, err := testClientLogs(cs, c)
c.Assert(err, check.IsNil)
c.Check(logs, check.DeepEquals, []client.Log{{Message: "hello"}, {Message: "bye"}})
}
func (cs *clientSuite) TestClientLogsSad(c *check.C) {
cs.err = fmt.Errorf("xyzzy")
actual, err := testClientLogs(cs, c)
c.Assert(err, check.ErrorMatches, ".* xyzzy")
c.Check(actual, check.HasLen, 0)
}
func (cs *clientSuite) TestClientLogsOpts(c *check.C) {
const (
maxint = int((^uint(0)) >> 1)
minint = -maxint - 1
)
for _, names := range [][]string{nil, {}, {"foo"}, {"foo", "bar"}} {
for _, n := range []int{-1, 0, 1, minint, maxint} {
for _, follow := range []bool{true, false} {
iterdesc := check.Commentf("names: %v, n: %v, follow: %v", names, n, follow)
ch, err := cs.cli.Logs(names, client.LogOptions{N: n, Follow: follow})
c.Check(err, check.IsNil, iterdesc)
c.Check(cs.req.URL.Path, check.Equals, "/v2/logs", iterdesc)
c.Check(cs.req.Method, check.Equals, "GET", iterdesc)
query := cs.req.URL.Query()
numQ := 0
var namesout []string
if ns := query.Get("names"); ns != "" {
namesout = strings.Split(ns, ",")
}
c.Check(len(namesout), check.Equals, len(names), iterdesc)
if len(names) != 0 {
c.Check(namesout, check.DeepEquals, names, iterdesc)
numQ++
}
nout, nerr := strconv.Atoi(query.Get("n"))
c.Check(nerr, check.IsNil, iterdesc)
c.Check(nout, check.Equals, n, iterdesc)
numQ++
if follow {
fout, ferr := strconv.ParseBool(query.Get("follow"))
c.Check(fout, check.Equals, true, iterdesc)
c.Check(ferr, check.IsNil, iterdesc)
numQ++
}
c.Check(query, check.HasLen, numQ, iterdesc)
for x := range ch {
c.Logf("expecting empty channel, got %v during %s", x, iterdesc)
c.Fail()
}
}
}
}
}
func (cs *clientSuite) TestClientLogsNotFound(c *check.C) {
cs.rsp = `{"type":"error","status-code":404,"status":"Not Found","result":{"message":"snap \"foo\" not found","kind":"snap-not-found","value":"foo"}}`
cs.status = 404
actual, err := testClientLogs(cs, c)
c.Assert(err, check.ErrorMatches, `snap "foo" not found`)
c.Check(actual, check.HasLen, 0)
}
func (cs *clientSuite) checkCommonFields(c *check.C, reqOp map[string]interface{}, names []string, scope client.ScopeSelector, users client.UserSelector, comment check.CommentInterface) {
inames := make([]interface{}, len(names))
for i, name := range names {
inames[i] = interface{}(name)
}
c.Check(reqOp["names"], check.DeepEquals, inames, comment)
if len(scope) > 0 {
snames := make([]interface{}, len(scope))
for i, scope := range scope {
snames[i] = interface{}(scope)
}
c.Check(reqOp["scope"], check.DeepEquals, snames, comment)
} else {
c.Check(reqOp["scope"], check.IsNil, comment)
}
switch users.Selector {
case client.UserSelectionSelf:
c.Check(reqOp["users"], check.Equals, `self`, comment)
case client.UserSelectionAll:
c.Check(reqOp["users"], check.Equals, `all`, comment)
case client.UserSelectionList:
if len(users.Names) > 0 {
unames := make([]interface{}, len(users.Names))
for i, u := range users.Names {
unames[i] = interface{}(u)
}
c.Check(reqOp["users"], check.DeepEquals, unames, comment)
} else {
c.Check(reqOp["users"], check.IsNil, comment)
}
}
}
func (cs *clientSuite) TestClientServiceStart(c *check.C) {
cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
tests := []struct {
names []string
scope client.ScopeSelector
users client.UserSelector
opts client.StartOptions
}{
{},
{
opts: client.StartOptions{
Enable: true,
},
},
{
names: []string{"foo"},
},
{
names: []string{"foo"},
opts: client.StartOptions{
Enable: true,
},
},
{
names: []string{"foo", "bar", "baz"},
},
{
names: []string{"foo", "bar", "baz"},
opts: client.StartOptions{
Enable: true,
},
},
{
names: []string{"foo"},
scope: []string{"user"},
},
{
names: []string{"foo"},
scope: []string{"system"},
},
{
names: []string{"foo"},
scope: []string{"system", "user"},
},
{
names: []string{"foo"},
users: client.UserSelector{
Selector: client.UserSelectionSelf,
},
},
{
names: []string{"foo"},
users: client.UserSelector{
Selector: client.UserSelectionAll,
},
},
}
for _, sc := range tests {
comment := check.Commentf("{%q; %q; %q; %#v}", sc.names, sc.scope, sc.users, sc.opts)
id, err := cs.cli.Start(sc.names, sc.scope, sc.users, sc.opts)
if len(sc.names) == 0 {
c.Check(id, check.Equals, "", comment)
c.Check(err, check.Equals, client.ErrNoNames, comment)
c.Check(cs.req, check.IsNil, comment) // i.e. the request was never done
} else {
c.Assert(err, check.IsNil, comment)
c.Check(id, check.Equals, "24", comment)
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", comment)
c.Check(cs.req.Method, check.Equals, "POST", comment)
c.Check(cs.req.URL.Query(), check.HasLen, 0, comment)
var reqOp map[string]interface{}
c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, comment)
c.Check(reqOp["action"], check.Equals, "start", comment)
cs.checkCommonFields(c, reqOp, sc.names, sc.scope, sc.users, comment)
if sc.opts.Enable {
c.Check(reqOp["enable"], check.Equals, true, comment)
} else {
c.Check(reqOp["enable"], check.IsNil, comment)
}
}
}
}
func (cs *clientSuite) TestClientServiceStop(c *check.C) {
cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
tests := []struct {
names []string
scope client.ScopeSelector
users client.UserSelector
opts client.StopOptions
}{
{},
{
opts: client.StopOptions{
Disable: true,
},
},
{
names: []string{"foo"},
},
{
names: []string{"foo"},
opts: client.StopOptions{
Disable: true,
},
},
{
names: []string{"foo", "bar", "baz"},
},
{
names: []string{"foo", "bar", "baz"},
opts: client.StopOptions{
Disable: true,
},
},
{
names: []string{"foo"},
scope: []string{"user"},
},
{
names: []string{"foo"},
scope: []string{"system"},
},
{
names: []string{"foo"},
scope: []string{"system", "user"},
},
{
names: []string{"foo"},
users: client.UserSelector{
Selector: client.UserSelectionSelf,
},
},
{
names: []string{"foo"},
users: client.UserSelector{
Selector: client.UserSelectionAll,
},
},
}
for _, sc := range tests {
comment := check.Commentf("{%q; %q; %q; %#v}", sc.names, sc.scope, sc.users, sc.opts)
id, err := cs.cli.Stop(sc.names, sc.scope, sc.users, sc.opts)
if len(sc.names) == 0 {
c.Check(id, check.Equals, "", comment)
c.Check(err, check.Equals, client.ErrNoNames, comment)
c.Check(cs.req, check.IsNil, comment) // i.e. the request was never done
} else {
c.Assert(err, check.IsNil, comment)
c.Check(id, check.Equals, "24", comment)
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", comment)
c.Check(cs.req.Method, check.Equals, "POST", comment)
c.Check(cs.req.URL.Query(), check.HasLen, 0, comment)
var reqOp map[string]interface{}
c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, comment)
c.Check(reqOp["action"], check.Equals, "stop", comment)
cs.checkCommonFields(c, reqOp, sc.names, sc.scope, sc.users, comment)
if sc.opts.Disable {
c.Check(reqOp["disable"], check.Equals, true, comment)
} else {
c.Check(reqOp["disable"], check.IsNil, comment)
}
}
}
}
func (cs *clientSuite) TestClientServiceRestart(c *check.C) {
cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
tests := []struct {
names []string
scope client.ScopeSelector
users client.UserSelector
opts client.RestartOptions
}{
{},
{
opts: client.RestartOptions{
Reload: true,
},
},
{
names: []string{"foo"},
},
{
names: []string{"foo"},
opts: client.RestartOptions{
Reload: true,
},
},
{
names: []string{"foo", "bar", "baz"},
},
{
names: []string{"foo", "bar", "baz"},
opts: client.RestartOptions{
Reload: true,
},
},
{
names: []string{"foo"},
scope: []string{"user"},
},
{
names: []string{"foo"},
scope: []string{"system"},
},
{
names: []string{"foo"},
scope: []string{"system", "user"},
},
{
names: []string{"foo"},
users: client.UserSelector{
Selector: client.UserSelectionSelf,
},
},
{
names: []string{"foo"},
users: client.UserSelector{
Selector: client.UserSelectionAll,
},
},
}
for _, sc := range tests {
comment := check.Commentf("{%q; %q; %q; %#v}", sc.names, sc.scope, sc.users, sc.opts)
id, err := cs.cli.Restart(sc.names, sc.scope, sc.users, sc.opts)
if len(sc.names) == 0 {
c.Check(id, check.Equals, "", comment)
c.Check(err, check.Equals, client.ErrNoNames, comment)
c.Check(cs.req, check.IsNil, comment) // i.e. the request was never done
} else {
c.Assert(err, check.IsNil, comment)
c.Check(id, check.Equals, "24", comment)
c.Check(cs.req.URL.Path, check.Equals, "/v2/apps", comment)
c.Check(cs.req.Method, check.Equals, "POST", comment)
c.Check(cs.req.URL.Query(), check.HasLen, 0, comment)
var reqOp map[string]interface{}
c.Assert(json.NewDecoder(cs.req.Body).Decode(&reqOp), check.IsNil, comment)
cs.checkCommonFields(c, reqOp, sc.names, sc.scope, sc.users, comment)
c.Check(reqOp["action"], check.Equals, "restart", comment)
if sc.opts.Reload {
c.Check(reqOp["reload"], check.Equals, true, comment)
} else {
c.Check(reqOp["reload"], check.IsNil, comment)
}
}
}
}
type userSelectorSuite struct{}
var _ = check.Suite(&userSelectorSuite{})
func (s *userSelectorSuite) TestUserScopeMarshalListOfUsernames(c *check.C) {
us := client.UserSelector{
Names: []string{"user", "user-two"},
}
b, err := json.Marshal(us)
c.Assert(err, check.IsNil)
c.Check(string(b), check.Equals, `["user","user-two"]`)
}
func (s *userSelectorSuite) TestUserScopeMarshalStringKeyword(c *check.C) {
us := client.UserSelector{
Selector: client.UserSelectionSelf,
}
b, err := json.Marshal(us)
c.Assert(err, check.IsNil)
c.Check(string(b), check.Equals, `"self"`)
}
func (s *userSelectorSuite) TestUserScopeMarshalInvalidSelector(c *check.C) {
us := client.UserSelector{
Selector: 42,
}
_, err := json.Marshal(us)
c.Assert(err, check.ErrorMatches, `.* internal error: unsupported selector 42 specified`)
}
func (s *userSelectorSuite) TestUserScopeUnmarshalInvalidType(c *check.C) {
const userScopeJson = `1`
var us client.UserSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.ErrorMatches, `cannot unmarshal, expected a string or a list of strings`)
}
func (s *userSelectorSuite) TestUserScopeUnmarshalListOfUsernames(c *check.C) {
const userScopeJson = `["my-user","other-user"]`
var us client.UserSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.IsNil)
c.Check(us, check.DeepEquals, client.UserSelector{
Names: []string{"my-user", "other-user"},
})
}
func (s *userSelectorSuite) TestUserScopeUnmarshalStringKeyword(c *check.C) {
const userScopeJson = `"all"`
var us client.UserSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.IsNil)
c.Check(us, check.DeepEquals, client.UserSelector{
Selector: client.UserSelectionAll,
})
}
func (s *userSelectorSuite) TestUserListCurrentUser(c *check.C) {
us := client.UserSelector{
Selector: client.UserSelectionSelf,
}
users, err := us.UserList(&user.User{
Uid: "1000",
Username: "my-user",
})
c.Assert(err, check.IsNil)
c.Check(users, check.DeepEquals, []string{"my-user"})
}
func (s *userSelectorSuite) TestUserListCurrentUserInvalidNil(c *check.C) {
us := client.UserSelector{
Selector: client.UserSelectionSelf,
}
users, err := us.UserList(nil)
c.Assert(err, check.ErrorMatches, `internal error: for "self" the current user must be provided`)
c.Check(users, check.IsNil)
}
func (s *userSelectorSuite) TestUserListCurrentUserNotValidForRoot(c *check.C) {
us := client.UserSelector{
Selector: client.UserSelectionSelf,
}
users, err := us.UserList(&user.User{
Uid: "0",
Username: "my-user",
})
c.Assert(err, check.ErrorMatches, `cannot use "self" for root user`)
c.Check(users, check.IsNil)
}
func (s *userSelectorSuite) TestUserListInvalidSelector(c *check.C) {
us := client.UserSelector{
Selector: 42,
}
users, err := us.UserList(nil)
c.Assert(err, check.ErrorMatches, `internal error: unsupported selector 42 specified`)
c.Check(users, check.IsNil)
}
func (s *userSelectorSuite) TestUserListUsersReturnsEmpty(c *check.C) {
us := client.UserSelector{
Selector: client.UserSelectionAll,
}
users, err := us.UserList(nil)
c.Assert(err, check.IsNil)
c.Check(users, check.IsNil)
}
type scopeSelectorSuite struct{}
var _ = check.Suite(&scopeSelectorSuite{})
func (s *scopeSelectorSuite) TestScopeUnmarshalInvalidType(c *check.C) {
const userScopeJson = `1`
var us client.ScopeSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.ErrorMatches, `cannot unmarshal, expected a list of strings`)
}
func (s *scopeSelectorSuite) TestScopeUnmarshalInvalidKeyword(c *check.C) {
const userScopeJson = `["all"]`
var us client.ScopeSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.ErrorMatches, `cannot unmarshal, expected one of: "system", "user"`)
}
func (s *scopeSelectorSuite) TestScopeUnmarshalNone(c *check.C) {
const userScopeJson = `[]`
var us client.ScopeSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.IsNil)
c.Check(us, check.DeepEquals, client.ScopeSelector{})
}
func (s *scopeSelectorSuite) TestScopeUnmarshalSystem(c *check.C) {
const userScopeJson = `["system"]`
var us client.ScopeSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.IsNil)
c.Check(us, check.DeepEquals, client.ScopeSelector{"system"})
}
func (s *scopeSelectorSuite) TestScopeUnmarshalUser(c *check.C) {
const userScopeJson = `["user"]`
var us client.ScopeSelector
err := json.Unmarshal([]byte(userScopeJson), &us)
c.Assert(err, check.IsNil)
c.Check(us, check.DeepEquals, client.ScopeSelector{"user"})
}