summaryrefslogtreecommitdiff
diff options
-rw-r--r--client/apps.go1
-rw-r--r--client/apps_test.go16
-rw-r--r--client/packages.go1
-rw-r--r--client/packages_test.go8
-rw-r--r--cmd/Makefile.am12
-rw-r--r--cmd/snap/cmd_pack_test.go16
-rwxr-xr-xcmd/snapd-apparmor/snapd-apparmor97
-rw-r--r--daemon/api_test.go39
-rw-r--r--daemon/snap.go7
-rw-r--r--data/systemd/Makefile3
-rw-r--r--data/systemd/snapd.apparmor.service.in22
-rw-r--r--interfaces/builtin/joystick.go34
-rw-r--r--interfaces/builtin/joystick_test.go5
-rw-r--r--netutil/metered.go65
-rw-r--r--overlord/configstate/configcore/refresh.go14
-rw-r--r--overlord/configstate/configcore/refresh_test.go30
-rw-r--r--overlord/devicestate/devicestate.go4
-rw-r--r--overlord/devicestate/devicestate_test.go2
-rw-r--r--overlord/devicestate/export_test.go2
-rw-r--r--overlord/snapstate/autorefresh.go54
-rw-r--r--overlord/snapstate/autorefresh_test.go90
-rw-r--r--overlord/snapstate/export_test.go15
-rw-r--r--packaging/arch/PKGBUILD2
-rw-r--r--packaging/fedora/snapd.spec5
-rw-r--r--packaging/opensuse-42.2/snapd.spec5
-rwxr-xr-xpackaging/ubuntu-14.04/rules8
-rwxr-xr-xpackaging/ubuntu-16.04/rules8
-rw-r--r--snap/info.go4
-rw-r--r--snap/info_snap_yaml.go8
-rw-r--r--snap/info_snap_yaml_test.go26
-rw-r--r--snap/validate.go19
-rw-r--r--snap/validate_test.go42
-rw-r--r--store/details.go3
-rw-r--r--store/details_v2.go3
-rw-r--r--store/details_v2_test.go2
-rw-r--r--store/store_test.go44
-rwxr-xr-xtests/lib/snaps/test-snapd-appstreamid/bin/run3
-rw-r--r--tests/lib/snaps/test-snapd-appstreamid/snapcraft.yaml25
-rw-r--r--tests/main/appstream-id/task.yaml31
-rw-r--r--tests/unit/spread-shellcheck/must1
40 files changed, 747 insertions, 29 deletions
diff --git a/client/apps.go b/client/apps.go
index adebbd9569..06638732c6 100644
--- a/client/apps.go
+++ b/client/apps.go
@@ -39,6 +39,7 @@ type AppInfo struct {
Daemon string `json:"daemon,omitempty"`
Enabled bool `json:"enabled,omitempty"`
Active bool `json:"active,omitempty"`
+ CommonID string `json:"common-id,omitempty"`
}
// IsService returns true if the application is a background daemon.
diff --git a/client/apps_test.go b/client/apps_test.go
index deecb38dd6..617c6158db 100644
--- a/client/apps_test.go
+++ b/client/apps_test.go
@@ -87,6 +87,22 @@ func (cs *clientSuite) TestClientServiceGetSad(c *check.C) {
}
}
+func (cs *clientSuite) TestClientAppCommonID(c *check.C) {
+ expected := []*client.AppInfo{{
+ Snap: "foo",
+ Name: "foo",
+ CommonID: "org.foo",
+ }}
+ buf, err := json.Marshal(expected)
+ c.Assert(err, check.IsNil)
+ cs.rsp = fmt.Sprintf(`{"type": "sync", "result": %s}`, buf)
+ for _, chkr := range appcheckers {
+ actual, err := chkr(cs, c)
+ c.Assert(err, check.IsNil)
+ c.Check(actual, check.DeepEquals, expected)
+ }
+}
+
func testClientLogs(cs *clientSuite, c *check.C) ([]client.Log, error) {
ch, err := cs.cli.Logs([]string{"foo", "bar"}, client.LogOptions{N: -1, Follow: false})
c.Check(cs.req.URL.Path, check.Equals, "/v2/logs")
diff --git a/client/packages.go b/client/packages.go
index 51cd234be8..d9b9742fec 100644
--- a/client/packages.go
+++ b/client/packages.go
@@ -58,6 +58,7 @@ type Snap struct {
Broken string `json:"broken,omitempty"`
Contact string `json:"contact"`
License string `json:"license,omitempty"`
+ CommonIDs []string `json:"common-ids,omitempty"`
Prices map[string]float64 `json:"prices,omitempty"`
Screenshots []Screenshot `json:"screenshots,omitempty"`
diff --git a/client/packages_test.go b/client/packages_test.go
index 8bf7a8e19c..f80e987fe2 100644
--- a/client/packages_test.go
+++ b/client/packages_test.go
@@ -121,7 +121,8 @@ func (cs *clientSuite) TestClientSnaps(c *check.C) {
"type": "app",
"version": "1.0.18",
"confinement": "strict",
- "private": true
+ "private": true,
+ "common-ids": ["org.funky.snap"]
}],
"suggested-currency": "GBP"
}`
@@ -144,6 +145,7 @@ func (cs *clientSuite) TestClientSnaps(c *check.C) {
Confinement: client.StrictConfinement,
Private: true,
DevMode: false,
+ CommonIDs: []string{"org.funky.snap"},
}})
}
@@ -197,7 +199,8 @@ func (cs *clientSuite) TestClientSnap(c *check.C) {
"screenshots": [
{"url":"http://example.com/shot1.png", "width":640, "height":480},
{"url":"http://example.com/shot2.png"}
- ]
+ ],
+ "common-ids": ["org.funky.snap"]
}
}`
pkg, _, err := cs.cli.Snap(pkgName)
@@ -227,6 +230,7 @@ func (cs *clientSuite) TestClientSnap(c *check.C) {
{URL: "http://example.com/shot1.png", Width: 640, Height: 480},
{URL: "http://example.com/shot2.png"},
},
+ CommonIDs: []string{"org.funky.snap"},
})
}
diff --git a/cmd/Makefile.am b/cmd/Makefile.am
index 371d812731..3e55b1025a 100644
--- a/cmd/Makefile.am
+++ b/cmd/Makefile.am
@@ -463,3 +463,15 @@ libexec_PROGRAMS += snapd-generator/snapd-generator
snapd_generator_snapd_generator_SOURCES = snapd-generator/main.c
snapd_generator_snapd_generator_LDADD = libsnap-confine-private.a
+
+##
+## snapd-apparmor
+##
+
+EXTRA_DIST += snapd-apparmor/snapd-apparmor
+
+install-exec-local::
+ install -d -m 755 $(DESTDIR)$(libexecdir)
+if APPARMOR
+ install -m 755 $(srcdir)/snapd-apparmor/snapd-apparmor $(DESTDIR)$(libexecdir)
+endif
diff --git a/cmd/snap/cmd_pack_test.go b/cmd/snap/cmd_pack_test.go
index 208087d19a..dcc433d453 100644
--- a/cmd/snap/cmd_pack_test.go
+++ b/cmd/snap/cmd_pack_test.go
@@ -55,6 +55,22 @@ apps:
c.Assert(err, check.ErrorMatches, "snap name cannot be empty")
}
+func (s *SnapSuite) TestPackCheckSkeletonConflictingCommonID(c *check.C) {
+ // conflicting common-id
+ snapYaml := `name: foo
+version: foobar
+apps:
+ foo:
+ common-id: org.foo.foo
+ bar:
+ common-id: org.foo.foo
+`
+ snapDir := makeSnapDirForPack(c, snapYaml)
+
+ _, err := snaprun.Parser().ParseArgs([]string{"pack", "--check-skeleton", snapDir})
+ c.Assert(err, check.ErrorMatches, `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) TestPackPacksFailsForMissingPaths(c *check.C) {
_, r := logger.MockLogger()
defer r()
diff --git a/cmd/snapd-apparmor/snapd-apparmor b/cmd/snapd-apparmor/snapd-apparmor
new file mode 100755
index 0000000000..a793fe7655
--- /dev/null
+++ b/cmd/snapd-apparmor/snapd-apparmor
@@ -0,0 +1,97 @@
+#!/bin/sh
+# This script is provided for integration with systemd on distributions where
+# apparmor profiles generated and managed by snapd are not loaded by the
+# system-wide apparmor systemd integration on early boot-up.
+#
+# Only the start operation is provided as all other activity is managed by
+# snapd as a part of the life-cycle of particular snaps.
+#
+# In addition the script assumes that the system-wide apparmor service has
+# already executed, initializing apparmor file-systems as necessary.
+
+# NOTE: This script doesn't set -e as it contains code copied from apparmor
+# init script that also does not set it. In addition the intent is to simply
+# load application profiles, as many as we can, even if for whatever reason
+# some of those fail.
+
+# The following portion is copied from /lib/apparmor/functions as shipped by Ubuntu
+# <copied-code>
+
+SECURITYFS="/sys/kernel/security"
+export AA_SFS="$SECURITYFS/apparmor"
+
+
+# Checks to see if the current container is capable of having internal AppArmor
+# profiles that should be loaded. Callers of this function should have already
+# verified that they're running inside of a container environment with
+# something like `systemd-detect-virt --container`.
+#
+# The only known container environments capable of supporting internal policy
+# are LXD and LXC environment.
+#
+# Returns 0 if the container environment is capable of having its own internal
+# policy and non-zero otherwise.
+#
+# IMPORTANT: This function will return 0 in the case of a non-LXD/non-LXC
+# system container technology being nested inside of a LXD/LXC container that
+# utilized an AppArmor namespace and profile stacking. The reason 0 will be
+# returned is because .ns_stacked will be "yes" and .ns_name will still match
+# "lx[dc]-*" since the nested system container technology will not have set up
+# a new AppArmor profile namespace. This will result in the nested system
+# container's boot process to experience failed policy loads but the boot
+# process should continue without any loss of functionality. This is an
+# unsupported configuration that cannot be properly handled by this function.
+is_container_with_internal_policy() {
+ ns_stacked_path="${AA_SFS}/.ns_stacked"
+ ns_name_path="${AA_SFS}/.ns_name"
+ ns_stacked
+ ns_name
+
+ if ! [ -f "$ns_stacked_path" ] || ! [ -f "$ns_name_path" ]; then
+ return 1
+ fi
+
+ read -r ns_stacked < "$ns_stacked_path"
+ if [ "$ns_stacked" != "yes" ]; then
+ return 1
+ fi
+
+ # LXD and LXC set up AppArmor namespaces starting with "lxd-" and
+ # "lxc-", respectively. Return non-zero for all other namespace
+ # identifiers.
+ read -r ns_name < "$ns_name_path"
+ if [ "${ns_name#lxd-*}" = "$ns_name" ] && \
+ [ "${ns_name#lxc-*}" = "$ns_name" ]; then
+ return 1
+ fi
+
+ return 0
+}
+
+# This terminates code copied from /lib/apparmor/functions on Ubuntu
+# </copied-code>
+
+case "$1" in
+ start)
+ # <copied-code>
+ if [ -x /usr/bin/systemd-detect-virt ] && \
+ systemd-detect-virt --quiet --container && \
+ ! is_container_with_internal_policy; then
+ exit 0
+ fi
+ # </copied-code>
+
+ for profile in /var/lib/snapd/apparmor/profiles/*; do
+ # Filter out profiles with names ending with ~, those are temporary files created by snapd.
+ test "${profile%\~}" != "${profile}" && continue
+ echo "$profile"
+ done | xargs \
+ -P"$(getconf _NPROCESSORS_ONLN)" \
+ apparmor_parser \
+ --replace \
+ --write-cache \
+ --cache-loc=/var/cache/apparmor \
+ -O no-expr-simplify \
+ --quiet
+ ;;
+esac
diff --git a/daemon/api_test.go b/daemon/api_test.go
index 37376efd01..2dd2b6b5e3 100644
--- a/daemon/api_test.go
+++ b/daemon/api_test.go
@@ -505,6 +505,9 @@ apps:
command: some.cmd
cmd2:
command: other.cmd
+ cmd3:
+ command: other.cmd
+ common-id: org.foo.cmd
svc1:
command: somed1
daemon: simple
@@ -604,6 +607,10 @@ UnitFileState=potatoes
// no desktop file
Snap: "foo", Name: "cmd2",
}, {
+ // has AppStream ID
+ Snap: "foo", Name: "cmd3",
+ CommonID: "org.foo.cmd",
+ }, {
// services
Snap: "foo", Name: "svc1",
Daemon: "simple",
@@ -619,17 +626,17 @@ UnitFileState=potatoes
Daemon: "oneshot",
Enabled: true,
Active: true,
- },
- {
+ }, {
Snap: "foo", Name: "svc4",
Daemon: "notify",
Enabled: false,
Active: false,
},
},
- Broken: "",
- Contact: "",
- License: "GPL-3.0",
+ Broken: "",
+ Contact: "",
+ License: "GPL-3.0",
+ CommonIDs: []string{"org.foo.cmd"},
},
Meta: meta,
}
@@ -1633,6 +1640,28 @@ func (s *apiSuite) TestFindSection(c *check.C) {
})
}
+func (s *apiSuite) TestFindCommonID(c *check.C) {
+ s.daemon(c)
+
+ s.rsnaps = []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: "store",
+ },
+ Publisher: "foo",
+ CommonIDs: []string{"org.foo"},
+ }}
+ s.mockSnap(c, "name: store\nversion: 1.0")
+
+ req, err := http.NewRequest("GET", "/v2/find?name=foo", nil)
+ c.Assert(err, check.IsNil)
+
+ rsp := searchStore(findCmd, req, nil).(*resp)
+
+ snaps := snapList(rsp.Result)
+ c.Assert(snaps, check.HasLen, 1)
+ c.Check(snaps[0]["common-ids"], check.DeepEquals, []interface{}{"org.foo"})
+}
+
func (s *apiSuite) TestFindOne(c *check.C) {
s.daemon(c)
diff --git a/daemon/snap.go b/daemon/snap.go
index 5f2edd60b3..fa472bc5ec 100644
--- a/daemon/snap.go
+++ b/daemon/snap.go
@@ -271,8 +271,9 @@ func clientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) []client.AppInfo {
out := make([]client.AppInfo, len(apps))
for i, app := range apps {
out[i] = client.AppInfo{
- Snap: app.Snap.Name(),
- Name: app.Name,
+ Snap: app.Snap.Name(),
+ Name: app.Name,
+ CommonID: app.CommonID,
}
if fn := app.DesktopFile(); osutil.FileExists(fn) {
out[i].DesktopFile = fn
@@ -338,6 +339,7 @@ func mapLocal(about aboutSnap) *client.Snap {
Contact: localSnap.Contact,
Title: localSnap.Title(),
License: localSnap.License,
+ CommonIDs: localSnap.CommonIDs,
}
return result
@@ -385,6 +387,7 @@ func mapRemote(remoteSnap *snap.Info) *client.Snap {
Prices: remoteSnap.Prices,
Channels: remoteSnap.Channels,
Tracks: remoteSnap.Tracks,
+ CommonIDs: remoteSnap.CommonIDs,
}
return result
diff --git a/data/systemd/Makefile b/data/systemd/Makefile
index 460cfca473..0bcbb575bd 100644
--- a/data/systemd/Makefile
+++ b/data/systemd/Makefile
@@ -20,7 +20,8 @@ LIBEXECDIR := /usr/lib
SYSTEMDSYSTEMUNITDIR := /lib/systemd/system
SYSTEMD_UNITS_GENERATED := $(wildcard *.in)
-SYSTEMD_UNITS = $(SYSTEMD_UNITS_GENERATED:.in=) $(wildcard *.timer) $(wildcard *.socket)
+# NOTE: sort removes duplicates so this gives us all the units, generated or otherwise
+SYSTEMD_UNITS = $(sort $(SYSTEMD_UNITS_GENERATED:.in=) $(wildcard *.service) $(wildcard *.timer) $(wildcard *.socket))
.PHONY: all
all: $(SYSTEMD_UNITS)
diff --git a/data/systemd/snapd.apparmor.service.in b/data/systemd/snapd.apparmor.service.in
new file mode 100644
index 0000000000..5e450b287e
--- /dev/null
+++ b/data/systemd/snapd.apparmor.service.in
@@ -0,0 +1,22 @@
+# This systemd unit is needed on distributions that use apparmor but don't have
+# special support for loading snapd apparmor profiles. Until upstream apparmor
+# user-space release contains a systemd unit that is actually shipped by
+# distributors and that contains the necessary extension points for snapd the
+# apparmor profiles for snap applications need to be loaded separately from
+# other applications.
+[Unit]
+Description=Load AppArmor profiles managed internally by snapd
+DefaultDependencies=no
+Before=sysinit.target
+Requisite=snapd.service
+# This dependency is meant to ensure that apparmor initialization (whatever that might entail) is complete.
+After=apparmor.service
+ConditionSecurity=apparmor
+
+[Service]
+Type=oneshot
+ExecStart=@libexecdir@/snapd/snapd-apparmor start
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/interfaces/builtin/joystick.go b/interfaces/builtin/joystick.go
index 79e31ec9ad..8c688e9821 100644
--- a/interfaces/builtin/joystick.go
+++ b/interfaces/builtin/joystick.go
@@ -30,13 +30,33 @@ const joystickBaseDeclarationSlots = `
`
const joystickConnectedPlugAppArmor = `
-# Description: Allow reading and writing to joystick devices (/dev/input/js*).
+# Description: Allow reading and writing to joystick devices
+
+#
+# Old joystick interface
+#
# Per https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/devices.txt
# only js0-js31 is valid so limit the /dev and udev entries to those devices.
/dev/input/js{[0-9],[12][0-9],3[01]} rw,
/run/udev/data/c13:{[0-9],[12][0-9],3[01]} r,
+#
+# New evdev-joystick interface
+#
+
+# Per https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/devices.txt
+# the minor is 65 and up so limit udev to that.
+/run/udev/data/c13:{6[5-9],[7-9][0-9],[1-9][0-9][0-9]*} r,
+
+# /dev/input/event* is unfortunately not namespaced and includes all input
+# devices, including keyboards and mice, which allows input sniffing and
+# injection. Until we have inode tagging of devices, we use a glob rule here
+# and rely on udev tagging to only add evdev devices to the snap's device
+# cgroup that are marked with ENV{ID_INPUT_JOYSTICK}=="1". As such, even though
+# AppArmor allows all evdev, the device cgroup does not.
+/dev/input/event[0-9]* rw,
+
# Allow reading for supported event reports for all input devices. See
# https://www.kernel.org/doc/Documentation/input/event-codes.txt
# FIXME: this is a very minor information leak and snapd should instead query
@@ -44,7 +64,17 @@ const joystickConnectedPlugAppArmor = `
/sys/devices/**/input[0-9]*/capabilities/* r,
`
-var joystickConnectedPlugUDev = []string{`KERNEL=="js[0-9]*"`}
+// Add the old joystick device (js*) and any evdev input interfaces which are
+// marked as joysticks. Note, some input devices are known to come up as
+// joysticks when they are not and while this rule would tag them, on systems
+// where this is happening the device is non-functional for its intended
+// purpose. In other words, in practice, users with such devices will have
+// updated their udev rules to set ENV{ID_INPUT_JOYSTICK}="" to make it work,
+// which means this rule will no longer match.
+var joystickConnectedPlugUDev = []string{
+ `KERNEL=="js[0-9]*"`,
+ `KERNEL=="event[0-9]*", SUBSYSTEM=="input", ENV{ID_INPUT_JOYSTICK}=="1"`,
+}
func init() {
registerIface(&commonInterface{
diff --git a/interfaces/builtin/joystick_test.go b/interfaces/builtin/joystick_test.go
index 304a03d420..9d31a878b5 100644
--- a/interfaces/builtin/joystick_test.go
+++ b/interfaces/builtin/joystick_test.go
@@ -85,14 +85,17 @@ func (s *JoystickInterfaceSuite) TestAppArmorSpec(c *C) {
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/input/js{[0-9],[12][0-9],3[01]} rw,`)
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/run/udev/data/c13:{6[5-9],[7-9][0-9],[1-9][0-9][0-9]*} r,`)
}
func (s *JoystickInterfaceSuite) TestUDevSpec(c *C) {
spec := &udev.Specification{}
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
- c.Assert(spec.Snippets(), HasLen, 2)
+ c.Assert(spec.Snippets(), HasLen, 3)
c.Assert(spec.Snippets(), testutil.Contains, `# joystick
KERNEL=="js[0-9]*", TAG+="snap_consumer_app"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `# joystick
+KERNEL=="event[0-9]*", SUBSYSTEM=="input", ENV{ID_INPUT_JOYSTICK}=="1", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_consumer_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`)
}
diff --git a/netutil/metered.go b/netutil/metered.go
new file mode 100644
index 0000000000..43d009d17b
--- /dev/null
+++ b/netutil/metered.go
@@ -0,0 +1,65 @@
+// -*- 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package netutil
+
+import (
+ "fmt"
+
+ "github.com/godbus/dbus"
+
+ "github.com/snapcore/snapd/logger"
+)
+
+const (
+ // https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMMetered
+ NetworkManagerMeteredUnknown = 0
+ NetworkManagerMeteredYes = 1
+ NetworkManagerMeteredNo = 2
+ NetworkManagerMeteredGuessYes = 3
+ NetworkManagerMeteredGuessNo = 4
+)
+
+// IsOnMeteredConnection checks whether the current default network connection
+// is metered. If the state can not be determined, returns false and an error.
+func IsOnMeteredConnection() (bool, error) {
+ // obtain a shared connection to system bus, no need to close it
+ conn, err := dbus.SystemBus()
+ if err != nil {
+ return false, fmt.Errorf("cannot connect to system bus: %v", err)
+ }
+
+ return isNMOnMetered(conn)
+}
+
+func isNMOnMetered(conn *dbus.Conn) (bool, error) {
+ nmObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
+ // https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html
+ dbusV, err := nmObj.GetProperty("org.freedesktop.NetworkManager.Metered")
+ if err != nil {
+ return false, err
+ }
+ v, ok := dbusV.Value().(uint32)
+ if !ok {
+ return false, fmt.Errorf("network manager returned invalid value for metering verification: %s", dbusV)
+ }
+ logger.Debugf("metered state reported by NetworkManager: %s", dbusV)
+
+ return v == NetworkManagerMeteredGuessYes || v == NetworkManagerMeteredYes, nil
+}
diff --git a/overlord/configstate/configcore/refresh.go b/overlord/configstate/configcore/refresh.go
index 893d5e77b0..a00f3c3403 100644
--- a/overlord/configstate/configcore/refresh.go
+++ b/overlord/configstate/configcore/refresh.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2017-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
@@ -31,6 +31,7 @@ func init() {
supportedConfigurations["core.refresh.hold"] = true
supportedConfigurations["core.refresh.schedule"] = true
supportedConfigurations["core.refresh.timer"] = true
+ supportedConfigurations["core.refresh.metered"] = true
}
func validateRefreshSchedule(tr Conf) error {
@@ -56,6 +57,17 @@ func validateRefreshSchedule(tr Conf) error {
}
}
+ refreshOnMeteredStr, err := coreCfg(tr, "refresh.metered")
+ if err != nil {
+ return err
+ }
+ switch refreshOnMeteredStr {
+ case "", "hold":
+ // noop
+ default:
+ return fmt.Errorf("refresh.metered value %q is invalid", refreshOnMeteredStr)
+ }
+
refreshScheduleStr, err := coreCfg(tr, "refresh.schedule")
if err != nil {
return err
diff --git a/overlord/configstate/configcore/refresh_test.go b/overlord/configstate/configcore/refresh_test.go
index d8456aa483..cb30cbc481 100644
--- a/overlord/configstate/configcore/refresh_test.go
+++ b/overlord/configstate/configcore/refresh_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2017-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
@@ -99,3 +99,31 @@ func (s *refreshSuite) TestConfigureRefreshHoldInvalid(c *C) {
})
c.Assert(err, ErrorMatches, `refresh\.hold cannot be parsed:.*`)
}
+
+func (s *refreshSuite) TestConfigureRefreshHoldOnMeteredInvalid(c *C) {
+ err := configcore.Run(&mockConf{
+ state: s.state,
+ conf: map[string]interface{}{
+ "refresh.metered": "invalid",
+ },
+ })
+ c.Assert(err, ErrorMatches, `refresh\.metered value "invalid" is invalid`)
+}
+
+func (s *refreshSuite) TestConfigureRefreshHoldOnMeteredHappy(c *C) {
+ err := configcore.Run(&mockConf{
+ state: s.state,
+ conf: map[string]interface{}{
+ "refresh.metered": "hold",
+ },
+ })
+ c.Assert(err, IsNil)
+
+ err = configcore.Run(&mockConf{
+ state: s.state,
+ conf: map[string]interface{}{
+ "refresh.metered": "",
+ },
+ })
+ c.Assert(err, IsNil)
+}
diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go
index 077ffa0060..9b10f2da39 100644
--- a/overlord/devicestate/devicestate.go
+++ b/overlord/devicestate/devicestate.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-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
@@ -27,6 +27,7 @@ import (
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/netutil"
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/configstate/config"
@@ -199,6 +200,7 @@ func delayedCrossMgrInit() {
})
snapstate.CanAutoRefresh = canAutoRefresh
snapstate.CanManageRefreshes = CanManageRefreshes
+ snapstate.IsOnMeteredConnection = netutil.IsOnMeteredConnection
}
// ProxyStore returns the store assertion for the proxy store if one is set.
diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go
index 58b46eead6..bec19018a0 100644
--- a/overlord/devicestate/devicestate_test.go
+++ b/overlord/devicestate/devicestate_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-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
diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go
index 8308fc8adb..ae4700ac39 100644
--- a/overlord/devicestate/export_test.go
+++ b/overlord/devicestate/export_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-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
diff --git a/overlord/snapstate/autorefresh.go b/overlord/snapstate/autorefresh.go
index 3b7a31c31e..66c71eae51 100644
--- a/overlord/snapstate/autorefresh.go
+++ b/overlord/snapstate/autorefresh.go
@@ -43,8 +43,9 @@ const maxPostponement = 60 * 24 * time.Hour
// hooks setup by devicestate
var (
- CanAutoRefresh func(st *state.State) (bool, error)
- CanManageRefreshes func(st *state.State) bool
+ CanAutoRefresh func(st *state.State) (bool, error)
+ CanManageRefreshes func(st *state.State) bool
+ IsOnMeteredConnection func() (bool, error)
)
// refreshRetryDelay specified the minimum time to retry failed refreshes
@@ -149,6 +150,44 @@ func (m *autoRefresh) AtSeed() error {
return nil
}
+func canRefreshOnMeteredConnection(st *state.State) (bool, error) {
+ tr := config.NewTransaction(st)
+ var onMetered string
+ err := tr.GetMaybe("core", "refresh.metered", &onMetered)
+ if err != nil && err != state.ErrNoState {
+ return false, err
+ }
+
+ return onMetered != "hold", nil
+}
+
+func (m *autoRefresh) canRefreshRespectingMetered(now, lastRefresh time.Time) (can bool, err error) {
+ can, err = canRefreshOnMeteredConnection(m.state)
+ if err != nil {
+ return false, err
+ }
+ if can {
+ return true, nil
+ }
+
+ // ignore any errors that occurred while checking if we are on a metered
+ // connection
+ metered, _ := IsOnMeteredConnection()
+ if !metered {
+ return true, nil
+ }
+
+ if now.Sub(lastRefresh) >= maxPostponement {
+ // TODO use warnings when the infra becomes available
+ logger.Noticef("Auto refresh disabled while on metered connections, but pending for too long (%s days). Trying to refresh now.", int(maxPostponement.Hours()/24))
+ return true, nil
+ }
+
+ logger.Debugf("Auto refresh disabled on metered connections")
+
+ return false, nil
+}
+
// Ensure ensures that we refresh all installed snaps periodically
func (m *autoRefresh) Ensure() error {
m.state.Lock()
@@ -237,6 +276,17 @@ func (m *autoRefresh) Ensure() error {
// do refresh attempt (if needed)
if !m.nextRefresh.After(now) {
+ var can bool
+ can, err = m.canRefreshRespectingMetered(now, lastRefresh)
+ if err != nil {
+ return err
+ }
+ if !can {
+ // clear nextRefresh so that another refresh time is calculated
+ m.nextRefresh = time.Time{}
+ return nil
+ }
+
err = m.launchAutoRefresh()
// clear nextRefresh only if the refresh worked. There is
// still the lastRefreshAttempt rate limit so things will
diff --git a/overlord/snapstate/autorefresh_test.go b/overlord/snapstate/autorefresh_test.go
index 00b50384a7..3262694d1e 100644
--- a/overlord/snapstate/autorefresh_test.go
+++ b/overlord/snapstate/autorefresh_test.go
@@ -106,6 +106,7 @@ func (s *autoRefreshTestSuite) SetUpTest(c *C) {
snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
return nil, nil
}
+ snapstate.IsOnMeteredConnection = func() (bool, error) { return false, nil }
s.state.Set("seed-time", time.Now())
}
@@ -452,3 +453,92 @@ func (s *autoRefreshTestSuite) TestAtSeedPolicy(c *C) {
c.Check(err, IsNil)
c.Check(t1.Equal(t2), Equals, true)
}
+
+func (s *autoRefreshTestSuite) TestCanRefreshOnMetered(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ can, err := snapstate.CanRefreshOnMeteredConnection(s.state)
+ c.Assert(can, Equals, true)
+ c.Assert(err, Equals, nil)
+
+ // enable holding refreshes when on metered connection
+ tr := config.NewTransaction(s.state)
+ err = tr.Set("core", "refresh.metered", "hold")
+ c.Assert(err, IsNil)
+ tr.Commit()
+
+ can, err = snapstate.CanRefreshOnMeteredConnection(s.state)
+ c.Assert(can, Equals, false)
+ c.Assert(err, Equals, nil)
+
+ // explicitly disable holding refreshes when on metered connection
+ tr = config.NewTransaction(s.state)
+ err = tr.Set("core", "refresh.metered", "")
+ c.Assert(err, IsNil)
+ tr.Commit()
+
+ can, err = snapstate.CanRefreshOnMeteredConnection(s.state)
+ c.Assert(can, Equals, true)
+ c.Assert(err, Equals, nil)
+}
+
+func (s *autoRefreshTestSuite) TestRefreshOnMeteredConnIsMetered(c *C) {
+ // pretend we're on metered connection
+ revert := snapstate.MockIsOnMeteredConnection(func() (bool, error) {
+ return true, nil
+ })
+ defer revert()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "refresh.metered", "hold")
+ tr.Commit()
+
+ af := snapstate.NewAutoRefresh(s.state)
+
+ s.state.Set("last-refresh", time.Now().Add(-5*24*time.Hour))
+ s.state.Unlock()
+ err := af.Ensure()
+ s.state.Lock()
+ c.Check(err, IsNil)
+ // no refresh
+ c.Check(s.store.ops, HasLen, 0)
+
+ c.Check(af.NextRefresh(), DeepEquals, time.Time{})
+
+ // last refresh over 60 days ago, new one is launched regardless of
+ // connection being metered
+ s.state.Set("last-refresh", time.Now().Add(-61*24*time.Hour))
+ s.state.Unlock()
+ err = af.Ensure()
+ s.state.Lock()
+ c.Check(err, IsNil)
+ c.Check(s.store.ops, DeepEquals, []string{"list-refresh"})
+}
+
+func (s *autoRefreshTestSuite) TestRefreshOnMeteredConnNotMetered(c *C) {
+ // pretend we're on non-metered connection
+ revert := snapstate.MockIsOnMeteredConnection(func() (bool, error) {
+ return false, nil
+ })
+ defer revert()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "refresh.metered", "hold")
+ tr.Commit()
+
+ af := snapstate.NewAutoRefresh(s.state)
+
+ s.state.Set("last-refresh", time.Now().Add(-5*24*time.Hour))
+ s.state.Unlock()
+ err := af.Ensure()
+ s.state.Lock()
+ c.Check(err, IsNil)
+ c.Check(s.store.ops, DeepEquals, []string{"list-refresh"})
+}
diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go
index 4ced725e4c..6980cdff7d 100644
--- a/overlord/snapstate/export_test.go
+++ b/overlord/snapstate/export_test.go
@@ -155,9 +155,10 @@ var (
// refreshes
var (
- NewAutoRefresh = newAutoRefresh
- NewRefreshHints = newRefreshHints
- NewCatalogRefresh = newCatalogRefresh
+ NewAutoRefresh = newAutoRefresh
+ NewRefreshHints = newRefreshHints
+ NewCatalogRefresh = newCatalogRefresh
+ CanRefreshOnMeteredConnection = canRefreshOnMeteredConnection
)
func MockNextRefresh(ar *autoRefresh, when time.Time) {
@@ -180,6 +181,14 @@ func MockRefreshRetryDelay(d time.Duration) func() {
}
}
+func MockIsOnMeteredConnection(mock func() (bool, error)) func() {
+ old := IsOnMeteredConnection
+ IsOnMeteredConnection = mock
+ return func() {
+ IsOnMeteredConnection = old
+ }
+}
+
func ByKindOrder(snaps ...*snap.Info) []*snap.Info {
sort.Sort(byKind(snaps))
return snaps
diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD
index 73071c032e..ef924e1da4 100644
--- a/packaging/arch/PKGBUILD
+++ b/packaging/arch/PKGBUILD
@@ -181,10 +181,12 @@ package() {
# Remove snappy core specific units
rm -fv "$pkgdir/usr/lib/systemd/system/snapd.system-shutdown.service"
rm -fv "$pkgdir/usr/lib/systemd/system/snapd.autoimport.service"
+ rm -fv "$pkgdir/usr/lib/systemd/system/snapd.apparmor.service"
rm -fv "$pkgdir"/usr/lib/systemd/system/snapd.snap-repair.*
rm -fv "$pkgdir"/usr/lib/systemd/system/snapd.core-fixup.*
# and scripts
rm -fv "$pkgdir/usr/lib/snapd/snapd.core-fixup.sh"
rm -fv "$pkgdir/usr/bin/ubuntu-core-launcher"
rm -fv "$pkgdir/usr/lib/snapd/system-shutdown"
+ rm -fv "$pkgdir/usr/lib/snapd/snapd-apparmor"
}
diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec
index ef50be4689..c523cb574c 100644
--- a/packaging/fedora/snapd.spec
+++ b/packaging/fedora/snapd.spec
@@ -537,6 +537,10 @@ popd
# Remove snappy core specific scripts
rm %{buildroot}%{_libexecdir}/snapd/snapd.core-fixup.sh
+# Remove snapd apparmor service
+rm -f %{buildroot}%{_unitdir}/snapd.apparmor.service
+rm -f %{buildroot}%{_libexecdir}/snapd/snapd-apparmor
+
# Install Polkit configuration
install -m 644 -D data/polkit/io.snapcraft.snapd.policy %{buildroot}%{_datadir}/polkit-1/actions
@@ -616,6 +620,7 @@ popd
%{_sysconfdir}/profile.d/snapd.sh
%{_unitdir}/snapd.socket
%{_unitdir}/snapd.service
+%{_unitdir}/snapd.seeded.service
%{_unitdir}/snapd.autoimport.service
%{_unitdir}/snapd.seeded.service
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
diff --git a/packaging/opensuse-42.2/snapd.spec b/packaging/opensuse-42.2/snapd.spec
index cbe26efe7c..834758480b 100644
--- a/packaging/opensuse-42.2/snapd.spec
+++ b/packaging/opensuse-42.2/snapd.spec
@@ -235,6 +235,11 @@ install -m 644 -D data/completion/etelpmoc.sh %{buildroot}%{_libexecdir}/snapd
install -m 755 -d %{buildroot}/lib/systemd/system-generators/
mv %{buildroot}%{_libexecdir}/snapd/snapd-generator %{buildroot}/lib/systemd/system-generators/
+# On openSUSE Leap 42.* (and perhaps 15 as well, untested) the apparmor stack is too old
+# so don't ship apparmor helper service.
+rm -f %{?buildroot}%{_unitdir}/snapd.apparmor.service
+rm -f %{?buildroot}%{_libexecdir}/snapd/snapd-apparmor
+
%verifyscript
%verify_permissions -e %{_libexecdir}/snapd/snap-confine
diff --git a/packaging/ubuntu-14.04/rules b/packaging/ubuntu-14.04/rules
index 62d5b252f7..7242771ae4 100755
--- a/packaging/ubuntu-14.04/rules
+++ b/packaging/ubuntu-14.04/rules
@@ -179,12 +179,12 @@ override_dh_install:
cp -R share/locale debian/snapd/usr/share; \
fi
- # install snapd's systemd units / upstart jobs, done
+ # Install snapd's systemd units / upstart jobs, done
# here instead of debian/snapd.install because the
# ubuntu/14.04 release branch adds/changes bits here
$(MAKE) -C data install DESTDIR=$(CURDIR)/debian/snapd/ \
SYSTEMDSYSTEMUNITDIR=$(SYSTEMD_UNITS_DESTDIR)
- # we called this apps-bin-path.sh instead of snapd.sh, and
+ # We called this apps-bin-path.sh instead of snapd.sh, and
# it's a conf file so we're stuck with it
mv debian/snapd/etc/profile.d/snapd.sh debian/snapd/etc/profile.d/apps-bin-path.sh
@@ -195,6 +195,10 @@ override_dh_install:
# trusty doesn't need the .real workaround
+ # On Ubuntu and Debian we don't need to install the apparmor helper service.
+ rm -f $(CURDIR)/debian/tmp/$(SYSTEMD_UNITS_DESTDIR)/snapd.apparmor.service
+ rm -f $(CURDIR)/debian/tmp/usr/lib/snapd/snapd-apparmor
+
dh_install
override_dh_auto_install: snap.8
diff --git a/packaging/ubuntu-16.04/rules b/packaging/ubuntu-16.04/rules
index 9dcf5f96ae..3bd8f71f02 100755
--- a/packaging/ubuntu-16.04/rules
+++ b/packaging/ubuntu-16.04/rules
@@ -207,12 +207,12 @@ override_dh_install:
cp -R share/locale debian/snapd/usr/share; \
fi
- # install snapd's systemd units / upstart jobs, done
+ # Install snapd's systemd units / upstart jobs, done
# here instead of debian/snapd.install because the
# ubuntu/14.04 release branch adds/changes bits here
$(MAKE) -C data install DESTDIR=$(CURDIR)/debian/snapd/ \
SYSTEMDSYSTEMUNITDIR=$(SYSTEMD_UNITS_DESTDIR)
- # we called this apps-bin-path.sh instead of snapd.sh, and
+ # We called this apps-bin-path.sh instead of snapd.sh, and
# it's a conf file so we're stuck with it
mv debian/snapd/etc/profile.d/snapd.sh debian/snapd/etc/profile.d/apps-bin-path.sh
@@ -221,6 +221,10 @@ override_dh_install:
# Rename the apparmor profile, see dh_apparmor call above for an explanation.
mv $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine.real
+ # On Ubuntu and Debian we don't need to install the apparmor helper service.
+ rm -f $(CURDIR)/debian/tmp/$(SYSTEMD_UNITS_DESTDIR)/snapd.apparmor.service
+ rm -f $(CURDIR)/debian/tmp/usr/lib/snapd/snapd-apparmor
+
dh_install
override_dh_auto_install: snap.8
diff --git a/snap/info.go b/snap/info.go
index f6053f1927..00ff5d3643 100644
--- a/snap/info.go
+++ b/snap/info.go
@@ -195,6 +195,9 @@ type Info struct {
Tracks []string
Layout map[string]*Layout
+
+ // The list of common-ids from all apps of the snap
+ CommonIDs []string
}
// Layout describes a single element of the layout section.
@@ -621,6 +624,7 @@ type AppInfo struct {
Name string
LegacyAliases []string // FIXME: eventually drop this
Command string
+ CommonID string
Daemon string
StopTimeout timeout.Timeout
diff --git a/snap/info_snap_yaml.go b/snap/info_snap_yaml.go
index 099be2da63..74a9ca81b9 100644
--- a/snap/info_snap_yaml.go
+++ b/snap/info_snap_yaml.go
@@ -75,7 +75,8 @@ type appYaml struct {
SlotNames []string `yaml:"slots,omitempty"`
PlugNames []string `yaml:"plugs,omitempty"`
- BusName string `yaml:"bus-name,omitempty"`
+ BusName string `yaml:"bus-name,omitempty"`
+ CommonID string `yaml:"common-id,omitempty"`
Environment strutil.OrderedMap `yaml:"environment,omitempty"`
@@ -299,6 +300,7 @@ func setAppsFromSnapYaml(y snapYaml, snap *Info) error {
PostStopCommand: yApp.PostStopCommand,
RestartCond: yApp.RestartCond,
BusName: yApp.BusName,
+ CommonID: yApp.CommonID,
Environment: yApp.Environment,
Completer: yApp.Completer,
StopMode: yApp.StopMode,
@@ -369,6 +371,10 @@ func setAppsFromSnapYaml(y snapYaml, snap *Info) error {
Timer: yApp.Timer,
}
}
+ // collect all common IDs
+ if app.CommonID != "" {
+ snap.CommonIDs = append(snap.CommonIDs, app.CommonID)
+ }
}
return nil
}
diff --git a/snap/info_snap_yaml_test.go b/snap/info_snap_yaml_test.go
index 8d276dce96..138ccd9d08 100644
--- a/snap/info_snap_yaml_test.go
+++ b/snap/info_snap_yaml_test.go
@@ -1711,3 +1711,29 @@ apps:
app = info.Apps["foo"]
c.Check(app.Autostart, Equals, "")
}
+
+func (s *YamlSuite) TestSnapYamlAppCommonID(c *C) {
+ yAutostart := []byte(`name: wat
+version: 42
+apps:
+ foo:
+ command: bin/foo
+ common-id: org.foo
+ bar:
+ command: bin/foo
+ common-id: org.bar
+ baz:
+ command: bin/foo
+
+`)
+ info, err := snap.InfoFromSnapYaml(yAutostart)
+ c.Assert(err, IsNil)
+ c.Check(info.Apps["foo"].CommonID, Equals, "org.foo")
+ c.Check(info.Apps["bar"].CommonID, Equals, "org.bar")
+ c.Check(info.Apps["baz"].CommonID, Equals, "")
+ c.Assert(info.CommonIDs, HasLen, 2)
+ c.Assert((info.CommonIDs[0] == "org.foo" && info.CommonIDs[1] == "org.bar") ||
+ (info.CommonIDs[1] == "org.foo" && info.CommonIDs[0] == "org.bar"),
+ Equals,
+ true)
+}
diff --git a/snap/validate.go b/snap/validate.go
index 15902e9615..292ba9d8b1 100644
--- a/snap/validate.go
+++ b/snap/validate.go
@@ -316,6 +316,11 @@ func Validate(info *Info) error {
}
}
+ // ensure that common-id(s) are unique
+ if err := ValidateCommonIDs(info); err != nil {
+ return err
+ }
+
return ValidateLayoutAll(info)
}
@@ -781,3 +786,17 @@ func ValidateLayout(layout *Layout, constraints []LayoutConstraint) error {
}
return nil
}
+
+func ValidateCommonIDs(info *Info) error {
+ seen := make(map[string]string, len(info.Apps))
+ for _, app := range info.Apps {
+ if app.CommonID != "" {
+ if other, was := seen[app.CommonID]; was {
+ return fmt.Errorf("application %q common-id %q must be unique, already used by application %q",
+ app.Name, app.CommonID, other)
+ }
+ seen[app.CommonID] = app.Name
+ }
+ }
+ return nil
+}
diff --git a/snap/validate_test.go b/snap/validate_test.go
index cb04232f3e..6a178da9b9 100644
--- a/snap/validate_test.go
+++ b/snap/validate_test.go
@@ -1274,3 +1274,45 @@ base: bar
err = Validate(info)
c.Check(err, ErrorMatches, `cannot have "base" field on "base" snap "foo"`)
}
+
+func (s *ValidateSuite) TestValidateCommonIDs(c *C) {
+ meta := `
+name: foo
+version: 1.0
+`
+ good := meta + `
+apps:
+ foo:
+ common-id: org.foo.foo
+ bar:
+ common-id: org.foo.bar
+ baz:
+`
+ bad := meta + `
+apps:
+ foo:
+ common-id: org.foo.foo
+ bar:
+ common-id: org.foo.foo
+ baz:
+`
+ for i, tc := range []struct {
+ meta string
+ err string
+ }{
+ {good, ""},
+ {bad, `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")`},
+ } {
+ c.Logf("tc #%v", i)
+ info, err := InfoFromSnapYaml([]byte(tc.meta))
+ c.Assert(err, IsNil)
+
+ err = Validate(info)
+ if tc.err == "" {
+ c.Assert(err, IsNil)
+ } else {
+ c.Assert(err, NotNil)
+ c.Check(err, ErrorMatches, tc.err)
+ }
+ }
+}
diff --git a/store/details.go b/store/details.go
index 5ef2229db2..ae590fb063 100644
--- a/store/details.go
+++ b/store/details.go
@@ -71,6 +71,8 @@ type snapDetails struct {
Confinement string `json:"confinement"`
ChannelMapList []channelMap `json:"channel_maps_list,omitempty"`
+
+ CommonIDs []string `json:"common_ids,omitempty"`
}
// channelMap contains
@@ -136,6 +138,7 @@ func infoFromRemote(d *snapDetails) *snap.Info {
info.Contact = d.Contact
info.License = d.License
info.Base = d.Base
+ info.CommonIDs = d.CommonIDs
deltas := make([]snap.DeltaInfo, len(d.Deltas))
for i, d := range d.Deltas {
diff --git a/store/details_v2.go b/store/details_v2.go
index 27899e5e8b..3e51913147 100644
--- a/store/details_v2.go
+++ b/store/details_v2.go
@@ -54,6 +54,8 @@ type storeSnap struct {
// media
Media []storeSnapMedia `json:"media"`
+
+ CommonIDs []string `json:"common-ids"`
}
type storeSnapDownload struct {
@@ -121,6 +123,7 @@ func infoFromStoreSnap(d *storeSnap) (*snap.Info, error) {
}
info.Deltas = deltas
}
+ info.CommonIDs = d.CommonIDs
// fill in the plug/slot data
if rawYamlInfo, err := snap.InfoFromSnapYaml([]byte(d.SnapYAML)); err == nil {
diff --git a/store/details_v2_test.go b/store/details_v2_test.go
index 7389762189..4631fc8b35 100644
--- a/store/details_v2_test.go
+++ b/store/details_v2_test.go
@@ -82,6 +82,7 @@ const (
"base": "base-18",
"confinement": "strict",
"contact": "https://thingy.com",
+ "common-ids": ["org.thingy"],
"created-at": "2018-01-26T11:38:35.536410+00:00",
"description": "Useful thingy for thinging",
"download": {
@@ -235,6 +236,7 @@ func (s *detailsV2Suite) TestInfoFromStoreSnap(c *C) {
{URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"},
{URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", Width: 600, Height: 200},
},
+ CommonIDs: []string{"org.thingy"},
})
// validate the plugs/slots
diff --git a/store/store_test.go b/store/store_test.go
index e01d468a98..50e3dce2e5 100644
--- a/store/store_test.go
+++ b/store/store_test.go
@@ -2621,7 +2621,7 @@ func (s *storeTestSuite) TestNoDetails(c *C) {
}
/* acquired via:
-curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Device-Channel: edge" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://api.snapcraft.io/api/v1/snaps/search?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&q=hello' | python -m json.tool | xsel -b
+curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Device-Channel: edge" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://api.snapcraft.io/api/v1/snaps/search?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin%2Ccommon_ids&q=hello' | python -m json.tool | xsel -b
Screenshot URLS set manually.
*/
const MockSearchJSON = `{
@@ -2634,6 +2634,7 @@ const MockSearchJSON = `{
],
"binary_filesize": 20480,
"channel": "edge",
+ "common_ids": [],
"content": "application",
"description": "This is a simple hello world example.",
"download_sha512": "4bf23ce93efa1f32f0aeae7ec92564b7b0f9f8253a0bd39b2741219c1be119bb676c21208c6845ccf995e6aabe791d3f28a733ebcbbc3171bb23f67981f4068e",
@@ -3133,6 +3134,47 @@ func (s *storeTestSuite) TestFindAuthFailed(c *C) {
c.Check(snaps[0].MustBuy, Equals, true)
}
+func (s *storeTestSuite) TestFindCommonIDs(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assertRequest(c, r, "GET", searchPath)
+ query := r.URL.Query()
+
+ name := query.Get("name")
+ q := query.Get("q")
+
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, Matches, ".*/search")
+ c.Check(name, Equals, "")
+ c.Check(q, Equals, "foo")
+ default:
+ c.Fatalf("what? %d", n)
+ }
+
+ w.Header().Set("Content-Type", "application/hal+json")
+ w.WriteHeader(200)
+ io.WriteString(w, strings.Replace(MockSearchJSON,
+ `"common_ids": []`,
+ `"common_ids": ["org.hello"]`, -1))
+
+ n++
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ serverURL, _ := url.Parse(mockServer.URL)
+ cfg := Config{
+ StoreBaseURL: serverURL,
+ }
+ sto := New(&cfg, nil)
+
+ infos, err := sto.Find(&Search{Query: "foo"}, nil)
+ c.Check(err, IsNil)
+ c.Assert(infos, HasLen, 1)
+ c.Check(infos[0].CommonIDs, DeepEquals, []string{"org.hello"})
+}
+
func (s *storeTestSuite) TestCurrentSnap(c *C) {
cand := &RefreshCandidate{
SnapID: helloWorldSnapID,
diff --git a/tests/lib/snaps/test-snapd-appstreamid/bin/run b/tests/lib/snaps/test-snapd-appstreamid/bin/run
new file mode 100755
index 0000000000..5ae1c70628
--- /dev/null
+++ b/tests/lib/snaps/test-snapd-appstreamid/bin/run
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+exec "$@" \ No newline at end of file
diff --git a/tests/lib/snaps/test-snapd-appstreamid/snapcraft.yaml b/tests/lib/snaps/test-snapd-appstreamid/snapcraft.yaml
new file mode 100644
index 0000000000..e9ecf0c4ab
--- /dev/null
+++ b/tests/lib/snaps/test-snapd-appstreamid/snapcraft.yaml
@@ -0,0 +1,25 @@
+name: test-snapd-appstreamid
+version: 1.0
+summary: Snap for testing snapd AppStream ID support
+description: |
+ Snap for testing snapd AppStream ID support
+confinement: strict
+
+apps:
+ foo:
+ command: bin/run
+ common-id: io.snapcraft.test-snapd-appstreamid.foo
+
+ bar:
+ command: bin/run
+ common-id: io.snapcraft.test-snapd-appstreamid.bar
+
+ baz:
+ command: bin/run
+
+parts:
+ bash:
+ plugin: dump
+ source: .
+ prime:
+ - bin/
diff --git a/tests/main/appstream-id/task.yaml b/tests/main/appstream-id/task.yaml
new file mode 100644
index 0000000000..3a3a477df9
--- /dev/null
+++ b/tests/main/appstream-id/task.yaml
@@ -0,0 +1,31 @@
+summary: Verify AppStream ID integration
+# ubuntu-*-32: the snap used in the test was published only for amd64
+# fedora: uses GNU netcat by default
+systems: [-ubuntu-16.04-32, -fedora-*]
+
+prepare: |
+ snap install jq
+
+restore: |
+ snap remove jq
+
+execute: |
+ echo "Verify that search results contain common-ids"
+ printf 'GET /v2/find?name=test-snapd-appstreamid HTTP/1.0\r\n\r\n' | \
+ nc -U -q 1 /run/snapd.socket| grep '{'| \
+ jq -r ' .result[0]["common-ids"] | sort | join (",")' | \
+ MATCH 'io.snapcraft.test-snapd-appstreamid.bar,io.snapcraft.test-snapd-appstreamid.foo'
+
+ snap install --edge test-snapd-appstreamid
+
+ echo "Verify that installed snap info contains common-ids"
+ printf 'GET /v2/snaps/test-snapd-appstreamid HTTP/1.0\r\n\r\n' | \
+ nc -U -q 1 /run/snapd.socket| grep '{'| \
+ jq -r ' .result["common-ids"] | sort | join(",")' | \
+ MATCH 'io.snapcraft.test-snapd-appstreamid.bar,io.snapcraft.test-snapd-appstreamid.foo'
+
+ echo "Verify that apps have their common-id set"
+ printf 'GET /v2/apps?names=test-snapd-appstreamid HTTP/1.0\r\n\r\n' | \
+ nc -U -q 1 /run/snapd.socket| grep '{'| \
+ jq -r ' .result | sort_by(.name) | [.[]."common-id"] | join(",")' | \
+ MATCH 'io.snapcraft.test-snapd-appstreamid.bar,,io.snapcraft.test-snapd-appstreamid.foo'
diff --git a/tests/unit/spread-shellcheck/must b/tests/unit/spread-shellcheck/must
index 0d6c98dcb4..d7e28618e5 100644
--- a/tests/unit/spread-shellcheck/must
+++ b/tests/unit/spread-shellcheck/must
@@ -44,4 +44,5 @@ tests/main/install-cache/task.yaml
tests/main/install-errors/task.yaml
tests/main/install-refresh-private/task.yaml
tests/main/install-refresh-remove-hooks/task.yaml
+tests/main/appstream-id/task.yaml
tests/main/prepare-image-grub-core18/task.yaml