diff options
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 |
