+ elems = attr_class[0].split(" ")
+ if "labels" in elems:
+ self.in_labels = True
+ self.deep = 1
+ logging.debug("labels start")
+ else:
+ # nesting counter
+ self.deep += 1
+
+ # inside labels
+ # label entry has
+ #
+ attr_data_name = [attr[1] for attr in attributes if attr[0] == "data-name"]
+ if len(attr_data_name) == 0:
+ return
+ data_name = attr_data_name[0]
+ logging.debug("found label: %s", data_name)
+ self.labels.append(data_name)
+
+ def handle_endtag(self, tag):
+ if self.in_labels:
+ self.deep -= 1
+ if self.deep < 1:
+ logging.debug("labels end")
+ self.in_labels = False
+
+ def handle_data(self, data):
+ if self.in_labels:
+ logging.debug("data: %s", data)
+
+
+def grab_pr_labels(pr_number: int):
+ # ideally we would use the github API - however we can't because:
+ # a) its rate limiting and travis IPs hit the API a lot so we regularly
+ # get errors
+ # b) using a API token is tricky because travis will not allow the secure
+ # vars for forks
+ # so instead we just scrape the html title which is unlikely to change
+ # radically
+ parser = GithubLabelsParser()
+ with urllib.request.urlopen(
+ "https://github.com/snapcore/snapd/pull/{}".format(pr_number)
+ ) as f:
+ parser.feed(f.read().decode("utf-8"))
+ return parser.labels
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "pr_number", metavar="PR number", help="the github PR number to check"
+ )
+ parser.add_argument(
+ "-d", "--debug", help="enable debug logging", action="store_true"
+ )
+ args = parser.parse_args()
+
+ lvl = logging.INFO
+ if args.debug:
+ lvl = logging.DEBUG
+ logging.basicConfig(level=lvl)
+
+ labels = grab_pr_labels(args.pr_number)
+ print("labels:", labels)
+
+ if LABEL_SKIP_SPREAD_JOB not in labels:
+ raise SystemExit(1)
+
+ print("requested to skip the spread job")
+
+
+if __name__ == "__main__":
+ main()
diff -Nru snapd-2.37.4ubuntu0.1/check-pr-title.py snapd-2.45.1ubuntu0.2/check-pr-title.py
--- snapd-2.37.4ubuntu0.1/check-pr-title.py 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/check-pr-title.py 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,78 @@
+#!/usr/bin/python3
+
+import argparse
+import re
+import urllib.request
+
+from html.parser import HTMLParser
+
+
+class InvalidPRTitle(Exception):
+ def __init__(self, invalid_title):
+ self.invalid_title = invalid_title
+
+
+class GithubTitleParser(HTMLParser):
+ def __init__(self):
+ HTMLParser.__init__(self)
+ self._cur_tag = ""
+ self.title = ""
+
+ def handle_starttag(self, tag, attributes):
+ self._cur_tag = tag
+
+ def handle_endtag(self, tag):
+ self._cur_tag = ""
+
+ def handle_data(self, data):
+ if self._cur_tag == "title":
+ self.title = data
+
+
+def check_pr_title(pr_number: int):
+ # ideally we would use the github API - however we can't because:
+ # a) its rate limiting and travis IPs hit the API a lot so we regularly
+ # get errors
+ # b) using a API token is tricky because travis will not allow the secure
+ # vars for forks
+ # so instead we just scrape the html title which is unlikely to change
+ # radically
+ parser = GithubTitleParser()
+ with urllib.request.urlopen(
+ "https://github.com/snapcore/snapd/pull/{}".format(pr_number)
+ ) as f:
+ parser.feed(f.read().decode("utf-8"))
+ # the title has the format:
+ # "Added api endpoint for downloading snaps by glower · Pull Request #6958 · snapcore/snapd · GitHub"
+ # so we rsplit() once to get the title (rsplit to not get confused by
+ # possible "by" words in the real title)
+ title = parser.title.rsplit(" by ", maxsplit=1)[0]
+ print(title)
+ # cover most common cases:
+ # package: foo
+ # package, otherpackage/subpackage: this is a title
+ # tests/regression/lp-12341234: foo
+ # [RFC] foo: bar
+ if not re.match(r"[a-zA-Z0-9_\-/,. \[\]{}]+: .*", title):
+ raise InvalidPRTitle(title)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "pr_number", metavar="PR number", help="the github PR number to check"
+ )
+ args = parser.parse_args()
+ try:
+ check_pr_title(args.pr_number)
+ except InvalidPRTitle as e:
+ print('Invalid PR title: "{}"\n'.format(e.invalid_title))
+ print("Please provide a title in the following format:")
+ print("module: short description")
+ print("E.g.:")
+ print("daemon: fix frobinator bug")
+ raise SystemExit(1)
+
+
+if __name__ == "__main__":
+ main()
diff -Nru snapd-2.37.4ubuntu0.1/.clang-format snapd-2.45.1ubuntu0.2/.clang-format
--- snapd-2.37.4ubuntu0.1/.clang-format 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/.clang-format 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,3 @@
+BasedOnStyle: Google
+IndentWidth: 4
+ColumnLimit: 120
diff -Nru snapd-2.37.4ubuntu0.1/client/aliases_test.go snapd-2.45.1ubuntu0.2/client/aliases_test.go
--- snapd-2.37.4ubuntu0.1/client/aliases_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/aliases_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -34,6 +34,7 @@
}
func (cs *clientSuite) TestClientAlias(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -62,6 +63,7 @@
}
func (cs *clientSuite) TestClientUnalias(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -89,6 +91,7 @@
}
func (cs *clientSuite) TestClientDisableAllAliases(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -115,6 +118,7 @@
}
func (cs *clientSuite) TestClientRemoveManualAlias(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -141,6 +145,7 @@
}
func (cs *clientSuite) TestClientPrefer(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
diff -Nru snapd-2.37.4ubuntu0.1/client/apps.go snapd-2.45.1ubuntu0.2/client/apps.go
--- snapd-2.37.4ubuntu0.1/client/apps.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/apps.go 2020-06-05 13:13:49.000000000 +0000
@@ -22,6 +22,7 @@
import (
"bufio"
"bytes"
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -119,7 +120,7 @@
query.Set("follow", strconv.FormatBool(opts.Follow))
}
- rsp, err := client.raw("GET", "/v2/logs", query, nil, nil)
+ rsp, err := client.raw(context.Background(), "GET", "/v2/logs", query, nil, nil)
if err != nil {
return nil, err
}
@@ -130,7 +131,7 @@
if err := decodeInto(rsp.Body, &r); err != nil {
return nil, err
}
- return nil, r.err(client)
+ return nil, r.err(client, rsp.StatusCode)
}
ch := make(chan Log, 20)
diff -Nru snapd-2.37.4ubuntu0.1/client/apps_test.go snapd-2.45.1ubuntu0.2/client/apps_test.go
--- snapd-2.37.4ubuntu0.1/client/apps_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/apps_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -107,6 +107,11 @@
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")
@@ -213,6 +218,7 @@
}
func (cs *clientSuite) TestClientServiceStart(c *check.C) {
+ cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
type scenario struct {
@@ -274,6 +280,7 @@
}
func (cs *clientSuite) TestClientServiceStop(c *check.C) {
+ cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
type tT struct {
@@ -335,6 +342,7 @@
}
func (cs *clientSuite) TestClientServiceRestart(c *check.C) {
+ cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
type tT struct {
diff -Nru snapd-2.37.4ubuntu0.1/client/asserts.go snapd-2.45.1ubuntu0.2/client/asserts.go
--- snapd-2.37.4ubuntu0.1/client/asserts.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/asserts.go 2020-06-05 13:13:49.000000000 +0000
@@ -21,12 +21,16 @@
import (
"bytes"
+ "context"
"fmt"
"io"
"net/url"
"strconv"
+ "golang.org/x/xerrors"
+
"github.com/snapcore/snapd/asserts" // for parsing
+ "github.com/snapcore/snapd/snap"
)
// Ack tries to add an assertion to the system assertion
@@ -49,14 +53,25 @@
}
_, err := client.doSync("GET", "/v2/assertions", nil, nil, nil, &types)
if err != nil {
- return nil, fmt.Errorf("cannot get assertion type names: %v", err)
+ fmt := "cannot get assertion type names: %w"
+ return nil, xerrors.Errorf(fmt, err)
}
return types.Types, nil
}
+// KnownOptions represent the options of the Known call.
+type KnownOptions struct {
+ // If Remote is true, the store is queried to find the assertion
+ Remote bool
+}
+
// Known queries assertions with type assertTypeName and matching assertion headers.
-func (client *Client) Known(assertTypeName string, headers map[string]string) ([]asserts.Assertion, error) {
+func (client *Client) Known(assertTypeName string, headers map[string]string, opts *KnownOptions) ([]asserts.Assertion, error) {
+ if opts == nil {
+ opts = &KnownOptions{}
+ }
+
path := fmt.Sprintf("/v2/assertions/%s", assertTypeName)
q := url.Values{}
@@ -65,11 +80,18 @@
q.Set(k, v)
}
}
+ if opts.Remote {
+ q.Set("remote", "true")
+ }
- response, err := client.raw("GET", path, q, nil, nil)
+ ctx, cancel := context.WithTimeout(context.Background(), doTimeout)
+ defer cancel()
+ response, err := client.raw(ctx, "GET", path, q, nil, nil)
if err != nil {
- return nil, fmt.Errorf("failed to query assertions: %v", err)
+ fmt := "failed to query assertions: %w"
+ return nil, xerrors.Errorf(fmt, err)
}
+
defer response.Body.Close()
if response.StatusCode != 200 {
return nil, parseError(response)
@@ -102,3 +124,31 @@
return asserts, nil
}
+
+// StoreAccount returns the full store account info for the specified accountID
+func (client *Client) StoreAccount(accountID string) (*snap.StoreAccount, error) {
+ assertions, err := client.Known("account", map[string]string{"account-id": accountID}, nil)
+ if err != nil {
+ return nil, err
+ }
+ switch len(assertions) {
+ case 1:
+ // happy case, break out of the switch
+ case 0:
+ return nil, fmt.Errorf("no assertion found for account-id %s", accountID)
+ default:
+ // unknown how this could happen...
+ return nil, fmt.Errorf("multiple assertions for account-id %s", accountID)
+ }
+
+ acct, ok := assertions[0].(*asserts.Account)
+ if !ok {
+ return nil, fmt.Errorf("incorrect type of account assertion returned")
+ }
+ return &snap.StoreAccount{
+ ID: acct.AccountID(),
+ Username: acct.Username(),
+ DisplayName: acct.DisplayName(),
+ Validation: acct.Validation(),
+ }, nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/asserts_test.go snapd-2.45.1ubuntu0.2/client/asserts_test.go
--- snapd-2.37.4ubuntu0.1/client/asserts_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/asserts_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -25,9 +25,13 @@
"net/http"
"net/url"
+ "golang.org/x/xerrors"
+
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/snap"
)
func (cs *clientSuite) TestClientAssert(c *C) {
@@ -60,16 +64,23 @@
}
func (cs *clientSuite) TestClientAssertsCallsEndpoint(c *C) {
- _, _ = cs.cli.Known("snap-revision", nil)
+ _, _ = cs.cli.Known("snap-revision", nil, nil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.URL.Path, Equals, "/v2/assertions/snap-revision")
+}
+
+func (cs *clientSuite) TestClientAssertsOptsCallsEndpoint(c *C) {
+ _, _ = cs.cli.Known("snap-revision", nil, &client.KnownOptions{Remote: true})
c.Check(cs.req.Method, Equals, "GET")
c.Check(cs.req.URL.Path, Equals, "/v2/assertions/snap-revision")
+ c.Check(cs.req.URL.Query()["remote"], DeepEquals, []string{"true"})
}
func (cs *clientSuite) TestClientAssertsCallsEndpointWithFilter(c *C) {
_, _ = cs.cli.Known("snap-revision", map[string]string{
"snap-id": "snap-id-1",
"snap-sha3-384": "sha3-384...",
- })
+ }, nil)
u, err := url.ParseRequestURI(cs.req.URL.String())
c.Assert(err, IsNil)
c.Check(u.Path, Equals, "/v2/assertions/snap-revision")
@@ -81,7 +92,7 @@
func (cs *clientSuite) TestClientAssertsHttpError(c *C) {
cs.err = errors.New("fail")
- _, err := cs.cli.Known("snap-build", nil)
+ _, err := cs.cli.Known("snap-build", nil, nil)
c.Assert(err, ErrorMatches, "failed to query assertions: cannot communicate with server: fail")
}
@@ -96,7 +107,7 @@
"message": "invalid"
}
}`
- _, err := cs.cli.Known("snap-build", nil)
+ _, err := cs.cli.Known("snap-build", nil, nil)
c.Assert(err, ErrorMatches, "invalid")
}
@@ -132,7 +143,7 @@
openpgp ...
`
- a, err := cs.cli.Known("snap-revision", nil)
+ a, err := cs.cli.Known("snap-revision", nil, nil)
c.Assert(err, IsNil)
c.Check(a, HasLen, 2)
@@ -144,7 +155,7 @@
cs.header.Add("X-Ubuntu-Assertions-Count", "0")
cs.rsp = ""
cs.status = 200
- a, err := cs.cli.Known("snap-revision", nil)
+ a, err := cs.cli.Known("snap-revision", nil, nil)
c.Assert(err, IsNil)
c.Check(a, HasLen, 0)
}
@@ -154,6 +165,75 @@
cs.header.Add("X-Ubuntu-Assertions-Count", "4")
cs.rsp = ""
cs.status = 200
- _, err := cs.cli.Known("snap-build", nil)
+ _, err := cs.cli.Known("snap-build", nil, nil)
c.Assert(err, ErrorMatches, "response did not have the expected number of assertions")
}
+
+func (cs *clientSuite) TestStoreAccount(c *C) {
+ cs.header = http.Header{}
+ cs.header.Add("X-Ubuntu-Assertions-Count", "1")
+ cs.rsp = `type: account
+authority-id: canonical
+account-id: canonicalID
+display-name: canonicalDisplay
+timestamp: 2016-04-01T00:00:00.0Z
+username: canonicalUser
+validation: certified
+sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
+
+AcLDXAQAAQoABgUCV7UYzwAKCRDUpVvql9g3IK7uH/4udqNOurx5WYVknzXdwekp0ovHCQJ0iBPw
+TSFxEVr9faZSzb7eqJ1WicHsShf97PYS3ClRYAiluFsjRA8Y03kkSVJHjC+sIwGFubsnkmgflt6D
+WEmYIl0UBmeaEDS8uY4Xvp9NsLTzNEj2kvzy/52gKaTc1ZSl5RDL9ppMav+0V9iBYpiDPBWH2rJ+
+aDSD8Rkyygm0UscfAKyDKH4lrvZ0WkYyi1YVNPrjQ/AtBySh6Q4iJ3LifzKa9woIyAuJET/4/FPY
+oirqHAfuvNod36yNQIyNqEc20AvTvZNH0PSsg4rq3DLjIPzv5KbJO9lhsasNJK1OdL6x8Yqrdsbk
+ldZp4qkzfjV7VOMQKaadfcZPRaVVeJWOBnBiaukzkhoNlQi1sdCdkBB/AJHZF8QXw6c7vPDcfnCV
+1lW7ddQ2p8IsJbT6LzpJu3GW/P4xhNgCjtCJ1AJm9a9RqLwQYgdLZwwDa9iCRtqTbRXBlfy3apps
+1VjbQ3h5iCd0hNfwDBnGVm1rhLKHCD1DUdNE43oN2ZlE7XGyh0HFV6vKlpqoW3eoXCIxWu+HBY96
++LSl/jQgCkb0nxYyzEYK4Reb31D0mYw1Nji5W+MIF5E09+DYZoOT0UvR05YMwMEOeSdI/hLWg/5P
+k+GDK+/KopMmpd4D1+jjtF7ZvqDpmAV98jJGB2F88RyVb4gcjmFFyTi4Kv6vzz/oLpbm0qrizC0W
+HLGDN/ymGA5sHzEgEx7U540vz/q9VX60FKqL2YZr/DcyY9GKX5kCG4sNqIIHbcJneZ4frM99oVDu
+7Jv+DIx/Di6D1ULXol2XjxbbJLKHFtHksR97ceaFvcZwTogC61IYUBJCvvMoqdXAWMhEXCr0QfQ5
+Xbi31XW2d4/lF/zWlAkRnGTzufIXFni7+nEuOK0SQEzO3/WaRedK1SGOOtTDjB8/3OJeW96AUYK5
+oTIynkYkEyHWMNCXALg+WQW6L4/YO7aUjZ97zOWIugd7Xy63aT3r/EHafqaY2nacOhLfkeKZ830b
+o/ezjoZQAxbh6ce7JnXRgE9ELxjdAhBTpGjmmmN2sYrJ7zP9bOgly0BnEPXGSQfFA+NNNw1FADx1
+MUY8q9DBjmVtgqY+1KGTV5X8KvQCBMODZIf/XJPHdCRAHxMd8COypcwgL2vDIIXpOFbi1J/B0GF+
+eklxk9wzBA8AecBMCwCzIRHDNpD1oa2we38bVFrOug6e/VId1k1jYFJjiLyLCDmV8IMYwEllHSXp
+LQAdm3xZ7t4WnxYC8YSCk9mXf3CZg59SpmnV5Q5Z6A5Pl7Nc3sj7hcsMBZEsOMPzNC9dPsBnZvjs
+WpPUffJzEdhHBFhvYMuD4Vqj6ejUv9l3oTrjQWVC
+`
+
+ account, err := cs.cli.StoreAccount("canonicalID")
+ c.Assert(err, IsNil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.URL.Query(), HasLen, 1)
+ c.Check(cs.req.URL.Query().Get("account-id"), Equals, "canonicalID")
+ c.Assert(account, DeepEquals, &snap.StoreAccount{
+ ID: "canonicalID",
+ Username: "canonicalUser",
+ DisplayName: "canonicalDisplay",
+ Validation: "verified",
+ })
+}
+
+func (cs *clientSuite) TestStoreAccountNoAssertionFound(c *C) {
+ cs.header = http.Header{}
+ cs.header.Add("X-Ubuntu-Assertions-Count", "0")
+ cs.rsp = ""
+
+ _, err := cs.cli.StoreAccount("canonicalID")
+ c.Assert(err, ErrorMatches, "no assertion found for account-id canonicalID")
+}
+
+func (cs *clientSuite) TestClientAssertTypesErrIsWrapped(c *C) {
+ cs.err = errors.New("boom")
+ _, err := cs.cli.AssertionTypes()
+ var e xerrors.Wrapper
+ c.Assert(err, Implements, &e)
+}
+
+func (cs *clientSuite) TestClientKnownErrIsWrapped(c *C) {
+ cs.err = errors.New("boom")
+ _, err := cs.cli.Known("foo", nil, nil)
+ var e xerrors.Wrapper
+ c.Assert(err, Implements, &e)
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/client.go snapd-2.45.1ubuntu0.2/client/client.go
--- snapd-2.37.4ubuntu0.1/client/client.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/client.go 2020-06-05 13:13:49.000000000 +0000
@@ -21,6 +21,7 @@
import (
"bytes"
+ "context"
"encoding/json"
"fmt"
"io"
@@ -70,6 +71,9 @@
// DisableKeepAlive indicates whether the connections should not be kept
// alive for later reuse
DisableKeepAlive bool
+
+ // User-Agent to sent to the snapd daemon
+ UserAgent string
}
// A Client knows how to talk to the snappy daemon.
@@ -84,6 +88,8 @@
warningCount int
warningTimestamp time.Time
+
+ userAgent string
}
// New returns a new instance of Client
@@ -103,6 +109,7 @@
doer: &http.Client{Transport: transport},
disableAuth: config.DisableAuth,
interactive: config.Interactive,
+ userAgent: config.UserAgent,
}
}
@@ -115,6 +122,7 @@
doer: &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}},
disableAuth: config.DisableAuth,
interactive: config.Interactive,
+ userAgent: config.UserAgent,
}
}
@@ -172,10 +180,23 @@
return fmt.Sprintf("cannot add authorization: %v", e.error)
}
-type ConnectionError struct{ error }
+type ConnectionError struct{ Err error }
func (e ConnectionError) Error() string {
- return fmt.Sprintf("cannot communicate with server: %v", e.error)
+ var errStr string
+ switch e.Err {
+ case context.DeadlineExceeded:
+ errStr = "timeout exceeded while waiting for response"
+ case context.Canceled:
+ errStr = "request canceled"
+ default:
+ errStr = e.Err.Error()
+ }
+ return fmt.Sprintf("cannot communicate with server: %s", errStr)
+}
+
+func (e ConnectionError) Unwrap() error {
+ return e.Err
}
// AllowInteractionHeader is the HTTP request header used to indicate
@@ -185,7 +206,7 @@
// raw performs a request and returns the resulting http.Response and
// error you usually only need to call this directly if you expect the
// response to not be JSON, otherwise you'd call Do(...) instead.
-func (client *Client) raw(method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) {
+func (client *Client) raw(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) {
// fake a url to keep http.Client happy
u := client.baseURL
u.Path = path.Join(client.baseURL.Path, urlpath)
@@ -194,6 +215,9 @@
if err != nil {
return nil, RequestError{err}
}
+ if client.userAgent != "" {
+ req.Header.Set("User-Agent", client.userAgent)
+ }
for key, value := range headers {
req.Header.Set(key, value)
@@ -211,6 +235,10 @@
req.Header.Set(AllowInteractionHeader, "true")
}
+ if ctx != nil {
+ req = req.WithContext(ctx)
+ }
+
rsp, err := client.doer.Do(req)
if err != nil {
return nil, ConnectionError{err}
@@ -219,13 +247,36 @@
return rsp, nil
}
+// rawWithTimeout is like raw(), but sets a timeout for the whole of request and
+// response (including rsp.Body() read) round trip. The caller is responsible
+// for canceling the internal context to release the resources associated with
+// the request by calling the returned cancel function.
+func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, timeout time.Duration) (*http.Response, context.CancelFunc, error) {
+ if timeout == 0 {
+ return nil, nil, fmt.Errorf("internal error: timeout not set for rawWithTimeout")
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, timeout)
+ rsp, err := client.raw(ctx, method, urlpath, query, headers, body)
+ if err != nil && ctx.Err() != nil {
+ cancel()
+ return nil, nil, ConnectionError{ctx.Err()}
+ }
+
+ return rsp, cancel, err
+}
+
var (
- doRetry = 250 * time.Millisecond
- doTimeout = 5 * time.Second
+ doRetry = 250 * time.Millisecond
+ // snapd may need to reach out to the store, where it uses a fixed 10s
+ // timeout for the whole of a single request to complete, requests are
+ // retried for up to 38s in total, make sure that the client timeout is
+ // not shorter than that
+ doTimeout = 50 * time.Second
)
-// MockDoRetry mocks the delays used by the do retry loop.
-func MockDoRetry(retry, timeout time.Duration) (restore func()) {
+// MockDoTimings mocks the delay used by the do retry loop and request timeout.
+func MockDoTimings(retry, timeout time.Duration) (restore func()) {
oldRetry := doRetry
oldTimeout := doTimeout
doRetry = retry
@@ -249,39 +300,56 @@
client.doer = hijacked{f}
}
+type doFlags struct {
+ NoTimeout bool
+}
+
// do performs a request and decodes the resulting json into the given
// value. It's low-level, for testing/experimenting only; you should
// usually use a higher level interface that builds on this.
-func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) error {
+func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, flags doFlags) (statusCode int, err error) {
retry := time.NewTicker(doRetry)
defer retry.Stop()
- timeout := time.After(doTimeout)
+ timeout := time.NewTimer(doTimeout)
+ defer timeout.Stop()
+
var rsp *http.Response
- var err error
+ var ctx context.Context = context.Background()
for {
- rsp, err = client.raw(method, path, query, headers, body)
+ if flags.NoTimeout {
+ rsp, err = client.raw(ctx, method, path, query, headers, body)
+ } else {
+ var cancel context.CancelFunc
+ // use the same timeout as for the whole of the retry
+ // loop to error out the whole do() call when a single
+ // request exceeds the deadline
+ rsp, cancel, err = client.rawWithTimeout(ctx, method, path, query, headers, body, doTimeout)
+ if err == nil {
+ defer cancel()
+ }
+ }
if err == nil || method != "GET" {
break
}
select {
case <-retry.C:
continue
- case <-timeout:
+ case <-timeout.C:
}
break
}
if err != nil {
- return err
+ return 0, err
}
defer rsp.Body.Close()
if v != nil {
if err := decodeInto(rsp.Body, v); err != nil {
- return err
+ return rsp.StatusCode, err
}
}
- return nil
+ return rsp.StatusCode, nil
}
func decodeInto(reader io.Reader, v interface{}) error {
@@ -303,10 +371,11 @@
// which produces json.Numbers instead of float64 types for numbers.
func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) {
var rsp response
- if err := client.do(method, path, query, headers, body, &rsp); err != nil {
+ statusCode, err := client.do(method, path, query, headers, body, &rsp, doFlags{})
+ if err != nil {
return nil, err
}
- if err := rsp.err(client); err != nil {
+ if err := rsp.err(client, statusCode); err != nil {
return nil, err
}
if rsp.Type != "sync" {
@@ -326,23 +395,28 @@
}
func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) {
- _, changeID, err = client.doAsyncFull(method, path, query, headers, body)
+ _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{})
return
}
-func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader) (result json.RawMessage, changeID string, err error) {
- var rsp response
+func (client *Client) doAsyncNoTimeout(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) {
+ _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{NoTimeout: true})
+ return changeID, err
+}
- if err := client.do(method, path, query, headers, body, &rsp); err != nil {
+func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader, flags doFlags) (result json.RawMessage, changeID string, err error) {
+ var rsp response
+ statusCode, err := client.do(method, path, query, headers, body, &rsp, flags)
+ if err != nil {
return nil, "", err
}
- if err := rsp.err(client); err != nil {
+ if err := rsp.err(client, statusCode); err != nil {
return nil, "", err
}
if rsp.Type != "async" {
return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type)
}
- if rsp.StatusCode != 202 {
+ if statusCode != 202 {
return nil, "", fmt.Errorf("operation not accepted")
}
if rsp.Change == "" {
@@ -359,7 +433,9 @@
OSVersionID string
OnClassic bool
- KernelVersion string
+ KernelVersion string
+ Architecture string
+ Virtualization string
}
func (client *Client) ServerVersion() (*ServerVersion, error) {
@@ -375,18 +451,18 @@
OSVersionID: sysInfo.OSRelease.VersionID,
OnClassic: sysInfo.OnClassic,
- KernelVersion: sysInfo.KernelVersion,
+ KernelVersion: sysInfo.KernelVersion,
+ Architecture: sysInfo.Architecture,
+ Virtualization: sysInfo.Virtualization,
}, nil
}
// A response produced by the REST API will usually fit in this
// (exceptions are the icons/ endpoints obvs)
type response struct {
- Result json.RawMessage `json:"result"`
- Status string `json:"status"`
- StatusCode int `json:"status-code"`
- Type string `json:"type"`
- Change string `json:"change"`
+ Result json.RawMessage `json:"result"`
+ Type string `json:"type"`
+ Change string `json:"change"`
WarningCount int `json:"warning-count"`
WarningTimestamp time.Time `json:"warning-timestamp"`
@@ -448,6 +524,10 @@
ErrorKindSystemRestart = "system-restart"
ErrorKindDaemonRestart = "daemon-restart"
+
+ ErrorKindAssertionNotFound = "assertion-not-found"
+
+ ErrorKindUnsuccessful = "unsuccessful"
)
// IsRetryable returns true if the given error is an error
@@ -481,6 +561,17 @@
return e.Kind == ErrorKindInterfacesUnchanged
}
+// IsAssertionNotFoundError returns whether the given error means that the
+// assertion wasn't found and thus the device isn't ready/seeded.
+func IsAssertionNotFoundError(err error) bool {
+ e, ok := err.(*Error)
+ if !ok || e == nil {
+ return false
+ }
+
+ return e.Kind == ErrorKindAssertionNotFound
+}
+
// OSRelease contains information about the system extracted from /etc/os-release.
type OSRelease struct {
ID string `json:"id"`
@@ -507,14 +598,16 @@
OnClassic bool `json:"on-classic"`
Managed bool `json:"managed"`
- KernelVersion string `json:"kernel-version,omitempty"`
+ KernelVersion string `json:"kernel-version,omitempty"`
+ Architecture string `json:"architecture,omitempty"`
+ Virtualization string `json:"virtualization,omitempty"`
Refresh RefreshInfo `json:"refresh,omitempty"`
Confinement string `json:"confinement"`
SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"`
}
-func (rsp *response) err(cli *Client) error {
+func (rsp *response) err(cli *Client, statusCode int) error {
if cli != nil {
maintErr := rsp.Maintenance
// avoid setting to (*client.Error)(nil)
@@ -530,9 +623,9 @@
var resultErr Error
err := json.Unmarshal(rsp.Result, &resultErr)
if err != nil || resultErr.Message == "" {
- return fmt.Errorf("server error: %q", rsp.Status)
+ return fmt.Errorf("server error: %q", http.StatusText(statusCode))
}
- resultErr.StatusCode = rsp.StatusCode
+ resultErr.StatusCode = statusCode
return &resultErr
}
@@ -548,7 +641,7 @@
return fmt.Errorf("cannot unmarshal error: %v", err)
}
- err := rsp.err(nil)
+ err := rsp.err(nil, r.StatusCode)
if err == nil {
return fmt.Errorf("server error: %q", r.Status)
}
@@ -566,105 +659,6 @@
return &sysInfo, nil
}
-// CreateUserResult holds the result of a user creation.
-type CreateUserResult struct {
- Username string `json:"username"`
- SSHKeys []string `json:"ssh-keys"`
-}
-
-// CreateUserOptions holds options for creating a local system user.
-//
-// If Known is false, the provided email is used to query the store for
-// username and SSH key details.
-//
-// If Known is true, the user will be created by looking through existing
-// system-user assertions and looking for a matching email. If Email is
-// empty then all such assertions are considered and multiple users may
-// be created.
-type CreateUserOptions struct {
- Email string `json:"email,omitempty"`
- Sudoer bool `json:"sudoer,omitempty"`
- Known bool `json:"known,omitempty"`
- ForceManaged bool `json:"force-managed,omitempty"`
-}
-
-// CreateUser creates a local system user. See CreateUserOptions for details.
-func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) {
- if options.Email == "" {
- return nil, fmt.Errorf("cannot create a user without providing an email")
- }
-
- var result CreateUserResult
- data, err := json.Marshal(options)
- if err != nil {
- return nil, err
- }
-
- if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
- return nil, fmt.Errorf("while creating user: %v", err)
- }
- return &result, nil
-}
-
-// CreateUsers creates multiple local system users. See CreateUserOptions for details.
-//
-// Results may be provided even if there are errors.
-func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) {
- for _, opts := range options {
- if opts.Email == "" && !opts.Known {
- return nil, fmt.Errorf("cannot create user from store details without an email to query for")
- }
- }
-
- var results []*CreateUserResult
- var errs []error
-
- for _, opts := range options {
- data, err := json.Marshal(opts)
- if err != nil {
- return nil, err
- }
-
- if opts.Email == "" {
- var result []*CreateUserResult
- if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
- errs = append(errs, err)
- } else {
- results = append(results, result...)
- }
- } else {
- var result *CreateUserResult
- if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil {
- errs = append(errs, err)
- } else {
- results = append(results, result)
- }
- }
- }
-
- if len(errs) == 1 {
- return results, errs[0]
- }
- if len(errs) > 1 {
- var buf bytes.Buffer
- for _, err := range errs {
- fmt.Fprintf(&buf, "\n- %s", err)
- }
- return results, fmt.Errorf("while creating users:%s", buf.Bytes())
- }
- return results, nil
-}
-
-// Users returns the local users.
-func (client *Client) Users() ([]*User, error) {
- var result []*User
-
- if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil {
- return nil, fmt.Errorf("while getting users: %v", err)
- }
- return result, nil
-}
-
type debugAction struct {
Action string `json:"action"`
Params interface{} `json:"params,omitempty"`
@@ -684,3 +678,12 @@
_, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result)
return err
}
+
+func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error {
+ urlParams := url.Values{"aspect": []string{aspect}}
+ for k, v := range params {
+ urlParams.Set(k, v)
+ }
+ _, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result)
+ return err
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/client_test.go snapd-2.45.1ubuntu0.2/client/client_test.go
--- snapd-2.37.4ubuntu0.1/client/client_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/client_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -26,6 +26,7 @@
"net"
"net/http"
"net/http/httptest"
+ "net/url"
"os"
"path/filepath"
"strings"
@@ -42,16 +43,17 @@
func Test(t *testing.T) { TestingT(t) }
type clientSuite struct {
- cli *client.Client
- req *http.Request
- reqs []*http.Request
- rsp string
- rsps []string
- err error
- doCalls int
- header http.Header
- status int
- restore func()
+ cli *client.Client
+ req *http.Request
+ reqs []*http.Request
+ rsp string
+ rsps []string
+ err error
+ doCalls int
+ header http.Header
+ status int
+ contentLength int64
+ restore func()
}
var _ = Suite(&clientSuite{})
@@ -69,10 +71,11 @@
cs.header = nil
cs.status = 200
cs.doCalls = 0
+ cs.contentLength = 0
dirs.SetRootDir(c.MkDir())
- cs.restore = client.MockDoRetry(time.Millisecond, 10*time.Millisecond)
+ cs.restore = client.MockDoTimings(time.Millisecond, 100*time.Millisecond)
}
func (cs *clientSuite) TearDownTest(c *C) {
@@ -88,9 +91,10 @@
body = cs.rsps[cs.doCalls]
}
rsp := &http.Response{
- Body: ioutil.NopCloser(strings.NewReader(body)),
- Header: cs.header,
- StatusCode: cs.status,
+ Body: ioutil.NopCloser(strings.NewReader(body)),
+ Header: cs.header,
+ StatusCode: cs.status,
+ ContentLength: cs.contentLength,
}
cs.doCalls++
return rsp, cs.err
@@ -99,12 +103,12 @@
func (cs *clientSuite) TestNewPanics(c *C) {
c.Assert(func() {
client.New(&client.Config{BaseURL: ":"})
- }, PanicMatches, `cannot parse server base URL: ":" \(parse :: missing protocol scheme\)`)
+ }, PanicMatches, `cannot parse server base URL: ":" \(parse \"?:\"?: missing protocol scheme\)`)
}
func (cs *clientSuite) TestClientDoReportsErrors(c *C) {
cs.err = errors.New("ouchie")
- err := cs.cli.Do("GET", "/", nil, nil, nil)
+ _, err := cs.cli.Do("GET", "/", nil, nil, nil, client.DoFlags{})
c.Check(err, ErrorMatches, "cannot communicate with server: ouchie")
if cs.doCalls < 2 {
c.Fatalf("do did not retry")
@@ -115,8 +119,25 @@
var v []int
cs.rsp = `[1,2]`
reqBody := ioutil.NopCloser(strings.NewReader(""))
- err := cs.cli.Do("GET", "/this", nil, reqBody, &v)
+ statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, client.DoFlags{})
c.Check(err, IsNil)
+ c.Check(statusCode, Equals, 200)
+ c.Check(v, DeepEquals, []int{1, 2})
+ c.Assert(cs.req, NotNil)
+ c.Assert(cs.req.URL, NotNil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.Body, Equals, reqBody)
+ c.Check(cs.req.URL.Path, Equals, "/this")
+}
+
+func (cs *clientSuite) TestClientUnderstandsStatusCode(c *C) {
+ var v []int
+ cs.status = 202
+ cs.rsp = `[1,2]`
+ reqBody := ioutil.NopCloser(strings.NewReader(""))
+ statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, client.DoFlags{})
+ c.Check(err, IsNil)
+ c.Check(statusCode, Equals, 202)
c.Check(v, DeepEquals, []int{1, 2})
c.Assert(cs.req, NotNil)
c.Assert(cs.req.URL, NotNil)
@@ -130,7 +151,7 @@
defer os.Unsetenv(client.TestAuthFileEnvKey)
var v string
- _ = cs.cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cs.cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{})
c.Assert(cs.req, NotNil)
authorization := cs.req.Header.Get("Authorization")
c.Check(authorization, Equals, "")
@@ -148,7 +169,7 @@
c.Assert(err, IsNil)
var v string
- _ = cs.cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cs.cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{})
authorization := cs.req.Header.Get("Authorization")
c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`)
}
@@ -167,7 +188,7 @@
var v string
cli := client.New(&client.Config{DisableAuth: true})
cli.SetDoer(cs)
- _ = cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{})
authorization := cs.req.Header.Get("Authorization")
c.Check(authorization, Equals, "")
}
@@ -176,13 +197,13 @@
var v string
cli := client.New(&client.Config{Interactive: false})
cli.SetDoer(cs)
- _ = cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{})
interactive := cs.req.Header.Get(client.AllowInteractionHeader)
c.Check(interactive, Equals, "")
cli = client.New(&client.Config{Interactive: true})
cli.SetDoer(cs)
- _ = cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{})
interactive = cs.req.Header.Get(client.AllowInteractionHeader)
c.Check(interactive, Equals, "true")
}
@@ -220,6 +241,8 @@
"on-classic": true,
"build-id": "1234",
"confinement": "strict",
+ "architecture": "TI-99/4A",
+ "virtualization": "MESS",
"sandbox-features": {"backend": ["feature-1", "feature-2"]}}}`
sysInfo, err := cs.cli.SysInfo()
c.Check(err, IsNil)
@@ -235,7 +258,9 @@
SandboxFeatures: map[string][]string{
"backend": {"feature-1", "feature-2"},
},
- BuildID: "1234",
+ BuildID: "1234",
+ Architecture: "TI-99/4A",
+ Virtualization: "MESS",
})
}
@@ -243,14 +268,19 @@
cs.rsp = `{"type": "sync", "result":
{"series": "16",
"version": "2",
- "os-release": {"id": "zyggy", "version-id": "123"}}}`
+ "os-release": {"id": "zyggy", "version-id": "123"},
+ "architecture": "m32",
+ "virtualization": "qemu"
+}}}`
version, err := cs.cli.ServerVersion()
c.Check(err, IsNil)
c.Check(version, DeepEquals, &client.ServerVersion{
- Version: "2",
- Series: "16",
- OSID: "zyggy",
- OSVersionID: "123",
+ Version: "2",
+ Series: "16",
+ OSID: "zyggy",
+ OSVersionID: "123",
+ Architecture: "m32",
+ Virtualization: "qemu",
})
}
@@ -277,7 +307,7 @@
cli := client.New(nil)
si, err := cli.SysInfo()
- c.Check(err, IsNil)
+ c.Assert(err, IsNil)
c.Check(si.Series, Equals, "42")
}
@@ -317,12 +347,14 @@
}
func (cs *clientSuite) TestClientReportsOpError(c *C) {
- cs.rsp = `{"type": "error", "status": "potatoes"}`
+ cs.status = 500
+ cs.rsp = `{"type": "error"}`
_, err := cs.cli.SysInfo()
- c.Check(err, ErrorMatches, `.*server error: "potatoes"`)
+ c.Check(err, ErrorMatches, `.*server error: "Internal Server Error"`)
}
func (cs *clientSuite) TestClientReportsOpErrorStr(c *C) {
+ cs.status = 400
cs.rsp = `{
"result": {},
"status": "Bad Request",
@@ -367,6 +399,7 @@
}
func (cs *clientSuite) TestClientAsyncOpMaintenance(c *C) {
+ cs.status = 202
cs.rsp = `{"type":"async", "status-code": 202, "change": "42", "maintenance": {"kind": "system-restart", "message": "system is restarting"}}`
_, err := cs.cli.Install("foo", nil)
c.Assert(err, IsNil)
@@ -431,105 +464,14 @@
c.Check(client.IsRetryable(&client.Error{Kind: client.ErrorKindChangeConflict}), Equals, true)
}
-func (cs *clientSuite) TestClientCreateUser(c *C) {
- _, err := cs.cli.CreateUser(&client.CreateUserOptions{})
- c.Assert(err, ErrorMatches, "cannot create a user without providing an email")
-
- cs.rsp = `{
- "type": "sync",
- "result": {
- "username": "karl",
- "ssh-keys": ["one", "two"]
- }
- }`
- rsp, err := cs.cli.CreateUser(&client.CreateUserOptions{Email: "one@email.com", Sudoer: true, Known: true})
- c.Assert(cs.req.Method, Equals, "POST")
- c.Assert(cs.req.URL.Path, Equals, "/v2/create-user")
- c.Assert(err, IsNil)
-
- body, err := ioutil.ReadAll(cs.req.Body)
- c.Assert(err, IsNil)
- c.Assert(string(body), Equals, `{"email":"one@email.com","sudoer":true,"known":true}`)
-
- c.Assert(rsp, DeepEquals, &client.CreateUserResult{
- Username: "karl",
- SSHKeys: []string{"one", "two"},
- })
-}
-
-var createUsersTests = []struct {
- options []*client.CreateUserOptions
- bodies []string
- responses []string
- results []*client.CreateUserResult
- error string
-}{{
- options: []*client.CreateUserOptions{{}},
- error: "cannot create user from store details without an email to query for",
-}, {
- options: []*client.CreateUserOptions{{
- Email: "one@example.com",
- Sudoer: true,
- }, {
- Known: true,
- }},
- bodies: []string{
- `{"email":"one@example.com","sudoer":true}`,
- `{"known":true}`,
- },
- responses: []string{
- `{"type": "sync", "result": {"username": "one", "ssh-keys":["a", "b"]}}`,
- `{"type": "sync", "result": [{"username": "two"}, {"username": "three"}]}`,
- },
- results: []*client.CreateUserResult{{
- Username: "one",
- SSHKeys: []string{"a", "b"},
- }, {
- Username: "two",
- }, {
- Username: "three",
- }},
-}}
-
-func (cs *clientSuite) TestClientCreateUsers(c *C) {
- for _, test := range createUsersTests {
- cs.rsps = test.responses
-
- results, err := cs.cli.CreateUsers(test.options)
- if test.error != "" {
- c.Assert(err, ErrorMatches, test.error)
- }
- c.Assert(results, DeepEquals, test.results)
-
- var bodies []string
- for _, req := range cs.reqs {
- c.Assert(req.Method, Equals, "POST")
- c.Assert(req.URL.Path, Equals, "/v2/create-user")
- data, err := ioutil.ReadAll(req.Body)
- c.Assert(err, IsNil)
- bodies = append(bodies, string(data))
- }
-
- c.Assert(bodies, DeepEquals, test.bodies)
- }
-}
-
-func (cs *clientSuite) TestClientJSONError(c *C) {
- cs.rsp = `some non-json error message`
- _, err := cs.cli.SysInfo()
- c.Assert(err, ErrorMatches, `cannot obtain system details: cannot decode "some non-json error message": invalid char.*`)
-}
+func (cs *clientSuite) TestUserAgent(c *C) {
+ cli := client.New(&client.Config{UserAgent: "some-agent/9.87"})
+ cli.SetDoer(cs)
-func (cs *clientSuite) TestUsers(c *C) {
- cs.rsp = `{"type": "sync", "result":
- [{"username": "foo","email":"foo@example.com"},
- {"username": "bar","email":"bar@example.com"}]}`
- users, err := cs.cli.Users()
- c.Check(err, IsNil)
- c.Check(users, DeepEquals, []*client.User{
- {Username: "foo", Email: "foo@example.com"},
- {Username: "bar", Email: "bar@example.com"},
- })
+ var v string
+ _, _ = cli.Do("GET", "/", nil, nil, &v, client.DoFlags{})
+ c.Assert(cs.req, NotNil)
+ c.Check(cs.req.Header.Get("User-Agent"), Equals, "some-agent/9.87")
}
func (cs *clientSuite) TestDebugEnsureStateSoon(c *C) {
@@ -558,3 +500,37 @@
c.Assert(err, IsNil)
c.Check(string(data), DeepEquals, `{"action":"do-something","params":["param1","param2"]}`)
}
+
+func (cs *clientSuite) TestDebugGet(c *C) {
+ cs.rsp = `{"type": "sync", "result":["res1","res2"]}`
+
+ var result []string
+ err := cs.cli.DebugGet("do-something", &result, map[string]string{"foo": "bar"})
+ c.Check(err, IsNil)
+ c.Check(result, DeepEquals, []string{"res1", "res2"})
+ c.Check(cs.reqs, HasLen, 1)
+ c.Check(cs.reqs[0].Method, Equals, "GET")
+ c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug")
+ c.Check(cs.reqs[0].URL.Query(), DeepEquals, url.Values{"aspect": []string{"do-something"}, "foo": []string{"bar"}})
+}
+
+type integrationSuite struct{}
+
+var _ = Suite(&integrationSuite{})
+
+func (cs *integrationSuite) TestClientTimeoutLP1837804(c *C) {
+ restore := client.MockDoTimings(time.Millisecond, 5*time.Millisecond)
+ defer restore()
+
+ testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+ time.Sleep(25 * time.Millisecond)
+ }))
+ defer func() { testServer.Close() }()
+
+ cli := client.New(&client.Config{BaseURL: testServer.URL})
+ _, err := cli.Do("GET", "/", nil, nil, nil, client.DoFlags{})
+ c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`)
+
+ _, err = cli.Do("POST", "/", nil, nil, nil, client.DoFlags{})
+ c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`)
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/cohort.go snapd-2.45.1ubuntu0.2/client/cohort.go
--- snapd-2.37.4ubuntu0.1/client/cohort.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/cohort.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,50 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+
+ "golang.org/x/xerrors"
+)
+
+type CohortAction struct {
+ Action string `json:"action"`
+ Snaps []string `json:"snaps"`
+}
+
+func (client *Client) CreateCohorts(snaps []string) (map[string]string, error) {
+ data, err := json.Marshal(&CohortAction{Action: "create", Snaps: snaps})
+ if err != nil {
+ return nil, fmt.Errorf("cannot request cohorts: %v", err)
+ }
+
+ var cohorts map[string]string
+
+ if _, err := client.doSync("POST", "/v2/cohorts", nil, nil, bytes.NewReader(data), &cohorts); err != nil {
+ fmt := "cannot create cohorts: %w"
+ return nil, xerrors.Errorf(fmt, err)
+ }
+
+ return cohorts, nil
+
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/cohort_test.go snapd-2.45.1ubuntu0.2/client/cohort_test.go
--- snapd-2.37.4ubuntu0.1/client/cohort_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/cohort_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,77 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+
+ "golang.org/x/xerrors"
+
+ "gopkg.in/check.v1"
+)
+
+func (cs *clientSuite) TestClientCreateCohortsEndpoint(c *check.C) {
+ cs.cli.CreateCohorts([]string{"foo", "bar"})
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/cohorts")
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+ var jsonBody map[string]interface{}
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, check.IsNil)
+ c.Check(jsonBody, check.DeepEquals, map[string]interface{}{
+ "action": "create",
+ "snaps": []interface{}{"foo", "bar"},
+ })
+}
+
+func (cs *clientSuite) TestClientCreateCohorts(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {"foo": "xyzzy", "bar": "what-what"}
+ }`
+ cohorts, err := cs.cli.CreateCohorts([]string{"foo", "bar"})
+ c.Assert(err, check.IsNil)
+ c.Check(cohorts, check.DeepEquals, map[string]string{
+ "foo": "xyzzy",
+ "bar": "what-what",
+ })
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+ var jsonBody map[string]interface{}
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, check.IsNil)
+ c.Check(jsonBody, check.DeepEquals, map[string]interface{}{
+ "action": "create",
+ "snaps": []interface{}{"foo", "bar"},
+ })
+}
+
+func (cs *clientSuite) TestClientCreateCohortsErrIsWrapped(c *check.C) {
+ cs.err = errors.New("boom")
+ _, err := cs.cli.CreateCohorts([]string{"foo", "bar"})
+ var e xerrors.Wrapper
+ c.Assert(err, check.Implements, &e)
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/conf_test.go snapd-2.45.1ubuntu0.2/client/conf_test.go
--- snapd-2.37.4ubuntu0.1/client/conf_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/conf_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -46,6 +46,7 @@
}
func (cs *clientSuite) TestClientSetConf(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
diff -Nru snapd-2.37.4ubuntu0.1/client/connections.go snapd-2.45.1ubuntu0.2/client/connections.go
--- snapd-2.37.4ubuntu0.1/client/connections.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/connections.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,81 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package client
+
+import (
+ "net/url"
+)
+
+// Connection describes a connection between a plug and a slot.
+type Connection struct {
+ Slot SlotRef `json:"slot"`
+ Plug PlugRef `json:"plug"`
+ Interface string `json:"interface"`
+ // Manual is set for connections that were established manually.
+ Manual bool `json:"manual"`
+ // Gadget is set for connections that were enabled by the gadget snap.
+ Gadget bool `json:"gadget"`
+ // SlotAttrs is the list of attributes of the slot side of the connection.
+ SlotAttrs map[string]interface{} `json:"slot-attrs,omitempty"`
+ // PlugAttrs is the list of attributes of the plug side of the connection.
+ PlugAttrs map[string]interface{} `json:"plug-attrs,omitempty"`
+}
+
+// Connections contains information about connections, as well as related plugs
+// and slots.
+type Connections struct {
+ // Established is the list of connections that are currently present.
+ Established []Connection `json:"established"`
+ // Undersired is a list of connections that are manually denied.
+ Undesired []Connection `json:"undesired"`
+ Plugs []Plug `json:"plugs"`
+ Slots []Slot `json:"slots"`
+}
+
+// ConnectionOptions contains criteria for selecting matching connections, plugs
+// and slots.
+type ConnectionOptions struct {
+ // Snap selects connections with the snap on one of the sides, as well
+ // as plugs and slots of a given snap.
+ Snap string
+ // Interface selects connections, plugs or slots using given interface.
+ Interface string
+ // All when true, selects established and undesired connections as well
+ // as all disconnected plugs and slots.
+ All bool
+}
+
+// Connections returns matching plugs, slots and their connections. Unless
+// specified by matching options, returns established connections.
+func (client *Client) Connections(opts *ConnectionOptions) (Connections, error) {
+ var conns Connections
+ query := url.Values{}
+ if opts != nil && opts.Snap != "" {
+ query.Set("snap", opts.Snap)
+ }
+ if opts != nil && opts.Interface != "" {
+ query.Set("interface", opts.Interface)
+ }
+ if opts != nil && opts.All {
+ query.Set("select", "all")
+ }
+ _, err := client.doSync("GET", "/v2/connections", query, nil, nil, &conns)
+ return conns, err
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/connections_test.go snapd-2.45.1ubuntu0.2/client/connections_test.go
--- snapd-2.37.4ubuntu0.1/client/connections_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/connections_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,270 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 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
.
+ *
+ */
+
+package client_test
+
+import (
+ "net/url"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+)
+
+func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) {
+ _, _ = cs.cli.Connections(nil)
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+}
+
+func (cs *clientSuite) TestClientConnectionsDefault(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "established": [
+ {
+ "slot": {"snap": "keyboard-lights", "slot": "capslock-led"},
+ "plug": {"snap": "canonical-pi2", "plug": "pin-13"},
+ "interface": "bool-file",
+ "gadget": true
+ }
+ ],
+ "plugs": [
+ {
+ "snap": "canonical-pi2",
+ "plug": "pin-13",
+ "interface": "bool-file",
+ "label": "Pin 13",
+ "connections": [
+ {"snap": "keyboard-lights", "slot": "capslock-led"}
+ ]
+ }
+ ],
+ "slots": [
+ {
+ "snap": "keyboard-lights",
+ "slot": "capslock-led",
+ "interface": "bool-file",
+ "label": "Capslock indicator LED",
+ "connections": [
+ {"snap": "canonical-pi2", "plug": "pin-13"}
+ ]
+ }
+ ]
+ }
+ }`
+ conns, err := cs.cli.Connections(nil)
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(conns, check.DeepEquals, client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-13"},
+ Slot: client.SlotRef{Snap: "keyboard-lights", Name: "capslock-led"},
+ Interface: "bool-file",
+ Gadget: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.SlotRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "bool-file",
+ Label: "Capslock indicator LED",
+ Connections: []client.PlugRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ },
+ },
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestClientConnectionsAll(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "established": [
+ {
+ "slot": {"snap": "keyboard-lights", "slot": "capslock-led"},
+ "plug": {"snap": "canonical-pi2", "plug": "pin-13"},
+ "interface": "bool-file",
+ "gadget": true
+ }
+ ],
+ "undesired": [
+ {
+ "slot": {"snap": "keyboard-lights", "slot": "numlock-led"},
+ "plug": {"snap": "canonical-pi2", "plug": "pin-14"},
+ "interface": "bool-file",
+ "gadget": true,
+ "manual": true
+ }
+ ],
+ "plugs": [
+ {
+ "snap": "canonical-pi2",
+ "plug": "pin-13",
+ "interface": "bool-file",
+ "label": "Pin 13",
+ "connections": [
+ {"snap": "keyboard-lights", "slot": "capslock-led"}
+ ]
+ },
+ {
+ "snap": "canonical-pi2",
+ "plug": "pin-14",
+ "interface": "bool-file",
+ "label": "Pin 14"
+ }
+ ],
+ "slots": [
+ {
+ "snap": "keyboard-lights",
+ "slot": "capslock-led",
+ "interface": "bool-file",
+ "label": "Capslock indicator LED",
+ "connections": [
+ {"snap": "canonical-pi2", "plug": "pin-13"}
+ ]
+ },
+ {
+ "snap": "keyboard-lights",
+ "slot": "numlock-led",
+ "interface": "bool-file",
+ "label": "Numlock LED"
+ }
+ ]
+ }
+ }`
+ conns, err := cs.cli.Connections(&client.ConnectionOptions{All: true})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "select=all")
+ c.Check(conns, check.DeepEquals, client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-13"},
+ Slot: client.SlotRef{Snap: "keyboard-lights", Name: "capslock-led"},
+ Interface: "bool-file",
+ Gadget: true,
+ },
+ },
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-14"},
+ Slot: client.SlotRef{Snap: "keyboard-lights", Name: "numlock-led"},
+ Interface: "bool-file",
+ Gadget: true,
+ Manual: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ Interface: "bool-file",
+ Label: "Pin 13",
+ Connections: []client.SlotRef{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ },
+ },
+ },
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-14",
+ Interface: "bool-file",
+ Label: "Pin 14",
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "bool-file",
+ Label: "Capslock indicator LED",
+ Connections: []client.PlugRef{
+ {
+ Snap: "canonical-pi2",
+ Name: "pin-13",
+ },
+ },
+ },
+ {
+ Snap: "keyboard-lights",
+ Name: "numlock-led",
+ Interface: "bool-file",
+ Label: "Numlock LED",
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestClientConnectionsFilter(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "established": [],
+ "plugs": [],
+ "slots": []
+ }
+ }`
+
+ _, err := cs.cli.Connections(&client.ConnectionOptions{All: true})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "select=all")
+
+ _, err = cs.cli.Connections(&client.ConnectionOptions{Snap: "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "snap=foo")
+
+ _, err = cs.cli.Connections(&client.ConnectionOptions{Interface: "test"})
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/connections")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "interface=test")
+
+ _, err = cs.cli.Connections(&client.ConnectionOptions{All: true, Snap: "foo", Interface: "test"})
+ c.Assert(err, check.IsNil)
+ query := cs.req.URL.Query()
+ c.Check(query, check.DeepEquals, url.Values{
+ "select": []string{"all"},
+ "interface": []string{"test"},
+ "snap": []string{"foo"},
+ })
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/export_test.go snapd-2.45.1ubuntu0.2/client/export_test.go
--- snapd-2.37.4ubuntu0.1/client/export_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/export_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -30,9 +30,11 @@
client.doer = d
}
+type DoFlags = doFlags
+
// Do does do.
-func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}) error {
- return client.do(method, path, query, nil, body, v)
+func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}, flags DoFlags) (statusCode int, err error) {
+ return client.do(method, path, query, nil, body, v, flags)
}
// expose parseError for testing
@@ -49,3 +51,5 @@
err = json.NewDecoder(body).Decode(&act)
return
}
+
+type DownloadAction = downloadAction
diff -Nru snapd-2.37.4ubuntu0.1/client/icons.go snapd-2.45.1ubuntu0.2/client/icons.go
--- snapd-2.37.4ubuntu0.1/client/icons.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/icons.go 2020-06-05 13:13:49.000000000 +0000
@@ -20,9 +20,12 @@
package client
import (
+ "context"
"fmt"
"io/ioutil"
"regexp"
+
+ "golang.org/x/xerrors"
)
// Icon represents the icon of an installed snap
@@ -37,9 +40,12 @@
func (c *Client) Icon(pkgID string) (*Icon, error) {
const errPrefix = "cannot retrieve icon"
- response, err := c.raw("GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil)
+ ctx, cancel := context.WithTimeout(context.Background(), doTimeout)
+ defer cancel()
+ response, err := c.raw(ctx, "GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil)
if err != nil {
- return nil, fmt.Errorf("%s: failed to communicate with server: %s", errPrefix, err)
+ fmt := "%s: failed to communicate with server: %w"
+ return nil, xerrors.Errorf(fmt, errPrefix, err)
}
defer response.Body.Close()
diff -Nru snapd-2.37.4ubuntu0.1/client/icons_test.go snapd-2.45.1ubuntu0.2/client/icons_test.go
--- snapd-2.37.4ubuntu0.1/client/icons_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/icons_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -24,6 +24,8 @@
"fmt"
"net/http"
+ "golang.org/x/xerrors"
+
. "gopkg.in/check.v1"
)
@@ -63,3 +65,10 @@
c.Assert(icon.Filename, Equals, "myicon.png")
c.Assert(icon.Content, DeepEquals, []byte("pixels"))
}
+
+func (cs *clientSuite) TestClientIconErrIsWrapped(c *C) {
+ cs.err = errors.New("boom")
+ _, err := cs.cli.Icon("something")
+ var e xerrors.Wrapper
+ c.Assert(err, Implements, &e)
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/interfaces.go snapd-2.45.1ubuntu0.2/client/interfaces.go
--- snapd-2.37.4ubuntu0.1/client/interfaces.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/interfaces.go 2020-06-05 13:13:49.000000000 +0000
@@ -60,12 +60,6 @@
Name string `json:"slot"`
}
-// Connections contains information about all plugs, slots and their connections
-type Connections struct {
- Plugs []Plug `json:"plugs"`
- Slots []Slot `json:"slots"`
-}
-
// Interface holds information about a given interface and its instances.
type Interface struct {
Name string `json:"name,omitempty"`
@@ -78,17 +72,11 @@
// InterfaceAction represents an action performed on the interface system.
type InterfaceAction struct {
Action string `json:"action"`
+ Forget bool `json:"forget,omitempty"`
Plugs []Plug `json:"plugs,omitempty"`
Slots []Slot `json:"slots,omitempty"`
}
-// Connections returns all plugs, slots and their connections.
-func (client *Client) Connections() (Connections, error) {
- var conns Connections
- _, err := client.doSync("GET", "/v2/interfaces", nil, nil, nil, &conns)
- return conns, err
-}
-
// InterfaceOptions represents opt-in elements include in responses.
type InterfaceOptions struct {
Names []string
@@ -98,6 +86,11 @@
Connected bool
}
+// DisconnectOptions represents extra options for disconnect op
+type DisconnectOptions struct {
+ Forget bool
+}
+
func (client *Client) Interfaces(opts *InterfaceOptions) ([]*Interface, error) {
query := url.Values{}
if opts != nil && len(opts.Names) > 0 {
@@ -146,9 +139,10 @@
}
// Disconnect breaks the connection between a plug and a slot.
-func (client *Client) Disconnect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) {
+func (client *Client) Disconnect(plugSnapName, plugName, slotSnapName, slotName string, opts *DisconnectOptions) (changeID string, err error) {
return client.performInterfaceAction(&InterfaceAction{
Action: "disconnect",
+ Forget: opts != nil && opts.Forget,
Plugs: []Plug{{Snap: plugSnapName, Name: plugName}},
Slots: []Slot{{Snap: slotSnapName, Name: slotName}},
})
diff -Nru snapd-2.37.4ubuntu0.1/client/interfaces_test.go snapd-2.45.1ubuntu0.2/client/interfaces_test.go
--- snapd-2.37.4ubuntu0.1/client/interfaces_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/interfaces_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -156,74 +156,6 @@
})
}
-func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) {
- _, _ = cs.cli.Connections()
- c.Check(cs.req.Method, check.Equals, "GET")
- c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces")
-}
-
-func (cs *clientSuite) TestClientConnections(c *check.C) {
- cs.rsp = `{
- "type": "sync",
- "result": {
- "plugs": [
- {
- "snap": "canonical-pi2",
- "plug": "pin-13",
- "interface": "bool-file",
- "label": "Pin 13",
- "connections": [
- {"snap": "keyboard-lights", "slot": "capslock-led"}
- ]
- }
- ],
- "slots": [
- {
- "snap": "keyboard-lights",
- "slot": "capslock-led",
- "interface": "bool-file",
- "label": "Capslock indicator LED",
- "connections": [
- {"snap": "canonical-pi2", "plug": "pin-13"}
- ]
- }
- ]
- }
- }`
- conns, err := cs.cli.Connections()
- c.Assert(err, check.IsNil)
- c.Check(conns, check.DeepEquals, client.Connections{
- Plugs: []client.Plug{
- {
- Snap: "canonical-pi2",
- Name: "pin-13",
- Interface: "bool-file",
- Label: "Pin 13",
- Connections: []client.SlotRef{
- {
- Snap: "keyboard-lights",
- Name: "capslock-led",
- },
- },
- },
- },
- Slots: []client.Slot{
- {
- Snap: "keyboard-lights",
- Name: "capslock-led",
- Interface: "bool-file",
- Label: "Capslock indicator LED",
- Connections: []client.PlugRef{
- {
- Snap: "canonical-pi2",
- Name: "pin-13",
- },
- },
- },
- },
- })
-}
-
func (cs *clientSuite) TestClientConnectCallsEndpoint(c *check.C) {
cs.cli.Connect("producer", "plug", "consumer", "slot")
c.Check(cs.req.Method, check.Equals, "POST")
@@ -231,6 +163,7 @@
}
func (cs *clientSuite) TestClientConnect(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -262,19 +195,54 @@
}
func (cs *clientSuite) TestClientDisconnectCallsEndpoint(c *check.C) {
- cs.cli.Disconnect("producer", "plug", "consumer", "slot")
+ cs.cli.Disconnect("producer", "plug", "consumer", "slot", nil)
c.Check(cs.req.Method, check.Equals, "POST")
c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces")
}
func (cs *clientSuite) TestClientDisconnect(c *check.C) {
+ cs.status = 202
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": { },
+ "change": "42"
+ }`
+ opts := &client.DisconnectOptions{Forget: false}
+ id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot", opts)
+ c.Assert(err, check.IsNil)
+ c.Check(id, check.Equals, "42")
+ var body map[string]interface{}
+ decoder := json.NewDecoder(cs.req.Body)
+ err = decoder.Decode(&body)
+ c.Check(err, check.IsNil)
+ c.Check(body, check.DeepEquals, map[string]interface{}{
+ "action": "disconnect",
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "slot": "slot",
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestClientDisconnectForget(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
"result": { },
"change": "42"
}`
- id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot")
+ opts := &client.DisconnectOptions{Forget: true}
+ id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot", opts)
c.Assert(err, check.IsNil)
c.Check(id, check.Equals, "42")
var body map[string]interface{}
@@ -283,6 +251,7 @@
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"action": "disconnect",
+ "forget": true,
"plugs": []interface{}{
map[string]interface{}{
"snap": "producer",
diff -Nru snapd-2.37.4ubuntu0.1/client/model.go snapd-2.45.1ubuntu0.2/client/model.go
--- snapd-2.37.4ubuntu0.1/client/model.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/model.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,105 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/url"
+
+ "golang.org/x/xerrors"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type remodelData struct {
+ NewModel string `json:"new-model"`
+}
+
+// Remodel tries to remodel the system with the given assertion data
+func (client *Client) Remodel(b []byte) (changeID string, err error) {
+ data, err := json.Marshal(&remodelData{
+ NewModel: string(b),
+ })
+ if err != nil {
+ return "", fmt.Errorf("cannot marshal remodel data: %v", err)
+ }
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ }
+
+ return client.doAsync("POST", "/v2/model", nil, headers, bytes.NewReader(data))
+}
+
+// CurrentModelAssertion returns the current model assertion
+func (client *Client) CurrentModelAssertion() (*asserts.Model, error) {
+ assert, err := currentAssertion(client, "/v2/model")
+ if err != nil {
+ return nil, err
+ }
+ modelAssert, ok := assert.(*asserts.Model)
+ if !ok {
+ return nil, fmt.Errorf("unexpected assertion type (%s) returned", assert.Type().Name)
+ }
+ return modelAssert, nil
+}
+
+// CurrentSerialAssertion returns the current serial assertion
+func (client *Client) CurrentSerialAssertion() (*asserts.Serial, error) {
+ assert, err := currentAssertion(client, "/v2/model/serial")
+ if err != nil {
+ return nil, err
+ }
+ serialAssert, ok := assert.(*asserts.Serial)
+ if !ok {
+ return nil, fmt.Errorf("unexpected assertion type (%s) returned", assert.Type().Name)
+ }
+ return serialAssert, nil
+}
+
+// helper function for getting assertions from the daemon via a REST path
+func currentAssertion(client *Client, path string) (asserts.Assertion, error) {
+ q := url.Values{}
+
+ ctx, cancel := context.WithTimeout(context.Background(), doTimeout)
+ defer cancel()
+ response, err := client.raw(ctx, "GET", path, q, nil, nil)
+ if err != nil {
+ fmt := "failed to query current assertion: %w"
+ return nil, xerrors.Errorf(fmt, err)
+ }
+ defer response.Body.Close()
+ if response.StatusCode != 200 {
+ return nil, parseError(response)
+ }
+
+ dec := asserts.NewDecoder(response.Body)
+
+ // only decode a single assertion - we can't ever get more than a single
+ // assertion through these endpoints by design
+ assert, err := dec.Decode()
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode assertions: %v", err)
+ }
+
+ return assert, nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/model_test.go snapd-2.45.1ubuntu0.2/client/model_test.go
--- snapd-2.37.4ubuntu0.1/client/model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/model_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,176 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+ "net/http"
+
+ "golang.org/x/xerrors"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+const happyModelAssertionResponse = `type: model
+authority-id: mememe
+series: 16
+brand-id: mememe
+model: test-model
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+required-snaps:
+ - core
+ - hello-world
+timestamp: 2017-07-27T00:00:00.0Z
+sign-key-sha3-384: 8B3Wmemeu3H6i4dEV4Q85Q4gIUCHIBCNMHq49e085QeLGHi7v27l3Cqmemer4__t
+
+AcLBcwQAAQoAHRYhBMbX+t6MbKGH5C3nnLZW7+q0g6ELBQJdTdwTAAoJELZW7+q0g6ELEvgQAI3j
+jXTqR6kKOqvw94pArwdMDUaZ++tebASAZgso8ejrW2DQGWSc0Q7SQICIR8bvHxqS1GtupQswOzwS
+U8hjDTv7WEchH1jylyTj/1W1GernmitTKycecRlEkSOE+EpuqBFgTtj6PdA1Fj3CiCRi1rLMhgF2
+luCOitBLaP+E8P3fuATsLqqDLYzt1VY4Y14MU75hMn+CxAQdnOZTI+NzGMasPsldmOYCPNaN/b3N
+6/fDLU47RtNlMJ3K0Tz8kj0bqRbegKlD0RdNbAgo9iZwNmrr5E9WCu9f/0rUor/NIxO77H2ExIll
+zhmsZ7E6qlxvAgBmzKgAXrn68gGrBkIb0eXKiCaKy/i2ApvjVZ9HkOzA6Ldd+SwNJv/iA8rdiMsq
+p2BfKV5f3ju5b6+WktHxAakJ8iqQmj9Yh7piHjsOAUf1PEJd2s2nqQ+pEEn1F0B23gVCY/Fa9YRQ
+iKtWVeL3rBw4dSAaK9rpTMqlNcr+yrdXfTK5YzkCC6RU4yzc5MW0hKeseeSiEDSaRYxvftjFfVNa
+ZaVXKg8Lu+cHtCJDeYXEkPIDQzXswdBO1M8Mb9D0mYxQwHxwvsWv1DByB+Otq08EYgPh4kyHo7ag
+85yK2e/NQ/fxSwQJMhBF74jM1z9arq6RMiE/KOleFAOraKn2hcROKnEeinABW+sOn6vNuMVv
+`
+
+// note: this serial assertion was generated by adding print statements to the
+// test in api_model_test.go that generate a fake serial assertion
+const happySerialAssertionResponse = `type: serial
+authority-id: my-brand
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+device-key:
+ AcZrBFaFwYABAvCgEOrrLA6FKcreHxCcOoTgBUZ+IRG7Nb8tzmEAklaQPGpv7skapUjwD1luE2go
+ mTcoTssVHrfLpBoSDV1aBs44rg3NK40ZKPJP7d2zkds1GxUo1Ea5vfet3SJ4h3aRABEBAAE=
+device-key-sha3-384: iqLo9doLzK8De9925UrdUyuvPbBad72OTWVE9YJXqd6nz9dKvwJ_lHP5bVxrl3VO
+timestamp: 2019-08-26T16:34:21-05:00
+sign-key-sha3-384: anCEGC2NYq7DzDEi6y7OafQCVeVLS90XlLt9PNjrRl9sim5rmRHDDNFNO7ODcWQW
+
+AcJwBAABCgAGBQJdZFBdAADCLALwR6Sy24wm9PffwbvUhOEXneyY3BnxKC0+NgdHu1gU8go9vEP1
+i+Flh5uoS70+MBIO+nmF8T+9JWIx2QWFDDxvcuFosnIhvUajCEQohauys5FMz/H/WvB0vrbTBpvK
+eg==`
+
+const noModelAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no model assertion yet",
+ "kind": "assertion-not-found",
+ "value": "model"
+ }
+}`
+
+const noSerialAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no serial assertion yet",
+ "kind": "assertion-not-found",
+ "value": "serial"
+ }
+}`
+
+func (cs *clientSuite) TestClientRemodelEndpoint(c *C) {
+ cs.cli.Remodel([]byte(`{"new-model": "some-model"}`))
+ c.Check(cs.req.Method, Equals, "POST")
+ c.Check(cs.req.URL.Path, Equals, "/v2/model")
+}
+
+func (cs *clientSuite) TestClientRemodel(c *C) {
+ cs.status = 202
+ cs.rsp = `{
+ "type": "async",
+ "status-code": 202,
+ "result": {},
+ "change": "d728"
+ }`
+ remodelJsonData := []byte(`{"new-model": "some-model"}`)
+ id, err := cs.cli.Remodel(remodelJsonData)
+ c.Assert(err, IsNil)
+ c.Check(id, Equals, "d728")
+ c.Assert(cs.req.Header.Get("Content-Type"), Equals, "application/json")
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, IsNil)
+ jsonBody := make(map[string]string)
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, IsNil)
+ c.Check(jsonBody, HasLen, 1)
+ c.Check(jsonBody["new-model"], Equals, string(remodelJsonData))
+}
+
+func (cs *clientSuite) TestClientGetModelHappy(c *C) {
+ cs.status = 200
+ cs.rsp = happyModelAssertionResponse
+ modelAssertion, err := cs.cli.CurrentModelAssertion()
+ c.Assert(err, IsNil)
+ expectedAssert, err := asserts.Decode([]byte(happyModelAssertionResponse))
+ c.Assert(err, IsNil)
+ c.Assert(modelAssertion, DeepEquals, expectedAssert)
+}
+
+func (cs *clientSuite) TestClientGetModelNoModel(c *C) {
+ cs.status = 404
+ cs.rsp = noModelAssertionYetResponse
+ cs.header = http.Header{}
+ cs.header.Add("Content-Type", "application/json")
+ _, err := cs.cli.CurrentModelAssertion()
+ c.Assert(err, ErrorMatches, "no model assertion yet")
+}
+
+func (cs *clientSuite) TestClientGetModelNoSerial(c *C) {
+ cs.status = 404
+ cs.rsp = noSerialAssertionYetResponse
+ cs.header = http.Header{}
+ cs.header.Add("Content-Type", "application/json")
+ _, err := cs.cli.CurrentSerialAssertion()
+ c.Assert(err, ErrorMatches, "no serial assertion yet")
+}
+
+func (cs *clientSuite) TestClientGetSerialHappy(c *C) {
+ cs.status = 200
+ cs.rsp = happySerialAssertionResponse
+ serialAssertion, err := cs.cli.CurrentSerialAssertion()
+ c.Assert(err, IsNil)
+ expectedAssert, err := asserts.Decode([]byte(happySerialAssertionResponse))
+ c.Assert(err, IsNil)
+ c.Assert(serialAssertion, DeepEquals, expectedAssert)
+}
+
+func (cs *clientSuite) TestClientCurrentModelAssertionErrIsWrapped(c *C) {
+ cs.err = errors.New("boom")
+ _, err := cs.cli.CurrentModelAssertion()
+ var e xerrors.Wrapper
+ c.Assert(err, Implements, &e)
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/packages.go snapd-2.45.1ubuntu0.2/client/packages.go
--- snapd-2.37.4ubuntu0.1/client/packages.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/packages.go 2020-06-05 13:13:49.000000000 +0000
@@ -27,6 +27,8 @@
"strings"
"time"
+ "golang.org/x/xerrors"
+
"github.com/snapcore/snapd/snap"
)
@@ -42,6 +44,7 @@
InstallDate time.Time `json:"install-date,omitempty"`
Name string `json:"name"`
Publisher *snap.StoreAccount `json:"publisher,omitempty"`
+ StoreURL string `json:"store-url,omitempty"`
// Developer is also the publisher's username for historic reasons.
Developer string `json:"developer"`
Status string `json:"status"`
@@ -63,6 +66,8 @@
License string `json:"license,omitempty"`
CommonIDs []string `json:"common-ids,omitempty"`
MountedFrom string `json:"mounted-from,omitempty"`
+ CohortKey string `json:"cohort-key,omitempty"`
+ Website string `json:"website,omitempty"`
Prices map[string]float64 `json:"prices,omitempty"`
Screenshots []snap.ScreenshotInfo `json:"screenshots,omitempty"`
@@ -73,6 +78,16 @@
// The ordered list of tracks that contains channels
Tracks []string `json:"tracks,omitempty"`
+
+ Health *SnapHealth `json:"health,omitempty"`
+}
+
+type SnapHealth struct {
+ Revision snap.Revision `json:"revision"`
+ Timestamp time.Time `json:"timestamp"`
+ Status string `json:"status"`
+ Message string `json:"message,omitempty"`
+ Code string `json:"code,omitempty"`
}
func (s *Snap) MarshalJSON() ([]byte, error) {
@@ -117,12 +132,17 @@
// - Private: return snaps that are private
// - Query: only return snaps that match the query string
type FindOptions struct {
- Refresh bool
- Private bool
- Prefix bool
- Query string
+ // Query is a term to search by or a prefix (if Prefix is true)
+ Query string
+ Prefix bool
+
+ CommonID string
+
Section string
+ Private bool
Scope string
+
+ Refresh bool
}
var ErrNoSnapsInstalled = errors.New("no snaps installed")
@@ -163,7 +183,8 @@
var sections []string
_, err := client.doSync("GET", "/v2/sections", nil, nil, nil, §ions)
if err != nil {
- return nil, fmt.Errorf("cannot get snap sections: %s", err)
+ fmt := "cannot get snap sections: %w"
+ return nil, xerrors.Errorf(fmt, err)
}
return sections, nil
}
@@ -179,8 +200,14 @@
if opts.Prefix {
q.Set("name", opts.Query+"*")
} else {
- q.Set("q", opts.Query)
+ if opts.CommonID != "" {
+ q.Set("common-id", opts.CommonID)
+ }
+ if opts.Query != "" {
+ q.Set("q", opts.Query)
+ }
}
+
switch {
case opts.Refresh && opts.Private:
return nil, nil, fmt.Errorf("cannot specify refresh and private together")
@@ -205,7 +232,8 @@
snaps, ri, err := client.snapsFromPath("/v2/find", q)
if err != nil {
- return nil, nil, fmt.Errorf("cannot find snap %q: %s", name, err)
+ fmt := "cannot find snap %q: %w"
+ return nil, nil, xerrors.Errorf(fmt, name, err)
}
if len(snaps) == 0 {
@@ -222,7 +250,8 @@
return nil, nil, e
}
if err != nil {
- return nil, nil, fmt.Errorf("cannot list snaps: %s", err)
+ fmt := "cannot list snaps: %w"
+ return nil, nil, xerrors.Errorf(fmt, err)
}
return snaps, ri, nil
}
@@ -234,7 +263,8 @@
path := fmt.Sprintf("/v2/snaps/%s", name)
ri, err := client.doSync("GET", path, nil, nil, nil, &snap)
if err != nil {
- return nil, nil, fmt.Errorf("cannot retrieve snap %q: %s", name, err)
+ fmt := "cannot retrieve snap %q: %w"
+ return nil, nil, xerrors.Errorf(fmt, name, err)
}
return snap, ri, nil
}
diff -Nru snapd-2.37.4ubuntu0.1/client/packages_test.go snapd-2.45.1ubuntu0.2/client/packages_test.go
--- snapd-2.37.4ubuntu0.1/client/packages_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/packages_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -21,10 +21,14 @@
import (
"encoding/json"
+ "errors"
"fmt"
+ "io/ioutil"
"net/url"
+ "os"
"time"
+ "golang.org/x/xerrors"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/client"
@@ -45,7 +49,7 @@
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "select": []string{"refresh"},
+ "select": []string{"refresh"},
})
}
@@ -57,7 +61,7 @@
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "section": []string{"mysection"}, "select": []string{"refresh"},
+ "section": []string{"mysection"}, "select": []string{"refresh"},
})
}
@@ -68,7 +72,7 @@
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "section": []string{"mysection"},
+ "section": []string{"mysection"},
})
}
@@ -89,7 +93,7 @@
c.Check(cs.req.Method, check.Equals, "GET")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{
- "q": []string{""}, "scope": []string{"mouthwash"},
+ "scope": []string{"mouthwash"},
})
}
@@ -115,6 +119,10 @@
}
func (cs *clientSuite) TestClientSnaps(c *check.C) {
+ healthTimestamp, err := time.Parse(time.RFC3339Nano, "2019-05-13T16:27:01.475851677+01:00")
+ c.Assert(err, check.IsNil)
+
+ // TODO: update this JSON as it's ancient
cs.rsp = `{
"type": "sync",
"result": [{
@@ -123,6 +131,11 @@
"summary": "salutation snap",
"description": "hello-world",
"download-size": 22212,
+ "health": {
+ "revision": "29",
+ "timestamp": "2019-05-13T16:27:01.475851677+01:00",
+ "status": "okay"
+ },
"icon": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
"installed-size": -1,
"license": "GPL-3.0",
@@ -147,12 +160,17 @@
applications, err := cs.cli.List(nil, nil)
c.Check(err, check.IsNil)
c.Check(applications, check.DeepEquals, []*client.Snap{{
- ID: "funky-snap-id",
- Title: "Title",
- Summary: "salutation snap",
- Description: "hello-world",
- DownloadSize: 22212,
- Icon: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ ID: "funky-snap-id",
+ Title: "Title",
+ Summary: "salutation snap",
+ Description: "hello-world",
+ DownloadSize: 22212,
+ Icon: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ Health: &client.SnapHealth{
+ Revision: snap.R(29),
+ Timestamp: healthTimestamp,
+ Status: "okay",
+ },
InstalledSize: -1,
License: "GPL-3.0",
Name: "hello-world",
@@ -185,6 +203,12 @@
c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo%2A") // 2A is `*`
}
+func (cs *clientSuite) TestClientFindCommonID(c *check.C) {
+ _, _, _ = cs.cli.Find(&client.FindOptions{CommonID: "org.kde.ktuberling.desktop"})
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
+ c.Check(cs.req.URL.RawQuery, check.Equals, "common-id=org.kde.ktuberling.desktop")
+}
+
func (cs *clientSuite) TestClientFindOne(c *check.C) {
_, _, _ = cs.cli.FindOne("foo")
c.Check(cs.req.URL.Path, check.Equals, "/v2/find")
@@ -198,6 +222,7 @@
func (cs *clientSuite) TestClientSnap(c *check.C) {
// example data obtained via
// printf "GET /v2/find?name=test-snapd-tools HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket|grep '{'|python3 -m json.tool
+ // XXX: update / sync with what daemon is actually putting out
cs.rsp = `{
"type": "sync",
"result": {
@@ -236,7 +261,10 @@
{"type": "screenshot", "url":"http://example.com/shot1.png", "width":640, "height":480},
{"type": "screenshot", "url":"http://example.com/shot2.png"}
],
- "common-ids": ["org.funky.snap"]
+ "cohort-key": "some-long-cohort-key",
+ "website": "http://example.com/funky",
+ "common-ids": ["org.funky.snap"],
+ "store-url": "https://snapcraft.io/chatroom"
}
}`
pkg, _, err := cs.cli.Snap(pkgName)
@@ -279,6 +307,9 @@
{Type: "screenshot", URL: "http://example.com/shot2.png"},
},
CommonIDs: []string{"org.funky.snap"},
+ CohortKey: "some-long-cohort-key",
+ Website: "http://example.com/funky",
+ StoreURL: "https://snapcraft.io/chatroom",
})
}
@@ -333,3 +364,44 @@
c.Check(app.Name, check.Equals, "hello")
c.Check(app.IsService(), check.Equals, true)
}
+
+func (cs *clientSuite) TestClientSectionsErrIsWrapped(c *check.C) {
+ cs.err = errors.New("boom")
+ _, err := cs.cli.Sections()
+ var e xerrors.Wrapper
+ c.Assert(err, check.Implements, &e)
+}
+
+func (cs *clientSuite) TestClientFindOneErrIsWrapped(c *check.C) {
+ cs.err = errors.New("boom")
+ _, _, err := cs.cli.FindOne("snap")
+ var e xerrors.Wrapper
+ c.Assert(err, check.Implements, &e)
+}
+
+func (cs *clientSuite) TestClientSnapErrIsWrapped(c *check.C) {
+ // setting cs.err will trigger a "client.ClientError"
+ cs.err = errors.New("boom")
+ _, _, err := cs.cli.Snap("snap")
+ var e xerrors.Wrapper
+ c.Assert(err, check.Implements, &e)
+}
+
+func (cs *clientSuite) TestClientFindFromPathErrIsWrapped(c *check.C) {
+ var e client.AuthorizationError
+
+ // this will trigger a "client.AuthorizationError"
+ err := ioutil.WriteFile(client.TestStoreAuthFilename(os.Getenv("HOME")), []byte("rubbish"), 0644)
+ c.Assert(err, check.IsNil)
+
+ // check that all the functions that use snapsFromPath() get a
+ // wrapped error
+ _, _, err = cs.cli.FindOne("snap")
+ c.Assert(xerrors.As(err, &e), check.Equals, true)
+
+ _, _, err = cs.cli.Find(nil)
+ c.Assert(xerrors.As(err, &e), check.Equals, true)
+
+ _, err = cs.cli.List([]string{"snap"}, nil)
+ c.Assert(xerrors.As(err, &e), check.Equals, true)
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/snap_op.go snapd-2.45.1ubuntu0.2/client/snap_op.go
--- snapd-2.37.4ubuntu0.1/client/snap_op.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/snap_op.go 2020-06-05 13:13:49.000000000 +0000
@@ -21,6 +21,7 @@
import (
"bytes"
+ "context"
"encoding/json"
"fmt"
"io"
@@ -30,15 +31,18 @@
)
type SnapOptions struct {
- Amend bool `json:"amend,omitempty"`
Channel string `json:"channel,omitempty"`
Revision string `json:"revision,omitempty"`
+ CohortKey string `json:"cohort-key,omitempty"`
+ LeaveCohort bool `json:"leave-cohort,omitempty"`
DevMode bool `json:"devmode,omitempty"`
JailMode bool `json:"jailmode,omitempty"`
Classic bool `json:"classic,omitempty"`
Dangerous bool `json:"dangerous,omitempty"`
IgnoreValidation bool `json:"ignore-validation,omitempty"`
Unaliased bool `json:"unaliased,omitempty"`
+ Purge bool `json:"purge,omitempty"`
+ Amend bool `json:"amend,omitempty"`
Users []string `json:"users,omitempty"`
}
@@ -200,7 +204,7 @@
"Content-Type": "application/json",
}
- return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data))
+ return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data), doFlags{})
}
// InstallPath sideloads the snap with the given path under optional provided name,
@@ -226,7 +230,7 @@
"Content-Type": mw.FormDataContentType(),
}
- return client.doAsync("POST", "/v2/snaps", nil, headers, pr)
+ return client.doAsyncNoTimeout("POST", "/v2/snaps", nil, headers, pr)
}
// Try
@@ -302,3 +306,89 @@
mw.Close()
pw.Close()
}
+
+type snapRevisionOptions struct {
+ Channel string `json:"channel,omitempty"`
+ Revision string `json:"revision,omitempty"`
+ CohortKey string `json:"cohort-key,omitempty"`
+}
+
+type downloadAction struct {
+ SnapName string `json:"snap-name"`
+
+ snapRevisionOptions
+
+ HeaderPeek bool `json:"header-peek,omitempty"`
+ ResumeToken string `json:"resume-token,omitempty"`
+}
+
+type DownloadInfo struct {
+ SuggestedFileName string
+ Size int64
+ Sha3_384 string
+ ResumeToken string
+}
+
+type DownloadOptions struct {
+ SnapOptions
+
+ HeaderPeek bool
+ ResumeToken string
+ Resume int64
+}
+
+// Download will stream the given snap to the client
+func (client *Client) Download(name string, options *DownloadOptions) (dlInfo *DownloadInfo, r io.ReadCloser, err error) {
+ if options == nil {
+ options = &DownloadOptions{}
+ }
+ action := downloadAction{
+ SnapName: name,
+ snapRevisionOptions: snapRevisionOptions{
+ Channel: options.Channel,
+ CohortKey: options.CohortKey,
+ Revision: options.Revision,
+ },
+ HeaderPeek: options.HeaderPeek,
+ ResumeToken: options.ResumeToken,
+ }
+ data, err := json.Marshal(&action)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot marshal snap action: %s", err)
+ }
+ headers := map[string]string{
+ "Content-Type": "application/json",
+ }
+ if options.Resume > 0 {
+ headers["range"] = fmt.Sprintf("bytes: %d-", options.Resume)
+ }
+
+ // no deadline for downloads
+ ctx := context.Background()
+ rsp, err := client.raw(ctx, "POST", "/v2/download", nil, headers, bytes.NewBuffer(data))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if rsp.StatusCode != 200 {
+ var r response
+ defer rsp.Body.Close()
+ if err := decodeInto(rsp.Body, &r); err != nil {
+ return nil, nil, err
+ }
+ return nil, nil, r.err(client, rsp.StatusCode)
+ }
+ matches := contentDispositionMatcher(rsp.Header.Get("Content-Disposition"))
+ if matches == nil || matches[1] == "" {
+ return nil, nil, fmt.Errorf("cannot determine filename")
+ }
+
+ dlInfo = &DownloadInfo{
+ SuggestedFileName: matches[1],
+ Size: rsp.ContentLength,
+ Sha3_384: rsp.Header.Get("Snap-Sha3-384"),
+ ResumeToken: rsp.Header.Get("Snap-Download-Token"),
+ }
+
+ return dlInfo, rsp.Body, nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/snap_op_test.go snapd-2.45.1ubuntu0.2/client/snap_op_test.go
--- snapd-2.37.4ubuntu0.1/client/snap_op_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/snap_op_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -27,6 +27,7 @@
"io/ioutil"
"mime"
"mime/multipart"
+ "net/http"
"path/filepath"
"gopkg.in/check.v1"
@@ -77,21 +78,23 @@
}
func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) {
- cs.rsp = `{"type": "error", "status": "potatoes"}`
+ cs.status = 400
+ cs.rsp = `{"type": "error"}`
for _, s := range ops {
_, err := s.op(cs.cli, pkgName, nil)
- c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action))
+ c.Check(err, check.ErrorMatches, `.*server error: "Bad Request"`, check.Commentf(s.action))
}
}
func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) {
- cs.rsp = `{"type": "error", "status": "potatoes"}`
+ cs.status = 500
+ cs.rsp = `{"type": "error"}`
for _, s := range multiOps {
_, err := s.op(cs.cli, nil, nil)
- c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action))
+ c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`, check.Commentf(s.action))
}
_, _, err := cs.cli.SnapshotMany(nil, nil)
- c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`)
+ c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`)
}
func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) {
@@ -114,6 +117,7 @@
}
func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"status-code": 202,
"type": "async"
@@ -125,6 +129,7 @@
}
func (cs *clientSuite) TestClientOpSnap(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "d728",
"status-code": 202,
@@ -136,6 +141,9 @@
c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action))
+ _, ok := cs.req.Context().Deadline()
+ c.Check(ok, check.Equals, true)
+
body, err := ioutil.ReadAll(cs.req.Body)
c.Assert(err, check.IsNil, check.Commentf(s.action))
jsonBody := make(map[string]string)
@@ -151,6 +159,7 @@
}
func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "d728",
"status-code": 202,
@@ -179,6 +188,7 @@
func (cs *clientSuite) TestClientMultiSnapshot(c *check.C) {
// Note body is essentially the same as TestClientMultiOpSnap; keep in sync
+ cs.status = 202
cs.rsp = `{
"result": {"set-id": 42},
"change": "d728",
@@ -203,6 +213,7 @@
}
func (cs *clientSuite) TestClientOpInstallPath(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -226,10 +237,13 @@
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 = `{
"change": "66b3",
"status-code": 202,
@@ -258,6 +272,7 @@
}
func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -294,6 +309,7 @@
}
func (cs *clientSuite) TestClientOpInstallUnaliased(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -344,6 +360,7 @@
}
func (cs *clientSuite) TestClientOpTryMode(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -400,3 +417,122 @@
_, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true})
c.Assert(err, check.Equals, client.ErrDangerousNotApplicable)
}
+
+func (cs *clientSuite) TestSnapOptionsSerialises(c *check.C) {
+ tests := map[string]client.SnapOptions{
+ "{}": {},
+ `{"channel":"edge"}`: {Channel: "edge"},
+ `{"revision":"42"}`: {Revision: "42"},
+ `{"cohort-key":"what"}`: {CohortKey: "what"},
+ `{"leave-cohort":true}`: {LeaveCohort: true},
+ `{"devmode":true}`: {DevMode: true},
+ `{"jailmode":true}`: {JailMode: true},
+ `{"classic":true}`: {Classic: true},
+ `{"dangerous":true}`: {Dangerous: true},
+ `{"ignore-validation":true}`: {IgnoreValidation: true},
+ `{"unaliased":true}`: {Unaliased: true},
+ `{"purge":true}`: {Purge: true},
+ `{"amend":true}`: {Amend: true},
+ }
+ for expected, opts := range tests {
+ buf, err := json.Marshal(&opts)
+ c.Assert(err, check.IsNil, check.Commentf("%s", expected))
+ c.Check(string(buf), check.Equals, expected)
+ }
+}
+
+func (cs *clientSuite) TestClientOpDownload(c *check.C) {
+ cs.status = 200
+ cs.header = http.Header{
+ "Content-Disposition": {"attachment; filename=foo_2.snap"},
+ "Snap-Sha3-384": {"sha3sha3sha3"},
+ "Snap-Download-Token": {"some-token"},
+ }
+ cs.contentLength = 1234
+
+ cs.rsp = `lots-of-foo-data`
+
+ dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{
+ SnapOptions: client.SnapOptions{
+ Revision: "2",
+ Channel: "edge",
+ },
+ HeaderPeek: true,
+ })
+ c.Check(err, check.IsNil)
+ c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{
+ SuggestedFileName: "foo_2.snap",
+ Size: 1234,
+ Sha3_384: "sha3sha3sha3",
+ ResumeToken: "some-token",
+ })
+
+ // check we posted the right stuff
+ c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json")
+ c.Assert(cs.req.Header.Get("range"), check.Equals, "")
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+ var jsonBody client.DownloadAction
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, check.IsNil)
+ c.Check(jsonBody.SnapName, check.DeepEquals, "foo")
+ c.Check(jsonBody.Revision, check.Equals, "2")
+ c.Check(jsonBody.Channel, check.Equals, "edge")
+ c.Check(jsonBody.HeaderPeek, check.Equals, true)
+
+ // ensure we can read the response
+ content, err := ioutil.ReadAll(rc)
+ c.Assert(err, check.IsNil)
+ c.Check(string(content), check.Equals, cs.rsp)
+ // and we can close it
+ c.Check(rc.Close(), check.IsNil)
+}
+
+func (cs *clientSuite) TestClientOpDownloadResume(c *check.C) {
+ cs.status = 200
+ cs.header = http.Header{
+ "Content-Disposition": {"attachment; filename=foo_2.snap"},
+ "Snap-Sha3-384": {"sha3sha3sha3"},
+ }
+ // we resume
+ cs.contentLength = 1234 - 64
+
+ cs.rsp = `lots-of-foo-data`
+
+ dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{
+ SnapOptions: client.SnapOptions{
+ Revision: "2",
+ Channel: "edge",
+ },
+ HeaderPeek: true,
+ ResumeToken: "some-token",
+ Resume: 64,
+ })
+ c.Check(err, check.IsNil)
+ c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{
+ SuggestedFileName: "foo_2.snap",
+ Size: 1234 - 64,
+ Sha3_384: "sha3sha3sha3",
+ })
+
+ // check we posted the right stuff
+ c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json")
+ c.Assert(cs.req.Header.Get("range"), check.Equals, "bytes: 64-")
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+ var jsonBody client.DownloadAction
+ err = json.Unmarshal(body, &jsonBody)
+ c.Assert(err, check.IsNil)
+ c.Check(jsonBody.SnapName, check.DeepEquals, "foo")
+ c.Check(jsonBody.Revision, check.Equals, "2")
+ c.Check(jsonBody.Channel, check.Equals, "edge")
+ c.Check(jsonBody.HeaderPeek, check.Equals, true)
+ c.Check(jsonBody.ResumeToken, check.Equals, "some-token")
+
+ // ensure we can read the response
+ content, err := ioutil.ReadAll(rc)
+ c.Assert(err, check.IsNil)
+ c.Check(string(content), check.Equals, cs.rsp)
+ // and we can close it
+ c.Check(rc.Close(), check.IsNil)
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/snapshot.go snapd-2.45.1ubuntu0.2/client/snapshot.go
--- snapd-2.37.4ubuntu0.1/client/snapshot.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/snapshot.go 2020-06-05 13:13:49.000000000 +0000
@@ -72,6 +72,9 @@
Size int64 `json:"size,omitempty"`
// if the snapshot failed to open this will be the reason why
Broken string `json:"broken,omitempty"`
+
+ // set if the snapshot was created automatically on snap removal
+ Auto bool `json:"auto,omitempty"`
}
// IsValid checks whether the snapshot is missing information that
diff -Nru snapd-2.37.4ubuntu0.1/client/snapshot_test.go snapd-2.45.1ubuntu0.2/client/snapshot_test.go
--- snapd-2.37.4ubuntu0.1/client/snapshot_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/snapshot_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -94,6 +94,7 @@
}
func (cs *clientSuite) testClientSnapshotActionFull(c *check.C, action string, users []string, f func() (string, error)) {
+ cs.status = 202
cs.rsp = `{
"status-code": 202,
"type": "async",
diff -Nru snapd-2.37.4ubuntu0.1/client/systems.go snapd-2.45.1ubuntu0.2/client/systems.go
--- snapd-2.37.4ubuntu0.1/client/systems.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/systems.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,105 @@
+// -*- 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
.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+
+ "golang.org/x/xerrors"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// SystemModelData contains information about the model
+type SystemModelData struct {
+ // Model as the model assertion
+ Model string `json:"model,omitempty"`
+ // BrandID corresponds to brand-id in the model assertion
+ BrandID string `json:"brand-id,omitempty"`
+ // DisplayName is human friendly name, corresponds to display-name in
+ // the model assertion
+ DisplayName string `json:"display-name,omitempty"`
+}
+
+type System struct {
+ // Current is true when the system running now was installed from that
+ // recovery seed
+ Current bool `json:"current,omitempty"`
+ // Label of the recovery system
+ Label string `json:"label,omitempty"`
+ // Model information
+ Model SystemModelData `json:"model,omitempty"`
+ // Brand information
+ Brand snap.StoreAccount `json:"brand,omitempty"`
+ // Actions available for this system
+ Actions []SystemAction `json:"actions,omitempty"`
+}
+
+type SystemAction struct {
+ // Title is a user presentable action description
+ Title string `json:"title,omitempty"`
+ // Mode given action can be executed in
+ Mode string `json:"mode,omitempty"`
+}
+
+// ListSystems list all systems available for seeding or recovery.
+func (client *Client) ListSystems() ([]System, error) {
+ type systemsResponse struct {
+ Systems []System `json:"systems,omitempty"`
+ }
+
+ var rsp systemsResponse
+
+ if _, err := client.doSync("GET", "/v2/systems", nil, nil, nil, &rsp); err != nil {
+ return nil, xerrors.Errorf("cannot list recovery systems: %v", err)
+ }
+ return rsp.Systems, nil
+}
+
+// DoSystemAction issues a request to perform an action using the given seed
+// system and its mode.
+func (client *Client) DoSystemAction(systemLabel string, action *SystemAction) error {
+ if systemLabel == "" {
+ return fmt.Errorf("cannot request an action without the system")
+ }
+ if action == nil {
+ return fmt.Errorf("cannot request an action without one")
+ }
+ // deeper verification is done by the backend
+
+ req := struct {
+ Action string `json:"action"`
+ *SystemAction
+ }{
+ Action: "do",
+ SystemAction: action,
+ }
+
+ var body bytes.Buffer
+ if err := json.NewEncoder(&body).Encode(&req); err != nil {
+ return err
+ }
+ if _, err := client.doSync("POST", "/v2/systems/"+systemLabel, nil, nil, &body, nil); err != nil {
+ return xerrors.Errorf("cannot request system action: %v", err)
+ }
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/systems_test.go snapd-2.45.1ubuntu0.2/client/systems_test.go
--- snapd-2.37.4ubuntu0.1/client/systems_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/systems_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,171 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package client_test
+
+import (
+ "encoding/json"
+ "io/ioutil"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/snap"
+)
+
+func (cs *clientSuite) TestListSystemsSome(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {
+ "systems": [
+ {
+ "current": true,
+ "label": "20200101",
+ "model": {
+ "model": "this-is-model-id",
+ "brand-id": "brand-id-1",
+ "display-name": "wonky model"
+ },
+ "brand": {
+ "id": "brand-id-1",
+ "username": "brand",
+ "display-name": "wonky publishing"
+ },
+ "actions": [
+ {"title": "recover", "mode": "recover"},
+ {"title": "reinstall", "mode": "install"}
+ ]
+ }, {
+ "label": "20200311",
+ "model": {
+ "model": "different-model-id",
+ "brand-id": "bulky-brand-id-1",
+ "display-name": "bulky model"
+ },
+ "brand": {
+ "id": "bulky-brand-id-1",
+ "username": "bulky-brand",
+ "display-name": "bulky publishing"
+ },
+ "actions": [
+ {"title": "factory-reset", "mode": "install"}
+ ]
+ }
+ ]
+ }
+ }`
+ systems, err := cs.cli.ListSystems()
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/systems")
+ c.Check(systems, check.DeepEquals, []client.System{
+ {
+ Current: true,
+ Label: "20200101",
+ Model: client.SystemModelData{
+ Model: "this-is-model-id",
+ BrandID: "brand-id-1",
+ DisplayName: "wonky model",
+ },
+ Brand: snap.StoreAccount{
+ ID: "brand-id-1",
+ Username: "brand",
+ DisplayName: "wonky publishing",
+ },
+ Actions: []client.SystemAction{
+ {Title: "recover", Mode: "recover"},
+ {Title: "reinstall", Mode: "install"},
+ },
+ }, {
+ Label: "20200311",
+ Model: client.SystemModelData{
+ Model: "different-model-id",
+ BrandID: "bulky-brand-id-1",
+ DisplayName: "bulky model",
+ },
+ Brand: snap.StoreAccount{
+ ID: "bulky-brand-id-1",
+ Username: "bulky-brand",
+ DisplayName: "bulky publishing",
+ },
+ Actions: []client.SystemAction{
+ {Title: "factory-reset", Mode: "install"},
+ },
+ },
+ })
+}
+
+func (cs *clientSuite) TestListSystemsNone(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {}
+ }`
+ systems, err := cs.cli.ListSystems()
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.Method, check.Equals, "GET")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/systems")
+ c.Check(systems, check.HasLen, 0)
+}
+
+func (cs *clientSuite) TestRequestSystemActionHappy(c *check.C) {
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {}
+ }`
+ err := cs.cli.DoSystemAction("1234", &client.SystemAction{
+ Title: "reinstall",
+ Mode: "install",
+ })
+ c.Assert(err, check.IsNil)
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/systems/1234")
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, check.IsNil)
+ var req map[string]interface{}
+ err = json.Unmarshal(body, &req)
+ c.Assert(err, check.IsNil)
+ c.Assert(req, check.DeepEquals, map[string]interface{}{
+ "action": "do",
+ "title": "reinstall",
+ "mode": "install",
+ })
+}
+
+func (cs *clientSuite) TestRequestSystemActionError(c *check.C) {
+ cs.rsp = `{
+ "type": "error",
+ "status-code": 500,
+ "result": {"message": "failed"}
+ }`
+ err := cs.cli.DoSystemAction("1234", &client.SystemAction{Mode: "install"})
+ c.Assert(err, check.ErrorMatches, "cannot request system action: failed")
+ c.Check(cs.req.Method, check.Equals, "POST")
+ c.Check(cs.req.URL.Path, check.Equals, "/v2/systems/1234")
+}
+
+func (cs *clientSuite) TestRequestSystemActionInvalid(c *check.C) {
+ err := cs.cli.DoSystemAction("", &client.SystemAction{})
+ c.Assert(err, check.ErrorMatches, "cannot request an action without the system")
+ err = cs.cli.DoSystemAction("1234", nil)
+ c.Assert(err, check.ErrorMatches, "cannot request an action without one")
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/users.go snapd-2.45.1ubuntu0.2/client/users.go
--- snapd-2.37.4ubuntu0.1/client/users.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/users.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,143 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-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
.
+ *
+ */
+
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+)
+
+// CreateUserResult holds the result of a user creation.
+type CreateUserResult struct {
+ Username string `json:"username"`
+ SSHKeys []string `json:"ssh-keys"`
+}
+
+// CreateUserOptions holds options for creating a local system user.
+//
+// If Known is false, the provided email is used to query the store for
+// username and SSH key details.
+//
+// If Known is true, the user will be created by looking through existing
+// system-user assertions and looking for a matching email. If Email is
+// empty then all such assertions are considered and multiple users may
+// be created.
+type CreateUserOptions struct {
+ Email string `json:"email,omitempty"`
+ Sudoer bool `json:"sudoer,omitempty"`
+ Known bool `json:"known,omitempty"`
+ ForceManaged bool `json:"force-managed,omitempty"`
+}
+
+// RemoveUserOptions holds options for removing a local system user.
+type RemoveUserOptions struct {
+ // Username indicates which user to remove.
+ Username string `json:"username,omitempty"`
+}
+
+type userAction struct {
+ Action string `json:"action"`
+ *CreateUserOptions
+ *RemoveUserOptions
+}
+
+func (client *Client) doUserAction(act *userAction, result interface{}) error {
+ data, err := json.Marshal(act)
+ if err != nil {
+ return err
+ }
+
+ _, err = client.doSync("POST", "/v2/users", nil, nil, bytes.NewReader(data), result)
+ return err
+}
+
+// CreateUser creates a local system user. See CreateUserOptions for details.
+func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) {
+ if options == nil || options.Email == "" {
+ return nil, fmt.Errorf("cannot create a user without providing an email")
+ }
+
+ var result []*CreateUserResult
+ err := client.doUserAction(&userAction{Action: "create", CreateUserOptions: options}, &result)
+ if err != nil {
+ return nil, fmt.Errorf("while creating user: %v", err)
+ }
+ return result[0], nil
+}
+
+// CreateUsers creates multiple local system users. See CreateUserOptions for details.
+//
+// Results may be provided even if there are errors.
+func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) {
+ for _, opts := range options {
+ if opts == nil || (opts.Email == "" && !opts.Known) {
+ return nil, fmt.Errorf("cannot create user from store details without an email to query for")
+ }
+ }
+
+ var results []*CreateUserResult
+ var errs []error
+ for _, opts := range options {
+ var result []*CreateUserResult
+ err := client.doUserAction(&userAction{Action: "create", CreateUserOptions: opts}, &result)
+ if err != nil {
+ errs = append(errs, err)
+ } else {
+ results = append(results, result...)
+ }
+ }
+
+ if len(errs) == 1 {
+ return results, errs[0]
+ }
+ if len(errs) > 1 {
+ var buf bytes.Buffer
+ for _, err := range errs {
+ fmt.Fprintf(&buf, "\n- %s", err)
+ }
+ return results, fmt.Errorf("while creating users:%s", buf.Bytes())
+ }
+ return results, nil
+}
+
+// RemoveUser removes a local system user.
+func (client *Client) RemoveUser(options *RemoveUserOptions) (removed []*User, err error) {
+ if options == nil || options.Username == "" {
+ return nil, fmt.Errorf("cannot remove a user without providing a username")
+ }
+ var result struct {
+ Removed []*User `json:"removed"`
+ }
+ if err := client.doUserAction(&userAction{Action: "remove", RemoveUserOptions: options}, &result); err != nil {
+ return nil, err
+ }
+ return result.Removed, nil
+}
+
+// Users returns the local users.
+func (client *Client) Users() ([]*User, error) {
+ var result []*User
+
+ if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil {
+ return nil, fmt.Errorf("while getting users: %v", err)
+ }
+ return result, nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/client/users_test.go snapd-2.45.1ubuntu0.2/client/users_test.go
--- snapd-2.37.4ubuntu0.1/client/users_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/client/users_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,184 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-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
.
+ *
+ */
+
+package client_test
+
+import (
+ "io/ioutil"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+)
+
+func (cs *clientSuite) TestClientRemoveUser(c *C) {
+ removed, err := cs.cli.RemoveUser(&client.RemoveUserOptions{})
+ c.Assert(err, ErrorMatches, "cannot remove a user without providing a username")
+ c.Assert(removed, IsNil)
+
+ cs.rsp = `{
+ "type": "sync",
+ "result": {
+ "removed": [{"id": 11, "username": "one-user", "email": "user@test.com"}]
+ }
+ }`
+ removed, err = cs.cli.RemoveUser(&client.RemoveUserOptions{Username: "one-user"})
+ c.Assert(cs.req.Method, Equals, "POST")
+ c.Assert(cs.req.URL.Path, Equals, "/v2/users")
+ c.Assert(err, IsNil)
+ c.Assert(removed, DeepEquals, []*client.User{
+ {ID: 11, Username: "one-user", Email: "user@test.com"},
+ })
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, `{"action":"remove","username":"one-user"}`)
+}
+
+func (cs *clientSuite) TestClientRemoveUserError(c *C) {
+ removed, err := cs.cli.RemoveUser(nil)
+ c.Assert(err, ErrorMatches, "cannot remove a user without providing a username")
+ c.Assert(removed, IsNil)
+ removed, err = cs.cli.RemoveUser(&client.RemoveUserOptions{})
+ c.Assert(err, ErrorMatches, "cannot remove a user without providing a username")
+ c.Assert(removed, IsNil)
+
+ cs.rsp = `{
+ "type": "error",
+ "result": {"message": "no can do"}
+ }`
+ removed, err = cs.cli.RemoveUser(&client.RemoveUserOptions{Username: "one-user"})
+ c.Assert(cs.req.Method, Equals, "POST")
+ c.Assert(cs.req.URL.Path, Equals, "/v2/users")
+ c.Assert(err, ErrorMatches, "no can do")
+ c.Assert(removed, IsNil)
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, `{"action":"remove","username":"one-user"}`)
+}
+
+func (cs *clientSuite) TestClientCreateUser(c *C) {
+ _, err := cs.cli.CreateUser(nil)
+ c.Assert(err, ErrorMatches, "cannot create a user without providing an email")
+ _, err = cs.cli.CreateUser(&client.CreateUserOptions{})
+ c.Assert(err, ErrorMatches, "cannot create a user without providing an email")
+
+ cs.rsp = `{
+ "type": "sync",
+ "result": [{
+ "username": "karl",
+ "ssh-keys": ["one", "two"]
+ }]
+ }`
+ rsp, err := cs.cli.CreateUser(&client.CreateUserOptions{Email: "one@email.com", Sudoer: true, Known: true})
+ c.Assert(cs.req.Method, Equals, "POST")
+ c.Assert(cs.req.URL.Path, Equals, "/v2/users")
+ c.Assert(err, IsNil)
+
+ body, err := ioutil.ReadAll(cs.req.Body)
+ c.Assert(err, IsNil)
+ c.Assert(string(body), Equals, `{"action":"create","email":"one@email.com","sudoer":true,"known":true}`)
+
+ c.Assert(rsp, DeepEquals, &client.CreateUserResult{
+ Username: "karl",
+ SSHKeys: []string{"one", "two"},
+ })
+}
+
+var createUsersTests = []struct {
+ options []*client.CreateUserOptions
+ bodies []string
+ responses []string
+ results []*client.CreateUserResult
+ error string
+}{{
+ // nothing in -> nothing out
+ options: nil,
+}, {
+ options: []*client.CreateUserOptions{nil},
+ error: "cannot create user from store details without an email to query for",
+}, {
+ options: []*client.CreateUserOptions{{}},
+ error: "cannot create user from store details without an email to query for",
+}, {
+ options: []*client.CreateUserOptions{{
+ Email: "one@example.com",
+ Sudoer: true,
+ }, {
+ Known: true,
+ }},
+ bodies: []string{
+ `{"action":"create","email":"one@example.com","sudoer":true}`,
+ `{"action":"create","known":true}`,
+ },
+ responses: []string{
+ `{"type": "sync", "result": [{"username": "one", "ssh-keys":["a", "b"]}]}`,
+ `{"type": "sync", "result": [{"username": "two"}, {"username": "three"}]}`,
+ },
+ results: []*client.CreateUserResult{{
+ Username: "one",
+ SSHKeys: []string{"a", "b"},
+ }, {
+ Username: "two",
+ }, {
+ Username: "three",
+ }},
+}}
+
+func (cs *clientSuite) TestClientCreateUsers(c *C) {
+ for _, test := range createUsersTests {
+ cs.rsps = test.responses
+
+ results, err := cs.cli.CreateUsers(test.options)
+ if test.error != "" {
+ c.Assert(err, ErrorMatches, test.error)
+ }
+ c.Assert(results, DeepEquals, test.results)
+
+ var bodies []string
+ for _, req := range cs.reqs {
+ c.Assert(req.Method, Equals, "POST")
+ c.Assert(req.URL.Path, Equals, "/v2/users")
+ data, err := ioutil.ReadAll(req.Body)
+ c.Assert(err, IsNil)
+ bodies = append(bodies, string(data))
+ }
+
+ c.Assert(bodies, DeepEquals, test.bodies)
+ }
+}
+
+func (cs *clientSuite) TestClientJSONError(c *C) {
+ cs.rsp = `some non-json error message`
+ _, err := cs.cli.SysInfo()
+ c.Assert(err, ErrorMatches, `cannot obtain system details: cannot decode "some non-json error message": invalid char.*`)
+}
+
+func (cs *clientSuite) TestUsers(c *C) {
+ cs.rsp = `{"type": "sync", "result":
+ [{"username": "foo","email":"foo@example.com"},
+ {"username": "bar","email":"bar@example.com"}]}`
+ users, err := cs.cli.Users()
+ c.Check(err, IsNil)
+ c.Check(users, DeepEquals, []*client.User{
+ {Username: "foo", Email: "foo@example.com"},
+ {Username: "bar", Email: "bar@example.com"},
+ })
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/appinfo.go snapd-2.45.1ubuntu0.2/cmd/appinfo.go
--- snapd-2.37.4ubuntu0.1/cmd/appinfo.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/appinfo.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,133 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 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
.
- *
- */
-
-package cmd
-
-import (
- "fmt"
- "path/filepath"
- "strings"
-
- "github.com/snapcore/snapd/client"
- "github.com/snapcore/snapd/dirs"
- "github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/progress"
- "github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/systemd"
-)
-
-func ClientAppInfoNotes(app *client.AppInfo) string {
- if !app.IsService() {
- return "-"
- }
-
- var notes = make([]string, 0, 2)
- var seenTimer, seenSocket bool
- for _, act := range app.Activators {
- switch act.Type {
- case "timer":
- seenTimer = true
- case "socket":
- seenSocket = true
- }
- }
- if seenTimer {
- notes = append(notes, "timer-activated")
- }
- if seenSocket {
- notes = append(notes, "socket-activated")
- }
- if len(notes) == 0 {
- return "-"
- }
- return strings.Join(notes, ",")
-}
-
-func ClientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) ([]client.AppInfo, error) {
- // TODO: pass in an actual notifier here instead of null
- // (Status doesn't _need_ it, but benefits from it)
- sysd := systemd.New(dirs.GlobalRootDir, progress.Null)
-
- out := make([]client.AppInfo, 0, len(apps))
- for _, app := range apps {
- appInfo := client.AppInfo{
- Snap: app.Snap.InstanceName(),
- Name: app.Name,
- CommonID: app.CommonID,
- }
- if fn := app.DesktopFile(); osutil.FileExists(fn) {
- appInfo.DesktopFile = fn
- }
-
- appInfo.Daemon = app.Daemon
- if !app.IsService() || !app.Snap.IsActive() {
- out = append(out, appInfo)
- continue
- }
-
- // collect all services for a single call to systemctl
- serviceNames := make([]string, 0, 1+len(app.Sockets)+1)
- serviceNames = append(serviceNames, app.ServiceName())
-
- sockSvcFileToName := make(map[string]string, len(app.Sockets))
- for _, sock := range app.Sockets {
- sockUnit := filepath.Base(sock.File())
- sockSvcFileToName[sockUnit] = sock.Name
- serviceNames = append(serviceNames, sockUnit)
- }
- if app.Timer != nil {
- timerUnit := filepath.Base(app.Timer.File())
- serviceNames = append(serviceNames, timerUnit)
- }
-
- // sysd.Status() makes sure that we get only the units we asked
- // for and raises an error otherwise
- sts, err := sysd.Status(serviceNames...)
- if err != nil {
- return nil, fmt.Errorf("cannot get status of services of app %q: %v", app.Name, err)
- }
- if len(sts) != len(serviceNames) {
- return nil, fmt.Errorf("cannot get status of services of app %q: expected %v results, got %v", app.Name, len(serviceNames), len(sts))
- }
- for _, st := range sts {
- switch filepath.Ext(st.UnitName) {
- case ".service":
- appInfo.Enabled = st.Enabled
- appInfo.Active = st.Active
- case ".timer":
- appInfo.Activators = append(appInfo.Activators, client.AppActivator{
- Name: app.Name,
- Enabled: st.Enabled,
- Active: st.Active,
- Type: "timer",
- })
- case ".socket":
- appInfo.Activators = append(appInfo.Activators, client.AppActivator{
- Name: sockSvcFileToName[st.UnitName],
- Enabled: st.Enabled,
- Active: st.Active,
- Type: "socket",
- })
- }
- }
- out = append(out, appInfo)
- }
-
- return out, nil
-}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/appinfo_test.go snapd-2.45.1ubuntu0.2/cmd/appinfo_test.go
--- snapd-2.37.4ubuntu0.1/cmd/appinfo_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/appinfo_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,71 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 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
.
- *
- */
-
-package cmd_test
-
-import (
- "gopkg.in/check.v1"
-
- "github.com/snapcore/snapd/client"
- "github.com/snapcore/snapd/cmd"
-)
-
-func (*cmdSuite) TestAppStatusNotes(c *check.C) {
- ai := client.AppInfo{}
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-")
-
- ai = client.AppInfo{
- Daemon: "oneshot",
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-")
-
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "timer"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated")
-
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "socket"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "socket-activated")
-
- // check that the output is stable regardless of the order of activators
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "timer"},
- {Type: "socket"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated")
- ai = client.AppInfo{
- Daemon: "oneshot",
- Activators: []client.AppActivator{
- {Type: "socket"},
- {Type: "timer"},
- },
- }
- c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated")
-}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/autogen.sh snapd-2.45.1ubuntu0.2/cmd/autogen.sh
--- snapd-2.37.4ubuntu0.1/cmd/autogen.sh 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/autogen.sh 2020-06-05 13:13:49.000000000 +0000
@@ -33,13 +33,13 @@
extra_opts="--libexecdir=/usr/lib/snapd"
;;
ubuntu)
- extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --enable-static-libseccomp --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
+ extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)"
if [ "$(dpkg-architecture -qDEB_HOST_ARCH)" = "amd64" ]; then
extra_opts="$extra_opts --with-host-arch-32bit-triplet=$(dpkg-architecture -ai386 -qDEB_HOST_MULTIARCH)"
fi
;;
fedora|centos|rhel)
- extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor"
+ extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor --enable-selinux"
;;
opensuse|opensuse-tumbleweed)
extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-biarch --with-32bit-libdir=/usr/lib --enable-merged-usr"
diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmd_linux.go snapd-2.45.1ubuntu0.2/cmd/cmd_linux.go
--- snapd-2.37.4ubuntu0.1/cmd/cmd_linux.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/cmd_linux.go 2020-06-05 13:13:49.000000000 +0000
@@ -20,14 +20,13 @@
package cmd
import (
- "bytes"
- "io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"syscall"
+ "github.com/snapcore/snapd/cmd/cmdutil"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
@@ -73,34 +72,18 @@
return true
}
-// coreSupportsReExec returns true if the given core snap should be used as re-exec target.
+// coreSupportsReExec returns true if the given core/snapd snap should be used as re-exec target.
//
// Ensure we do not use older version of snapd, look for info file and ignore
// version of core that do not yet have it.
-func coreSupportsReExec(corePath string) bool {
- fullInfo := filepath.Join(corePath, filepath.Join(dirs.CoreLibExecDir, "info"))
- content, err := ioutil.ReadFile(fullInfo)
+func coreSupportsReExec(coreOrSnapdPath string) bool {
+ infoPath := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir, "info"))
+ ver, err := cmdutil.SnapdVersionFromInfoFile(infoPath)
if err != nil {
- if !os.IsNotExist(err) {
- logger.Noticef("cannot open snapd info file %q: %s", fullInfo, err)
- }
+ logger.Noticef("%v", err)
return false
}
- if !bytes.HasPrefix(content, []byte("VERSION=")) {
- idx := bytes.Index(content, []byte("\nVERSION="))
- if idx < 0 {
- logger.Noticef("cannot find snapd version information in %q", content)
- return false
- }
- content = content[idx+1:]
- }
- content = content[8:]
- idx := bytes.IndexByte(content, '\n')
- if idx > -1 {
- content = content[:idx]
- }
- ver := string(content)
// > 0 means our Version is bigger than the version of snapd in core
res, err := strutil.VersionCompare(Version, ver)
if err != nil {
@@ -108,20 +91,21 @@
return false
}
if res > 0 {
- logger.Debugf("core snap (at %q) is older (%q) than distribution package (%q)", corePath, ver, Version)
+ logger.Debugf("snap (at %q) is older (%q) than distribution package (%q)", coreOrSnapdPath, ver, Version)
return false
}
return true
}
+// TODO: move to cmd/cmdutil/
+//
// InternalToolPath returns the path of an internal snapd tool. The tool
-// *must* be located inside /usr/lib/snapd/.
+// *must* be located inside the same tree as the current binary.
//
// The return value is either the path of the tool in the current distribution
-// or in the core snap (or the ubuntu-core snap). This handles spiritual
-// "re-exec" where we run the tool from the core snap if the environment allows
-// us to do so.
-func InternalToolPath(tool string) string {
+// or in the core/snapd snap (or the ubuntu-core snap) if the current binary is
+// ran from that location.
+func InternalToolPath(tool string) (string, error) {
distroTool := filepath.Join(dirs.DistroLibExecDir, tool)
// find the internal path relative to the running snapd, this
@@ -129,23 +113,34 @@
// having a valid "current" symlink).
exe, err := osReadlink("/proc/self/exe")
if err != nil {
- logger.Noticef("cannot read /proc/self/exe: %v, using tool outside core", err)
- return distroTool
+ return "", err
}
- // ensure we never use this helper from anything but
- if !strings.HasSuffix(exe, "/snapd") && !strings.HasSuffix(exe, ".test") {
- log.Panicf("InternalToolPath can only be used from snapd, got: %s", exe)
- }
+ if !strings.HasPrefix(exe, dirs.DistroLibExecDir) {
+ // either running from mounted location or /usr/bin/snap*
- if !strings.HasPrefix(exe, dirs.SnapMountDir) {
- logger.Debugf("exe doesn't have snap mount dir prefix: %q vs %q", exe, dirs.SnapMountDir)
- return distroTool
+ // find the local prefix to the snap:
+ // /snap/snapd/123/usr/bin/snap -> /snap/snapd/123
+ // /snap/core/234/usr/lib/snapd/snapd -> /snap/core/234
+ idx := strings.LastIndex(exe, "/usr/")
+ if idx > 0 {
+ // only assume mounted location when path contains
+ // /usr/, but does not start with one
+ prefix := exe[:idx]
+ return filepath.Join(prefix, "/usr/lib/snapd", tool), nil
+ }
+ if idx == -1 {
+ // or perhaps some other random location, make sure the tool
+ // exists there and is an executable
+ maybeTool := filepath.Join(filepath.Dir(exe), tool)
+ if osutil.IsExecutable(maybeTool) {
+ return maybeTool, nil
+ }
+ }
}
- // if we are re-execed, then the tool is at the same location
- // as snapd
- return filepath.Join(filepath.Dir(exe), tool)
+ // fallback to distro tool
+ return distroTool, nil
}
// mustUnsetenv will unset the given environment key or panic if it
@@ -194,10 +189,10 @@
}
// Is this executable in the core snap too?
- corePath := snapdSnap
+ coreOrSnapdPath := snapdSnap
full := filepath.Join(snapdSnap, exe)
if !osutil.FileExists(full) {
- corePath = coreSnap
+ coreOrSnapdPath = coreSnap
full = filepath.Join(coreSnap, exe)
if !osutil.FileExists(full) {
return
@@ -205,10 +200,19 @@
}
// If the core snap doesn't support re-exec or run-from-core then don't do it.
- if !coreSupportsReExec(corePath) {
+ if !coreSupportsReExec(coreOrSnapdPath) {
return
}
logger.Debugf("restarting into %q", full)
panic(syscallExec(full, os.Args, os.Environ()))
}
+
+// MockOsReadlink is for use in tests
+func MockOsReadlink(f func(string) (string, error)) func() {
+ realOsReadlink := osReadlink
+ osReadlink = f
+ return func() {
+ osReadlink = realOsReadlink
+ }
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmd_other.go snapd-2.45.1ubuntu0.2/cmd/cmd_other.go
--- snapd-2.37.4ubuntu0.1/cmd/cmd_other.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/cmd_other.go 2020-06-05 13:13:49.000000000 +0000
@@ -20,9 +20,21 @@
package cmd
+import (
+ "errors"
+)
+
// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in
// the snapd/core snap.
// On this OS this is a stub.
func ExecInSnapdOrCoreSnap() {
return
}
+
+// InternalToolPath returns the path of an internal snapd tool. The tool
+// *must* be located inside the same tree as the current binary.
+//
+// On this OS this is a stub and always returns an error.
+func InternalToolPath(tool string) (string, error) {
+ return "", errors.New("unsupported on non-Linux systems")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmd_test.go snapd-2.45.1ubuntu0.2/cmd/cmd_test.go
--- snapd-2.37.4ubuntu0.1/cmd/cmd_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/cmd_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -206,7 +206,9 @@
})
defer restore()
- c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.DistroLibExecDir, "potato"))
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato"))
}
func (s *cmdSuite) TestInternalToolPathWithReexec(c *C) {
@@ -216,16 +218,126 @@
})
defer restore()
- c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato"))
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato"))
}
-func (s *cmdSuite) TestInternalToolPathFromIncorrectHelper(c *C) {
+func (s *cmdSuite) TestInternalToolPathWithOtherLocation(c *C) {
+ s.fakeInternalTool(c, s.snapdPath, "potato")
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/tmp/tmp.foo_1234/usr/lib/snapd/snapd"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato")
+}
+
+func (s *cmdSuite) TestInternalToolSnapPathWithOtherLocation(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/tmp/tmp.foo_1234/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato")
+}
+
+func (s *cmdSuite) TestInternalToolPathWithOtherCrazyLocation(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/usr/foo/usr/tmp/tmp.foo_1234/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, "/usr/foo/usr/tmp/tmp.foo_1234/usr/lib/snapd/potato")
+}
+
+func (s *cmdSuite) TestInternalToolPathWithDevLocationFallback(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/home/dev/snapd/snapd"), nil
+ })
+ defer restore()
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato"))
+}
+
+func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationWhenExecutable(c *C) {
restore := cmd.MockOsReadlink(func(string) (string, error) {
- return "/usr/bin/potato", nil
+ return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil
+ })
+ defer restore()
+
+ devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/potato")
+ err := os.MkdirAll(filepath.Dir(devTool), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(devTool, []byte(""), 0755)
+ c.Assert(err, IsNil)
+
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.GlobalRootDir, "/tmp/potato"))
+}
+
+func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationNonExecutable(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil
+ })
+ defer restore()
+
+ devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/non-executable-potato")
+ err := os.MkdirAll(filepath.Dir(devTool), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(devTool, []byte(""), 0644)
+ c.Assert(err, IsNil)
+
+ path, err := cmd.InternalToolPath("non-executable-potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "non-executable-potato"))
+}
+
+func (s *cmdSuite) TestInternalToolPathSnapdPathReexec(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ p, err := cmd.InternalToolPath("snapd")
+ c.Assert(err, IsNil)
+ c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/core/111/usr/lib/snapd/snapd"))
+}
+
+func (s *cmdSuite) TestInternalToolPathSnapdSnap(c *C) {
+ restore := cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "snapd/22/usr/bin/snap"), nil
+ })
+ defer restore()
+ p, err := cmd.InternalToolPath("snapd")
+ c.Assert(err, IsNil)
+ c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/snapd/22/usr/lib/snapd/snapd"))
+}
+
+func (s *cmdSuite) TestInternalToolPathWithLibexecdirLocation(c *C) {
+ defer dirs.SetRootDir(s.fakeroot)
+ restore := release.MockReleaseInfo(&release.OS{ID: "fedora"})
+ defer restore()
+ // reload directory paths
+ dirs.SetRootDir("/")
+
+ restore = cmd.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join("/usr/bin/snap"), nil
})
defer restore()
- c.Check(func() { cmd.InternalToolPath("potato") }, PanicMatches, "InternalToolPath can only be used from snapd, got: /usr/bin/potato")
+ path, err := cmd.InternalToolPath("potato")
+ c.Check(err, IsNil)
+ c.Check(path, Equals, filepath.Join("/usr/libexec/snapd/potato"))
}
func (s *cmdSuite) TestExecInSnapdOrCoreSnap(c *C) {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil.go
--- snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,140 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package cmdutil
+
+import (
+ "bufio"
+ "bytes"
+ "debug/elf"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+func elfInterp(cmd string) (string, error) {
+ el, err := elf.Open(cmd)
+ if err != nil {
+ return "", err
+ }
+ defer el.Close()
+
+ for _, prog := range el.Progs {
+ if prog.Type == elf.PT_INTERP {
+ r := prog.Open()
+ interp, err := ioutil.ReadAll(r)
+ if err != nil {
+ return "", nil
+ }
+
+ return string(bytes.Trim(interp, "\x00")), nil
+ }
+ }
+
+ return "", fmt.Errorf("cannot find PT_INTERP header")
+}
+
+func parseLdSoConf(root string, confPath string) []string {
+ f, err := os.Open(filepath.Join(root, confPath))
+ if err != nil {
+ return nil
+ }
+ defer f.Close()
+
+ var out []string
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line := scanner.Text()
+ switch {
+ case strings.HasPrefix(line, "#"):
+ // nothing
+ case strings.TrimSpace(line) == "":
+ // nothing
+ case strings.HasPrefix(line, "include "):
+ l := strings.SplitN(line, "include ", 2)
+ files, err := filepath.Glob(filepath.Join(root, l[1]))
+ if err != nil {
+ return nil
+ }
+ for _, f := range files {
+ out = append(out, parseLdSoConf(root, f[len(root):])...)
+ }
+ default:
+ out = append(out, filepath.Join(root, line))
+ }
+
+ }
+ if err := scanner.Err(); err != nil {
+ return nil
+ }
+
+ return out
+}
+
+// CommandFromSystemSnap runs a command from the snapd/core snap
+// using the proper interpreter and library paths.
+//
+// At the moment it can only run ELF files, expects a standard ld.so
+// interpreter, and can't handle RPATH.
+func CommandFromSystemSnap(name string, cmdArgs ...string) (*exec.Cmd, error) {
+ from := "snapd"
+ root := filepath.Join(dirs.SnapMountDir, "/snapd/current")
+ if !osutil.FileExists(root) {
+ from = "core"
+ root = filepath.Join(dirs.SnapMountDir, "/core/current")
+ }
+
+ cmdPath := filepath.Join(root, name)
+ interp, err := elfInterp(cmdPath)
+ if err != nil {
+ return nil, err
+ }
+ coreLdSo := filepath.Join(root, interp)
+ // we cannot use EvalSymlink here because we need to resolve
+ // relative and an absolute symlinks differently. A absolute
+ // symlink is relative to root of the snapd/core snap.
+ seen := map[string]bool{}
+ for osutil.IsSymlink(coreLdSo) {
+ link, err := os.Readlink(coreLdSo)
+ if err != nil {
+ return nil, err
+ }
+ if filepath.IsAbs(link) {
+ coreLdSo = filepath.Join(root, link)
+ } else {
+ coreLdSo = filepath.Join(filepath.Dir(coreLdSo), link)
+ }
+ if seen[coreLdSo] {
+ return nil, fmt.Errorf("cannot run command from %s: symlink cycle found", from)
+ }
+ seen[coreLdSo] = true
+ }
+
+ ldLibraryPathForCore := parseLdSoConf(root, "/etc/ld.so.conf")
+
+ ldSoArgs := []string{"--library-path", strings.Join(ldLibraryPathForCore, ":"), cmdPath}
+ allArgs := append(ldSoArgs, cmdArgs...)
+ return exec.Command(coreLdSo, allArgs...), nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil_test.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil_test.go
--- snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,118 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 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
.
+ *
+ */
+
+package cmdutil_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/cmdutil"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+var truePath = osutil.LookPathDefault("true", "/bin/true")
+
+type cmdutilSuite struct{}
+
+var _ = Suite(&cmdutilSuite{})
+
+func (s *cmdutilSuite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+}
+
+func (s *cmdutilSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("")
+}
+
+func (s *cmdutilSuite) makeMockLdSoConf(c *C, root string) {
+ ldSoConf := filepath.Join(root, "/etc/ld.so.conf")
+ ldSoConfD := ldSoConf + ".d"
+
+ err := os.MkdirAll(filepath.Dir(ldSoConf), 0755)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll(ldSoConfD, 0755)
+ c.Assert(err, IsNil)
+
+ err = ioutil.WriteFile(ldSoConf, []byte("include /etc/ld.so.conf.d/*.conf"), 0644)
+ c.Assert(err, IsNil)
+
+ ldSoConf1 := filepath.Join(ldSoConfD, "x86_64-linux-gnu.conf")
+
+ err = ioutil.WriteFile(ldSoConf1, []byte(`
+# Multiarch support
+/lib/x86_64-linux-gnu
+/usr/lib/x86_64-linux-gnu`), 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *cmdutilSuite) TestCommandFromSystemSnap(c *C) {
+ for _, snap := range []string{"core", "snapd"} {
+
+ root := filepath.Join(dirs.SnapMountDir, snap, "current")
+ s.makeMockLdSoConf(c, root)
+
+ os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755)
+ osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0)
+ cmd, err := cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg")
+ c.Assert(err, IsNil)
+
+ out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("readelf -l %s |grep interpreter:|cut -f2 -d:|cut -f1 -d]", truePath)).Output()
+ c.Assert(err, IsNil)
+ interp := strings.TrimSpace(string(out))
+
+ c.Check(cmd.Args, DeepEquals, []string{
+ filepath.Join(root, interp),
+ "--library-path",
+ fmt.Sprintf("%s/lib/x86_64-linux-gnu:%s/usr/lib/x86_64-linux-gnu", root, root),
+ filepath.Join(root, "/usr/bin/xdelta3"),
+ "--some-xdelta-arg",
+ })
+ }
+}
+
+func (s *cmdutilSuite) TestCommandFromCoreSymlinkCycle(c *C) {
+ root := filepath.Join(dirs.SnapMountDir, "/core/current")
+ s.makeMockLdSoConf(c, root)
+
+ os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755)
+ osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0)
+
+ out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output()
+ c.Assert(err, IsNil)
+ interp := strings.TrimSpace(string(out))
+
+ coreInterp := filepath.Join(root, interp)
+ c.Assert(os.MkdirAll(filepath.Dir(coreInterp), 0755), IsNil)
+ c.Assert(os.Symlink(filepath.Base(coreInterp), coreInterp), IsNil)
+
+ _, err = cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg")
+ c.Assert(err, ErrorMatches, "cannot run command from core: symlink cycle found")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/version.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/version.go
--- snapd-2.37.4ubuntu0.1/cmd/cmdutil/version.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/version.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,53 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package cmdutil
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+)
+
+// SnapdVersionFromInfoFile returns snapd version read for the
+// given info" file, pointed by infoPath.
+// The format of the "info" file is a single line with "VERSION=..."
+// in it. The file is produced by mkversion.sh and normally installed
+// along snapd binary in /usr/lib/snapd.
+func SnapdVersionFromInfoFile(infoPath string) (string, error) {
+ content, err := ioutil.ReadFile(infoPath)
+ if err != nil {
+ return "", fmt.Errorf("cannot open snapd info file %q: %s", infoPath, err)
+ }
+
+ if !bytes.HasPrefix(content, []byte("VERSION=")) {
+ idx := bytes.Index(content, []byte("\nVERSION="))
+ if idx < 0 {
+ return "", fmt.Errorf("cannot find snapd version information in %q", content)
+ }
+ content = content[idx+1:]
+ }
+ content = content[8:]
+ idx := bytes.IndexByte(content, '\n')
+ if idx > -1 {
+ content = content[:idx]
+ }
+
+ return string(content), nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/version_test.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/version_test.go
--- snapd-2.37.4ubuntu0.1/cmd/cmdutil/version_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/version_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,57 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
+package cmdutil_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/cmdutil"
+)
+
+type versionSuite struct{}
+
+var _ = Suite(&versionSuite{})
+
+func (s *versionSuite) TestNoVersionFile(c *C) {
+ _, err := cmdutil.SnapdVersionFromInfoFile("/non-existing-file")
+ c.Assert(err, ErrorMatches, `cannot open snapd info file "/non-existing-file":.*`)
+}
+
+func (s *versionSuite) TestNoVersionData(c *C) {
+ top := c.MkDir()
+ infoFile := filepath.Join(top, "info")
+ c.Assert(ioutil.WriteFile(infoFile, []byte("foo"), 0644), IsNil)
+
+ _, err := cmdutil.SnapdVersionFromInfoFile(infoFile)
+ c.Assert(err, ErrorMatches, `cannot find snapd version information in "foo"`)
+}
+
+func (s *versionSuite) TestVersionHappy(c *C) {
+ top := c.MkDir()
+ infoFile := filepath.Join(top, "info")
+ c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=1.2.3"), 0644), IsNil)
+
+ ver, err := cmdutil.SnapdVersionFromInfoFile(infoFile)
+ c.Assert(err, IsNil)
+ c.Check(ver, Equals, "1.2.3")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/configure.ac snapd-2.45.1ubuntu0.2/cmd/configure.ac
--- snapd-2.37.4ubuntu0.1/cmd/configure.ac 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/configure.ac 2020-06-05 13:13:49.000000000 +0000
@@ -64,34 +64,26 @@
esac], [enable_apparmor=yes])
AM_CONDITIONAL([APPARMOR], [test "x$enable_apparmor" = "xyes"])
-# Allow to build without seccomp support by calling:
-# ./configure --disable-seccomp
-# This is separate because seccomp support is generally very good and it
-# provides useful confinement for unsafe system calls.
-AC_ARG_ENABLE([seccomp],
- AS_HELP_STRING([--disable-seccomp], [Disable seccomp support]),
- [case "${enableval}" in
- yes) enable_seccomp=yes ;;
- no) enable_seccomp=no ;;
- *) AC_MSG_ERROR([bad value ${enableval} for --disable-seccomp])
- esac], [enable_seccomp=yes])
-AM_CONDITIONAL([SECCOMP], [test "x$enable_seccomp" = "xyes"])
+# Allow to build with SELinux support by calling:
+# ./configure --enable-selinux
+AC_ARG_ENABLE([selinux],
+ AS_HELP_STRING([--enable-selinux], [Enable SELinux support]),
+ [case "${enableval}" in
+ yes) enable_selinux=yes ;;
+ no) enable_selinux=no ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-selinux])
+ esac], [enable_selinux=no])
+AM_CONDITIONAL([SELINUX], [test "x$enable_selinux" = "xyes"])
# Enable older tests only when confinement is enabled and we're building for PC
# The tests are of smaller value as we port more and more tests to spread.
-AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && test "x$enable_seccomp" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))])
+AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))])
# Check for glib that we use for unit testing
AS_IF([test "x$with_unit_tests" = "xyes"], [
PKG_CHECK_MODULES([GLIB], [glib-2.0])
])
-# Check if seccomp userspace library is available
-AS_IF([test "x$enable_seccomp" = "xyes"], [
- PKG_CHECK_MODULES([SECCOMP], [libseccomp], [
- AC_DEFINE([HAVE_SECCOMP], [1], [Build with seccomp support])])
-])
-
# Check if apparmor userspace library is available.
AS_IF([test "x$enable_apparmor" = "xyes"], [
PKG_CHECK_MODULES([APPARMOR], [libapparmor], [
@@ -105,6 +97,12 @@
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX])
])
+# Check if SELinux userspace library is available.
+AS_IF([test "x$enable_selinux" = "xyes"], [
+PKG_CHECK_MODULES([SELINUX], [libselinux], [
+AC_DEFINE([HAVE_SELINUX], [1], [Build with SELinux support])])
+])
+
# Check if udev and libudev are available.
# Those are now used unconditionally even if apparmor is disabled.
PKG_CHECK_MODULES([LIBUDEV], [libudev])
@@ -161,18 +159,9 @@
AC_SUBST(SNAP_MOUNT_DIR)
AC_DEFINE_UNQUOTED([SNAP_MOUNT_DIR], "${SNAP_MOUNT_DIR}", [Location of the snap mount points])
-AC_ARG_ENABLE([caps-over-setuid],
- AS_HELP_STRING([--enable-caps-over-setuid], [Use capabilities rather than setuid bit]),
- [case "${enableval}" in
- yes) enable_caps_over_setuid=yes ;;
- no) enable_caps_over_setuid=no ;;
- *) AC_MSG_ERROR([bad value ${enableval} for --enable-caps-over-setuid])
- esac], [enable_caps_over_setuid=no])
-AM_CONDITIONAL([CAPS_OVER_SETUID], [test "x$enable_caps_over_setuid" = "xyes"])
-
-AS_IF([test "x$enable_caps_over_setuid" = "xyes"], [
- AC_DEFINE([CAPS_OVER_SETUID], [1],
- [Use capabilities rather than setuid bit])])
+SNAP_MOUNT_DIR_SYSTEMD_UNIT="$(systemd-escape -p "$SNAP_MOUNT_DIR")"
+AC_SUBST([SNAP_MOUNT_DIR_SYSTEMD_UNIT])
+AC_DEFINE_UNQUOTED([SNAP_MOUNT_DIR_SYSTEMD_UNIT], "${SNAP_MOUNT_DIR_SYSTEMD_UNIT}", [Systemd unit name for snap mount points location])
AC_PATH_PROGS([HAVE_RST2MAN],[rst2man rst2man.py])
AS_IF([test "x$HAVE_RST2MAN" = "x"], [AC_MSG_WARN(["cannot find the rst2man tool, install python-docutils or similar"])])
@@ -182,6 +171,7 @@
AM_CONDITIONAL([HAVE_VALGRIND], [test "x${HAVE_VALGRIND}" != "x"])
AS_IF([test "x$HAVE_VALGRIND" = "x"], [AC_MSG_WARN(["cannot find the valgrind tool, will not run unit tests through valgrind"])])
+# Allow linking selected libraries statically for reexec.
AC_ARG_ENABLE([static-libcap],
AS_HELP_STRING([--enable-static-libcap], [Link libcap statically]),
[case "${enableval}" in
@@ -200,14 +190,14 @@
esac], [enable_static_libapparmor=no])
AM_CONDITIONAL([STATIC_LIBAPPARMOR], [test "x$enable_static_libapparmor" = "xyes"])
-AC_ARG_ENABLE([static-libseccomp],
- AS_HELP_STRING([--enable-static-libseccomp], [Link libseccomp statically]),
- [case "${enableval}" in
- yes) enable_static_libseccomp=yes ;;
- no) enable_static_libseccomp=no ;;
- *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libseccomp])
- esac], [enable_static_libseccomp=no])
-AM_CONDITIONAL([STATIC_LIBSECCOMP], [test "x$enable_static_libseccomp" = "xyes"])
+AC_ARG_ENABLE([static-libselinux],
+AS_HELP_STRING([--enable-static-libselinux], [Link libselinux statically]),
+[case "${enableval}" in
+yes) enable_static_libselinux=yes ;;
+no) enable_static_libselinux=no ;;
+*) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libselinux])
+esac], [enable_static_libselinux=no])
+AM_CONDITIONAL([STATIC_LIBSELINUX], [test "x$enable_static_libselinux" = "xyes"])
LIB32_DIR="${prefix}/lib32"
AC_ARG_WITH([32bit-libdir],
diff -Nru snapd-2.37.4ubuntu0.1/cmd/export_test.go snapd-2.45.1ubuntu0.2/cmd/export_test.go
--- snapd-2.37.4ubuntu0.1/cmd/export_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/export_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -50,11 +50,3 @@
syscallExec = oldSyscallExec
}
}
-
-func MockOsReadlink(f func(string) (string, error)) func() {
- realOsReadlink := osReadlink
- osReadlink = f
- return func() {
- osReadlink = realOsReadlink
- }
-}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.c 2020-06-05 13:13:49.000000000 +0000
@@ -1,3 +1,20 @@
+/*
+ * Copyright (C) 2019 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
.
+ *
+ */
+
// For AT_EMPTY_PATH and O_PATH
#define _GNU_SOURCE
@@ -11,6 +28,7 @@
#include
#include
+#include "cgroup-support.h"
#include "cleanup-funcs.h"
#include "string-utils.h"
#include "utils.h"
@@ -19,51 +37,9 @@
void sc_cgroup_freezer_join(const char *snap_name, pid_t pid)
{
- // Format the name of the cgroup hierarchy.
char buf[PATH_MAX] = { 0 };
sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name);
-
- // Open the freezer cgroup directory.
- int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1;
- cgroup_fd = open(freezer_cgroup_dir,
- O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
- if (cgroup_fd < 0) {
- die("cannot open freezer cgroup (%s)", freezer_cgroup_dir);
- }
- // Create the freezer hierarchy for the given snap.
- if (mkdirat(cgroup_fd, buf, 0755) < 0 && errno != EEXIST) {
- die("cannot create freezer cgroup hierarchy for snap %s",
- snap_name);
- }
- // Open the hierarchy directory for the given snap.
- int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1;
- hierarchy_fd = openat(cgroup_fd, buf,
- O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
- if (hierarchy_fd < 0) {
- die("cannot open freezer cgroup hierarchy for snap %s",
- snap_name);
- }
- // Since we may be running from a setuid but not setgid executable, ensure
- // that the group and owner of the hierarchy directory is root.root.
- if (fchownat(hierarchy_fd, "", 0, 0, AT_EMPTY_PATH) < 0) {
- die("cannot change owner of freezer cgroup hierarchy for snap %s to root.root", snap_name);
- }
- // Open the tasks file.
- int tasks_fd SC_CLEANUP(sc_cleanup_close) = -1;
- tasks_fd = openat(hierarchy_fd, "tasks",
- O_WRONLY | O_NOFOLLOW | O_CLOEXEC);
- if (tasks_fd < 0) {
- die("cannot open tasks file for freezer cgroup hierarchy for snap %s", snap_name);
- }
- // Write the process (task) number to the tasks file. Linux task IDs are
- // limited to 2^29 so a long int is enough to represent it.
- // See include/linux/threads.h in the kernel source tree for details.
- int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid);
- if (write(tasks_fd, buf, n) < n) {
- die("cannot move process %ld to freezer cgroup hierarchy for snap %s", (long)pid, snap_name);
- }
- debug("moved process %ld to freezer cgroup hierarchy for snap %s",
- (long)pid, snap_name);
+ sc_cgroup_create_and_join(freezer_cgroup_dir, buf, pid);
}
bool sc_cgroup_freezer_occupied(const char *snap_name)
@@ -108,7 +84,7 @@
FILE *cgroup_procs SC_CLEANUP(sc_cleanup_file) = NULL;
cgroup_procs = fdopen(cgroup_procs_fd, "r");
if (cgroup_procs == NULL) {
- die("cannot convert tasks file descriptor to FILE");
+ die("cannot convert cgroups.procs file descriptor to FILE");
}
cgroup_procs_fd = -1; // cgroup_procs_fd will now be closed by fclose.
@@ -116,7 +92,7 @@
size_t line_buf_size = 0;
ssize_t num_read;
struct stat statbuf;
- do {
+ for (;;) {
num_read = getline(&line_buf, &line_buf_size, cgroup_procs);
if (num_read < 0 && errno != 0) {
die("cannot read next PID belonging to snap %s",
@@ -143,7 +119,7 @@
debug("found process %s belonging to user %d",
line_buf, statbuf.st_uid);
return true;
- } while (num_read > 0);
+ }
return false;
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.h 2020-06-05 13:13:49.000000000 +0000
@@ -1,3 +1,20 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
#ifndef SC_CGROUP_FREEZER_SUPPORT_H
#define SC_CGROUP_FREEZER_SUPPORT_H
@@ -14,7 +31,7 @@
* allows us to track processes belonging to a given snap. This makes the
* measurement "are any processes of this snap still alive" very simple.
*
- * The "tasks" file belonging to the cgroup contains the set of all the
+ * The "cgroup.procs" file belonging to the cgroup contains the set of all the
* processes that originate from the given snap. Examining that file one can
* reliably determine if the set is empty or not.
*
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.c 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "cgroup-pids-support.h"
+
+#include "cgroup-support.h"
+
+static const char *pids_cgroup_dir = "/sys/fs/cgroup/pids";
+
+void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid) {
+ sc_cgroup_create_and_join(pids_cgroup_dir, snap_security_tag, pid);
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.h 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.h 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SC_CGROUP_PIDS_SUPPORT_H
+#define SC_CGROUP_PIDS_SUPPORT_H
+
+#include
+
+/**
+ * Join the pid cgroup for the given snap application.
+ *
+ * This function adds the specified task to the pid cgroup specific to the
+ * given snap. The name of the cgroup is "snap.$snap_name.$app_name" for apps
+ * or "snap.$snap_name.hook.$hook_name" for hooks.
+ *
+ * The "tasks" file belonging to the cgroup contains the set of all the
+ * threads that originate from the given snap app or hook. Examining that
+ * file one can reliably determine if the set is empty or not.
+ *
+ * Similarly the "cgroup.procs" file belonging to the same directory contains
+ * the set of all the processes that originate from the given snap app or
+ * hook.
+ *
+ * For more details please review:
+ * https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt
+ **/
+void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid);
+
+#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.c 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+// For AT_EMPTY_PATH and O_PATH
+#define _GNU_SOURCE
+
+#include "cgroup-support.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "cleanup-funcs.h"
+#include "string-utils.h"
+#include "utils.h"
+
+void sc_cgroup_create_and_join(const char *parent, const char *name, pid_t pid) {
+ int parent_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ parent_fd = open(parent, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (parent_fd < 0) {
+ die("cannot open cgroup hierarchy %s", parent);
+ }
+ // Since we may be running from a setuid but not setgid executable, switch
+ // to the effective group to root so that the mkdirat call creates a cgroup
+ // that is always owned by root.root.
+ sc_identity old = sc_set_effective_identity(sc_root_group_identity());
+ if (mkdirat(parent_fd, name, 0755) < 0 && errno != EEXIST) {
+ die("cannot create cgroup hierarchy %s/%s", parent, name);
+ }
+ (void)sc_set_effective_identity(old);
+ int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ hierarchy_fd = openat(parent_fd, name, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (hierarchy_fd < 0) {
+ die("cannot open cgroup hierarchy %s/%s", parent, name);
+ }
+ // Open the cgroup.procs file.
+ int procs_fd SC_CLEANUP(sc_cleanup_close) = -1;
+ procs_fd = openat(hierarchy_fd, "cgroup.procs", O_WRONLY | O_NOFOLLOW | O_CLOEXEC);
+ if (procs_fd < 0) {
+ die("cannot open file %s/%s/cgroup.procs", parent, name);
+ }
+ // Write the process (task) number to the procs file. Linux task IDs are
+ // limited to 2^29 so a long int is enough to represent it.
+ // See include/linux/threads.h in the kernel source tree for details.
+ char buf[22] = {0}; // 2^64 base10 + 2 for NUL and '-' for long
+ int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid);
+ if (write(procs_fd, buf, n) < n) {
+ die("cannot move process %ld to cgroup hierarchy %s/%s", (long)pid, parent, name);
+ }
+ debug("moved process %ld to cgroup hierarchy %s/%s", (long)pid, parent, name);
+}
+
+static const char *cgroup_dir = "/sys/fs/cgroup";
+
+// from statfs(2)
+#ifndef CGRUOP2_SUPER_MAGIC
+#define CGROUP2_SUPER_MAGIC 0x63677270
+#endif
+
+// Detect if we are running in cgroup v2 unified mode (as opposed to
+// hybrid or legacy) The algorithm is described in
+// https://systemd.io/CGROUP_DELEGATION.html
+bool sc_cgroup_is_v2() {
+ static bool did_warn = false;
+ struct statfs buf;
+
+ if (statfs(cgroup_dir, &buf) != 0) {
+ if (errno == ENOENT) {
+ return false;
+ }
+ die("cannot statfs %s", cgroup_dir);
+ }
+ if (buf.f_type == CGROUP2_SUPER_MAGIC) {
+ if (!did_warn) {
+ fprintf(stderr, "WARNING: cgroup v2 is not fully supported yet, proceeding with partial confinement\n");
+ did_warn = true;
+ }
+ return true;
+ }
+ return false;
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.h 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.h 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SC_CGROUP_SUPPORT_H
+#define SC_CGROUP_SUPPORT_H
+
+#include
+#include
+
+/**
+ * sc_cgroup_create_and_join joins, perhaps creating, a cgroup hierarchy.
+ *
+ * The code assumes that an existing hierarchy rooted at "parent". It follows
+ * up with a sub-hierarchy called "name", creating it if necessary. The created
+ * sub-hierarchy is made to belong to root.root and the specified process is
+ * moved there.
+ **/
+void sc_cgroup_create_and_join(const char *parent, const char *name, pid_t pid);
+
+/**
+ * sc_cgroup_is_v2() returns true if running on cgroups v2
+ *
+ **/
+bool sc_cgroup_is_v2(void);
+
+#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.c 2020-06-05 13:13:49.000000000 +0000
@@ -56,8 +56,3 @@
return SC_DISTRO_CLASSIC;
}
}
-
-bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name)
-{
- return distro != SC_DISTRO_CORE16 || !sc_streq(base_snap_name, "core");
-}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.h 2020-06-05 13:13:49.000000000 +0000
@@ -30,6 +30,4 @@
sc_distro sc_classify_distro(void);
-bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name);
-
#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -176,19 +176,7 @@
g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER);
}
-static void test_should_use_normal_mode(void)
-{
- g_assert_false(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core"));
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE_OTHER, "core"));
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core"));
-
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core18"));
- g_assert_true(sc_should_use_normal_mode
- (SC_DISTRO_CORE_OTHER, "core18"));
- g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core18"));
-}
-
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/classic/on-classic", test_is_on_classic);
g_test_add_func("/classic/on-classic-with-long-line",
@@ -199,6 +187,4 @@
g_test_add_func("/classic/on-fedora-base", test_is_on_fedora_base);
g_test_add_func("/classic/on-fedora-ws", test_is_on_fedora_ws);
g_test_add_func("/classic/on-custom-base", test_is_on_custom_base);
- g_test_add_func("/classic/should-use-normal-mode",
- test_should_use_normal_mode);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.c 2020-06-05 13:13:49.000000000 +0000
@@ -22,8 +22,9 @@
void sc_cleanup_string(char **ptr)
{
- if (ptr != NULL) {
+ if (ptr != NULL && *ptr != NULL) {
free(*ptr);
+ *ptr = NULL;
}
}
@@ -31,6 +32,7 @@
{
if (ptr != NULL && *ptr != NULL) {
fclose(*ptr);
+ *ptr = NULL;
}
}
@@ -38,6 +40,7 @@
{
if (ptr != NULL && *ptr != NULL) {
endmntent(*ptr);
+ *ptr = NULL;
}
}
@@ -45,6 +48,7 @@
{
if (ptr != NULL && *ptr != NULL) {
closedir(*ptr);
+ *ptr = NULL;
}
}
@@ -52,5 +56,6 @@
{
if (ptr != NULL && *ptr != -1) {
close(*ptr);
+ *ptr = -1;
}
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.h 2020-06-05 13:13:49.000000000 +0000
@@ -34,40 +34,45 @@
/**
* Free a dynamically allocated string.
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_string))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_string(char **ptr);
/**
* Close an open file.
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_file))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_file(FILE ** ptr);
/**
* Close an open file with endmntent(3)
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_endmntent))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_endmntent(FILE ** ptr);
/**
* Close an open directory with closedir(3)
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_closedir))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is NULL.
**/
void sc_cleanup_closedir(DIR ** ptr);
/**
* Close an open file descriptor with close(2)
*
- * This function is designed to be used with
- * __attribute__((cleanup(sc_cleanup_close))).
+ * This function is designed to be used with SC_CLEANUP() macro.
+ * The variable MUST be initialized for correct operation.
+ * The safe initialisation value is -1.
**/
void sc_cleanup_close(int *ptr);
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -20,6 +20,8 @@
#include
+#include
+
static int called = 0;
static void cleanup_fn(int *ptr)
@@ -38,7 +40,94 @@
g_assert_cmpint(called, ==, 1);
}
-static void __attribute__ ((constructor)) init(void)
+static void test_cleanup_string(void)
+{
+ /* It is safe to use with a NULL pointer to a string. */
+ sc_cleanup_string(NULL);
+
+ /* It is safe to use with a NULL string. */
+ char *str = NULL;
+ sc_cleanup_string(&str);
+
+ /* It is safe to use with a non-NULL string. */
+ str = malloc(1);
+ g_assert_nonnull(str);
+ sc_cleanup_string(&str);
+ g_assert_null(str);
+}
+
+static void test_cleanup_file(void)
+{
+ /* It is safe to use with a NULL pointer to a FILE. */
+ sc_cleanup_file(NULL);
+
+ /* It is safe to use with a NULL FILE. */
+ FILE *f = NULL;
+ sc_cleanup_file(&f);
+
+ /* It is safe to use with a non-NULL FILE. */
+ f = fmemopen(NULL, 10, "rt");
+ g_assert_nonnull(f);
+ sc_cleanup_file(&f);
+ g_assert_null(f);
+}
+
+static void test_cleanup_endmntent(void)
+{
+ /* It is safe to use with a NULL pointer to a FILE. */
+ sc_cleanup_endmntent(NULL);
+
+ /* It is safe to use with a NULL FILE. */
+ FILE *f = NULL;
+ sc_cleanup_endmntent(&f);
+
+ /* It is safe to use with a non-NULL FILE. */
+ f = setmntent("/etc/fstab", "rt");
+ g_assert_nonnull(f);
+ sc_cleanup_endmntent(&f);
+ g_assert_null(f);
+}
+
+static void test_cleanup_closedir(void)
+{
+ /* It is safe to use with a NULL pointer to a DIR. */
+ sc_cleanup_closedir(NULL);
+
+ /* It is safe to use with a NULL DIR. */
+ DIR *d = NULL;
+ sc_cleanup_closedir(&d);
+
+ /* It is safe to use with a non-NULL DIR. */
+ d = opendir(".");
+ g_assert_nonnull(d);
+ sc_cleanup_closedir(&d);
+ g_assert_null(d);
+}
+
+static void test_cleanup_close(void)
+{
+ /* It is safe to use with a NULL pointer to an int. */
+ sc_cleanup_close(NULL);
+
+ /* It is safe to use with a -1 file descriptor. */
+ int fd = -1;
+ sc_cleanup_close(&fd);
+
+ /* It is safe to use with a non-invalid file descriptor. */
+ /* Timerfd is a simple to use and widely available object that can be
+ * created and closed without interacting with the filesystem. */
+ fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
+ g_assert_cmpint(fd, !=, -1);
+ sc_cleanup_close(&fd);
+ g_assert_cmpint(fd, ==, -1);
+}
+
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/cleanup/sanity", test_cleanup_sanity);
+ g_test_add_func("/cleanup/string", test_cleanup_string);
+ g_test_add_func("/cleanup/file", test_cleanup_file);
+ g_test_add_func("/cleanup/endmntent", test_cleanup_endmntent);
+ g_test_add_func("/cleanup/closedir", test_cleanup_closedir);
+ g_test_add_func("/cleanup/close", test_cleanup_close);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.c 2020-06-05 13:13:49.000000000 +0000
@@ -26,20 +26,10 @@
#include
#include
-struct sc_error {
- // Error domain defines a scope for particular error codes.
- const char *domain;
- // Code differentiates particular errors for the programmer.
- // The code may be zero if the particular meaning is not relevant.
- int code;
- // Message carries a formatted description of the problem.
- char *msg;
-};
-
-static struct sc_error *sc_error_initv(const char *domain, int code,
- const char *msgfmt, va_list ap)
+static sc_error *sc_error_initv(const char *domain, int code,
+ const char *msgfmt, va_list ap)
{
- struct sc_error *err = calloc(1, sizeof *err);
+ sc_error *err = calloc(1, sizeof *err);
if (err == NULL) {
die("cannot allocate memory for error object");
}
@@ -51,28 +41,45 @@
return err;
}
-struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt,
- ...)
+sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, ...)
+{
+ va_list ap;
+ va_start(ap, msgfmt);
+ sc_error *err = sc_error_initv(domain, code, msgfmt, ap);
+ va_end(ap);
+ return err;
+}
+
+sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, ...)
+{
+ va_list ap;
+ va_start(ap, msgfmt);
+ sc_error *err = sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap);
+ va_end(ap);
+ return err;
+}
+
+sc_error *sc_error_init_simple(const char *msgfmt, ...)
{
va_list ap;
va_start(ap, msgfmt);
- struct sc_error *err = sc_error_initv(domain, code, msgfmt, ap);
+ sc_error *err = sc_error_initv(SC_LIBSNAP_DOMAIN,
+ SC_UNSPECIFIED_ERROR, msgfmt, ap);
va_end(ap);
return err;
}
-struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt,
- ...)
+sc_error *sc_error_init_api_misuse(const char *msgfmt, ...)
{
va_list ap;
va_start(ap, msgfmt);
- struct sc_error *err =
- sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap);
+ sc_error *err = sc_error_initv(SC_LIBSNAP_DOMAIN,
+ SC_API_MISUSE, msgfmt, ap);
va_end(ap);
return err;
}
-const char *sc_error_domain(struct sc_error *err)
+const char *sc_error_domain(sc_error * err)
{
if (err == NULL) {
die("cannot obtain error domain from NULL error");
@@ -80,7 +87,7 @@
return err->domain;
}
-int sc_error_code(struct sc_error *err)
+int sc_error_code(sc_error * err)
{
if (err == NULL) {
die("cannot obtain error code from NULL error");
@@ -88,7 +95,7 @@
return err->code;
}
-const char *sc_error_msg(struct sc_error *err)
+const char *sc_error_msg(sc_error * err)
{
if (err == NULL) {
die("cannot obtain error message from NULL error");
@@ -96,7 +103,7 @@
return err->msg;
}
-void sc_error_free(struct sc_error *err)
+void sc_error_free(sc_error * err)
{
if (err != NULL) {
free(err->msg);
@@ -105,36 +112,36 @@
}
}
-void sc_cleanup_error(struct sc_error **ptr)
+void sc_cleanup_error(sc_error ** ptr)
{
sc_error_free(*ptr);
*ptr = NULL;
}
-void sc_die_on_error(struct sc_error *error)
+void sc_die_on_error(sc_error * error)
{
if (error != NULL) {
if (strcmp(sc_error_domain(error), SC_ERRNO_DOMAIN) == 0) {
- // Set errno just before the call to die() as it is used internally
- errno = sc_error_code(error);
- die("%s", sc_error_msg(error));
+ fprintf(stderr, "%s: %s\n", sc_error_msg(error), strerror(sc_error_code(error)));
} else {
- errno = 0;
- die("%s", sc_error_msg(error));
+ fprintf(stderr, "%s\n", sc_error_msg(error));
}
+ sc_error_free(error);
+ exit(1);
}
}
-void sc_error_forward(struct sc_error **recipient, struct sc_error *error)
+int sc_error_forward(sc_error ** recipient, sc_error * error)
{
if (recipient != NULL) {
*recipient = error;
} else {
sc_die_on_error(error);
}
+ return error != NULL ? -1 : 0;
}
-bool sc_error_match(struct sc_error *error, const char *domain, int code)
+bool sc_error_match(sc_error * error, const char *domain, int code)
{
if (domain == NULL) {
die("cannot match error to a NULL domain");
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.h 2020-06-05 13:13:49.000000000 +0000
@@ -43,9 +43,17 @@
**/
/**
- * Opaque error structure.
+ * Error structure.
**/
-struct sc_error;
+typedef struct sc_error {
+ // Error domain defines a scope for particular error codes.
+ const char *domain;
+ // Code differentiates particular errors for the programmer.
+ // The code may be zero if the particular meaning is not relevant.
+ int code;
+ // Message carries a formatted description of the problem.
+ char *msg;
+} sc_error;
/**
* Error domain for errors related to system errno.
@@ -53,6 +61,21 @@
#define SC_ERRNO_DOMAIN "errno"
/**
+ * Error domain for errors in the libsnap-confine-private library.
+ **/
+#define SC_LIBSNAP_DOMAIN "libsnap-confine-private"
+
+/** sc_libsnap_error represents distinct error codes used by libsnap-confine-private library. */
+typedef enum sc_libsnap_error {
+ /** SC_UNSPECIFIED_ERROR indicates an error not worthy of a distinct code. */
+ SC_UNSPECIFIED_ERROR = 0,
+ /** SC_API_MISUSE indicates that public API was called incorrectly. */
+ SC_API_MISUSE,
+ /** SC_BUG indicates that private API was called incorrectly. */
+ SC_BUG,
+} sc_libsnap_error;
+
+/**
* Initialize a new error object.
*
* The domain is a cookie-like string that allows the caller to distinguish
@@ -62,10 +85,29 @@
*
* This function calls die() in case of memory allocation failure.
**/
-__attribute__ ((warn_unused_result,
- format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL))
-struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt,
- ...);
+__attribute__((warn_unused_result,
+ format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, ...);
+
+/**
+ * Initialize an unspecified error with formatted message.
+ *
+ * This is just syntactic sugar for sc_error_init(SC_LIBSNAP_ERROR,
+ * SC_UNSPECIFIED_ERROR, msgfmt, ...) which is repeated often.
+ **/
+__attribute__((warn_unused_result,
+ format(printf, 1, 2) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init_simple(const char *msgfmt, ...);
+
+/**
+ * Initialize an API misuse error with formatted message.
+ *
+ * This is just syntactic sugar for sc_error_init(SC_LIBSNAP_DOMAIN,
+ * SC_API_MISUSE, msgfmt, ...) which is repeated often.
+ **/
+__attribute__((warn_unused_result,
+ format(printf, 1, 2) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init_api_misuse(const char *msgfmt, ...);
/**
* Initialize an errno-based error.
@@ -75,10 +117,9 @@
*
* This function calls die() in case of memory allocation failure.
**/
-__attribute__ ((warn_unused_result,
- format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL))
-struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt,
- ...);
+__attribute__((warn_unused_result,
+ format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, ...);
/**
* Get the error domain out of an error object.
@@ -86,8 +127,8 @@
* The error domain acts as a namespace for error codes.
* No change of ownership takes place.
**/
-__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL))
-const char *sc_error_domain(struct sc_error *err);
+__attribute__((warn_unused_result SC_APPEND_RETURNS_NONNULL))
+const char *sc_error_domain(sc_error * err);
/**
* Get the error code out of an error object.
@@ -99,8 +140,8 @@
* can rely on programmatically. This can be used to return an error message
* without having to allocate a distinct code for each one.
**/
-__attribute__ ((warn_unused_result))
-int sc_error_code(struct sc_error *err);
+__attribute__((warn_unused_result))
+int sc_error_code(sc_error * err);
/**
* Get the error message out of an error object.
@@ -108,15 +149,15 @@
* The error message is bound to the life-cycle of the error object.
* No change of ownership takes place.
**/
-__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL))
-const char *sc_error_msg(struct sc_error *err);
+__attribute__((warn_unused_result SC_APPEND_RETURNS_NONNULL))
+const char *sc_error_msg(sc_error * err);
/**
* Free an error object.
*
* The error object can be NULL.
**/
-void sc_error_free(struct sc_error *error);
+void sc_error_free(sc_error * error);
/**
* Cleanup an error with sc_error_free()
@@ -124,8 +165,8 @@
* This function is designed to be used with
* __attribute__((cleanup(sc_cleanup_error))).
**/
-__attribute__ ((nonnull))
-void sc_cleanup_error(struct sc_error **ptr);
+__attribute__((nonnull))
+void sc_cleanup_error(sc_error ** ptr);
/**
*
@@ -136,7 +177,7 @@
* The error message is derived from the data in the error, using the special
* errno domain to provide additional information if that is available.
**/
-void sc_die_on_error(struct sc_error *error);
+void sc_die_on_error(sc_error * error);
/**
* Forward an error to the caller.
@@ -146,10 +187,14 @@
* sc_die_on_error() is called as a safety measure.
*
* Change of ownership takes place and the error is now stored in the recipient.
+ *
+ * The return value -1 if error is non-NULL and 0 otherwise. The return value
+ * makes it convenient to `return sc_error_forward(err_out, err);` as the last
+ * line of a function.
**/
// NOTE: There's no nonnull(1) attribute as the recipient *can* be NULL. With
// the attribute in place GCC optimizes some things out and tests fail.
-void sc_error_forward(struct sc_error **recipient, struct sc_error *error);
+int sc_error_forward(sc_error ** recipient, sc_error * error);
/**
* Check if a given error matches the specified domain and code.
@@ -157,7 +202,7 @@
* It is okay to match a NULL error, the function simply returns false in that
* case. The domain cannot be NULL though.
**/
-__attribute__ ((warn_unused_result))
-bool sc_error_match(struct sc_error *error, const char *domain, int code);
+__attribute__((warn_unused_result))
+bool sc_error_match(sc_error * error, const char *domain, int code);
#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -49,6 +49,34 @@
g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire");
}
+static void test_sc_error_init_simple(void)
+{
+ struct sc_error *err;
+ // Create an error
+ err = sc_error_init_simple("hello %s", "errors");
+ g_assert_nonnull(err);
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+
+ // Inspect the exposed attributes
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, 0);
+ g_assert_cmpstr(sc_error_msg(err), ==, "hello errors");
+}
+
+static void test_sc_error_init_api_misuse(void)
+{
+ struct sc_error *err;
+ // Create an error
+ err = sc_error_init_api_misuse("foo cannot be %d", 42);
+ g_assert_nonnull(err);
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+
+ // Inspect the exposed attributes
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
+ g_assert_cmpstr(sc_error_msg(err), ==, "foo cannot be 42");
+}
+
static void test_sc_error_cleanup(void)
{
// Check that sc_error_cleanup() is safe to use.
@@ -161,8 +189,10 @@
// Check that forwarding NULL does exactly that.
struct sc_error *recipient = (void *)0xDEADBEEF;
struct sc_error *err = NULL;
- sc_error_forward(&recipient, err);
+ int rc;
+ rc = sc_error_forward(&recipient, err);
g_assert_null(recipient);
+ g_assert_cmpint(rc, ==, 0);
}
static void test_sc_error_forward__something_somewhere(void)
@@ -170,10 +200,12 @@
// Check that forwarding a real error works OK.
struct sc_error *recipient = NULL;
struct sc_error *err = sc_error_init("domain", 42, "just testing");
+ int rc;
g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
g_assert_nonnull(err);
- sc_error_forward(&recipient, err);
+ rc = sc_error_forward(&recipient, err);
g_assert_nonnull(recipient);
+ g_assert_cmpint(rc, ==, -1);
}
static void test_sc_error_forward__something_nowhere(void)
@@ -227,11 +259,15 @@
g_test_trap_assert_stderr("cannot match error to a NULL domain\n");
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/error/sc_error_init", test_sc_error_init);
g_test_add_func("/error/sc_error_init_from_errno",
test_sc_error_init_from_errno);
+ g_test_add_func("/error/sc_error_init_simple",
+ test_sc_error_init_simple);
+ g_test_add_func("/error/sc_error_init_api_misue",
+ test_sc_error_init_api_misuse);
g_test_add_func("/error/sc_error_cleanup", test_sc_error_cleanup);
g_test_add_func("/error/sc_error_domain/NULL",
test_sc_error_domain__NULL);
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection.h 2020-06-05 13:13:49.000000000 +0000
@@ -37,7 +37,7 @@
struct sc_fault_state;
-typedef bool(*sc_fault_fn) (struct sc_fault_state * state, void *ptr);
+typedef bool (*sc_fault_fn)(struct sc_fault_state * state, void *ptr);
struct sc_fault_state {
int ncalls;
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -57,7 +57,7 @@
sc_reset_faults();
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/fault-injection", test_fault_injection);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.c 2020-06-05 13:13:49.000000000 +0000
@@ -34,15 +34,23 @@
{
const char *file_name;
switch (flag) {
- case SC_PER_USER_MOUNT_NAMESPACE:
+ case SC_FEATURE_PER_USER_MOUNT_NAMESPACE:
file_name = "per-user-mount-namespace";
break;
+ case SC_FEATURE_REFRESH_APP_AWARENESS:
+ file_name = "refresh-app-awareness";
+ break;
+ case SC_FEATURE_PARALLEL_INSTANCES:
+ file_name = "parallel-instances";
+ break;
default:
die("unknown feature flag code %d", flag);
}
int dirfd SC_CLEANUP(sc_cleanup_close) = -1;
- dirfd = open(feature_flag_dir, O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH);
+ dirfd =
+ open(feature_flag_dir,
+ O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH);
if (dirfd < 0 && errno == ENOENT) {
return false;
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.h 2020-06-05 13:13:49.000000000 +0000
@@ -21,7 +21,9 @@
#include
typedef enum sc_feature_flag {
- SC_PER_USER_MOUNT_NAMESPACE,
+ SC_FEATURE_PER_USER_MOUNT_NAMESPACE = 1 << 0,
+ SC_FEATURE_REFRESH_APP_AWARENESS = 1 << 1,
+ SC_FEATURE_PARALLEL_INSTANCES = 1 << 2,
} sc_feature_flag;
/**
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -54,14 +54,14 @@
char subd[PATH_MAX];
sc_must_snprintf(subd, sizeof subd, "%s/absent", d);
sc_mock_feature_flag_dir(subd);
- g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE));
+ g_assert(!sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE));
}
static void test_feature_enabled__missing_file(void)
{
const char *d = sc_testdir();
sc_mock_feature_flag_dir(d);
- g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE));
+ g_assert(!sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE));
}
static void test_feature_enabled__present_file(void)
@@ -72,10 +72,24 @@
sc_must_snprintf(pname, sizeof pname, "%s/per-user-mount-namespace", d);
g_file_set_contents(pname, "", -1, NULL);
- g_assert(sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE));
+ g_assert(sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE));
}
-static void __attribute__ ((constructor)) init(void)
+static void test_feature_parallel_instances(void)
+{
+ const char *d = sc_testdir();
+ sc_mock_feature_flag_dir(d);
+
+ g_assert(!sc_feature_enabled(SC_FEATURE_PARALLEL_INSTANCES));
+
+ char pname[PATH_MAX];
+ sc_must_snprintf(pname, sizeof pname, "%s/parallel-instances", d);
+ g_file_set_contents(pname, "", -1, NULL);
+
+ g_assert(sc_feature_enabled(SC_FEATURE_PARALLEL_INSTANCES));
+}
+
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/feature/missing_dir",
test_feature_enabled__missing_dir);
@@ -83,4 +97,6 @@
test_feature_enabled__missing_file);
g_test_add_func("/feature/present_file",
test_feature_enabled__present_file);
+ g_test_add_func("/feature/parallel_instances",
+ test_feature_parallel_instances);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.c 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "infofile.h"
+
+#include
+#include
+#include
+#include
+
+#include "../libsnap-confine-private/cleanup-funcs.h"
+#include "../libsnap-confine-private/error.h"
+#include "../libsnap-confine-private/string-utils.h"
+#include "../libsnap-confine-private/utils.h"
+
+int sc_infofile_get_key(FILE *stream, const char *key, char **value, sc_error **err_out) {
+ sc_error *err = NULL;
+ size_t line_size = 0;
+ char *line_buf SC_CLEANUP(sc_cleanup_string) = NULL;
+
+ if (stream == NULL) {
+ err = sc_error_init_api_misuse("stream cannot be NULL");
+ goto out;
+ }
+ if (key == NULL) {
+ err = sc_error_init_api_misuse("key cannot be NULL");
+ goto out;
+ }
+ if (value == NULL) {
+ err = sc_error_init_api_misuse("value cannot be NULL");
+ goto out;
+ }
+
+ /* Store NULL in case we don't find the key.
+ * This makes the value always well-defined. */
+ *value = NULL;
+
+ /* This loop advances through subsequent lines. */
+ for (int lineno = 1;; ++lineno) {
+ errno = 0;
+ ssize_t nread = getline(&line_buf, &line_size, stream);
+ if (nread < 0 && errno != 0) {
+ err = sc_error_init_from_errno(errno, "cannot read beyond line %d", lineno);
+ goto out;
+ }
+ if (nread <= 0) {
+ break; /* There is nothing more to read. */
+ }
+ /* NOTE: beyond this line the buffer is never empty (ie, nread > 0). */
+
+ /* Guard against malformed input that may contain NUL bytes that
+ * would confuse the code below. */
+ if (memchr(line_buf, '\0', nread) != NULL) {
+ err = sc_error_init_simple("line %d contains NUL byte", lineno);
+ goto out;
+ }
+ /* Guard against non-strictly formatted input that doesn't contain
+ * trailing newline. */
+ if (line_buf[nread - 1] != '\n') {
+ err = sc_error_init(SC_LIBSNAP_DOMAIN, 0, "line %d does not end with a newline", lineno);
+ goto out;
+ }
+ /* Replace the trailing newline character with the NUL byte. */
+ line_buf[nread - 1] = '\0';
+ /* Guard against malformed input that does not contain '=' byte */
+ char *eq_ptr = memchr(line_buf, '=', nread);
+ if (eq_ptr == NULL) {
+ err = sc_error_init_simple("line %d is not a key=value assignment", lineno);
+ goto out;
+ }
+ /* Guard against malformed input with empty key. */
+ if (eq_ptr == line_buf) {
+ err = sc_error_init_simple("line %d contains empty key", lineno);
+ goto out;
+ }
+ /* Replace the first '=' with string terminator byte. */
+ *eq_ptr = '\0';
+
+ /* If the key matches the one we are looking for, store it and stop scanning. */
+ const char *scanned_key = line_buf;
+ const char *scanned_value = eq_ptr + 1;
+ if (sc_streq(scanned_key, key)) {
+ *value = sc_strdup(scanned_value);
+ break;
+ }
+ }
+
+out:
+ return sc_error_forward(err_out, err);
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.h 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.h 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SNAP_CONFINE_INFOFILE_H
+#define SNAP_CONFINE_INFOFILE_H
+
+#include
+
+#include "../libsnap-confine-private/error.h"
+
+/**
+ * sc_infofile_get_key extracts a single value of a key=value pair from a given
+ * stream.
+ *
+ * On success the return value is zero and err_out, if not NULL, is deferences
+ * and set to NULL. On failure the return value is -1 is and detailed error
+ * information is stored by dereferencing err_out. If an error occurs and
+ * err_out is NULL then the program dies, printing the error message.
+ **/
+int sc_infofile_get_key(FILE *stream, const char *key, char **value, sc_error **err_out);
+
+#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile-test.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "infofile.h"
+#include "infofile.c"
+
+#include
+#include
+
+static void test_infofile_get_key(void) {
+ int rc;
+ sc_error *err;
+
+ char text[] =
+ "key=value\n"
+ "other-key=other-value\n"
+ "dup-key=value-one\n"
+ "dup-key=value-two\n";
+ FILE *stream = fmemopen(text, sizeof text - 1, "r");
+ g_assert_nonnull(stream);
+
+ char *value;
+
+ /* Caller must provide the stream to scan. */
+ rc = sc_infofile_get_key(NULL, "key", &value, &err);
+ g_assert_cmpint(rc, ==, -1);
+ g_assert_nonnull(err);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
+ g_assert_cmpstr(sc_error_msg(err), ==, "stream cannot be NULL");
+ sc_error_free(err);
+
+ /* Caller must provide the key to look for. */
+ rc = sc_infofile_get_key(stream, NULL, &value, &err);
+ g_assert_cmpint(rc, ==, -1);
+ g_assert_nonnull(err);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
+ g_assert_cmpstr(sc_error_msg(err), ==, "key cannot be NULL");
+ sc_error_free(err);
+
+ /* Caller must provide storage for the value. */
+ rc = sc_infofile_get_key(stream, "key", NULL, &err);
+ g_assert_cmpint(rc, ==, -1);
+ g_assert_nonnull(err);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
+ g_assert_cmpstr(sc_error_msg(err), ==, "value cannot be NULL");
+ sc_error_free(err);
+
+ /* Keys that are not found get NULL values. */
+ value = (void *)0xfefefefe;
+ rewind(stream);
+ rc = sc_infofile_get_key(stream, "missing-key", &value, &err);
+ g_assert_cmpint(rc, ==, 0);
+ g_assert_null(err);
+ g_assert_null(value);
+
+ /* Keys that are found get strdup-duplicated values. */
+ value = NULL;
+ rewind(stream);
+ rc = sc_infofile_get_key(stream, "key", &value, &err);
+ g_assert_cmpint(rc, ==, 0);
+ g_assert_null(err);
+ g_assert_nonnull(value);
+ g_assert_cmpstr(value, ==, "value");
+ free(value);
+
+ /* When duplicate keys are present the first value is extracted. */
+ char *dup_value;
+ rewind(stream);
+ rc = sc_infofile_get_key(stream, "dup-key", &dup_value, &err);
+ g_assert_cmpint(rc, ==, 0);
+ g_assert_null(err);
+ g_assert_nonnull(dup_value);
+ g_assert_cmpstr(dup_value, ==, "value-one");
+ free(dup_value);
+
+ fclose(stream);
+
+ /* Key without a value. */
+ char *tricky_value;
+ char tricky1[] = "key\n";
+ stream = fmemopen(tricky1, sizeof tricky1 - 1, "r");
+ g_assert_nonnull(stream);
+ rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
+ g_assert_cmpint(rc, ==, -1);
+ g_assert_nonnull(err);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, 0);
+ g_assert_cmpstr(sc_error_msg(err), ==, "line 1 is not a key=value assignment");
+ g_assert_null(tricky_value);
+ sc_error_free(err);
+ fclose(stream);
+
+ /* Key-value pair with embedded NUL byte. */
+ char tricky2[] = "key=value\0garbage\n";
+ stream = fmemopen(tricky2, sizeof tricky2 - 1, "r");
+ g_assert_nonnull(stream);
+ rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
+ g_assert_cmpint(rc, ==, -1);
+ g_assert_nonnull(err);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, 0);
+ g_assert_cmpstr(sc_error_msg(err), ==, "line 1 contains NUL byte");
+ g_assert_null(tricky_value);
+ sc_error_free(err);
+ fclose(stream);
+
+ /* Key with empty value but without trailing newline. */
+ char tricky3[] = "key=";
+ stream = fmemopen(tricky3, sizeof tricky3 - 1, "r");
+ g_assert_nonnull(stream);
+ rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
+ g_assert_cmpint(rc, ==, -1);
+ g_assert_nonnull(err);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, 0);
+ g_assert_cmpstr(sc_error_msg(err), ==, "line 1 does not end with a newline");
+ g_assert_null(tricky_value);
+ sc_error_free(err);
+ fclose(stream);
+
+ /* Key with empty value with a trailing newline (which is also valid). */
+ char tricky4[] = "key=\n";
+ stream = fmemopen(tricky4, sizeof tricky4 - 1, "r");
+ g_assert_nonnull(stream);
+ rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
+ g_assert_cmpint(rc, ==, 0);
+ g_assert_null(err);
+ g_assert_cmpstr(tricky_value, ==, "");
+ sc_error_free(err);
+ fclose(stream);
+ free(tricky_value);
+
+ /* The equals character alone (key is empty) */
+ char tricky5[] = "=\n";
+ stream = fmemopen(tricky5, sizeof tricky5 - 1, "r");
+ g_assert_nonnull(stream);
+ rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
+ g_assert_cmpint(rc, ==, -1);
+ g_assert_nonnull(err);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, 0);
+ g_assert_cmpstr(sc_error_msg(err), ==, "line 1 contains empty key");
+ g_assert_null(tricky_value);
+ sc_error_free(err);
+ fclose(stream);
+}
+
+static void __attribute__((constructor)) init(void) { g_test_add_func("/infofile/get_key", test_infofile_get_key); }
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking.c 2020-06-05 13:13:49.000000000 +0000
@@ -94,12 +94,14 @@
{
// Create (if required) and open the lock directory.
debug("creating lock directory %s (if missing)", sc_lock_dir);
+ sc_identity old = sc_set_effective_identity(sc_root_group_identity());
if (sc_nonfatal_mkpath(sc_lock_dir, 0755) < 0) {
die("cannot create lock directory %s", sc_lock_dir);
}
debug("opening lock directory %s", sc_lock_dir);
int dir_fd =
open(sc_lock_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW);
+ (void)sc_set_effective_identity(old);
if (dir_fd < 0) {
die("cannot open lock directory");
}
@@ -131,8 +133,10 @@
// Open the lock file and acquire an exclusive lock.
debug("opening lock file: %s/%s", sc_lock_dir, lock_fname);
+ sc_identity old = sc_set_effective_identity(sc_root_group_identity());
lock_fd = openat(dir_fd, lock_fname,
O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600);
+ (void)sc_set_effective_identity(old);
if (lock_fd < 0) {
die("cannot open lock file: %s/%s", sc_lock_dir, lock_fname);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -68,6 +68,11 @@
// Check that locking a namespace actually flock's the mutex with LOCK_EX
static void test_sc_lock_unlock(void)
{
+ if (geteuid() != 0) {
+ g_test_skip("this test only runs as root");
+ return;
+ }
+
const char *lock_dir = sc_test_use_fake_lock_dir();
int fd = sc_lock_generic("foo", 123);
// Construct the name of the lock file
@@ -95,6 +100,11 @@
// Check that holding a lock is properly detected.
static void test_sc_verify_snap_lock__locked(void)
{
+ if (geteuid() != 0) {
+ g_test_skip("this test only runs as root");
+ return;
+ }
+
(void)sc_test_use_fake_lock_dir();
int fd = sc_lock_snap("foo");
sc_verify_snap_lock("foo");
@@ -104,6 +114,11 @@
// Check that holding a lock is properly detected.
static void test_sc_verify_snap_lock__unlocked(void)
{
+ if (geteuid() != 0) {
+ g_test_skip("this test only runs as root");
+ return;
+ }
+
(void)sc_test_use_fake_lock_dir();
if (g_test_subprocess()) {
sc_verify_snap_lock("foo");
@@ -117,6 +132,11 @@
static void test_sc_enable_sanity_timeout(void)
{
+ if (geteuid() != 0) {
+ g_test_skip("this test only runs as root");
+ return;
+ }
+
if (g_test_subprocess()) {
sc_enable_sanity_timeout();
debug("waiting...");
@@ -128,10 +148,11 @@
g_test_trap_subprocess(NULL, 1 * G_USEC_PER_SEC,
G_TEST_SUBPROCESS_INHERIT_STDERR);
g_test_trap_assert_failed();
- g_test_trap_assert_stderr ("sanity timeout expired: Interrupted system call\n");
+ g_test_trap_assert_stderr
+ ("sanity timeout expired: Interrupted system call\n");
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock);
g_test_add_func("/locking/sc_enable_sanity_timeout",
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.c 2020-06-05 13:13:49.000000000 +0000
@@ -17,6 +17,7 @@
#include "mountinfo.h"
#include
+#include
#include
#include
#include
@@ -43,35 +44,34 @@
* (10) mount source: filesystem specific information or "none"
* (11) super options: per super block options
**/
-static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
- __attribute__ ((nonnull(1)));
+static sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
+ __attribute__((nonnull(1)));
/**
* Free a sc_mountinfo structure and all its entries.
**/
-static void sc_free_mountinfo(struct sc_mountinfo *info)
- __attribute__ ((nonnull(1)));
+static void sc_free_mountinfo(sc_mountinfo * info)
+ __attribute__((nonnull(1)));
/**
* Free a sc_mountinfo entry.
**/
-static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry)
- __attribute__ ((nonnull(1)));
+static void sc_free_mountinfo_entry(sc_mountinfo_entry * entry)
+ __attribute__((nonnull(1)));
-struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info)
+sc_mountinfo_entry *sc_first_mountinfo_entry(sc_mountinfo * info)
{
return info->first;
}
-struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry
- *entry)
+sc_mountinfo_entry *sc_next_mountinfo_entry(sc_mountinfo_entry * entry)
{
return entry->next;
}
-struct sc_mountinfo *sc_parse_mountinfo(const char *fname)
+sc_mountinfo *sc_parse_mountinfo(const char *fname)
{
- struct sc_mountinfo *info = calloc(1, sizeof *info);
+ sc_mountinfo *info = calloc(1, sizeof *info);
if (info == NULL) {
return NULL;
}
@@ -86,7 +86,7 @@
}
char *line SC_CLEANUP(sc_cleanup_string) = NULL;
size_t line_size = 0;
- struct sc_mountinfo_entry *entry, *last = NULL;
+ sc_mountinfo_entry *entry, *last = NULL;
for (;;) {
errno = 0;
if (getline(&line, &line_size, f) == -1) {
@@ -112,7 +112,7 @@
}
static void show_buffers(const char *line, int offset,
- struct sc_mountinfo_entry *entry)
+ sc_mountinfo_entry * entry)
{
#ifdef MOUNTINFO_DEBUG
fprintf(stderr, "Input buffer (first), with offset arrow\n");
@@ -127,7 +127,7 @@
fprintf(stderr, ">%s<\n", line);
fputc('>', stderr);
- for (int i = 0; i < strlen(line); ++i) {
+ for (size_t i = 0; i < strlen(line); ++i) {
int c = entry->line_buf[i];
fputc(c == 0 ? '@' : c == 1 ? '#' : c, stderr);
}
@@ -135,37 +135,98 @@
fputc('\n', stderr);
fputc('>', stderr);
- for (int i = 0; i < strlen(line); ++i)
+ for (size_t i = 0; i < strlen(line); ++i)
fputc('=', stderr);
fputc('<', stderr);
fputc('\n', stderr);
#endif // MOUNTINFO_DEBUG
}
-static char *parse_next_string_field(struct sc_mountinfo_entry *entry,
- const char *line, int *offset)
+static bool is_octal_digit(char c)
{
- int offset_delta = 0;
- char *field = &entry->line_buf[0] + *offset;
- if (line[*offset] == ' ') {
- // Special case for empty fields which cannot be parsed with %s.
- *field = '\0';
- *offset += 1;
- } else {
- int nscanned =
- sscanf(line + *offset, "%s%n", field, &offset_delta);
- if (nscanned != 1)
- return NULL;
- *offset += offset_delta;
- if (line[*offset] == ' ') {
- *offset += 1;
+ return c >= '0' && c <= '7';
+}
+
+static char *parse_next_string_field(sc_mountinfo_entry * entry,
+ const char *line, size_t *offset)
+{
+ const char *input = &line[*offset];
+ char *output = &entry->line_buf[*offset];
+ size_t input_idx = 0; // reading index
+ size_t output_idx = 0; // writing index
+
+ // Scan characters until we run out of memory to scan or we find a
+ // space. The kernel uses simple octal escape sequences for the
+ // following: space, tab, newline, backwards slash. Everything else is
+ // copied verbatim.
+ for (;;) {
+ int c = input[input_idx];
+ if (c == '\0') {
+ // The string is over before we see anything then
+ // return NULL. This is an indication of end-of-input
+ // to the caller.
+ if (output_idx == 0) {
+ return NULL;
+ }
+ // The scanned line is NUL terminated. This ensures that the
+ // terminator is copied to the output buffer.
+ output[output_idx] = '\0';
+ // NOTE: we must not advance the reading index since we
+ // reached the end of the buffer.
+ break;
+ } else if (c == ' ') {
+ // Fields are space delimited or end-of-string terminated.
+ // Represent either as the end-of-string marker, skip over it,
+ // and stop parsing by terminating the output, then
+ // breaking out of the loop but advancing the reading
+ // index which is needed for subsequent calls.
+ output[output_idx] = '\0';
+ input_idx++;
+ break;
+ } else if (c == '\\') {
+ // Three *more* octal digits required for the escape
+ // sequence. For reference see mangle_path() in
+ // fs/seq_file.c. Note that is_octal_digit returns
+ // false on the string terminator character NUL and the
+ // short-circuiting behavior of && makes this check
+ // correct even if '\\' is the last character of the
+ // string.
+ const char *s = &input[input_idx];
+ if (is_octal_digit(s[1]) && is_octal_digit(s[2])
+ && is_octal_digit(s[3])) {
+ // Unescape the octal value encoded in s[1],
+ // s[2] and s[3]. Because we are working with
+ // byte values there are no issues related to
+ // byte order.
+ output[output_idx++] =
+ ((s[1] - '0') << 6) |
+ ((s[2] - '0') << 3) | ((s[3] - '0'));
+ // Advance the reading index by the length of the escape
+ // sequence.
+ input_idx += 4;
+ } else {
+ // Partial escape sequence, copy verbatim and
+ // continue (since we don't use this).
+ output[output_idx++] = c;
+ input_idx++;
+ }
+ } else {
+ // All other characters are simply copied verbatim.
+ output[output_idx++] = c;
+ input_idx++;
}
}
+ *offset += input_idx;
+#ifdef MOUNTINFO_DEBUG
+ fprintf(stderr,
+ "\nscanned: >%s< (%zd bytes), input idx: %zd, output idx: %zd\n",
+ output, strlen(output), input_idx, output_idx);
+#endif
show_buffers(line, *offset, entry);
- return field;
+ return output;
}
-static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
+static sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line)
{
// NOTE: the sc_mountinfo structure is allocated along with enough extra
// storage to hold the whole line we are parsing. This is used as backing
@@ -189,8 +250,7 @@
//
// If MOUNTINFO_DEBUG is defined then extra debugging is printed to stderr
// and this allows for visual analysis of what is going on.
- struct sc_mountinfo_entry *entry =
- calloc(1, sizeof *entry + strlen(line) + 1);
+ sc_mountinfo_entry *entry = calloc(1, sizeof *entry + strlen(line) + 1);
if (entry == NULL) {
return NULL;
}
@@ -199,14 +259,15 @@
// by show_buffers() below. This is "unaltered" memory.
memset(entry->line_buf, 1, strlen(line));
#endif // MOUNTINFO_DEBUG
- int nscanned;
- int offset_delta, offset = 0;
+ int nscanned, initial_offset = 0;
+ size_t offset = 0;
nscanned = sscanf(line, "%d %d %u:%u %n",
&entry->mount_id, &entry->parent_id,
- &entry->dev_major, &entry->dev_minor, &offset_delta);
+ &entry->dev_major, &entry->dev_minor,
+ &initial_offset);
if (nscanned != 4)
goto fail;
- offset += offset_delta;
+ offset += initial_offset;
show_buffers(line, offset, entry);
@@ -243,14 +304,13 @@
if ((entry->super_opts =
parse_next_string_field(entry, line, &offset)) == NULL)
goto fail;
- show_buffers(line, offset, entry);
return entry;
fail:
free(entry);
return NULL;
}
-void sc_cleanup_mountinfo(struct sc_mountinfo **ptr)
+void sc_cleanup_mountinfo(sc_mountinfo ** ptr)
{
if (*ptr != NULL) {
sc_free_mountinfo(*ptr);
@@ -258,9 +318,9 @@
}
}
-static void sc_free_mountinfo(struct sc_mountinfo *info)
+static void sc_free_mountinfo(sc_mountinfo * info)
{
- struct sc_mountinfo_entry *entry, *next;
+ sc_mountinfo_entry *entry, *next;
for (entry = info->first; entry != NULL; entry = next) {
next = entry->next;
sc_free_mountinfo_entry(entry);
@@ -268,7 +328,7 @@
free(info);
}
-static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry)
+static void sc_free_mountinfo_entry(sc_mountinfo_entry * entry)
{
free(entry);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.h 2020-06-05 13:13:49.000000000 +0000
@@ -18,16 +18,9 @@
#define SNAP_CONFINE_MOUNTINFO_H
/**
- * Structure describing entire /proc/self/sc_mountinfo file
- **/
-struct sc_mountinfo {
- struct sc_mountinfo_entry *first;
-};
-
-/**
* Structure describing a single entry in /proc/self/sc_mountinfo
**/
-struct sc_mountinfo_entry {
+typedef struct sc_mountinfo_entry {
/**
* The mount identifier of a given mount entry.
**/
@@ -91,7 +84,14 @@
// along with the structure itself and does not need to be freed
// separately.
char line_buf[0];
-};
+} sc_mountinfo_entry;
+
+/**
+ * Structure describing entire /proc/self/sc_mountinfo file
+ **/
+typedef struct sc_mountinfo {
+ sc_mountinfo_entry *first;
+} sc_mountinfo;
/**
* Parse a file in according to sc_mountinfo syntax.
@@ -100,7 +100,7 @@
* implicitly parse /proc/self/sc_mountinfo, that is the mount information
* associated with the current process.
**/
-struct sc_mountinfo *sc_parse_mountinfo(const char *fname);
+sc_mountinfo *sc_parse_mountinfo(const char *fname);
/**
* Free a sc_mountinfo structure.
@@ -108,8 +108,8 @@
* This function is designed to be used with __attribute__((cleanup)) so it
* takes a pointer to the freed object (which is also a pointer).
**/
-void sc_cleanup_mountinfo(struct sc_mountinfo **ptr)
- __attribute__ ((nonnull(1)));
+void sc_cleanup_mountinfo(sc_mountinfo ** ptr)
+ __attribute__((nonnull(1)));
/**
* Get the first sc_mountinfo entry.
@@ -118,8 +118,8 @@
* returned value is bound to the lifecycle of the whole sc_mountinfo structure
* and should not be freed explicitly.
**/
-struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info)
- __attribute__ ((nonnull(1)));
+sc_mountinfo_entry *sc_first_mountinfo_entry(sc_mountinfo * info)
+ __attribute__((nonnull(1)));
/**
* Get the next sc_mountinfo entry.
@@ -128,8 +128,7 @@
* was the last entry. The returned value is bound to the lifecycle of the
* whole sc_mountinfo structure and should not be freed explicitly.
**/
-struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry
- *entry)
- __attribute__ ((nonnull(1)));
+sc_mountinfo_entry *sc_next_mountinfo_entry(sc_mountinfo_entry * entry)
+ __attribute__((nonnull(1)));
#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -24,7 +24,7 @@
{
const char *line =
"19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 19);
@@ -48,7 +48,7 @@
{
const char *line =
"104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 104);
@@ -69,7 +69,7 @@
{
const char *line =
"256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 256);
@@ -89,7 +89,7 @@
static void test_parse_mountinfo_entry__garbage(void)
{
const char *line = "256 104 0:3";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_null(entry);
}
@@ -97,7 +97,7 @@
{
const char *line =
"1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 1);
@@ -118,7 +118,7 @@
{
const char *line =
"1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 1);
@@ -139,7 +139,7 @@
{
const char *line =
"1 2 3:4 root mount-dir mount-opts tag:1 tag:2 tag:3 tag:4 - fs-type mount-source super-opts";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 1);
@@ -160,7 +160,7 @@
{
const char *line =
"304 301 0:45 / /snap/test-snapd-content-advanced-plug/x1 rw,relatime - tmpfs rw";
- struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
g_assert_nonnull(entry);
g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
g_assert_cmpint(entry->mount_id, ==, 304);
@@ -178,7 +178,85 @@
g_assert_null(entry->next);
}
-static void __attribute__ ((constructor)) init(void)
+static void test_parse_mountinfo_entry__octal_escaping(void)
+{
+ const char *line;
+ struct sc_mountinfo_entry *entry;
+
+ // The kernel escapes spaces as \040
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\040path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky path");
+
+ // kernel escapes newlines as \012
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\012path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky\npath");
+
+ // kernel escapes tabs as \011
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\011path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky\tpath");
+
+ // kernel escapes forward slashes as \057
+ line = "2 1 0:54 / /tmp rw - tmpfs tricky\\057path rw";
+ entry = sc_parse_mountinfo_entry(line);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_nonnull(entry);
+ g_assert_cmpstr(entry->mount_source, ==, "tricky/path");
+}
+
+static void test_parse_mountinfo_entry__broken_octal_escaping(void)
+{
+ // Invalid octal escape sequences are left intact.
+ const char *line =
+ "2074 27 0:54 / /tmp/strange-dir rw,relatime shared:1039 - tmpfs no\\888thing rw\\";
+ struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 2074);
+ g_assert_cmpint(entry->parent_id, ==, 27);
+ g_assert_cmpint(entry->dev_major, ==, 0);
+ g_assert_cmpint(entry->dev_minor, ==, 54);
+ g_assert_cmpstr(entry->root, ==, "/");
+ g_assert_cmpstr(entry->mount_dir, ==, "/tmp/strange-dir");
+ g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime");
+ g_assert_cmpstr(entry->optional_fields, ==, "shared:1039");
+ g_assert_cmpstr(entry->fs_type, ==, "tmpfs");
+ g_assert_cmpstr(entry->mount_source, ==, "no\\888thing");
+ g_assert_cmpstr(entry->super_opts, ==, "rw\\");
+ g_assert_null(entry->next);
+}
+
+static void test_parse_mountinfo_entry__unescaped_whitespace(void)
+{
+ // The kernel does not escape '\r'
+ const char *line =
+ "2074 27 0:54 / /tmp/strange\rdir rw,relatime shared:1039 - tmpfs tmpfs rw";
+ struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line);
+ g_assert_nonnull(entry);
+ g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry);
+ g_assert_cmpint(entry->mount_id, ==, 2074);
+ g_assert_cmpint(entry->parent_id, ==, 27);
+ g_assert_cmpint(entry->dev_major, ==, 0);
+ g_assert_cmpint(entry->dev_minor, ==, 54);
+ g_assert_cmpstr(entry->root, ==, "/");
+ g_assert_cmpstr(entry->mount_dir, ==, "/tmp/strange\rdir");
+ g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime");
+ g_assert_cmpstr(entry->optional_fields, ==, "shared:1039");
+ g_assert_cmpstr(entry->fs_type, ==, "tmpfs");
+ g_assert_cmpstr(entry->mount_source, ==, "tmpfs");
+ g_assert_cmpstr(entry->super_opts, ==, "rw");
+ g_assert_null(entry->next);
+}
+
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/mountinfo/parse_mountinfo_entry/sysfs",
test_parse_mountinfo_entry__sysfs);
@@ -197,4 +275,11 @@
g_test_add_func
("/mountinfo/parse_mountinfo_entry/empty_source",
test_parse_mountinfo_entry__empty_source);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/octal_escaping",
+ test_parse_mountinfo_entry__octal_escaping);
+ g_test_add_func
+ ("/mountinfo/parse_mountinfo_entry/broken_octal_escaping",
+ test_parse_mountinfo_entry__broken_octal_escaping);
+ g_test_add_func("/mountinfo/parse_mountinfo_entry/unescaped_whitespace",
+ test_parse_mountinfo_entry__unescaped_whitespace);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mount-opt-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mount-opt-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mount-opt-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mount-opt-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -319,7 +319,7 @@
}
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str);
g_test_add_func("/mount/sc_mount_cmd", test_sc_mount_cmd);
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.c 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "panic.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+static sc_panic_exit_fn panic_exit_fn = NULL;
+static sc_panic_msg_fn panic_msg_fn = NULL;
+
+void sc_panic(const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ sc_panicv(fmt, ap);
+ va_end(ap);
+}
+
+void sc_panicv(const char *fmt, va_list ap) {
+ int errno_copy = errno;
+
+ if (panic_msg_fn != NULL) {
+ panic_msg_fn(fmt, ap, errno_copy);
+ } else {
+ vfprintf(stderr, fmt, ap);
+ if (errno != 0) {
+ fprintf(stderr, ": %s\n", strerror(errno_copy));
+ } else {
+ fprintf(stderr, "\n");
+ }
+ }
+
+ if (panic_exit_fn != NULL) {
+ panic_exit_fn();
+ }
+ exit(1);
+}
+
+sc_panic_exit_fn sc_set_panic_exit_fn(sc_panic_exit_fn fn) {
+ sc_panic_exit_fn old = panic_exit_fn;
+ panic_exit_fn = fn;
+ return old;
+}
+
+sc_panic_msg_fn sc_set_panic_msg_fn(sc_panic_msg_fn fn) {
+ sc_panic_msg_fn old = panic_msg_fn;
+ panic_msg_fn = fn;
+ return old;
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.h 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.h 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SC_PANIC_H
+#define SC_PANIC_H
+
+#include
+
+/**
+ * sc_panic is an exit-with-message utility function.
+ *
+ * The function takes a printf-like format string that is formatted and printed
+ * somehow. The function then terminates the process by calling exit. Both
+ * aspects can be customized.
+ *
+ * The particular nature of the exit can be customized by calling
+ * sc_set_panic_action. The panic action is a function that is called before
+ * attempting to exit.
+ *
+ * The way the error message is formatted and printed can be customized by
+ * calling sc_set_panic_format_fn(). By default the error is printed to
+ * standard error. If the error is related to a system call failure then errno
+ * can be set to a non-zero value just prior to calling sc_panic. The value
+ * will then be used when crafting the error message.
+ **/
+__attribute__((noreturn, format(printf, 1, 2))) void sc_panic(const char *fmt, ...);
+
+/**
+ * sc_panicv is a variant of sc_panic with an argument list.
+ **/
+__attribute__((noreturn)) void sc_panicv(const char *fmt, va_list ap);
+
+/**
+ * sc_panic_exit_fn is the type of the exit function used by sc_panic().
+ **/
+typedef void (*sc_panic_exit_fn)(void);
+
+/**
+ * sc_set_panic_exit_fn sets the panic exit function.
+ *
+ * When sc_panic is called it will eventually exit the running process. Just
+ * prior to that, it will call the panic exit function, if one has been set.
+ *
+ * If exiting the process is undesired, for example while running in intrd as
+ * pid 1, during the system shutdown phase, then a process can set the panic
+ * exit function. Note that if the specified function returns then panic will
+ * proceed to call exit(3) anyway.
+ *
+ * The old exit function, if any, is returned.
+ **/
+sc_panic_exit_fn sc_set_panic_exit_fn(sc_panic_exit_fn fn);
+
+/**
+ * sc_panic_msg_fn is the type of the format function used by sc_panic().
+ **/
+typedef void (*sc_panic_msg_fn)(const char *fmt, va_list ap, int errno_copy);
+
+/**
+ * sc_set_panic_msg_fn sets the panic message function.
+ *
+ * When sc_panic is called it will attempt to print an error message to
+ * standard error. The message includes information provided by the caller: the
+ * format string, the argument vector for a printf-like function as well as a
+ * copy of the system errno value, which may be zero if the error is not
+ * originated by a system call error.
+ *
+ * If custom formatting of the error message is desired, for example while
+ * running in initrd as pid 1, during the system shutdown phase, then a process
+ * can set the panic message function. Once set the function takes over the
+ * responsibility of printing an error message (in whatever form is
+ * appropriate).
+ *
+ * The old message function, if any, is returned.
+ **/
+sc_panic_msg_fn sc_set_panic_msg_fn(sc_panic_msg_fn fn);
+
+#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic-test.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "panic.h"
+#include "panic.c"
+
+#include
+
+static void test_panic(void)
+{
+ if (g_test_subprocess()) {
+ errno = 0;
+ sc_panic("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("death message\n");
+}
+
+static void test_panic_with_errno(void)
+{
+ if (g_test_subprocess()) {
+ errno = EPERM;
+ sc_panic("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("death message: Operation not permitted\n");
+}
+
+static void custom_panic_msg(const char *fmt, va_list ap, int errno_copy)
+{
+ fprintf(stderr, "PANIC: ");
+ vfprintf(stderr, fmt, ap);
+ fprintf(stderr, " (errno: %d)", errno_copy);
+ fprintf(stderr, "\n");
+}
+
+static void custom_panic_exit(void)
+{
+ fprintf(stderr, "EXITING\n");
+ exit(2);
+}
+
+static void test_panic_customization(void)
+{
+ if (g_test_subprocess()) {
+ sc_set_panic_msg_fn(custom_panic_msg);
+ sc_set_panic_exit_fn(custom_panic_exit);
+ errno = 123;
+ sc_panic("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("PANIC: death message (errno: 123)\n"
+ "EXITING\n");
+ // NOTE: g_test doesn't offer facilities to observe the exit code.
+}
+
+static void __attribute__((constructor)) init(void)
+{
+ g_test_add_func("/panic/panic", test_panic);
+ g_test_add_func("/panic/panic_with_errno", test_panic_with_errno);
+ g_test_add_func("/panic/panic_customization", test_panic_customization);
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/privs-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/privs-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/privs-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/privs-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -61,7 +61,7 @@
g_test_trap_assert_passed();
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/privs/sc_privs_drop", test_sc_privs_drop);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/secure-getenv.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/secure-getenv.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/secure-getenv.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/secure-getenv.h 2020-06-05 13:13:49.000000000 +0000
@@ -30,7 +30,7 @@
* only used when glibc is not available.
**/
char *secure_getenv(const char *name)
- __attribute__ ((nonnull(1), warn_unused_result));
+ __attribute__((nonnull(1), warn_unused_result));
#endif // ! HAVE_SECURE_GETENV
#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.c 2020-06-05 13:13:49.000000000 +0000
@@ -31,7 +31,7 @@
bool verify_security_tag(const char *security_tag, const char *snap_name)
{
const char *whitelist_re =
- "^snap\\.([a-z0-9](-?[a-z0-9])*(_[a-z0-9]{1,10})?)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$";
+ "^snap\\.([a-z0-9](-?[a-z0-9])*(_[a-z0-9]{1,10})?)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z0-9])*)$";
regex_t re;
if (regcomp(&re, whitelist_re, REG_EXTENDED) != 0)
die("can not compile regex %s", whitelist_re);
@@ -98,12 +98,11 @@
return 0;
}
-void sc_instance_name_validate(const char *instance_name,
- struct sc_error **errorp)
+void sc_instance_name_validate(const char *instance_name, sc_error ** errorp)
{
// NOTE: This function should be synchronized with the two other
// implementations: validate_instance_name and snap.ValidateInstanceName.
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Ensure that name is not NULL
if (instance_name == NULL) {
@@ -112,9 +111,8 @@
"snap instance name cannot be NULL");
goto out;
}
- // 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1
- // NULL
- char s[53] = { 0 };
+ // instance name length + 1 extra overflow + 1 NULL
+ char s[SNAP_INSTANCE_LEN + 1 + 1] = { 0 };
strncpy(s, instance_name, sizeof(s) - 1);
char *t = s;
@@ -142,12 +140,11 @@
sc_error_forward(errorp, err);
}
-void sc_instance_key_validate(const char *instance_key,
- struct sc_error **errorp)
+void sc_instance_key_validate(const char *instance_key, sc_error ** errorp)
{
// NOTE: see snap.ValidateInstanceName for reference of a valid instance key
// format
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Ensure that name is not NULL
if (instance_key == NULL) {
@@ -177,7 +174,7 @@
err =
sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY,
"instance key must contain at least one letter or digit");
- } else if (i > 10) {
+ } else if (i > SNAP_INSTANCE_KEY_LEN) {
err =
sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY,
"instance key must be shorter than 10 characters");
@@ -186,11 +183,11 @@
sc_error_forward(errorp, err);
}
-void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp)
+void sc_snap_name_validate(const char *snap_name, sc_error ** errorp)
{
// NOTE: This function should be synchronized with the two other
// implementations: validate_snap_name and snap.ValidateName.
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Ensure that name is not NULL
if (snap_name == NULL) {
@@ -255,7 +252,7 @@
"snap name must be longer than 1 character");
goto out;
}
- if (n > 40) {
+ if (n > SNAP_NAME_LEN) {
err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME,
"snap name must be shorter than 40 characters");
goto out;
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.h 2020-06-05 13:13:49.000000000 +0000
@@ -37,6 +37,17 @@
SC_SNAP_INVALID_INSTANCE_NAME = 3,
};
+/* SNAP_NAME_LEN is the maximum length of a snap name, enforced by snapd and the
+ * store. */
+#define SNAP_NAME_LEN 40
+/* SNAP_INSTANCE_KEY_LEN is the maximum length of instance key, enforced locally
+ * by snapd. */
+#define SNAP_INSTANCE_KEY_LEN 10
+/* SNAP_INSTANCE_LEN is the maximum length of snap instance name, composed of
+ * the snap name, separator '_' and the instance key, enforced locally by
+ * snapd. */
+#define SNAP_INSTANCE_LEN (SNAP_NAME_LEN + 1 + SNAP_INSTANCE_KEY_LEN)
+
/**
* Validate the given snap name.
*
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -90,6 +90,10 @@
g_assert_true(verify_security_tag("snap.123test.123test", "123test"));
g_assert_true(verify_security_tag
("snap.123test.hook.configure", "123test"));
+
+ // regression test snap.eon-edg-shb-pulseaudio.hook.connect-plug-i2c
+ g_assert_true(verify_security_tag
+ ("snap.foo.hook.connect-plug-i2c", "foo"));
}
static void test_sc_is_hook_security_tag(void)
@@ -111,13 +115,13 @@
static void test_sc_snap_or_instance_name_validate(gconstpointer data)
{
- typedef void (*validate_func_t) (const char *, struct sc_error **);
+ typedef void (*validate_func_t)(const char *, sc_error **);
validate_func_t validate = (validate_func_t) data;
bool is_instance =
(validate == sc_instance_name_validate) ? true : false;
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
// Smoke test, a valid snap name
validate("hello-world", &err);
@@ -267,7 +271,7 @@
static void test_sc_instance_name_validate(void)
{
- struct sc_error *err = NULL;
+ sc_error *err = NULL;
sc_instance_name_validate("hello-world", &err);
g_assert_null(err);
@@ -359,7 +363,6 @@
{
if (g_test_subprocess()) {
sc_snap_drop_instance_key("foo_bar", NULL, 0);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -373,7 +376,6 @@
char dest[10] = { 0 };
sc_snap_drop_instance_key("foo-foo-foo-foo-foo_bar", dest,
sizeof dest);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -385,7 +387,6 @@
if (g_test_subprocess()) {
char dest[3] = { 0 }; // "foo" sans the nil byte
sc_snap_drop_instance_key("foo", dest, sizeof dest);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -397,7 +398,20 @@
if (g_test_subprocess()) {
char dest[10] = { 0 };
sc_snap_drop_instance_key(NULL, dest, sizeof dest);
- g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+}
+
+static void test_sc_snap_drop_instance_key_short_dest_max(void)
+{
+ if (g_test_subprocess()) {
+ char dest[SNAP_NAME_LEN + 1] = { 0 };
+ /* 40 chars (max valid length), pretend dest is the same length, no space for terminator */
+ sc_snap_drop_instance_key
+ ("01234567890123456789012345678901234567890", dest,
+ sizeof dest - 1);
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -406,7 +420,7 @@
static void test_sc_snap_drop_instance_key_basic(void)
{
- char name[41] = { 0xff };
+ char name[SNAP_NAME_LEN + 1] = { 0xff };
sc_snap_drop_instance_key("foo_bar", name, sizeof name);
g_assert_cmpstr(name, ==, "foo");
@@ -426,6 +440,12 @@
memset(name, 0xff, sizeof name);
sc_snap_drop_instance_key("foo", name, sizeof name);
g_assert_cmpstr(name, ==, "foo");
+
+ memset(name, 0xff, sizeof name);
+ /* 40 chars - snap name length */
+ sc_snap_drop_instance_key("0123456789012345678901234567890123456789",
+ name, sizeof name);
+ g_assert_cmpstr(name, ==, "0123456789012345678901234567890123456789");
}
static void test_sc_snap_split_instance_name_trailing_nil(void)
@@ -434,7 +454,6 @@
char dest[3] = { 0 };
// pretend there is no place for trailing \0
sc_snap_split_instance_name("_", NULL, 0, dest, 0);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -447,7 +466,6 @@
char dest[10] = { 0 };
sc_snap_split_instance_name("foo_barbarbarbar", NULL, 0,
dest, sizeof dest);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -456,7 +474,7 @@
static void test_sc_snap_split_instance_name_basic(void)
{
- char name[41] = { 0xff };
+ char name[SNAP_NAME_LEN + 1] = { 0xff };
char instance[20] = { 0xff };
sc_snap_split_instance_name("foo_bar", name, sizeof name, instance,
@@ -530,7 +548,7 @@
g_assert_cmpstr(instance, ==, "");
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/snap/verify_security_tag", test_verify_security_tag);
g_test_add_func("/snap/sc_is_hook_security_tag",
@@ -558,6 +576,8 @@
test_sc_snap_drop_instance_key_short_dest);
g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest2",
test_sc_snap_drop_instance_key_short_dest2);
+ g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest_max",
+ test_sc_snap_drop_instance_key_short_dest_max);
g_test_add_func("/snap/sc_snap_split_instance_name/basic",
test_sc_snap_split_instance_name_basic);
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.c 2020-06-05 13:13:49.000000000 +0000
@@ -56,6 +56,22 @@
return strncmp(str - xlen + slen, suffix, xlen) == 0;
}
+bool sc_startswith(const char *str, const char *prefix)
+{
+ if (!str || !prefix) {
+ return false;
+ }
+
+ size_t xlen = strlen(prefix);
+ size_t slen = strlen(str);
+
+ if (slen < xlen) {
+ return false;
+ }
+
+ return strncmp(str, prefix, xlen) == 0;
+}
+
char *sc_strdup(const char *str)
{
size_t len;
@@ -81,7 +97,7 @@
n = vsnprintf(str, size, format, va);
va_end(va);
- if (n < 0 || (size_t) n >= size)
+ if (n < 0 || (size_t)n >= size)
die("cannot format string: %s", str);
return n;
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.h 2020-06-05 13:13:49.000000000 +0000
@@ -32,6 +32,11 @@
bool sc_endswith(const char *str, const char *suffix);
/**
+ * Check if a string has a given prefix.
+ **/
+bool sc_startswith(const char *str, const char *prefix);
+
+/**
* Allocate and return a copy of a string.
**/
char *sc_strdup(const char *str);
@@ -41,7 +46,7 @@
*
* This version dies on any error condition.
**/
-__attribute__ ((format(printf, 3, 4)))
+__attribute__((format(printf, 3, 4)))
int sc_must_snprintf(char *str, size_t size, const char *format, ...);
/**
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -53,6 +53,27 @@
g_assert_false(sc_endswith("ba", "bar"));
}
+static void test_sc_startswith(void)
+{
+ // NULL doesn't start with anything, nothing starts with NULL
+ g_assert_false(sc_startswith("", NULL));
+ g_assert_false(sc_startswith(NULL, ""));
+ g_assert_false(sc_startswith(NULL, NULL));
+ // Empty string starts with an empty string
+ g_assert_true(sc_startswith("", ""));
+ // Starts-with (matches)
+ g_assert_true(sc_startswith("foobar", "foo"));
+ g_assert_true(sc_startswith("foobar", "fo"));
+ g_assert_true(sc_startswith("foobar", "f"));
+ g_assert_true(sc_startswith("foobar", ""));
+ g_assert_true(sc_startswith("bar", "bar"));
+ // Starts-with (non-matches)
+ g_assert_false(sc_startswith("foobar", "quux"));
+ g_assert_false(sc_startswith("", "bar"));
+ g_assert_false(sc_startswith("b", "bar"));
+ g_assert_false(sc_startswith("ba", "bar"));
+}
+
static void test_sc_must_snprintf(void)
{
char buf[5] = { 0 };
@@ -86,7 +107,8 @@
};
} data = {
.buf = {
- 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,};
+ 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,
+ };
// Sanity check, ensure that the layout of structures is as spelled above.
// (first canary1, then buf and finally canary2.
@@ -117,7 +139,8 @@
};
} data = {
.buf = {
- 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,};
+ 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,
+ };
// Sanity check, ensure that the layout of structures is as spelled above.
// (first canary1, then buf and finally canary2.
@@ -788,10 +811,11 @@
free(s);
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/string-utils/sc_streq", test_sc_streq);
g_test_add_func("/string-utils/sc_endswith", test_sc_endswith);
+ g_test_add_func("/string-utils/sc_startswith", test_sc_startswith);
g_test_add_func("/string-utils/sc_must_snprintf",
test_sc_must_snprintf);
g_test_add_func("/string-utils/sc_must_snprintf/fail",
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.c 2020-06-05 13:13:49.000000000 +0000
@@ -16,6 +16,7 @@
*/
#include "test-utils.h"
+#include "string-utils.h"
#include "error.h"
#include "utils.h"
@@ -71,3 +72,37 @@
g_free(argv[3]);
g_free(argv);
}
+
+void
+ __attribute__((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...)
+{
+ int argc = 0;
+ char **argv = NULL;
+ va_list ap;
+
+ /* find out how many elements there are */
+ va_start(ap, argvp);
+ while (NULL != va_arg(ap, const char *)) {
+ argc += 1;
+ }
+ va_end(ap);
+
+ /* argc + terminating NULL entry */
+ argv = calloc(argc + 1, sizeof argv[0]);
+ g_assert_nonnull(argv);
+
+ va_start(ap, argvp);
+ for (int i = 0; i < argc; i++) {
+ const char *arg = va_arg(ap, const char *);
+ char *arg_copy = sc_strdup(arg);
+ g_test_queue_free(arg_copy);
+ argv[i] = arg_copy;
+ }
+ va_end(ap);
+
+ /* free argv last, so that entries do not leak */
+ g_test_queue_free(argv);
+
+ *argcp = argc;
+ *argvp = argv;
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.h 2020-06-05 13:13:49.000000000 +0000
@@ -23,4 +23,10 @@
*/
void rm_rf_tmp(const char *dir);
+/**
+ * Create an argc + argv pair out of a NULL terminated argument list.
+ **/
+void
+ __attribute__((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...);
+
#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -39,7 +39,31 @@
g_test_trap_assert_failed();
}
-static void __attribute__ ((constructor)) init(void)
+static void test_test_argc_argv(void)
+{
+ // Check that test_argc_argv() correctly stores data
+ int argc = 0;
+ char **argv = NULL;
+
+ test_argc_argv(&argc, &argv, NULL);
+ g_assert_cmpint(argc, ==, 0);
+ g_assert_nonnull(argv);
+ g_assert_null(argv[0]);
+
+ argc = 0;
+ argv = NULL;
+
+ test_argc_argv(&argc, &argv, "zero", "one", "two", NULL);
+ g_assert_cmpint(argc, ==, 3);
+ g_assert_nonnull(argv);
+ g_assert_cmpstr(argv[0], ==, "zero");
+ g_assert_cmpstr(argv[1], ==, "one");
+ g_assert_cmpstr(argv[2], ==, "two");
+ g_assert_null(argv[3]);
+}
+
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/test-utils/rm_rf_tmp", test_rm_rf_tmp);
+ g_test_add_func("/test-utils/test_argc_argv", test_test_argc_argv);
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/tool.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/tool.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/tool.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/tool.c 2020-06-05 13:13:49.000000000 +0000
@@ -88,9 +88,14 @@
snap_name_copy, NULL
};
char *envp[] = { "SNAPD_DEBUG=x", NULL };
+
+ /* Switch the group to root so that directories, files and locks created by
+ * snap-update-ns are owned by the root group. */
+ sc_identity old = sc_set_effective_identity(sc_root_group_identity());
sc_call_snapd_tool_with_apparmor(snap_update_ns_fd,
"snap-update-ns", apparmor,
aa_profile, argv, envp);
+ (void)sc_set_effective_identity(old);
}
void sc_call_snap_update_ns_as_user(int snap_update_ns_fd,
@@ -105,7 +110,7 @@
snap_name);
const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
- char xdg_runtime_dir_env[PATH_MAX+strlen("XDG_RUNTIME_DIR=")];
+ char xdg_runtime_dir_env[PATH_MAX + strlen("XDG_RUNTIME_DIR=")];
if (xdg_runtime_dir != NULL) {
sc_must_snprintf(xdg_runtime_dir_env,
sizeof(xdg_runtime_dir_env),
@@ -115,7 +120,7 @@
char *argv[] = {
"snap-update-ns",
/* This tells snap-update-ns we are calling from snap-confine and locking is in place */
- /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */
+ "--from-snap-confine",
/* This tells snap-update-ns that we want to process the per-user profile */
"--user-mounts", snap_name_copy, NULL
};
@@ -145,7 +150,11 @@
/* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor with
* either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function for details. */
char *envp[] = { "SNAPD_DEBUG=x", NULL };
+ /* Switch the group to root so that directories and locks created by
+ * snap-discard-ns are owned by the root group. */
+ sc_identity old = sc_set_effective_identity(sc_root_group_identity());
sc_call_snapd_tool(snap_discard_ns_fd, "snap-discard-ns", argv, envp);
+ (void)sc_set_effective_identity(old);
}
static int sc_open_snapd_tool(const char *tool_name)
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.c 2020-06-05 13:13:49.000000000 +0000
@@ -23,33 +23,16 @@
#include
#include
-#include "utils.h"
#include "cleanup-funcs.h"
+#include "panic.h"
+#include "utils.h"
void die(const char *msg, ...)
{
- int saved_errno = errno;
- va_list va;
- va_start(va, msg);
- vfprintf(stderr, msg, va);
- va_end(va);
-
- if (errno != 0) {
- fprintf(stderr, ": %s\n", strerror(saved_errno));
- } else {
- fprintf(stderr, "\n");
- }
- exit(1);
-}
-
-bool error(const char *msg, ...)
-{
- va_list va;
- va_start(va, msg);
- vfprintf(stderr, msg, va);
- va_end(va);
-
- return false;
+ va_list ap;
+ va_start(ap, msg);
+ sc_panicv(msg, ap);
+ va_end(ap);
}
struct sc_bool_name {
@@ -74,7 +57,7 @@
*
* If the text cannot be recognized, the default value is used.
**/
-static int parse_bool(const char *text, bool * value, bool default_value)
+static int parse_bool(const char *text, bool *value, bool default_value)
{
if (value == NULL) {
errno = EFAULT;
@@ -157,6 +140,43 @@
die("fclose failed");
}
+sc_identity sc_set_effective_identity(sc_identity identity)
+{
+ debug("set_effective_identity uid:%d (change: %s), gid:%d (change: %s)",
+ identity.uid, identity.change_uid ? "yes" : "no",
+ identity.gid, identity.change_gid ? "yes" : "no");
+ /* We are being careful not to return a value instructing us to change GID
+ * or UID by accident. */
+ sc_identity old = {
+ .change_gid = 0,
+ .change_uid = 0,
+ };
+
+ if (identity.change_gid) {
+ old.gid = getegid();
+ old.change_gid = 1;
+ if (setegid(identity.gid) < 0) {
+ die("cannot set effective group to %d", identity.gid);
+ }
+ if (getegid() != identity.gid) {
+ die("effective group change from %d to %d has failed",
+ old.gid, identity.gid);
+ }
+ }
+ if (identity.change_uid) {
+ old.uid = geteuid();
+ old.change_uid = 1;
+ if (seteuid(identity.uid) < 0) {
+ die("cannot set effective user to %d", identity.uid);
+ }
+ if (geteuid() != identity.uid) {
+ die("effective user change from %d to %d has failed",
+ old.uid, identity.uid);
+ }
+ }
+ return old;
+}
+
int sc_nonfatal_mkpath(const char *const path, mode_t mode)
{
// If asked to create an empty path, return immediately.
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.h
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.h 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.h 2020-06-05 13:13:49.000000000 +0000
@@ -20,14 +20,11 @@
#include
#include
-__attribute__ ((noreturn))
- __attribute__ ((format(printf, 1, 2)))
+__attribute__((noreturn))
+ __attribute__((format(printf, 1, 2)))
void die(const char *fmt, ...);
-__attribute__ ((format(printf, 1, 2)))
-bool error(const char *fmt, ...);
-
-__attribute__ ((format(printf, 1, 2)))
+__attribute__((format(printf, 1, 2)))
void debug(const char *fmt, ...);
/**
@@ -42,6 +39,50 @@
**/
bool sc_is_reexec_enabled(void);
+/**
+ * sc_identity describes the user performing certain operation.
+ *
+ * UID and GID represent user and group accounts numbers and are controlled by
+ * change_uid and change_gid flags.
+**/
+typedef struct sc_identity {
+ uid_t uid;
+ gid_t gid;
+ unsigned change_uid:1;
+ unsigned change_gid:1;
+} sc_identity;
+
+/**
+ * Identity of the root group.
+ *
+ * The return value is suitable for passing to sc_set_effective_identity. It
+ * causes the effective group to change to the root group. No change is made to
+ * effective user identity.
+ **/
+static inline sc_identity sc_root_group_identity(void)
+{
+ sc_identity id = {
+ /* Explicitly set our intent of changing just the GID.
+ * Refactoring of this code must retain this property. */
+ .change_uid = 0,
+ .change_gid = 1,
+ .gid = 0,
+ };
+ return id;
+}
+
+/**
+ * Set the effective user and group IDs to given values.
+ *
+ * Effective user and group identifiers are applied to the system. The
+ * current values are returned as another identity that can be restored via
+ * another call to sc_set_effective_identity.
+ *
+ * The fields change_uid and change_gid control if user and group ID is changed.
+ * The returned old identity has identical values of both use flags.
+**/
+sc_identity sc_set_effective_identity(sc_identity identity);
+
void write_string_to_file(const char *filepath, const char *buf);
/**
@@ -58,6 +99,6 @@
*
* The function returns -1 in case of any error.
**/
-__attribute__ ((warn_unused_result))
+__attribute__((warn_unused_result))
int sc_nonfatal_mkpath(const char *const path, mode_t mode);
#endif
diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils-test.c
--- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils-test.c 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils-test.c 2020-06-05 13:13:49.000000000 +0000
@@ -191,7 +191,7 @@
_test_sc_nonfatal_mkpath(dirname, subdirname);
}
-static void __attribute__ ((constructor)) init(void)
+static void __attribute__((constructor)) init(void)
{
g_test_add_func("/utils/parse_bool", test_parse_bool);
g_test_add_func("/utils/die", test_die);
diff -Nru snapd-2.37.4ubuntu0.1/cmd/Makefile.am snapd-2.45.1ubuntu0.2/cmd/Makefile.am
--- snapd-2.37.4ubuntu0.1/cmd/Makefile.am 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/Makefile.am 2020-06-05 13:13:49.000000000 +0000
@@ -55,33 +55,51 @@
endif
new_format = \
+ libsnap-confine-private/cgroup-pids-support.c \
+ libsnap-confine-private/cgroup-pids-support.h \
+ libsnap-confine-private/cgroup-support.c \
+ libsnap-confine-private/cgroup-support.h \
+ libsnap-confine-private/infofile-test.c \
+ libsnap-confine-private/infofile.c \
+ libsnap-confine-private/infofile.h \
+ libsnap-confine-private/panic-test.h \
+ libsnap-confine-private/panic.c \
+ libsnap-confine-private/panic.h \
snap-confine/seccomp-support-ext.c \
snap-confine/seccomp-support-ext.h \
+ snap-confine/selinux-support.c \
+ snap-confine/selinux-support.h \
+ snap-confine/snap-confine-invocation-test.c \
+ snap-confine/snap-confine-invocation.c \
+ snap-confine/snap-confine-invocation.h \
snap-discard-ns/snap-discard-ns.c
+
+# NOTE: clang-format is using project-wide .clang-format file.
.PHONY: fmt
fmt:: $(filter $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])))
- clang-format -style='{BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 120}' -i $^
+ clang-format -i $^
fmt:: $(filter-out $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])))
HOME=$(srcdir) indent $^
-# The hack target helps devlopers work on snap-confine on their live system by
+# The hack target helps developers work on snap-confine on their live system by
# installing a fresh copy of snap confine and the appropriate apparmor profile.
.PHONY: hack
hack: snap-confine/snap-confine-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns
- sudo install -D -m 6755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine
- sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real
+ sudo install -D -m 4755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine
+ if [ -d /etc/apparmor.d ]; then sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real; fi
sudo install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/
- sudo apparmor_parser -r snap-confine/snap-confine.apparmor
+ if [ "$$(command -v apparmor_parser)" != "" ]; then sudo apparmor_parser -r snap-confine/snap-confine.apparmor; fi
sudo install -m 755 snap-update-ns/snap-update-ns $(DESTDIR)$(libexecdir)/snap-update-ns
sudo install -m 755 snap-discard-ns/snap-discard-ns $(DESTDIR)$(libexecdir)/snap-discard-ns
sudo install -m 755 snap-seccomp/snap-seccomp $(DESTDIR)$(libexecdir)/snap-seccomp
+ if [ "$$(command -v restorecon)" != "" ]; then sudo restorecon -R -v $(DESTDIR)$(libexecdir)/; fi
# for the hack target also:
snap-update-ns/snap-update-ns: snap-update-ns/*.go snap-update-ns/*.[ch]
- cd snap-update-ns && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v
+ cd snap-update-ns && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -v
snap-seccomp/snap-seccomp: snap-seccomp/*.go
- cd snap-seccomp && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v
+ cd snap-seccomp && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -v
##
## libsnap-confine-private.a
@@ -94,6 +112,10 @@
libsnap-confine-private/apparmor-support.h \
libsnap-confine-private/cgroup-freezer-support.c \
libsnap-confine-private/cgroup-freezer-support.h \
+ libsnap-confine-private/cgroup-pids-support.c \
+ libsnap-confine-private/cgroup-pids-support.h \
+ libsnap-confine-private/cgroup-support.c \
+ libsnap-confine-private/cgroup-support.h \
libsnap-confine-private/classic.c \
libsnap-confine-private/classic.h \
libsnap-confine-private/cleanup-funcs.c \
@@ -104,12 +126,15 @@
libsnap-confine-private/fault-injection.h \
libsnap-confine-private/feature.c \
libsnap-confine-private/feature.h \
+ libsnap-confine-private/infofile.c \
libsnap-confine-private/locking.c \
libsnap-confine-private/locking.h \
libsnap-confine-private/mount-opt.c \
libsnap-confine-private/mount-opt.h \
libsnap-confine-private/mountinfo.c \
libsnap-confine-private/mountinfo.h \
+ libsnap-confine-private/panic.c \
+ libsnap-confine-private/panic.h \
libsnap-confine-private/privs.c \
libsnap-confine-private/privs.h \
libsnap-confine-private/secure-getenv.c \
@@ -136,9 +161,11 @@
libsnap-confine-private/error-test.c \
libsnap-confine-private/fault-injection-test.c \
libsnap-confine-private/feature-test.c \
+ libsnap-confine-private/infofile-test.c \
libsnap-confine-private/locking-test.c \
libsnap-confine-private/mount-opt-test.c \
libsnap-confine-private/mountinfo-test.c \
+ libsnap-confine-private/panic-test.c \
libsnap-confine-private/privs-test.c \
libsnap-confine-private/secure-getenv-test.c \
libsnap-confine-private/snap-test.c \
@@ -216,8 +243,14 @@
snap-confine/mount-support.h \
snap-confine/ns-support.c \
snap-confine/ns-support.h \
+ snap-confine/seccomp-support-ext.c \
+ snap-confine/seccomp-support-ext.h \
+ snap-confine/seccomp-support.c \
+ snap-confine/seccomp-support.h \
snap-confine/snap-confine-args.c \
snap-confine/snap-confine-args.h \
+ snap-confine/snap-confine-invocation.c \
+ snap-confine/snap-confine-invocation.h \
snap-confine/snap-confine.c \
snap-confine/udev-support.c \
snap-confine/udev-support.h \
@@ -255,20 +288,6 @@
snap_confine_snap_confine_CFLAGS += $(SUID_CFLAGS)
snap_confine_snap_confine_LDFLAGS += $(SUID_LDFLAGS)
-if SECCOMP
-snap_confine_snap_confine_SOURCES += \
- snap-confine/seccomp-support-ext.c \
- snap-confine/seccomp-support-ext.h \
- snap-confine/seccomp-support.c \
- snap-confine/seccomp-support.h
-snap_confine_snap_confine_CFLAGS += $(SECCOMP_CFLAGS)
-if STATIC_LIBSECCOMP
-snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libseccomp)
-else
-snap_confine_snap_confine_extra_libs += $(SECCOMP_LIBS)
-endif # STATIC_LIBSECCOMP
-endif # SECCOMP
-
if APPARMOR
snap_confine_snap_confine_CFLAGS += $(APPARMOR_CFLAGS)
if STATIC_LIBAPPARMOR
@@ -278,6 +297,18 @@
endif # STATIC_LIBAPPARMOR
endif # APPARMOR
+if SELINUX
+snap_confine_snap_confine_SOURCES += \
+ snap-confine/selinux-support.c \
+ snap-confine/selinux-support.h
+snap_confine_snap_confine_CFLAGS += $(SELINUX_CFLAGS)
+if STATIC_LIBSELINUX
+snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libselinux)
+else
+snap_confine_snap_confine_extra_libs += $(SELINUX_LIBS)
+endif # STATIC_LIBSELINUX
+endif # SELINUX
+
# an extra build that has additional debugging enabled at compile time
noinst_PROGRAMS += snap-confine/snap-confine-debug
@@ -306,6 +337,7 @@
snap-confine/mount-support-test.c \
snap-confine/ns-support-test.c \
snap-confine/snap-confine-args-test.c \
+ snap-confine/snap-confine-invocation-test.c \
snap-confine/snap-device-helper-test.c
snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS)
snap_confine_unit_tests_LDADD = $(snap_confine_snap_confine_LDADD) $(GLIB_LIBS)
@@ -326,7 +358,7 @@
endif
snap-confine/snap-confine.apparmor: snap-confine/snap-confine.apparmor.in Makefile
- sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@
+ sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),g' <$< >$@
# Install the apparmor profile
#
@@ -340,18 +372,13 @@
endif
install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/
-# NOTE: The 'void' directory *has to* be chmod 000
+# NOTE: The 'void' directory *has to* be chmod 111
install-data-local::
- install -d -m 000 $(DESTDIR)/var/lib/snapd/void
+ install -d -m 111 $(DESTDIR)/var/lib/snapd/void
install-exec-hook::
-if CAPS_OVER_SETUID
-# Ensure that snap-confine has CAP_SYS_ADMIN capability
- setcap cap_sys_admin=pe $(DESTDIR)$(libexecdir)/snap-confine
-else
-# Ensure that snap-confine is u+s,g+s (setuid and setgid)
- chmod 6755 $(DESTDIR)$(libexecdir)/snap-confine
-endif
+# Ensure that snap-confine is u+s (setuid)
+ chmod 4755 $(DESTDIR)$(libexecdir)/snap-confine
##
## snap-mgmt
@@ -367,6 +394,17 @@
snap-mgmt/snap-mgmt: snap-mgmt/snap-mgmt.sh.in Makefile snap-mgmt/$(am__dirstamp)
sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@
+if SELINUX
+##
+## snap-mgmt-selinux
+##
+
+libexec_SCRIPTS += snap-mgmt/snap-mgmt-selinux
+CLEANFILES += snap-mgmt/$(am__dirstamp) snap-mgmt/snap-mgmt-selinux
+
+snap-mgmt/snap-mgmt-selinux: snap-mgmt/snap-mgmt-selinux.sh.in Makefile snap-mgmt/$(am__dirstamp)
+ sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@
+endif
##
## ubuntu-core-launcher
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise.go 2020-06-05 13:13:49.000000000 +0000
@@ -26,6 +26,7 @@
"io"
"net"
"os"
+ "sort"
"strconv"
"github.com/jessevdk/go-flags"
@@ -47,6 +48,9 @@
// FromApt tells advise that it got started from an apt hook
// and needs to communicate over a socket
FromApt bool `long:"from-apt"`
+
+ // DumpDb dumps the whole advise database
+ DumpDb bool `long:"dump-db"`
}
var shortAdviseSnapHelp = i18n.G("Advise on available snaps")
@@ -64,7 +68,9 @@
// TRANSLATORS: This should not start with a lowercase letter.
"command": i18n.G("Advise on snaps that provide the given command"),
// TRANSLATORS: This should not start with a lowercase letter.
- "from-apt": i18n.G("Advise will talk to apt via an apt hook"),
+ "dump-db": i18n.G("Dump advise database for use by command-not-found."),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "from-apt": i18n.G("Run as an apt hook"),
// TRANSLATORS: This should not start with a lowercase letter.
"format": i18n.G("Use the given output format"),
}, []argDesc{
@@ -112,7 +118,6 @@
Method string `json:"method"`
Params struct {
Command string `json:"command"`
- SearchTerms []string `json:"search-terms"`
UnknownPackages []string `json:"unknown-packages"`
}
}
@@ -214,11 +219,55 @@
return nil
}
+type Snap struct {
+ Snap string
+ Version string
+ Command string
+}
+
+func dumpDbHook() error {
+ commands, err := advisor.DumpCommands()
+ if err != nil {
+ return err
+ }
+
+ commands_processed := make([]string, 0)
+ var b []Snap
+
+ var sortedCmds []string
+ for cmd := range commands {
+ sortedCmds = append(sortedCmds, cmd)
+ }
+ sort.Strings(sortedCmds)
+
+ for _, key := range sortedCmds {
+ value := commands[key]
+ err := json.Unmarshal([]byte(value), &b)
+ if err != nil {
+ return err
+ }
+ for i := range b {
+ var s = fmt.Sprintf("%s %s %s\n", key, b[i].Snap, b[i].Version)
+ commands_processed = append(commands_processed, s)
+ }
+ }
+
+ for _, value := range commands_processed {
+ fmt.Fprint(Stdout, value)
+ }
+
+ return nil
+}
+
func (x *cmdAdviseSnap) Execute(args []string) error {
if len(args) > 0 {
return ErrExtraArgs
}
+ if x.DumpDb {
+ return dumpDbHook()
+ }
+
if x.FromApt {
return adviseViaAptHook()
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -33,6 +33,7 @@
"github.com/snapcore/snapd/advisor"
snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/dirs"
)
type sillyFinder struct{}
@@ -98,6 +99,23 @@
c.Assert(s.Stderr(), Equals, "")
}
+func (s *SnapSuite) TestAdviseCommandDumpDb(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil)
+ defer dirs.SetRootDir("")
+
+ db, err := advisor.Create()
+ c.Assert(err, IsNil)
+ c.Assert(db.AddSnap("foo", "1.0", "foo summary", []string{"foo", "bar"}), IsNil)
+ c.Assert(db.Commit(), IsNil)
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"advise-snap", "--dump-db"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stdout(), Matches, `bar foo 1.0\nfoo foo 1.0\n`)
+}
+
func (s *SnapSuite) TestAdviseCommandMisspellText(c *C) {
restore := advisor.ReplaceCommandsFinder(mkSillyFinder)
defer restore()
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_alias_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_alias_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_alias_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_alias_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -55,6 +55,7 @@
"app": "cmd1",
"alias": "alias1",
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_auto_import.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_auto_import.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * 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
@@ -28,17 +28,20 @@
"os"
"os/exec"
"path/filepath"
+ "sort"
"strings"
"syscall"
"github.com/jessevdk/go-flags"
+ "github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snapdenv"
)
const autoImportsName = "auto-import.assert"
@@ -56,6 +59,8 @@
}
defer f.Close()
+ isTesting := snapdenv.Testing()
+
scanner := bufio.NewScanner(f)
for scanner.Scan() {
l := strings.Fields(scanner.Text())
@@ -85,7 +90,7 @@
continue
}
// skip all ram disks (unless in tests)
- if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") {
+ if !isTesting && strings.HasPrefix(mountSrc, "/dev/ram") {
continue
}
@@ -183,8 +188,10 @@
return added, nil
}
+var ioutilTempDir = ioutil.TempDir
+
func tryMount(deviceName string) (string, error) {
- tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-")
+ tmpMountTarget, err := ioutilTempDir("", "snapd-auto-import-mount-")
if err != nil {
err = fmt.Errorf("cannot create temporary mount point: %v", err)
logger.Noticef("error: %v", err)
@@ -206,8 +213,10 @@
return tmpMountTarget, nil
}
+var syscallUnmount = syscall.Unmount
+
func doUmount(mp string) error {
- if err := syscall.Unmount(mp, 0); err != nil {
+ if err := syscallUnmount(mp, 0); err != nil {
return err
}
return os.Remove(mp)
@@ -259,6 +268,55 @@
return cmd.Execute(nil)
}
+func removableBlockDevices() (removableDevices []string) {
+ // eg. /sys/block/sda/removable
+ removable, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/removable"))
+ if err != nil {
+ return nil
+ }
+ for _, removableAttr := range removable {
+ val, err := ioutil.ReadFile(removableAttr)
+ if err != nil || string(val) != "1\n" {
+ // non removable
+ continue
+ }
+ // let's see if it has partitions
+ dev := filepath.Base(filepath.Dir(removableAttr))
+
+ pattern := fmt.Sprintf(filepath.Join(dirs.GlobalRootDir, "/sys/block/%s/%s*/partition"), dev, dev)
+ // eg. /sys/block/sda/sda1/partition
+ partitionAttrs, _ := filepath.Glob(pattern)
+
+ if len(partitionAttrs) == 0 {
+ // not partitioned? try to use the main device
+ removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", dev))
+ continue
+ }
+
+ for _, partAttr := range partitionAttrs {
+ val, err := ioutil.ReadFile(partAttr)
+ if err != nil || string(val) != "1\n" {
+ // non partition?
+ continue
+ }
+ pdev := filepath.Base(filepath.Dir(partAttr))
+ removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", pdev))
+ // hasPartitions = true
+ }
+ }
+ sort.Strings(removableDevices)
+ return removableDevices
+}
+
+// inInstallmode returns true if it's UC20 system in install mode
+func inInstallMode() bool {
+ mode, _, err := boot.ModeAndRecoverySystemFromKernelCommandLine()
+ if err != nil {
+ return false
+ }
+ return mode == "install"
+}
+
func (x *cmdAutoImport) Execute(args []string) error {
if len(args) > 0 {
return ErrExtraArgs
@@ -268,8 +326,19 @@
fmt.Fprintf(Stderr, "auto-import is disabled on classic\n")
return nil
}
+ // TODO:UC20: workaround for LP: #1860231
+ if inInstallMode() {
+ fmt.Fprintf(Stderr, "auto-import is disabled in install-mode\n")
+ return nil
+ }
+
+ devices := x.Mount
+ if len(devices) == 0 {
+ // coldplug scenario, try all removable devices
+ devices = removableBlockDevices()
+ }
- for _, path := range x.Mount {
+ for _, path := range devices {
// udev adds new /dev/loopX devices on the fly when a
// loop mount happens and there is no loop device left.
//
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_auto_import_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_auto_import_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -28,11 +28,14 @@
. "gopkg.in/check.v1"
+ "github.com/snapcore/snapd/boot"
snap "github.com/snapcore/snapd/cmd/snap"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
)
func makeMockMountInfo(c *C, content string) string {
@@ -62,10 +65,10 @@
n++
case 1:
c.Check(r.Method, Equals, "POST")
- c.Check(r.URL.Path, Equals, "/v2/create-user")
+ c.Check(r.URL.Path, Equals, "/v2/users")
postData, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
- c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`)
+ c.Check(string(postData), Equals, `{"action":"create","sudoer":true,"known":true}`)
fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`)
n++
@@ -239,10 +242,10 @@
n++
case 1:
c.Check(r.Method, Equals, "POST")
- c.Check(r.URL.Path, Equals, "/v2/create-user")
+ c.Check(r.URL.Path, Equals, "/v2/users")
postData, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
- c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`)
+ c.Check(string(postData), Equals, `{"action":"create","sudoer":true,"known":true}`)
fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`)
n++
@@ -300,3 +303,163 @@
_, err = snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"})
c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384")
}
+
+func (s *SnapSuite) TestAutoImportUnhappyInInstallMode(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ _, restoreLogger := logger.MockLogger()
+ defer restoreLogger()
+
+ mockProcCmdlinePath := filepath.Join(c.MkDir(), "cmdline")
+ err := ioutil.WriteFile(mockProcCmdlinePath, []byte("foo=bar snapd_recovery_mode=install snapd_recovery_system=20191118"), 0644)
+ c.Assert(err, IsNil)
+
+ restore = boot.MockProcCmdline(mockProcCmdlinePath)
+ defer restore()
+
+ _, err = snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "auto-import is disabled in install-mode\n")
+}
+
+var mountStatic = []string{"mount", "-t", "ext4,vfat", "-o", "ro", "--make-private"}
+
+func (s *SnapSuite) TestAutoImportFromRemovable(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ _, restoreLogger := logger.MockLogger()
+ defer restoreLogger()
+
+ rootdir := c.MkDir()
+ dirs.SetRootDir(rootdir)
+
+ var umounts []string
+ restore = snap.MockSyscallUmount(func(p string, _ int) error {
+ umounts = append(umounts, p)
+ return nil
+ })
+ defer restore()
+
+ var tmpdirIdx int
+ restore = snap.MockIoutilTempDir(func(where string, p string) (string, error) {
+ c.Check(where, Equals, "")
+ tmpdirIdx++
+ return filepath.Join(rootdir, fmt.Sprintf("/tmp/%s%d", p, tmpdirIdx)), nil
+ })
+ defer restore()
+
+ mountCmd := testutil.MockCommand(c, "mount", "")
+ defer mountCmd.Restore()
+
+ snaptest.PopulateDir(rootdir, [][]string{
+ // removable without partitions
+ {"sys/block/sdremovable/removable", "1\n"},
+ // fixed disk
+ {"sys/block/sdfixed/removable", "0\n"},
+ // removable with partitions
+ {"sys/block/sdpart/removable", "1\n"},
+ {"sys/block/sdpart/sdpart1/partition", "1\n"},
+ {"sys/block/sdpart/sdpart2/partition", "0\n"},
+ {"sys/block/sdpart/sdpart3/partition", "1\n"},
+ // removable but subdevices are not partitions?
+ {"sys/block/sdother/removable", "1\n"},
+ {"sys/block/sdother/sdother1/partition", "0\n"},
+ })
+
+ // do not mock mountinfo contents, we just want to observe whether we
+ // try to mount and umount the right stuff
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+ c.Check(mountCmd.Calls(), DeepEquals, [][]string{
+ append(mountStatic, "/dev/sdpart1", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1")),
+ append(mountStatic, "/dev/sdpart3", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-2")),
+ append(mountStatic, "/dev/sdremovable", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-3")),
+ })
+ c.Check(umounts, DeepEquals, []string{
+ filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-3"),
+ filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-2"),
+ filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1"),
+ })
+}
+
+func (s *SnapSuite) TestAutoImportNoRemovable(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ rootdir := c.MkDir()
+ dirs.SetRootDir(rootdir)
+
+ var umounts []string
+ restore = snap.MockSyscallUmount(func(p string, _ int) error {
+ return fmt.Errorf("unexpected call")
+ })
+ defer restore()
+
+ mountCmd := testutil.MockCommand(c, "mount", "exit 1")
+ defer mountCmd.Restore()
+
+ snaptest.PopulateDir(rootdir, [][]string{
+ // fixed disk
+ {"sys/block/sdfixed/removable", "0\n"},
+ // removable but subdevices are not partitions?
+ {"sys/block/sdother/removable", "1\n"},
+ {"sys/block/sdother/sdother1/partition", "0\n"},
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+ c.Check(mountCmd.Calls(), HasLen, 0)
+ c.Check(umounts, HasLen, 0)
+}
+
+func (s *SnapSuite) TestAutoImportFromMount(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ _, restoreLogger := logger.MockLogger()
+ defer restoreLogger()
+
+ mountCmd := testutil.MockCommand(c, "mount", "")
+
+ rootdir := c.MkDir()
+ dirs.SetRootDir(rootdir)
+
+ var umounts []string
+ restore = snap.MockSyscallUmount(func(p string, _ int) error {
+ c.Assert(umounts, HasLen, 0)
+ umounts = append(umounts, p)
+ return nil
+ })
+ defer restore()
+
+ var tmpdircalls int
+ restore = snap.MockIoutilTempDir(func(where string, p string) (string, error) {
+ c.Check(where, Equals, "")
+ c.Assert(tmpdircalls, Equals, 0)
+ tmpdircalls++
+ return filepath.Join(rootdir, fmt.Sprintf("/tmp/%s1", p)), nil
+ })
+ defer restore()
+
+ // do not mock mountinfo contents, we just want to observe whether we
+ // try to mount and umount the right stuff
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import", "--mount", "/dev/foobar"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+ c.Check(mountCmd.Calls(), DeepEquals, [][]string{
+ append(mountStatic, "/dev/foobar", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1")),
+ })
+ c.Check(umounts, DeepEquals, []string{
+ filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1"),
+ })
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame_generated.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame_generated.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame_generated.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame_generated.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,7 +0,0 @@
-package main
-
-// generated by mkauthors.sh; do not edit
-
-func init() {
- authors = []string{"Mark Shuttleworth", "Gustavo Niemeyer", "Sergio Schvezov", "Simon Fels", "Kyle Fazzari", "Leo Arias", "Sergio Cazzolato", "Gustavo Niemeyer", "Federico Gimenez", "Maciej Borzecki", "Jamie Strandboge", "Pawel Stolowski", "John R. Lenton", "Samuele Pedroni", "Zygmunt Krynicki", "Michael Vogt"}
-}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,55 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 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 .
- *
- */
-
-package main
-
-//go:generate mkauthors.sh
-
-import (
- "fmt"
- "math/rand"
-
- "github.com/jessevdk/go-flags"
-)
-
-type cmdBlame struct{}
-
-var authors []string
-
-func init() {
- cmd := addCommand("blame",
- "",
- "",
- func() flags.Commander {
- return &cmdBlame{}
- }, nil, nil)
- cmd.hidden = true
-}
-
-func (x *cmdBlame) Execute(args []string) error {
- if len(args) > 0 {
- return ErrExtraArgs
- }
- if len(authors) == 0 {
- return nil
- }
-
- fmt.Fprintf(Stdout, "It's all %s's fault.\n", authors[rand.Intn(len(authors))])
- return nil
-}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_booted.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_booted.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_booted.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_booted.go 2020-06-05 13:13:49.000000000 +0000
@@ -29,7 +29,7 @@
func init() {
cmd := addCommand("booted",
- "Internal",
+ "Deprecated (hidden)",
"The booted command is only retained for backwards compatibility.",
func() flags.Commander {
return &cmdBooted{}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,214 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdConnections struct {
+ clientMixin
+ All bool `long:"all"`
+ Positionals struct {
+ Snap installedSnapName
+ } `positional-args:"true"`
+}
+
+var shortConnectionsHelp = i18n.G("List interface connections")
+var longConnectionsHelp = i18n.G(`
+The connections command lists connections between plugs and slots
+in the system.
+
+Unless is provided, the listing is for connected plugs and
+slots for all snaps in the system. In this mode, pass --all to also
+list unconnected plugs and slots.
+
+$ snap connections
+
+Lists connected and unconnected plugs and slots for the specified
+snap.
+`)
+
+func init() {
+ addCommand("connections", shortConnectionsHelp, longConnectionsHelp, func() flags.Commander {
+ return &cmdConnections{}
+ }, map[string]string{
+ "all": i18n.G("Show connected and unconnected plugs and slots"),
+ }, []argDesc{{
+ // TRANSLATORS: This needs to be wrapped in <>s.
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Constrain listing to a specific snap"),
+ }})
+}
+
+func isSystemSnap(snap string) bool {
+ return snap == "core" || snap == "snapd" || snap == "system"
+}
+
+func endpoint(snap, name string) string {
+ if isSystemSnap(snap) {
+ return ":" + name
+ }
+ return snap + ":" + name
+}
+
+type connection struct {
+ slot string
+ plug string
+ interfaceName string
+ interfaceDeterminant string
+ manual bool
+ gadget bool
+}
+
+func (cn connection) String() string {
+ opts := []string{}
+ if cn.manual {
+ opts = append(opts, "manual")
+ }
+ if cn.gadget {
+ opts = append(opts, "gadget")
+ }
+ if len(opts) == 0 {
+ return "-"
+ }
+ return strings.Join(opts, ",")
+}
+
+type byConnectionData []connection
+
+func (b byConnectionData) Len() int { return len(b) }
+func (b byConnectionData) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b byConnectionData) Less(i, j int) bool {
+ iCon, jCon := b[i], b[j]
+ if iCon.interfaceName != jCon.interfaceName {
+ return iCon.interfaceName < jCon.interfaceName
+ }
+ if iCon.plug != jCon.plug {
+ return iCon.plug < jCon.plug
+ }
+ return iCon.slot < jCon.slot
+}
+
+func interfaceDeterminant(conn *client.Connection) string {
+ var value string
+
+ switch conn.Interface {
+ case "content":
+ value, _ = conn.PlugAttrs["content"].(string)
+ if value == "" {
+ value, _ = conn.SlotAttrs["content"].(string)
+ }
+ }
+ if value == "" {
+ return ""
+ }
+ return fmt.Sprintf("[%v]", value)
+}
+
+func (x *cmdConnections) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ opts := client.ConnectionOptions{
+ All: x.All,
+ }
+ wanted := string(x.Positionals.Snap)
+ if wanted != "" {
+ if x.All {
+ // passing a snap name already implies --all, error out
+ // when it was passed explicitly
+ return fmt.Errorf(i18n.G("cannot use --all with snap name"))
+ }
+ // when asking for a single snap, include its disconnected plugs
+ // and slots
+ opts.Snap = wanted
+ opts.All = true
+ // print all slots
+ x.All = true
+ }
+
+ connections, err := x.client.Connections(&opts)
+ if err != nil {
+ return err
+ }
+ if len(connections.Plugs) == 0 && len(connections.Slots) == 0 {
+ return nil
+ }
+
+ annotatedConns := make([]connection, 0, len(connections.Established)+len(connections.Undesired))
+ for _, conn := range connections.Established {
+ annotatedConns = append(annotatedConns, connection{
+ plug: endpoint(conn.Plug.Snap, conn.Plug.Name),
+ slot: endpoint(conn.Slot.Snap, conn.Slot.Name),
+ manual: conn.Manual,
+ gadget: conn.Gadget,
+ interfaceName: conn.Interface,
+ interfaceDeterminant: interfaceDeterminant(&conn),
+ })
+ }
+
+ w := tabWriter()
+ fmt.Fprintln(w, i18n.G("Interface\tPlug\tSlot\tNotes"))
+
+ for _, plug := range connections.Plugs {
+ if len(plug.Connections) == 0 && x.All {
+ annotatedConns = append(annotatedConns, connection{
+ plug: endpoint(plug.Snap, plug.Name),
+ slot: "-",
+ interfaceName: plug.Interface,
+ })
+ }
+ }
+ for _, slot := range connections.Slots {
+ if !isSystemSnap(wanted) && isSystemSnap(slot.Snap) {
+ // displaying unconnected system snap slots is boring,
+ // unless explicitly asked to show them
+ continue
+ }
+ if len(slot.Connections) == 0 && x.All {
+ annotatedConns = append(annotatedConns, connection{
+ plug: "-",
+ slot: endpoint(slot.Snap, slot.Name),
+ interfaceName: slot.Interface,
+ })
+ }
+ }
+
+ sort.Sort(byConnectionData(annotatedConns))
+
+ for _, note := range annotatedConns {
+ fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\n", note.interfaceName, note.interfaceDeterminant, note.plug, note.slot, note)
+ }
+
+ if len(annotatedConns) > 0 {
+ w.Flush()
+ }
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,853 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ . "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestConnectionsNoneConnected(c *C) {
+ result := client.Connections{}
+ query := url.Values{}
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+ _, err := Parser(Client()).ParseArgs([]string{"connections"})
+ c.Check(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.ResetStdStreams()
+
+ query = url.Values{
+ "select": []string{"all"},
+ }
+ _, err = Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Check(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsNotInstalled(c *C) {
+ query := url.Values{
+ "snap": []string{"foo"},
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ fmt.Fprintln(w, `{"type": "error", "result": {"message": "not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`)
+ })
+ _, err := Parser(Client()).ParseArgs([]string{"connections", "foo"})
+ c.Check(err, ErrorMatches, `not found`)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsNoneConnectedPlugs(c *C) {
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ result := client.Connections{
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds keyboard-lights:capslock-led - -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.ResetStdStreams()
+
+ query = url.Values{
+ "select": []string{"all"},
+ "snap": []string{"keyboard-lights"},
+ }
+
+ rest, err = Parser(Client()).ParseArgs([]string{"connections", "keyboard-lights"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout = "" +
+ "Interface Plug Slot Notes\n" +
+ "leds keyboard-lights:capslock-led - -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsNoneConnectedSlots(c *C) {
+ result := client.Connections{}
+ query := url.Values{}
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+ _, err := Parser(Client()).ParseArgs([]string{"connections"})
+ c.Check(err, IsNil)
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+
+ s.ResetStdStreams()
+
+ query = url.Values{
+ "select": []string{"all"},
+ }
+ result = client.Connections{
+ Slots: []client.Slot{
+ {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds - leds-provider:capslock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsSomeConnected(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "capslock"},
+ Slot: client.SlotRef{Snap: "leds-provider", Name: "capslock-led"},
+ Interface: "leds",
+ Gadget: true,
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"},
+ Slot: client.SlotRef{Snap: "core", Name: "numlock-led"},
+ Interface: "leds",
+ Manual: true,
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "scrollock"},
+ Slot: client.SlotRef{Snap: "core", Name: "scrollock-led"},
+ Interface: "leds",
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "numlock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "numlock-led",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "scrollock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "scrollock-led",
+ }},
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "core",
+ Name: "numlock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "numlock",
+ }},
+ }, {
+ Snap: "core",
+ Name: "scrollock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "scrollock",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "capslock",
+ }},
+ },
+ },
+ }
+ query := url.Values{}
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+ rest, err := Parser(Client()).ParseArgs([]string{"connections"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds keyboard-lights:capslock leds-provider:capslock-led gadget\n" +
+ "leds keyboard-lights:numlock :numlock-led manual\n" +
+ "leds keyboard-lights:scrollock :scrollock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsSomeDisconnected(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "scrollock"},
+ Slot: client.SlotRef{Snap: "core", Name: "scrollock-led"},
+ Interface: "leds",
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "capslock"},
+ Slot: client.SlotRef{Snap: "leds-provider", Name: "capslock-led"},
+ Interface: "leds",
+ },
+ },
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"},
+ Slot: client.SlotRef{Snap: "core", Name: "numlock-led"},
+ Interface: "leds",
+ Manual: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "keyboard-lights",
+ Name: "capslock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "numlock",
+ Interface: "leds",
+ }, {
+ Snap: "keyboard-lights",
+ Name: "scrollock",
+ Interface: "leds",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "scrollock-led",
+ }},
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "core",
+ Name: "capslock-led",
+ Interface: "leds",
+ }, {
+ Snap: "core",
+ Name: "numlock-led",
+ Interface: "leds",
+ }, {
+ Snap: "core",
+ Name: "scrollock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "scrollock",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ Connections: []client.PlugRef{{
+ Snap: "keyuboard-lights",
+ Name: "capslock",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "numlock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds - leds-provider:numlock-led -\n" +
+ "leds keyboard-lights:capslock leds-provider:capslock-led -\n" +
+ "leds keyboard-lights:numlock - -\n" +
+ "leds keyboard-lights:scrollock :scrollock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsOnlyDisconnected(c *C) {
+ result := client.Connections{
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"},
+ Slot: client.SlotRef{Snap: "leds-provider", Name: "numlock-led"},
+ Interface: "leds",
+ Manual: true,
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "leds-provider",
+ Name: "capslock-led",
+ Interface: "leds",
+ }, {
+ Snap: "leds-provider",
+ Name: "numlock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ query := url.Values{
+ "snap": []string{"leds-provider"},
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "leds-provider"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "leds - leds-provider:capslock-led -\n" +
+ "leds - leds-provider:numlock-led -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsFiltering(c *C) {
+ result := client.Connections{}
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ query = url.Values{
+ "select": []string{"all"},
+ "snap": []string{"mouse-buttons"},
+ }
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "mouse-buttons"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ rest, err = Parser(Client()).ParseArgs([]string{"connections", "mouse-buttons", "--all"})
+ c.Assert(err, ErrorMatches, "cannot use --all with snap name")
+ c.Assert(rest, DeepEquals, []string{"--all"})
+}
+
+func (s *SnapSuite) TestConnectionsSorting(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"},
+ Interface: "content",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "b-content-provider", Name: "data"},
+ Interface: "content",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "desktop-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "desktop"},
+ Interface: "desktop",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "x11-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "x11"},
+ Interface: "x11",
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "a-x11-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "x11"},
+ Interface: "x11",
+ }, {
+ Plug: client.PlugRef{Snap: "a-foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"},
+ Interface: "content",
+ }, {
+ Plug: client.PlugRef{Snap: "keyboard-app", Name: "x11"},
+ Slot: client.SlotRef{Snap: "core", Name: "x11"},
+ Interface: "x11",
+ Manual: true,
+ },
+ },
+ Undesired: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "foo", Name: "plug"},
+ Slot: client.SlotRef{Snap: "c-content-provider", Name: "data"},
+ Interface: "content",
+ Manual: true,
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "foo",
+ Name: "plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "a-content-provider",
+ Name: "data",
+ }, {
+ Snap: "b-content-provider",
+ Name: "data",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "desktop-plug",
+ Interface: "desktop",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "desktop",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "x11-plug",
+ Interface: "x11",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "a-x11-plug",
+ Interface: "x11",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "a-foo",
+ Name: "plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "a-content-provider",
+ Name: "data",
+ }},
+ }, {
+ Snap: "keyboard-app",
+ Name: "x11",
+ Interface: "x11",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "keyboard-lights",
+ Name: "numlock",
+ Interface: "leds",
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "c-content-provider",
+ Name: "data",
+ Interface: "content",
+ }, {
+ Snap: "a-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "plug",
+ }, {
+ Snap: "a-foo",
+ Name: "plug",
+ }},
+ }, {
+ Snap: "b-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "plug",
+ }},
+ }, {
+ Snap: "core",
+ Name: "x11",
+ Interface: "x11",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-x11-plug",
+ }, {
+ Snap: "foo",
+ Name: "x11-plug",
+ }, {
+ Snap: "keyboard-app",
+ Name: "x11",
+ }},
+ }, {
+ Snap: "leds-provider",
+ Name: "numlock-led",
+ Interface: "leds",
+ },
+ },
+ }
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "content - c-content-provider:data -\n" +
+ "content a-foo:plug a-content-provider:data -\n" +
+ "content foo:plug a-content-provider:data -\n" +
+ "content foo:plug b-content-provider:data -\n" +
+ "desktop foo:desktop-plug :desktop -\n" +
+ "leds - leds-provider:numlock-led -\n" +
+ "leds keyboard-lights:numlock - -\n" +
+ "x11 foo:a-x11-plug :x11 -\n" +
+ "x11 foo:x11-plug :x11 -\n" +
+ "x11 keyboard-app:x11 :x11 manual\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestConnectionsDefiningAttribute(c *C) {
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Plug: client.PlugRef{Snap: "foo", Name: "a-plug"},
+ Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"},
+ Interface: "content",
+ PlugAttrs: map[string]interface{}{
+ "content": "plug-some-data",
+ "target": "$SNAP/foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "b-plug"},
+ Slot: client.SlotRef{Snap: "b-content-provider", Name: "data"},
+ Interface: "content",
+ PlugAttrs: map[string]interface{}{
+ // no content attribute for plug, falls back to slot
+ "target": "$SNAP/foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "c-plug"},
+ Slot: client.SlotRef{Snap: "c-content-provider", Name: "data"},
+ Interface: "content",
+ PlugAttrs: map[string]interface{}{
+ // no content attribute for plug
+ "target": "$SNAP/foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ // no content attribute for slot either
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "d-plug"},
+ Slot: client.SlotRef{Snap: "d-content-provider", Name: "data"},
+ Interface: "content",
+ // no attributes at all
+ }, {
+ Plug: client.PlugRef{Snap: "foo", Name: "desktop-plug"},
+ Slot: client.SlotRef{Snap: "core", Name: "desktop"},
+ // desktop interface does not have any defining attributes
+ Interface: "desktop",
+ PlugAttrs: map[string]interface{}{
+ "this-is-ignored": "foo",
+ },
+ SlotAttrs: map[string]interface{}{
+ "this-is-ignored-too": "foo",
+ },
+ },
+ },
+ Plugs: []client.Plug{
+ {
+ Snap: "foo",
+ Name: "a-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "a-content-provider",
+ Name: "data",
+ }},
+ Attrs: map[string]interface{}{
+ "content": "plug-some-data",
+ "target": "$SNAP/foo",
+ },
+ }, {
+ Snap: "foo",
+ Name: "b-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "b-content-provider",
+ Name: "data",
+ }},
+ Attrs: map[string]interface{}{
+ // no content attribute for plug, falls back to slot
+ "target": "$SNAP/foo",
+ },
+ }, {
+ Snap: "foo",
+ Name: "c-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "c-content-provider",
+ Name: "data",
+ }},
+ Attrs: map[string]interface{}{
+ // no content attribute for plug
+ "target": "$SNAP/foo",
+ },
+ }, {
+ Snap: "foo",
+ Name: "d-plug",
+ Interface: "content",
+ Connections: []client.SlotRef{{
+ Snap: "d-content-provider",
+ Name: "data",
+ }},
+ }, {
+ Snap: "foo",
+ Name: "desktop-plug",
+ Interface: "desktop",
+ Connections: []client.SlotRef{{
+ Snap: "core",
+ Name: "desktop",
+ }},
+ },
+ },
+ Slots: []client.Slot{
+ {
+ Snap: "a-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ Attrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Snap: "b-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ Attrs: map[string]interface{}{
+ "content": "slot-some-data",
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Snap: "c-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ Attrs: map[string]interface{}{
+ "source": map[string]interface{}{
+ "read": []string{"$SNAP/bar"},
+ },
+ },
+ }, {
+ Snap: "a-content-provider",
+ Name: "data",
+ Interface: "content",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "a-plug",
+ }},
+ }, {
+ Snap: "core",
+ Name: "desktop",
+ Interface: "desktop",
+ Connections: []client.PlugRef{{
+ Snap: "foo",
+ Name: "desktop-plug",
+ }},
+ },
+ },
+ }
+ query := url.Values{
+ "select": []string{"all"},
+ }
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, query)
+ body, err := ioutil.ReadAll(r.Body)
+ c.Check(err, IsNil)
+ c.Check(body, DeepEquals, []byte{})
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ })
+
+ rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ expectedStdout := "" +
+ "Interface Plug Slot Notes\n" +
+ "content[plug-some-data] foo:a-plug a-content-provider:data -\n" +
+ "content[slot-some-data] foo:b-plug b-content-provider:data -\n" +
+ "content foo:c-plug c-content-provider:data -\n" +
+ "content foo:d-plug d-content-provider:data -\n" +
+ "desktop foo:desktop-plug :desktop -\n"
+ c.Assert(s.Stdout(), Equals, expectedStdout)
+ c.Assert(s.Stderr(), Equals, "")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check.go 2020-06-05 13:13:49.000000000 +0000
@@ -44,10 +44,9 @@
}
var status struct {
- Connectivity bool
- Unreachable []string
+ Unreachable []string
}
- if err := x.client.Debug("connectivity", nil, &status); err != nil {
+ if err := x.client.DebugGet("connectivity", &status, nil); err != nil {
return err
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -34,12 +34,12 @@
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch n {
case 0:
- c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/debug")
- c.Check(r.URL.RawQuery, check.Equals, "")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=connectivity")
data, err := ioutil.ReadAll(r.Body)
c.Check(err, check.IsNil)
- c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`))
+ c.Check(data, check.HasLen, 0)
fmt.Fprintln(w, `{"type": "sync", "result": {}}`)
default:
c.Fatalf("expected to get 1 requests, now on %d", n+1)
@@ -61,12 +61,12 @@
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch n {
case 0:
- c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/debug")
- c.Check(r.URL.RawQuery, check.Equals, "")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=connectivity")
data, err := ioutil.ReadAll(r.Body)
c.Check(err, check.IsNil)
- c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`))
+ c.Check(data, check.HasLen, 0)
fmt.Fprintln(w, `{"type": "sync", "result": {"connectivity":false,"unreachable":["foo.bar.com"]}}`)
default:
c.Fatalf("expected to get 1 requests, now on %d", n+1)
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connect_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connect_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connect_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connect_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -80,6 +80,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -113,6 +114,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -146,6 +148,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -179,6 +182,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -276,7 +280,7 @@
func (s *SnapSuite) TestConnectCompletion(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
- case "/v2/interfaces":
+ case "/v2/connections":
c.Assert(r.Method, Equals, "GET")
EncodeResponseBody(c, w, map[string]interface{}{
"type": "sync",
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,85 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+var shortCreateCohortHelp = i18n.G("Create cohort keys for a series of snaps")
+var longCreateCohortHelp = i18n.G(`
+The create-cohort command creates a set of cohort keys for a given set of snaps.
+
+A cohort is a view or snapshot of a snap's "channel map" at a given point in
+time that fixes the set of revisions for the snap given other constraints
+(e.g. channel or architecture). The cohort is then identified by an opaque
+per-snap key that works across systems. Installations or refreshes of the snap
+using a given cohort key would use a fixed revision for up to 90 days, after
+which a new set of revisions would be fixed under that same cohort key and a
+new 90 days window started.
+`)
+
+type cmdCreateCohort struct {
+ clientMixin
+ Positional struct {
+ Snaps []anySnapName `positional-arg-name:"" required:"1"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func init() {
+ addCommand("create-cohort", shortCreateCohortHelp, longCreateCohortHelp, func() flags.Commander { return &cmdCreateCohort{} }, nil, nil)
+}
+
+// output should be YAML, so we use these two as helpers to get that done easy
+type cohortInnerYAML struct {
+ CohortKey string `yaml:"cohort-key"`
+}
+type cohortOutYAML struct {
+ Cohorts map[string]cohortInnerYAML `yaml:"cohorts"`
+}
+
+func (x *cmdCreateCohort) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ snaps := make([]string, len(x.Positional.Snaps))
+ for i, s := range x.Positional.Snaps {
+ snaps[i] = string(s)
+ }
+
+ cohorts, err := x.client.CreateCohorts(snaps)
+ if len(cohorts) == 0 || err != nil {
+ return err
+ }
+
+ var out cohortOutYAML
+ out.Cohorts = make(map[string]cohortInnerYAML, len(cohorts))
+ for k, v := range cohorts {
+ out.Cohorts[k] = cohortInnerYAML{v}
+ }
+
+ enc := yaml.NewEncoder(Stdout)
+ defer enc.Close()
+ return enc.Encode(out)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,87 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestCreateCohort(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ fmt.Fprintln(w, `{
+"type": "sync",
+"status-code": 200,
+"status": "OK",
+"result": {"foo": "what", "bar": "this"}}`)
+
+ })
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.HasLen, 0)
+
+ var v map[string]map[string]map[string]string
+ c.Assert(yaml.Unmarshal(s.stdout.Bytes(), &v), check.IsNil)
+ c.Check(v, check.DeepEquals, map[string]map[string]map[string]string{
+ "cohorts": {
+ "foo": {"cohort-key": "what"},
+ "bar": {"cohort-key": "this"},
+ },
+ })
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestCreateCohortNoSnaps(c *check.C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ panic("shouldn't be called")
+ })
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort"})
+ c.Check(err, check.ErrorMatches, "the required argument .* was not provided")
+}
+
+func (s *SnapSuite) TestCreateCohortNotFound(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "kind": "snap-not-found"}, "status-code": 404}`)
+ })
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"})
+ c.Check(err, check.ErrorMatches, "cannot create cohorts: snap not found")
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestCreateCohortError(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ fmt.Fprintln(w, `{"type": "error", "result": {"message": "something went wrong"}}`)
+ })
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"})
+ c.Check(err, check.ErrorMatches, "cannot create cohorts: something went wrong")
+ c.Check(n, check.Equals, 1)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_key.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_key.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_key.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_key.go 2020-06-05 13:13:49.000000000 +0000
@@ -52,6 +52,7 @@
desc: i18n.G("Name of key to create; defaults to 'default'"),
}})
cmd.hidden = true
+ cmd.completeHidden = true
}
func (x *cmdCreateKey) Execute(args []string) error {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user.go 2020-06-05 13:13:49.000000000 +0000
@@ -23,10 +23,10 @@
"encoding/json"
"fmt"
+ "github.com/jessevdk/go-flags"
+
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/i18n"
-
- "github.com/jessevdk/go-flags"
)
var shortCreateUserHelp = i18n.G("Create a local system user")
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -35,13 +35,15 @@
switch *n {
case 0:
c.Check(r.Method, check.Equals, "POST")
- c.Check(r.URL.Path, check.Equals, "/v2/create-user")
+ c.Check(r.URL.Path, check.Equals, "/v2/users")
var gotBody map[string]interface{}
dec := json.NewDecoder(r.Body)
err := dec.Decode(&gotBody)
c.Assert(err, check.IsNil)
- wantBody := make(map[string]interface{})
+ wantBody := map[string]interface{}{
+ "action": "create",
+ }
if email != "" {
wantBody["email"] = "one@email.com"
}
@@ -56,7 +58,7 @@
if email == "" {
fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`)
} else {
- fmt.Fprintln(w, `{"type": "sync", "result": {"username": "karl", "ssh-keys": ["a","b"]}}`)
+ fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`)
}
default:
c.Fatalf("got too many requests (now on %d)", *n+1)
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,57 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "errors"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/release"
+)
+
+type cmdBootvars struct {
+ UC20 bool `long:"uc20"`
+ RootDir string `long:"root-dir"`
+}
+
+func init() {
+ cmd := addDebugCommand("boot-vars",
+ "(internal) obtain the snapd boot variables",
+ "(internal) obtain the snapd boot variables",
+ func() flags.Commander {
+ return &cmdBootvars{}
+ }, map[string]string{
+ "uc20": i18n.G("Whether to use uc20 boot vars or not"),
+ "root-dir": i18n.G("Root directory to look for boot variables in"),
+ }, nil)
+ if release.OnClassic {
+ cmd.hidden = true
+ }
+}
+
+func (x *cmdBootvars) Execute(args []string) error {
+ if release.OnClassic {
+ return errors.New(`the "boot-vars" command is not available on classic systems`)
+ }
+ return boot.DumpBootVars(Stdout, x.RootDir, x.UC20)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,62 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
+ snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/release"
+)
+
+func (s *SnapSuite) TestDebugBootvars(c *check.C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
+ bloader.BootVars = map[string]string{
+ "snap_mode": "try",
+ "unrelated": "thing",
+ "snap_core": "core18_1.snap",
+ "snap_try_core": "core18_2.snap",
+ "snap_kernel": "pc-kernel_3.snap",
+ "snap_try_kernel": "pc-kernel_4.snap",
+ }
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "boot-vars"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `snap_mode=try
+snap_core=core18_1.snap
+snap_try_core=core18_2.snap
+snap_kernel=pc-kernel_3.snap
+snap_try_kernel=pc-kernel_4.snap
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestDebugBootvarsNotOnClassic(c *check.C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "boot-vars"})
+ c.Assert(err, check.ErrorMatches, `the "boot-vars" command is not available on classic systems`)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,54 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/jessevdk/go-flags"
+)
+
+type cmdGetModel struct {
+ clientMixin
+}
+
+func init() {
+ cmd := addDebugCommand("model",
+ "(internal) obtain the active model assertion",
+ "(internal) obtain the active model assertion",
+ func() flags.Commander {
+ return &cmdGetModel{}
+ }, nil, nil)
+ cmd.hidden = true
+}
+
+func (x *cmdGetModel) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ var resp struct {
+ Model string `json:"model"`
+ }
+ if err := x.client.DebugGet("model", &resp, nil); err != nil {
+ return err
+ }
+ fmt.Fprintf(Stdout, "%s\n", resp.Model)
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,56 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestGetModel(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/debug")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=model")
+ data, err := ioutil.ReadAll(r.Body)
+ c.Check(err, check.IsNil)
+ c.Check(string(data), check.Equals, "")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"model": "some-model-json"}}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "model"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "some-model-json\n")
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_state.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_state.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,335 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "text/tabwriter"
+
+ "github.com/jessevdk/go-flags"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type cmdDebugState struct {
+ timeMixin
+
+ st *state.State
+
+ Changes bool `long:"changes"`
+ TaskID string `long:"task"`
+ ChangeID string `long:"change"`
+
+ IsSeeded bool `long:"is-seeded"`
+
+ // flags for --change=N output
+ DotOutput bool `long:"dot"` // XXX: mildly useful (too crowded in many cases), but let's have it just in case
+ // When inspecting errors/undone tasks, those in Hold state are usually irrelevant, make it possible to ignore them
+ NoHoldState bool `long:"no-hold"`
+
+ Positional struct {
+ StateFilePath string `positional-args:"yes" positional-arg-name:""`
+ } `positional-args:"yes"`
+}
+
+var cmdDebugStateShortHelp = i18n.G("Inspect a snapd state file.")
+var cmdDebugStateLongHelp = i18n.G("Inspect a snapd state file, bypassing snapd API.")
+
+type byChangeID []*state.Change
+
+func (c byChangeID) Len() int { return len(c) }
+func (c byChangeID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
+func (c byChangeID) Less(i, j int) bool { return c[i].ID() < c[j].ID() }
+
+func loadState(path string) (*state.State, error) {
+ if path == "" {
+ path = "state.json"
+ }
+ r, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read the state file: %s", err)
+ }
+ defer r.Close()
+
+ return state.ReadState(nil, r)
+}
+
+func init() {
+ addDebugCommand("state", cmdDebugStateShortHelp, cmdDebugStateLongHelp, func() flags.Commander {
+ return &cmdDebugState{}
+ }, timeDescs.also(map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "change": i18n.G("ID of the change to inspect"),
+ "task": i18n.G("ID of the task to inspect"),
+ "dot": i18n.G("Dot (graphviz) output"),
+ "no-hold": i18n.G("Omit tasks in 'Hold' state in the change output"),
+ "changes": i18n.G("List all changes"),
+ "is-seeded": i18n.G("Output seeding status (true or false)"),
+ }), nil)
+}
+
+type byLaneAndWaitTaskChain []*state.Task
+
+func (t byLaneAndWaitTaskChain) Len() int { return len(t) }
+func (t byLaneAndWaitTaskChain) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
+func (t byLaneAndWaitTaskChain) Less(i, j int) bool {
+ // cover the typical case (just one lane), and order by first lane
+ if t[i].Lanes()[0] == t[j].Lanes()[0] {
+ return waitChainSearch(t[i], t[j])
+ }
+ return t[i].Lanes()[0] < t[j].Lanes()[0]
+}
+
+func waitChainSearch(startT, searchT *state.Task) bool {
+ for _, cand := range startT.HaltTasks() {
+ if cand == searchT {
+ return true
+ }
+ if waitChainSearch(cand, searchT) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (c *cmdDebugState) writeDotOutput(st *state.State, changeID string) error {
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.Change(changeID)
+ if chg == nil {
+ return fmt.Errorf("no such change: %s", changeID)
+ }
+
+ fmt.Fprintf(Stdout, "digraph D{\n")
+ tasks := chg.Tasks()
+ for _, t := range tasks {
+ if c.NoHoldState && t.Status() == state.HoldStatus {
+ continue
+ }
+ fmt.Fprintf(Stdout, " %s [label=%q];\n", t.ID(), t.Kind())
+ for _, wt := range t.WaitTasks() {
+ if c.NoHoldState && wt.Status() == state.HoldStatus {
+ continue
+ }
+ fmt.Fprintf(Stdout, " %s -> %s;\n", t.ID(), wt.ID())
+ }
+ }
+ fmt.Fprintf(Stdout, "}\n")
+
+ return nil
+}
+
+func (c *cmdDebugState) showTasks(st *state.State, changeID string) error {
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.Change(changeID)
+ if chg == nil {
+ return fmt.Errorf("no such change: %s", changeID)
+ }
+
+ tasks := chg.Tasks()
+ sort.Sort(byLaneAndWaitTaskChain(tasks))
+
+ w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
+ fmt.Fprintf(w, "Lanes\tID\tStatus\tSpawn\tReady\tKind\tSummary\n")
+ for _, t := range tasks {
+ if c.NoHoldState && t.Status() == state.HoldStatus {
+ continue
+ }
+ var lanes []string
+ for _, lane := range t.Lanes() {
+ lanes = append(lanes, fmt.Sprintf("%d", lane))
+ }
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
+ strings.Join(lanes, ","),
+ t.ID(),
+ t.Status().String(),
+ c.fmtTime(t.SpawnTime()),
+ c.fmtTime(t.ReadyTime()),
+ t.Kind(),
+ t.Summary())
+ }
+
+ w.Flush()
+
+ for _, t := range tasks {
+ logs := t.Log()
+ if len(logs) > 0 {
+ fmt.Fprintf(Stdout, "---\n")
+ fmt.Fprintf(Stdout, "%s %s\n", t.ID(), t.Summary())
+ for _, log := range logs {
+ fmt.Fprintf(Stdout, " %s\n", log)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (c *cmdDebugState) showChanges(st *state.State) error {
+ st.Lock()
+ defer st.Unlock()
+
+ changes := st.Changes()
+ sort.Sort(byChangeID(changes))
+
+ w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
+ fmt.Fprintf(w, "ID\tStatus\tSpawn\tReady\tLabel\tSummary\n")
+ for _, chg := range changes {
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
+ chg.ID(),
+ chg.Status().String(),
+ c.fmtTime(chg.SpawnTime()),
+ c.fmtTime(chg.ReadyTime()),
+ chg.Kind(),
+ chg.Summary())
+ }
+ w.Flush()
+
+ return nil
+}
+
+func (c *cmdDebugState) showIsSeeded(st *state.State) error {
+ st.Lock()
+ defer st.Unlock()
+
+ var isSeeded bool
+ err := st.Get("seeded", &isSeeded)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ fmt.Fprintf(Stdout, "%v\n", isSeeded)
+
+ return nil
+}
+
+func (c *cmdDebugState) showTask(st *state.State, taskID string) error {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task(taskID)
+ if task == nil {
+ return fmt.Errorf("no such task: %s", taskID)
+ }
+
+ termWidth, _ := termSize()
+ termWidth -= 3
+ if termWidth > 100 {
+ // any wider than this and it gets hard to read
+ termWidth = 100
+ }
+
+ // the output of 'debug task' is yaml'ish
+ fmt.Fprintf(Stdout, "id: %s\nkind: %s\nsummary: %s\nstatus: %s\n",
+ taskID, task.Kind(),
+ task.Summary(),
+ task.Status().String())
+ log := task.Log()
+ if len(log) > 0 {
+ fmt.Fprintf(Stdout, "log: |\n")
+ for _, msg := range log {
+ if err := wrapLine(Stdout, []rune(msg), " ", termWidth); err != nil {
+ break
+ }
+ }
+ fmt.Fprintln(Stdout)
+ }
+
+ fmt.Fprintf(Stdout, "halt-tasks:")
+ if len(task.HaltTasks()) == 0 {
+ fmt.Fprintln(Stdout, " []")
+ } else {
+ fmt.Fprintln(Stdout)
+ for _, ht := range task.HaltTasks() {
+ fmt.Fprintf(Stdout, " - %s (%s)\n", ht.Kind(), ht.ID())
+ }
+ }
+
+ return nil
+}
+
+func (c *cmdDebugState) Execute(args []string) error {
+ st, err := loadState(c.Positional.StateFilePath)
+ if err != nil {
+ return err
+ }
+
+ // check valid combinations of args
+ var cmds []string
+ if c.Changes {
+ cmds = append(cmds, "--changes")
+ }
+ if c.ChangeID != "" {
+ cmds = append(cmds, "--change=")
+ }
+ if c.TaskID != "" {
+ cmds = append(cmds, "--task=")
+ }
+ if c.IsSeeded != false {
+ cmds = append(cmds, "--is-seeded")
+ }
+ if len(cmds) > 1 {
+ return fmt.Errorf("cannot use %s and %s together", cmds[0], cmds[1])
+ }
+
+ if c.IsSeeded {
+ return c.showIsSeeded(st)
+ }
+
+ if c.DotOutput && c.ChangeID == "" {
+ return fmt.Errorf("--dot can only be used with --change=")
+ }
+ if c.NoHoldState && c.ChangeID == "" {
+ return fmt.Errorf("--no-hold can only be used with --change=")
+ }
+
+ if c.Changes {
+ return c.showChanges(st)
+ }
+
+ if c.ChangeID != "" {
+ _, err := strconv.ParseInt(c.ChangeID, 0, 64)
+ if err != nil {
+ return fmt.Errorf("invalid change: %s", c.ChangeID)
+ }
+ if c.DotOutput {
+ return c.writeDotOutput(st, c.ChangeID)
+ }
+ return c.showTasks(st, c.ChangeID)
+ }
+
+ if c.TaskID != "" {
+ _, err := strconv.ParseInt(c.TaskID, 0, 64)
+ if err != nil {
+ return fmt.Errorf("invalid task: %s", c.TaskID)
+ }
+ return c.showTask(st, c.TaskID)
+ }
+
+ // show changes by default
+ return c.showChanges(st)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_state_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_state_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,232 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ main "github.com/snapcore/snapd/cmd/snap"
+)
+
+var stateJSON = []byte(`
+{
+ "last-task-id": 31,
+ "last-change-id": 2,
+
+ "data": {
+ "snaps": {},
+ "seeded": true
+ },
+ "changes": {
+ "1": {
+ "id": "1",
+ "kind": "install-snap",
+ "summary": "install a snap",
+ "status": 0,
+ "data": {"snap-names": ["a"]},
+ "task-ids": ["11","12"]
+ },
+ "2": {
+ "id": "2",
+ "kind": "revert-snap",
+ "summary": "revert c snap",
+ "status": 0,
+ "data": {"snap-names": ["c"]},
+ "task-ids": ["21","31"]
+ }
+ },
+ "tasks": {
+ "11": {
+ "id": "11",
+ "change": "1",
+ "kind": "download-snap",
+ "summary": "Download snap a from channel edge",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "edge",
+ "flags": 1
+ }},
+ "halt-tasks": ["12"]
+ },
+ "12": {"id": "12", "change": "1", "kind": "some-other-task"},
+ "21": {
+ "id": "21",
+ "change": "2",
+ "kind": "download-snap",
+ "summary": "Download snap b from channel beta",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "beta",
+ "flags": 2
+ }},
+ "halt-tasks": ["12"]
+ },
+ "31": {
+ "id": "31",
+ "change": "2",
+ "kind": "prepare-snap",
+ "summary": "Prepare snap c",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "stable",
+ "flags": 1073741828
+ }},
+ "halt-tasks": ["12"],
+ "log": ["logline1", "logline2"]
+ }
+ }
+}
+`)
+
+func (s *SnapSuite) TestDebugChanges(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--abs-time", "--changes", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Matches,
+ "ID Status Spawn Ready Label Summary\n"+
+ "1 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z install-snap install a snap\n"+
+ "2 Done 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z revert-snap revert c snap\n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugChangesMissingState(c *C) {
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--changes", "/missing-state.json"})
+ c.Check(err, ErrorMatches, "cannot read the state file: open /missing-state.json: no such file or directory")
+}
+
+func (s *SnapSuite) TestDebugTask(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=31", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "id: 31\n"+
+ "kind: prepare-snap\n"+
+ "summary: Prepare snap c\n"+
+ "status: Done\n"+
+ "log: |\n"+
+ " logline1\n"+
+ " logline2\n"+
+ "\n"+
+ "halt-tasks:\n"+
+ " - some-other-task (12)\n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugTaskEmptyLists(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=12", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "id: 12\n"+
+ "kind: some-other-task\n"+
+ "summary: \n"+
+ "status: Do\n"+
+ "halt-tasks: []\n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugTaskMissingState(c *C) {
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=1", "/missing-state.json"})
+ c.Check(err, ErrorMatches, "cannot read the state file: open /missing-state.json: no such file or directory")
+}
+
+func (s *SnapSuite) TestDebugTaskNoSuchTaskError(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=99", stateFile})
+ c.Check(err, ErrorMatches, "no such task: 99")
+}
+
+func (s *SnapSuite) TestDebugTaskMutuallyExclusiveCommands(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=99", "--changes", stateFile})
+ c.Check(err, ErrorMatches, "cannot use --changes and --task= together")
+
+ _, err = main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--changes", "--change=1", stateFile})
+ c.Check(err, ErrorMatches, "cannot use --changes and --change= together")
+
+ _, err = main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--change=1", "--task=1", stateFile})
+ c.Check(err, ErrorMatches, "cannot use --change= and --task= together")
+
+ _, err = main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--change=1", "--is-seeded", stateFile})
+ c.Check(err, ErrorMatches, "cannot use --change= and --is-seeded together")
+}
+
+func (s *SnapSuite) TestDebugTasks(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--abs-time", "--change=1", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Matches,
+ "Lanes ID Status Spawn Ready Kind Summary\n"+
+ "0 11 Done 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z download-snap Download snap a from channel edge\n"+
+ "0 12 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z some-other-task \n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugTasksMissingState(c *C) {
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--change=1", "/missing-state.json"})
+ c.Check(err, ErrorMatches, "cannot read the state file: open /missing-state.json: no such file or directory")
+}
+
+func (s *SnapSuite) TestDebugIsSeededHappy(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--is-seeded", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Matches, "true\n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugIsSeededNo(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, []byte("{}"), 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--is-seeded", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Matches, "false\n")
+ c.Check(s.Stderr(), Equals, "")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,294 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdChangeTimings struct {
+ changeIDMixin
+ EnsureTag string `long:"ensure" choice:"auto-refresh" choice:"become-operational" choice:"refresh-catalogs" choice:"refresh-hints" choice:"seed"`
+ All bool `long:"all"`
+ StartupTag string `long:"startup" choice:"load-state" choice:"ifacemgr"`
+ Verbose bool `long:"verbose"`
+}
+
+func init() {
+ addDebugCommand("timings",
+ i18n.G("Get the timings of the tasks of a change"),
+ i18n.G("The timings command displays details about the time each task runs."),
+ func() flags.Commander {
+ return &cmdChangeTimings{}
+ }, changeIDMixinOptDesc.also(map[string]string{
+ "ensure": i18n.G("Show timings for a change related to the given Ensure activity (one of: auto-refresh, become-operational, refresh-catalogs, refresh-hints, seed)"),
+ "all": i18n.G("Show timings for all executions of the given Ensure or startup activity, not just the latest"),
+ "startup": i18n.G("Show timings for the startup of given subsystem (one of: load-state, ifacemgr)"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "verbose": i18n.G("Show more information"),
+ }), changeIDMixinArgDesc)
+}
+
+type Timing struct {
+ Level int `json:"level,omitempty"`
+ Label string `json:"label,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Duration time.Duration `json:"duration,omitempty"`
+}
+
+func formatDuration(dur time.Duration) string {
+ return fmt.Sprintf("%dms", dur/time.Millisecond)
+}
+
+func printTiming(w io.Writer, verbose bool, nestLevel int, id, status, doingTimeStr, undoingTimeStr, label, summary string) {
+ // don't display id for nesting>1, instead show nesting indicator
+ if nestLevel > 0 {
+ id = strings.Repeat(" ", nestLevel) + "^"
+ }
+ // Duration formats to 17m14.342s or 2.038s or 970ms, so with
+ // 11 chars we can go up to 59m59.999s
+ if verbose {
+ fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\t%s\n", id, status, doingTimeStr, undoingTimeStr, label, strings.Repeat(" ", 2*nestLevel)+summary)
+ } else {
+ fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\n", id, status, doingTimeStr, undoingTimeStr, strings.Repeat(" ", 2*nestLevel)+summary)
+ }
+}
+
+func printTaskTiming(w io.Writer, t *Timing, verbose, doing bool) {
+ var doingTimeStr, undoingTimeStr string
+ if doing {
+ doingTimeStr = formatDuration(t.Duration)
+ undoingTimeStr = "-"
+ } else {
+ if doing {
+ doingTimeStr = "-"
+ undoingTimeStr = formatDuration(t.Duration)
+ }
+ }
+ printTiming(w, verbose, t.Level+1, "", "", doingTimeStr, undoingTimeStr, t.Label, t.Summary)
+}
+
+// sortTimingsTasks sorts tasks from changeTimings by lane and ready time with special treatment of lane 0 tasks:
+// - tasks from lanes >0 are grouped by lanes and sorted by ready time.
+// - tasks from lane 0 are sorted by ready time and inserted before and after other lanes based on the min/max
+// ready times of non-zero lanes.
+// - tasks from lane 0 with ready time between non-zero lane tasks are not really expected in our system and will
+// appear after non-zero lane tasks.
+func sortTimingsTasks(timings map[string]changeTimings) []string {
+ tasks := make([]string, 0, len(timings))
+
+ var minReadyTime time.Time
+ // determine min ready time from all non-zero lane tasks
+ for taskID, taskData := range timings {
+ if taskData.Lane > 0 {
+ if minReadyTime.IsZero() {
+ minReadyTime = taskData.ReadyTime
+ }
+ if taskData.ReadyTime.Before(minReadyTime) {
+ minReadyTime = taskData.ReadyTime
+ }
+ }
+ tasks = append(tasks, taskID)
+ }
+
+ sort.Slice(tasks, func(i, j int) bool {
+ t1 := timings[tasks[i]]
+ t2 := timings[tasks[j]]
+ if t1.Lane != t2.Lane {
+ // if either t1 or t2 is from lane 0, then it comes before or after non-zero lane tasks
+ if t1.Lane == 0 {
+ return t1.ReadyTime.Before(minReadyTime)
+ }
+ if t2.Lane == 0 {
+ return !t2.ReadyTime.Before(minReadyTime)
+ }
+ // different lanes (but neither of them is 0), order by lane
+ return t1.Lane < t2.Lane
+ }
+
+ // same lane - order by ready time
+ return t1.ReadyTime.Before(t2.ReadyTime)
+ })
+
+ return tasks
+}
+
+func (x *cmdChangeTimings) printChangeTimings(w io.Writer, timing *timingsData) error {
+ tasks := sortTimingsTasks(timing.ChangeTimings)
+
+ for _, taskID := range tasks {
+ chgTiming := timing.ChangeTimings[taskID]
+ doingTime := formatDuration(timing.ChangeTimings[taskID].DoingTime)
+ if chgTiming.DoingTime == 0 {
+ doingTime = "-"
+ }
+ undoingTime := formatDuration(timing.ChangeTimings[taskID].UndoingTime)
+ if chgTiming.UndoingTime == 0 {
+ undoingTime = "-"
+ }
+
+ printTiming(w, x.Verbose, 0, taskID, chgTiming.Status, doingTime, undoingTime, chgTiming.Kind, chgTiming.Summary)
+ for _, nested := range timing.ChangeTimings[taskID].DoingTimings {
+ showDoing := true
+ printTaskTiming(w, &nested, x.Verbose, showDoing)
+ }
+ for _, nested := range timing.ChangeTimings[taskID].UndoingTimings {
+ showDoing := false
+ printTaskTiming(w, &nested, x.Verbose, showDoing)
+ }
+ }
+
+ return nil
+}
+
+func (x *cmdChangeTimings) printEnsureTimings(w io.Writer, timings []*timingsData) error {
+ for _, td := range timings {
+ printTiming(w, x.Verbose, 0, x.EnsureTag, "", formatDuration(td.TotalDuration), "-", "", "")
+ for _, t := range td.EnsureTimings {
+ printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary)
+ }
+
+ // change is optional for ensure timings
+ if td.ChangeID != "" {
+ x.printChangeTimings(w, td)
+ }
+ }
+ return nil
+}
+
+func (x *cmdChangeTimings) printStartupTimings(w io.Writer, timings []*timingsData) error {
+ for _, td := range timings {
+ printTiming(w, x.Verbose, 0, x.StartupTag, "", formatDuration(td.TotalDuration), "-", "", "")
+ for _, t := range td.StartupTimings {
+ printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary)
+ }
+ }
+ return nil
+}
+
+type changeTimings struct {
+ Status string `json:"status,omitempty"`
+ Kind string `json:"kind,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Lane int `json:"lane,omitempty"`
+ ReadyTime time.Time `json:"ready-time,omitempty"`
+ DoingTime time.Duration `json:"doing-time,omitempty"`
+ UndoingTime time.Duration `json:"undoing-time,omitempty"`
+ DoingTimings []Timing `json:"doing-timings,omitempty"`
+ UndoingTimings []Timing `json:"undoing-timings,omitempty"`
+}
+
+type timingsData struct {
+ ChangeID string `json:"change-id"`
+ EnsureTimings []Timing `json:"ensure-timings,omitempty"`
+ StartupTimings []Timing `json:"startup-timings,omitempty"`
+ TotalDuration time.Duration `json:"total-duration,omitempty"`
+ // ChangeTimings are indexed by task id
+ ChangeTimings map[string]changeTimings `json:"change-timings,omitempty"`
+}
+
+func (x *cmdChangeTimings) checkConflictingFlags() error {
+ var i int
+ for _, opt := range []string{string(x.Positional.ID), x.StartupTag, x.EnsureTag} {
+ if opt != "" {
+ i++
+ if i > 1 {
+ return fmt.Errorf("cannot use change id, 'startup' or 'ensure' together")
+ }
+ }
+ }
+
+ if x.All && (x.Positional.ID != "" || x.LastChangeType != "") {
+ return fmt.Errorf("cannot use 'all' with change id or 'last'")
+ }
+ return nil
+}
+
+func (x *cmdChangeTimings) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ if err := x.checkConflictingFlags(); err != nil {
+ return err
+ }
+
+ var chgid string
+ var err error
+
+ if x.EnsureTag == "" && x.StartupTag == "" {
+ if x.Positional.ID == "" && x.LastChangeType == "" {
+ // GetChangeID() below checks for empty change ID / --last, check them early here to provide more helpful error message
+ return fmt.Errorf("please provide change ID or type with --last=, or query for --ensure= or --startup=")
+ }
+
+ // GetChangeID takes care of --last=... if change ID was not specified by the user
+ chgid, err = x.GetChangeID()
+ if err != nil {
+ return err
+ }
+ }
+
+ // gather debug timings first
+ var timings []*timingsData
+ var allEnsures string
+ if x.All {
+ allEnsures = "true"
+ } else {
+ allEnsures = "false"
+ }
+ if err := x.client.DebugGet("change-timings", &timings, map[string]string{"change-id": chgid, "ensure": x.EnsureTag, "all": allEnsures, "startup": x.StartupTag}); err != nil {
+ return err
+ }
+
+ w := tabWriter()
+ if x.Verbose {
+ fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tLabel\tSummary\n", "Doing", "Undoing")
+ } else {
+ fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tSummary\n", "Doing", "Undoing")
+ }
+
+ // If a specific change was requested, we expect exactly one timingsData element.
+ // If "ensure" activity was requested, we may get multiple elements (for multiple executions of the ensure)
+ if chgid != "" && len(timings) > 0 {
+ x.printChangeTimings(w, timings[0])
+ }
+
+ if x.EnsureTag != "" {
+ x.printEnsureTimings(w, timings)
+ }
+
+ if x.StartupTag != "" {
+ x.printStartupTimings(w, timings)
+ }
+
+ w.Flush()
+ fmt.Fprintln(Stdout)
+
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,337 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/snap"
+)
+
+type timingsCmdArgs struct {
+ args, stdout, stderr, error string
+}
+
+var timingsTests = []timingsCmdArgs{{
+ args: "debug timings",
+ error: "please provide change ID or type with --last=, or query for --ensure= or --startup=",
+}, {
+ args: "debug timings --ensure=seed 9",
+ error: "cannot use change id, 'startup' or 'ensure' together",
+}, {
+ args: "debug timings --ensure=seed --startup=ifacemgr",
+ error: "cannot use change id, 'startup' or 'ensure' together",
+}, {
+ args: "debug timings --last=install --all",
+ error: "cannot use 'all' with change id or 'last'",
+}, {
+ args: "debug timings --last=remove",
+ error: `no changes of type "remove" found`,
+}, {
+ args: "debug timings --startup=load-state 9",
+ error: "cannot use change id, 'startup' or 'ensure' together",
+}, {
+ args: "debug timings --all 9",
+ error: "cannot use 'all' with change id or 'last'",
+}, {
+ args: "debug timings --last=install",
+ stdout: "ID Status Doing Undoing Summary\n" +
+ "40 Doing 910ms - lane 0 task bar summary\n" +
+ " ^ 1ms - foo summary\n" +
+ " ^ 1ms - bar summary\n" +
+ "41 Done 210ms - lane 1 task baz summary\n" +
+ "42 Done 310ms - lane 1 task boo summary\n" +
+ "43 Done 310ms - lane 0 task doh summary\n\n",
+}, {
+ args: "debug timings 1",
+ stdout: "ID Status Doing Undoing Summary\n" +
+ "40 Doing 910ms - lane 0 task bar summary\n" +
+ " ^ 1ms - foo summary\n" +
+ " ^ 1ms - bar summary\n" +
+ "41 Done 210ms - lane 1 task baz summary\n" +
+ "42 Done 310ms - lane 1 task boo summary\n" +
+ "43 Done 310ms - lane 0 task doh summary\n\n",
+}, {
+ args: "debug timings 1 --verbose",
+ stdout: "ID Status Doing Undoing Label Summary\n" +
+ "40 Doing 910ms - bar lane 0 task bar summary\n" +
+ " ^ 1ms - foo foo summary\n" +
+ " ^ 1ms - bar bar summary\n" +
+ "41 Done 210ms - baz lane 1 task baz summary\n" +
+ "42 Done 310ms - boo lane 1 task boo summary\n" +
+ "43 Done 310ms - doh lane 0 task doh summary\n\n",
+}, {
+ args: "debug timings --ensure=seed",
+ stdout: "ID Status Doing Undoing Summary\n" +
+ "seed 8ms - \n" +
+ " ^ 8ms - baz summary\n" +
+ " ^ 8ms - booze summary\n" +
+ "40 Doing 910ms - task bar summary\n" +
+ " ^ 1ms - foo summary\n" +
+ " ^ 1ms - bar summary\n\n",
+}, {
+ args: "debug timings --ensure=seed --all",
+ stdout: "ID Status Doing Undoing Summary\n" +
+ "seed 8ms - \n" +
+ " ^ 8ms - bar summary 1\n" +
+ " ^ 8ms - bar summary 2\n" +
+ "40 Doing 910ms - task bar summary\n" +
+ " ^ 1ms - foo summary\n" +
+ " ^ 1ms - bar summary\n" +
+ "seed 7ms - \n" +
+ " ^ 7ms - baz summary 2\n" +
+ "60 Doing 910ms - task bar summary\n" +
+ " ^ 1ms - foo summary\n" +
+ " ^ 1ms - bar summary\n\n",
+}, {
+ args: "debug timings --ensure=seed --all --verbose",
+ stdout: "ID Status Doing Undoing Label Summary\n" +
+ "seed 8ms - \n" +
+ " ^ 8ms - abc bar summary 1\n" +
+ " ^ 8ms - abc bar summary 2\n" +
+ "40 Doing 910ms - bar task bar summary\n" +
+ " ^ 1ms - foo foo summary\n" +
+ " ^ 1ms - bar bar summary\n" +
+ "seed 7ms - \n" +
+ " ^ 7ms - ghi baz summary 2\n" +
+ "60 Doing 910ms - bar task bar summary\n" +
+ " ^ 1ms - foo foo summary\n" +
+ " ^ 1ms - bar bar summary\n\n",
+}, {
+ args: "debug timings --startup=ifacemgr",
+ stdout: "ID Status Doing Undoing Summary\n" +
+ "ifacemgr 8ms - \n" +
+ " ^ 8ms - baz summary\n" +
+ " ^ 8ms - booze summary\n\n",
+}, {
+ args: "debug timings --startup=ifacemgr --all",
+ stdout: "ID Status Doing Undoing Summary\n" +
+ "ifacemgr 8ms - \n" +
+ " ^ 8ms - baz summary\n" +
+ "ifacemgr 9ms - \n" +
+ " ^ 9ms - baz summary\n\n",
+}}
+
+func (s *SnapSuite) TestGetDebugTimings(c *C) {
+ s.mockCmdTimingsAPI(c)
+
+ restore := main.MockIsStdinTTY(true)
+ defer restore()
+
+ for _, test := range timingsTests {
+ s.stdout.Truncate(0)
+ s.stderr.Truncate(0)
+
+ c.Logf("Test: %s", test.args)
+
+ _, err := main.Parser(main.Client()).ParseArgs(strings.Fields(test.args))
+ if test.error != "" {
+ c.Check(err, ErrorMatches, test.error)
+ } else {
+ c.Check(err, IsNil)
+ c.Check(s.Stderr(), Equals, test.stderr)
+ c.Check(s.Stdout(), Equals, test.stdout)
+ }
+ }
+}
+
+func (s *SnapSuite) mockCmdTimingsAPI(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Assert(r.Method, Equals, "GET")
+
+ if r.URL.Path == "/v2/debug" {
+ q := r.URL.Query()
+ aspect := q.Get("aspect")
+ c.Assert(aspect, Equals, "change-timings")
+
+ changeID := q.Get("change-id")
+ ensure := q.Get("ensure")
+ startup := q.Get("startup")
+ all := q.Get("all")
+
+ switch {
+ case changeID == "1":
+ // lane 0 and lane 1 tasks, interleaved
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[
+ {"change-id":"1", "change-timings":{
+ "41":{"doing-time":210000000, "status": "Done", "lane": 1, "ready-time": "2016-04-22T01:02:04Z", "kind": "baz", "summary": "lane 1 task baz summary"},
+ "43":{"doing-time":310000000, "status": "Done", "ready-time": "2016-04-25T01:02:04Z", "kind": "doh", "summary": "lane 0 task doh summary"},
+ "40":{"doing-time":910000000, "status": "Doing", "ready-time": "2016-04-20T00:00:00Z", "kind": "bar", "summary": "lane 0 task bar summary",
+ "doing-timings":[
+ {"label":"foo", "summary": "foo summary", "duration": 1000001},
+ {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002}
+ ]},
+ "42":{"doing-time":310000000, "status": "Done", "lane": 1, "ready-time": "2016-04-23T01:02:04Z", "kind": "boo", "summary": "lane 1 task boo summary"}
+ }}]}`)
+ case ensure == "seed" && all == "false":
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[
+ {"change-id":"1",
+ "total-duration": 8000002,
+ "ensure-timings": [
+ {"label":"baz", "summary": "baz summary", "duration": 8000001},
+ {"level":1, "label":"booze", "summary": "booze summary", "duration": 8000002}
+ ],
+ "change-timings":{
+ "40":{"doing-time":910000000, "status": "Doing", "kind": "bar", "summary": "task bar summary",
+ "doing-timings":[
+ {"label":"foo", "summary": "foo summary", "duration": 1000001},
+ {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002}
+ ]}}}]}`)
+ case ensure == "seed" && all == "true":
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[
+ {"change-id":"1",
+ "total-duration": 8000002,
+ "ensure-timings": [
+ {"label":"abc", "summary": "bar summary 1", "duration": 8000001},
+ {"label":"abc", "summary": "bar summary 2", "duration": 8000002}
+ ],
+ "change-timings":{
+ "40":{"doing-time":910000000, "status": "Doing", "kind": "bar", "summary": "task bar summary",
+ "doing-timings":[
+ {"label":"foo", "summary": "foo summary", "duration": 1000001},
+ {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002}
+ ]}}},
+ {"change-id":"2",
+ "total-duration": 7000002,
+ "ensure-timings": [{"label":"ghi", "summary": "baz summary 2", "duration": 7000002}],
+ "change-timings":{
+ "60":{"doing-time":910000000, "status": "Doing", "kind": "bar", "summary": "task bar summary",
+ "doing-timings":[
+ {"label":"foo", "summary": "foo summary", "duration": 1000001},
+ {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002}
+ ]}}}]}`)
+ case startup == "ifacemgr" && all == "false":
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[
+ {"total-duration": 8000002, "startup-timings": [
+ {"label":"baz", "summary": "baz summary", "duration": 8000001},
+ {"level":1, "label":"booze", "summary": "booze summary", "duration": 8000002}
+ ]}]}`)
+ case startup == "ifacemgr" && all == "true":
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[
+ {"total-duration": 8000002, "startup-timings": [
+ {"label":"baz", "summary": "baz summary", "duration": 8000001}
+ ]},
+ {"total-duration": 9000002, "startup-timings": [
+ {"label":"baz", "summary": "baz summary", "duration": 9000001}
+ ]}]}`)
+ default:
+ c.Errorf("unexpected request: %s, %s, %s", changeID, ensure, all)
+ }
+ return
+ }
+
+ // request for all changes on --last=...
+ if r.URL.Path == "/v2/changes" {
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[{
+ "id": "1",
+ "kind": "install-snap",
+ "summary": "a",
+ "status": "Doing",
+ "ready": false,
+ "spawn-time": "2016-04-21T01:02:03Z",
+ "ready-time": "2016-04-21T01:02:04Z",
+ "tasks": [{"id":"99", "kind": "bar", "summary": ".", "status": "Doing", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}]
+ }]}`)
+ return
+ }
+ c.Errorf("unexpected path %q", r.URL.Path)
+ })
+}
+
+type TaskDef struct {
+ TaskID string
+ Lane int
+ ReadyTime time.Time
+}
+
+func (s *SnapSuite) TestSortTimingsTasks(c *C) {
+ mkTime := func(timeStr string) time.Time {
+ t, err := time.Parse(time.RFC3339, timeStr)
+ c.Assert(err, IsNil)
+ return t
+ }
+
+ testData := []struct {
+ ChangeTimings map[string]main.ChangeTimings
+ Expected []string
+ }{{
+ // nothing to do
+ ChangeTimings: map[string]main.ChangeTimings{},
+ Expected: []string{},
+ }, {
+ ChangeTimings: map[string]main.ChangeTimings{
+ // tasks in lane 0 only
+ "1": {ReadyTime: mkTime("2019-04-21T00:00:00Z")},
+ "2": {ReadyTime: mkTime("2019-05-21T00:00:00Z")},
+ "3": {ReadyTime: mkTime("2019-02-21T00:00:00Z")},
+ "4": {ReadyTime: mkTime("2019-03-21T00:00:00Z")},
+ "5": {ReadyTime: mkTime("2019-01-21T00:00:00Z")},
+ },
+ Expected: []string{"5", "3", "4", "1", "2"},
+ }, {
+ // task in lane 1 with a task in lane 0 before and after it
+ ChangeTimings: map[string]main.ChangeTimings{
+ "1": {Lane: 1, ReadyTime: mkTime("2019-01-21T00:00:00Z")},
+ "2": {Lane: 0, ReadyTime: mkTime("2019-01-20T00:00:00Z")},
+ "3": {Lane: 0, ReadyTime: mkTime("2019-01-22T00:00:00Z")},
+ },
+ Expected: []string{"2", "1", "3"},
+ }, {
+ // tasks in lane 1 only
+ ChangeTimings: map[string]main.ChangeTimings{
+ "1": {Lane: 1, ReadyTime: mkTime("2019-01-21T00:00:00Z")},
+ "2": {Lane: 1, ReadyTime: mkTime("2019-01-20T00:00:00Z")},
+ "3": {Lane: 1, ReadyTime: mkTime("2019-01-16T00:00:00Z")},
+ },
+ Expected: []string{"3", "2", "1"},
+ }, {
+ // tasks in lanes 0, 1, 2 with tasks from line 0 before and after lanes 1, 2
+ ChangeTimings: map[string]main.ChangeTimings{
+ "1": {Lane: 1, ReadyTime: mkTime("2019-01-21T00:00:00Z")},
+ "2": {Lane: 0, ReadyTime: mkTime("2019-01-19T00:00:00Z")},
+ "3": {Lane: 2, ReadyTime: mkTime("2019-01-20T00:00:00Z")},
+ "4": {Lane: 0, ReadyTime: mkTime("2019-01-25T00:00:00Z")},
+ "5": {Lane: 1, ReadyTime: mkTime("2019-01-20T00:00:00Z")},
+ "6": {Lane: 2, ReadyTime: mkTime("2019-01-21T00:00:00Z")},
+ "7": {Lane: 0, ReadyTime: mkTime("2019-01-18T00:00:00Z")},
+ "8": {Lane: 0, ReadyTime: mkTime("2019-01-27T00:00:00Z")},
+ },
+ Expected: []string{"7", "2", "5", "1", "3", "6", "4", "8"},
+ }, {
+ // pathological case: lane 0 tasks have ready-time between lane 1 tasks
+ ChangeTimings: map[string]main.ChangeTimings{
+ "1": {Lane: 1, ReadyTime: mkTime("2019-01-20T00:00:00Z")},
+ "2": {Lane: 1, ReadyTime: mkTime("2019-01-30T00:00:00Z")},
+ "3": {Lane: 0, ReadyTime: mkTime("2019-01-27T00:00:00Z")},
+ "4": {Lane: 0, ReadyTime: mkTime("2019-01-25T00:00:00Z")},
+ },
+ Expected: []string{"1", "2", "4", "3"},
+ }}
+
+ for _, data := range testData {
+ tasks := main.SortTimingsTasks(data.ChangeTimings)
+ c.Check(tasks, DeepEquals, data.Expected)
+ }
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,50 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/seed"
+)
+
+type cmdValidateSeed struct {
+ Positionals struct {
+ SeedYamlPath flags.Filename `positional-arg-name:""`
+ } `positional-args:"true" required:"true"`
+}
+
+func init() {
+ cmd := addDebugCommand("validate-seed",
+ "(internal) validate seed.yaml",
+ "(internal) validate seed.yaml",
+ func() flags.Commander {
+ return &cmdValidateSeed{}
+ }, nil, nil)
+ cmd.hidden = true
+}
+
+func (x *cmdValidateSeed) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ return seed.ValidateFromYaml(string(x.Positionals.SeedYamlPath))
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,45 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019-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 .
+ *
+ */
+
+package main_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestDebugValidateCannotValidate(c *C) {
+ tmpf := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(tmpf, []byte(`
+snaps:
+ -
+ name: core
+ channel: stable
+ file: core_6673.snap
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "validate-seed", tmpf})
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+ - no seed assertions`)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_delete_key.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_delete_key.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_delete_key.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_delete_key.go 2020-06-05 13:13:49.000000000 +0000
@@ -48,6 +48,7 @@
desc: i18n.G("Name of key to delete"),
}})
cmd.hidden = true
+ cmd.completeHidden = true
}
func (x *cmdDeleteKey) Execute(args []string) error {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect.go 2020-06-05 13:13:49.000000000 +0000
@@ -30,6 +30,7 @@
type cmdDisconnect struct {
waitMixin
+ Forget bool `long:"forget"`
Positionals struct {
Offer disconnectSlotOrPlugSpec `required:"true"`
Use disconnectSlotSpec
@@ -49,12 +50,17 @@
Disconnects everything from the provided plug or slot.
The snap name may be omitted for the core snap.
+
+When an automatic connection is manually disconnected, its disconnected state
+is retained after a snap refresh. The --forget flag can be added to the
+disconnect command to reset this behaviour, and consequently re-enable
+an automatic reconnection after a snap refresh.
`)
func init() {
addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander {
return &cmdDisconnect{}
- }, waitDescs, []argDesc{
+ }, waitDescs.also(map[string]string{"forget": "Forget remembered state about the given connection."}), []argDesc{
// TRANSLATORS: This needs to begin with < and end with >
{name: i18n.G(":")},
// TRANSLATORS: This needs to begin with < and end with >
@@ -80,7 +86,8 @@
return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", use.Snap)
}
- id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name)
+ opts := &client.DisconnectOptions{Forget: x.Forget}
+ id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name, opts)
if err != nil {
if client.IsInterfacesUnchangedError(err) {
fmt.Fprintf(Stdout, i18n.G("No connections to disconnect"))
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -46,9 +46,15 @@
Disconnects everything from the provided plug or slot.
The snap name may be omitted for the core snap.
+When an automatic connection is manually disconnected, its disconnected state
+is retained after a snap refresh. The --forget flag can be added to the
+disconnect command to reset this behaviour, and consequently re-enable
+an automatic reconnection after a snap refresh.
+
[disconnect command options]
--no-wait Do not wait for the operation to finish but just print
the change id.
+ --forget Forget remembered state about the given connection.
`
s.testSubCommandHelp(c, "disconnect", msg)
}
@@ -73,6 +79,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -88,6 +95,43 @@
c.Assert(s.Stderr(), Equals, "")
}
+func (s *SnapSuite) TestDisconnectWithForgetFlag(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/interfaces":
+ c.Check(r.Method, Equals, "POST")
+ c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{
+ "action": "disconnect",
+ "forget": true,
+ "plugs": []interface{}{
+ map[string]interface{}{
+ "snap": "consumer",
+ "plug": "plug",
+ },
+ },
+ "slots": []interface{}{
+ map[string]interface{}{
+ "snap": "producer",
+ "slot": "slot",
+ },
+ },
+ })
+ w.WriteHeader(202)
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ case "/v2/changes/zzz":
+ c.Check(r.Method, Equals, "GET")
+ fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ })
+ rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "--forget", "consumer:plug", "producer:slot"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Assert(s.Stdout(), Equals, "")
+ c.Assert(s.Stderr(), Equals, "")
+}
+
func (s *SnapSuite) TestDisconnectEverythingFromSpecificSlot(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
@@ -108,6 +152,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -143,6 +188,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -172,7 +218,7 @@
func (s *SnapSuite) TestDisconnectCompletion(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
- case "/v2/interfaces":
+ case "/v2/connections":
c.Assert(r.Method, Equals, "GET")
EncodeResponseBody(c, w, map[string]interface{}{
"type": "sync",
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-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
@@ -36,8 +36,11 @@
type cmdDownload struct {
channelMixin
- Revision string `long:"revision"`
+ Revision string `long:"revision"`
+ Basename string `long:"basename"`
+ TargetDir string `long:"target-directory"`
+ CohortKey string `long:"cohort"`
Positional struct {
Snap remoteSnapName
} `positional-args:"true" required:"true"`
@@ -53,7 +56,14 @@
addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander {
return &cmdDownload{}
}, channelDescs.also(map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
"revision": i18n.G("Download the given revision of a snap, to which you must have developer access"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "cohort": i18n.G("Download from the given cohort"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "basename": i18n.G("Use this basename for the snap and assertion files (defaults to _)"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "target-directory": i18n.G("Download to this directory (defaults to the current directory)"),
}), []argDesc{{
name: "",
// TRANSLATORS: This should not start with a lowercase letter.
@@ -87,7 +97,26 @@
return assertPath, err
}
+func printInstallHint(assertPath, snapPath string) {
+ // simplify paths
+ wd, _ := os.Getwd()
+ if p, err := filepath.Rel(wd, assertPath); err == nil {
+ assertPath = p
+ }
+ if p, err := filepath.Rel(wd, snapPath); err == nil {
+ snapPath = p
+ }
+ // add a hint what to do with the downloaded snap (LP:1676707)
+ fmt.Fprintf(Stdout, i18n.G(`Install the snap with:
+ snap ack %s
+ snap install %s
+`), assertPath, snapPath)
+}
+
func (x *cmdDownload) Execute(args []string) error {
+ if strings.ContainsRune(x.Basename, filepath.Separator) {
+ return fmt.Errorf(i18n.G("cannot specify a path in basename (use --target-dir for that)"))
+ }
if err := x.setChannelFromCommandline(); err != nil {
return err
}
@@ -103,6 +132,9 @@
if x.Channel != "" {
return fmt.Errorf(i18n.G("cannot specify both channel and revision"))
}
+ if x.CohortKey != "" {
+ return fmt.Errorf(i18n.G("cannot specify both cohort and revision"))
+ }
var err error
revision, err = snap.ParseRevision(x.Revision)
if err != nil {
@@ -119,10 +151,15 @@
fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName)
dlOpts := image.DownloadOptions{
- TargetDir: "", // cwd
+ TargetDir: x.TargetDir,
+ Basename: x.Basename,
Channel: x.Channel,
+ CohortKey: x.CohortKey,
+ Revision: revision,
+ // if something goes wrong, don't force it to start over again
+ LeavePartialOnError: true,
}
- snapPath, snapInfo, err := tsto.DownloadSnap(snapName, revision, &dlOpts)
+ snapPath, snapInfo, _, err := tsto.DownloadSnap(snapName, dlOpts)
if err != nil {
return err
}
@@ -132,20 +169,7 @@
if err != nil {
return err
}
-
- // simplify paths
- wd, _ := os.Getwd()
- if p, err := filepath.Rel(wd, assertPath); err == nil {
- assertPath = p
- }
- if p, err := filepath.Rel(wd, snapPath); err == nil {
- snapPath = p
- }
- // add a hint what to do with the downloaded snap (LP:1676707)
- fmt.Fprintf(Stdout, i18n.G(`Install the snap with:
- snap ack %s
- snap install %s
-`), assertPath, snapPath)
+ printInstallHint(assertPath, snapPath)
return nil
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,84 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "os"
+ "path/filepath"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+// these only cover errors that happen before hitting the network,
+// because we're not (yet!) mocking the tooling store
+
+func (s *SnapSuite) TestDownloadBadBasename(c *check.C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{
+ "download", "--basename=/foo", "a-snap",
+ })
+
+ c.Check(err, check.ErrorMatches, "cannot specify a path in basename .use --target-dir for that.")
+}
+
+func (s *SnapSuite) TestDownloadBadChannelCombo(c *check.C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{
+ "download", "--beta", "--channel=foo", "a-snap",
+ })
+
+ c.Check(err, check.ErrorMatches, "Please specify a single channel")
+}
+
+func (s *SnapSuite) TestDownloadCohortAndRevision(c *check.C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{
+ "download", "--cohort=what", "--revision=1234", "a-snap",
+ })
+
+ c.Check(err, check.ErrorMatches, "cannot specify both cohort and revision")
+}
+
+func (s *SnapSuite) TestDownloadChannelAndRevision(c *check.C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{
+ "download", "--beta", "--revision=1234", "a-snap",
+ })
+
+ c.Check(err, check.ErrorMatches, "cannot specify both channel and revision")
+}
+
+func (s *SnapSuite) TestPrintInstalHint(c *check.C) {
+ snap.PrintInstallHint("foo_1.assert", "foo_1.snap")
+ c.Check(s.Stdout(), check.Equals, `Install the snap with:
+ snap ack foo_1.assert
+ snap install foo_1.snap
+`)
+ s.stdout.Reset()
+
+ cwd, err := os.Getwd()
+ c.Assert(err, check.IsNil)
+ as := filepath.Join(cwd, "some-dir/foo_1.assert")
+ sn := filepath.Join(cwd, "some-dir/foo_1.snap")
+ snap.PrintInstallHint(as, sn)
+ c.Check(s.Stdout(), check.Equals, `Install the snap with:
+ snap ack some-dir/foo_1.assert
+ snap install some-dir/foo_1.snap
+`)
+
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_find.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_find.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_find.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_find.go 2020-06-05 13:13:49.000000000 +0000
@@ -38,7 +38,7 @@
var shortFindHelp = i18n.G("Find packages to install")
var longFindHelp = i18n.G(`
-The find command queries the store for available packages in the stable channel.
+The find command queries the store for available packages.
With the --private flag, which requires the user to be logged-in to the store
(see 'snap help login'), it instead searches for private snaps that the user
@@ -153,7 +153,7 @@
clientMixin
Private bool `long:"private"`
Narrow bool `long:"narrow"`
- Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified"`
+ Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified" default-mask:"-"`
Positional struct {
Query string
} `positional-args:"yes"`
@@ -165,11 +165,11 @@
return &cmdFind{}
}, colorDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
- "private": i18n.G("Search private snaps"),
+ "private": i18n.G("Search private snaps."),
// TRANSLATORS: This should not start with a lowercase letter.
- "narrow": i18n.G("Only search for snaps in “stable”"),
+ "narrow": i18n.G("Only search for snaps in “stable”."),
// TRANSLATORS: This should not start with a lowercase letter.
- "section": i18n.G("Restrict the search to a given section"),
+ "section": i18n.G("Restrict the search to a given section."),
}), []argDesc{{
// TRANSLATORS: This needs to begin with < and end with >
name: i18n.G(""),
@@ -224,9 +224,9 @@
}
opts := &client.FindOptions{
- Private: x.Private,
- Section: string(x.Section),
Query: x.Positional.Query,
+ Section: string(x.Section),
+ Private: x.Private,
}
if !x.Narrow {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_first_boot.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_first_boot.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_first_boot.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_first_boot.go 2020-06-05 13:13:49.000000000 +0000
@@ -29,7 +29,7 @@
func init() {
cmd := addCommand("firstboot",
- "Internal",
+ "Deprecated (hidden)",
"The firstboot command is only retained for backwards compatibility.",
func() flags.Commander {
return &cmdInternalFirstBoot{}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration.go 2020-06-05 13:13:49.000000000 +0000
@@ -26,15 +26,24 @@
)
type cmdGetBaseDeclaration struct {
+ get bool
clientMixin
}
func init() {
cmd := addDebugCommand("get-base-declaration",
+ "(internal) obtain the base declaration for all interfaces (deprecated)",
+ "(internal) obtain the base declaration for all interfaces (deprecated)",
+ func() flags.Commander {
+ return &cmdGetBaseDeclaration{}
+ }, nil, nil)
+ cmd.hidden = true
+
+ cmd = addDebugCommand("base-declaration",
"(internal) obtain the base declaration for all interfaces",
"(internal) obtain the base declaration for all interfaces",
func() flags.Commander {
- return &cmdGetBaseDeclaration{}
+ return &cmdGetBaseDeclaration{get: true}
}, nil, nil)
cmd.hidden = true
}
@@ -46,7 +55,13 @@
var resp struct {
BaseDeclaration string `json:"base-declaration"`
}
- if err := x.client.Debug("get-base-declaration", nil, &resp); err != nil {
+ var err error
+ if x.get {
+ err = x.client.DebugGet("base-declaration", &resp, nil)
+ } else {
+ err = x.client.Debug("get-base-declaration", nil, &resp)
+ }
+ if err != nil {
return err
}
fmt.Fprintf(Stdout, "%s\n", resp.BaseDeclaration)
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -53,3 +53,28 @@
c.Check(s.Stdout(), check.Equals, "hello\n")
c.Check(s.Stderr(), check.Equals, "")
}
+
+func (s *SnapSuite) TestBaseDeclaration(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/debug")
+ c.Check(r.URL.RawQuery, check.Equals, "aspect=base-declaration")
+ data, err := ioutil.ReadAll(r.Body)
+ c.Check(err, check.IsNil)
+ c.Check(data, check.HasLen, 0)
+ fmt.Fprintln(w, `{"type": "sync", "result": {"base-declaration": "hello"}}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "base-declaration"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "hello\n")
+ c.Check(s.Stderr(), check.Equals, "")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get.go 2020-06-05 13:13:49.000000000 +0000
@@ -37,13 +37,12 @@
$ snap get snap-name username
frank
-If multiple option names are provided, a document is returned:
+If multiple option names are provided, the corresponding values are returned:
$ snap get snap-name username password
- {
- "username": "frank",
- "password": "..."
- }
+ Key Value
+ username frank
+ password ...
Nested values may be retrieved via a dotted path:
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_handle_link.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_handle_link.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_handle_link.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_handle_link.go 2020-06-05 13:13:49.000000000 +0000
@@ -28,7 +28,7 @@
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/i18n"
- "github.com/snapcore/snapd/userd/ui"
+ "github.com/snapcore/snapd/usersession/userd/ui"
)
type cmdHandleLink struct {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_help.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_help.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help.go 2020-06-05 13:13:49.000000000 +0000
@@ -22,6 +22,8 @@
import (
"bytes"
"fmt"
+ "io"
+ "regexp"
"strings"
"unicode/utf8"
@@ -46,8 +48,18 @@
// on which help is being requested (like "snap foo
// --help", active is foo), or nil in the toplevel.
if parser.Command.Active == nil {
- // toplevel --help will get handled via ErrCommandRequired
- return nil
+ // this means *either* a bare 'snap --help',
+ // *or* 'snap --help command'
+ //
+ // If we return nil in the first case go-flags
+ // will throw up an ErrCommandRequired on its
+ // own, but in the second case it'll go on to
+ // run the command, which is very unexpected.
+ //
+ // So we force the ErrCommandRequired here.
+
+ // toplevel --help gets handled via ErrCommandRequired
+ return &flags.Error{Type: flags.ErrCommandRequired}
}
// not toplevel, so ask for regular help
return &flags.Error{Type: flags.ErrHelp}
@@ -69,7 +81,7 @@
Manpage bool `long:"man" hidden:"true"`
Positional struct {
// TODO: find a way to make Command tab-complete
- Sub string `positional-arg-name:""`
+ Subs []string `positional-arg-name:""`
} `positional-args:"yes"`
parser *flags.Parser
}
@@ -88,10 +100,11 @@
cmd.parser = parser
}
-// manfixer is a hackish way to get the generated manpage into section 8
-// (go-flags doesn't have an option for this; I'll be proposing something
-// there soon, but still waiting on some other PRs to make it through)
+// manfixer is a hackish way to fix drawbacks in the generated manpage:
+// - no way to get it into section 8
+// - duplicated TP lines that break older groff (e.g. 14.04), lp:1814767
type manfixer struct {
+ bytes.Buffer
done bool
}
@@ -100,13 +113,20 @@
w.done = true
if bytes.HasPrefix(buf, []byte(".TH snap 1 ")) {
// io.Writer.Write must not modify the buffer, even temporarily
- n, _ := Stdout.Write(buf[:9])
- Stdout.Write([]byte{'8'})
- m, err := Stdout.Write(buf[10:])
+ n, _ := w.Buffer.Write(buf[:9])
+ w.Buffer.Write([]byte{'8'})
+ m, err := w.Buffer.Write(buf[10:])
return n + m + 1, err
}
}
- return Stdout.Write(buf)
+ return w.Buffer.Write(buf)
+}
+
+var tpRegexp = regexp.MustCompile(`(?m)(?:^\.TP\n)+`)
+
+func (w *manfixer) flush() {
+ str := tpRegexp.ReplaceAllLiteralString(w.Buffer.String(), ".TP\n")
+ io.Copy(Stdout, strings.NewReader(str))
}
func (cmd cmdHelp) Execute(args []string) error {
@@ -116,27 +136,36 @@
if cmd.Manpage {
// you shouldn't try to to combine --man with --all nor a
// subcommand, but --man is hidden so no real need to check.
- cmd.parser.WriteManPage(&manfixer{})
+ out := &manfixer{}
+ cmd.parser.WriteManPage(out)
+ out.flush()
return nil
}
if cmd.All {
- if cmd.Positional.Sub != "" {
+ if len(cmd.Positional.Subs) > 0 {
return fmt.Errorf(i18n.G("help accepts a command, or '--all', but not both."))
}
printLongHelp(cmd.parser)
return nil
}
- if cmd.Positional.Sub != "" {
- subcmd := cmd.parser.Find(cmd.Positional.Sub)
+ var subcmd = cmd.parser.Command
+ for _, subname := range cmd.Positional.Subs {
+ subcmd = subcmd.Find(subname)
if subcmd == nil {
- return fmt.Errorf(i18n.G("Unknown command %q. Try 'snap help'."), cmd.Positional.Sub)
+ sug := "snap help"
+ if x := cmd.parser.Command.Active; x != nil && x.Name != "help" {
+ sug = "snap help " + x.Name
+ }
+ // TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help '
+ return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), subname, sug)
}
// this makes "snap help foo" work the same as "snap foo --help"
cmd.parser.Command.Active = subcmd
+ }
+ if subcmd != cmd.parser.Command {
return &flags.Error{Type: flags.ErrHelp}
}
-
return &flags.Error{Type: flags.ErrCommandRequired}
}
@@ -171,7 +200,7 @@
}, {
Label: i18n.G("Configuration"),
Description: i18n.G("system administration and configuration"),
- Commands: []string{"get", "set", "wait"},
+ Commands: []string{"get", "set", "unset", "wait"},
}, {
Label: i18n.G("Account"),
Description: i18n.G("authentication to snapd and the snap store"),
@@ -179,7 +208,7 @@
}, {
Label: i18n.G("Permissions"),
Description: i18n.G("manage permissions"),
- Commands: []string{"interfaces", "interface", "connect", "disconnect"},
+ Commands: []string{"connections", "interface", "connect", "disconnect"},
}, {
Label: i18n.G("Snapshots"),
Description: i18n.G("archives of snap data"),
@@ -187,11 +216,11 @@
}, {
Label: i18n.G("Other"),
Description: i18n.G("miscellanea"),
- Commands: []string{"version", "warnings", "okay"},
+ Commands: []string{"version", "warnings", "okay", "ack", "known", "model", "create-cohort"},
}, {
Label: i18n.G("Development"),
Description: i18n.G("developer-oriented features"),
- Commands: []string{"run", "pack", "try", "ack", "known", "download"},
+ Commands: []string{"run", "pack", "try", "download", "prepare-image"},
},
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_help_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_help_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -41,6 +41,7 @@
{"snap", "help"},
{"snap", "--help"},
{"snap", "-h"},
+ {"snap", "--help", "install"},
} {
s.ResetStdStreams()
@@ -174,3 +175,24 @@
c.Check(s.Stdout(), check.Matches, `\.TH snap 8 (?s).*`)
}
+
+func (s *SnapSuite) TestManpageNoDoubleTP(c *check.C) {
+ origArgs := os.Args
+ defer func() { os.Args = origArgs }()
+ os.Args = []string{"snap", "help", "--man"}
+
+ err := snap.RunMain()
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.Stdout(), check.Not(check.Matches), `(?s).*(?m-s)^\.TP\n\.TP$(?s-m).*`)
+
+}
+
+func (s *SnapSuite) TestBadSub(c *check.C) {
+ origArgs := os.Args
+ defer func() { os.Args = origArgs }()
+ os.Args = []string{"snap", "debug", "brotato"}
+
+ err := snap.RunMain()
+ c.Assert(err, check.ErrorMatches, `unknown command "brotato", see 'snap help debug'.`)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info.go 2020-06-05 13:13:49.000000000 +0000
@@ -23,6 +23,7 @@
"fmt"
"io"
"path/filepath"
+ "strconv"
"strings"
"text/tabwriter"
"time"
@@ -34,9 +35,12 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snapfile"
+ "github.com/snapcore/snapd/snap/squashfs"
"github.com/snapcore/snapd/strutil"
)
@@ -73,106 +77,66 @@
}), nil)
}
-func norm(path string) string {
- path = filepath.Clean(path)
- if osutil.IsDirectory(path) {
- path = path + "/"
- }
-
- return path
-}
-
-func maybePrintPrice(w io.Writer, snap *client.Snap, resInfo *client.ResultInfo) {
- if resInfo == nil {
+func (iw *infoWriter) maybePrintHealth() {
+ if iw.localSnap == nil {
return
}
- price, currency, err := getPrice(snap.Prices, resInfo.SuggestedCurrency)
- if err != nil {
- return
+ health := iw.localSnap.Health
+ if health == nil {
+ if !iw.verbose {
+ return
+ }
+ health = &client.SnapHealth{
+ Status: "unknown",
+ Message: "health has not been set",
+ }
}
- fmt.Fprintf(w, "price:\t%s\n", formatPrice(price, currency))
-}
-
-func maybePrintType(w io.Writer, t string) {
- // XXX: using literals here until we reshuffle snap & client properly
- // (and os->core rename happens, etc)
- switch t {
- case "", "app", "application":
+ if health.Status == "okay" && !iw.verbose {
return
- case "os":
- t = "core"
}
- fmt.Fprintf(w, "type:\t%s\n", t)
-}
-
-func maybePrintID(w io.Writer, snap *client.Snap) {
- if snap.ID != "" {
- fmt.Fprintf(w, "snap-id:\t%s\n", snap.ID)
+ fmt.Fprintln(iw, "health:")
+ fmt.Fprintf(iw, " status:\t%s\n", health.Status)
+ if health.Message != "" {
+ wrapGeneric(iw, quotedIfNeeded(health.Message), " message:\t", " ", iw.termWidth)
}
-}
-
-func maybePrintBase(w io.Writer, base string, verbose bool) {
- if verbose && base != "" {
- fmt.Fprintf(w, "base:\t%s\n", base)
+ if health.Code != "" {
+ fmt.Fprintf(iw, " code:\t%s\n", health.Code)
}
+ if !health.Timestamp.IsZero() {
+ fmt.Fprintf(iw, " checked:\t%s\n", iw.fmtTime(health.Timestamp))
+ }
+ if !health.Revision.Unset() {
+ fmt.Fprintf(iw, " revision:\t%s\n", health.Revision)
+ }
+ iw.Flush()
}
-func tryDirect(w io.Writer, path string, verbose bool) bool {
- path = norm(path)
-
- snapf, err := snap.Open(path)
+func clientSnapFromPath(path string) (*client.Snap, error) {
+ snapf, err := snapfile.Open(path)
if err != nil {
- return false
+ return nil, err
}
-
- var sha3_384 string
- if verbose && !osutil.IsDirectory(path) {
- var err error
- sha3_384, _, err = asserts.SnapFileSHA3_384(path)
- if err != nil {
- return false
- }
- }
-
info, err := snap.ReadInfoFromSnapFile(snapf, nil)
if err != nil {
- return false
+ return nil, err
}
- fmt.Fprintf(w, "path:\t%q\n", path)
- fmt.Fprintf(w, "name:\t%s\n", info.InstanceName())
- fmt.Fprintf(w, "summary:\t%s\n", formatSummary(info.Summary()))
-
- var notes *Notes
- if verbose {
- fmt.Fprintln(w, "notes:\t")
- fmt.Fprintf(w, " confinement:\t%s\n", info.Confinement)
- if info.Broken == "" {
- fmt.Fprintln(w, " broken:\tfalse")
- } else {
- fmt.Fprintf(w, " broken:\ttrue (%s)\n", info.Broken)
- }
- } else {
- notes = NotesFromInfo(info)
- }
- fmt.Fprintf(w, "version:\t%s %s\n", info.Version, notes)
- maybePrintType(w, string(info.Type))
- maybePrintBase(w, info.Base, verbose)
- if sha3_384 != "" {
- fmt.Fprintf(w, "sha3-384:\t%s\n", sha3_384)
+ direct, err := cmd.ClientSnapFromSnapInfo(info)
+ if err != nil {
+ return nil, err
}
- return true
+ return direct, nil
}
-func coalesce(snaps ...*client.Snap) *client.Snap {
- for _, s := range snaps {
- if s != nil {
- return s
- }
+func norm(path string) string {
+ path = filepath.Clean(path)
+ if osutil.IsDirectory(path) {
+ path = path + "/"
}
- return nil
+
+ return path
}
// runesTrimRightSpace returns text, with any trailing whitespace dropped.
@@ -195,9 +159,38 @@
return -1
}
-// wrapLine wraps a line to fit into width, preserving the line's indent, and
+// wrapLine wraps a line, assumed to be part of a block-style yaml
+// string, to fit into termWidth, preserving the line's indent, and
// writes it out prepending padding to each line.
-func wrapLine(out io.Writer, text []rune, pad string, width int) error {
+func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error {
+ // discard any trailing whitespace
+ text = runesTrimRightSpace(text)
+ // establish the indent of the whole block
+ idx := 0
+ for idx < len(text) && unicode.IsSpace(text[idx]) {
+ idx++
+ }
+ indent := pad + string(text[:idx])
+ text = text[idx:]
+ if len(indent) > termWidth/2 {
+ // If indent is too big there's not enough space for the actual
+ // text, in the pathological case the indent can even be bigger
+ // than the terminal which leads to lp:1828425.
+ // Rather than let that happen, give up.
+ indent = pad + " "
+ }
+ return wrapGeneric(out, text, indent, indent, termWidth)
+}
+
+// wrapFlow wraps the text using yaml's flow style, allowing indent
+// characters for the first line.
+func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error {
+ return wrapGeneric(out, text, indent, " ", termWidth)
+}
+
+// wrapGeneric wraps the given text to the given width, prefixing the
+// first line with indent and the remaining lines with indent2
+func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error {
// Note: this is _wrong_ for much of unicode (because the width of a rune on
// the terminal is anything between 0 and 2, not always 1 as this code
// assumes) but fixing that is Hard. Long story short, you can get close
@@ -210,20 +203,15 @@
// This (and possibly printDescr below) should move to strutil once
// we're happy with it getting wider (heh heh) use.
- // discard any trailing whitespace
- text = runesTrimRightSpace(text)
+ indentWidth := utf8.RuneCountInString(indent)
+ delta := indentWidth - utf8.RuneCountInString(indent2)
+ width := termWidth - indentWidth
+
// establish the indent of the whole block
- idx := 0
- for idx < len(text) && unicode.IsSpace(text[idx]) {
- idx++
- }
- indent := pad + string(text[:idx])
- text = text[idx:]
- width -= idx + utf8.RuneCountInString(pad)
var err error
for len(text) > width && err == nil {
// find a good place to chop the text
- idx = runesLastIndexSpace(text[:width+1])
+ idx := runesLastIndexSpace(text[:width+1])
if idx < 0 {
// there's no whitespace; just chop at line width
idx = width
@@ -234,6 +222,9 @@
idx++
}
text = text[idx:]
+ width += delta
+ indent = indent2
+ delta = 0
}
if err != nil {
return err
@@ -242,6 +233,20 @@
return err
}
+func quotedIfNeeded(raw string) []rune {
+ // simplest way of checking to see if it needs quoting is to try
+ raw = strings.TrimSpace(raw)
+ type T struct {
+ S string
+ }
+ if len(raw) == 0 {
+ raw = `""`
+ } else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil {
+ raw = strconv.Quote(raw)
+ }
+ return []rune(raw)
+}
+
// printDescr formats a given string (typically a snap description)
// in a user friendly way.
//
@@ -249,11 +254,11 @@
// - trim trailing whitespace
// - word wrap at "max" chars preserving line indent
// - keep \n intact and break there
-func printDescr(w io.Writer, descr string, max int) error {
+func printDescr(w io.Writer, descr string, termWidth int) error {
var err error
descr = strings.TrimRightFunc(descr, unicode.IsSpace)
for _, line := range strings.Split(descr, "\n") {
- err = wrapLine(w, []rune(line), " ", max)
+ err = wrapLine(w, []rune(line), " ", termWidth)
if err != nil {
break
}
@@ -261,37 +266,246 @@
return err
}
-func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) {
- if len(allApps) == 0 {
+type writeflusher interface {
+ io.Writer
+ Flush() error
+}
+
+type infoWriter struct {
+ // fields that are set every iteration
+ theSnap *client.Snap
+ diskSnap *client.Snap
+ localSnap *client.Snap
+ remoteSnap *client.Snap
+ resInfo *client.ResultInfo
+ path string
+ // fields that don't change and so can be set once
+ writeflusher
+ esc *escapes
+ termWidth int
+ fmtTime func(time.Time) string
+ absTime bool
+ verbose bool
+}
+
+func (iw *infoWriter) setupDiskSnap(path string, diskSnap *client.Snap) {
+ iw.localSnap, iw.remoteSnap, iw.resInfo = nil, nil, nil
+ iw.path = path
+ iw.diskSnap = diskSnap
+ iw.theSnap = diskSnap
+}
+
+func (iw *infoWriter) setupSnap(localSnap, remoteSnap *client.Snap, resInfo *client.ResultInfo) {
+ iw.path, iw.diskSnap = "", nil
+ iw.localSnap = localSnap
+ iw.remoteSnap = remoteSnap
+ iw.resInfo = resInfo
+ if localSnap != nil {
+ iw.theSnap = localSnap
+ } else {
+ iw.theSnap = remoteSnap
+ }
+}
+
+func (iw *infoWriter) maybePrintPrice() {
+ if iw.resInfo == nil {
+ return
+ }
+ price, currency, err := getPrice(iw.remoteSnap.Prices, iw.resInfo.SuggestedCurrency)
+ if err != nil {
+ return
+ }
+ fmt.Fprintf(iw, "price:\t%s\n", formatPrice(price, currency))
+}
+
+func (iw *infoWriter) maybePrintType() {
+ // XXX: using literals here until we reshuffle snap & client properly
+ // (and os->core rename happens, etc)
+ t := iw.theSnap.Type
+ switch t {
+ case "", "app", "application":
return
+ case "os":
+ t = "core"
+ }
+
+ fmt.Fprintf(iw, "type:\t%s\n", t)
+}
+
+func (iw *infoWriter) maybePrintID() {
+ if iw.theSnap.ID != "" {
+ fmt.Fprintf(iw, "snap-id:\t%s\n", iw.theSnap.ID)
}
+}
- commands := make([]string, 0, len(allApps))
- for _, app := range allApps {
+func (iw *infoWriter) maybePrintTrackingChannel() {
+ if iw.localSnap == nil {
+ return
+ }
+ if iw.localSnap.TrackingChannel == "" {
+ return
+ }
+ fmt.Fprintf(iw, "tracking:\t%s\n", iw.localSnap.TrackingChannel)
+}
+
+func (iw *infoWriter) maybePrintInstallDate() {
+ if iw.localSnap == nil {
+ return
+ }
+ if iw.localSnap.InstallDate.IsZero() {
+ return
+ }
+ fmt.Fprintf(iw, "refresh-date:\t%s\n", iw.fmtTime(iw.localSnap.InstallDate))
+}
+
+func (iw *infoWriter) maybePrintChinfo() {
+ if iw.diskSnap != nil {
+ return
+ }
+ chInfos := channelInfos{
+ chantpl: "%s%s:\t%s %s%*s %*s %s\n",
+ releasedfmt: "2006-01-02",
+ esc: iw.esc,
+ }
+ if iw.absTime {
+ chInfos.releasedfmt = time.RFC3339
+ }
+ if iw.remoteSnap != nil && iw.remoteSnap.Channels != nil && iw.remoteSnap.Tracks != nil {
+ iw.Flush()
+ chInfos.chantpl = "%s%s:\t%s\t%s\t%*s\t%*s\t%s\n"
+ chInfos.addFromRemote(iw.remoteSnap)
+ }
+ if iw.localSnap != nil {
+ chInfos.addFromLocal(iw.localSnap)
+ }
+ chInfos.dump(iw)
+}
+
+func (iw *infoWriter) maybePrintBase() {
+ if iw.verbose && iw.theSnap.Base != "" {
+ fmt.Fprintf(iw, "base:\t%s\n", iw.theSnap.Base)
+ }
+}
+
+func (iw *infoWriter) maybePrintPath() {
+ if iw.path != "" {
+ fmt.Fprintf(iw, "path:\t%q\n", iw.path)
+ }
+}
+
+func (iw *infoWriter) printName() {
+ fmt.Fprintf(iw, "name:\t%s\n", iw.theSnap.Name)
+}
+
+func (iw *infoWriter) printSummary() {
+ wrapFlow(iw, quotedIfNeeded(iw.theSnap.Summary), "summary:\t", iw.termWidth)
+}
+
+func (iw *infoWriter) maybePrintStoreURL() {
+ storeURL := ""
+ // XXX: store-url for local snaps comes from aux data, but that gets
+ // updated only when the snap is refreshed, be smart and poke remote
+ // snap info if available
+ switch {
+ case iw.theSnap.StoreURL != "":
+ storeURL = iw.theSnap.StoreURL
+ case iw.remoteSnap != nil && iw.remoteSnap.StoreURL != "":
+ storeURL = iw.remoteSnap.StoreURL
+ }
+ if storeURL == "" {
+ return
+ }
+ fmt.Fprintf(iw, "store-url:\t%s\n", storeURL)
+}
+
+func (iw *infoWriter) maybePrintPublisher() {
+ if iw.diskSnap != nil {
+ // snaps read from disk won't have a publisher
+ return
+ }
+ fmt.Fprintf(iw, "publisher:\t%s\n", longPublisher(iw.esc, iw.theSnap.Publisher))
+}
+
+func (iw *infoWriter) maybePrintStandaloneVersion() {
+ if iw.diskSnap == nil {
+ // snaps not read from disk will have version information shown elsewhere
+ return
+ }
+ version := iw.diskSnap.Version
+ if version == "" {
+ version = iw.esc.dash
+ }
+ // NotesFromRemote might be better called NotesFromNotInstalled but that's nasty
+ fmt.Fprintf(iw, "version:\t%s %s\n", version, NotesFromRemote(iw.diskSnap, nil))
+}
+
+func (iw *infoWriter) maybePrintBuildDate() {
+ if iw.diskSnap == nil {
+ return
+ }
+ if osutil.IsDirectory(iw.path) {
+ return
+ }
+ buildDate := squashfs.BuildDate(iw.path)
+ if buildDate.IsZero() {
+ return
+ }
+ fmt.Fprintf(iw, "build-date:\t%s\n", iw.fmtTime(buildDate))
+}
+
+func (iw *infoWriter) maybePrintContact() error {
+ contact := strings.TrimPrefix(iw.theSnap.Contact, "mailto:")
+ if contact == "" {
+ return nil
+ }
+ _, err := fmt.Fprintf(iw, "contact:\t%s\n", contact)
+ return err
+}
+
+func (iw *infoWriter) printLicense() {
+ license := iw.theSnap.License
+ if license == "" {
+ license = "unset"
+ }
+ fmt.Fprintf(iw, "license:\t%s\n", license)
+}
+
+func (iw *infoWriter) printDescr() {
+ fmt.Fprintln(iw, "description: |")
+ printDescr(iw, iw.theSnap.Description, iw.termWidth)
+}
+
+func (iw *infoWriter) maybePrintCommands() {
+ if len(iw.theSnap.Apps) == 0 {
+ return
+ }
+
+ commands := make([]string, 0, len(iw.theSnap.Apps))
+ for _, app := range iw.theSnap.Apps {
if app.IsService() {
continue
}
- cmdStr := snap.JoinSnapApp(snapName, app.Name)
+ cmdStr := snap.JoinSnapApp(iw.theSnap.Name, app.Name)
commands = append(commands, cmdStr)
}
if len(commands) == 0 {
return
}
- fmt.Fprintf(w, "commands:\n")
+ fmt.Fprintf(iw, "commands:\n")
for _, cmd := range commands {
- fmt.Fprintf(w, " - %s\n", cmd)
+ fmt.Fprintf(iw, " - %s\n", cmd)
}
}
-func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, n int) {
- if len(allApps) == 0 {
+func (iw *infoWriter) maybePrintServices() {
+ if len(iw.theSnap.Apps) == 0 {
return
}
- services := make([]string, 0, len(allApps))
- for _, app := range allApps {
+ services := make([]string, 0, len(iw.theSnap.Apps))
+ for _, app := range iw.theSnap.Apps {
if !app.IsService() {
continue
}
@@ -307,82 +521,159 @@
} else {
enabled = "disabled"
}
- services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(snapName, app.Name), app.Daemon, enabled, active))
+ services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(iw.theSnap.Name, app.Name), app.Daemon, enabled, active))
}
if len(services) == 0 {
return
}
- fmt.Fprintf(w, "services:\n")
+ fmt.Fprintf(iw, "services:\n")
for _, svc := range services {
- fmt.Fprintln(w, svc)
+ fmt.Fprintln(iw, svc)
+ }
+}
+
+func (iw *infoWriter) maybePrintNotes() {
+ if !iw.verbose {
+ return
+ }
+ fmt.Fprintln(iw, "notes:\t")
+ fmt.Fprintf(iw, " private:\t%t\n", iw.theSnap.Private)
+ fmt.Fprintf(iw, " confinement:\t%s\n", iw.theSnap.Confinement)
+ if iw.localSnap == nil {
+ return
}
+ jailMode := iw.localSnap.Confinement == client.DevModeConfinement && !iw.localSnap.DevMode
+ fmt.Fprintf(iw, " devmode:\t%t\n", iw.localSnap.DevMode)
+ fmt.Fprintf(iw, " jailmode:\t%t\n", jailMode)
+ fmt.Fprintf(iw, " trymode:\t%t\n", iw.localSnap.TryMode)
+ fmt.Fprintf(iw, " enabled:\t%t\n", iw.localSnap.Status == client.StatusActive)
+ if iw.localSnap.Broken == "" {
+ fmt.Fprintf(iw, " broken:\t%t\n", false)
+ } else {
+ fmt.Fprintf(iw, " broken:\t%t (%s)\n", true, iw.localSnap.Broken)
+ }
+
+ fmt.Fprintf(iw, " ignore-validation:\t%t\n", iw.localSnap.IgnoreValidation)
+ return
+}
+
+func (iw *infoWriter) maybePrintCohortKey() {
+ if !iw.verbose {
+ return
+ }
+ if iw.localSnap == nil {
+ return
+ }
+ coh := iw.localSnap.CohortKey
+ if coh == "" {
+ return
+ }
+ if isStdoutTTY {
+ // 15 is 1 + the length of "refresh-date: "
+ coh = strutil.ElliptLeft(iw.localSnap.CohortKey, iw.termWidth-15)
+ }
+ fmt.Fprintf(iw, "cohort:\t%s\n", coh)
+}
+
+func (iw *infoWriter) maybePrintSum() {
+ if !iw.verbose {
+ return
+ }
+ if iw.diskSnap == nil {
+ // TODO: expose the sha via /v2/snaps and /v2/find
+ return
+ }
+ if osutil.IsDirectory(iw.path) {
+ // no sha3_384 of a directory :-)
+ return
+ }
+ sha3_384, _, _ := asserts.SnapFileSHA3_384(iw.path)
+ if sha3_384 == "" {
+ return
+ }
+ fmt.Fprintf(iw, "sha3-384:\t%s\n", sha3_384)
}
var channelRisks = []string{"stable", "candidate", "beta", "edge"}
-// displayChannels displays channels and tracks in the right order
-func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) {
- fmt.Fprintln(w, "channels:")
+type channelInfo struct {
+ indent, name, version, released, revision, size, notes string
+}
+
+type channelInfos struct {
+ channels []*channelInfo
+ maxRevLen, maxSizeLen int
+ releasedfmt, chantpl string
+ needsHeader bool
+ esc *escapes
+}
- releasedfmt := "2006-01-02"
- if x.AbsTime {
- releasedfmt = time.RFC3339
+func (chInfos *channelInfos) add(indent, name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
+ chInfo := &channelInfo{
+ indent: indent,
+ name: name,
+ version: version,
+ revision: fmt.Sprintf("(%s)", revision),
+ size: strutil.SizeToStr(size),
+ notes: notes.String(),
+ }
+ if !released.IsZero() {
+ chInfo.released = released.Format(chInfos.releasedfmt)
+ }
+ if len(chInfo.revision) > chInfos.maxRevLen {
+ chInfos.maxRevLen = len(chInfo.revision)
}
+ if len(chInfo.size) > chInfos.maxSizeLen {
+ chInfos.maxSizeLen = len(chInfo.size)
+ }
+ chInfos.channels = append(chInfos.channels, chInfo)
+}
+
+func (chInfos *channelInfos) addFromLocal(local *client.Snap) {
+ chInfos.add("", "installed", local.Version, local.Revision, time.Time{}, local.InstalledSize, NotesFromLocal(local))
+}
- type chInfoT struct {
- name, version, released, revision, size, notes string
+func (chInfos *channelInfos) addOpenChannel(name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
+ chInfos.add(" ", name, version, revision, released, size, notes)
+}
+
+func (chInfos *channelInfos) addClosedChannel(name string, trackHasOpenChannel bool) {
+ chInfo := &channelInfo{indent: " ", name: name}
+ if trackHasOpenChannel {
+ chInfo.version = chInfos.esc.uparrow
+ } else {
+ chInfo.version = chInfos.esc.dash
}
- var chInfos []*chInfoT
- maxRevLen, maxSizeLen = revLen, sizeLen
+ chInfos.channels = append(chInfos.channels, chInfo)
+}
+
+func (chInfos *channelInfos) addFromRemote(remote *client.Snap) {
// order by tracks
for _, tr := range remote.Tracks {
trackHasOpenChannel := false
for _, risk := range channelRisks {
chName := fmt.Sprintf("%s/%s", tr, risk)
ch, ok := remote.Channels[chName]
- if tr == "latest" {
- chName = risk
- }
- chInfo := chInfoT{name: chName}
if ok {
- chInfo.version = ch.Version
- chInfo.revision = fmt.Sprintf("(%s)", ch.Revision)
- if len(chInfo.revision) > maxRevLen {
- maxRevLen = len(chInfo.revision)
- }
- chInfo.released = ch.ReleasedAt.Format(releasedfmt)
- chInfo.size = strutil.SizeToStr(ch.Size)
- if len(chInfo.size) > maxSizeLen {
- maxSizeLen = len(chInfo.size)
- }
- chInfo.notes = NotesFromChannelSnapInfo(ch).String()
+ chInfos.addOpenChannel(chName, ch.Version, ch.Revision, ch.ReleasedAt, ch.Size, NotesFromChannelSnapInfo(ch))
trackHasOpenChannel = true
} else {
- if trackHasOpenChannel {
- chInfo.version = esc.uparrow
- } else {
- chInfo.version = esc.dash
- }
+ chInfos.addClosedChannel(chName, trackHasOpenChannel)
}
- chInfos = append(chInfos, &chInfo)
}
}
-
- for _, chInfo := range chInfos {
- fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes)
- }
-
- return maxRevLen, maxSizeLen
+ chInfos.needsHeader = len(chInfos.channels) > 0
}
-func formatSummary(raw string) string {
- s, err := yaml.Marshal(raw)
- if err != nil {
- return fmt.Sprintf("cannot marshal summary: %s", err)
+func (chInfos *channelInfos) dump(w io.Writer) {
+ if chInfos.needsHeader {
+ fmt.Fprintln(w, "channels:")
+ }
+ for _, c := range chInfos.channels {
+ fmt.Fprintf(w, chInfos.chantpl, c.indent, c.name, c.version, c.released, chInfos.maxRevLen, c.revision, chInfos.maxSizeLen, c.size, c.notes)
}
- return strings.TrimSpace(string(s))
}
func (x *infoCmd) Execute([]string) error {
@@ -395,6 +686,14 @@
esc := x.getEscapes()
w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0)
+ iw := &infoWriter{
+ writeflusher: w,
+ esc: esc,
+ termWidth: termWidth,
+ verbose: x.Verbose,
+ fmtTime: x.fmtTime,
+ absTime: x.AbsTime,
+ }
noneOK := true
for i, snapName := range x.Positional.Snaps {
@@ -407,17 +706,18 @@
continue
}
- if tryDirect(w, snapName, x.Verbose) {
- noneOK = false
- continue
+ if diskSnap, err := clientSnapFromPath(snapName); err == nil {
+ iw.setupDiskSnap(norm(snapName), diskSnap)
+ } else {
+ remoteSnap, resInfo, _ := x.client.FindOne(snap.InstanceSnap(snapName))
+ localSnap, _, _ := x.client.Snap(snapName)
+ iw.setupSnap(localSnap, remoteSnap, resInfo)
}
- remote, resInfo, _ := x.client.FindOne(snapName)
- local, _, _ := x.client.Snap(snapName)
+ // note diskSnap == nil, or localSnap == nil and remoteSnap == nil
- both := coalesce(local, remote)
-
- if both == nil {
+ if iw.theSnap == nil {
if len(x.Positional.Snaps) == 1 {
+ w.Flush()
return fmt.Errorf("no snap found for %q", snapName)
}
@@ -426,80 +726,31 @@
}
noneOK = false
- fmt.Fprintf(w, "name:\t%s\n", both.Name)
- fmt.Fprintf(w, "summary:\t%s\n", formatSummary(both.Summary))
- fmt.Fprintf(w, "publisher:\t%s\n", longPublisher(esc, both.Publisher))
- if both.Contact != "" {
- fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:"))
- }
- license := both.License
- if license == "" {
- license = "unset"
- }
- fmt.Fprintf(w, "license:\t%s\n", license)
- maybePrintPrice(w, remote, resInfo)
- fmt.Fprintln(w, "description: |")
- printDescr(w, both.Description, termWidth)
- maybePrintCommands(w, snapName, both.Apps, termWidth)
- maybePrintServices(w, snapName, both.Apps, termWidth)
-
- if x.Verbose {
- fmt.Fprintln(w, "notes:\t")
- fmt.Fprintf(w, " private:\t%t\n", both.Private)
- fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement)
- }
-
- var notes *Notes
- if local != nil {
- if x.Verbose {
- jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode
- fmt.Fprintf(w, " devmode:\t%t\n", local.DevMode)
- fmt.Fprintf(w, " jailmode:\t%t\n", jailMode)
- fmt.Fprintf(w, " trymode:\t%t\n", local.TryMode)
- fmt.Fprintf(w, " enabled:\t%t\n", local.Status == client.StatusActive)
- if local.Broken == "" {
- fmt.Fprintf(w, " broken:\t%t\n", false)
- } else {
- fmt.Fprintf(w, " broken:\t%t (%s)\n", true, local.Broken)
- }
-
- fmt.Fprintf(w, " ignore-validation:\t%t\n", local.IgnoreValidation)
- } else {
- notes = NotesFromLocal(local)
- }
- }
+ iw.maybePrintPath()
+ iw.printName()
+ iw.printSummary()
+ iw.maybePrintHealth()
+ iw.maybePrintPublisher()
+ iw.maybePrintStoreURL()
+ iw.maybePrintStandaloneVersion()
+ iw.maybePrintBuildDate()
+ iw.maybePrintContact()
+ iw.printLicense()
+ iw.maybePrintPrice()
+ iw.printDescr()
+ iw.maybePrintCommands()
+ iw.maybePrintServices()
+ iw.maybePrintNotes()
// stops the notes etc trying to be aligned with channels
- w.Flush()
- maybePrintType(w, both.Type)
- maybePrintBase(w, both.Base, x.Verbose)
- maybePrintID(w, both)
- var localRev, localSize string
- var revLen, sizeLen int
- if local != nil {
- if local.TrackingChannel != "" {
- fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel)
- }
- if !local.InstallDate.IsZero() {
- fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate))
- }
- localRev = fmt.Sprintf("(%s)", local.Revision)
- revLen = len(localRev)
- localSize = strutil.SizeToStr(local.InstalledSize)
- sizeLen = len(localSize)
- }
-
- chantpl := "%s:\t%s %s%*s %*s %s\n"
- if remote != nil && remote.Channels != nil && remote.Tracks != nil {
- chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n"
-
- w.Flush()
- revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen)
- }
- if local != nil {
- fmt.Fprintf(w, chantpl,
- "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes)
- }
-
+ iw.Flush()
+ iw.maybePrintType()
+ iw.maybePrintBase()
+ iw.maybePrintSum()
+ iw.maybePrintID()
+ iw.maybePrintCohortKey()
+ iw.maybePrintTrackingChannel()
+ iw.maybePrintInstallDate()
+ iw.maybePrintChinfo()
}
w.Flush()
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -22,13 +22,18 @@
import (
"bytes"
"fmt"
+ "io/ioutil"
"net/http"
+ "path/filepath"
"time"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/client"
snap "github.com/snapcore/snapd/cmd/snap"
+ snaplib "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/snap/squashfs"
)
var cmdAppInfos = []client.AppInfo{{Name: "app1"}, {Name: "app2"}}
@@ -55,10 +60,17 @@
var _ = check.Suite(&infoSuite{})
+type flushBuffer struct{ bytes.Buffer }
+
+func (*flushBuffer) Flush() error { return nil }
+
func (s *infoSuite) TestMaybePrintServices(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
for _, infos := range [][]client.AppInfo{svcAppInfos, mixedAppInfos} {
- var buf bytes.Buffer
- snap.MaybePrintServices(&buf, "foo", infos, -1)
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos})
+ snap.MaybePrintServices(iw)
c.Check(buf.String(), check.Equals, `services:
foo.svc1: simple, disabled, active
@@ -68,18 +80,23 @@
}
func (s *infoSuite) TestMaybePrintServicesNoServices(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
for _, infos := range [][]client.AppInfo{cmdAppInfos, nil} {
- var buf bytes.Buffer
- snap.MaybePrintServices(&buf, "foo", infos, -1)
-
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos})
+ snap.MaybePrintServices(iw)
c.Check(buf.String(), check.Equals, "")
}
}
func (s *infoSuite) TestMaybePrintCommands(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
for _, infos := range [][]client.AppInfo{cmdAppInfos, mixedAppInfos} {
- var buf bytes.Buffer
- snap.MaybePrintCommands(&buf, "foo", infos, -1)
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos})
+ snap.MaybePrintCommands(iw)
c.Check(buf.String(), check.Equals, `commands:
- foo.app1
@@ -89,14 +106,342 @@
}
func (s *infoSuite) TestMaybePrintCommandsNoCommands(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
for _, infos := range [][]client.AppInfo{svcAppInfos, nil} {
- var buf bytes.Buffer
- snap.MaybePrintCommands(&buf, "foo", infos, -1)
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos})
+ snap.MaybePrintCommands(iw)
c.Check(buf.String(), check.Equals, "")
}
}
+func (infoSuite) TestPrintType(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ for from, to := range map[string]string{
+ "": "",
+ "app": "",
+ "application": "",
+ "gadget": "type:\tgadget\n",
+ "core": "type:\tcore\n",
+ "os": "type:\tcore\n",
+ } {
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Type: from})
+ snap.MaybePrintType(iw)
+ c.Check(buf.String(), check.Equals, to, check.Commentf("%q", from))
+ }
+}
+
+func (infoSuite) TestPrintSummary(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ for from, to := range map[string]string{
+ "": `""`, // empty results in quoted empty
+ "foo": "foo", // plain text results in unquoted
+ "two words": "two words", // ...even when multi-word
+ "{": `"{"`, // but yaml-breaking is quoted
+ "very long text": "very long\n text", // too-long text gets split (TODO: split with tabbed indent to preserve alignment)
+ } {
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Summary: from})
+ snap.PrintSummary(iw)
+ c.Check(buf.String(), check.Equals, "summary:\t"+to+"\n", check.Commentf("%q", from))
+ }
+}
+
+func (s *infoSuite) TestMaybePrintPublisher(c *check.C) {
+ acct := &snaplib.StoreAccount{
+ Validation: "verified",
+ Username: "team-potato",
+ DisplayName: "Team Potato",
+ }
+
+ type T struct {
+ diskSnap, localSnap *client.Snap
+ expected string
+ }
+
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ for i, t := range []T{
+ {&client.Snap{}, nil, ""}, // nothing output for on-disk snap
+ {nil, &client.Snap{}, "publisher:\t--\n"}, // from-snapd snap with no publisher is explicit
+ {nil, &client.Snap{Publisher: acct}, "publisher:\tTeam Potato*\n"},
+ } {
+ buf.Reset()
+ if t.diskSnap == nil {
+ snap.SetupSnap(iw, t.localSnap, nil, nil)
+ } else {
+ snap.SetupDiskSnap(iw, "", t.diskSnap)
+ }
+ snap.MaybePrintPublisher(iw)
+ c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d", i))
+ }
+}
+
+func (s *infoSuite) TestMaybePrintNotes(c *check.C) {
+ type T struct {
+ localSnap, diskSnap *client.Snap
+ expected string
+ }
+
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ for i, t := range []T{
+ {
+ nil,
+ &client.Snap{Private: true, Confinement: "devmode"},
+ "notes:\t\n" +
+ " private:\ttrue\n" +
+ " confinement:\tdevmode\n",
+ }, {
+ &client.Snap{Private: true, Confinement: "devmode"},
+ nil,
+ "notes:\t\n" +
+ " private:\ttrue\n" +
+ " confinement:\tdevmode\n" +
+ " devmode:\tfalse\n" +
+ " jailmode:\ttrue\n" +
+ " trymode:\tfalse\n" +
+ " enabled:\tfalse\n" +
+ " broken:\tfalse\n" +
+ " ignore-validation:\tfalse\n",
+ }, {
+ &client.Snap{Private: true, Confinement: "devmode", Broken: "ouch"},
+ nil,
+ "notes:\t\n" +
+ " private:\ttrue\n" +
+ " confinement:\tdevmode\n" +
+ " devmode:\tfalse\n" +
+ " jailmode:\ttrue\n" +
+ " trymode:\tfalse\n" +
+ " enabled:\tfalse\n" +
+ " broken:\ttrue (ouch)\n" +
+ " ignore-validation:\tfalse\n",
+ },
+ } {
+ buf.Reset()
+ snap.SetVerbose(iw, false)
+ if t.diskSnap == nil {
+ snap.SetupSnap(iw, t.localSnap, nil, nil)
+ } else {
+ snap.SetupDiskSnap(iw, "", t.diskSnap)
+ }
+ snap.MaybePrintNotes(iw)
+ c.Check(buf.String(), check.Equals, "", check.Commentf("%d/false", i))
+
+ buf.Reset()
+ snap.SetVerbose(iw, true)
+ snap.MaybePrintNotes(iw)
+ c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d/true", i))
+ }
+}
+
+func (s *infoSuite) TestMaybePrintStandaloneVersion(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+
+ // no disk snap -> no version
+ snap.MaybePrintStandaloneVersion(iw)
+ c.Check(buf.String(), check.Equals, "")
+
+ for version, expected := range map[string]string{
+ "": "--",
+ "4.2": "4.2",
+ } {
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Version: version})
+ snap.MaybePrintStandaloneVersion(iw)
+ c.Check(buf.String(), check.Equals, "version:\t"+expected+" -\n", check.Commentf("%q", version))
+
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Version: version, Confinement: "devmode"})
+ snap.MaybePrintStandaloneVersion(iw)
+ c.Check(buf.String(), check.Equals, "version:\t"+expected+" devmode\n", check.Commentf("%q", version))
+ }
+}
+
+func (s *infoSuite) TestMaybePrintBuildDate(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ // some prep
+ dir := c.MkDir()
+ arbfile := filepath.Join(dir, "arb")
+ c.Assert(ioutil.WriteFile(arbfile, nil, 0600), check.IsNil)
+ filename := filepath.Join(c.MkDir(), "foo.snap")
+ diskSnap := squashfs.New(filename)
+ c.Assert(diskSnap.Build(dir, nil), check.IsNil)
+ buildDate := diskSnap.BuildDate().Format(time.Kitchen)
+
+ // no disk snap -> no build date
+ snap.MaybePrintBuildDate(iw)
+ c.Check(buf.String(), check.Equals, "")
+
+ // path is directory -> no build date
+ buf.Reset()
+ snap.SetupDiskSnap(iw, dir, &client.Snap{})
+ snap.MaybePrintBuildDate(iw)
+ c.Check(buf.String(), check.Equals, "")
+
+ // not actually a snap -> no build date
+ buf.Reset()
+ snap.SetupDiskSnap(iw, arbfile, &client.Snap{})
+ snap.MaybePrintBuildDate(iw)
+ c.Check(buf.String(), check.Equals, "")
+
+ // disk snap -> get build date
+ buf.Reset()
+ snap.SetupDiskSnap(iw, filename, &client.Snap{})
+ snap.MaybePrintBuildDate(iw)
+ c.Check(buf.String(), check.Equals, "build-date:\t"+buildDate+"\n")
+}
+
+func (s *infoSuite) TestMaybePrintSum(c *check.C) {
+ var buf flushBuffer
+ // some prep
+ dir := c.MkDir()
+ filename := filepath.Join(c.MkDir(), "foo.snap")
+ diskSnap := squashfs.New(filename)
+ c.Assert(diskSnap.Build(dir, nil), check.IsNil)
+ iw := snap.NewInfoWriter(&buf)
+ snap.SetVerbose(iw, true)
+
+ // no disk snap -> no checksum
+ snap.MaybePrintSum(iw)
+ c.Check(buf.String(), check.Equals, "")
+
+ // path is directory -> no checksum
+ buf.Reset()
+ snap.SetupDiskSnap(iw, dir, &client.Snap{})
+ snap.MaybePrintSum(iw)
+ c.Check(buf.String(), check.Equals, "")
+
+ // disk snap and verbose -> get checksum
+ buf.Reset()
+ snap.SetupDiskSnap(iw, filename, &client.Snap{})
+ snap.MaybePrintSum(iw)
+ c.Check(buf.String(), check.Matches, "sha3-384:\t\\S+\n")
+
+ // disk snap but not verbose -> no checksum
+ buf.Reset()
+ snap.SetVerbose(iw, false)
+ snap.MaybePrintSum(iw)
+ c.Check(buf.String(), check.Equals, "")
+}
+
+func (s *infoSuite) TestMaybePrintContact(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+
+ for contact, expected := range map[string]string{
+ "mailto:joe@example.com": "contact:\tjoe@example.com\n",
+ // gofmt 1.9 being silly
+ "foo": "contact:\tfoo\n",
+ "": "",
+ } {
+ buf.Reset()
+ snap.SetupDiskSnap(iw, "", &client.Snap{Contact: contact})
+ snap.MaybePrintContact(iw)
+ c.Check(buf.String(), check.Equals, expected, check.Commentf("%q", contact))
+ }
+}
+
+func (s *infoSuite) TestMaybePrintBase(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ dSnap := &client.Snap{}
+ snap.SetupDiskSnap(iw, "", dSnap)
+
+ // no verbose -> no base
+ snap.SetVerbose(iw, false)
+ snap.MaybePrintBase(iw)
+ c.Check(buf.String(), check.Equals, "")
+ buf.Reset()
+
+ // no base -> no base :)
+ snap.SetVerbose(iw, true)
+ snap.MaybePrintBase(iw)
+ c.Check(buf.String(), check.Equals, "")
+ buf.Reset()
+
+ // base + verbose -> base
+ dSnap.Base = "xyzzy"
+ snap.MaybePrintBase(iw)
+ c.Check(buf.String(), check.Equals, "base:\txyzzy\n")
+ buf.Reset()
+}
+
+func (s *infoSuite) TestMaybePrintPath(c *check.C) {
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ dSnap := &client.Snap{}
+
+ // no path -> no path
+ snap.SetupDiskSnap(iw, "", dSnap)
+ snap.MaybePrintPath(iw)
+ c.Check(buf.String(), check.Equals, "")
+ buf.Reset()
+
+ // path -> path (quoted!)
+ snap.SetupDiskSnap(iw, "xyzzy", dSnap)
+ snap.MaybePrintPath(iw)
+ c.Check(buf.String(), check.Equals, "path:\t\"xyzzy\"\n")
+ buf.Reset()
+}
+
+func (s *infoSuite) TestClientSnapFromPath(c *check.C) {
+ // minimal sanity check
+ fn := snaptest.MakeTestSnapWithFiles(c, `
+name: some-snap
+version: 9
+`, nil)
+ dSnap, err := snap.ClientSnapFromPath(fn)
+ c.Assert(err, check.IsNil)
+ c.Check(dSnap.Version, check.Equals, "9")
+}
+
+func (s *infoSuite) TestInfoPricedNarrowTerminal(c *check.C) {
+ defer snap.MockTermSize(func() (int, int) { return 44, 25 })()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ fmt.Fprintln(w, findPricedJSON)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello")
+ fmt.Fprintln(w, "{}")
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+name: hello
+summary: GNU Hello, the "hello world"
+ snap
+publisher: Canonical*
+license: Proprietary
+price: 1.99GBP
+description: |
+ GNU hello prints a friendly greeting.
+ This is part of the snapcraft tour at
+ https://snapcraft.io/
+snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
func (s *infoSuite) TestInfoPriced(c *check.C) {
n := 0
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
@@ -131,6 +476,7 @@
c.Check(s.Stderr(), check.Equals, "")
}
+// only used for results on /v2/find
const mockInfoJSON = `
{
"type": "sync",
@@ -195,6 +541,7 @@
"revision": "1",
"status": "available",
"summary": "The GNU Hello snap",
+ "store-url": "https://snapcraft.io/hello",
"type": "app",
"version": "2.10",
"license": "MIT",
@@ -250,6 +597,7 @@
c.Check(s.Stderr(), check.Equals, "")
}
+// only used for /v2/snaps/hello
const mockInfoJSONOtherLicense = `
{
"type": "sync",
@@ -266,6 +614,7 @@
"display-name": "Canonical",
"validation": "verified"
},
+ "health": {"revision": "1", "status": "blocked", "message": "please configure the grawflit", "timestamp": "2019-05-13T16:27:01.475851677+01:00"},
"id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
"install-date": "2006-01-02T22:04:07.123456789Z",
"installed-size": 1024,
@@ -336,8 +685,14 @@
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Equals, `name: hello
-summary: The GNU Hello snap
+ c.Check(s.Stdout(), check.Equals, `
+name: hello
+summary: The GNU Hello snap
+health:
+ status: blocked
+ message: please configure the grawflit
+ checked: 2019-05-13T16:27:01+01:00
+ revision: 1
publisher: Canonical*
license: BSD-3
description: |
@@ -346,8 +701,30 @@
snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6
tracking: beta
refresh-date: 2006-01-02T22:04:07Z
-installed: 2.10 (1) 1kB disabled
-`)
+installed: 2.10 (1) 1kB disabled,blocked
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *infoSuite) TestInfoNotFound(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n % 2 {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/x")
+ }
+ w.WriteHeader(404)
+ fmt.Fprintln(w, `{"type":"error","status-code":404,"status":"Not Found","result":{"message":"No.","kind":"snap-not-found","value":"x"}}`)
+
+ n++
+ })
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--verbose", "/x"})
+ c.Check(err, check.ErrorMatches, `no snap found for "/x"`)
+ c.Check(s.Stdout(), check.Equals, "")
c.Check(s.Stderr(), check.Equals, "")
}
@@ -411,6 +788,7 @@
c.Check(s.Stdout(), check.Equals, `name: hello
summary: The GNU Hello snap
publisher: Canonical*
+store-url: https://snapcraft.io/hello
license: unset
description: |
GNU hello prints a friendly greeting. This is part of the snapcraft tour at
@@ -436,6 +814,7 @@
c.Check(s.Stdout(), check.Equals, `name: hello
summary: The GNU Hello snap
publisher: Canonical*
+store-url: https://snapcraft.io/hello
license: unset
description: |
GNU hello prints a friendly greeting. This is part of the snapcraft tour at
@@ -461,6 +840,7 @@
c.Check(s.Stdout(), check.Equals, `name: hello
summary: The GNU Hello snap
publisher: Canonical✓
+store-url: https://snapcraft.io/hello
license: unset
description: |
GNU hello prints a friendly greeting. This is part of the snapcraft tour at
@@ -550,3 +930,294 @@
c.Check(buf.String(), check.Equals, v, check.Commentf("%q", k))
}
}
+
+func (infoSuite) TestMaybePrintCohortKey(c *check.C) {
+ type T struct {
+ snap *client.Snap
+ verbose bool
+ expected string
+ }
+
+ tests := []T{
+ {snap: nil, verbose: false, expected: ""},
+ {snap: nil, verbose: true, expected: ""},
+ {snap: &client.Snap{}, verbose: false, expected: ""},
+ {snap: &client.Snap{}, verbose: true, expected: ""},
+ {snap: &client.Snap{CohortKey: "some-cohort-key"}, verbose: false, expected: ""},
+ {snap: &client.Snap{CohortKey: "some-cohort-key"}, verbose: true, expected: "cohort:\t…-key\n"},
+ }
+
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ defer snap.MockIsStdoutTTY(true)()
+
+ for i, t := range tests {
+ buf.Reset()
+ snap.SetupSnap(iw, t.snap, nil, nil)
+ snap.SetVerbose(iw, t.verbose)
+ snap.MaybePrintCohortKey(iw)
+ c.Check(buf.String(), check.Equals, t.expected, check.Commentf("tty:true/%d", i))
+ }
+ // now the same but without a tty -> the last test should no longer ellipt
+ tests[len(tests)-1].expected = "cohort:\tsome-cohort-key\n"
+ snap.MockIsStdoutTTY(false)
+ for i, t := range tests {
+ buf.Reset()
+ snap.SetupSnap(iw, t.snap, nil, nil)
+ snap.SetVerbose(iw, t.verbose)
+ snap.MaybePrintCohortKey(iw)
+ c.Check(buf.String(), check.Equals, t.expected, check.Commentf("tty:false/%d", i))
+ }
+}
+
+func (infoSuite) TestMaybePrintHealth(c *check.C) {
+ type T struct {
+ snap *client.Snap
+ verbose bool
+ expected string
+ }
+
+ goodHealth := &client.SnapHealth{Status: "okay"}
+ t0 := time.Date(1970, 1, 1, 10, 24, 0, 0, time.UTC)
+ badHealth := &client.SnapHealth{
+ Status: "waiting",
+ Message: "godot should be here any moment now",
+ Code: "godot-is-a-lie",
+ Revision: snaplib.R("42"),
+ Timestamp: t0,
+ }
+
+ tests := []T{
+ {snap: nil, verbose: false, expected: ""},
+ {snap: nil, verbose: true, expected: ""},
+ {snap: &client.Snap{}, verbose: false, expected: ""},
+ {snap: &client.Snap{}, verbose: true, expected: `health:
+ status: unknown
+ message: health
+ has not been set
+`},
+ {snap: &client.Snap{Health: goodHealth}, verbose: false, expected: ``},
+ {snap: &client.Snap{Health: goodHealth}, verbose: true, expected: `health:
+ status: okay
+`},
+ {snap: &client.Snap{Health: badHealth}, verbose: false, expected: `health:
+ status: waiting
+ message: godot
+ should be here
+ any moment now
+ code: godot-is-a-lie
+ checked: 10:24AM
+ revision: 42
+`},
+ {snap: &client.Snap{Health: badHealth}, verbose: true, expected: `health:
+ status: waiting
+ message: godot
+ should be here
+ any moment now
+ code: godot-is-a-lie
+ checked: 10:24AM
+ revision: 42
+`},
+ }
+
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ defer snap.MockIsStdoutTTY(false)()
+
+ for i, t := range tests {
+ buf.Reset()
+ snap.SetupSnap(iw, t.snap, nil, nil)
+ snap.SetVerbose(iw, t.verbose)
+ snap.MaybePrintHealth(iw)
+ c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d", i))
+ }
+}
+
+func (infoSuite) TestWrapCornerCase(c *check.C) {
+ // this particular corner case isn't currently reachable from
+ // printDescr nor printSummary, but best to have it covered
+ var buf bytes.Buffer
+ const s = "This is a paragraph indented with leading spaces that are encoded as multiple bytes. All hail EN SPACE."
+ snap.WrapFlow(&buf, []rune(s), "\u2002\u2002", 30)
+ c.Check(buf.String(), check.Equals, `
+ This is a paragraph indented
+ with leading spaces that are
+ encoded as multiple bytes.
+ All hail EN SPACE.
+`[1:])
+}
+
+func (infoSuite) TestBug1828425(c *check.C) {
+ const s = `This is a description
+ that has
+ lines
+ too deeply
+ indented.
+`
+ var buf bytes.Buffer
+ err := snap.PrintDescr(&buf, s, 30)
+ c.Assert(err, check.IsNil)
+ c.Check(buf.String(), check.Equals, ` This is a description
+ that has
+ lines
+ too deeply
+ indented.
+`)
+}
+
+const mockInfoJSONParallelInstance = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "publisher": {
+ "id": "canonical",
+ "username": "canonical",
+ "display-name": "Canonical",
+ "validation": "verified"
+ },
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "install-date": "2006-01-02T22:04:07.123456789Z",
+ "installed-size": 1024,
+ "name": "hello_foo",
+ "private": false,
+ "revision": "100",
+ "status": "available",
+ "summary": "The GNU Hello snap",
+ "type": "app",
+ "version": "2.10",
+ "license": "",
+ "tracking-channel": "beta"
+ }
+}
+`
+
+func (s *infoSuite) TestInfoParllelInstance(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ q := r.URL.Query()
+ // asks for the instance snap
+ c.Check(q.Get("name"), check.Equals, "hello")
+ fmt.Fprintln(w, mockInfoJSONWithChannels)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello_foo")
+ fmt.Fprintln(w, mockInfoJSONParallelInstance)
+ default:
+ c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello_foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ // make sure local and remote info is combined in the output
+ c.Check(s.Stdout(), check.Equals, `name: hello_foo
+summary: The GNU Hello snap
+publisher: Canonical*
+store-url: https://snapcraft.io/hello
+license: unset
+description: |
+ GNU hello prints a friendly greeting. This is part of the snapcraft tour at
+ https://snapcraft.io/
+snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6
+tracking: beta
+refresh-date: 2006-01-02
+channels:
+ 1/stable: 2.10 2018-12-18 (1) 65kB -
+ 1/candidate: ^
+ 1/beta: ^
+ 1/edge: ^
+installed: 2.10 (100) 1kB disabled
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+const mockInfoJSONWithStoreURL = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "publisher": {
+ "id": "canonical",
+ "username": "canonical",
+ "display-name": "Canonical",
+ "validation": "verified"
+ },
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "install-date": "2006-01-02T22:04:07.123456789Z",
+ "installed-size": 1024,
+ "name": "hello",
+ "private": false,
+ "revision": "100",
+ "status": "available",
+ "store-url": "https://snapcraft.io/hello",
+ "summary": "The GNU Hello snap",
+ "type": "app",
+ "version": "2.10",
+ "license": "",
+ "tracking-channel": "beta"
+ }
+}
+`
+
+func (s *infoSuite) TestInfoStoreURL(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ q := r.URL.Query()
+ // asks for the instance snap
+ c.Check(q.Get("name"), check.Equals, "hello")
+ fmt.Fprintln(w, mockInfoJSONWithChannels)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello")
+ fmt.Fprintln(w, mockInfoJSONWithStoreURL)
+ default:
+ c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ // make sure local and remote info is combined in the output
+ c.Check(s.Stdout(), check.Equals, `name: hello
+summary: The GNU Hello snap
+publisher: Canonical*
+store-url: https://snapcraft.io/hello
+license: unset
+description: |
+ GNU hello prints a friendly greeting. This is part of the snapcraft tour at
+ https://snapcraft.io/
+snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6
+tracking: beta
+refresh-date: 2006-01-02
+channels:
+ 1/stable: 2.10 2018-12-18 (1) 65kB -
+ 1/candidate: ^
+ 1/beta: ^
+ 1/edge: ^
+installed: 2.10 (100) 1kB disabled
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_interfaces.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_interfaces.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces.go 2020-06-05 13:13:49.000000000 +0000
@@ -22,6 +22,7 @@
import (
"fmt"
+ "github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/i18n"
"github.com/jessevdk/go-flags"
@@ -53,10 +54,13 @@
Filters the complete output so only plugs and/or slots matching the provided
details are listed.
+
+NOTE this command is deprecated and has been replaced with the 'connections'
+ command.
`)
func init() {
- addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander {
+ cmd := addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander {
return &cmdInterfaces{}
}, map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
@@ -67,26 +71,35 @@
// TRANSLATORS: This should not start with a lowercase letter.
desc: i18n.G("Constrain listing to a specific snap or snap:name"),
}})
+ cmd.hidden = true
}
+var interfacesDeprecationNotice = i18n.G("'snap interfaces' is deprecated; use 'snap connections'.")
+
func (x *cmdInterfaces) Execute(args []string) error {
if len(args) > 0 {
return ErrExtraArgs
}
- ifaces, err := x.client.Connections()
+ opts := client.ConnectionOptions{
+ All: true,
+ Snap: x.Positionals.Query.Snap,
+ }
+ ifaces, err := x.client.Connections(&opts)
if err != nil {
return err
}
if len(ifaces.Plugs) == 0 && len(ifaces.Slots) == 0 {
return fmt.Errorf(i18n.G("no interfaces found"))
}
+
+ defer fmt.Fprintln(Stderr, "\n"+fill(interfacesDeprecationNotice, 0))
+
w := tabWriter()
defer w.Flush()
fmt.Fprintln(w, i18n.G("Slot\tPlug"))
wantedSnap := x.Positionals.Query.Snap
-
for _, slot := range ifaces.Slots {
if wantedSnap != "" {
var ok bool
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_interfaces_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_interfaces_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -29,12 +29,13 @@
"github.com/snapcore/snapd/client"
. "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/testutil"
)
-func (s *SnapSuite) TestConnectionsZeroSlotsOnePlug(c *C) {
+func (s *SnapSuite) TestInterfacesZeroSlotsOnePlug(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -57,13 +58,13 @@
"Slot Plug\n" +
"- keyboard-lights:capslock-led\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsZeroPlugsOneSlot(c *C) {
+func (s *SnapSuite) TestInterfacesZeroPlugsOneSlot(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -88,13 +89,13 @@
"Slot Plug\n" +
"canonical-pi2:pin-13 -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOneSlotOnePlug(c *C) {
+func (s *SnapSuite) TestInterfacesOneSlotOnePlug(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -139,7 +140,7 @@
"Slot Plug\n" +
"canonical-pi2:pin-13 keyboard-lights:capslock-led\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
s.SetUpTest(c)
// should be the same
@@ -147,7 +148,7 @@
c.Assert(err, IsNil)
c.Assert(rest, DeepEquals, []string{})
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
s.SetUpTest(c)
// and the same again
@@ -155,13 +156,13 @@
c.Assert(err, IsNil)
c.Assert(rest, DeepEquals, []string{})
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsTwoPlugs(c *C) {
+func (s *SnapSuite) TestInterfacesTwoPlugs(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -196,13 +197,13 @@
"Slot Plug\n" +
"canonical-pi2:pin-13 keyboard-lights:capslock-led,keyboard-lights:scrollock-led\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsPlugsWithCommonName(c *C) {
+func (s *SnapSuite) TestInterfacesPlugsWithCommonName(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -263,13 +264,13 @@
"Slot Plug\n" +
"canonical-pi2:network-listening paste-daemon,time-daemon\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOsSnapSlots(c *C) {
+func (s *SnapSuite) TestInterfacesOsSnapSlots(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -330,13 +331,13 @@
"Slot Plug\n" +
":network-listening paste-daemon,time-daemon\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsTwoSlotsAndFiltering(c *C) {
+func (s *SnapSuite) TestInterfacesTwoSlotsAndFiltering(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -379,13 +380,13 @@
"Slot Plug\n" +
"canonical-pi2:debug-console core\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOfSpecificSnap(c *C) {
+func (s *SnapSuite) TestInterfacesOfSpecificSnap(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -423,13 +424,13 @@
"wake-up-alarm:toggle -\n" +
"wake-up-alarm:snooze -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOfSystemNicknameSnap(c *C) {
+func (s *SnapSuite) TestInterfacesOfSystemNicknameSnap(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -466,7 +467,7 @@
"Slot Plug\n" +
":core-support core:core-support-plug\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
s.ResetStdStreams()
@@ -478,13 +479,13 @@
"Slot Plug\n" +
":core-support core:core-support-plug\n"
c.Assert(s.Stdout(), Equals, expectedStdoutSystem)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsOfSpecificSnapAndSlot(c *C) {
+func (s *SnapSuite) TestInterfacesOfSpecificSnapAndSlot(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -521,13 +522,13 @@
"Slot Plug\n" +
"wake-up-alarm:snooze -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsNothingAtAll(c *C) {
+func (s *SnapSuite) TestInterfacesNothingAtAll(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -545,10 +546,10 @@
c.Assert(s.Stderr(), Equals, "")
}
-func (s *SnapSuite) TestConnectionsOfSpecificType(c *C) {
+func (s *SnapSuite) TestInterfacesOfSpecificType(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -587,13 +588,13 @@
"wake-up-alarm:toggle -\n" +
"wake-up-alarm:snooze -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
-func (s *SnapSuite) TestConnectionsCompletion(c *C) {
+func (s *SnapSuite) TestInterfacesCompletion(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
- case "/v2/interfaces":
+ case "/v2/connections":
c.Assert(r.Method, Equals, "GET")
EncodeResponseBody(c, w, map[string]interface{}{
"type": "sync",
@@ -628,26 +629,26 @@
c.Assert(s.Stderr(), Equals, "")
}
-func (s *SnapSuite) TestConnectionsCoreNicknamedSystem(c *C) {
+func (s *SnapSuite) TestInterfacesCoreNicknamedSystem(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "core", "system")
}
-func (s *SnapSuite) TestConnectionsSnapdNicknamedSystem(c *C) {
+func (s *SnapSuite) TestInterfacesSnapdNicknamedSystem(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "snapd", "system")
}
-func (s *SnapSuite) TestConnectionsSnapdNicknamedCore(c *C) {
+func (s *SnapSuite) TestInterfacesSnapdNicknamedCore(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "snapd", "core")
}
-func (s *SnapSuite) TestConnectionsCoreSnap(c *C) {
+func (s *SnapSuite) TestInterfacesCoreSnap(c *C) {
s.checkConnectionsSystemCoreRemapping(c, "core", "core")
}
func (s *SnapSuite) checkConnectionsSystemCoreRemapping(c *C, apiSnapName, cliSnapName string) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Path, Equals, "/v2/interfaces")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
body, err := ioutil.ReadAll(r.Body)
c.Check(err, IsNil)
c.Check(body, DeepEquals, []byte{})
@@ -670,5 +671,5 @@
"Slot Plug\n" +
":network -\n"
c.Assert(s.Stdout(), Equals, expectedStdout)
- c.Assert(s.Stderr(), Equals, "")
+ c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice)
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_keys.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_keys.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_keys.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_keys.go 2020-06-05 13:13:49.000000000 +0000
@@ -47,6 +47,7 @@
"json": i18n.G("Output results in JSON format"),
}, nil)
cmd.hidden = true
+ cmd.completeHidden = true
}
// Key represents a key that can be used for signing assertions.
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known.go 2020-06-05 13:13:49.000000000 +0000
@@ -23,12 +23,14 @@
"fmt"
"strings"
+ "github.com/jessevdk/go-flags"
+ "golang.org/x/xerrors"
+
"github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/store"
-
- "github.com/jessevdk/go-flags"
)
type cmdKnown struct {
@@ -40,6 +42,7 @@
} `positional-args:"true" required:"true"`
Remote bool `long:"remote"`
+ Direct bool `long:"direct"`
}
var shortKnownHelp = i18n.G("Show known assertions of the provided type")
@@ -52,7 +55,12 @@
func init() {
addCommand("known", shortKnownHelp, longKnownHelp, func() flags.Commander {
return &cmdKnown{}
- }, nil, []argDesc{
+ }, map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "remote": i18n.G("Query the store for the assertion, via snapd if possible"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "direct": i18n.G("Query the store for the assertion, without attempting to go via snapd"),
+ }, []argDesc{
{
// TRANSLATORS: This needs to begin with < and end with >
name: i18n.G(""),
@@ -73,7 +81,7 @@
var user *auth.UserState
// FIXME: set auth context
- var authContext auth.AuthContext
+ var storeCtx store.DeviceAndAuthContext
at := asserts.Type(typeName)
if at == nil {
@@ -84,7 +92,7 @@
return nil, fmt.Errorf("cannot query remote assertion: %v", err)
}
- sto := storeNew(nil, authContext)
+ sto := storeNew(nil, storeCtx)
as, err := sto.Assertion(at, primaryKeys, user)
if err != nil {
return nil, err
@@ -110,10 +118,21 @@
var assertions []asserts.Assertion
var err error
- if x.Remote {
+ switch {
+ case x.Remote && !x.Direct:
+ // --remote will query snapd
+ assertions, err = x.client.Known(string(x.KnownOptions.AssertTypeName), headers, &client.KnownOptions{Remote: true})
+ // if snapd is unavailable automatically fallback
+ var connErr client.ConnectionError
+ if xerrors.As(err, &connErr) {
+ assertions, err = downloadAssertion(string(x.KnownOptions.AssertTypeName), headers)
+ }
+ case x.Direct:
+ // --direct implies remote
assertions, err = downloadAssertion(string(x.KnownOptions.AssertTypeName), headers)
- } else {
- assertions, err = x.client.Known(string(x.KnownOptions.AssertTypeName), headers)
+ default:
+ // default is to look only local
+ assertions, err = x.client.Known(string(x.KnownOptions.AssertTypeName), headers, nil)
}
if err != nil {
return err
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -28,7 +28,7 @@
"github.com/jessevdk/go-flags"
"gopkg.in/check.v1"
- "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/store"
snap "github.com/snapcore/snapd/cmd/snap"
@@ -50,16 +50,85 @@
AcLorsomethingthatlooksvaguelylikeasignature==
`
-func (s *SnapSuite) TestKnownRemote(c *check.C) {
+func (s *SnapSuite) TestKnownViaSnapd(c *check.C) {
+ n := 0
+ expectedQuery := url.Values{
+ "series": []string{"16"},
+ "brand-id": []string{"canonical"},
+ "model": []string{"pi99"},
+ }
+
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, check.Equals, "/v2/assertions/model")
+ c.Check(r.URL.Query(), check.DeepEquals, expectedQuery)
+ w.Header().Set("X-Ubuntu-Assertions-Count", "1")
+ fmt.Fprintln(w, mockModelAssertion)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+ n++
+ })
+
+ // first run "normal"
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "model", "series=16", "brand-id=canonical", "model=pi99"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, mockModelAssertion)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+
+ // then with "--remote"
+ n = 0
+ s.stdout.Reset()
+ expectedQuery["remote"] = []string{"true"}
+ rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, mockModelAssertion)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestKnownRemoteViaSnapd(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, check.Equals, "/v2/assertions/model")
+ c.Check(r.URL.Query(), check.DeepEquals, url.Values{
+ "series": []string{"16"},
+ "brand-id": []string{"canonical"},
+ "model": []string{"pi99"},
+ "remote": []string{"true"},
+ })
+ w.Header().Set("X-Ubuntu-Assertions-Count", "1")
+ fmt.Fprintln(w, mockModelAssertion)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+ n++
+ })
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, mockModelAssertion)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestKnownRemoteDirect(c *check.C) {
var server *httptest.Server
- restorer := snap.MockStoreNew(func(cfg *store.Config, auth auth.AuthContext) *store.Store {
+ restorer := snap.MockStoreNew(func(cfg *store.Config, stoCtx store.DeviceAndAuthContext) *store.Store {
if cfg == nil {
cfg = store.DefaultConfig()
}
serverURL, _ := url.Parse(server.URL)
cfg.AssertionsBaseURL = serverURL
- return store.New(cfg, auth)
+ return store.New(cfg, stoCtx)
})
defer restorer()
@@ -78,7 +147,58 @@
n++
}))
- rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"})
+ // first test "--remote --direct"
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "--direct", "model", "series=16", "brand-id=canonical", "model=pi99"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, mockModelAssertion)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+
+ // "--direct" behave the same as "--remote --direct"
+ s.stdout.Reset()
+ n = 0
+ rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"known", "--direct", "model", "series=16", "brand-id=canonical", "model=pi99"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, mockModelAssertion)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestKnownRemoteAutoFallback(c *check.C) {
+ var server *httptest.Server
+
+ restorer := snap.MockStoreNew(func(cfg *store.Config, stoCtx store.DeviceAndAuthContext) *store.Store {
+ if cfg == nil {
+ cfg = store.DefaultConfig()
+ }
+ serverURL, _ := url.Parse(server.URL)
+ cfg.AssertionsBaseURL = serverURL
+ return store.New(cfg, stoCtx)
+ })
+ defer restorer()
+
+ n := 0
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Assert(r.URL.Path, check.Matches, ".*/assertions/.*") // sanity check request
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/api/v1/snaps/assertions/model/16/canonical/pi99")
+ fmt.Fprintln(w, mockModelAssertion)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+ n++
+ }))
+
+ cli := snap.Client()
+ cli.Hijack(func(*http.Request) (*http.Response, error) {
+ return nil, client.ConnectionError{Err: fmt.Errorf("no snapd")}
+ })
+
+ rest, err := snap.Parser(cli).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
c.Check(s.Stdout(), check.Equals, mockModelAssertion)
@@ -86,7 +206,7 @@
}
func (s *SnapSuite) TestKnownRemoteMissingPrimaryKey(c *check.C) {
- _, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"})
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "--direct", "model", "series=16", "brand-id=canonical"})
c.Assert(err, check.ErrorMatches, `cannot query remote assertion: must provide primary key: model`)
}
@@ -106,4 +226,5 @@
})
c.Check(snap.AssertTypeNameCompletion("v"), check.DeepEquals, []flags.Completion{{Item: "validation"}})
+ c.Check(n, check.Equals, 1)
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list.go 2020-06-05 13:13:49.000000000 +0000
@@ -30,7 +30,6 @@
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/i18n"
- "github.com/snapcore/snapd/strutil"
)
var shortListHelp = i18n.G("List installed snaps")
@@ -69,38 +68,20 @@
// snapd will give us and we want
// "" (local snap) "-"
-// risk risk
-// track track (not yet returned by snapd)
-// track/stable track
+// latest/risk latest/risk
// track/risk track/risk
-// risk/branch risk/…
// track/risk/branch track/risk/…
+// anything else SISO
func fmtChannel(ch string) string {
if ch == "" {
// "" -> "-" (local snap)
return "-"
}
- idx := strings.IndexByte(ch, '/')
- if idx < 0 {
- // risk -> risk
+ if strings.Count(ch, "/") != 2 {
return ch
}
- first, rest := ch[:idx], ch[idx+1:]
- if rest == "stable" && first != "" {
- // track/stable -> track
- return first
- }
- if idx2 := strings.IndexByte(rest, '/'); idx2 >= 0 {
- // track/risk/branch -> track/risk/…
- return ch[:idx2+idx+2] + "…"
- }
- // so it's foo/bar -> either risk/branch, or track/risk.
- if strutil.ListContains(channelRisks, first) {
- // risk/branch -> risk/…
- return first + "/…"
- }
- // track/risk -> track/risk
- return ch
+ idx := strings.LastIndexByte(ch, '/')
+ return ch[:idx+1] + "…"
}
func (x *cmdList) Execute(args []string) error {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -55,7 +55,17 @@
c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/snaps")
c.Check(r.URL.RawQuery, check.Equals, "")
- fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "potatoes"}]}`)
+ fmt.Fprintln(w, `{"type": "sync", "result": [
+{
+ "name": "foo",
+ "status": "active",
+ "version": "4.2",
+ "developer": "bar",
+ "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"},
+ "health": {"status": "blocked"},
+ "revision": 17,
+ "tracking-channel": "potatoes"
+}]}`)
default:
c.Fatalf("expected to get 1 requests, now on %d", n+1)
}
@@ -65,9 +75,10 @@
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes
-foo +4.2 +17 +potatoes +bar +-
-`)
+ c.Check(s.Stdout(), check.Equals, `
+Name Version Rev Tracking Publisher Notes
+foo 4.2 17 potatoes bar blocked
+`[1:])
c.Check(s.Stderr(), check.Equals, "")
}
@@ -216,15 +227,11 @@
}
for _, t := range []tableT{
{"", "-"},
- {"stable", "stable"},
- {"edge", "edge"},
- {"foo/stable", "foo"},
+ {"latest/stable", "latest/stable"},
+ {"foo/stable", "foo/stable"},
{"foo/edge", "foo/edge"},
- {"foo", "foo"},
{"foo/stable/bar", "foo/stable/…"},
{"foo/edge/bar", "foo/edge/…"},
- {"stable/bar", "stable/…"},
- {"edge/bar", "edge/…"},
} {
c.Check(snap.FormatChannel(t.channel), check.Equals, t.expected, check.Commentf(t.channel))
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_model.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_model.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,327 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+var (
+ shortModelHelp = i18n.G("Get the active model for this device")
+ longModelHelp = i18n.G(`
+The model command returns the active model assertion information for this
+device.
+
+By default, only the essential model identification information is
+included in the output, but this can be expanded to include all of an
+assertion's non-meta headers.
+
+The verbose output is presented in a structured, yaml-like format.
+
+Similarly, the active serial assertion can be used for the output instead of the
+model assertion.
+`)
+
+ invalidTypeMessage = i18n.G("invalid type for %q header")
+ errNoMainAssertion = errors.New(i18n.G("device not ready yet (no assertions found)"))
+ errNoSerial = errors.New(i18n.G("device not registered yet (no serial assertion found)"))
+ errNoVerboseAssertion = errors.New(i18n.G("cannot use --verbose with --assertion"))
+
+ // this list is a "nice" "human" "readable" "ordering" of headers to print
+ // off, sorted in lexographical order with meta headers and primary key
+ // headers removed, and big nasty keys such as device-key-sha3-384 and
+ // device-key at the bottom
+ // it also contains both serial and model assertion headers, but we
+ // follow the same code path for both assertion types and some of the
+ // headers are shared between the two, so it still works out correctly
+ niceOrdering = [...]string{
+ "architecture",
+ "base",
+ "classic",
+ "display-name",
+ "gadget",
+ "kernel",
+ "revision",
+ "timestamp",
+ "required-snaps",
+ "device-key-sha3-384",
+ "device-key",
+ }
+)
+
+type cmdModel struct {
+ clientMixin
+ timeMixin
+ colorMixin
+
+ Serial bool `long:"serial"`
+ Verbose bool `long:"verbose"`
+ Assertion bool `long:"assertion"`
+}
+
+func init() {
+ addCommand("model",
+ shortModelHelp,
+ longModelHelp,
+ func() flags.Commander {
+ return &cmdModel{}
+ }, colorDescs.also(timeDescs).also(map[string]string{
+ "assertion": i18n.G("Print the raw assertion."),
+ "verbose": i18n.G("Print all specific assertion fields."),
+ "serial": i18n.G(
+ "Print the serial assertion instead of the model assertion."),
+ }),
+ []argDesc{},
+ )
+}
+
+func (x *cmdModel) Execute(args []string) error {
+ if x.Verbose && x.Assertion {
+ // can't do a verbose mode for the assertion
+ return errNoVerboseAssertion
+ }
+
+ var mainAssertion asserts.Assertion
+ serialAssertion, serialErr := x.client.CurrentSerialAssertion()
+ modelAssertion, modelErr := x.client.CurrentModelAssertion()
+
+ // if we didn't get a model assertion bail early
+ if modelErr != nil {
+ if client.IsAssertionNotFoundError(modelErr) {
+ // device is not registered yet - use specific error message
+ return errNoMainAssertion
+ }
+ return modelErr
+ }
+
+ // if the serial assertion error is anything other than not found, also
+ // bail early
+ // the serial assertion not being found may not be fatal
+ if serialErr != nil && !client.IsAssertionNotFoundError(serialErr) {
+ return serialErr
+ }
+
+ if x.Serial {
+ mainAssertion = serialAssertion
+ } else {
+ mainAssertion = modelAssertion
+ }
+
+ if x.Assertion {
+ // if we are using the serial assertion and we specifically didn't find the
+ // serial assertion, bail with specific error
+ if x.Serial && client.IsAssertionNotFoundError(serialErr) {
+ return errNoMainAssertion
+ }
+
+ _, err := Stdout.Write(asserts.Encode(mainAssertion))
+ return err
+ }
+
+ termWidth, _ := termSize()
+ termWidth -= 3
+ if termWidth > 100 {
+ // any wider than this and it gets hard to read
+ termWidth = 100
+ }
+
+ esc := x.getEscapes()
+
+ w := tabWriter()
+
+ if x.Serial && client.IsAssertionNotFoundError(serialErr) {
+ // for serial assertion, the primary keys are output (model and
+ // brand-id), but if we didn't find the serial assertion then we still
+ // output the brand-id and model from the model assertion, but also
+ // return a devNotReady error
+ fmt.Fprintf(w, "brand-id:\t%s\n", modelAssertion.HeaderString("brand-id"))
+ fmt.Fprintf(w, "model:\t%s\n", modelAssertion.HeaderString("model"))
+ w.Flush()
+ return errNoSerial
+ }
+
+ // the rest of this function is the main flow for outputting either the
+ // model or serial assertion in normal or verbose mode
+
+ // for the `snap model` case with no options, we don't want colons, we want
+ // to be like `snap version`
+ separator := ":"
+ if !x.Verbose && !x.Serial {
+ separator = ""
+ }
+
+ // ordering of the primary keys for model: brand, model, serial
+ // ordering of primary keys for serial is brand-id, model, serial
+
+ // output brand/brand-id
+ brandIDHeader := mainAssertion.HeaderString("brand-id")
+ modelHeader := mainAssertion.HeaderString("model")
+ // for the serial header, if there's no serial yet, it's not an error for
+ // model (and we already handled the serial error above) but need to add a
+ // parenthetical about the device not being registered yet
+ var serial string
+ if client.IsAssertionNotFoundError(serialErr) {
+ if x.Verbose || x.Serial {
+ // verbose and serial are yamlish, so we need to escape the dash
+ serial = esc.dash
+ } else {
+ serial = "-"
+ }
+ serial += " (device not registered yet)"
+ } else {
+ serial = serialAssertion.HeaderString("serial")
+ }
+
+ // handle brand/brand-id and model/model + display-name differently on just
+ // `snap model` w/o opts
+ if x.Serial || x.Verbose {
+ fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader)
+ fmt.Fprintf(w, "model:\t%s\n", modelHeader)
+ } else {
+ // for the model command (not --serial) we want to show a publisher
+ // style display of "brand" instead of just "brand-id"
+ storeAccount, err := x.client.StoreAccount(brandIDHeader)
+ if err != nil {
+ return err
+ }
+ // use the longPublisher helper to format the brand store account
+ // like we do in `snap info`
+ fmt.Fprintf(w, "brand%s\t%s\n", separator, longPublisher(x.getEscapes(), storeAccount))
+
+ // for model, if there's a display-name, we show that first with the
+ // real model in parenthesis
+ if displayName := modelAssertion.HeaderString("display-name"); displayName != "" {
+ modelHeader = fmt.Sprintf("%s (%s)", displayName, modelHeader)
+ }
+ fmt.Fprintf(w, "model%s\t%s\n", separator, modelHeader)
+ }
+
+ // serial is same for all variants
+ fmt.Fprintf(w, "serial%s\t%s\n", separator, serial)
+
+ // --verbose means output more information
+ if x.Verbose {
+ allHeadersMap := mainAssertion.Headers()
+
+ for _, headerName := range niceOrdering {
+ invalidTypeErr := fmt.Errorf(invalidTypeMessage, headerName)
+
+ headerValue, ok := allHeadersMap[headerName]
+ // make sure the header is in the map
+ if !ok {
+ continue
+ }
+
+ // switch on which header it is to handle some special cases
+ switch headerName {
+ // list of scalars
+ case "required-snaps":
+ headerIfaceList, ok := headerValue.([]interface{})
+ if !ok {
+ return invalidTypeErr
+ }
+ if len(headerIfaceList) == 0 {
+ continue
+ }
+ fmt.Fprintf(w, "%s:\t\n", headerName)
+ for _, elem := range headerIfaceList {
+ headerStringElem, ok := elem.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+ // note we don't wrap these, since for now this is
+ // specifically just required-snaps and so all of these
+ // will be snap names which are required to be short
+ fmt.Fprintf(w, " - %s\n", headerStringElem)
+ }
+
+ //timestamp needs to be formatted with fmtTime from the timeMixin
+ case "timestamp":
+ timestamp, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+
+ // parse the time string as RFC3339, which is what the format is
+ // always in for assertions
+ t, err := time.Parse(time.RFC3339, timestamp)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "timestamp:\t%s\n", x.fmtTime(t))
+
+ // long string key we don't want to rewrap but can safely handle
+ // on "reasonable" width terminals
+ case "device-key-sha3-384":
+ // also flush the writer before continuing so the previous keys
+ // don't try to align with this key
+ w.Flush()
+ headerString, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+
+ switch {
+ case termWidth > 86:
+ fmt.Fprintf(w, "device-key-sha3-384: %s\n", headerString)
+ case termWidth <= 86 && termWidth > 66:
+ fmt.Fprintln(w, "device-key-sha3-384: |")
+ wrapLine(w, []rune(headerString), " ", termWidth)
+ }
+
+ // long base64 key we can rewrap safely
+ case "device-key":
+ headerString, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+ // the string value here has newlines inserted as part of the
+ // raw assertion, but base64 doesn't care about whitespace, so
+ // it's safe to split by newlines and re-wrap to make it
+ // prettier
+ headerString = strings.Join(
+ strings.Split(headerString, "\n"),
+ "")
+ fmt.Fprintln(w, "device-key: |")
+ wrapLine(w, []rune(headerString), " ", termWidth)
+
+ // the default is all the rest of short scalar values, which all
+ // should be strings
+ default:
+ headerString, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+ fmt.Fprintf(w, "%s:\t%s\n", headerName, headerString)
+ }
+ }
+ }
+
+ return w.Flush()
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_model_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,486 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+const happyModelAssertionResponse = `type: model
+authority-id: mememe
+series: 16
+brand-id: mememe
+model: test-model
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+required-snaps:
+ - core
+ - hello-world
+timestamp: 2017-07-27T00:00:00.0Z
+sign-key-sha3-384: 8B3Wmemeu3H6i4dEV4Q85Q4gIUCHIBCNMHq49e085QeLGHi7v27l3Cqmemer4__t
+
+AcLBcwQAAQoAHRYhBMbX+t6MbKGH5C3nnLZW7+q0g6ELBQJdTdwTAAoJELZW7+q0g6ELEvgQAI3j
+jXTqR6kKOqvw94pArwdMDUaZ++tebASAZgso8ejrW2DQGWSc0Q7SQICIR8bvHxqS1GtupQswOzwS
+U8hjDTv7WEchH1jylyTj/1W1GernmitTKycecRlEkSOE+EpuqBFgTtj6PdA1Fj3CiCRi1rLMhgF2
+luCOitBLaP+E8P3fuATsLqqDLYzt1VY4Y14MU75hMn+CxAQdnOZTI+NzGMasPsldmOYCPNaN/b3N
+6/fDLU47RtNlMJ3K0Tz8kj0bqRbegKlD0RdNbAgo9iZwNmrr5E9WCu9f/0rUor/NIxO77H2ExIll
+zhmsZ7E6qlxvAgBmzKgAXrn68gGrBkIb0eXKiCaKy/i2ApvjVZ9HkOzA6Ldd+SwNJv/iA8rdiMsq
+p2BfKV5f3ju5b6+WktHxAakJ8iqQmj9Yh7piHjsOAUf1PEJd2s2nqQ+pEEn1F0B23gVCY/Fa9YRQ
+iKtWVeL3rBw4dSAaK9rpTMqlNcr+yrdXfTK5YzkCC6RU4yzc5MW0hKeseeSiEDSaRYxvftjFfVNa
+ZaVXKg8Lu+cHtCJDeYXEkPIDQzXswdBO1M8Mb9D0mYxQwHxwvsWv1DByB+Otq08EYgPh4kyHo7ag
+85yK2e/NQ/fxSwQJMhBF74jM1z9arq6RMiE/KOleFAOraKn2hcROKnEeinABW+sOn6vNuMVv
+`
+
+const happyModelWithDisplayNameAssertionResponse = `type: model
+authority-id: mememe
+series: 16
+brand-id: mememe
+model: test-model
+architecture: amd64
+display-name: Model Name
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+required-snaps:
+ - core
+ - hello-world
+timestamp: 2017-07-27T00:00:00.0Z
+sign-key-sha3-384: 8B3Wmemeu3H6i4dEV4Q85Q4gIUCHIBCNMHq49e085QeLGHi7v27l3Cqmemer4__t
+
+AcLBcwQAAQoAHRYhBMbX+t6MbKGH5C3nnLZW7+q0g6ELBQJdTdwTAAoJELZW7+q0g6ELEvgQAI3j
+jXTqR6kKOqvw94pArwdMDUaZ++tebASAZgso8ejrW2DQGWSc0Q7SQICIR8bvHxqS1GtupQswOzwS
+U8hjDTv7WEchH1jylyTj/1W1GernmitTKycecRlEkSOE+EpuqBFgTtj6PdA1Fj3CiCRi1rLMhgF2
+luCOitBLaP+E8P3fuATsLqqDLYzt1VY4Y14MU75hMn+CxAQdnOZTI+NzGMasPsldmOYCPNaN/b3N
+6/fDLU47RtNlMJ3K0Tz8kj0bqRbegKlD0RdNbAgo9iZwNmrr5E9WCu9f/0rUor/NIxO77H2ExIll
+zhmsZ7E6qlxvAgBmzKgAXrn68gGrBkIb0eXKiCaKy/i2ApvjVZ9HkOzA6Ldd+SwNJv/iA8rdiMsq
+p2BfKV5f3ju5b6+WktHxAakJ8iqQmj9Yh7piHjsOAUf1PEJd2s2nqQ+pEEn1F0B23gVCY/Fa9YRQ
+iKtWVeL3rBw4dSAaK9rpTMqlNcr+yrdXfTK5YzkCC6RU4yzc5MW0hKeseeSiEDSaRYxvftjFfVNa
+ZaVXKg8Lu+cHtCJDeYXEkPIDQzXswdBO1M8Mb9D0mYxQwHxwvsWv1DByB+Otq08EYgPh4kyHo7ag
+85yK2e/NQ/fxSwQJMhBF74jM1z9arq6RMiE/KOleFAOraKn2hcROKnEeinABW+sOn6vNuMVv
+`
+
+const happyAccountAssertionResponse = `type: account
+authority-id: canonical
+account-id: mememe
+display-name: MeMeMe
+timestamp: 2016-04-01T00:00:00.0Z
+username: meuser
+validation: certified
+sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
+
+AcLDXAQAAQoABgUCV7UYzwAKCRDUpVvql9g3IK7uH/4udqNOurx5WYVknzXdwekp0ovHCQJ0iBPw
+TSFxEVr9faZSzb7eqJ1WicHsShf97PYS3ClRYAiluFsjRA8Y03kkSVJHjC+sIwGFubsnkmgflt6D
+WEmYIl0UBmeaEDS8uY4Xvp9NsLTzNEj2kvzy/52gKaTc1ZSl5RDL9ppMav+0V9iBYpiDPBWH2rJ+
+aDSD8Rkyygm0UscfAKyDKH4lrvZ0WkYyi1YVNPrjQ/AtBySh6Q4iJ3LifzKa9woIyAuJET/4/FPY
+oirqHAfuvNod36yNQIyNqEc20AvTvZNH0PSsg4rq3DLjIPzv5KbJO9lhsasNJK1OdL6x8Yqrdsbk
+ldZp4qkzfjV7VOMQKaadfcZPRaVVeJWOBnBiaukzkhoNlQi1sdCdkBB/AJHZF8QXw6c7vPDcfnCV
+1lW7ddQ2p8IsJbT6LzpJu3GW/P4xhNgCjtCJ1AJm9a9RqLwQYgdLZwwDa9iCRtqTbRXBlfy3apps
+1VjbQ3h5iCd0hNfwDBnGVm1rhLKHCD1DUdNE43oN2ZlE7XGyh0HFV6vKlpqoW3eoXCIxWu+HBY96
++LSl/jQgCkb0nxYyzEYK4Reb31D0mYw1Nji5W+MIF5E09+DYZoOT0UvR05YMwMEOeSdI/hLWg/5P
+k+GDK+/KopMmpd4D1+jjtF7ZvqDpmAV98jJGB2F88RyVb4gcjmFFyTi4Kv6vzz/oLpbm0qrizC0W
+HLGDN/ymGA5sHzEgEx7U540vz/q9VX60FKqL2YZr/DcyY9GKX5kCG4sNqIIHbcJneZ4frM99oVDu
+7Jv+DIx/Di6D1ULXol2XjxbbJLKHFtHksR97ceaFvcZwTogC61IYUBJCvvMoqdXAWMhEXCr0QfQ5
+Xbi31XW2d4/lF/zWlAkRnGTzufIXFni7+nEuOK0SQEzO3/WaRedK1SGOOtTDjB8/3OJeW96AUYK5
+oTIynkYkEyHWMNCXALg+WQW6L4/YO7aUjZ97zOWIugd7Xy63aT3r/EHafqaY2nacOhLfkeKZ830b
+o/ezjoZQAxbh6ce7JnXRgE9ELxjdAhBTpGjmmmN2sYrJ7zP9bOgly0BnEPXGSQfFA+NNNw1FADx1
+MUY8q9DBjmVtgqY+1KGTV5X8KvQCBMODZIf/XJPHdCRAHxMd8COypcwgL2vDIIXpOFbi1J/B0GF+
+eklxk9wzBA8AecBMCwCzIRHDNpD1oa2we38bVFrOug6e/VId1k1jYFJjiLyLCDmV8IMYwEllHSXp
+LQAdm3xZ7t4WnxYC8YSCk9mXf3CZg59SpmnV5Q5Z6A5Pl7Nc3sj7hcsMBZEsOMPzNC9dPsBnZvjs
+WpPUffJzEdhHBFhvYMuD4Vqj6ejUv9l3oTrjQWVC`
+
+// note: this serial assertion was generated by adding print statements to the
+// test in api_model_test.go that generate a fake serial assertion
+const happySerialAssertionResponse = `type: serial
+authority-id: my-brand
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+device-key:
+ AcZrBFaFwYABAvCgEOrrLA6FKcreHxCcOoTgBUZ+IRG7Nb8tzmEAklaQPGpv7skapUjwD1luE2go
+ mTcoTssVHrfLpBoSDV1aBs44rg3NK40ZKPJP7d2zkds1GxUo1Ea5vfet3SJ4h3aRABEBAAE=
+device-key-sha3-384: iqLo9doLzK8De9925UrdUyuvPbBad72OTWVE9YJXqd6nz9dKvwJ_lHP5bVxrl3VO
+timestamp: 2019-08-26T16:34:21-05:00
+sign-key-sha3-384: anCEGC2NYq7DzDEi6y7OafQCVeVLS90XlLt9PNjrRl9sim5rmRHDDNFNO7ODcWQW
+
+AcJwBAABCgAGBQJdZFBdAADCLALwR6Sy24wm9PffwbvUhOEXneyY3BnxKC0+NgdHu1gU8go9vEP1
+i+Flh5uoS70+MBIO+nmF8T+9JWIx2QWFDDxvcuFosnIhvUajCEQohauys5FMz/H/WvB0vrbTBpvK
+eg==
+`
+
+const noModelAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no model assertion yet",
+ "kind": "assertion-not-found",
+ "value": "model"
+ }
+}`
+
+const noSerialAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no serial assertion yet",
+ "kind": "assertion-not-found",
+ "value": "serial"
+ }
+}`
+
+// helper for constructing different types of responses to the client
+type checkResponder func(c *check.C, w http.ResponseWriter, r *http.Request)
+
+func simpleHappyResponder(body string) checkResponder {
+ return func(c *check.C, w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.RawQuery, check.Equals, "")
+ fmt.Fprintln(w, body)
+ }
+}
+
+func simpleUnhappyResponder(errBody string) checkResponder {
+ return func(c *check.C, w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.RawQuery, check.Equals, "")
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(404)
+ fmt.Fprintln(w, errBody)
+ }
+}
+
+func simpleAssertionAccountResponder(body string) checkResponder {
+ return func(c *check.C, w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, check.Equals, "GET")
+ w.Header().Set("X-Ubuntu-Assertions-Count", "1")
+ fmt.Fprintln(w, body)
+ }
+}
+
+func makeHappyTestServerHandler(c *check.C, modelResp, serialResp, accountResp checkResponder) func(w http.ResponseWriter, r *http.Request) {
+ var nModelSerial, nModel, nKnown int
+ return func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/model":
+ switch nModel {
+ case 0:
+ modelResp(c, w, r)
+ default:
+ c.Fatalf("expected to get 1 request for /v2/model, now on %d", nModel+1)
+ }
+ nModel++
+ case "/v2/model/serial":
+ switch nModelSerial {
+ case 0:
+ serialResp(c, w, r)
+ default:
+ c.Fatalf("expected to get 1 request for /v2/model, now on %d", nModelSerial+1)
+ }
+ nModelSerial++
+ case "/v2/assertions/account":
+ switch nKnown {
+ case 0:
+ accountResp(c, w, r)
+ default:
+ c.Fatalf("expected to get 1 request for /v2/model, now on %d", nKnown+1)
+ }
+ nKnown++
+ default:
+ c.Fatalf("unexpected request to %s", r.URL.Path)
+ }
+ }
+}
+
+func (s *SnapSuite) TestNoModelYet(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleUnhappyResponder(noModelAssertionYetResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model"})
+ c.Assert(err, check.ErrorMatches, `device not ready yet \(no assertions found\)`)
+}
+
+func (s *SnapSuite) TestNoSerialYet(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial"})
+ c.Assert(err, check.ErrorMatches, `device not registered yet \(no serial assertion found\)`)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: mememe
+model: test-model
+`[1:])
+}
+
+func (s *SnapSuite) TestModel(c *check.C) {
+
+ for _, tt := range []struct {
+ comment string
+ modelF checkResponder
+ serialF checkResponder
+ outText string
+ }{
+ {
+ comment: "normal serial and model asserts",
+ modelF: simpleHappyResponder(happyModelAssertionResponse),
+ serialF: simpleHappyResponder(happySerialAssertionResponse),
+ outText: `
+brand MeMeMe (meuser*)
+model test-model
+serial serialserial
+`[1:],
+ },
+ {
+ comment: "model assert has display-name",
+ modelF: simpleHappyResponder(happyModelWithDisplayNameAssertionResponse),
+ serialF: simpleHappyResponder(happySerialAssertionResponse),
+ outText: `
+brand MeMeMe (meuser*)
+model Model Name (test-model)
+serial serialserial
+`[1:],
+ },
+ {
+ comment: "missing serial assert",
+ modelF: simpleHappyResponder(happyModelAssertionResponse),
+ serialF: simpleUnhappyResponder(noSerialAssertionYetResponse),
+ outText: `
+brand MeMeMe (meuser*)
+model test-model
+serial - (device not registered yet)
+`[1:],
+ },
+ } {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ tt.modelF,
+ tt.serialF,
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, tt.outText, check.Commentf("\n%s\n", tt.outText))
+ c.Check(s.Stderr(), check.Equals, "")
+ s.ResetStdStreams()
+ }
+}
+
+func (s *SnapSuite) TestModelVerbose(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--verbose", "--abs-time"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: mememe
+model: test-model
+serial: serialserial
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2017-07-27T00:00:00Z
+required-snaps:
+ - core
+ - hello-world
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestModelVerboseDisplayName(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelWithDisplayNameAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--verbose", "--abs-time"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: mememe
+model: test-model
+serial: serialserial
+architecture: amd64
+base: core18
+display-name: Model Name
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2017-07-27T00:00:00Z
+required-snaps:
+ - core
+ - hello-world
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestModelVerboseNoSerialYet(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--verbose", "--abs-time"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: mememe
+model: test-model
+serial: -- (device not registered yet)
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2017-07-27T00:00:00Z
+required-snaps:
+ - core
+ - hello-world
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestModelAssertion(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--assertion"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, happyModelAssertionResponse)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestModelAssertionVerbose(c *check.C) {
+ // check that no calls to the server happen
+ s.RedirectClientToTestServer(
+ func(w http.ResponseWriter, r *http.Request) {
+ c.Fatalf("unexpected request to %s", r.URL.Path)
+ },
+ )
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--assertion", "--verbose"})
+ c.Assert(err, check.ErrorMatches, "cannot use --verbose with --assertion")
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerial(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerialVerbose(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial", "--verbose", "--abs-time"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+timestamp: 2019-08-26T16:34:21-05:00
+device-key-sha3-384: |
+ iqLo9doLzK8De9925UrdUyuvPbBad72OTWVE9YJXqd6nz9dKvwJ_lHP5bVxrl3VO
+device-key: |
+ AcZrBFaFwYABAvCgEOrrLA6FKcreHxCcOoTgBUZ+IRG7Nb8tzmEAklaQPGpv7skapUjwD1luE2g
+ omTcoTssVHrfLpBoSDV1aBs44rg3NK40ZKPJP7d2zkds1GxUo1Ea5vfet3SJ4h3aRABEBAAE=
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerialAssertion(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial", "--assertion"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, happySerialAssertionResponse)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerialAssertionSerialAssertionMissing(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial", "--assertion"})
+ c.Assert(err, check.ErrorMatches, `device not ready yet \(no assertions found\)`)
+ c.Assert(s.Stdout(), check.Equals, "")
+ c.Assert(s.Stderr(), check.Equals, "")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_pack.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_pack.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack.go 2020-06-05 13:13:49.000000000 +0000
@@ -28,11 +28,15 @@
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/pack"
+
+ // for SanitizePlugsSlots
+ "github.com/snapcore/snapd/interfaces/builtin"
)
type packCmd struct {
CheckSkeleton bool `long:"check-skeleton"`
Filename string `long:"filename"`
+ Compression string `long:"compression" hidden:"yes"`
Positional struct {
SnapDir string `positional-arg-name:""`
TargetDir string `positional-arg-name:""`
@@ -69,6 +73,8 @@
"check-skeleton": i18n.G("Validate snap-dir metadata only"),
// TRANSLATORS: This should not start with a lowercase letter.
"filename": i18n.G("Output to this filename"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "compression": i18n.G("Compression to use (e.g. xz)"),
}, nil)
cmd.extra = func(cmd *flags.Command) {
// TRANSLATORS: this describes the default filename for a snap, e.g. core_16-2.35.2_amd64.snap
@@ -77,6 +83,10 @@
}
func (x *packCmd) Execute([]string) error {
+ // plug/slot sanitization is disabled (no-op) by default at the package level for "snap" command,
+ // for "snap pack" however we want real validation.
+ snap.SanitizePlugsSlots = builtin.SanitizePlugsSlots
+
if x.Positional.TargetDir != "" && x.Filename != "" && filepath.IsAbs(x.Filename) {
return fmt.Errorf(i18n.G("you can't specify an absolute filename while also specifying target dir."))
}
@@ -89,14 +99,18 @@
}
if x.CheckSkeleton {
- err := pack.CheckSkeleton(x.Positional.SnapDir)
+ err := pack.CheckSkeleton(Stderr, x.Positional.SnapDir)
if err == snap.ErrMissingPaths {
return nil
}
return err
}
- snapPath, err := pack.Snap(x.Positional.SnapDir, x.Positional.TargetDir, x.Filename)
+ snapPath, err := pack.Snap(x.Positional.SnapDir, &pack.Options{
+ TargetDir: x.Positional.TargetDir,
+ SnapName: x.Filename,
+ Compression: x.Compression,
+ })
if err != nil {
// TRANSLATORS: the %q is the snap-dir (the first positional
// argument to the command); the %v is an error
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_pack_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_pack_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,6 +1,7 @@
package main_test
import (
+ "fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -71,6 +72,20 @@
c.Assert(err, check.ErrorMatches, `cannot validate snap "foo": application ("bar" common-id "org.foo.foo" must be unique, already used by application "foo"|"foo" common-id "org.foo.foo" must be unique, already used by application "bar")`)
}
+func (s *SnapSuite) TestPackCheckSkeletonWonkyInterfaces(c *check.C) {
+ snapYaml := `
+name: foo
+version: 1.0.1
+slots:
+ kale:
+`
+ snapDir := makeSnapDirForPack(c, snapYaml)
+
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir})
+ c.Assert(err, check.IsNil)
+ c.Check(s.stderr.String(), check.Equals, "snap \"foo\" has bad plugs or slots: kale (unknown interface \"kale\")\n")
+}
+
func (s *SnapSuite) TestPackPacksFailsForMissingPaths(c *check.C) {
_, r := logger.MockLogger()
defer r()
@@ -101,3 +116,27 @@
c.Assert(err, check.IsNil)
c.Assert(matches, check.HasLen, 1)
}
+
+func (s *SnapSuite) TestPackPacksASnapWithCompressionHappy(c *check.C) {
+ snapDir := makeSnapDirForPack(c, "name: hello\nversion: 1.0")
+
+ for _, comp := range []string{"xz", "lzo"} {
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--compression", comp, snapDir, snapDir})
+ c.Assert(err, check.IsNil)
+
+ matches, err := filepath.Glob(snapDir + "/hello*.snap")
+ c.Assert(err, check.IsNil)
+ c.Assert(matches, check.HasLen, 1)
+ err = os.Remove(matches[0])
+ c.Assert(err, check.IsNil)
+ }
+}
+
+func (s *SnapSuite) TestPackPacksASnapWithCompressionUnhappy(c *check.C) {
+ snapDir := makeSnapDirForPack(c, "name: hello\nversion: 1.0")
+
+ for _, comp := range []string{"gzip", "zstd", "silly"} {
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--compression", comp, snapDir, snapDir})
+ c.Assert(err, check.ErrorMatches, fmt.Sprintf(`cannot pack "/.*": cannot use compression %q`, comp))
+ }
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prefer_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prefer_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prefer_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prefer_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -52,6 +52,7 @@
"action": "prefer",
"snap": "some-snap",
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prepare_image.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prepare_image.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * Copyright (C) 2014-2019 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
@@ -20,7 +20,7 @@
package main
import (
- "path/filepath"
+ "strings"
"github.com/jessevdk/go-flags"
@@ -29,27 +29,41 @@
)
type cmdPrepareImage struct {
+ Classic bool `long:"classic"`
+ Architecture string `long:"arch"`
+
Positional struct {
ModelAssertionFn string
- Rootdir string
+ TargetDir string
} `positional-args:"yes" required:"yes"`
- ExtraSnaps []string `long:"extra-snaps"`
- Channel string `long:"channel" default:"stable"`
+ Channel string `long:"channel"`
+ // TODO: introduce SnapWithChannel?
+ Snaps []string `long:"snap" value-name:"[=]"`
+ ExtraSnaps []string `long:"extra-snaps" hidden:"yes"` // DEPRECATED
}
func init() {
- cmd := addCommand("prepare-image",
- i18n.G("Prepare a core device image"),
+ addCommand("prepare-image",
+ i18n.G("Prepare a device image"),
i18n.G(`
-The prepare-image command performs some of the steps necessary for creating
-core device images.
-`),
- func() flags.Commander {
- return &cmdPrepareImage{}
- }, map[string]string{
+The prepare-image command performs some of the steps necessary for
+creating device images.
+
+For core images it is not invoked directly but usually via
+ubuntu-image.
+
+For preparing classic images it supports a --classic mode`),
+ func() flags.Commander { return &cmdPrepareImage{} },
+ map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "classic": i18n.G("Enable classic mode to prepare a classic model image"),
// TRANSLATORS: This should not start with a lowercase letter.
- "extra-snaps": i18n.G("Extra snaps to be installed"),
+ "arch": i18n.G("Specify an architecture for snaps for --classic when the model does not"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "snap": i18n.G("Include the given snap from the store or a local file and/or specify the channel to track for the given snap"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "extra-snaps": i18n.G("Extra snaps to be installed (DEPRECATED)"),
// TRANSLATORS: This should not start with a lowercase letter.
"channel": i18n.G("The channel to use"),
}, []argDesc{
@@ -60,23 +74,44 @@
desc: i18n.G("The model assertion name"),
}, {
// TRANSLATORS: This needs to begin with < and end with >
- name: i18n.G(""),
+ name: i18n.G(""),
// TRANSLATORS: This should not start with a lowercase letter.
- desc: i18n.G("The output directory"),
+ desc: i18n.G("The target directory"),
},
})
- cmd.hidden = true
}
+var imagePrepare = image.Prepare
+
func (x *cmdPrepareImage) Execute(args []string) error {
opts := &image.Options{
- ModelFile: x.Positional.ModelAssertionFn,
+ Snaps: x.ExtraSnaps,
+ ModelFile: x.Positional.ModelAssertionFn,
+ Channel: x.Channel,
+ Architecture: x.Architecture,
+ }
+
+ snaps := make([]string, 0, len(x.Snaps)+len(x.ExtraSnaps))
+ snapChannels := make(map[string]string)
+ for _, snapWChannel := range x.Snaps {
+ snapAndChannel := strings.SplitN(snapWChannel, "=", 2)
+ snaps = append(snaps, snapAndChannel[0])
+ if len(snapAndChannel) == 2 {
+ snapChannels[snapAndChannel[0]] = snapAndChannel[1]
+ }
+ }
- RootDir: filepath.Join(x.Positional.Rootdir, "image"),
- GadgetUnpackDir: filepath.Join(x.Positional.Rootdir, "gadget"),
- Channel: x.Channel,
- Snaps: x.ExtraSnaps,
+ snaps = append(snaps, x.ExtraSnaps...)
+
+ if len(snaps) != 0 {
+ opts.Snaps = snaps
+ }
+ if len(snapChannels) != 0 {
+ opts.SnapChannels = snapChannels
}
- return image.Prepare(opts)
+ opts.PrepareDir = x.Positional.TargetDir
+ opts.Classic = x.Classic
+
+ return imagePrepare(opts)
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prepare_image_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prepare_image_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,115 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/image"
+)
+
+type SnapPrepareImageSuite struct {
+ BaseSnapSuite
+}
+
+var _ = Suite(&SnapPrepareImageSuite{})
+
+func (s *SnapPrepareImageSuite) TestPrepareImageCore(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "model", "prepare-dir"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ ModelFile: "model",
+ PrepareDir: "prepare-dir",
+ })
+}
+
+func (s *SnapPrepareImageSuite) TestPrepareImageClassic(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--classic", "model", "prepare-dir"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ Classic: true,
+ ModelFile: "model",
+ PrepareDir: "prepare-dir",
+ })
+}
+
+func (s *SnapPrepareImageSuite) TestPrepareImageClassicArch(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--classic", "--arch", "i386", "model", "prepare-dir"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ Classic: true,
+ Architecture: "i386",
+ ModelFile: "model",
+ PrepareDir: "prepare-dir",
+ })
+}
+
+func (s *SnapPrepareImageSuite) TestPrepareImageExtraSnaps(c *C) {
+ var opts *image.Options
+ prep := func(o *image.Options) error {
+ opts = o
+ return nil
+ }
+ r := snap.MockImagePrepare(prep)
+ defer r()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "model", "prepare-dir", "--channel", "candidate", "--snap", "foo", "--snap", "bar=t/edge", "--snap", "local.snap", "--extra-snaps", "local2.snap", "--extra-snaps", "store-snap"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+
+ c.Check(opts, DeepEquals, &image.Options{
+ ModelFile: "model",
+ Channel: "candidate",
+ PrepareDir: "prepare-dir",
+ Snaps: []string{"foo", "bar", "local.snap", "local2.snap", "store-snap"},
+ SnapChannels: map[string]string{"bar": "t/edge"},
+ })
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remodel.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remodel.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remodel.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remodel.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,86 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+var (
+ shortRemodelHelp = i18n.G("Remodel this device")
+ longRemodelHelp = i18n.G(`
+The remodel command changes the model assertion of the device, either to a new
+revision or a full new model.
+
+In the process it applies any implied changes to the device: new required
+snaps, new kernel or gadget etc.
+`)
+)
+
+type cmdRemodel struct {
+ waitMixin
+ RemodelOptions struct {
+ NewModelFile flags.Filename
+ } `positional-args:"true" required:"true"`
+}
+
+func init() {
+ cmd := addCommand("remodel",
+ shortRemodelHelp,
+ longRemodelHelp,
+ func() flags.Commander {
+ return &cmdRemodel{}
+ }, nil, []argDesc{{
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("New model file"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdRemodel) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+ newModelFile := x.RemodelOptions.NewModelFile
+ modelData, err := ioutil.ReadFile(string(newModelFile))
+ if err != nil {
+ return err
+ }
+ changeID, err := x.client.Remodel(modelData)
+ if err != nil {
+ return fmt.Errorf("cannot remodel: %v", err)
+ }
+
+ if _, err := x.wait(changeID); err != nil {
+ if err == noWait {
+ return nil
+ }
+ return err
+ }
+ fmt.Fprintf(Stdout, i18n.G("New model %s set\n"), newModelFile)
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,75 @@
+// -*- 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+
+ "github.com/jessevdk/go-flags"
+)
+
+var shortRemoveUserHelp = i18n.G("Remove a local system user")
+var longRemoveUserHelp = i18n.G(`
+The remove-user command removes a local system user.
+`)
+
+type cmdRemoveUser struct {
+ clientMixin
+ Positional struct {
+ Username string
+ } `positional-args:"yes"`
+}
+
+func init() {
+ cmd := addCommand("remove-user", shortRemoveUserHelp, longRemoveUserHelp, func() flags.Commander { return &cmdRemoveUser{} },
+ map[string]string{}, []argDesc{{
+ // TRANSLATORS: This is a noun and it needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter
+ desc: i18n.G("The username to remove"),
+ }})
+ cmd.hidden = true
+}
+
+func (x *cmdRemoveUser) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ username := x.Positional.Username
+
+ options := client.RemoveUserOptions{
+ Username: username,
+ }
+
+ removed, err := x.client.RemoveUser(&options)
+ if err != nil {
+ return err
+ }
+ if len(removed) != 1 {
+ return fmt.Errorf("internal error: RemoveUser returned unexpected number of removed users: %v", len(removed))
+ }
+ fmt.Fprintf(Stdout, i18n.G("removed user %q\n"), removed[0].Username)
+
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,109 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+var removeUserJsonFmtReplyHappy = `{
+ "type": "sync",
+ "result": {
+ "removed": [{"username": %q}]
+ }
+}`
+
+var removeUserJsonReplyTooMany = `{
+ "type": "sync",
+ "result": {
+ "removed": [{"username": "too"}, {"username": "many"}]
+ }
+}`
+
+var removeUserJsonReplyTooFew = `{
+ "type": "sync",
+ "result": {
+ "removed": []
+ }
+}`
+
+func makeRemoveUserChecker(c *check.C, n *int, username string, fmtJsonReply string) func(w http.ResponseWriter, r *http.Request) {
+ f := func(w http.ResponseWriter, r *http.Request) {
+ switch *n {
+ case 0:
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/users")
+ var gotBody map[string]interface{}
+ dec := json.NewDecoder(r.Body)
+ err := dec.Decode(&gotBody)
+ c.Assert(err, check.IsNil)
+
+ wantBody := map[string]interface{}{
+ "username": username,
+ "action": "remove",
+ }
+ c.Check(gotBody, check.DeepEquals, wantBody)
+
+ fmt.Fprint(w, fmtJsonReply)
+ default:
+ c.Fatalf("got too many requests (now on %d)", *n+1)
+ }
+
+ *n++
+ }
+ return f
+}
+
+func (s *SnapSuite) TestRemoveUser(c *check.C) {
+ n := 0
+ username := "karl"
+ s.RedirectClientToTestServer(makeRemoveUserChecker(c, &n, username, fmt.Sprintf(removeUserJsonFmtReplyHappy, username)))
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove-user", "karl"})
+ c.Assert(err, check.IsNil)
+ c.Check(rest, check.DeepEquals, []string{})
+ c.Check(n, check.Equals, 1)
+ c.Assert(s.Stdout(), check.Equals, fmt.Sprintf("removed user %q\n", username))
+ c.Assert(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestRemoveUserUnhappyTooMany(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeRemoveUserChecker(c, &n, "karl", removeUserJsonReplyTooMany))
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove-user", "karl"})
+ c.Assert(err, check.ErrorMatches, `internal error: RemoveUser returned unexpected number of removed users: 2`)
+ c.Check(n, check.Equals, 1)
+}
+
+func (s *SnapSuite) TestRemoveUserUnhappyTooFew(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(makeRemoveUserChecker(c, &n, "karl", removeUserJsonReplyTooFew))
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove-user", "karl"})
+ c.Assert(err, check.ErrorMatches, `internal error: RemoveUser returned unexpected number of removed users: 0`)
+ c.Check(n, check.Equals, 1)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs.go 2020-06-05 13:13:49.000000000 +0000
@@ -69,6 +69,10 @@
}
func (x *cmdShowRepair) Execute(args []string) error {
+ if len(x.Positional.Repair) == 0 {
+ return fmt.Errorf("no given. Try 'snap repairs' to list all repairs or specify a specific repair id.")
+ }
+
return runSnapRepair("show", x.Positional.Repair)
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -20,7 +20,6 @@
package main_test
import (
- "os"
"path/filepath"
. "gopkg.in/check.v1"
@@ -32,10 +31,7 @@
)
func mockSnapRepair(c *C) *testutil.MockCmd {
- coreLibExecDir := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir)
- err := os.MkdirAll(coreLibExecDir, 0755)
- c.Assert(err, IsNil)
- return testutil.MockCommand(c, filepath.Join(coreLibExecDir, "snap-repair"), "")
+ return testutil.MockCommand(c, filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snap-repair"), "")
}
func (s *SnapSuite) TestSnapShowRepair(c *C) {
@@ -52,6 +48,14 @@
})
}
+func (s *SnapSuite) TestSnapShowRepairNoArgs(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"repair"})
+ c.Assert(err, ErrorMatches, "no given. Try 'snap repairs' to list all repairs or specify a specific repair id.")
+}
+
func (s *SnapSuite) TestSnapListRepairs(c *C) {
restore := release.MockOnClassic(false)
defer restore()
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,216 @@
+// -*- 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdRoutineFileAccess struct {
+ clientMixin
+ FileAccessOptions struct {
+ Snap installedSnapName
+ Path flags.Filename
+ } `positional-args:"true" required:"true"`
+}
+
+var shortRoutineFileAccessHelp = i18n.G("Return information about file access by a snap")
+var longRoutineFileAccessHelp = i18n.G(`
+The file-access command returns information about a snap's file system access.
+
+This command is used by the xdg-document-portal service to identify
+files that do not need to be proxied to provide access within
+confinement.
+
+File paths are interpreted as host file system paths. The tool may
+return false negatives (e.g. report that a file path is unreadable,
+despite being readable under a different path). It also does not
+check if file system permissions would render a file unreadable.
+`)
+
+func init() {
+ addRoutineCommand("file-access", shortRoutineFileAccessHelp, longRoutineFileAccessHelp, func() flags.Commander {
+ return &cmdRoutineFileAccess{}
+ }, nil, []argDesc{
+ {
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Snap name"),
+ },
+ {
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("File path"),
+ },
+ })
+}
+
+func (x *cmdRoutineFileAccess) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ snapName := string(x.FileAccessOptions.Snap)
+ path := string(x.FileAccessOptions.Path)
+
+ snap, _, err := x.client.Snap(snapName)
+ if err != nil {
+ return fmt.Errorf("cannot retrieve info for snap %q: %v", snapName, err)
+ }
+
+ // Check whether the snap has home or removable-media plugs connected
+ connections, err := x.client.Connections(&client.ConnectionOptions{
+ Snap: snap.Name,
+ })
+ if err != nil {
+ return fmt.Errorf("cannot get connections for snap %q: %v", snap.Name, err)
+ }
+ var hasHome, hasRemovableMedia bool
+ for _, conn := range connections.Established {
+ if conn.Plug.Snap != snap.Name {
+ continue
+ }
+ switch conn.Interface {
+ case "home":
+ hasHome = true
+ case "removable-media":
+ hasRemovableMedia = true
+ }
+ }
+
+ access, err := x.checkAccess(snap, hasHome, hasRemovableMedia, path)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(Stdout, access)
+ return nil
+}
+
+type FileAccess string
+
+const (
+ FileAccessHidden FileAccess = "hidden"
+ FileAccessReadOnly FileAccess = "read-only"
+ FileAccessReadWrite FileAccess = "read-write"
+)
+
+func splitPathAbs(path string) ([]string, error) {
+ // Abs also cleans the path, removing any ".." components
+ path, err := filepath.Abs(path)
+ if err != nil {
+ return nil, err
+ }
+ // Ignore the empty component before the first slash
+ return strings.Split(path, string(os.PathSeparator))[1:], nil
+}
+
+func pathHasPrefix(path, prefix []string) bool {
+ if len(path) < len(prefix) {
+ return false
+ }
+ for i := range prefix {
+ if path[i] != prefix[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func (x *cmdRoutineFileAccess) checkAccess(snap *client.Snap, hasHome, hasRemovableMedia bool, path string) (FileAccess, error) {
+ // Classic confinement snaps run in the host system namespace,
+ // so can see everything.
+ if snap.Confinement == client.ClassicConfinement {
+ return FileAccessReadWrite, nil
+ }
+
+ pathParts, err := splitPathAbs(path)
+ if err != nil {
+ return "", err
+ }
+
+ // Snaps have access to $SNAP_DATA and $SNAP_COMMON
+ if pathHasPrefix(pathParts, []string{"var", "snap", snap.Name}) {
+ if len(pathParts) == 3 {
+ return FileAccessReadOnly, nil
+ }
+ switch pathParts[3] {
+ case "common", "current", snap.Revision.String():
+ return FileAccessReadWrite, nil
+ default:
+ return FileAccessReadOnly, nil
+ }
+ }
+
+ // Snaps with removable-media plugged can access removable
+ // media mount points.
+ if hasRemovableMedia {
+ if pathHasPrefix(pathParts, []string{"mnt"}) || pathHasPrefix(pathParts, []string{"media"}) || pathHasPrefix(pathParts, []string{"run", "media"}) {
+ return FileAccessReadWrite, nil
+ }
+ }
+
+ usr, err := userCurrent()
+ if err != nil {
+ return "", fmt.Errorf("cannot get the current user: %v", err)
+ }
+
+ home, err := splitPathAbs(usr.HomeDir)
+ if err != nil {
+ return "", err
+ }
+ if pathHasPrefix(pathParts, home) {
+ pathInHome := pathParts[len(home):]
+ // Snaps have access to $SNAP_USER_DATA and $SNAP_USER_COMMON
+ if pathHasPrefix(pathInHome, []string{"snap"}) {
+ if !pathHasPrefix(pathInHome, []string{"snap", snap.Name}) {
+ return FileAccessHidden, nil
+ }
+ if len(pathInHome) < 3 {
+ return FileAccessReadOnly, nil
+ }
+ switch pathInHome[2] {
+ case "common", "current", snap.Revision.String():
+ return FileAccessReadWrite, nil
+ default:
+ return FileAccessReadOnly, nil
+ }
+ }
+ // If the home interface is connected, the snap has
+ // access to other files in home, except top-level dot
+ // files.
+ if hasHome {
+ if len(pathInHome) == 0 || !strings.HasPrefix(pathInHome[0], ".") {
+ return FileAccessReadWrite, nil
+ }
+ }
+ }
+
+ return FileAccessHidden, nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,185 @@
+// -*- 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "os/user"
+ "path/filepath"
+ "strings"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+type SnapRoutineFileAccessSuite struct {
+ BaseSnapSuite
+
+ fakeHome string
+}
+
+var _ = Suite(&SnapRoutineFileAccessSuite{})
+
+func (s *SnapRoutineFileAccessSuite) SetUpTest(c *C) {
+ s.BaseSnapSuite.SetUpTest(c)
+
+ s.fakeHome = c.MkDir()
+ u, err := user.Current()
+ c.Assert(err, IsNil)
+ s.AddCleanup(snap.MockUserCurrent(func() (*user.User, error) {
+ return &user.User{Uid: u.Uid, HomeDir: s.fakeHome}, nil
+ }))
+}
+
+func (s *SnapRoutineFileAccessSuite) setUpClient(c *C, isClassic, hasHome, hasRemovableMedia bool) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/snaps/hello":
+ c.Check(r.Method, Equals, "GET")
+ // snap hello at revision 100
+ response := mockInfoJSONNoLicense
+ if isClassic {
+ response = strings.Replace(response, `"confinement": "strict"`, `"confinement": "classic"`, 1)
+ }
+ fmt.Fprintln(w, response)
+ case "/v2/connections":
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, url.Values{
+ "snap": []string{"hello"},
+ })
+ connections := []client.Connection{}
+ if hasHome {
+ connections = append(connections, client.Connection{
+ Slot: client.SlotRef{
+ Snap: "core",
+ Name: "home",
+ },
+ Plug: client.PlugRef{
+ Snap: "hello",
+ Name: "home",
+ },
+ Interface: "home",
+ })
+ }
+ if hasRemovableMedia {
+ connections = append(connections, client.Connection{
+ Slot: client.SlotRef{
+ Snap: "core",
+ Name: "removable-media",
+ },
+ Plug: client.PlugRef{
+ Snap: "hello",
+ Name: "removable-media",
+ },
+ Interface: "removable-media",
+ })
+ }
+ result := client.Connections{Established: connections}
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ default:
+ c.Fatalf("unexpected request: %v", r)
+ }
+ })
+}
+
+func (s *SnapRoutineFileAccessSuite) checkAccess(c *C, path, access string) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "file-access", "hello", path})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, access)
+ c.Check(s.Stderr(), Equals, "")
+ s.ResetStdStreams()
+}
+
+func (s *SnapRoutineFileAccessSuite) checkBasicAccess(c *C) {
+ // Check access to SNAP_DATA and SNAP_COMMON
+ s.checkAccess(c, "/var/snap", "hidden\n")
+ s.checkAccess(c, "/var/snap/other-snap", "hidden\n")
+ s.checkAccess(c, "/var/snap/hello", "read-only\n")
+ s.checkAccess(c, "/var/snap/hello/common", "read-write\n")
+ s.checkAccess(c, "/var/snap/hello/current", "read-write\n")
+ s.checkAccess(c, "/var/snap/hello/100", "read-write\n")
+ s.checkAccess(c, "/var/snap/hello/99", "read-only\n")
+
+ // Check access to SNAP_USER_DATA and SNAP_USER_COMMON
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap"), "hidden\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap/other-snap"), "hidden\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello"), "read-only\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/common"), "read-write\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/current"), "read-write\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/100"), "read-write\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/99"), "read-only\n")
+}
+
+func (s *SnapRoutineFileAccessSuite) TestAccessDefault(c *C) {
+ s.setUpClient(c, false, false, false)
+ s.checkBasicAccess(c)
+
+ // No access to root
+ s.checkAccess(c, "/", "hidden\n")
+ s.checkAccess(c, "/usr/lib/libfoo.so", "hidden\n")
+ // No access to removable media
+ s.checkAccess(c, "/media/foo", "hidden\n")
+ // No access to home directory
+ s.checkAccess(c, s.fakeHome, "hidden\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "Documents"), "hidden\n")
+}
+
+func (s *SnapRoutineFileAccessSuite) TestAccessClassicConfinement(c *C) {
+ s.setUpClient(c, true, false, false)
+
+ // Classic confinement snaps run in the host file system
+ // namespace, so have access to everything.
+ s.checkAccess(c, "/", "read-write\n")
+ s.checkAccess(c, "/usr/lib/libfoo.so", "read-write\n")
+ s.checkAccess(c, "/", "read-write\n")
+ s.checkAccess(c, s.fakeHome, "read-write\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "snap/other-snap"), "read-write\n")
+}
+
+func (s *SnapRoutineFileAccessSuite) TestAccessHomeInterface(c *C) {
+ s.setUpClient(c, false, true, false)
+ s.checkBasicAccess(c)
+
+ // Access to non-hidden files in the home directory
+ s.checkAccess(c, s.fakeHome, "read-write\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "Documents/foo.txt"), "read-write\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, "Documents/.hidden"), "read-write\n")
+ s.checkAccess(c, filepath.Join(s.fakeHome, ".config"), "hidden\n")
+}
+
+func (s *SnapRoutineFileAccessSuite) TestAccessRemovableMedia(c *C) {
+ s.setUpClient(c, false, false, true)
+ s.checkBasicAccess(c)
+
+ s.checkAccess(c, "/mnt", "read-write\n")
+ s.checkAccess(c, "/mnt/path/file.txt", "read-write\n")
+ s.checkAccess(c, "/media", "read-write\n")
+ s.checkAccess(c, "/media/path/file.txt", "read-write\n")
+ s.checkAccess(c, "/run/media", "read-write\n")
+ s.checkAccess(c, "/run/media/path/file.txt", "read-write\n")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,35 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdRoutine struct{}
+
+var shortRoutineHelp = i18n.G("Run routine commands")
+var longRoutineHelp = i18n.G(`
+The routine command contains a selection of additional sub-commands.
+
+Routine commands are not intended to be directly invoked by the user.
+Instead, they are intended to be called by other programs and produce
+machine readable output.
+`)
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,152 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "text/template"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/sandbox/apparmor"
+ "github.com/snapcore/snapd/sandbox/cgroup"
+)
+
+type cmdRoutinePortalInfo struct {
+ clientMixin
+ PortalInfoOptions struct {
+ Pid int
+ } `positional-args:"true" required:"true"`
+}
+
+var shortRoutinePortalInfoHelp = i18n.G("Return information about a process")
+var longRoutinePortalInfoHelp = i18n.G(`
+The portal-info command returns information about a process in keyfile format.
+
+This command is used by the xdg-desktop-portal service to retrieve
+information about snap confined processes.
+`)
+
+func init() {
+ addRoutineCommand("portal-info", shortRoutinePortalInfoHelp, longRoutinePortalInfoHelp, func() flags.Commander {
+ return &cmdRoutinePortalInfo{}
+ }, nil, []argDesc{{
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Process ID of confined app"),
+ }})
+}
+
+var (
+ cgroupSnapNameFromPid = cgroup.SnapNameFromPid
+ apparmorSnapAppFromPid = apparmor.SnapAppFromPid
+)
+
+func (x *cmdRoutinePortalInfo) Execute(args []string) error {
+ if len(args) > 0 {
+ return ErrExtraArgs
+ }
+
+ snapName, err := cgroupSnapNameFromPid(x.PortalInfoOptions.Pid)
+ if err != nil {
+ return err
+ }
+ snap, _, err := x.client.Snap(snapName)
+ if err != nil {
+ return fmt.Errorf("cannot retrieve info for snap %q: %v", snapName, err)
+ }
+
+ // Try to identify the application name from AppArmor
+ var app *client.AppInfo
+ if snapName, appName, _, err := apparmorSnapAppFromPid(x.PortalInfoOptions.Pid); err == nil && snapName == snap.Name && appName != "" {
+ for i := range snap.Apps {
+ if snap.Apps[i].Name == appName {
+ app = &snap.Apps[i]
+ break
+ }
+ }
+ }
+ // As a fallback, pick an app with a desktop file, favouring
+ // the app named identically to the snap.
+ if app == nil {
+ for i := range snap.Apps {
+ if snap.Apps[i].DesktopFile != "" && (app == nil || snap.Apps[i].Name == snap.Name) {
+ app = &snap.Apps[i]
+ }
+ }
+ }
+
+ var desktopFile string
+ if app != nil {
+ desktopFile = filepath.Base(app.DesktopFile)
+ }
+
+ // Determine whether the snap has access to the network status
+ // TODO: use direct API for asking about interface being connected if
+ // that becomes available
+ connections, err := x.client.Connections(&client.ConnectionOptions{
+ Snap: snap.Name,
+ Interface: "network-status",
+ })
+ if err != nil {
+ return fmt.Errorf("cannot get connections for snap %q: %v", snap.Name, err)
+ }
+ // XXX: on non-AppArmor systems, or systems where there is only a
+ // partial AppArmor support, the snap may still be able to access the
+ // network despite the 'network' interface being disconnected
+ var hasNetworkStatus bool
+ for _, conn := range connections.Established {
+ if conn.Plug.Snap == snap.Name && conn.Interface == "network-status" {
+ hasNetworkStatus = true
+ break
+ }
+ }
+
+ const portalInfoTemplate = `[Snap Info]
+InstanceName={{.Snap.Name}}
+{{- if .App}}
+AppName={{.App.Name}}
+{{- end}}
+{{- if .DesktopFile}}
+DesktopFile={{.DesktopFile}}
+{{- end}}
+HasNetworkStatus={{.HasNetworkStatus}}
+`
+ t := template.Must(template.New("portal-info").Parse(portalInfoTemplate))
+ data := struct {
+ Snap *client.Snap
+ App *client.AppInfo
+ DesktopFile string
+ HasNetworkStatus bool
+ }{
+ Snap: snap,
+ App: app,
+ DesktopFile: desktopFile,
+ HasNetworkStatus: hasNetworkStatus,
+ }
+ if err := t.Execute(Stdout, data); err != nil {
+ return fmt.Errorf("cannot render output template: %s", err)
+ }
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,188 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+// only used for /v2/snaps/hello
+const mockInfoJSONWithApps = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": {
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "title": "hello",
+ "summary": "GNU Hello, the \"hello world\" snap",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "installed-size": 98304,
+ "name": "hello",
+ "publisher": {
+ "id": "canonical",
+ "username": "canonical",
+ "display-name": "Canonical",
+ "validation": "verified"
+ },
+ "developer": "canonical",
+ "status": "active",
+ "type": "app",
+ "version": "2.10",
+ "channel": "stable",
+ "tracking-channel": "stable",
+ "ignore-validation": false,
+ "revision": "38",
+ "confinement": "strict",
+ "private": false,
+ "devmode": false,
+ "jailmode": false,
+ "apps": [
+ {
+ "snap": "hello",
+ "name": "hello",
+ "desktop-file": "/path/to/hello_hello.desktop"
+ },
+ {
+ "snap": "hello",
+ "name": "universe",
+ "desktop-file": "/path/to/hello_universe.desktop"
+ }
+ ],
+ "contact": "mailto:snaps@canonical.com",
+ "mounted-from": "/var/lib/snapd/snaps/hello_38.snap",
+ "install-date": "2019-10-11T13:34:15.630955389+08:00"
+ }
+}
+`
+
+func (s *SnapSuite) TestPortalInfo(c *C) {
+ restore := snap.MockCgroupSnapNameFromPid(func(pid int) (string, error) {
+ c.Check(pid, Equals, 42)
+ return "hello", nil
+ })
+ defer restore()
+ restore = snap.MockApparmorSnapAppFromPid(func(pid int) (string, string, string, error) {
+ c.Check(pid, Equals, 42)
+ return "hello", "universe", "", nil
+ })
+ defer restore()
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/snaps/hello")
+ fmt.Fprintln(w, mockInfoJSONWithApps)
+ case 1:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, url.Values{
+ "snap": []string{"hello"},
+ "interface": []string{"network-status"},
+ })
+ result := client.Connections{
+ Established: []client.Connection{
+ {
+ Slot: client.SlotRef{
+ Snap: "core",
+ Name: "network-status",
+ },
+ Plug: client.PlugRef{
+ Snap: "hello",
+ Name: "network-status",
+ },
+ Interface: "network-status",
+ },
+ },
+ }
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ default:
+ c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r)
+ }
+ n++
+ })
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "portal-info", "42"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, `[Snap Info]
+InstanceName=hello
+AppName=universe
+DesktopFile=hello_universe.desktop
+HasNetworkStatus=true
+`)
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestPortalInfoNoAppInfo(c *C) {
+ restore := snap.MockCgroupSnapNameFromPid(func(pid int) (string, error) {
+ c.Check(pid, Equals, 42)
+ return "hello", nil
+ })
+ defer restore()
+ restore = snap.MockApparmorSnapAppFromPid(func(pid int) (string, string, string, error) {
+ c.Check(pid, Equals, 42)
+ return "", "", "", errors.New("no apparmor")
+ })
+ defer restore()
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/snaps/hello")
+ fmt.Fprintln(w, mockInfoJSONWithApps)
+ case 1:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/connections")
+ c.Check(r.URL.Query(), DeepEquals, url.Values{
+ "snap": []string{"hello"},
+ "interface": []string{"network-status"},
+ })
+ result := client.Connections{}
+ EncodeResponseBody(c, w, map[string]interface{}{
+ "type": "sync",
+ "result": result,
+ })
+ default:
+ c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r)
+ }
+ n++
+ })
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "portal-info", "42"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, `[Snap Info]
+InstanceName=hello
+AppName=hello
+DesktopFile=hello_hello.desktop
+HasNetworkStatus=false
+`)
+ c.Check(s.Stderr(), Equals, "")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run.go 2020-06-05 13:13:49.000000000 +0000
@@ -44,7 +44,7 @@
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/osutil/strace"
- "github.com/snapcore/snapd/selinux"
+ "github.com/snapcore/snapd/sandbox/selinux"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapenv"
"github.com/snapcore/snapd/strutil/shlex"
@@ -112,6 +112,26 @@
}, nil)
}
+// isStopping returns true if the system is shutting down.
+func isStopping() (bool, error) {
+ // Make sure, just in case, that systemd doesn't localize the output string.
+ env, err := osutil.OSEnvironment()
+ if err != nil {
+ return false, err
+ }
+ env["LC_MESSAGES"] = "C"
+ // Check if systemd is stopping (shutting down or rebooting).
+ cmd := exec.Command("systemctl", "is-system-running")
+ cmd.Env = env.ForExec()
+ stdout, err := cmd.Output()
+ // systemctl is-system-running returns non-zero for outcomes other than "running"
+ // As such, ignore any ExitError and just process the stdout buffer.
+ if _, ok := err.(*exec.ExitError); ok {
+ return string(stdout) == "stopping\n", nil
+ }
+ return false, err
+}
+
func maybeWaitForSecurityProfileRegeneration(cli *client.Client) error {
// check if the security profiles key has changed, if so, we need
// to wait for snapd to re-generate all profiles
@@ -125,6 +145,18 @@
logger.Debugf("SystemKeyMismatch returned an error: %v", err)
}
+ // We have a mismatch but maybe it is only because systemd is shutting down
+ // and core or snapd were already unmounted and we failed to re-execute.
+ // For context see: https://bugs.launchpad.net/snapd/+bug/1871652
+ stopping, err := isStopping()
+ if err != nil {
+ logger.Debugf("cannot check if system is stopping: %s", err)
+ }
+ if stopping {
+ logger.Debugf("ignoring system key mismatch during system shutdown/reboot")
+ return nil
+ }
+
// We have a mismatch, try to connect to snapd, once we can
// connect we just continue because that usually means that
// a new snapd is ready and has generated profiles.
@@ -145,11 +177,13 @@
}
}
+ logger.Debugf("system key mismatch detected, waiting for snapd to start responding...")
+
for i := 0; i < timeout; i++ {
if _, err := cli.SysInfo(); err == nil {
return nil
}
- // sleep a litte bit for good measure
+ // sleep a little bit for good measure
time.Sleep(1 * time.Second)
}
@@ -313,6 +347,14 @@
}
func createUserDataDirs(info *snap.Info) error {
+ // Adjust umask so that the created directories have the permissions we
+ // expect and are unaffected by the initial umask. While go runtime creates
+ // threads at will behind the scenes, the setting of umask applies to the
+ // entire process so it doesn't need any special handling to lock the
+ // executing goroutine to a single thread.
+ oldUmask := syscall.Umask(0)
+ defer syscall.Umask(oldUmask)
+
usr, err := userCurrent()
if err != nil {
return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
@@ -448,13 +490,32 @@
var osReadlink = os.Readlink
-func isReexeced() bool {
+// snapdHelperPath return the path of a helper like "snap-confine" or
+// "snap-exec" based on if snapd is re-execed or not
+func snapdHelperPath(toolName string) (string, error) {
exe, err := osReadlink("/proc/self/exe")
if err != nil {
- logger.Noticef("cannot read /proc/self/exe: %v", err)
- return false
+ return "", fmt.Errorf("cannot read /proc/self/exe: %v", err)
}
- return strings.HasPrefix(exe, dirs.SnapMountDir)
+ // no re-exec
+ if !strings.HasPrefix(exe, dirs.SnapMountDir) {
+ return filepath.Join(dirs.DistroLibExecDir, toolName), nil
+ }
+ // The logic below only works if the last two path components
+ // are /usr/bin
+ // FIXME: use a snap warning?
+ if !strings.HasSuffix(exe, "/usr/bin/"+filepath.Base(exe)) {
+ logger.Noticef("(internal error): unexpected exe input in snapdHelperPath: %v", exe)
+ return filepath.Join(dirs.DistroLibExecDir, toolName), nil
+ }
+ // snapBase will be "/snap/{core,snapd}/$rev/" because
+ // the snap binary is always at $root/usr/bin/snap
+ snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", ".."))
+ // Run snap-confine from the core/snapd snap. The tools in
+ // core/snapd snap are statically linked, or mostly
+ // statically, with the exception of libraries such as libudev
+ // and libc.
+ return filepath.Join(snapBase, dirs.CoreLibExecDir, toolName), nil
}
func migrateXauthority(info *snap.Info) (string, error) {
@@ -685,9 +746,9 @@
return nil
}
-func (x *cmdRun) runCmdUnderGdb(origCmd, env []string) error {
- env = append(env, "SNAP_CONFINE_RUN_UNDER_GDB=1")
+type envForExecFunc func(extra map[string]string) []string
+func (x *cmdRun) runCmdUnderGdb(origCmd []string, envForExec envForExecFunc) error {
cmd := []string{"sudo", "-E", "gdb", "-ex=run", "-ex=catch exec", "-ex=continue", "--args"}
cmd = append(cmd, origCmd...)
@@ -695,11 +756,11 @@
gcmd.Stdin = os.Stdin
gcmd.Stdout = os.Stdout
gcmd.Stderr = os.Stderr
- gcmd.Env = env
+ gcmd.Env = envForExec(map[string]string{"SNAP_CONFINE_RUN_UNDER_GDB": "1"})
return gcmd.Run()
}
-func (x *cmdRun) runCmdWithTraceExec(origCmd, env []string) error {
+func (x *cmdRun) runCmdWithTraceExec(origCmd []string, envForExec envForExecFunc) error {
// setup private tmp dir with strace fifo
straceTmp, err := ioutil.TempDir("", "exec-trace")
if err != nil {
@@ -734,7 +795,7 @@
return err
}
// run
- cmd.Env = env
+ cmd.Env = envForExec(nil)
cmd.Stdin = Stdin
cmd.Stdout = Stdout
cmd.Stderr = Stderr
@@ -754,7 +815,7 @@
return err
}
-func (x *cmdRun) runCmdUnderStrace(origCmd, env []string) error {
+func (x *cmdRun) runCmdUnderStrace(origCmd []string, envForExec envForExecFunc) error {
extraStraceOpts, raw, err := x.straceOpts()
if err != nil {
return err
@@ -765,7 +826,7 @@
}
// run with filter
- cmd.Env = env
+ cmd.Env = envForExec(nil)
cmd.Stdin = Stdin
cmd.Stdout = Stdout
stderr, err := cmd.StderrPipe()
@@ -841,24 +902,10 @@
}
func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error {
- snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine")
- // if we re-exec, we must run the snap-confine from the core/snapd snap
- // as well, if they get out of sync, havoc will happen
- if isReexeced() {
- // exe is something like /snap/{snapd,core}/123/usr/bin/snap
- exe, err := osReadlink("/proc/self/exe")
- if err != nil {
- return err
- }
- // snapBase will be "/snap/{core,snapd}/$rev/" because
- // the snap binary is always at $root/usr/bin/snap
- snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", ".."))
- // Run snap-confine from the core/snapd snap. That
- // will work because snap-confine on the core/snapd snap is
- // mostly statically linked (except libudev and libc)
- snapConfine = filepath.Join(snapBase, dirs.CoreLibExecDir, "snap-confine")
+ snapConfine, err := snapdHelperPath("snap-confine")
+ if err != nil {
+ return err
}
-
if !osutil.FileExists(snapConfine) {
if hook != "" {
logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName())
@@ -884,6 +931,11 @@
if info.NeedsClassic() {
cmd = append(cmd, "--classic")
}
+
+ // this should never happen since we validate snaps with "base: none" and do not allow hooks/apps
+ if info.Base == "none" {
+ return fmt.Errorf(`cannot run hooks / applications with base "none"`)
+ }
if info.Base != "" {
cmd = append(cmd, "--base", info.Base)
}
@@ -895,14 +947,9 @@
if info.NeedsClassic() {
// running with classic confinement, carefully pick snap-exec we
// are going to use
- if isReexeced() {
- // same rule as when choosing the location of snap-confine
- snapExecPath = filepath.Join(dirs.SnapMountDir, "core/current",
- dirs.CoreLibExecDir, "snap-exec")
- } else {
- // there is no mount namespace where 'core' is the
- // rootfs, hence we need to use distro's snap-exec
- snapExecPath = filepath.Join(dirs.DistroLibExecDir, "snap-exec")
+ snapExecPath, err = snapdHelperPath("snap-exec")
+ if err != nil {
+ return err
}
}
cmd = append(cmd, snapExecPath)
@@ -925,19 +972,44 @@
cmd = append(cmd, snapApp)
cmd = append(cmd, args...)
- extraEnv := make(map[string]string)
+ env, err := osutil.OSEnvironment()
+ if err != nil {
+ return err
+ }
+ snapenv.ExtendEnvForRun(env, info)
+
if len(xauthPath) > 0 {
- extraEnv["XAUTHORITY"] = xauthPath
+ // Environment is not nil here because it comes from
+ // osutil.OSEnvironment and that guarantees this
+ // property.
+ env["XAUTHORITY"] = xauthPath
+ }
+
+ // on each run variant path this will be used once to get
+ // the environment plus additions in the right form
+ envForExec := func(extra map[string]string) []string {
+ for varName, value := range extra {
+ env[varName] = value
+ }
+ if !info.NeedsClassic() {
+ return env.ForExec()
+ }
+ // For a classic snap, environment variables that are
+ // usually stripped out by ld.so when starting a
+ // setuid process are presevered by being renamed by
+ // prepending PreservedUnsafePrefix -- which snap-exec
+ // will remove, restoring the variables to their
+ // original names.
+ return env.ForExecEscapeUnsafe(snapenv.PreservedUnsafePrefix)
}
- env := snapenv.ExecEnv(info, extraEnv)
if x.TraceExec {
- return x.runCmdWithTraceExec(cmd, env)
+ return x.runCmdWithTraceExec(cmd, envForExec)
} else if x.Gdb {
- return x.runCmdUnderGdb(cmd, env)
+ return x.runCmdUnderGdb(cmd, envForExec)
} else if x.useStrace() {
- return x.runCmdUnderStrace(cmd, env)
+ return x.runCmdUnderStrace(cmd, envForExec)
} else {
- return syscallExec(cmd[0], cmd, env)
+ return syscallExec(cmd[0], cmd, envForExec(nil))
}
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -34,7 +34,7 @@
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/selinux"
+ "github.com/snapcore/snapd/sandbox/selinux"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
@@ -50,7 +50,40 @@
configure:
`)
-func (s *SnapSuite) TestInvalidParameters(c *check.C) {
+var mockYamlBaseNone1 = []byte(`name: snapname1
+version: 1.0
+base: none
+apps:
+ app:
+ command: run-app
+`)
+
+var mockYamlBaseNone2 = []byte(`name: snapname2
+version: 1.0
+base: none
+hooks:
+ configure:
+`)
+
+type RunSuite struct {
+ fakeHome string
+ BaseSnapSuite
+}
+
+var _ = check.Suite(&RunSuite{})
+
+func (s *RunSuite) SetUpTest(c *check.C) {
+ s.BaseSnapSuite.SetUpTest(c)
+ s.fakeHome = c.MkDir()
+
+ u, err := user.Current()
+ c.Assert(err, check.IsNil)
+ s.AddCleanup(snaprun.MockUserCurrent(func() (*user.User, error) {
+ return &user.User{Uid: u.Uid, HomeDir: s.fakeHome}, nil
+ }))
+}
+
+func (s *RunSuite) TestInvalidParameters(c *check.C) {
invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "--", "snap-name"}
_, err := snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*")
@@ -76,7 +109,25 @@
c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*")
}
-func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) {
+func (s *RunSuite) TestRunCmdWithBaseNone(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYamlBaseNone1), &snap.SideInfo{
+ Revision: snap.R("1"),
+ })
+ snaptest.MockSnapCurrent(c, string(mockYamlBaseNone2), &snap.SideInfo{
+ Revision: snap.R("1"),
+ })
+
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname1.app", "--arg1", "arg2"})
+ c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`)
+
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname2"})
+ c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`)
+}
+
+func (s *RunSuite) TestSnapRunWhenMissingConfine(c *check.C) {
_, r := logger.MockLogger()
defer r()
@@ -105,9 +156,16 @@
c.Check(execs, check.IsNil)
}
-func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunAppIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
+ tmpdir := os.Getenv("TMPDIR")
+ if tmpdir == "" {
+ tmpdir = "/var/tmp"
+ os.Setenv("TMPDIR", tmpdir)
+ defer os.Unsetenv("TMPDIR")
+ }
+
// mock installed snap
snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
Revision: snap.R("x2"),
@@ -136,11 +194,19 @@
filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
"snapname.app", "--arg1", "arg2"})
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+ c.Check(execEnv, testutil.Contains, fmt.Sprintf("TMPDIR=%s", tmpdir))
}
-func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunClassicAppIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
+ tmpdir := os.Getenv("TMPDIR")
+ if tmpdir == "" {
+ tmpdir = "/var/tmp"
+ os.Setenv("TMPDIR", tmpdir)
+ defer os.Unsetenv("TMPDIR")
+ }
+
// mock installed snap
snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{
Revision: snap.R("x2"),
@@ -169,10 +235,10 @@
filepath.Join(dirs.DistroLibExecDir, "snap-exec"),
"snapname.app", "--arg1", "arg2"})
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
-
+ c.Check(execEnv, testutil.Contains, fmt.Sprintf("SNAP_SAVED_TMPDIR=%s", tmpdir))
}
-func (s *SnapSuite) TestSnapRunClassicAppIntegrationReexeced(c *check.C) {
+func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromCore(c *check.C) {
mountedCorePath := filepath.Join(dirs.SnapMountDir, "core/current")
mountedCoreLibExecPath := filepath.Join(mountedCorePath, dirs.CoreLibExecDir)
@@ -205,7 +271,40 @@
"snapname.app", "--arg1", "arg2"})
}
-func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromSnapd(c *check.C) {
+ mountedSnapdPath := filepath.Join(dirs.SnapMountDir, "snapd/current")
+ mountedSnapdLibExecPath := filepath.Join(mountedSnapdPath, dirs.CoreLibExecDir)
+
+ defer mockSnapConfine(mountedSnapdLibExecPath)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ restore := snaprun.MockOsReadlink(func(name string) (string, error) {
+ // pretend 'snap' is reexeced from 'core'
+ return filepath.Join(mountedSnapdPath, "usr/bin/snap"), nil
+ })
+ defer restore()
+
+ execArgs := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArgs = args
+ return nil
+ })
+ defer restorer()
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(mountedSnapdLibExecPath, "snap-confine"), "--classic",
+ "snap.snapname.app",
+ filepath.Join(mountedSnapdLibExecPath, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+}
+
+func (s *RunSuite) TestSnapRunAppWithCommandIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -237,48 +336,36 @@
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
}
-func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) {
+func (s *RunSuite) TestSnapRunCreateDataDirs(c *check.C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, check.IsNil)
info.SideInfo.Revision = snap.R(42)
- fakeHome := c.MkDir()
- restorer := snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
err = snaprun.CreateUserDataDirs(info)
c.Assert(err, check.IsNil)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/42")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/common")), check.Equals, true)
}
-func (s *SnapSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) {
+func (s *RunSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) {
info, err := snap.InfoFromSnapYaml(mockYaml)
c.Assert(err, check.IsNil)
info.SideInfo.Revision = snap.R(42)
info.InstanceKey = "foo"
- fakeHome := c.MkDir()
- restorer := snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
err = snaprun.CreateUserDataDirs(info)
c.Assert(err, check.IsNil)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/42")), check.Equals, true)
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/common")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/42")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/common")), check.Equals, true)
// mount point for snap instance mapping has been created
- c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname")), check.Equals, true)
// and it's empty inside
- m, err := filepath.Glob(filepath.Join(fakeHome, "/snap/snapname/*"))
+ m, err := filepath.Glob(filepath.Join(s.fakeHome, "/snap/snapname/*"))
c.Assert(err, check.IsNil)
c.Assert(m, check.HasLen, 0)
}
-func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -310,7 +397,7 @@
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
}
-func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -342,7 +429,7 @@
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
}
-func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -378,7 +465,7 @@
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41")
}
-func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) {
// Only create revision 42
snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
Revision: snap.R(42),
@@ -396,13 +483,13 @@
c.Check(err, check.ErrorMatches, "cannot find .*")
}
-func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "--", "snapname"})
c.Assert(err, check.NotNil)
c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"")
}
-func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunHookMissingHookIntegration(c *check.C) {
// Only create revision 42
snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
Revision: snap.R(42),
@@ -421,22 +508,22 @@
c.Check(called, check.Equals, false)
}
-func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) {
+func (s *RunSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--unknown", "--", "snapname.app", "--arg1", "arg2"})
c.Assert(err, check.ErrorMatches, "unknown flag `unknown'")
}
-func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) {
+func (s *RunSuite) TestSnapRunErorsForMissingApp(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=shell"})
c.Assert(err, check.ErrorMatches, "need the application to run as argument")
}
-func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) {
+func (s *RunSuite) TestSnapRunErorrForUnavailableApp(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "not-there"})
c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir))
}
-func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) {
+func (s *RunSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -471,26 +558,39 @@
c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES")
}
-func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) {
+func (s *RunSuite) TestSnapRunSnapdHelperPath(c *check.C) {
var osReadlinkResult string
restore := snaprun.MockOsReadlink(func(name string) (string, error) {
return osReadlinkResult, nil
})
defer restore()
+ tool := "snap-confine"
for _, t := range []struct {
readlink string
- expected bool
+ expected string
}{
- {filepath.Join(dirs.SnapMountDir, dirs.CoreLibExecDir, "snapd"), true},
- {filepath.Join(dirs.DistroLibExecDir, "snapd"), false},
+ {
+ filepath.Join(dirs.SnapMountDir, "core/current/usr/bin/snap"),
+ filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, tool),
+ },
+ {
+ filepath.Join(dirs.SnapMountDir, "snapd/current/usr/bin/snap"),
+ filepath.Join(dirs.SnapMountDir, "snapd/current", dirs.CoreLibExecDir, tool),
+ },
+ {
+ filepath.Join("/usr/bin/snap"),
+ filepath.Join(dirs.DistroLibExecDir, tool),
+ },
} {
osReadlinkResult = t.readlink
- c.Check(snaprun.IsReexeced(), check.Equals, t.expected)
+ toolPath, err := snaprun.SnapdHelperPath(tool)
+ c.Assert(err, check.IsNil)
+ c.Check(toolPath, check.Equals, t.expected)
}
}
-func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) {
+func (s *RunSuite) TestSnapRunAppIntegrationFromCore(c *check.C) {
defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))()
// mock installed snap
@@ -529,7 +629,7 @@
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
}
-func (s *SnapSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) {
+func (s *RunSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) {
defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "snapd", "222", dirs.CoreLibExecDir))()
// mock installed snap
@@ -568,7 +668,7 @@
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
}
-func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) {
+func (s *RunSuite) TestSnapRunXauthorityMigration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
u, err := user.Current()
@@ -644,7 +744,7 @@
return out
}
-func (s *SnapSuite) TestAntialiasHappy(c *check.C) {
+func (s *RunSuite) TestAntialiasHappy(c *check.C) {
c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil)
inArgs := mkCompArgs("10", "alias", "alias", "bo-alias")
@@ -672,7 +772,7 @@
})
}
-func (s *SnapSuite) TestAntialiasBailsIfUnhappy(c *check.C) {
+func (s *RunSuite) TestAntialiasBailsIfUnhappy(c *check.C) {
// alias exists but args are somehow wonky
c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil)
c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil)
@@ -701,7 +801,7 @@
}
}
-func (s *SnapSuite) TestSnapRunAppWithStraceIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunAppWithStraceIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -781,7 +881,7 @@
c.Check(s.Stderr(), check.Equals, fmt.Sprintf(expectedFullFmt, dirs.SnapMountDir))
}
-func (s *SnapSuite) TestSnapRunAppWithStraceOptions(c *check.C) {
+func (s *RunSuite) TestSnapRunAppWithStraceOptions(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -822,7 +922,7 @@
})
}
-func (s *SnapSuite) TestSnapRunShellIntegration(c *check.C) {
+func (s *RunSuite) TestSnapRunShellIntegration(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -855,7 +955,7 @@
c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
}
-func (s *SnapSuite) TestSnapRunAppTimer(c *check.C) {
+func (s *RunSuite) TestSnapRunAppTimer(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -910,7 +1010,7 @@
"snapname.app", "--arg1", "arg2"})
}
-func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) {
+func (s *RunSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) {
defer mockSnapConfine(dirs.DistroLibExecDir)()
// mock installed snap
@@ -933,7 +1033,7 @@
c.Check(s.Stderr(), check.Equals, "")
}
-func (s *SnapSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) {
+func (s *RunSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) {
logbuf, restorer := logger.MockLogger()
defer restorer()
@@ -944,12 +1044,6 @@
Revision: snap.R("x2"),
})
- fakeHome := c.MkDir()
- restorer = snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
// redirect exec
execCalled := 0
restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error {
@@ -964,7 +1058,7 @@
enabled := false
verify := true
- snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir)
+ snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)
restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) {
c.Check(what, check.Equals, snapUserDir)
@@ -1020,7 +1114,7 @@
c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir))
}
-func (s *SnapSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) {
+func (s *RunSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) {
logbuf, restorer := logger.MockLogger()
defer restorer()
@@ -1031,12 +1125,6 @@
Revision: snap.R("x2"),
})
- fakeHome := c.MkDir()
- restorer = snaprun.MockUserCurrent(func() (*user.User, error) {
- return &user.User{HomeDir: fakeHome}, nil
- })
- defer restorer()
-
// redirect exec
execCalled := 0
restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error {
@@ -1052,7 +1140,7 @@
verifyErr := errors.New("verify failed")
restoreErr := errors.New("restore failed")
- snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir)
+ snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)
restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) {
c.Check(what, check.Equals, snapUserDir)
@@ -1109,3 +1197,63 @@
c.Check(verifyCalls, check.Equals, 2)
c.Check(restoreCalls, check.Equals, 1)
}
+
+// systemctl is-system-running returns "running" in normal situations.
+func (s *RunSuite) TestIsStoppingRunning(c *check.C) {
+ systemctl := testutil.MockCommand(c, "systemctl", `
+case "$1" in
+ is-system-running)
+ echo "running"
+ exit 0
+ ;;
+esac
+`)
+ defer systemctl.Restore()
+ stop, err := snaprun.IsStopping()
+ c.Check(err, check.IsNil)
+ c.Check(stop, check.Equals, false)
+ c.Check(systemctl.Calls(), check.DeepEquals, [][]string{
+ {"systemctl", "is-system-running"},
+ })
+}
+
+// systemctl is-system-running returns "stopping" when the system is
+// shutting down or rebooting. At the same time it returns a non-zero
+// exit status.
+func (s *RunSuite) TestIsStoppingStopping(c *check.C) {
+ systemctl := testutil.MockCommand(c, "systemctl", `
+case "$1" in
+ is-system-running)
+ echo "stopping"
+ exit 1
+ ;;
+esac
+`)
+ defer systemctl.Restore()
+ stop, err := snaprun.IsStopping()
+ c.Check(err, check.IsNil)
+ c.Check(stop, check.Equals, true)
+ c.Check(systemctl.Calls(), check.DeepEquals, [][]string{
+ {"systemctl", "is-system-running"},
+ })
+}
+
+// systemctl is-system-running can often return "degraded"
+// Let's make sure that is not confusing us.
+func (s *RunSuite) TestIsStoppingDegraded(c *check.C) {
+ systemctl := testutil.MockCommand(c, "systemctl", `
+case "$1" in
+ is-system-running)
+ echo "degraded"
+ exit 1
+ ;;
+esac
+`)
+ defer systemctl.Restore()
+ stop, err := snaprun.IsStopping()
+ c.Check(err, check.IsNil)
+ c.Check(stop, check.Equals, false)
+ c.Check(systemctl.Calls(), check.DeepEquals, [][]string{
+ {"systemctl", "is-system-running"},
+ })
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_services_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_services_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_services_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_services_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -23,6 +23,8 @@
"encoding/json"
"fmt"
"net/http"
+ "sort"
+ "strings"
"time"
"gopkg.in/check.v1"
@@ -33,8 +35,6 @@
type appOpSuite struct {
BaseSnapSuite
-
- restoreAll func()
}
var _ = check.Suite(&appOpSuite{})
@@ -42,16 +42,13 @@
func (s *appOpSuite) SetUpTest(c *check.C) {
s.BaseSnapSuite.SetUpTest(c)
- restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond)
+ restoreClientRetry := client.MockDoTimings(time.Millisecond, 100*time.Millisecond)
restorePollTime := snap.MockPollTime(time.Millisecond)
- s.restoreAll = func() {
- restoreClientRetry()
- restorePollTime()
- }
+ s.AddCleanup(restoreClientRetry)
+ s.AddCleanup(restorePollTime)
}
func (s *appOpSuite) TearDownTest(c *check.C) {
- s.restoreAll()
s.BaseSnapSuite.TearDownTest(c)
}
@@ -222,6 +219,51 @@
c.Check(n, check.Equals, 1)
}
+func (s *appOpSuite) TestServiceCompletion(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.URL.Path, check.Equals, "/v2/apps")
+ c.Check(r.URL.Query(), check.HasLen, 1)
+ c.Check(r.URL.Query().Get("select"), check.Equals, "service")
+ c.Check(r.Method, check.Equals, "GET")
+ w.WriteHeader(200)
+ enc := json.NewEncoder(w)
+ enc.Encode(map[string]interface{}{
+ "type": "sync",
+ "result": []map[string]interface{}{
+ {"snap": "a-snap", "name": "foo", "daemon": "simple"},
+ {"snap": "a-snap", "name": "bar", "daemon": "simple"},
+ {"snap": "b-snap", "name": "baz", "daemon": "simple"},
+ },
+ "status": "OK",
+ "status-code": 200,
+ })
+
+ n++
+ })
+
+ var comp = func(s string) string {
+ comps := snap.ServiceName("").Complete(s)
+ as := make([]string, len(comps))
+ for i := range comps {
+ as[i] = comps[i].Item
+ }
+ sort.Strings(as)
+ return strings.Join(as, " ")
+ }
+
+ c.Check(comp(""), check.Equals, "a-snap a-snap.bar a-snap.foo b-snap.baz")
+ c.Check(comp("a"), check.Equals, "a-snap a-snap.bar a-snap.foo")
+ c.Check(comp("a-snap"), check.Equals, "a-snap a-snap.bar a-snap.foo")
+ c.Check(comp("a-snap."), check.Equals, "a-snap.bar a-snap.foo")
+ c.Check(comp("a-snap.b"), check.Equals, "a-snap.bar")
+ c.Check(comp("b"), check.Equals, "b-snap.baz")
+ c.Check(comp("c"), check.Equals, "")
+
+ // ensure that the fake server api was actually hit
+ c.Check(n, check.Equals, 7)
+}
+
func (s *appOpSuite) TestAppStatusNoServices(c *check.C) {
n := 0
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_set.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_set.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set.go 2020-06-05 13:13:49.000000000 +0000
@@ -40,7 +40,10 @@
Nested values may be modified via a dotted path:
- $ snap set author.name=frank
+ $ snap set snap-name author.name=frank
+
+Configuration option may be unset with exclamation mark:
+ $ snap set snap-name author!
`)
type cmdSet struct {
@@ -61,7 +64,7 @@
// TRANSLATORS: This needs to begin with < and end with >
name: i18n.G(""),
// TRANSLATORS: This should not start with a lowercase letter.
- desc: i18n.G("Configuration value (key=value)"),
+ desc: i18n.G("Set (key=value) or unset (key!) configuration value"),
},
})
}
@@ -70,6 +73,10 @@
patchValues := make(map[string]interface{})
for _, patchValue := range x.Positional.ConfValues {
parts := strings.SplitN(patchValue, "=", 2)
+ if len(parts) == 1 && strings.HasSuffix(patchValue, "!") {
+ patchValues[strings.TrimSuffix(patchValue, "!")] = nil
+ continue
+ }
if len(parts) != 2 {
return fmt.Errorf(i18n.G("invalid configuration: %q (want key=value)"), patchValue)
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_set_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_set_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -27,78 +27,89 @@
"gopkg.in/check.v1"
snapset "github.com/snapcore/snapd/cmd/snap"
- "github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/snap/snaptest"
)
-var validApplyYaml = []byte(`name: snapname
-version: 1.0
-hooks:
- configure:
-`)
+type snapSetSuite struct {
+ BaseSnapSuite
-func (s *SnapSuite) TestInvalidSetParameters(c *check.C) {
+ setConfApiCalls int
+}
+
+var _ = check.Suite(&snapSetSuite{})
+
+func (s *snapSetSuite) SetUpTest(c *check.C) {
+ s.BaseSnapSuite.SetUpTest(c)
+ s.setConfApiCalls = 0
+}
+
+func (s *snapSetSuite) TestInvalidSetParameters(c *check.C) {
invalidParameters := []string{"set", "snap-name", "key", "value"}
_, err := snapset.Parser(snapset.Client()).ParseArgs(invalidParameters)
c.Check(err, check.ErrorMatches, ".*invalid configuration:.*(want key=value).*")
+ c.Check(s.setConfApiCalls, check.Equals, 0)
}
-func (s *SnapSuite) TestSnapSetIntegrationString(c *check.C) {
- // mock installed snap
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationString(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, "value")
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=value"})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) TestSnapSetIntegrationNumber(c *check.C) {
- // mock installed snap
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationNumber(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, json.Number("1.2"))
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1.2"})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) TestSnapSetIntegrationBigInt(c *check.C) {
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationBigInt(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, json.Number("1234567890"))
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1234567890"})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) TestSnapSetIntegrationJson(c *check.C) {
- // mock installed snap
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationJson(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, map[string]interface{}{"subkey": "value"})
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", `key={"subkey":"value"}`})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
+}
+
+func (s *snapSetSuite) TestSnapSetIntegrationUnsetWithExclamationMark(c *check.C) {
+ // and mock the server
+ s.mockSetConfigServer(c, nil)
+
+ // Unset config value via exclamation mark
+ _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key!"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
+}
+
+func (s *snapSetSuite) TestSnapSetIntegrationStringWithExclamationMark(c *check.C) {
+ // and mock the server
+ s.mockSetConfigServer(c, "value!")
+
+ // Set a config value ending with exclamation mark
+ _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=value!"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) mockSetConfigServer(c *check.C, expectedValue interface{}) {
+func (s *snapSetSuite) mockSetConfigServer(c *check.C, expectedValue interface{}) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v2/snaps/snapname/conf":
@@ -106,7 +117,9 @@
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
"key": expectedValue,
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ s.setConfApiCalls += 1
case "/v2/changes/zzz":
c.Check(r.Method, check.Equals, "GET")
fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_sign.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_sign.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_sign.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_sign.go 2020-06-05 13:13:49.000000000 +0000
@@ -38,6 +38,10 @@
`)
type cmdSign struct {
+ Positional struct {
+ Filename flags.Filename
+ } `positional-args:"yes"`
+
KeyName keyName `short:"k" default:"default"`
}
@@ -47,8 +51,14 @@
}, map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"k": i18n.G("Name of the key to use, otherwise use the default key"),
- }, nil)
+ }, []argDesc{{
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("File to sign (defaults to stdin)"),
+ }})
cmd.hidden = true
+ cmd.completeHidden = true
}
func (x *cmdSign) Execute(args []string) error {
@@ -56,7 +66,17 @@
return ErrExtraArgs
}
- statement, err := ioutil.ReadAll(Stdin)
+ useStdin := x.Positional.Filename == "" || x.Positional.Filename == "-"
+
+ var (
+ statement []byte
+ err error
+ )
+ if !useStdin {
+ statement, err = ioutil.ReadFile(string(x.Positional.Filename))
+ } else {
+ statement, err = ioutil.ReadAll(Stdin)
+ }
if err != nil {
return fmt.Errorf(i18n.G("cannot read assertion input: %v"), err)
}
@@ -64,7 +84,8 @@
keypairMgr := asserts.NewGPGKeypairManager()
privKey, err := keypairMgr.GetByName(string(x.KeyName))
if err != nil {
- return err
+ // TRANSLATORS: %q is the key name, %v the error message
+ return fmt.Errorf(i18n.G("cannot use %q key: %v"), x.KeyName, err)
}
signOpts := signtool.Options{
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op.go 2020-06-05 13:13:49.000000000 +0000
@@ -32,8 +32,11 @@
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap/channel"
+ "github.com/snapcore/snapd/strutil"
)
var (
@@ -111,6 +114,7 @@
waitMixin
Revision string `long:"revision"`
+ Purge bool `long:"purge"`
Positional struct {
Snaps []installedSnapName `positional-arg-name:"" required:"1"`
} `positional-args:"yes" required:"yes"`
@@ -182,7 +186,7 @@
}
func (x *cmdRemove) Execute([]string) error {
- opts := &client.SnapOptions{Revision: x.Revision}
+ opts := &client.SnapOptions{Revision: x.Revision, Purge: x.Purge}
if len(x.Positional.Snaps) == 1 {
return x.removeOne(opts)
}
@@ -248,15 +252,62 @@
mx.Channel = ch.chName
}
- if !strings.Contains(mx.Channel, "/") && mx.Channel != "" && mx.Channel != "edge" && mx.Channel != "beta" && mx.Channel != "candidate" && mx.Channel != "stable" {
- // shortcut to jump to a different track, e.g.
- // snap install foo --channel=3.4 # implies 3.4/stable
- mx.Channel += "/stable"
+ if mx.Channel != "" {
+ if _, err := channel.Parse(mx.Channel, ""); err != nil {
+ full, er := channel.Full(mx.Channel)
+ if er != nil {
+ // the parse error has more detailed info
+ return err
+ }
+
+ // TODO: get escapes in here so we can bold the Warning
+ head := i18n.G("Warning:")
+ msg := i18n.G("Specifying a channel %q is relying on undefined behaviour. Interpreting it as %q for now, but this will be an error later.\n")
+ warn := fill(fmt.Sprintf(msg, mx.Channel, full), utf8.RuneCountInString(head)+1) // +1 for the space
+ fmt.Fprint(Stderr, head, " ", warn, "\n\n")
+ mx.Channel = full // so a malformed-but-eh channel will always be full, i.e. //stable// -> latest/stable
+ }
}
return nil
}
+// isSnapInPath checks whether the snap binaries dir (e.g. /snap/bin)
+// is in $PATH.
+//
+// TODO: consider symlinks
+func isSnapInPath() bool {
+ paths := filepath.SplitList(os.Getenv("PATH"))
+ for _, path := range paths {
+ if filepath.Clean(path) == dirs.SnapBinariesDir {
+ return true
+ }
+ }
+ return false
+}
+
+func isSameRisk(tracking, current string) (bool, error) {
+ if tracking == current {
+ return true, nil
+ }
+ var trackingRisk, currentRisk string
+ if tracking != "" {
+ traCh, err := channel.Parse(tracking, "")
+ if err != nil {
+ return false, err
+ }
+ trackingRisk = traCh.Risk
+ }
+ if current != "" {
+ curCh, err := channel.Parse(current, "")
+ if err != nil {
+ return false, err
+ }
+ currentRisk = curCh.Risk
+ }
+ return trackingRisk == currentRisk, nil
+}
+
// show what has been done
func showDone(cli *client.Client, names []string, op string, opts *client.SnapOptions, esc *escapes) error {
snaps, err := cli.List(names, nil)
@@ -264,19 +315,33 @@
return err
}
+ needsPathWarning := !isSnapInPath()
for _, snap := range snaps {
channelStr := ""
- if snap.Channel != "" && snap.Channel != "stable" {
- channelStr = fmt.Sprintf(" (%s)", snap.Channel)
+ if snap.Channel != "" {
+ ch, err := channel.Parse(snap.Channel, "")
+ if err != nil {
+ return err
+ }
+ if ch.Name != "stable" {
+ channelStr = fmt.Sprintf(" (%s)", ch.Name)
+ }
}
switch op {
case "install":
+ if needsPathWarning {
+ head := i18n.G("Warning:")
+ warn := fill(fmt.Sprintf(i18n.G("%s was not found in your $PATH. If you've not restarted your session since you installed snapd, try doing that. Please see https://forum.snapcraft.io/t/9469 for more details."), dirs.SnapBinariesDir), utf8.RuneCountInString(head)+1) // +1 for the space
+ fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n")
+ needsPathWarning = false
+ }
+
if opts != nil && opts.Classic && snap.Confinement != client.ClassicConfinement {
// requested classic but the snap is not classic
head := i18n.G("Warning:")
// TRANSLATORS: the arg is a snap name (e.g. "some-snap")
warn := fill(fmt.Sprintf(i18n.G("flag --classic ignored for strictly confined snap %s"), snap.Name), utf8.RuneCountInString(head)+1) // +1 for the space
- fmt.Fprint(Stderr, head, " ", warn, "\n\n")
+ fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n")
}
if snap.Publisher != nil {
@@ -297,12 +362,40 @@
case "revert":
// TRANSLATORS: first %s is a snap name, second %s is a revision
fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), snap.Name, snap.Version)
+ case "switch":
+ switchCohort := opts.CohortKey != ""
+ switchChannel := opts.Channel != ""
+ var msg string
+ // we have three boolean things to check, meaning 2³=8 possibilities,
+ // minus 3 error cases which are handled before the call to showDone.
+ switch {
+ case switchCohort && !opts.LeaveCohort && !switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second an ellipted cohort string
+ msg = fmt.Sprintf(i18n.G("%q switched to the %q cohort\n"), snap.Name, strutil.ElliptLeft(opts.CohortKey, 10))
+ case switchCohort && !opts.LeaveCohort && switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel, the third an ellipted cohort string
+ msg = fmt.Sprintf(i18n.G("%q switched to the %q channel and the %q cohort\n"), snap.Name, snap.TrackingChannel, strutil.ElliptLeft(opts.CohortKey, 10))
+ case !switchCohort && !opts.LeaveCohort && switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
+ msg = fmt.Sprintf(i18n.G("%q switched to the %q channel\n"), snap.Name, snap.TrackingChannel)
+ case !switchCohort && opts.LeaveCohort && switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
+ msg = fmt.Sprintf(i18n.G("%q left the cohort, and switched to the %q channel"), snap.Name, snap.TrackingChannel)
+ case !switchCohort && opts.LeaveCohort && !switchChannel:
+ // TRANSLATORS: %q will be the (quoted) snap name
+ msg = fmt.Sprintf(i18n.G("%q left the cohort"), snap.Name)
+ }
+ fmt.Fprintln(Stdout, msg)
default:
fmt.Fprintf(Stdout, "internal error: unknown op %q", op)
}
- if snap.TrackingChannel != snap.Channel && snap.Channel != "" {
- // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again.
- fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel)
+ if op == "install" || op == "refresh" {
+ if snap.TrackingChannel != snap.Channel && snap.Channel != "" {
+ if sameRisk, err := isSameRisk(snap.TrackingChannel, snap.Channel); err == nil && !sameRisk {
+ // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again.
+ fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel)
+ }
+ }
}
}
@@ -364,6 +457,7 @@
Name string `long:"name"`
+ Cohort string `long:"cohort"`
Positional struct {
Snaps []remoteSnapName `positional-arg-name:""`
} `positional-args:"yes" required:"yes"`
@@ -409,6 +503,7 @@
}
}
+ // TODO: mention details of the install (e.g. like switch does)
return showDone(x.client, []string{snapName}, "install", opts, x.getEscapes())
}
@@ -483,6 +578,7 @@
Revision: x.Revision,
Dangerous: dangerous,
Unaliased: x.Unaliased,
+ CohortKey: x.Cohort,
}
x.setModes(opts)
@@ -519,6 +615,8 @@
Amend bool `long:"amend"`
Revision string `long:"revision"`
+ Cohort string `long:"cohort"`
+ LeaveCohort bool `long:"leave-cohort"`
List bool `long:"list"`
Time bool `long:"time"`
IgnoreValidation bool `long:"ignore-validation"`
@@ -573,6 +671,8 @@
return err
}
+ // TODO: this doesn't really tell about all the things you
+ // could set while refreshing (something switch does)
return showDone(x.client, []string{name}, "refresh", opts, x.getEscapes())
}
@@ -686,6 +786,8 @@
Channel: x.Channel,
IgnoreValidation: x.IgnoreValidation,
Revision: x.Revision,
+ CohortKey: x.Cohort,
+ LeaveCohort: x.LeaveCohort,
}
x.setModes(opts)
return x.refreshOne(names[0], opts)
@@ -904,6 +1006,9 @@
waitMixin
channelMixin
+ Cohort string `long:"cohort"`
+ LeaveCohort bool `long:"leave-cohort"`
+
Positional struct {
Snap installedSnapName `positional-arg-name:"" required:"1"`
} `positional-args:"yes" required:"yes"`
@@ -913,14 +1018,28 @@
if err := x.setChannelFromCommandline(); err != nil {
return err
}
- if x.Channel == "" {
- return fmt.Errorf("missing --channel= parameter")
- }
name := string(x.Positional.Snap)
channel := string(x.Channel)
+
+ switchCohort := x.Cohort != ""
+ switchChannel := x.Channel != ""
+
+ // we have three boolean things to check, meaning 2³=8 possibilities
+ // of which 3 are errors (which is why we look at the errors first).
+ // the 5 valid cases are handled by showDone.
+ if switchCohort && x.LeaveCohort {
+ // this one counts as two (no channel filter)
+ return fmt.Errorf(i18n.G("cannot specify both --cohort and --leave-cohort"))
+ }
+ if !switchCohort && !x.LeaveCohort && !switchChannel {
+ return fmt.Errorf(i18n.G("nothing to switch; specify --channel (and/or one of --cohort/--leave-cohort)"))
+ }
+
opts := &client.SnapOptions{
- Channel: channel,
+ Channel: channel,
+ CohortKey: x.Cohort,
+ LeaveCohort: x.LeaveCohort,
}
changeID, err := x.client.Switch(name, opts)
if err != nil {
@@ -934,8 +1053,7 @@
return err
}
- fmt.Fprintf(Stdout, i18n.G("%q switched to the %q channel\n"), name, channel)
- return nil
+ return showDone(x.client, []string{name}, "switch", opts, nil)
}
func init() {
@@ -943,6 +1061,8 @@
waitDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"revision": i18n.G("Remove only the given revision"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "purge": i18n.G("Remove the snap without saving a snapshot of its data"),
}), nil)
addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} },
colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(map[string]string{
@@ -956,6 +1076,8 @@
"unaliased": i18n.G("Install the given snap without enabling its automatic aliases"),
// TRANSLATORS: This should not start with a lowercase letter.
"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"),
}), nil)
addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} },
colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{
@@ -969,6 +1091,10 @@
"time": i18n.G("Show auto refresh information but do not perform a refresh"),
// 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.
+ "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"),
}), nil)
addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil)
addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil)
@@ -977,5 +1103,10 @@
// TRANSLATORS: This should not start with a lowercase letter.
"revision": i18n.G("Revert to the given revision"),
}), nil)
- addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs), 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.
+ "cohort": i18n.G("Switch the snap into the given cohort"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "leave-cohort": i18n.G("Switch the snap out of its cohort"),
+ }), nil)
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -44,13 +44,14 @@
type snapOpTestServer struct {
c *check.C
- checker func(r *http.Request)
- n int
- total int
- channel string
- confinement string
- rebooting bool
- snap string
+ checker func(r *http.Request)
+ n int
+ total int
+ channel string
+ trackingChannel string
+ confinement string
+ rebooting bool
+ snap string
}
var _ = check.Suite(&SnapOpSuite{})
@@ -81,7 +82,7 @@
case 3:
t.c.Check(r.Method, check.Equals, "GET")
t.c.Check(r.URL.Path, check.Equals, "/v2/snaps")
- fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.confinement)
+ fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "tracking-channel": "%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.trackingChannel, t.confinement)
default:
t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1)
}
@@ -99,7 +100,7 @@
func (s *SnapOpSuite) SetUpTest(c *check.C) {
s.BaseSnapSuite.SetUpTest(c)
- restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond)
+ restoreClientRetry := client.MockDoTimings(time.Millisecond, 100*time.Millisecond)
restorePollTime := snap.MockPollTime(time.Millisecond)
s.restoreAll = func() {
restoreClientRetry()
@@ -139,8 +140,6 @@
func (s *SnapOpSuite) TestWaitRecovers(c *check.C) {
meter := &progresstest.Meter{}
defer progress.MockMeter(meter)()
- restore := snap.MockMaxGoneTime(time.Millisecond)
- defer restore()
nah := true
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
@@ -154,7 +153,7 @@
cli := snap.Client()
chg, err := snap.Wait(cli, "x")
// we got the change
- c.Assert(chg, check.NotNil)
+ c.Check(chg, check.NotNil)
c.Assert(err, check.IsNil)
// but only after recovering
@@ -190,6 +189,29 @@
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",
+ "channel": "candidate",
+ "cohort-key": "what",
+ })
+ s.srv.channel = "candidate"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "--cohort", "what", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 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")
+ 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",
"channel": "candidate",
})
@@ -201,7 +223,7 @@
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from Bar installed`)
- c.Check(s.Stderr(), check.Equals, "")
+ c.Check(s.Stderr(), testutil.MatchesWrapped, `Warning: \S+/bin was not found in your \$PATH.*`)
// ensure that the fake server api was actually hit
c.Check(s.srv.n, check.Equals, s.srv.total)
}
@@ -211,7 +233,7 @@
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
"action": "install",
- "channel": "3.4/stable",
+ "channel": "3.4",
})
s.srv.channel = "3.4/stable"
}
@@ -232,16 +254,81 @@
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
"action": "install",
- "channel": "3.4/hotfix-1",
+ "channel": "3.4/stable/hotfix-1",
})
- s.srv.channel = "3.4/hotfix-1"
+ s.srv.channel = "3.4/stable/hotfix-1"
}
s.RedirectClientToTestServer(s.srv.handle)
- rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"})
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/stable/hotfix-1", "foo"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 1.0 from Bar installed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable/hotfix-1\) 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) TestInstallSameRiskInTrack(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",
+ "channel": "latest/stable",
+ })
+ s.srv.channel = "stable"
+ s.srv.trackingChannel = "latest/stable"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "latest/stable", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "foo 1.0 from Bar installed\n")
+ 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) TestInstallSameRiskInDefaultTrack(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",
+ "channel": "stable",
+ })
+ s.srv.channel = "18/stable"
+ s.srv.trackingChannel = "18/stable"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--stable", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, "foo (18/stable) 1.0 from Bar installed\n")
+ 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) TestInstallRiskChannelClosed(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",
+ "channel": "edge",
+ })
+ s.srv.channel = "stable"
+ s.srv.trackingChannel = "edge"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "edge", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `foo 1.0 from Bar installed
+Channel edge for foo is closed; temporarily forwarding to stable.
+`)
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)
@@ -362,11 +449,7 @@
})
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=mytrack", "foo"})
- c.Assert(err, check.NotNil)
- c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, `
-error: snap "foo" not available on channel "mytrack/stable" (see 'snap info
- foo')
-`)
+ c.Check(err, check.ErrorMatches, `snap "foo" not available on channel "mytrack" \(see 'snap info foo'\)`)
c.Check(s.Stdout(), check.Equals, "")
c.Check(s.Stderr(), check.Equals, "")
@@ -573,21 +656,11 @@
func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableInvalidChannel(c *check.C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": {
- "snap-name": "foo",
- "action": "install",
- "architecture": "amd64",
- "channel": "a/b/c/d",
- "releases": [{"architecture": "amd64", "channel": "stable"}]
-}, "kind": "snap-channel-not-available"}, "status-code": 404}`)
+ c.Fatal("unexpected call to server")
})
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=a/b/c/d", "foo"})
- c.Assert(err, check.NotNil)
- c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, `
-error: requested channel "a/b/c/d" is not valid (see 'snap info foo' for valid
- ones)
-`)
+ c.Assert(err, check.ErrorMatches, "channel name has too many components: a/b/c/d")
c.Check(s.Stdout(), check.Equals, "")
c.Check(s.Stderr(), check.Equals, "")
@@ -854,7 +927,6 @@
c.Assert(rest, check.DeepEquals, []string{})
// tracking channel is "" in the test server
c.Check(s.Stdout(), check.Equals, `foo reverted to 1.0
-Channel for foo is closed; temporarily forwarding to potato.
`)
c.Check(s.Stderr(), check.Equals, "")
// ensure that the fake server api was actually hit
@@ -1090,6 +1162,53 @@
c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar refreshed`)
}
+func (s *SnapOpSuite) TestRefreshOneSwitchCohort(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "cohort-key": "what",
+ })
+ }
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--cohort=what", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`)
+}
+
+func (s *SnapOpSuite) TestRefreshOneLeaveCohort(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "leave-cohort": true,
+ })
+ }
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--leave-cohort", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`)
+}
+
+func (s *SnapOpSuite) TestRefreshOneWithPinnedTrack(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "channel": "stable",
+ })
+ s.srv.channel = "18/stable"
+ s.srv.trackingChannel = "18/stable"
+ }
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--stable", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Equals, "foo (18/stable) 1.0 from Bar refreshed\n")
+}
+
func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) {
s.RedirectClientToTestServer(s.srv.handle)
s.srv.checker = func(r *http.Request) {
@@ -1166,6 +1285,25 @@
}
+func (s *SnapOpSuite) TestRefreshOneChanDeprecated(c *check.C) {
+ var in, out string
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "refresh", "channel": out})
+ fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`)
+ })
+
+ for in, out = range map[string]string{
+ "/foo": "foo/stable",
+ "/stable": "latest/stable",
+ "///foo/stable//": "foo/stable",
+ } {
+ s.stderr.Reset()
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--channel=" + in, "one"})
+ c.Assert(err, check.ErrorMatches, "snap \"one\" not found")
+ c.Check(s.Stderr(), testutil.EqualsWrapped, `Warning: Specifying a channel "`+in+`" is relying on undefined behaviour. Interpreting it as "`+out+`" for now, but this will be an error later.`)
+ }
+}
+
func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) {
s.RedirectClientToTestServer(nil)
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"})
@@ -1455,6 +1593,26 @@
c.Check(s.srv.n, check.Equals, s.srv.total)
}
+func (s *SnapOpSuite) TestRemoveWithPurge(c *check.C) {
+ s.srv.total = 3
+ 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": "remove",
+ "purge": true,
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--purge", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo removed`)
+ 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) TestRemoveRevision(c *check.C) {
s.srv.total = 3
s.srv.checker = func(r *http.Request) {
@@ -1720,20 +1878,107 @@
}
func (s *SnapOpSuite) TestSwitchHappy(c *check.C) {
- s.srv.total = 3
+ s.srv.total = 4
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": "switch",
"channel": "beta",
})
+ s.srv.trackingChannel = "beta"
}
s.RedirectClientToTestServer(s.srv.handle)
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--beta", "foo"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "beta" channel`)
+ c.Check(s.Stdout(), check.Equals, `"foo" switched to the "beta" channel
+
+`)
+ 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) TestSwitchHappyCohort(c *check.C) {
+ s.srv.total = 4
+ 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": "switch",
+ "cohort-key": "what",
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--cohort=what", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "what" cohort`)
+ 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) TestSwitchHappyLeaveCohort(c *check.C) {
+ s.srv.total = 4
+ 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": "switch",
+ "leave-cohort": true,
+ })
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--leave-cohort", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" left the cohort`)
+ 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) TestSwitchHappyChannelAndCohort(c *check.C) {
+ s.srv.total = 4
+ 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": "switch",
+ "cohort-key": "what",
+ "channel": "edge",
+ })
+ s.srv.trackingChannel = "edge"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--cohort=what", "--edge", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "edge" channel and the "what" cohort`)
+ 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) TestSwitchHappyChannelAndLeaveCohort(c *check.C) {
+ s.srv.total = 4
+ 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": "switch",
+ "leave-cohort": true,
+ "channel": "edge",
+ })
+ s.srv.trackingChannel = "edge"
+ }
+
+ s.RedirectClientToTestServer(s.srv.handle)
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--leave-cohort", "--edge", "foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" left the cohort, and switched to the "edge" channel`)
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)
@@ -1746,7 +1991,12 @@
func (s *SnapOpSuite) TestSwitchAlsoUnhappy(c *check.C) {
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo"})
- c.Assert(err, check.ErrorMatches, `missing --channel= parameter`)
+ c.Assert(err, check.ErrorMatches, `nothing to switch.*`)
+}
+
+func (s *SnapOpSuite) TestSwitchMoreUnhappy(c *check.C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo", "--cohort=what", "--leave-cohort"})
+ c.Assert(err, check.ErrorMatches, `cannot specify both --cohort and --leave-cohort`)
}
func (s *SnapOpSuite) TestSnapOpNetworkTimeoutError(c *check.C) {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snapshot.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snapshot.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot.go 2020-06-05 13:13:49.000000000 +0000
@@ -21,6 +21,8 @@
import (
"fmt"
+ "strconv"
+ "strings"
"github.com/jessevdk/go-flags"
@@ -30,7 +32,7 @@
)
func fmtSize(size int64) string {
- return quantity.FormatAmount(uint64(size), -1)
+ return quantity.FormatAmount(uint64(size), -1) + "B"
}
var (
@@ -106,7 +108,14 @@
}
func (x *savedCmd) Execute([]string) error {
- setID := uint64(x.ID)
+ var setID uint64
+ var err error
+ if x.ID != "" {
+ setID, err = x.ID.ToUint()
+ if err != nil {
+ return err
+ }
+ }
snaps := installedSnapNames(x.Positional.Snaps)
list, err := x.client.SnapshotSets(setID, snaps)
if err != nil {
@@ -133,12 +142,20 @@
i18n.G("Notes"))
for _, sg := range list {
for _, sh := range sg.Snapshots {
- note := "-"
+ notes := []string{}
+ if sh.Auto {
+ notes = append(notes, "auto")
+ }
if sh.Broken != "" {
- note = "broken: " + sh.Broken
+ notes = append(notes, "broken: "+sh.Broken)
}
- size := quantity.FormatAmount(uint64(sh.Size), -1) + "B"
- fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, x.fmtDuration(sh.Time), sh.Version, sh.Revision, size, note)
+ note := "-"
+ if len(notes) > 0 {
+ note = strings.Join(notes, ", ")
+ }
+ size := fmtSize(sh.Size)
+ age := x.fmtDuration(sh.Time)
+ fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, age, sh.Version, sh.Revision, size, note)
}
}
return nil
@@ -170,7 +187,7 @@
y := &savedCmd{
clientMixin: x.clientMixin,
durationMixin: x.durationMixin,
- ID: snapshotID(setID),
+ ID: snapshotID(strconv.FormatUint(setID, 10)),
}
return y.Execute(nil)
}
@@ -184,7 +201,10 @@
}
func (x *forgetCmd) Execute([]string) error {
- setID := uint64(x.Positional.ID)
+ setID, err := x.Positional.ID.ToUint()
+ if err != nil {
+ return err
+ }
snaps := installedSnapNames(x.Positional.Snaps)
changeID, err := x.client.ForgetSnapshots(setID, snaps)
if err != nil {
@@ -200,9 +220,9 @@
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
- fmt.Fprintf(Stdout, i18n.NG("Snapshot #%d of snap %s forgotten.\n", "Snapshot #%d of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps))
+ fmt.Fprintf(Stdout, i18n.NG("Snapshot #%s of snap %s forgotten.\n", "Snapshot #%s of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps))
} else {
- fmt.Fprintf(Stdout, i18n.G("Snapshot #%d forgotten.\n"), x.Positional.ID)
+ fmt.Fprintf(Stdout, i18n.G("Snapshot #%s forgotten.\n"), x.Positional.ID)
}
return nil
}
@@ -217,7 +237,10 @@
}
func (x *checkSnapshotCmd) Execute([]string) error {
- setID := uint64(x.Positional.ID)
+ setID, err := x.Positional.ID.ToUint()
+ if err != nil {
+ return err
+ }
snaps := installedSnapNames(x.Positional.Snaps)
users := strutil.CommaSeparatedList(x.Users)
changeID, err := x.client.CheckSnapshots(setID, snaps, users)
@@ -235,10 +258,10 @@
// TODO: also mention the home archives that were actually checked
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
- fmt.Fprintf(Stdout, i18n.G("Snapshot #%d of snaps %s verified successfully.\n"),
+ fmt.Fprintf(Stdout, i18n.G("Snapshot #%s of snaps %s verified successfully.\n"),
x.Positional.ID, strutil.Quoted(snaps))
} else {
- fmt.Fprintf(Stdout, i18n.G("Snapshot #%d verified successfully.\n"), x.Positional.ID)
+ fmt.Fprintf(Stdout, i18n.G("Snapshot #%s verified successfully.\n"), x.Positional.ID)
}
return nil
}
@@ -253,7 +276,10 @@
}
func (x *restoreCmd) Execute([]string) error {
- setID := uint64(x.Positional.ID)
+ setID, err := x.Positional.ID.ToUint()
+ if err != nil {
+ return err
+ }
snaps := installedSnapNames(x.Positional.Snaps)
users := strutil.CommaSeparatedList(x.Users)
changeID, err := x.client.RestoreSnapshots(setID, snaps, users)
@@ -271,10 +297,10 @@
// TODO: also mention the home archives that were actually restored
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
- fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d of snaps %s.\n"),
+ fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s of snaps %s.\n"),
x.Positional.ID, strutil.Quoted(snaps))
} else {
- fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d.\n"), x.Positional.ID)
+ fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s.\n"), x.Positional.ID)
}
return nil
}
@@ -310,14 +336,34 @@
}, waitDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"users": i18n.G("Restore data of only specific users (comma-separated) (default: all users)"),
- }), nil)
+ }), []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Set id of snapshot to restore (see 'snap help saved')"),
+ }, {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap for which data will be restored"),
+ },
+ })
addCommand("forget",
shortForgetHelp,
longForgetHelp,
func() flags.Commander {
return &forgetCmd{}
- }, waitDescs, nil)
+ }, waitDescs, []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Set id of snapshot to delete (see 'snap help saved')"),
+ }, {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap for which data will be deleted"),
+ },
+ })
addCommand("check-snapshot",
shortCheckHelp,
@@ -327,5 +373,15 @@
}, waitDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"users": i18n.G("Check data of only specific users (comma-separated) (default: all users)"),
- }), nil)
+ }), []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Set id of snapshot to verify (see 'snap help saved')"),
+ }, {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap for which data will be verified"),
+ },
+ })
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snapshot_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snapshot_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,114 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+var snapshotsTests = []getCmdArgs{{
+ args: "restore x",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
+}, {
+ args: "saved --id=x",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
+}, {
+ args: "saved --id=3",
+ stdout: "Set Snap Age Version Rev Size Notes\n3 htop .* 2 1168 1B auto\n",
+}, {
+ args: "saved",
+ stdout: "Set Snap Age Version Rev Size Notes\n1 htop .* 2 1168 1B -\n",
+}, {
+ args: "forget x",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
+}, {
+ args: "check-snapshot x",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
+}, {
+ args: "restore 1",
+ stdout: "Restored snapshot #1.\n",
+}, {
+ args: "forget 2",
+ stdout: "Snapshot #2 forgotten.\n",
+}, {
+ args: "forget 2 snap1 snap2",
+ stdout: "Snapshot #2 of snaps \"snap1\", \"snap2\" forgotten.\n",
+}, {
+ args: "check-snapshot 4",
+ stdout: "Snapshot #4 verified successfully.\n",
+}, {
+ args: "check-snapshot 4 snap1 snap2",
+ stdout: "Snapshot #4 of snaps \"snap1\", \"snap2\" verified successfully.\n",
+}}
+
+func (s *SnapSuite) TestSnapSnaphotsTest(c *C) {
+ s.mockSnapshotsServer(c)
+
+ restore := main.MockIsStdinTTY(true)
+ defer restore()
+
+ for _, test := range snapshotsTests {
+ s.stdout.Truncate(0)
+ s.stderr.Truncate(0)
+
+ c.Logf("Test: %s", test.args)
+
+ _, err := main.Parser(main.Client()).ParseArgs(strings.Fields(test.args))
+ if test.error != "" {
+ c.Check(err, ErrorMatches, test.error)
+ } else {
+ c.Check(err, IsNil)
+ c.Check(s.Stderr(), testutil.EqualsWrapped, test.stderr)
+ c.Check(s.Stdout(), testutil.MatchesWrapped, test.stdout)
+ }
+ }
+}
+
+func (s *SnapSuite) mockSnapshotsServer(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/snapshots":
+ if r.Method == "GET" {
+ // simulate a 1-month old snapshot
+ snapshotTime := time.Now().AddDate(0, -1, 0).Format(time.RFC3339)
+ if r.URL.Query().Get("set") == "3" {
+ fmt.Fprintf(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":3,"snapshots":[{"set":3,"time":%q,"snap":"htop","revision":"1168","snap-id":"Z","auto":true,"epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`, snapshotTime)
+ return
+ }
+ fmt.Fprintf(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":1,"snapshots":[{"set":1,"time":%q,"snap":"htop","revision":"1168","snap-id":"Z","epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`, snapshotTime)
+ } else {
+ w.WriteHeader(202)
+ fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "9"}`)
+ }
+ case "/v2/changes/9":
+ fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {}}}`)
+ default:
+ c.Errorf("unexpected path %q", r.URL.Path)
+ }
+ })
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unalias_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unalias_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unalias_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unalias_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -53,6 +53,7 @@
"snap": "alias1",
"alias": "alias1",
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unset.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unset.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,85 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+var shortUnsetHelp = i18n.G("Remove configuration options")
+var longUnsetHelp = i18n.G(`
+The unset command removes the provided configuration options as requested.
+
+ $ snap unset snap-name name address
+
+All configuration changes are persisted at once, and only after the
+snap's configuration hook returns successfully.
+
+Nested values may be removed via a dotted path:
+
+ $ snap unset snap-name user.name
+`)
+
+type cmdUnset struct {
+ waitMixin
+ Positional struct {
+ Snap installedSnapName
+ ConfKeys []string `required:"1"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func init() {
+ addCommand("unset", shortUnsetHelp, longUnsetHelp, func() flags.Commander { return &cmdUnset{} }, waitDescs, []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap to configure (e.g. hello-world)"),
+ }, {
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Configuration key to unset"),
+ },
+ })
+}
+
+func (x *cmdUnset) Execute(args []string) error {
+ patchValues := make(map[string]interface{})
+ for _, confKey := range x.Positional.ConfKeys {
+ patchValues[confKey] = nil
+ }
+
+ snapName := string(x.Positional.Snap)
+ id, err := x.client.SetConf(snapName, patchValues)
+ if err != nil {
+ return err
+ }
+
+ if _, err := x.wait(id); err != nil {
+ if err == noWait {
+ return nil
+ }
+ return err
+ }
+
+ return nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unset_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unset_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,47 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "gopkg.in/check.v1"
+
+ snapunset "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *snapSetSuite) TestInvalidUnsetParameters(c *check.C) {
+ invalidParameters := []string{"unset"}
+ _, err := snapunset.Parser(snapunset.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, "the required arguments `` and ` \\(at least 1 argument\\)` were not provided")
+ c.Check(s.setConfApiCalls, check.Equals, 0)
+
+ invalidParameters = []string{"unset", "snap-name"}
+ _, err = snapunset.Parser(snapunset.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, "the required argument ` \\(at least 1 argument\\)` was not provided")
+ c.Check(s.setConfApiCalls, check.Equals, 0)
+}
+
+func (s *snapSetSuite) TestSnapUnset(c *check.C) {
+ // expected value is "nil" as the key is unset
+ s.mockSetConfigServer(c, nil)
+
+ _, err := snapunset.Parser(snapunset.Client()).ParseArgs([]string{"unset", "snapname", "key"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_userd.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_userd.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,7 +1,8 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build !darwin
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2017-2019 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
@@ -27,14 +28,16 @@
"github.com/jessevdk/go-flags"
+ "github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/i18n"
- "github.com/snapcore/snapd/userd"
+ "github.com/snapcore/snapd/usersession/agent"
+ "github.com/snapcore/snapd/usersession/autostart"
+ "github.com/snapcore/snapd/usersession/userd"
)
type cmdUserd struct {
- userd userd.Userd
-
Autostart bool `long:"autostart"`
+ Agent bool `long:"agent"`
}
var shortUserdHelp = i18n.G("Start the userd service")
@@ -51,6 +54,8 @@
}, map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"autostart": i18n.G("Autostart user applications"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "agent": i18n.G("Run the user session agent"),
}, nil)
cmd.hidden = true
}
@@ -64,26 +69,66 @@
return x.runAutostart()
}
- if err := x.userd.Init(); err != nil {
+ if x.Agent {
+ return x.runAgent()
+ }
+
+ return x.runUserd()
+}
+
+var signalNotify = signalNotifyImpl
+
+func (x *cmdUserd) runUserd() error {
+ var userd userd.Userd
+ if err := userd.Init(); err != nil {
+ return err
+ }
+ userd.Start()
+
+ ch, stop := signalNotify(syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
+
+ select {
+ case sig := <-ch:
+ fmt.Fprintf(Stdout, "Exiting on %s.\n", sig)
+ case <-userd.Dying():
+ // something called Stop()
+ }
+
+ return userd.Stop()
+}
+
+func (x *cmdUserd) runAgent() error {
+ agent, err := agent.New()
+ if err != nil {
return err
}
- x.userd.Start()
+ agent.Version = cmd.Version
+ agent.Start()
+
+ ch, stop := signalNotify(syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
- ch := make(chan os.Signal, 3)
- signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
select {
case sig := <-ch:
fmt.Fprintf(Stdout, "Exiting on %s.\n", sig)
- case <-x.userd.Dying():
+ case <-agent.Dying():
// something called Stop()
}
- return x.userd.Stop()
+ return agent.Stop()
}
func (x *cmdUserd) runAutostart() error {
- if err := userd.AutostartSessionApps(); err != nil {
+ if err := autostart.AutostartSessionApps(); err != nil {
return fmt.Errorf("autostart failed for the following apps:\n%v", err)
}
return nil
}
+
+func signalNotifyImpl(sig ...os.Signal) (ch chan os.Signal, stop func()) {
+ ch = make(chan os.Signal, len(sig))
+ signal.Notify(ch, sig...)
+ stop = func() { signal.Stop(ch) }
+ return ch, stop
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_userd_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_userd_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,7 +1,8 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build !darwin
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2019 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
@@ -20,6 +21,9 @@
package main_test
import (
+ "fmt"
+ "net"
+ "net/http"
"os"
"strings"
"syscall"
@@ -28,7 +32,9 @@
. "gopkg.in/check.v1"
snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/testutil"
)
@@ -36,7 +42,7 @@
BaseSnapSuite
testutil.DBusTest
- restoreLogger func()
+ agentSocketPath string
}
var _ = Suite(&userdSuite{})
@@ -45,14 +51,17 @@
s.BaseSnapSuite.SetUpTest(c)
s.DBusTest.SetUpTest(c)
- _, s.restoreLogger = logger.MockLogger()
+ _, restore := logger.MockLogger()
+ s.AddCleanup(restore)
+
+ xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid())
+ c.Assert(os.MkdirAll(xdgRuntimeDir, 0700), IsNil)
+ s.agentSocketPath = fmt.Sprintf("%s/snapd-session-agent.socket", xdgRuntimeDir)
}
func (s *userdSuite) TearDownTest(c *C) {
s.BaseSnapSuite.TearDownTest(c)
s.DBusTest.TearDownTest(c)
-
- s.restoreLogger()
}
func (s *userdSuite) TestUserdBadCommandline(c *C) {
@@ -60,13 +69,29 @@
c.Assert(err, ErrorMatches, "too many arguments for command")
}
+type mockSignal struct{}
+
+func (m *mockSignal) String() string {
+ return ""
+}
+
+func (m *mockSignal) Signal() {}
+
func (s *userdSuite) TestUserdDBus(c *C) {
+ sigCh := make(chan os.Signal, 1)
+ sigStopCalls := 0
+
+ restore := snap.MockSignalNotify(func(sig ...os.Signal) (chan os.Signal, func()) {
+ c.Assert(sig, DeepEquals, []os.Signal{syscall.SIGINT, syscall.SIGTERM})
+ return sigCh, func() { sigStopCalls++ }
+ })
+ defer restore()
+
go func() {
myPid := os.Getpid()
+
defer func() {
- me, err := os.FindProcess(myPid)
- c.Assert(err, IsNil)
- me.Signal(syscall.SIGUSR1)
+ sigCh <- &mockSignal{}
}()
names := map[string]bool{
@@ -98,5 +123,72 @@
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd"})
c.Assert(err, IsNil)
c.Check(rest, DeepEquals, []string{})
- c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on user defined signal 1.\n")
+ c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on .\n")
+ c.Check(sigStopCalls, Equals, 1)
+}
+
+func (s *userdSuite) makeAgentClient() *http.Client {
+ transport := &http.Transport{
+ Dial: func(_, _ string) (net.Conn, error) {
+ return net.Dial("unix", s.agentSocketPath)
+ },
+ DisableKeepAlives: true,
+ }
+ return &http.Client{Transport: transport}
+}
+
+func (s *userdSuite) TestSessionAgentSocket(c *C) {
+ sigCh := make(chan os.Signal, 1)
+ sigStopCalls := 0
+
+ restore := snap.MockSignalNotify(func(sig ...os.Signal) (chan os.Signal, func()) {
+ c.Assert(sig, DeepEquals, []os.Signal{syscall.SIGINT, syscall.SIGTERM})
+ return sigCh, func() { sigStopCalls++ }
+ })
+ defer restore()
+
+ go func() {
+ defer func() {
+ sigCh <- &mockSignal{}
+ }()
+
+ // Wait for command to create socket file
+ for i := 0; i < 1000; i++ {
+ if osutil.FileExists(s.agentSocketPath) {
+ break
+ }
+ time.Sleep(10 * time.Millisecond)
+ }
+
+ // Check that agent functions
+ client := s.makeAgentClient()
+ response, err := client.Get("http://localhost/v1/session-info")
+ c.Assert(err, IsNil)
+ defer response.Body.Close()
+ c.Check(response.StatusCode, Equals, 200)
+ }()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd", "--agent"})
+ c.Assert(err, IsNil)
+ c.Check(rest, DeepEquals, []string{})
+ c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on .\n")
+ c.Check(sigStopCalls, Equals, 1)
+}
+
+func (s *userdSuite) TestSignalNotify(c *C) {
+ ch, stop := snap.SignalNotify(syscall.SIGUSR1)
+ defer stop()
+ go func() {
+ myPid := os.Getpid()
+ me, err := os.FindProcess(myPid)
+ c.Assert(err, IsNil)
+ err = me.Signal(syscall.SIGUSR1)
+ c.Assert(err, IsNil)
+ }()
+ select {
+ case sig := <-ch:
+ c.Assert(sig, Equals, syscall.SIGUSR1)
+ case <-time.After(5 * time.Second):
+ c.Fatal("signal not received within 5s")
+ }
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_version.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_version.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version.go 2020-06-05 13:13:49.000000000 +0000
@@ -68,6 +68,7 @@
if sv.KernelVersion != "" {
fmt.Fprintf(w, "kernel\t%s\n", sv.KernelVersion)
}
+
w.Flush()
return nil
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_version_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_version_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -30,7 +30,7 @@
func (s *SnapSuite) TestVersionCommandOnClassic(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`)
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89","architecture":"ia64"}}`)
})
restore := mockArgs("snap", "version")
defer restore()
@@ -45,7 +45,7 @@
func (s *SnapSuite) TestVersionCommandOnAllSnap(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`)
+ fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89","architecture":"powerpc","virtualization":"qemu"}}`)
})
restore := mockArgs("snap", "--version")
defer restore()
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_wait.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_wait.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_wait.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_wait.go 2020-06-05 13:13:49.000000000 +0000
@@ -22,7 +22,6 @@
import (
"encoding/json"
"fmt"
- "math/rand"
"reflect"
"time"
@@ -43,7 +42,7 @@
func init() {
addCommand("wait",
"Wait for configuration",
- "The wait command waits until a configration becomes true.",
+ "The wait command waits until a configuration becomes true.",
func() flags.Commander {
return &cmdWait{}
}, nil, []argDesc{
@@ -117,21 +116,6 @@
snapName := string(x.Positional.Snap)
confKey := x.Positional.Key
- // This is fine because not providing a confKey is unsupported so this
- // won't interfere with supported uses of `snap wait`.
- if snapName == "godot" && confKey == "" {
- switch rand.Intn(10) {
- case 0:
- fmt.Fprintln(Stdout, `The tears of the world are a constant quantity.
-For each one who begins to weep somewhere else another stops.
-The same is true of the laugh.`)
- case 1:
- fmt.Fprintln(Stdout, "Nothing happens. Nobody comes, nobody goes. It's awful.")
- default:
- fmt.Fprintln(Stdout, `"Let's go." "We can't." "Why not?" "We're waiting for Godot."`)
- }
- return nil
- }
if confKey == "" {
return fmt.Errorf("the required argument `` was not provided")
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings.go 2020-06-05 13:13:49.000000000 +0000
@@ -38,6 +38,7 @@
type cmdWarnings struct {
clientMixin
timeMixin
+ unicodeMixin
All bool `long:"all"`
Verbose bool `long:"verbose"`
}
@@ -64,7 +65,7 @@
`)
func init() {
- addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(map[string]string{
+ addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(unicodeDescs).also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"all": i18n.G("Show all warnings"),
// TRANSLATORS: This should not start with a lowercase letter.
@@ -96,35 +97,36 @@
return err
}
+ termWidth, _ := termSize()
+ if termWidth > 100 {
+ // any wider than this and it gets hard to read
+ termWidth = 100
+ }
+
+ esc := cmd.getEscapes()
w := tabWriter()
- if cmd.Verbose {
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
- i18n.G("First occurrence"),
- i18n.G("Last occurrence"),
- i18n.G("Expires after"),
- i18n.G("Acknowledged"),
- i18n.G("Repeats after"),
- i18n.G("Warning"))
- for _, warning := range warnings {
- lastShown := "-"
+ for i, warning := range warnings {
+ if i > 0 {
+ fmt.Fprintln(w, "---")
+ }
+ if cmd.Verbose {
+ fmt.Fprintf(w, "first-occurrence:\t%s\n", cmd.fmtTime(warning.FirstAdded))
+ }
+ fmt.Fprintf(w, "last-occurrence:\t%s\n", cmd.fmtTime(warning.LastAdded))
+ if cmd.Verbose {
+ lastShown := esc.dash
if !warning.LastShown.IsZero() {
lastShown = cmd.fmtTime(warning.LastShown)
}
- fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
- cmd.fmtTime(warning.FirstAdded),
- cmd.fmtTime(warning.LastAdded),
- quantity.FormatDuration(warning.ExpireAfter.Seconds()),
- lastShown,
- quantity.FormatDuration(warning.RepeatAfter.Seconds()),
- warning.Message)
- }
- } else {
- fmt.Fprintf(w, "%s\t%s\n", i18n.G("Last occurrence"), i18n.G("Warning"))
- for _, warning := range warnings {
- fmt.Fprintf(w, "%s\t%s\n", cmd.fmtTime(warning.LastAdded), warning.Message)
+ fmt.Fprintf(w, "acknowledged:\t%s\n", lastShown)
+ // TODO: cmd.fmtDuration() using timeutil.HumanDuration
+ fmt.Fprintf(w, "repeats-after:\t%s\n", quantity.FormatDuration(warning.RepeatAfter.Seconds()))
+ fmt.Fprintf(w, "expires-after:\t%s\n", quantity.FormatDuration(warning.ExpireAfter.Seconds()))
}
+ fmt.Fprintln(w, "warning: |")
+ printDescr(w, warning.Message, termWidth)
+ w.Flush()
}
- w.Flush()
return nil
}
@@ -136,7 +138,7 @@
last, err := lastWarningTimestamp()
if err != nil {
- return fmt.Errorf("no client-side warning timestamp found: %v", err)
+ return err
}
return cmd.client.Okay(last)
@@ -191,8 +193,12 @@
if err != nil {
return time.Time{}, fmt.Errorf("cannot determine real user: %v", err)
}
+
f, err := os.Open(warnFilename(user.HomeDir))
if err != nil {
+ if os.IsNotExist(err) {
+ return time.Time{}, fmt.Errorf("you must have looked at the warnings before acknowledging them. Try 'snap warnings'.")
+ }
return time.Time{}, fmt.Errorf("cannot open timestamp file: %v", err)
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -102,28 +102,44 @@
func (s *warningSuite) TestWarnings(c *check.C) {
s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings))
- rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"})
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--unicode=never"})
c.Assert(err, check.IsNil)
c.Check(rest, check.HasLen, 0)
c.Check(s.Stderr(), check.Equals, "")
c.Check(s.Stdout(), check.Equals, `
-Last occurrence Warning
-2018-09-19T12:41:18Z hello world number one
-2018-09-19T12:44:19Z hello world number two
+last-occurrence: 2018-09-19T12:41:18Z
+warning: |
+ hello world number one
+---
+last-occurrence: 2018-09-19T12:44:19Z
+warning: |
+ hello world number two
`[1:])
}
func (s *warningSuite) TestVerboseWarnings(c *check.C) {
s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings))
- rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose"})
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose", "--unicode=never"})
c.Assert(err, check.IsNil)
c.Check(rest, check.HasLen, 0)
c.Check(s.Stderr(), check.Equals, "")
c.Check(s.Stdout(), check.Equals, `
-First occurrence Last occurrence Expires after Acknowledged Repeats after Warning
-2018-09-19T12:41:18Z 2018-09-19T12:41:18Z 28d0h - 1d00h hello world number one
-2018-09-19T12:44:19Z 2018-09-19T12:44:19Z 28d0h - 1d00h hello world number two
+first-occurrence: 2018-09-19T12:41:18Z
+last-occurrence: 2018-09-19T12:41:18Z
+acknowledged: --
+repeats-after: 1d00h
+expires-after: 28d0h
+warning: |
+ hello world number one
+---
+first-occurrence: 2018-09-19T12:44:19Z
+last-occurrence: 2018-09-19T12:44:19Z
+acknowledged: --
+repeats-after: 1d00h
+expires-after: 28d0h
+warning: |
+ hello world number two
`[1:])
}
@@ -155,6 +171,13 @@
c.Check(s.Stderr(), check.Equals, "")
c.Check(s.Stdout(), check.Equals, "")
}
+
+func (s *warningSuite) TestOkayBeforeWarnings(c *check.C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"okay"})
+ c.Assert(err, check.ErrorMatches, "you must have looked at the warnings before acknowledging them. Try 'snap warnings'.")
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(s.Stdout(), check.Equals, "")
+}
func (s *warningSuite) TestListWithWarnings(c *check.C) {
var called bool
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_whoami_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_whoami_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_whoami_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_whoami_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -0,0 +1,69 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+package main_test
+
+import (
+ "net/http"
+
+ "github.com/snapcore/snapd/osutil"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestWhoamiLoggedInUser(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ panic("unexpected call to snapd API")
+ })
+
+ s.Login(c)
+ defer s.Logout(c)
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"whoami"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "email: hello@mail.com\n")
+}
+
+func (s *SnapSuite) TestWhoamiNotLoggedInUser(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ panic("unexpected call to snapd API")
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"whoami"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "email: -\n")
+}
+
+func (s *SnapSuite) TestWhoamiExtraParamError(c *C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"whoami", "test"})
+ c.Check(err, ErrorMatches, "too many arguments for command")
+}
+
+func (s *SnapSuite) TestWhoamiEmptyAuthFile(c *C) {
+ s.Login(c)
+ defer s.Logout(c)
+
+ err := osutil.AtomicWriteFile(s.AuthFile, []byte(``), 0600, 0)
+ c.Assert(err, IsNil)
+
+ _, err = snap.Parser(snap.Client()).ParseArgs([]string{"whoami"})
+ c.Check(err, ErrorMatches, "EOF")
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/color.go snapd-2.45.1ubuntu0.2/cmd/snap/color.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/color.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/color.go 2020-06-05 13:13:49.000000000 +0000
@@ -30,14 +30,12 @@
"github.com/snapcore/snapd/snap"
)
-type colorMixin struct {
- Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"`
- Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"` // do we want this hidden?
+type unicodeMixin struct {
+ Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"`
}
-func (mx colorMixin) getEscapes() *escapes {
- esc := colorTable(mx.Color)
- if canUnicode(mx.Unicode) {
+func (ux unicodeMixin) addUnicodeChars(esc *escapes) {
+ if canUnicode(ux.Unicode) {
esc.dash = "–" // that's an en dash (so yaml is happy)
esc.uparrow = "↑"
esc.tick = "✓"
@@ -46,7 +44,22 @@
esc.uparrow = "^"
esc.tick = "*"
}
+}
+func (ux unicodeMixin) getEscapes() *escapes {
+ esc := &escapes{}
+ ux.addUnicodeChars(esc)
+ return esc
+}
+
+type colorMixin struct {
+ Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"`
+ unicodeMixin
+}
+
+func (mx colorMixin) getEscapes() *escapes {
+ esc := colorTable(mx.Color)
+ mx.addUnicodeChars(&esc)
return &esc
}
@@ -103,13 +116,18 @@
var colorDescs = mixinDescs{
// TRANSLATORS: This should not start with a lowercase letter.
- "color": i18n.G("Use a little bit of color to highlight some things."),
+ "color": i18n.G("Use a little bit of color to highlight some things."),
+ "unicode": unicodeDescs["unicode"],
+}
+
+var unicodeDescs = mixinDescs{
// TRANSLATORS: This should not start with a lowercase letter.
"unicode": i18n.G("Use a little bit of Unicode to improve legibility."),
}
type escapes struct {
green string
+ bold string
end string
tick, dash, uparrow string
@@ -118,11 +136,13 @@
var (
color = escapes{
green: "\033[32m",
+ bold: "\033[1m",
end: "\033[0m",
}
mono = escapes{
green: "\033[1m",
+ bold: "\033[1m",
end: "\033[0m",
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/complete.go snapd-2.45.1ubuntu0.2/cmd/snap/complete.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/complete.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/complete.go 2020-06-05 13:13:49.000000000 +0000
@@ -31,6 +31,7 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/snap"
)
@@ -105,8 +106,8 @@
return nil
}
snaps, _, err := mkClient().Find(&client.FindOptions{
- Prefix: true,
Query: match,
+ Prefix: true,
})
if err != nil {
return nil
@@ -296,7 +297,10 @@
parts := strings.SplitN(match, ":", 2)
// Ask snapd about available interfaces.
- ifaces, err := mkClient().Connections()
+ opts := client.ConnectionOptions{
+ All: true,
+ }
+ ifaces, err := mkClient().Connections(&opts)
if err != nil {
return nil
}
@@ -435,17 +439,25 @@
return nil
}
- snaps := map[string]bool{}
+ snaps := map[string]int{}
var ret []flags.Completion
for _, app := range apps {
if !app.IsService() {
continue
}
- if !snaps[app.Snap] {
- snaps[app.Snap] = true
- ret = append(ret, flags.Completion{Item: app.Snap})
+ name := snap.JoinSnapApp(app.Snap, app.Name)
+ if !strings.HasPrefix(name, match) {
+ continue
+ }
+ ret = append(ret, flags.Completion{Item: name})
+ if len(match) <= len(app.Snap) {
+ snaps[app.Snap]++
+ }
+ }
+ for snap, n := range snaps {
+ if n > 1 {
+ ret = append(ret, flags.Completion{Item: snap})
}
- ret = append(ret, flags.Completion{Item: app.Snap + "." + app.Name})
}
return ret
@@ -475,7 +487,7 @@
return ret
}
-type snapshotID uint64
+type snapshotID string
func (snapshotID) Complete(match string) []flags.Completion {
shots, err := mkClient().SnapshotSets(0, nil)
@@ -492,3 +504,11 @@
return ret
}
+
+func (s snapshotID) ToUint() (uint64, error) {
+ setID, err := strconv.ParseUint((string)(s), 10, 64)
+ if err != nil {
+ return 0, fmt.Errorf(i18n.G("invalid argument for snapshot set id: expected a non-negative integer argument (see 'snap help saved')"))
+ }
+ return setID, nil
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/error.go snapd-2.45.1ubuntu0.2/cmd/snap/error.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/error.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/error.go 2020-06-05 13:13:49.000000000 +0000
@@ -35,13 +35,15 @@
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/strutil"
)
var errorPrefix = i18n.G("error: %v\n")
-func termSize() (width, height int) {
+var termSize = termSizeImpl
+
+func termSizeImpl() (width, height int) {
if f, ok := Stdout.(*os.File); ok {
width, height, _ = terminal.GetSize(int(f.Fd()))
}
@@ -237,16 +239,17 @@
return msg, nil
}
-func snapRevisionNotAvailableMessage(kind, snapName, action, arch, channel string, releases []interface{}) string {
+func snapRevisionNotAvailableMessage(kind, snapName, action, arch, snapChannel string, releases []interface{}) string {
// releases contains all available (arch x channel)
// as reported by the store through the daemon
- req, err := snap.ParseChannel(channel, arch)
+ req, err := channel.Parse(snapChannel, arch)
if err != nil {
+ // XXX: this is no longer possible (should be caught before hitting the store), unless the state itself has an invalid channel
// TRANSLATORS: %q is the invalid request channel, %s is the snap name
- msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), channel, snapName)
+ msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), snapChannel, snapName)
return msg
}
- avail := make([]*snap.Channel, 0, len(releases))
+ avail := make([]*channel.Channel, 0, len(releases))
for _, v := range releases {
rel, _ := v.(map[string]interface{})
relCh, _ := rel["channel"].(string)
@@ -255,7 +258,7 @@
logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v)
continue
}
- a, err := snap.ParseChannel(relCh, relArch)
+ a, err := channel.Parse(relCh, relArch)
if err != nil {
logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v)
continue
@@ -263,7 +266,7 @@
avail = append(avail, &a)
}
- matches := map[string][]*snap.Channel{}
+ matches := map[string][]*channel.Channel{}
for _, a := range avail {
m := req.Match(a)
matchRepr := m.String()
@@ -296,7 +299,7 @@
if req.Branch != "" {
// there are matching arch+track+risk, give main track info
if len(matches["architecture:track:risk"]) != 0 {
- trackRisk := snap.Channel{Track: req.Track, Risk: req.Risk}
+ trackRisk := channel.Channel{Track: req.Track, Risk: req.Risk}
trackRisk = trackRisk.Clean()
// TRANSLATORS: %q is for the snap name, first %s is the full requested channel
@@ -348,7 +351,7 @@
return msg
}
-func installTable(snapName, action string, avail []*snap.Channel, full bool) string {
+func installTable(snapName, action string, avail []*channel.Channel, full bool) string {
b := &bytes.Buffer{}
w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0)
first := true
@@ -377,7 +380,7 @@
return strings.Join(lines, "")
}
-func channelOption(c *snap.Channel) string {
+func channelOption(c *channel.Channel) string {
if c.Branch == "" {
if c.Track == "" {
return fmt.Sprintf("--%s", c.Risk)
@@ -389,7 +392,7 @@
return fmt.Sprintf("--channel=%s", c)
}
-func archsForChannels(cs []*snap.Channel) []string {
+func archsForChannels(cs []*channel.Channel) []string {
archs := []string{}
for _, c := range cs {
if !strutil.ListContains(archs, c.Architecture) {
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/export_test.go snapd-2.45.1ubuntu0.2/cmd/snap/export_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/export_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/export_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2019 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
@@ -20,14 +20,15 @@
package main
import (
+ "os"
"os/user"
"time"
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/client"
- "github.com/snapcore/snapd/overlord/auth"
- "github.com/snapcore/snapd/selinux"
+ "github.com/snapcore/snapd/image"
+ "github.com/snapcore/snapd/sandbox/selinux"
"github.com/snapcore/snapd/store"
)
@@ -38,17 +39,18 @@
FirstNonOptionIsRun = firstNonOptionIsRun
- CreateUserDataDirs = createUserDataDirs
- ResolveApp = resolveApp
- IsReexeced = isReexeced
- MaybePrintServices = maybePrintServices
- MaybePrintCommands = maybePrintCommands
- SortByPath = sortByPath
- AdviseCommand = adviseCommand
- Antialias = antialias
- FormatChannel = fmtChannel
- PrintDescr = printDescr
- TrueishJSON = trueishJSON
+ CreateUserDataDirs = createUserDataDirs
+ ResolveApp = resolveApp
+ SnapdHelperPath = snapdHelperPath
+ SortByPath = sortByPath
+ AdviseCommand = adviseCommand
+ Antialias = antialias
+ FormatChannel = fmtChannel
+ PrintDescr = printDescr
+ WrapFlow = wrapFlow
+ TrueishJSON = trueishJSON
+ CompletionHandler = completionHandler
+ MarkForNoCompletion = markForNoCompletion
CanUnicode = canUnicode
ColorTable = colorTable
@@ -76,6 +78,59 @@
LintDesc = lintDesc
FixupArg = fixupArg
+
+ InterfacesDeprecationNotice = interfacesDeprecationNotice
+
+ SignalNotify = signalNotify
+
+ SortTimingsTasks = sortTimingsTasks
+
+ PrintInstallHint = printInstallHint
+
+ IsStopping = isStopping
+)
+
+func HiddenCmd(descr string, completeHidden bool) *cmdInfo {
+ return &cmdInfo{
+ shortHelp: descr,
+ hidden: true,
+ completeHidden: completeHidden,
+ }
+}
+
+type ChangeTimings = changeTimings
+
+func NewInfoWriter(w writeflusher) *infoWriter {
+ return &infoWriter{
+ writeflusher: w,
+ termWidth: 20,
+ esc: &escapes{dash: "--", tick: "*"},
+ fmtTime: func(t time.Time) string { return t.Format(time.Kitchen) },
+ }
+}
+
+func SetVerbose(iw *infoWriter, verbose bool) {
+ iw.verbose = verbose
+}
+
+var (
+ ClientSnapFromPath = clientSnapFromPath
+ SetupDiskSnap = (*infoWriter).setupDiskSnap
+ SetupSnap = (*infoWriter).setupSnap
+ MaybePrintServices = (*infoWriter).maybePrintServices
+ MaybePrintCommands = (*infoWriter).maybePrintCommands
+ MaybePrintType = (*infoWriter).maybePrintType
+ PrintSummary = (*infoWriter).printSummary
+ MaybePrintPublisher = (*infoWriter).maybePrintPublisher
+ MaybePrintNotes = (*infoWriter).maybePrintNotes
+ MaybePrintStandaloneVersion = (*infoWriter).maybePrintStandaloneVersion
+ MaybePrintBuildDate = (*infoWriter).maybePrintBuildDate
+ MaybePrintContact = (*infoWriter).maybePrintContact
+ MaybePrintBase = (*infoWriter).maybePrintBase
+ MaybePrintPath = (*infoWriter).maybePrintPath
+ MaybePrintSum = (*infoWriter).maybePrintSum
+ MaybePrintCohortKey = (*infoWriter).maybePrintCohortKey
+ MaybePrintHealth = (*infoWriter).maybePrintHealth
)
func MockPollTime(d time.Duration) (restore func()) {
@@ -110,7 +165,7 @@
}
}
-func MockStoreNew(f func(*store.Config, auth.AuthContext) *store.Store) (restore func()) {
+func MockStoreNew(f func(*store.Config, store.DeviceAndAuthContext) *store.Store) (restore func()) {
storeNewOrig := storeNew
storeNew = f
return func() {
@@ -211,7 +266,10 @@
}
func ColorMixin(cmode, umode string) colorMixin {
- return colorMixin{Color: cmode, Unicode: umode}
+ return colorMixin{
+ Color: cmode,
+ unicodeMixin: unicodeMixin{Unicode: umode},
+ }
}
func CmdAdviseSnap() *cmdAdviseSnap {
@@ -241,3 +299,61 @@
selinuxRestoreContext = old
}
}
+
+func MockTermSize(newTermSize func() (int, int)) (restore func()) {
+ old := termSize
+ termSize = newTermSize
+ return func() {
+ termSize = old
+ }
+}
+
+func MockImagePrepare(newImagePrepare func(*image.Options) error) (restore func()) {
+ old := imagePrepare
+ imagePrepare = newImagePrepare
+ return func() {
+ imagePrepare = old
+ }
+}
+
+func MockSignalNotify(newSignalNotify func(sig ...os.Signal) (chan os.Signal, func())) (restore func()) {
+ old := signalNotify
+ signalNotify = newSignalNotify
+ return func() {
+ signalNotify = old
+ }
+}
+
+type ServiceName = serviceName
+
+func MockApparmorSnapAppFromPid(f func(pid int) (string, string, string, error)) (restore func()) {
+ old := apparmorSnapAppFromPid
+ apparmorSnapAppFromPid = f
+ return func() {
+ apparmorSnapAppFromPid = old
+ }
+}
+
+func MockCgroupSnapNameFromPid(f func(pid int) (string, error)) (restore func()) {
+ old := cgroupSnapNameFromPid
+ cgroupSnapNameFromPid = f
+ return func() {
+ cgroupSnapNameFromPid = old
+ }
+}
+
+func MockSyscallUmount(f func(string, int) error) (restore func()) {
+ old := syscallUnmount
+ syscallUnmount = f
+ return func() {
+ syscallUnmount = old
+ }
+}
+
+func MockIoutilTempDir(f func(string, string) (string, error)) (restore func()) {
+ old := ioutilTempDir
+ ioutilTempDir = f
+ return func() {
+ ioutilTempDir = old
+ }
+}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/main.go snapd-2.45.1ubuntu0.2/cmd/snap/main.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/main.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/main.go 2020-06-05 13:13:49.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2015 Canonical Ltd
+ * 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
@@ -37,24 +37,25 @@
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/dirs"
- "github.com/snapcore/snapd/httputil"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snapdenv"
)
func init() {
// set User-Agent for when 'snap' talks to the store directly (snap download etc...)
- httputil.SetUserAgentFromVersion(cmd.Version, "snap")
+ snapdenv.SetUserAgentFromVersion(cmd.Version, nil, "snap")
- if osutil.GetenvBool("SNAPD_DEBUG") || osutil.GetenvBool("SNAPPY_TESTING") {
+ if osutil.GetenvBool("SNAPD_DEBUG") || snapdenv.Testing() {
// in tests or when debugging, enforce the "tidy" lint checks
noticef = logger.Panicf
}
- // plug/slot sanitization not used nor possible from snap command, make it no-op
+ // plug/slot sanitization not used by snap commands (except for snap pack
+ // which re-sets it), make it no-op.
snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {}
}
@@ -88,10 +89,13 @@
name, shortHelp, longHelp string
builder func() flags.Commander
hidden bool
- optDescs map[string]string
- argDescs []argDesc
- alias string
- extra func(*flags.Command)
+ // completeHidden set to true forces completion even of
+ // a hidden command
+ completeHidden bool
+ optDescs map[string]string
+ argDescs []argDesc
+ alias string
+ extra func(*flags.Command)
}
// commands holds information about all non-debug commands.
@@ -100,6 +104,9 @@
// debugCommands holds information about all debug commands.
var debugCommands []*cmdInfo
+// routineCommands holds information about all internal commands.
+var routineCommands []*cmdInfo
+
// addCommand replaces parser.addCommand() in a way that is compatible with
// re-constructing a pristine parser.
func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo {
@@ -131,6 +138,22 @@
return info
}
+// addRoutineCommand replaces parser.addCommand() in a way that is
+// compatible with re-constructing a pristine parser. It is meant for
+// adding "snap routine" commands.
+func addRoutineCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo {
+ info := &cmdInfo{
+ name: name,
+ shortHelp: shortHelp,
+ longHelp: longHelp,
+ builder: builder,
+ optDescs: optDescs,
+ argDescs: argDescs,
+ }
+ routineCommands = append(routineCommands, info)
+ return info
+}
+
type parserSetter interface {
setParser(*flags.Parser)
}
@@ -146,11 +169,7 @@
// decode the first rune instead of converting all of desc into []rune
r, _ := utf8.DecodeRuneInString(desc)
// note IsLower != !IsUpper for runes with no upper/lower.
- // Also note that login.u.c. is the only exception we're allowing for
- // now, but the list of exceptions could grow -- if it does, we might
- // want to change it to check for urlish things instead of just
- // login.u.c.
- if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") {
+ if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") && !strings.HasPrefix(desc, cmdName) {
noticef("description of %s's %q is lowercase: %q", cmdName, optName, desc)
}
}
@@ -206,32 +225,35 @@
return false
}
-// Parser creates and populates a fresh parser.
-// Since commands have local state a fresh parser is required to isolate tests
-// from each other.
-func Parser(cli *client.Client) *flags.Parser {
- optionsData.Version = func() {
- printVersions(cli)
- panic(&exitStatus{0})
- }
- flagopts := flags.Options(flags.PassDoubleDash)
- if firstNonOptionIsRun() {
- flagopts |= flags.PassAfterNonOption
+// noCompletion marks command descriptions of commands that should not
+// be completed
+var noCompletion = make(map[string]bool)
+
+func markForNoCompletion(ci *cmdInfo) {
+ if ci.hidden && !ci.completeHidden {
+ if ci.shortHelp == "" {
+ logger.Panicf("%q missing short help", ci.name)
+ }
+ noCompletion[ci.shortHelp] = true
}
- parser := flags.NewParser(&optionsData, flagopts)
- parser.ShortDescription = i18n.G("Tool to interact with snaps")
- parser.LongDescription = longSnapDescription
- // hide the unhelpful "[OPTIONS]" from help output
- parser.Usage = ""
- if version := parser.FindOptionByLongName("version"); version != nil {
- version.Description = i18n.G("Print the version and exit")
- version.Hidden = true
+}
+
+// completionHandler filters out unwanted completions based on
+// the noCompletion map before dumping them to stdout.
+func completionHandler(comps []flags.Completion) {
+ for _, comp := range comps {
+ if noCompletion[comp.Description] {
+ continue
+ }
+ fmt.Fprintln(Stdout, comp.Item)
}
- // add --help like what go-flags would do for us, but hidden
- addHelp(parser)
+}
- // Add all regular commands
+func registerCommands(cli *client.Client, parser *flags.Parser, baseCmd *flags.Command, commands []*cmdInfo, checkUnique func(*cmdInfo)) {
for _, c := range commands {
+ checkUnique(c)
+ markForNoCompletion(c)
+
obj := c.builder()
if x, ok := obj.(clientSetter); ok {
x.setClient(cli)
@@ -240,7 +262,7 @@
x.setParser(parser)
}
- cmd, err := parser.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj)
+ cmd, err := baseCmd.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj)
if err != nil {
logger.Panicf("cannot add command %q: %v", c.name, err)
}
@@ -287,6 +309,45 @@
c.extra(cmd)
}
}
+}
+
+// Parser creates and populates a fresh parser.
+// Since commands have local state a fresh parser is required to isolate tests
+// from each other.
+func Parser(cli *client.Client) *flags.Parser {
+ optionsData.Version = func() {
+ printVersions(cli)
+ panic(&exitStatus{0})
+ }
+ flagopts := flags.Options(flags.PassDoubleDash)
+ if firstNonOptionIsRun() {
+ flagopts |= flags.PassAfterNonOption
+ }
+ parser := flags.NewParser(&optionsData, flagopts)
+ parser.CompletionHandler = completionHandler
+ parser.ShortDescription = i18n.G("Tool to interact with snaps")
+ parser.LongDescription = longSnapDescription
+ // hide the unhelpful "[OPTIONS]" from help output
+ parser.Usage = ""
+ if version := parser.FindOptionByLongName("version"); version != nil {
+ version.Description = i18n.G("Print the version and exit")
+ version.Hidden = true
+ }
+ // add --help like what go-flags would do for us, but hidden
+ addHelp(parser)
+
+ seen := make(map[string]bool, len(commands)+len(debugCommands)+len(routineCommands))
+ checkUnique := func(ci *cmdInfo, kind string) {
+ if seen[ci.shortHelp] && ci.shortHelp != "Internal" && ci.shortHelp != "Deprecated (hidden)" {
+ logger.Panicf(`%scommand %q has an already employed description != "Internal"|"Deprecated (hidden)": %s`, kind, ci.name, ci.shortHelp)
+ }
+ seen[ci.shortHelp] = true
+ }
+
+ // Add all regular commands
+ registerCommands(cli, parser, parser.Command, commands, func(ci *cmdInfo) {
+ checkUnique(ci, "")
+ })
// Add the debug command
debugCommand, err := parser.AddCommand("debug", shortDebugHelp, longDebugHelp, &cmdDebug{})
debugCommand.Hidden = true
@@ -294,51 +355,19 @@
logger.Panicf("cannot add command %q: %v", "debug", err)
}
// Add all the sub-commands of the debug command
- for _, c := range debugCommands {
- obj := c.builder()
- if x, ok := obj.(clientSetter); ok {
- x.setClient(cli)
- }
- cmd, err := debugCommand.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj)
- if err != nil {
- logger.Panicf("cannot add debug command %q: %v", c.name, err)
- }
- cmd.Hidden = c.hidden
- opts := cmd.Options()
- if c.optDescs != nil && len(opts) != len(c.optDescs) {
- logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs))
- }
- for _, opt := range opts {
- name := opt.LongName
- if name == "" {
- name = string(opt.ShortName)
- }
- desc, ok := c.optDescs[name]
- if !(c.optDescs == nil || ok) {
- logger.Panicf("%s missing description for %s", c.name, name)
- }
- lintDesc(c.name, name, desc, opt.Description)
- if desc != "" {
- opt.Description = desc
- }
- }
-
- args := cmd.Args()
- if c.argDescs != nil && len(args) != len(c.argDescs) {
- logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs))
- }
- for i, arg := range args {
- name, desc := arg.Name, ""
- if c.argDescs != nil {
- name = c.argDescs[i].name
- desc = c.argDescs[i].desc
- }
- lintArg(c.name, name, desc, arg.Description)
- name = fixupArg(name)
- arg.Name = name
- arg.Description = desc
- }
+ registerCommands(cli, parser, debugCommand, debugCommands, func(ci *cmdInfo) {
+ checkUnique(ci, "debug ")
+ })
+ // Add the internal command
+ routineCommand, err := parser.AddCommand("routine", shortRoutineHelp, longRoutineHelp, &cmdRoutine{})
+ routineCommand.Hidden = true
+ if err != nil {
+ logger.Panicf("cannot add command %q: %v", "internal", err)
}
+ // Add all the sub-commands of the routine command
+ registerCommands(cli, parser, routineCommand, routineCommands, func(ci *cmdInfo) {
+ checkUnique(ci, "routine ")
+ })
return parser
}
@@ -355,7 +384,12 @@
// Client returns a new client using ClientConfig as configuration.
// commands should (in general) not use this, and instead use clientMixin.
func mkClient() *client.Client {
- cli := client.New(&ClientConfig)
+ cfg := &ClientConfig
+ // Set client user-agent when talking to the snapd daemon to the
+ // same value as when talking to the store.
+ cfg.UserAgent = snapdenv.UserAgent()
+
+ cli := client.New(cfg)
goos := runtime.GOOS
if release.OnWSL {
goos = "Windows Subsystem for Linux"
@@ -478,7 +512,7 @@
func run() error {
cli := mkClient()
parser := Parser(cli)
- _, err := parser.Parse()
+ xtra, err := parser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); ok {
switch e.Type {
@@ -489,7 +523,16 @@
parser.WriteHelp(Stdout)
return nil
case flags.ErrUnknownCommand:
- return fmt.Errorf(i18n.G(`unknown command %q, see 'snap help'`), os.Args[1])
+ sub := os.Args[1]
+ sug := "snap help"
+ if len(xtra) > 0 {
+ sub = xtra[0]
+ if x := parser.Command.Active; x != nil && x.Name != "help" {
+ sug = "snap help " + x.Name
+ }
+ }
+ // TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help '
+ return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), sub, sug)
}
}
diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/main_test.go snapd-2.45.1ubuntu0.2/cmd/snap/main_test.go
--- snapd-2.37.4ubuntu0.1/cmd/snap/main_test.go 2019-02-27 18:53:36.000000000 +0000
+++ snapd-2.45.1ubuntu0.2/cmd/snap/main_test.go 2020-06-05 13:13:49.000000000 +0000
@@ -31,16 +31,15 @@
"strings"
"testing"
- . "gopkg.in/check.v1"
-
+ "github.com/jessevdk/go-flags"
"golang.org/x/crypto/ssh/terminal"
+ . "gopkg.in/check.v1"
"github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
- snapdsnap "github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
snap "github.com/snapcore/snapd/cmd/snap"
@@ -67,6 +66,12 @@
s.BaseTest.SetUpTest(c)
dirs.SetRootDir(c.MkDir())
+ path := os.Getenv("PATH")
+ s.AddCleanup(func() {
+ os.Setenv("PATH", path)
+ })
+ os.Setenv("PATH", path+":"+dirs.SnapBinariesDir)
+
s.stdin = bytes.NewBuffer(nil)
s.stdout = bytes.NewBuffer(nil)
s.stderr = bytes.NewBuffer(nil)
@@ -79,8 +84,6 @@
s.AuthFile = filepath.Join(c.MkDir(), "json")
os.Setenv(TestAuthFileEnvKey, s.AuthFile)
- s.AddCleanup(snapdsnap.MockSanitizePlugsSlots(func(snapInfo *snapdsnap.Info) {}))
-
s.AddCleanup(interfaces.MockSystemKey(`
{
"build-id": "7a94e9736c091b3984bd63f5aebfc883c4d859e0",
@@ -261,7 +264,7 @@
defer restore()
err := snap.RunMain()
- c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see 'snap help'`)
+ c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see 'snap help'.`)
}
func (s *SnapSuite) TestResolveApp(c *C) {
@@ -364,6 +367,10 @@
}
c.Check(fn, PanicMatches, `option on "command" has no name`)
log.Reset()
+
+ snap.LintDesc("snap-advise", "from-apt", "snap-advise will run as a hook", "")
+ c.Check(log.String(), HasLen, 0)
+ log.Reset()
}
func (s *SnapSuite) TestLintArg(c *C) {
@@ -398,3 +405,29 @@
// Trailing ">s" is fixed to just >.
c.Check(snap.FixupArg("