summaryrefslogtreecommitdiff
diff options
authorMichael Vogt <mvo@ubuntu.com>2016-09-20 09:03:38 +0200
committerMichael Vogt <mvo@ubuntu.com>2016-09-20 09:03:38 +0200
commit342e0f638730228c6a0a2bf56bb16acea569926b (patch)
treec3daca940a752ee5c7338c1618084b1dcdd8f2fb
parent62ee6722ff09fa4187ce1668bf53ee6b914ad54e (diff)
parent86d128fd04c1b40a805f88d24c8bda39ff28848c (diff)
Merge remote-tracking branch 'upstream/master' into feature/spread-classic-tests2feature/spread-classic-tests2
-rw-r--r--.gitignore2
-rw-r--r--CONTRIBUTING.md32
-rw-r--r--HACKING.md164
-rw-r--r--README.md159
-rw-r--r--asserts/asserts.go18
-rw-r--r--asserts/asserts_test.go36
-rw-r--r--asserts/assertstest/assertstest.go8
-rw-r--r--asserts/assertstest/assertstest_test.go4
-rw-r--r--asserts/database_test.go1
-rw-r--r--asserts/fetcher.go25
-rw-r--r--asserts/gpgkeypairmgr.go67
-rw-r--r--asserts/header_checks.go12
-rw-r--r--asserts/snap_asserts.go129
-rw-r--r--asserts/snap_asserts_test.go264
-rw-r--r--asserts/snapasserts/snapasserts.go21
-rw-r--r--asserts/systestkeys/trusted.go42
-rw-r--r--cmd/snap/cmd_abort.go7
-rw-r--r--cmd/snap/cmd_ack.go7
-rw-r--r--cmd/snap/cmd_booted.go2
-rw-r--r--cmd/snap/cmd_buy.go15
-rw-r--r--cmd/snap/cmd_buy_test.go50
-rw-r--r--cmd/snap/cmd_changes.go4
-rw-r--r--cmd/snap/cmd_connect.go7
-rw-r--r--cmd/snap/cmd_connect_test.go2
-rw-r--r--cmd/snap/cmd_create_key.go7
-rw-r--r--cmd/snap/cmd_create_user.go17
-rw-r--r--cmd/snap/cmd_delete_key.go7
-rw-r--r--cmd/snap/cmd_disconnect.go7
-rw-r--r--cmd/snap/cmd_disconnect_test.go2
-rw-r--r--cmd/snap/cmd_download.go13
-rw-r--r--cmd/snap/cmd_export_key.go11
-rw-r--r--cmd/snap/cmd_find.go8
-rw-r--r--cmd/snap/cmd_first_boot.go2
-rw-r--r--cmd/snap/cmd_get.go20
-rw-r--r--cmd/snap/cmd_help.go5
-rw-r--r--cmd/snap/cmd_help_test.go2
-rw-r--r--cmd/snap/cmd_interfaces.go11
-rw-r--r--cmd/snap/cmd_interfaces_test.go6
-rw-r--r--cmd/snap/cmd_keys.go4
-rw-r--r--cmd/snap/cmd_keys_test.go33
-rw-r--r--cmd/snap/cmd_known.go12
-rw-r--r--cmd/snap/cmd_list.go2
-rw-r--r--cmd/snap/cmd_login.go10
-rw-r--r--cmd/snap/cmd_logout.go2
-rw-r--r--cmd/snap/cmd_prepare_image.go19
-rw-r--r--cmd/snap/cmd_run.go38
-rw-r--r--cmd/snap/cmd_run_test.go76
-rw-r--r--cmd/snap/cmd_set.go14
-rw-r--r--cmd/snap/cmd_shell.go7
-rw-r--r--cmd/snap/cmd_sign.go4
-rw-r--r--cmd/snap/cmd_sign_build.go20
-rw-r--r--cmd/snap/cmd_sign_test.go1
-rw-r--r--cmd/snap/cmd_snap_op.go85
-rw-r--r--cmd/snap/export_test.go1
-rw-r--r--cmd/snap/gnupg2_test.go27
-rw-r--r--cmd/snap/main.go70
-rw-r--r--cmd/snap/main_test.go24
-rw-r--r--cmd/snap/notes.go11
-rw-r--r--daemon/api.go23
-rw-r--r--daemon/api_test.go64
-rw-r--r--daemon/snap.go2
-rw-r--r--debian/changelog133
-rw-r--r--debian/control1
-rwxr-xr-xdebian/rules2
-rw-r--r--debian/snapd.postrm4
-rw-r--r--debian/tests/control3
-rw-r--r--debian/tests/integrationtests22
-rw-r--r--docs/hooks.md31
-rw-r--r--docs/interfaces.md2
-rw-r--r--docs/rest.md26
-rw-r--r--firstboot/firstboot.go11
-rw-r--r--image/helpers.go15
-rw-r--r--image/image.go32
-rw-r--r--image/image_test.go73
-rw-r--r--interfaces/apparmor/template.go4
-rw-r--r--interfaces/backends/backends.go2
-rw-r--r--interfaces/builtin/bluez.go8
-rw-r--r--interfaces/builtin/bool_file.go8
-rw-r--r--interfaces/builtin/browser_support.go76
-rw-r--r--interfaces/builtin/camera.go1
-rw-r--r--interfaces/builtin/common.go8
-rw-r--r--interfaces/builtin/content.go8
-rw-r--r--interfaces/builtin/fwupd.go9
-rw-r--r--interfaces/builtin/gpio.go8
-rw-r--r--interfaces/builtin/hidraw.go8
-rw-r--r--interfaces/builtin/libvirt.go3
-rw-r--r--interfaces/builtin/libvirt_test.go4
-rw-r--r--interfaces/builtin/location_control.go8
-rw-r--r--interfaces/builtin/location_observe.go8
-rw-r--r--interfaces/builtin/lxd_support.go8
-rw-r--r--interfaces/builtin/mir.go9
-rw-r--r--interfaces/builtin/modem_manager.go8
-rw-r--r--interfaces/builtin/mpris.go8
-rw-r--r--interfaces/builtin/network_manager.go8
-rw-r--r--interfaces/builtin/ppp.go8
-rw-r--r--interfaces/builtin/pulseaudio.go10
-rw-r--r--interfaces/builtin/serial_port.go8
-rw-r--r--interfaces/builtin/udisks2.go8
-rw-r--r--interfaces/core.go2
-rw-r--r--interfaces/kmod/backend.go42
-rw-r--r--interfaces/kmod/kmod.go20
-rw-r--r--interfaces/repo.go34
-rw-r--r--interfaces/repo_test.go82
-rw-r--r--overlord/assertstate/assertmgr.go122
-rw-r--r--overlord/assertstate/assertmgr_test.go120
-rw-r--r--overlord/assertstate/export_test.go2
-rw-r--r--overlord/auth/auth.go44
-rw-r--r--overlord/auth/auth_test.go80
-rw-r--r--overlord/hookstate/hookmgr.go5
-rw-r--r--overlord/managers_test.go2
-rw-r--r--overlord/patch/patch.go2
-rw-r--r--overlord/patch/patch4.go282
-rw-r--r--overlord/patch/patch4_test.go420
-rw-r--r--overlord/snapstate/backend.go2
-rw-r--r--overlord/snapstate/backend/copydata.go20
-rw-r--r--overlord/snapstate/backend/copydata_test.go67
-rw-r--r--overlord/snapstate/backend/mountunit.go7
-rw-r--r--overlord/snapstate/backend/snapdata.go79
-rw-r--r--overlord/snapstate/backend_test.go8
-rw-r--r--overlord/snapstate/snapmgr.go61
-rw-r--r--overlord/snapstate/snapmgr_test.go513
-rw-r--r--overlord/snapstate/snapstate.go57
-rw-r--r--overlord/state/task.go6
-rw-r--r--overlord/state/task_test.go19
-rwxr-xr-xrun-checks8
-rw-r--r--snap/info.go5
-rw-r--r--snap/snapenv/snapenv.go101
-rw-r--r--snap/snapenv/snapenv_test.go82
-rw-r--r--spread.yaml18
-rw-r--r--store/auth.go5
-rw-r--r--store/auth_test.go3
-rw-r--r--store/details.go40
-rw-r--r--store/store.go60
-rw-r--r--store/store_test.go1268
-rw-r--r--tests/lib/assertions/developer1.account-key22
-rw-r--r--tests/lib/assertions/testrootorg-store.account-key21
-rw-r--r--tests/lib/fakestore/refresh/refresh.go10
-rw-r--r--tests/lib/fakestore/store/store_test.go1
-rwxr-xr-xtests/lib/prepare.sh15
-rwxr-xr-xtests/lib/reset.sh7
-rwxr-xr-xtests/lib/snaps/locale-control-consumer/bin/get6
-rw-r--r--tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml14
-rw-r--r--tests/lib/store.sh2
-rw-r--r--tests/main/ack/developer1.account19
-rw-r--r--tests/main/ack/task.yaml6
-rw-r--r--tests/main/ack/testrootorg-store.account-key29
-rw-r--r--tests/main/create-key/successful_default.exp7
-rw-r--r--tests/main/create-key/successful_non_default.exp7
-rw-r--r--tests/main/create-key/task.yaml27
-rw-r--r--tests/main/install-sideload/task.yaml4
-rw-r--r--tests/main/interfaces-locale-control/task.yaml6
-rw-r--r--tests/main/interfaces-upower-observe/task.yaml54
-rw-r--r--tests/main/login/missing_email_error.exp2
-rw-r--r--tests/main/op-remove-retry/task.yaml4
-rw-r--r--tests/main/prepare-image-grub/task.yaml3
-rw-r--r--tests/main/refresh-devmode/task.yaml4
-rw-r--r--tests/main/refresh/task.yaml4
-rw-r--r--tests/main/revert-devmode/task.yaml4
-rw-r--r--tests/main/revert/task.yaml4
-rw-r--r--tests/main/snapctl/task.yaml1
-rw-r--r--tests/main/ubuntu-core-update-rollback-stresstest/task.yaml13
-rw-r--r--tests/main/writable-areas/task.yaml15
-rw-r--r--tests/manual-tests.md (renamed from integration-tests/manual-tests.md)51
163 files changed, 4372 insertions, 2016 deletions
diff --git a/.gitignore b/.gitignore
index 572bba7ac3..c075f9adab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,3 @@
-integration-tests/bin/
-integration-tests/data/output/
share
tags
.coverage
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f1a197cd0b..712c273ef5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,15 +1,27 @@
-Before contributing you should sign [Canonical's contributor agreement](http://www.ubuntu.com/legal/contributors), it’s the easiest way for you to give us permission to use your contributions.
+Before contributing you should sign [Canonical's contributor agreement][1],
+it’s the easiest way for you to give us permission to use your contributions.
-## Pull Request management
+## Pull Requests and tests
-Before merging any pull request to snappy's code base we need to verify that the code functionality and quality is not degraded by that addition. In order to do that there's a set of checks that we run for each PR, some of them using external services (TravisCI and Coveralls) and others using our own CI infrastructure (integration tests and autopkgtests). The checks based on external services are run for all the pull request. We are using the [GitHub Pull Request Builder Plugin](https://github.com/jenkinsci/ghprb-plugin/blob/master/README.md) for easing the management of PRs in relation with our internal infrastructure.
+We need to verify that the code functionality and quality is not degraded
+by additions before merging any changes to snapd's codebase. For each PR
+we run checks in three different groups: static, unit and spread.
-Depending on the affiliation of the GitHub user submitting a PR, the following actions may happen after receiving it:
+Static test use several code analysis tools present in the GoLang ecosystem
+(go vet, go lint and go fmt) to make sure that the code always aligns with
+the standards. They also check the markdown format of documentation files.
+All the existing unit tests are also executed, and the coverage info is
+reported to coveralls. Regarding [spread](https://github.com/snapcore/spread),
+we use it to verify the integrity of the product exercising it as a whole,
+both from an end user standpoint (eg. all kind of interactions with the
+snap tool from the command line) and from a more systemic approach (testing
+upgrades, for instance).
-* If the user belongs to the `ubuntu-core` organization or has been previously whitelisted, the internal downstream verification jobs will be triggered, their progress is reported in the PR's status section. Any of the users of the organization can retrigger the execution by posting a `retest this please` comment in the PR.
-* For user's outside the `ubuntu-core` organization, the internal checks won't be triggered by default and the [snappy-m-o](https://github.com/snappy-m-o) user, managed by the ghrbp plugin, will post a comment "Can one of the admins verify this patch?". After this, an `ubuntu-core` admin can post one of these comments:
- * `add to whitelist`: the external user will be whitelisted and all the further PRs will be tested automatically.
- * `ok to test`: the tests will be triggered and all the subsequent commits will retrigger them only for this PR.
- * `test this please`: the tests will be triggered once.
+We do not set as a requirement the addition of spread and unit tests for a PR
+to be merged, but encourage the contributors to add them so that the expected
+behaviour is explained and verified through the tests and the review process
+can be made on the solid base of a working system after the addition of the
+changes. If any tests need to be added for a PR to be merged it will be denoted
+during the review process.
-Once the PR has been reviewed and is accepted by at least two `ubuntu-core` members it can be merged by admins with the `merge this please` comment. This command triggers the internal checks over the PR's branch merged with master, so that we make sure that after merging the tests keep passing. After a successful run the branch is merged.
+[1]: http://www.ubuntu.com/legal/contributors
diff --git a/HACKING.md b/HACKING.md
new file mode 100644
index 0000000000..7947f12e8b
--- /dev/null
+++ b/HACKING.md
@@ -0,0 +1,164 @@
+# Hacking on snapd
+
+Hacking on snapd is fun and straightfoward. The code is extensively
+unit tested and we use the [spread](https://github.com/snapcore/spread)
+integration test framework for the integration/system level tests.
+
+## Development
+
+### Setting up a GOPATH
+
+When working with the source of Go programs, you should define a path within
+your home directory (or other workspace) which will be your `GOPATH`. `GOPATH`
+is similar to Java's `CLASSPATH` or Python's `~/.local`. `GOPATH` is documented
+[online](http://golang.org/pkg/go/build/) and inside the go tool itself
+
+ go help gopath
+
+Various conventions exist for naming the location of your `GOPATH`, but it
+should exist, and be writable by you. For example
+
+ export GOPATH=${HOME}/work
+ mkdir $GOPATH
+
+will define and create `$HOME/work` as your local `GOPATH`. The `go` tool
+itself will create three subdirectories inside your `GOPATH` when required;
+`src`, `pkg` and `bin`, which hold the source of Go programs, compiled packages
+and compiled binaries, respectively.
+
+Setting `GOPATH` correctly is critical when developing Go programs. Set and
+export it as part of your login script.
+
+Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install:
+
+ PATH="$PATH:$GOPATH/bin"
+
+### Getting the snapd sources
+
+The easiest way to get the source for `snapd` is to use the `go get` command.
+
+ go get -d -v github.com/snapcore/snapd/...
+
+This command will checkout the source of `snapd` and inspect it for any unmet
+Go package dependencies, downloading those as well. `go get` will also build
+and install `snapd` and its dependencies. To also build and install `snapd`
+itself into `$GOPATH/bin`, omit the `-d` flag. More details on the `go get`
+flags are available using
+
+ go help get
+
+At this point you will have the git local repository of the `snapd` source at
+`$GOPATH/github.com/snapcore/snapd`. The source for any
+dependent packages will also be available inside `$GOPATH`.
+
+### Dependencies handling
+
+To generate dependencies.tsv you need `godeps`, so
+
+ go get launchpad.net/godeps
+
+To obtain the correct dependencies for the project, run (from the
+projects root directory):
+
+ godeps -t -u dependencies.tsv
+
+You can use the script `get-deps.sh` to run the two previous steps.
+
+If the dependencies need updating, run (from the projects root directory):
+
+ godeps -t ./... > dependencies.tsv
+
+### Building
+
+To build, once the sources are available and `GOPATH` is set, you can just run
+
+ go build -o /tmp/snap github.com/snapcore/snapd/cmd/snap
+
+to get the `snap` binary in /tmp (or without -o to get it in the current
+working directory). Alternatively:
+
+ go install github.com/snapcore/snapd/...
+
+to have it available in `$GOPATH/bin`
+
+### Contributing
+
+Contributions are always welcome! Please make sure that you sign the
+Canonical contributor licence agreement at
+http://www.ubuntu.com/legal/contributors
+
+Snapd can be found on Github, so in order to fork the source and contribute,
+go to https://github.com/snapcore/snapd. Check out [Github's help
+pages](https://help.github.com/) to find out how to set up your local branch,
+commit changes and create pull requests.
+
+We value good tests, so when you fix a bug or add a new feature we highly
+encourage you to create a test in `$source_test.go`. See also the section
+about Testing.
+
+### Testing
+
+To run the various tests that we have to ensure a high quality source just run:
+
+ ./run-checks
+
+This will check if the source format is consistent, that it builds, all tests
+work as expected and that "go vet" has nothing to complain.
+
+You can run individual test for a sub-package by changing into that directory and:
+
+ go test -check.f $testname
+
+If a test hangs, you can enable verbose mode:
+
+ go test -v -check.vv
+
+(or -check.v for less verbose output).
+
+There is more to read about the testing framework on the [website](https://labix.org/gocheck)
+
+### Running the spread tests
+
+To run the spread tests locally you need the latest version of spread
+from https://github.com/snapcore/spread. It can be installed via:
+
+ $ sudo apt install qemu-kvm autopkgtest
+ $ sudo snap install --devmode spread
+
+Then setup the environment via:
+
+ $ mkdir -p .spread/qemu
+ $ cd .spread/qemu
+ $ adt-buildvm-ubuntu-cloud
+ $ mv adt-xenial-amd64-cloud.img ubuntu-16.04.img
+
+And you can run the tests via:
+
+ $ spread -v qemu:
+
+For quick reuse you can use:
+
+ $ spread -keep qemu:
+
+It will print how to reuse the systems. Make sure to use
+`export REUSE_PROJECT=1` in your environment too.
+
+
+### Testing snapd
+
+To test the `snapd` REST API daemon on a snappy system you need to
+transfer it to the snappy system and then run:
+
+ sudo systemctl stop snapd.service snapd.socket
+ sudo /lib/systemd/systemd-activate -E SNAPD_DEBUG=1 -E SNAP_REEXEC=0 -E SNAPD_DEBUG_HTTP3 -l /run/snapd.socket -l /run/snapd-snap.socket ./snapd
+ or with systemd version >= 230
+ sudo systemd-socket-activate -E SNAPD_DEBUG=1 -E SNAP_REEXEC=0 -E SNAPD_DEBUG_HTTP3 -l /run/snapd.socket -l /run/snapd-snap.socket ./snapd
+
+This will stop the installed snapd and activate the new one. Once it's
+printed out something like `Listening on /run/snapd.socket as 3.` you
+should then
+
+ sudo chmod 0666 /run/snapd*.socket
+
+so the socket has the right permissions (otherwise you need `sudo` to
+connect).
diff --git a/README.md b/README.md
index 50a14c905f..3b3a1ab119 100644
--- a/README.md
+++ b/README.md
@@ -1,162 +1,15 @@
[![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url]
# snapd
-The snapd and snap tools enable systems to work with .snap files.
+The snapd and snap tools enable systems to work with .snap files. See
+[snapcraft.io](http://snapcraft.io) for a high level overview about
+snap files and the snapd application.
## Development
-### Setting up a GOPATH
-
-When working with the source of Go programs, you should define a path within
-your home directory (or other workspace) which will be your `GOPATH`. `GOPATH`
-is similar to Java's `CLASSPATH` or Python's `~/.local`. `GOPATH` is documented
-[http://golang.org/pkg/go/build/](online) and inside the go tool itself
-
- go help gopath
-
-Various conventions exist for naming the location of your `GOPATH`, but it
-should exist, and be writable by you. For example
-
- export GOPATH=${HOME}/work
- mkdir $GOPATH
-
-will define and create `$HOME/work` as your local `GOPATH`. The `go` tool
-itself will create three subdirectories inside your `GOPATH` when required;
-`src`, `pkg` and `bin`, which hold the source of Go programs, compiled packages
-and compiled binaries, respectively.
-
-Setting `GOPATH` correctly is critical when developing Go programs. Set and
-export it as part of your login script.
-
-Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install:
-
- PATH="$PATH:$GOPATH/bin"
-
-### Getting the snappy sources
-
-The easiest way to get the source for `snappy` is to use the `go get` command.
-
- go get -d -v github.com/snapcore/snapd/...
-
-This command will checkout the source of `snappy` and inspect it for any unmet
-Go package dependencies, downloading those as well. `go get` will also build
-and install `snappy` and its dependencies. To checkout without installing, use
-the `-d` flag. More details on the `go get` flags are available using
-
- go help get
-
-At this point you will have the git local repository of the `snappy` source at
-`$GOPATH/github.com/snapcore/snapd`. The source for any
-dependent packages will also be available inside `$GOPATH`.
-
-### Dependencies handling
-
-To generate dependencies.tsv you need `godeps`, so
-
- go get launchpad.net/godeps
-
-To obtain the correct dependencies for the project, run:
-
- godeps -t -u dependencies.tsv
-
-You can use the script `get-deps.sh` to run the two previous steps.
-
-If the dependencies need updating
-
- godeps -t ./... > dependencies.tsv
-
-### Building
-
-To build, once the sources are available and `GOPATH` is set, you can just run
-
- go build -o /tmp/snap github.com/snapcore/snapd/cmd/snap
-
-to get the `snap` binary in /tmp (or without -o to get it in the current
-working directory). Alternatively:
-
- go install github.com/snapcore/snapd/...
-
-to have it available in `$GOPATH/bin`
-
-### Contributing
-
-Contributions are always welcome! Please make sure that you sign the
-Canonical contributor licence agreement at
-http://www.ubuntu.com/legal/contributors
-
-Snappy can be found on Github, so in order to fork the source and contribute,
-go to https://github.com/snapcore/snapd. Check out [Github's help
-pages](https://help.github.com/) to find out how to set up your local branch,
-commit changes and create pull requests.
-
-We value good tests, so when you fix a bug or add a new feature we highly
-encourage you to create a test in $source_testing.go. See also the section
-about Testing.
-
-### Testing
-
-To run the various tests that we have to ensure a high quality source just run:
-
- ./run-checks
-
-This will check if the source format is consistent, that it build, all tests
-work as expected and that "go vet" and "golint" have nothing to complain.
-
-You can run individual test for a sub-package by changing into that directory and:
-
- go test -check.f $testname
-
-If a test hangs, you can enable verbose mode:
-
- go test -v -check.vv
-
-(or -check.v for less verbose output).
-
-There is more to read about the testing framework on the [website](https://labix.org/gocheck)
-
-### Running the spread tests
-
-To run the spread tests locally you need the latest version of spread
-from https://github.com/snapcore/spread. It can be installed via:
-
- $ sudo apt install qemu-kvm autopkgtest
- $ sudo snap install spread
-
-Then setup the environment via:
-
- $ mkdir -p .spread/qemu
- $ cd .spread/qemu
- $ adt-buildvm-ubuntu-cloud
- $ mv adt-xenial-amd64-cloud.img ubuntu-16.04.img
-
-And you can run the tests via:
-
- $ spread -v qemu:
-
-For quick reuse you can use:
-
- $ spread -keep qemu:
-
-It will print how to reuse the systems. Make sure to use
-`export REUSE_PROJECT=1` in your environment too.
-
-
-### Testing snapd on a snappy system
-
-To test the `snapd` REST API daemon on a snappy system you need to
-transfer it to the snappy system and then run:
-
- sudo systemctl stop snapd.service snapd.socket
- sudo /lib/systemd/systemd-activate -E SNAPD_DEBUG=1 -l /run/snapd.socket -l /run/snapd-snap.socket ./snapd
-
-This will stop the installed snapd and activate the new one. Once it's
-printed out something like `Listening on /run/snapd.socket as 3.` you
-should then
-
- sudo chmod 0666 /run/snapd.socket
-
-so the socket has the right permissions (otherwise you need `sudo` to
-connect).
+To get started with development off the snapd code itself, please check
+out [HACKING.md](https://github.com/snapcore/snapd/blob/master/HACKING.md)
+for in-depth details.
## Reporting bugs
diff --git a/asserts/asserts.go b/asserts/asserts.go
index 2f58f7f8cc..a1f9734533 100644
--- a/asserts/asserts.go
+++ b/asserts/asserts.go
@@ -58,6 +58,7 @@ var (
SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration, 0}
SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild, 0}
SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision, 0}
+ ValidationType = &AssertionType{"validation", []string{"series", "snap-id", "approved-snap-id", "approved-snap-revision"}, assembleValidation, 0}
// ...
)
@@ -78,6 +79,7 @@ var typeRegistry = map[string]*AssertionType{
SnapDeclarationType.Name: SnapDeclarationType,
SnapBuildType.Name: SnapBuildType,
SnapRevisionType.Name: SnapRevisionType,
+ ValidationType.Name: ValidationType,
// no authority
DeviceSessionRequestType.Name: DeviceSessionRequestType,
SerialProofType.Name: SerialProofType,
@@ -97,7 +99,21 @@ type Ref struct {
}
func (ref *Ref) String() string {
- return fmt.Sprintf("%s %v", ref.Type.Name, ref.PrimaryKey)
+ pkStr := "-"
+ n := len(ref.Type.PrimaryKey)
+ if n != len(ref.PrimaryKey) {
+ pkStr = "???"
+ } else if n > 0 {
+ pkStr = ref.PrimaryKey[n-1]
+ if n > 1 {
+ sfx := []string{pkStr + ";"}
+ for i, k := range ref.Type.PrimaryKey[:n-1] {
+ sfx = append(sfx, fmt.Sprintf("%s:%s", k, ref.PrimaryKey[i]))
+ }
+ pkStr = strings.Join(sfx, " ")
+ }
+ }
+ return fmt.Sprintf("%s (%s)", ref.Type.Name, pkStr)
}
// Unique returns a unique string representing the reference that can be used as a key in maps.
diff --git a/asserts/asserts_test.go b/asserts/asserts_test.go
index b338fd1e4c..619a613dd3 100644
--- a/asserts/asserts_test.go
+++ b/asserts/asserts_test.go
@@ -50,6 +50,41 @@ func (as *assertsSuite) TestRef(c *C) {
c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz")
}
+func (as *assertsSuite) TestRefString(c *C) {
+ ref := &asserts.Ref{
+ Type: asserts.AccountType,
+ PrimaryKey: []string{"canonical"},
+ }
+
+ c.Check(ref.String(), Equals, "account (canonical)")
+
+ ref = &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"18", "SNAPID"},
+ }
+
+ c.Check(ref.String(), Equals, "snap-declaration (SNAPID; series:18)")
+
+ ref = &asserts.Ref{
+ Type: asserts.ModelType,
+ PrimaryKey: []string{"18", "BRAND", "baz-3000"},
+ }
+
+ c.Check(ref.String(), Equals, "model (baz-3000; series:18 brand-id:BRAND)")
+
+ // broken primary key
+ ref = &asserts.Ref{
+ Type: asserts.ModelType,
+ PrimaryKey: []string{"18"},
+ }
+ c.Check(ref.String(), Equals, "model (???)")
+
+ ref = &asserts.Ref{
+ Type: asserts.TestOnlyNoAuthorityType,
+ }
+ c.Check(ref.String(), Equals, "test-only-no-authority (-)")
+}
+
func (as *assertsSuite) TestRefResolveError(c *C) {
ref := &asserts.Ref{
Type: asserts.TestOnly2Type,
@@ -671,6 +706,7 @@ func (as *assertsSuite) TestWithAuthority(c *C) {
"snap-revision",
"model",
"serial",
+ "validation",
}
c.Check(withAuthority, HasLen, asserts.NumAssertionType-4) // excluding device-session-request, serial-request, serial-proof, account-key-request
for _, name := range withAuthority {
diff --git a/asserts/assertstest/assertstest.go b/asserts/assertstest/assertstest.go
index 45a7d69d3b..45d605200c 100644
--- a/asserts/assertstest/assertstest.go
+++ b/asserts/assertstest/assertstest.go
@@ -192,6 +192,9 @@ func NewAccountKey(db SignerDB, acct *asserts.Account, otherHeaders map[string]i
}
otherHeaders["account-id"] = acct.AccountID()
otherHeaders["public-key-sha3-384"] = pubKey.ID()
+ if otherHeaders["name"] == nil {
+ otherHeaders["name"] = "default"
+ }
if otherHeaders["since"] == nil {
otherHeaders["since"] = time.Now().Format(time.RFC3339)
}
@@ -272,6 +275,7 @@ func NewStoreStack(authorityID string, rootPrivKey, storePrivKey asserts.Private
"timestamp": ts,
}, "")
trustedKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{
+ "name": "root",
"since": ts,
}, rootPrivKey.PublicKey(), "")
trusted := []asserts.Assertion{trustedAcct, trustedKey}
@@ -287,7 +291,9 @@ func NewStoreStack(authorityID string, rootPrivKey, storePrivKey asserts.Private
if err != nil {
panic(err)
}
- storeKey := NewAccountKey(rootSigning, trustedAcct, nil, storePrivKey.PublicKey(), "")
+ storeKey := NewAccountKey(rootSigning, trustedAcct, map[string]interface{}{
+ "name": "store",
+ }, storePrivKey.PublicKey(), "")
err = db.Add(storeKey)
if err != nil {
panic(err)
diff --git a/asserts/assertstest/assertstest_test.go b/asserts/assertstest/assertstest_test.go
index 1cdb33dacf..3c1b0c9b18 100644
--- a/asserts/assertstest/assertstest_test.go
+++ b/asserts/assertstest/assertstest_test.go
@@ -84,6 +84,7 @@ func (s *helperSuite) TestStoreStack(c *C) {
c.Check(store.TrustedAccount.IsCertified(), Equals, true)
c.Check(store.TrustedKey.AccountID(), Equals, "super")
+ c.Check(store.TrustedKey.Name(), Equals, "root")
db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
Backstore: asserts.NewMemoryBackstore(),
@@ -97,6 +98,7 @@ func (s *helperSuite) TestStoreStack(c *C) {
c.Check(storeAccKey.AccountID(), Equals, "super")
c.Check(storeAccKey.AccountID(), Equals, store.AuthorityID)
c.Check(storeAccKey.PublicKeyID(), Equals, store.KeyID)
+ c.Check(storeAccKey.Name(), Equals, "store")
acct := assertstest.NewAccount(store, "devel1", nil, "")
c.Check(acct.Username(), Equals, "devel1")
@@ -115,4 +117,6 @@ func (s *helperSuite) TestStoreStack(c *C) {
err = db.Add(acctKey)
c.Assert(err, IsNil)
+
+ c.Check(acctKey.Name(), Equals, "default")
}
diff --git a/asserts/database_test.go b/asserts/database_test.go
index e78b5d9104..20a2de22cc 100644
--- a/asserts/database_test.go
+++ b/asserts/database_test.go
@@ -671,6 +671,7 @@ func (safs *signAddFindSuite) TestDontLetAddConfusinglyAssertionClashingWithTrus
"authority-id": "canonical",
"account-id": "canonical",
"public-key-sha3-384": safs.signingKeyID,
+ "name": "default",
"since": now.Format(time.RFC3339),
"until": now.AddDate(1, 0, 0).Format(time.RFC3339),
}
diff --git a/asserts/fetcher.go b/asserts/fetcher.go
index a35a0d083c..cc3c93af77 100644
--- a/asserts/fetcher.go
+++ b/asserts/fetcher.go
@@ -31,8 +31,17 @@ const (
fetchSaved
)
-// Fetcher helps fetching assertions and their prerequisites.
-type Fetcher struct {
+// A Fetcher helps fetching assertions and their prerequisites.
+type Fetcher interface {
+ // Fetch retrieves the assertion indicated by ref then its prerequisites
+ // recursively, along the way saving prerequisites before dependent assertions.
+ Fetch(*Ref) error
+ // Save retrieves the prerequisites of the assertion recursively,
+ // along the way saving them, and finally saves the assertion.
+ Save(Assertion) error
+}
+
+type fetcher struct {
db RODatabase
retrieve func(*Ref) (Assertion, error)
save func(Assertion) error
@@ -41,8 +50,8 @@ type Fetcher struct {
}
// NewFetcher creates a Fetcher which will use trustedDB to determine trusted assertions, will fetch assertions following prerequisites using retrieve, and then will pass them to save, saving prerequisites before dependent assertions.
-func NewFetcher(trustedDB RODatabase, retrieve func(*Ref) (Assertion, error), save func(Assertion) error) *Fetcher {
- return &Fetcher{
+func NewFetcher(trustedDB RODatabase, retrieve func(*Ref) (Assertion, error), save func(Assertion) error) Fetcher {
+ return &fetcher{
db: trustedDB,
retrieve: retrieve,
save: save,
@@ -50,7 +59,7 @@ func NewFetcher(trustedDB RODatabase, retrieve func(*Ref) (Assertion, error), sa
}
}
-func (f *Fetcher) chase(ref *Ref, a Assertion) error {
+func (f *fetcher) chase(ref *Ref, a Assertion) error {
// check if ref points to a trusted assertion, in which case
// there is nothing to do
_, err := ref.Resolve(f.db.FindTrusted)
@@ -92,12 +101,12 @@ func (f *Fetcher) chase(ref *Ref, a Assertion) error {
// Fetch retrieves the assertion indicated by ref then its prerequisites
// recursively, along the way saving prerequisites before dependent assertions.
-func (f *Fetcher) Fetch(ref *Ref) error {
+func (f *fetcher) Fetch(ref *Ref) error {
return f.chase(ref, nil)
}
// fetchAccountKey behaves like Fetch for the account-key with the given key id.
-func (f *Fetcher) fetchAccountKey(keyID string) error {
+func (f *fetcher) fetchAccountKey(keyID string) error {
keyRef := &Ref{
Type: AccountKeyType,
PrimaryKey: []string{keyID},
@@ -107,6 +116,6 @@ func (f *Fetcher) fetchAccountKey(keyID string) error {
// Save retrieves the prerequisites of the assertion recursively,
// along the way saving them, and finally saves the assertion.
-func (f *Fetcher) Save(a Assertion) error {
+func (f *fetcher) Save(a Assertion) error {
return f.chase(a.Ref(), a)
}
diff --git a/asserts/gpgkeypairmgr.go b/asserts/gpgkeypairmgr.go
index 42496aad02..9024975172 100644
--- a/asserts/gpgkeypairmgr.go
+++ b/asserts/gpgkeypairmgr.go
@@ -66,6 +66,10 @@ func ensureGPGHomeDirectory() (string, error) {
// test suite). GnuPG 1 is still supported so it's reasonable to continue
// using that for now.
func findGPGCommand() (string, error) {
+ if path := os.Getenv("SNAP_GNUPG_CMD"); path != "" {
+ return path, nil
+ }
+
path, err := exec.LookPath("gpg1")
if err != nil {
path, err = exec.LookPath("gpg")
@@ -79,6 +83,18 @@ func runGPGImpl(input []byte, args ...string) ([]byte, error) {
return nil, err
}
+ // Ensure the gpg-agent knows what tty to talk to to ask for
+ // the passphrase. This is needed because we drive gpg over
+ // a pipe and if the agent is not already started it will
+ // fail to be able to ask for a password.
+ if os.Getenv("GPG_TTY") == "" {
+ tty, err := os.Readlink("/proc/self/fd/0")
+ if err != nil {
+ return nil, err
+ }
+ os.Setenv("GPG_TTY", tty)
+ }
+
general := []string{"--homedir", homedir, "-q", "--no-auto-check-trustdb"}
allArgs := append(general, args...)
@@ -147,7 +163,7 @@ func (gkm *GPGKeypairManager) retrieve(fpr string) (PrivateKey, error) {
// Walk iterates over all the RSA private keys in the local GPG setup calling the provided callback until this returns an error
func (gkm *GPGKeypairManager) Walk(consider func(privk PrivateKey, fingerprint string, uid string) error) error {
// see GPG source doc/DETAILS
- out, err := gkm.gpg(nil, "--batch", "--list-secret-keys", "--fingerprint", "--with-colons")
+ out, err := gkm.gpg(nil, "--batch", "--list-secret-keys", "--fingerprint", "--with-colons", "--fixed-list-mode")
if err != nil {
return err
}
@@ -161,6 +177,7 @@ func (gkm *GPGKeypairManager) Walk(consider func(privk PrivateKey, fingerprint s
}
lines = lines[:n]
for j := 0; j < n; j++ {
+ // sec: line
line := lines[j]
if !strings.HasPrefix(line, "sec:") {
continue
@@ -174,24 +191,42 @@ func (gkm *GPGKeypairManager) Walk(consider func(privk PrivateKey, fingerprint s
}
keyID := secFields[4]
uid := ""
- if len(secFields) >= 10 {
- uid = secFields[9]
- }
- if j+1 >= n || !strings.HasPrefix(lines[j+1], "fpr:") {
- continue
+ fpr := ""
+ var privKey PrivateKey
+ // look for fpr:, uid: lines, order may vary and gpg2.1
+ // may springle additional lines in (like gpr:)
+ Loop:
+ for k := j + 1; k < n && !strings.HasPrefix(lines[k], "sec:"); k++ {
+ switch {
+ case strings.HasPrefix(lines[k], "fpr:"):
+ fprFields := strings.Split(lines[k], ":")
+ // extract "Field 10 - User-ID"
+ // A FPR record stores the fingerprint here.
+ if len(fprFields) < 10 {
+ break Loop
+ }
+ fpr = fprFields[9]
+ if !strings.HasSuffix(fpr, keyID) {
+ break // strange, skip
+ }
+ privKey, err = gkm.retrieve(fpr)
+ if err != nil {
+ return err
+ }
+ case strings.HasPrefix(lines[k], "uid:"):
+ uidFields := strings.Split(lines[k], ":")
+ // extract "*** Field 10 - User-ID"
+ if len(uidFields) < 10 {
+ break Loop
+ }
+ uid = uidFields[9]
+ }
}
- fprFields := strings.Split(lines[j+1], ":")
- if len(fprFields) < 10 {
+ // sanity checking
+ if privKey == nil || uid == "" {
continue
}
- fpr := fprFields[9]
- if !strings.HasSuffix(fpr, keyID) {
- continue // strange, skip
- }
- privKey, err := gkm.retrieve(fpr)
- if err != nil {
- return err
- }
+ // collected it all
err = consider(privKey, fpr, uid)
if err != nil {
return err
diff --git a/asserts/header_checks.go b/asserts/header_checks.go
index 0b44fef56a..04f3e11d06 100644
--- a/asserts/header_checks.go
+++ b/asserts/header_checks.go
@@ -213,3 +213,15 @@ func checkStringMatches(headers map[string]interface{}, name string, pattern *re
}
return s, nil
}
+
+func checkOptionalBool(headers map[string]interface{}, name string) (bool, error) {
+ value, ok := headers[name]
+ if !ok {
+ return false, nil
+ }
+ s, ok := value.(string)
+ if !ok || (s != "true" && s != "false") {
+ return false, fmt.Errorf("%q header must be 'true' or 'false'", name)
+ }
+ return s == "true", nil
+}
diff --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go
index 840843b706..083a422488 100644
--- a/asserts/snap_asserts.go
+++ b/asserts/snap_asserts.go
@@ -35,7 +35,8 @@ import (
// publisher and its other properties.
type SnapDeclaration struct {
assertionBase
- timestamp time.Time
+ refreshControl []string
+ timestamp time.Time
}
// Series returns the series for which the snap is being declared.
@@ -63,6 +64,11 @@ func (snapdcl *SnapDeclaration) Timestamp() time.Time {
return snapdcl.timestamp
}
+// RefreshControl returns the ids of snaps whose updates are controlled by this declaration.
+func (snapdcl *SnapDeclaration) RefreshControl() []string {
+ return snapdcl.refreshControl
+}
+
// Implement further consistency checks.
func (snapdcl *SnapDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error {
if !db.IsTrustedAccount(snapdcl.AuthorityID()) {
@@ -77,6 +83,7 @@ func (snapdcl *SnapDeclaration) checkConsistency(db RODatabase, acck *AccountKey
if err != nil {
return err
}
+
return nil
}
@@ -106,9 +113,15 @@ func assembleSnapDeclaration(assert assertionBase) (Assertion, error) {
return nil, err
}
+ refControl, err := checkStringList(assert.headers, "refresh-control")
+ if err != nil {
+ return nil, err
+ }
+
return &SnapDeclaration{
- assertionBase: assert,
- timestamp: timestamp,
+ assertionBase: assert,
+ timestamp: timestamp,
+ refreshControl: refControl,
}, nil
}
@@ -316,3 +329,113 @@ func assembleSnapRevision(assert assertionBase) (Assertion, error) {
timestamp: timestamp,
}, nil
}
+
+// Validation holds a validation assertion, describing that a combination of
+// (snap-id, approved-snap-id, approved-revision) has been validated for
+// the series, meaning updating to that revision of approved-snap-id
+// has been approved by the owner of the gating snap with snap-id.
+type Validation struct {
+ assertionBase
+ revoked bool
+ timestamp time.Time
+ approvedSnapRevision int
+}
+
+// Series returns the series for which the validation holds.
+func (validation *Validation) Series() string {
+ return validation.HeaderString("series")
+}
+
+// SnapID returns the ID of the gating snap.
+func (validation *Validation) SnapID() string {
+ return validation.HeaderString("snap-id")
+}
+
+// ApprovedSnapID returns the ID of the gated snap.
+func (validation *Validation) ApprovedSnapID() string {
+ return validation.HeaderString("approved-snap-id")
+}
+
+// ApprovedSnapRevision returns the approved revision of the gated snap.
+func (validation *Validation) ApprovedSnapRevision() int {
+ return validation.approvedSnapRevision
+}
+
+// Revoked returns true if the validation has been revoked.
+func (validation *Validation) Revoked() bool {
+ return validation.revoked
+}
+
+// Timestamp returns the time when the validation was issued.
+func (validation *Validation) Timestamp() time.Time {
+ return validation.timestamp
+}
+
+// Implement further consistency checks.
+func (validation *Validation) checkConsistency(db RODatabase, acck *AccountKey) error {
+ _, err := db.Find(SnapDeclarationType, map[string]string{
+ "series": validation.Series(),
+ "snap-id": validation.ApprovedSnapID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion for approved-snap-id %q", validation.SnapID(), validation.ApprovedSnapID())
+ }
+ if err != nil {
+ return err
+ }
+ a, err := db.Find(SnapDeclarationType, map[string]string{
+ "series": validation.Series(),
+ "snap-id": validation.SnapID(),
+ })
+ if err == ErrNotFound {
+ return fmt.Errorf("validation assertion by snap-id %q does not have a matching snap-declaration assertion", validation.SnapID())
+ }
+ if err != nil {
+ return err
+ }
+
+ gatingDecl := a.(*SnapDeclaration)
+ if gatingDecl.PublisherID() != validation.AuthorityID() {
+ return fmt.Errorf("validation assertion by snap %q (id %q) not signed by its publisher", gatingDecl.SnapName(), validation.SnapID())
+ }
+
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*Validation)(nil)
+
+// Prerequisites returns references to this validation's prerequisite assertions.
+func (validation *Validation) Prerequisites() []*Ref {
+ return []*Ref{
+ &Ref{Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.SnapID()}},
+ &Ref{Type: SnapDeclarationType, PrimaryKey: []string{validation.Series(), validation.ApprovedSnapID()}},
+ }
+}
+
+func assembleValidation(assert assertionBase) (Assertion, error) {
+ approvedSnapRevision, err := checkInt(assert.headers, "approved-snap-revision")
+ if err != nil {
+ return nil, err
+ }
+ if approvedSnapRevision < 1 {
+ return nil, fmt.Errorf(`"approved-snap-revision" header must be >=1: %d`, approvedSnapRevision)
+ }
+
+ revoked, err := checkOptionalBool(assert.headers, "revoked")
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ return &Validation{
+ assertionBase: assert,
+ revoked: revoked,
+ timestamp: timestamp,
+ approvedSnapRevision: approvedSnapRevision,
+ }, nil
+}
diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go
index 35271f10d8..7011836622 100644
--- a/asserts/snap_asserts_test.go
+++ b/asserts/snap_asserts_test.go
@@ -38,6 +38,7 @@ var (
_ = Suite(&snapFileDigestSuite{})
_ = Suite(&snapBuildSuite{})
_ = Suite(&snapRevSuite{})
+ _ = Suite(&validationSuite{})
)
type snapDeclSuite struct {
@@ -57,6 +58,7 @@ func (sds *snapDeclSuite) TestDecodeOK(c *C) {
"snap-id: snap-id-1\n" +
"snap-name: first\n" +
"publisher-id: dev-id1\n" +
+ "refresh-control:\n - foo\n - bar\n" +
sds.tsLine +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
@@ -72,6 +74,7 @@ func (sds *snapDeclSuite) TestDecodeOK(c *C) {
c.Check(snapDecl.SnapID(), Equals, "snap-id-1")
c.Check(snapDecl.SnapName(), Equals, "first")
c.Check(snapDecl.PublisherID(), Equals, "dev-id1")
+ c.Check(snapDecl.RefreshControl(), DeepEquals, []string{"foo", "bar"})
}
func (sds *snapDeclSuite) TestEmptySnapName(c *C) {
@@ -92,6 +95,24 @@ func (sds *snapDeclSuite) TestEmptySnapName(c *C) {
c.Check(snapDecl.SnapName(), Equals, "")
}
+func (sds *snapDeclSuite) TestMissingRefreshControl(c *C) {
+ encoded := "type: snap-declaration\n" +
+ "authority-id: canonical\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "snap-name: \n" +
+ "publisher-id: dev-id1\n" +
+ sds.tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ snapDecl := a.(*asserts.SnapDeclaration)
+ c.Check(snapDecl.RefreshControl(), HasLen, 0)
+}
+
const (
snapDeclErrPrefix = "assertion snap-declaration: "
)
@@ -103,6 +124,7 @@ func (sds *snapDeclSuite) TestDecodeInvalid(c *C) {
"snap-id: snap-id-1\n" +
"snap-name: first\n" +
"publisher-id: dev-id1\n" +
+ "refresh-control:\n - foo\n - bar\n" +
sds.tsLine +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
@@ -117,6 +139,8 @@ func (sds *snapDeclSuite) TestDecodeInvalid(c *C) {
{"snap-name: first\n", "", `"snap-name" header is mandatory`},
{"publisher-id: dev-id1\n", "", `"publisher-id" header is mandatory`},
{"publisher-id: dev-id1\n", "publisher-id: \n", `"publisher-id" header should not be empty`},
+ {"refresh-control:\n - foo\n - bar\n", "refresh-control: foo\n", `"refresh-control" header must be a list of strings`},
+ {"refresh-control:\n - foo\n - bar\n", "refresh-control:\n -\n - nested\n", `"refresh-control" header must be a list of strings`},
{sds.tsLine, "", `"timestamp" header is mandatory`},
{sds.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
{sds.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
@@ -148,7 +172,6 @@ func (sds *snapDeclSuite) TestSnapDeclarationCheck(c *C) {
"snap-id": "snap-id-1",
"snap-name": "foo",
"publisher-id": "dev-id1",
- "gates": "",
"timestamp": time.Now().Format(time.RFC3339),
}
snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "")
@@ -168,7 +191,6 @@ func (sds *snapDeclSuite) TestSnapDeclarationCheckUntrustedAuthority(c *C) {
"snap-id": "snap-id-1",
"snap-name": "foo",
"publisher-id": "dev-id1",
- "gates": "",
"timestamp": time.Now().Format(time.RFC3339),
}
snapDecl, err := otherDB.Sign(asserts.SnapDeclarationType, headers, nil, "")
@@ -186,7 +208,6 @@ func (sds *snapDeclSuite) TestSnapDeclarationCheckMissingPublisherAccount(c *C)
"snap-id": "snap-id-1",
"snap-name": "foo",
"publisher-id": "dev-id1",
- "gates": "",
"timestamp": time.Now().Format(time.RFC3339),
}
snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, headers, nil, "")
@@ -227,7 +248,6 @@ func (sds *snapDeclSuite) TestPrerequisites(c *C) {
"snap-id: snap-id-1\n" +
"snap-name: first\n" +
"publisher-id: dev-id1\n" +
- "gates: snap-id-3,snap-id-4\n" +
sds.tsLine +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
@@ -341,7 +361,9 @@ func makeStoreAndCheckDB(c *C) (storeDB *assertstest.SigningDB, checkDB *asserts
func setup3rdPartySigning(c *C, username string, storeDB *assertstest.SigningDB, checkDB *asserts.Database) (signingDB *assertstest.SigningDB) {
privKey := testPrivKey2
- acct := assertstest.NewAccount(storeDB, username, nil, "")
+ acct := assertstest.NewAccount(storeDB, username, map[string]interface{}{
+ "account-id": username,
+ }, "")
accKey := assertstest.NewAccountKey(storeDB, acct, nil, privKey.PublicKey(), "")
err := checkDB.Add(acct)
@@ -357,7 +379,7 @@ func (sbs *snapBuildSuite) TestSnapBuildCheck(c *C) {
devDB := setup3rdPartySigning(c, "devel1", storeDB, db)
headers := map[string]interface{}{
- "authority-id": devDB.AuthorityID,
+ "authority-id": "devel1",
"snap-sha3-384": blobSHA3_384,
"snap-id": "snap-id-1",
"grade": "devel",
@@ -493,7 +515,6 @@ func prereqSnapDecl(c *C, storeDB assertstest.SignerDB, db *asserts.Database) {
"snap-id": "snap-id-1",
"snap-name": "foo",
"publisher-id": "dev-id1",
- "gates": "",
"timestamp": time.Now().Format(time.RFC3339),
}, nil, "")
c.Assert(err, IsNil)
@@ -599,3 +620,232 @@ func (srs *snapRevSuite) TestPrerequisites(c *C) {
PrimaryKey: []string{"dev-id1"},
})
}
+
+type validationSuite struct {
+ ts time.Time
+ tsLine string
+}
+
+func (vs *validationSuite) SetUpSuite(c *C) {
+ vs.ts = time.Now().Truncate(time.Second).UTC()
+ vs.tsLine = "timestamp: " + vs.ts.Format(time.RFC3339) + "\n"
+}
+
+func (vs *validationSuite) makeValidEncoded() string {
+ return "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+}
+
+func (vs *validationSuite) makeHeaders(overrides map[string]interface{}) map[string]interface{} {
+ headers := map[string]interface{}{
+ "authority-id": "dev-id1",
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "approved-snap-id": "snap-id-2",
+ "approved-snap-revision": "42",
+ "revision": "1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for k, v := range overrides {
+ headers[k] = v
+ }
+ return headers
+}
+
+func (vs *validationSuite) TestDecodeOK(c *C) {
+ encoded := vs.makeValidEncoded()
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ValidationType)
+ validation := a.(*asserts.Validation)
+ c.Check(validation.AuthorityID(), Equals, "dev-id1")
+ c.Check(validation.Timestamp(), Equals, vs.ts)
+ c.Check(validation.Series(), Equals, "16")
+ c.Check(validation.SnapID(), Equals, "snap-id-1")
+ c.Check(validation.ApprovedSnapID(), Equals, "snap-id-2")
+ c.Check(validation.ApprovedSnapRevision(), Equals, 42)
+ c.Check(validation.Revoked(), Equals, false)
+ c.Check(validation.Revision(), Equals, 1)
+}
+
+const (
+ validationErrPrefix = "assertion validation: "
+)
+
+func (vs *validationSuite) TestDecodeInvalid(c *C) {
+ encoded := vs.makeValidEncoded()
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"series: 16\n", "", `"series" header is mandatory`},
+ {"series: 16\n", "series: \n", `"series" header should not be empty`},
+ {"snap-id: snap-id-1\n", "", `"snap-id" header is mandatory`},
+ {"snap-id: snap-id-1\n", "snap-id: \n", `"snap-id" header should not be empty`},
+ {"approved-snap-id: snap-id-2\n", "", `"approved-snap-id" header is mandatory`},
+ {"approved-snap-id: snap-id-2\n", "approved-snap-id: \n", `"approved-snap-id" header should not be empty`},
+ {"approved-snap-revision: 42\n", "", `"approved-snap-revision" header is mandatory`},
+ {"approved-snap-revision: 42\n", "approved-snap-revision: z\n", `"approved-snap-revision" header is not an integer: z`},
+ {"approved-snap-revision: 42\n", "approved-snap-revision: 0\n", `"approved-snap-revision" header must be >=1: 0`},
+ {"approved-snap-revision: 42\n", "approved-snap-revision: -1\n", `"approved-snap-revision" header must be >=1: -1`},
+ {vs.tsLine, "", `"timestamp" header is mandatory`},
+ {vs.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {vs.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, validationErrPrefix+test.expectedErr)
+ }
+}
+
+func prereqSnapDecl2(c *C, storeDB assertstest.SignerDB, db *asserts.Database) {
+ snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "snap-id-2",
+ "snap-name": "bar",
+ "publisher-id": "dev-id1",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = db.Add(snapDecl)
+ c.Assert(err, IsNil)
+}
+
+func (vs *validationSuite) TestValidationCheck(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+ devDB := setup3rdPartySigning(c, "dev-id1", storeDB, db)
+
+ prereqSnapDecl(c, storeDB, db)
+ prereqSnapDecl2(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ validation, err := devDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(validation)
+ c.Assert(err, IsNil)
+}
+
+func (vs *validationSuite) TestValidationCheckWrongAuthority(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+ prereqSnapDecl(c, storeDB, db)
+ prereqSnapDecl2(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ validation, err := storeDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(validation)
+ c.Assert(err, ErrorMatches, `validation assertion by snap "foo" \(id "snap-id-1"\) not signed by its publisher`)
+}
+
+func (vs *validationSuite) TestRevocation(c *C) {
+ encoded := "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revoked: true\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ validation := a.(*asserts.Validation)
+ c.Check(validation.Revoked(), Equals, true)
+}
+
+func (vs *validationSuite) TestRevokedFalse(c *C) {
+ encoded := "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revoked: false\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ validation := a.(*asserts.Validation)
+ c.Check(validation.Revoked(), Equals, false)
+}
+
+func (vs *validationSuite) TestRevokedInvalid(c *C) {
+ encoded := "type: validation\n" +
+ "authority-id: dev-id1\n" +
+ "series: 16\n" +
+ "snap-id: snap-id-1\n" +
+ "approved-snap-id: snap-id-2\n" +
+ "approved-snap-revision: 42\n" +
+ "revoked: foo\n" +
+ "revision: 1\n" +
+ vs.tsLine +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+ _, err := asserts.Decode([]byte(encoded))
+ c.Check(err, ErrorMatches, `.*: "revoked" header must be 'true' or 'false'`)
+}
+
+func (vs *validationSuite) TestMissingGatedSnapDeclaration(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ a, err := storeDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(a)
+ c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion for approved-snap-id "snap-id-2"`)
+}
+
+func (vs *validationSuite) TestMissingGatingSnapDeclaration(c *C) {
+ storeDB, db := makeStoreAndCheckDB(c)
+
+ prereqDevAccount(c, storeDB, db)
+ prereqSnapDecl2(c, storeDB, db)
+
+ headers := vs.makeHeaders(nil)
+ a, err := storeDB.Sign(asserts.ValidationType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(a)
+ c.Assert(err, ErrorMatches, `validation assertion by snap-id "snap-id-1" does not have a matching snap-declaration assertion`)
+}
+
+func (vs *validationSuite) TestPrerequisites(c *C) {
+ encoded := vs.makeValidEncoded()
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ prereqs := a.Prerequisites()
+ c.Assert(prereqs, HasLen, 2)
+ c.Check(prereqs[0], DeepEquals, &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "snap-id-1"},
+ })
+ c.Check(prereqs[1], DeepEquals, &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "snap-id-2"},
+ })
+
+}
diff --git a/asserts/snapasserts/snapasserts.go b/asserts/snapasserts/snapasserts.go
index 9c6067b90f..eadd6f6a94 100644
--- a/asserts/snapasserts/snapasserts.go
+++ b/asserts/snapasserts/snapasserts.go
@@ -126,3 +126,24 @@ func DeriveSideInfo(snapPath string, db asserts.RODatabase) (*snap.SideInfo, err
Developer: devAcct.Username(),
}, nil
}
+
+// FetchSnapAssertions fetches the assertions matching the snap file digest using the given fetcher.
+func FetchSnapAssertions(f asserts.Fetcher, snapSHA3_384 string) error {
+ // for now starting from the snap-revision will get us all other relevant assertions
+ ref := &asserts.Ref{
+ Type: asserts.SnapRevisionType,
+ PrimaryKey: []string{snapSHA3_384},
+ }
+
+ return f.Fetch(ref)
+}
+
+// FetchSnapDeclaration fetches the snap declaration and its prerequisites for the given snap id using the given fetcher.
+func FetchSnapDeclaration(f asserts.Fetcher, snapID string) error {
+ ref := &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{release.Series, snapID},
+ }
+
+ return f.Fetch(ref)
+}
diff --git a/asserts/systestkeys/trusted.go b/asserts/systestkeys/trusted.go
index 72390457de..90834f6823 100644
--- a/asserts/systestkeys/trusted.go
+++ b/asserts/systestkeys/trusted.go
@@ -113,6 +113,7 @@ C0QC8xuHSvOv3YRtzKna3smAfRlB
authority-id: testrootorg
public-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
account-id: testrootorg
+name: test-root
since: 2016-08-11T18:30:57+02:00
body-length: 717
sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
@@ -128,16 +129,16 @@ a1zzJoTKTqYWX9B+ZfENGKJUnhTP0x7Cm6lg3EUGay/b5hsA4DBoqShuf/N0jVLojdhxi3Ck/DBN
lqCD0zy4uzvinjX+b4ay+LKBE3N15AsfEkWIwzI+1OdDlOWWqOxJkM6lrQ5hRQ1fHZoCiGjHbjeE
1RIFO2TAw2tpyUcAEQEAAQ==
-AcLBUgQAAQoABgUCV6yoQQAAAaQQAJ+6saqG2DElfKZBbmthhlN8fHXSR8RX5LnbfE5zd4vTbthC
-//MjJtpUwq5vpM1/XB9p8cGZD1UlEdUa8l9N8oGSfJARZ+rAsPLlguzSoV4p6ph16HPlvBVt5npB
-DqK/Oxw+mtx2cnxn8X9Zw3wyz4mXp3cuu7PwSQvFSvcrxoNIOVkaHYEytQqqvZp8Lq1AirllGEL8
-EocRLOiG0O99P3BJytLWLYePRJ6qToiz58WuZEVj2lkC+HqrIoVrjgFAUlq100R15xgc4WtNFdWr
-hInauQxco+/vwHvCgxa/Ky+dABY/W+D9fuM7kjrhh/zqQiiIRGhfAndoi9I7Q/FISrECckZEN0yb
-N3ntOkTJpCnonTfGW6S0VDfGjQreekEU4nwYk3ewdCDY9n9N4zOPmylqU3u2lLJJNsi9rHWWYTOM
-9tXI1yocgrbKaQ8WQQeBQx0SVFdWOl+NvsGcKvs/7qm7SWr/pXo4F+MabqIzX1bb/WvgarpDiGYB
-p+ELFp1KRq+vS0qtP1fggrhyGmuQFeSf411cXKa21h870GcaBlmbZZMB/C1lD5fPG1WsrT7DO2Yu
-Uhf1Q4y+kAgxqL7zZUqJogpxNgw3He66uB7V7hf/UpOfFNeQZaZDCfSzbz/fNzNvNaqiMh6OUrbd
-k9v1ImHrPI6+o+xjCbMc2xdRcvM+
+AcLBXAQAAQoABgUCV8656QAKCRBMcZp594FxpNWlEADQgBlROdBTHpdZ3/9BbasxenUC3VXusMeK
+0DmnsHrsAsyVk6xiHQQ3hWxvXKWoDkDsOhUqcQTsDBcIaZ18+qwpQciyItd+w3d7SSJ+MKSUpwsB
+NOdgw1ykj7l1M/W7xAAPscFoV1xVSk9+rsLYFYDe23R+ecyotSmF+4QHj5b+hXeVIOUaqQTl5xPC
+h0zVYNIUWv42q4Z+hiBS8+8UJ0G+7z/27XORkGHY6TXCt0aph7s5egr8Lm+/jq7c95HVsa7DwSpv
+SqPajRnlyLiHFXUYAUPEU9oDgPwtLsqUkFfrv1WZ3ja1rDexgKBta+8BRyCAq3gPcMAjhiHXdjoW
+90p893l9N6K82RiEOO9ic0pEezjQldg97oU+ajXNm3ryns+HX6hRd39rpzIsrbVdbCqun4RwMbCM
+EVxgC/cuxMGcS40Co3O8wG3H/WIWOqcRQfolQTexmyzQljYt9WyWJdXmtPtaMzQGbOqE/dIjOK9j
+xvrghVU4kX6fJFwPi+azMrluHV+WGSVxPCuLW8o2aipjOd1/bUQCL5OwRuaEWuLCiV01J8H/JjWV
+hL4gGVqEM2KEPIDwY2yqX36jE7uN9O+mIPnS4Tdj0JQ5ZD1qh34wv+4QvhgNeyP120nuS1ykO9X0
+A806uPC5QK1+cgRMUz8zJ0afDNwE/DvpBQvE5CIi9A==
`
TestStorePrivKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
@@ -204,6 +205,7 @@ o7ZSZ/h/bUY1EjE2
authority-id: testrootorg
public-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
account-id: testrootorg
+name: test-store
since: 2016-08-11T18:42:22+02:00
body-length: 717
sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
@@ -219,16 +221,16 @@ iAIwA4DpGMmFJ26maqVzJuiLvicri2FR/sJaSA24N8HbGne3gSS7WrSQS+jKe3IZPVy64NCoGvrW
o/HvTeqsIfihKPEpXm8QVtjNhtkVn3RdIUgOaNWyAfnZ4dW1TVIATe+OHDw2TNyImTjE0x75nL6B
1/Rrn+9VP9Swhv8AEQEAAQ==
-AcLBUgQAAQoABgUCV6yq7gAAhaYQAMIVYhta2uUvm5PXApdXmZFWr+iZYfkAZW8PEMOsuYVHbDoH
-oA7dpO0EwZXl/mCgGjNNc4nUmqQLBiIwwrcnmcYSRl2Xz+u+ssou4YMueXOD2tHo1N2J39SKpS72
-VqQsnF77Qylgdp2j7Q9lJrU0qHz0M195OJXNSppfdHYeWptfsO02cApPobU9s6KT5VggVg1ushNM
-1u97A+uvoClfJ53PPafC0kr1+vwFVPj+mko4gc7sIB42xwz+YeR47CgSaT8i8K1u5ouaHCNxe61+
-siQxAdIOv+hOAWAOMoOWZjxh5K7J9A1Dc18EMyggf716IUCBtkvDKfwcXFcoic1X6EVPO5kNhlh7
-aLFS8UVsBPaZKnJm3ZFudhYQUmZt22ntslgGrq9+NBeh9i6nswUPKdj2idHkyQsjnYgYyTpEH/xh
-sBqkqkedPkUtn+tP5dS4TP/L3Xq/q2tQbA4+gNVbZXIo8g8wfpsP8+sIOnEV5UcoxaO3oI3YUJrN
-oFGmC7No802XKG1ZNHhBtMSaan0pafWrBrHn+axT9Jbl/1B73TYg3zQWOpDuSpH3SU+gXJ/eukUb
-LvC8UWZM9YEhu/ZINSvSDQzvobV/NVrHWEJBXxrc3CwseFDgQq+Yz/CFGud79z2mS8lNHKSqQdFK
-3Bd0HG1HheYcX0nGIva0KIG0Sgf/
+AcLBXAQAAQoABgUCV866kwAKCRBMcZp594FxpHWHD/9AaZXqyT/Zsmq/VzmAMpd9JvCH4PHQKtAP
+bXfP2Dnpa2wk2wuzQuSWunR8NDRyVh/aNVeTEZ9dFm/B8LR+U2O4rsHmFSeicmsTmo9u/HouRdEU
+zeSc6cbAxMPpfNSjr5J+URLjGRT6oX5fEBmRPx/OC9pEIScMx7uKmTKEnuyMzLRNN/6HiGWKrFCo
+nJdKkwRXrkCHyXWAOv1GumT7NDuyFcjAqt/UdHliTZkDBImKOsBmBVXMUjg7HCSS2uq/5WjStJ+B
+JHQ4GSsXBvVINs6BncNWcvV6mCQ73D57MzGhqo997Zb4tSrn7UNGWK7GLCzV3e/pFlG7pw6HbgnQ
++rxU2Oj/TPVw0tcnUiRl2ttKpm+nua0Cl+MD+Gx0KXLAVp0ZGOQ9yGyP9AePFzcOR8SlRIgxi0EI
+iJkSeYilqoKo3AJhnICRiqvAca2TGJoiJUryEgZ8jbTOElfaF2p+y0xvXGlWbKZm1gzGyvFM5fV5
+hJTlp/am+2uVn6U8wPACir4PrbuXYo7L4MIXww2OEO0ruBIaLARbc5IutSWmw6AEYQUxtsa9bdHV
+Zin7LGbEj6lZm8GycWQwh4B6Vnt6dJRIyPc/9G7uM8Ds/2Wa7+yAxhiPqm8DwlbOYh1npw4X4TLD
+IMGnTv5N3zllI+Xz4rqJzNTzEbvOIcrqWxCedQe79A==
`
)
diff --git a/cmd/snap/cmd_abort.go b/cmd/snap/cmd_abort.go
index 5835df95b9..7fd468f327 100644
--- a/cmd/snap/cmd_abort.go
+++ b/cmd/snap/cmd_abort.go
@@ -27,7 +27,7 @@ import (
type cmdAbort struct {
Positional struct {
- ID string `positional-arg-name:"change-id"`
+ ID string
} `positional-args:"yes" required:"yes"`
}
@@ -43,7 +43,10 @@ func init() {
longAbortHelp,
func() flags.Commander {
return &cmdAbort{}
- })
+ },
+ nil,
+ []argDesc{{name: i18n.G("<change-id>")}},
+ )
}
func (x *cmdAbort) Execute(args []string) error {
diff --git a/cmd/snap/cmd_ack.go b/cmd/snap/cmd_ack.go
index f96ab4550c..4c4756920f 100644
--- a/cmd/snap/cmd_ack.go
+++ b/cmd/snap/cmd_ack.go
@@ -29,7 +29,7 @@ import (
type cmdAck struct {
AckOptions struct {
- AssertionFile string `positional-arg-name:"<assertion file>" description:"assertion file"`
+ AssertionFile string
} `positional-args:"true" required:"true"`
}
@@ -48,7 +48,10 @@ database.
func init() {
addCommand("ack", shortAckHelp, longAckHelp, func() flags.Commander {
return &cmdAck{}
- })
+ }, nil, []argDesc{{
+ name: i18n.G("<assertion file>"),
+ desc: i18n.G("Assertion file"),
+ }})
}
func (x *cmdAck) Execute(args []string) error {
diff --git a/cmd/snap/cmd_booted.go b/cmd/snap/cmd_booted.go
index 6af94f3c62..e74fdef6ab 100644
--- a/cmd/snap/cmd_booted.go
+++ b/cmd/snap/cmd_booted.go
@@ -39,7 +39,7 @@ func init() {
"internal",
func() flags.Commander {
return &cmdBooted{}
- })
+ }, nil, nil)
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_buy.go b/cmd/snap/cmd_buy.go
index 9d58ddd14f..aa32f125e4 100644
--- a/cmd/snap/cmd_buy.go
+++ b/cmd/snap/cmd_buy.go
@@ -44,17 +44,22 @@ var positiveResponse = map[string]bool{
}
type cmdBuy struct {
- Currency string `long:"currency" description:"ISO 4217 code for currency (https://en.wikipedia.org/wiki/ISO_4217)"`
+ Currency string `long:"currency"`
Positional struct {
- SnapName string `positional-arg-name:"<snap-name>"`
+ SnapName string
} `positional-args:"yes" required:"yes"`
}
func init() {
addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander {
return &cmdBuy{}
- })
+ }, map[string]string{
+ "currency": i18n.G("ISO 4217 code for currency (https://en.wikipedia.org/wiki/ISO_4217)"),
+ }, []argDesc{{
+ name: "<snap>",
+ desc: i18n.G("Snap name"),
+ }})
}
func (x *cmdBuy) Execute(args []string) error {
@@ -71,6 +76,10 @@ func (x *cmdBuy) Execute(args []string) error {
func buySnap(opts *store.BuyOptions) error {
cli := Client()
+ if !cli.LoggedIn() {
+ return fmt.Errorf(i18n.G("You need to be logged in to purchase software. Please run 'snap login' and try again."))
+ }
+
if strings.ContainsAny(opts.SnapName, ":*") {
return fmt.Errorf(i18n.G("cannot buy snap %q: invalid characters in name"), opts.SnapName)
}
diff --git a/cmd/snap/cmd_buy_test.go b/cmd/snap/cmd_buy_test.go
index 313fa88df8..e11b021974 100644
--- a/cmd/snap/cmd_buy_test.go
+++ b/cmd/snap/cmd_buy_test.go
@@ -29,6 +29,10 @@ import (
snap "github.com/snapcore/snapd/cmd/snap"
)
+type BuySnapSuite struct {
+ SnapSuite
+}
+
type expectedURL struct {
Body string
Checker func(r *http.Request)
@@ -72,15 +76,25 @@ func (s *buyTestMockSnapServer) checkCounts() {
}
}
-func (s *SnapSuite) TestBuyHelp(c *check.C) {
+func (s *BuySnapSuite) SetUpTest(c *check.C) {
+ s.SnapSuite.SetUpTest(c)
+ s.Login(c)
+}
+
+func (s *BuySnapSuite) TearDownTest(c *check.C) {
+ s.Logout(c)
+ s.SnapSuite.TearDownTest(c)
+}
+
+func (s *BuySnapSuite) TestBuyHelp(c *check.C) {
_, err := snap.Parser().ParseArgs([]string{"buy"})
c.Assert(err, check.NotNil)
- c.Check(err.Error(), check.Equals, "the required argument `<snap-name>` was not provided")
+ c.Check(err.Error(), check.Equals, "the required argument `<snap>` was not provided")
c.Check(s.Stdout(), check.Equals, "")
c.Check(s.Stderr(), check.Equals, "")
}
-func (s *SnapSuite) TestBuyInvalidCharacters(c *check.C) {
+func (s *BuySnapSuite) TestBuyInvalidCharacters(c *check.C) {
_, err := snap.Parser().ParseArgs([]string{"buy", "a:b"})
c.Assert(err, check.NotNil)
c.Check(err.Error(), check.Equals, "cannot buy snap \"a:b\": invalid characters in name")
@@ -125,7 +139,7 @@ const buyFreeSnapFailsFindJson = `
}
`
-func (s *SnapSuite) TestBuyFreeSnapFails(c *check.C) {
+func (s *BuySnapSuite) TestBuyFreeSnapFails(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -237,7 +251,7 @@ const buySnapJson = `
}
`
-func (s *SnapSuite) TestBuySnapAutomaticPayment(c *check.C) {
+func (s *BuySnapSuite) TestBuySnapAutomaticPayment(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -330,7 +344,7 @@ const buyMethodsSelectPaymentMethodJson = `
}
`
-func (s *SnapSuite) TestBuySnapSelectPaymentMethod(c *check.C) {
+func (s *BuySnapSuite) TestBuySnapSelectPaymentMethod(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -438,7 +452,7 @@ const buyMethodsSelectPaymentMethodWithDefaultJson = `
}
`
-func (s *SnapSuite) TestBuySnapSelectPaymentMethodWithDefault(c *check.C) {
+func (s *BuySnapSuite) TestBuySnapSelectPaymentMethodWithDefault(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -513,7 +527,7 @@ const buyNoPaymentMethodsJson = `
}
`
-func (s *SnapSuite) TestBuySnapFailsNoPaymentMethods(c *check.C) {
+func (s *BuySnapSuite) TestBuySnapFailsNoPaymentMethods(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -536,7 +550,7 @@ func (s *SnapSuite) TestBuySnapFailsNoPaymentMethods(c *check.C) {
c.Check(s.Stderr(), check.Equals, "")
}
-func (s *SnapSuite) TestBuySnapFailsInvalidMethodID(c *check.C) {
+func (s *BuySnapSuite) TestBuySnapFailsInvalidMethodID(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -566,7 +580,7 @@ Type a number to select payment method: `)
c.Check(s.Stderr(), check.Equals, "")
}
-func (s *SnapSuite) TestBuySnapFailsEmptyMethodID(c *check.C) {
+func (s *BuySnapSuite) TestBuySnapFailsEmptyMethodID(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -596,7 +610,7 @@ Type a number to select payment method: `)
c.Check(s.Stderr(), check.Equals, "")
}
-func (s *SnapSuite) TestBuySnapFailsOutOfRangeMethodID(c *check.C) {
+func (s *BuySnapSuite) TestBuySnapFailsOutOfRangeMethodID(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -626,7 +640,7 @@ Type a number to select payment method: `)
c.Check(s.Stderr(), check.Equals, "")
}
-func (s *SnapSuite) TestBuyCancel(c *check.C) {
+func (s *BuySnapSuite) TestBuyCancel(c *check.C) {
mockServer := &buyTestMockSnapServer{
ExpectedMethods: expectedMethods{
"GET": &expectedMethod{
@@ -649,3 +663,15 @@ func (s *SnapSuite) TestBuyCancel(c *check.C) {
c.Check(s.Stdout(), check.Equals, `Do you want to buy "hello" from "canonical" for 2.99GBP? (Y/n): `)
c.Check(s.Stderr(), check.Equals, "")
}
+
+func (s *BuySnapSuite) TestBuyFailsWithoutLogin(c *check.C) {
+ // We don't login here
+ s.Logout(c)
+
+ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"})
+ c.Check(err, check.NotNil)
+ c.Check(err.Error(), check.Equals, "You need to be logged in to purchase software. Please run 'snap login' and try again.")
+ c.Check(rest, check.DeepEquals, []string{"hello"})
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
diff --git a/cmd/snap/cmd_changes.go b/cmd/snap/cmd_changes.go
index afe9c79195..11c96f379a 100644
--- a/cmd/snap/cmd_changes.go
+++ b/cmd/snap/cmd_changes.go
@@ -51,8 +51,8 @@ type cmdChange struct {
}
func init() {
- addCommand("changes", shortChangesHelp, longChangesHelp, func() flags.Commander { return &cmdChanges{} })
- addCommand("change", shortChangeHelp, longChangeHelp, func() flags.Commander { return &cmdChange{} })
+ addCommand("changes", shortChangesHelp, longChangesHelp, func() flags.Commander { return &cmdChanges{} }, nil, nil)
+ addCommand("change", shortChangeHelp, longChangeHelp, func() flags.Commander { return &cmdChange{} }, nil, nil)
}
type changesByTime []*client.Change
diff --git a/cmd/snap/cmd_connect.go b/cmd/snap/cmd_connect.go
index d8ad807637..cc32889983 100644
--- a/cmd/snap/cmd_connect.go
+++ b/cmd/snap/cmd_connect.go
@@ -27,8 +27,8 @@ import (
type cmdConnect struct {
Positionals struct {
- Offer SnapAndName `positional-arg-name:"<snap>:<plug>" required:"true"`
- Use SnapAndName `positional-arg-name:"<snap>:<slot>" required:"true"`
+ Offer SnapAndName `required:"true"`
+ Use SnapAndName `required:"true"`
} `positional-args:"true" required:"true"`
}
@@ -58,6 +58,9 @@ proceeds as above.
func init() {
addCommand("connect", shortConnectHelp, longConnectHelp, func() flags.Commander {
return &cmdConnect{}
+ }, nil, []argDesc{
+ {name: i18n.G("<snap>:<plug>")},
+ {name: i18n.G("<snap>:<slot>")},
})
}
diff --git a/cmd/snap/cmd_connect_test.go b/cmd/snap/cmd_connect_test.go
index 9f32984d14..e1d9416230 100644
--- a/cmd/snap/cmd_connect_test.go
+++ b/cmd/snap/cmd_connect_test.go
@@ -53,7 +53,7 @@ first of these snaps that has a matching plug name is used and the command
proceeds as above.
Application Options:
- --version print the version and exit
+ --version Print the version and exit
Help Options:
-h, --help Show this help message
diff --git a/cmd/snap/cmd_create_key.go b/cmd/snap/cmd_create_key.go
index 89c2fc3988..63ae5d2742 100644
--- a/cmd/snap/cmd_create_key.go
+++ b/cmd/snap/cmd_create_key.go
@@ -32,7 +32,7 @@ import (
type cmdCreateKey struct {
Positional struct {
- KeyName string `positional-arg-name:"<key-name>" description:"name of key to create; defaults to 'default'"`
+ KeyName string
} `positional-args:"true"`
}
@@ -42,7 +42,10 @@ func init() {
i18n.G("Create a cryptographic key pair that can be used for signing assertions."),
func() flags.Commander {
return &cmdCreateKey{}
- })
+ }, nil, []argDesc{{
+ name: i18n.G("<key-name>"),
+ desc: i18n.G("Name of key to create; defaults to 'default'"),
+ }})
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_create_user.go b/cmd/snap/cmd_create_user.go
index 6b07a9ab9d..4c17124bfd 100644
--- a/cmd/snap/cmd_create_user.go
+++ b/cmd/snap/cmd_create_user.go
@@ -38,15 +38,24 @@ An account can be setup at https://login.ubuntu.com.
`)
type cmdCreateUser struct {
- JSON bool `long:"json" description:"output results in JSON format"`
- Sudoer bool `long:"sudoer" description:"grant sudo access to the created user"`
+ JSON bool `long:"json"`
+ Sudoer bool `long:"sudoer"`
Positional struct {
- Email string `positional-arg-name:"email"`
+ Email string
} `positional-args:"yes"`
}
func init() {
- addCommand("create-user", shortCreateUserHelp, longCreateUserHelp, func() flags.Commander { return &cmdCreateUser{} })
+ addCommand("create-user", shortCreateUserHelp, longCreateUserHelp, func() flags.Commander { return &cmdCreateUser{} },
+ map[string]string{
+ "json": i18n.G("Output results in JSON format"),
+ "sudoer": i18n.G("Grant sudo access to the created user"),
+ }, []argDesc{{
+ // TRANSLATORS: noun
+ name: i18n.G("<email>"),
+ // TRANSLATORS: note users on login.ubuntu.com can have multiple email addresses
+ desc: i18n.G("An email of a user on login.ubuntu.com"),
+ }})
}
func (x *cmdCreateUser) Execute(args []string) error {
diff --git a/cmd/snap/cmd_delete_key.go b/cmd/snap/cmd_delete_key.go
index c8412e9781..91ef986ce5 100644
--- a/cmd/snap/cmd_delete_key.go
+++ b/cmd/snap/cmd_delete_key.go
@@ -28,7 +28,7 @@ import (
type cmdDeleteKey struct {
Positional struct {
- KeyName string `positional-arg-name:"<key-name>" description:"name of key to delete"`
+ KeyName string
} `positional-args:"true" required:"true"`
}
@@ -38,7 +38,10 @@ func init() {
i18n.G("Delete the local cryptographic key pair with the given name."),
func() flags.Commander {
return &cmdDeleteKey{}
- })
+ }, nil, []argDesc{{
+ name: i18n.G("<key-name>"),
+ desc: i18n.G("Name of key to delete"),
+ }})
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_disconnect.go b/cmd/snap/cmd_disconnect.go
index 447bef1559..3c40ccbe43 100644
--- a/cmd/snap/cmd_disconnect.go
+++ b/cmd/snap/cmd_disconnect.go
@@ -27,8 +27,8 @@ import (
type cmdDisconnect struct {
Positionals struct {
- Offer SnapAndName `positional-arg-name:"<snap>:<plug>" required:"true"`
- Use SnapAndName `positional-arg-name:"<snap>:<slot>"`
+ Offer SnapAndName `required:"true"`
+ Use SnapAndName
} `positional-args:"true"`
}
@@ -53,6 +53,9 @@ Disconnects all plugs from the provided snap.
func init() {
addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander {
return &cmdDisconnect{}
+ }, nil, []argDesc{
+ {name: i18n.G("<snap>:<plug>")},
+ {name: i18n.G("<snap>:<slot>")},
})
}
diff --git a/cmd/snap/cmd_disconnect_test.go b/cmd/snap/cmd_disconnect_test.go
index f93a2c29f2..e8d84d32f5 100644
--- a/cmd/snap/cmd_disconnect_test.go
+++ b/cmd/snap/cmd_disconnect_test.go
@@ -48,7 +48,7 @@ $ snap disconnect <snap>
Disconnects all plugs from the provided snap.
Application Options:
- --version print the version and exit
+ --version Print the version and exit
Help Options:
-h, --help Show this help message
diff --git a/cmd/snap/cmd_download.go b/cmd/snap/cmd_download.go
index 2838e025f1..132e53c8cd 100644
--- a/cmd/snap/cmd_download.go
+++ b/cmd/snap/cmd_download.go
@@ -36,10 +36,10 @@ import (
type cmdDownload struct {
channelMixin
- Revision string `long:"revision" description:"Download the given revision of a snap, to which you must have developer access"`
+ Revision string `long:"revision"`
Positional struct {
- Snap string `positional-arg-name:"<snap>" description:"snap name"`
+ Snap string
} `positional-args:"true" required:"true"`
}
@@ -51,7 +51,12 @@ The download command will download the given snap and its supporting assertions
func init() {
addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander {
return &cmdDownload{}
- })
+ }, channelDescs.also(map[string]string{
+ "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"),
+ }), []argDesc{{
+ name: "<snap>",
+ desc: i18n.G("Snap name"),
+ }})
}
func fetchSnapAssertions(sto *store.Store, snapPath string, snapInfo *snap.Info, dlOpts *image.DownloadOptions) error {
@@ -75,7 +80,7 @@ func fetchSnapAssertions(sto *store.Store, snapPath string, snapInfo *snap.Info,
}
f := image.StoreAssertionFetcher(sto, dlOpts, db, save)
- return image.FetchSnapAssertions(snapPath, snapInfo, f, db)
+ return image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db)
}
func (x *cmdDownload) Execute(args []string) error {
diff --git a/cmd/snap/cmd_export_key.go b/cmd/snap/cmd_export_key.go
index 9c6f457647..3979fd3766 100644
--- a/cmd/snap/cmd_export_key.go
+++ b/cmd/snap/cmd_export_key.go
@@ -30,9 +30,9 @@ import (
)
type cmdExportKey struct {
- Account string `long:"account" description:"format public key material as a request for an account-key for this account-id"`
+ Account string `long:"account"`
Positional struct {
- KeyName string `positional-arg-name:"<key-name>" description:"name of key to export"`
+ KeyName string
} `positional-args:"true"`
}
@@ -42,7 +42,12 @@ func init() {
i18n.G("Export a public key assertion body that may be imported by other systems."),
func() flags.Commander {
return &cmdExportKey{}
- })
+ }, map[string]string{
+ "account": i18n.G("Format public key material as a request for an account-key for this account-id"),
+ }, []argDesc{{
+ name: i18n.G("<key-name>"),
+ desc: i18n.G("Name of key to export"),
+ }})
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_find.go b/cmd/snap/cmd_find.go
index bb93e2499f..b2adfadf60 100644
--- a/cmd/snap/cmd_find.go
+++ b/cmd/snap/cmd_find.go
@@ -85,16 +85,18 @@ func getPriceString(prices map[string]float64, suggestedCurrency, status string)
}
type cmdFind struct {
- Private bool `long:"private" description:"search private snaps"`
+ Private bool `long:"private"`
Positional struct {
- Query string `positional-arg-name:"<query>"`
+ Query string
} `positional-args:"yes"`
}
func init() {
addCommand("find", shortFindHelp, longFindHelp, func() flags.Commander {
return &cmdFind{}
- })
+ }, map[string]string{
+ "private": i18n.G("Search private snaps"),
+ }, []argDesc{{name: i18n.G("<query>")}})
}
func (x *cmdFind) Execute(args []string) error {
diff --git a/cmd/snap/cmd_first_boot.go b/cmd/snap/cmd_first_boot.go
index 4064aefe24..fb6eede034 100644
--- a/cmd/snap/cmd_first_boot.go
+++ b/cmd/snap/cmd_first_boot.go
@@ -32,7 +32,7 @@ func init() {
"internal",
"internal", func() flags.Commander {
return &cmdInternalFirstBoot{}
- })
+ }, nil, nil)
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_get.go b/cmd/snap/cmd_get.go
index bdc4e3ceba..ba83bc0610 100644
--- a/cmd/snap/cmd_get.go
+++ b/cmd/snap/cmd_get.go
@@ -35,15 +35,27 @@ The get command prints the configuration for the given snap.`)
type cmdGet struct {
Positional struct {
- Snap string `positional-arg-name:"<snap name>" description:"the snap whose conf is being requested"`
- Keys []string `positional-arg-name:"<keys>" description:"key of interest within the confuration"`
+ Snap string
+ Keys []string
} `positional-args:"yes" required:"yes"`
- Document bool `short:"d" description:"always return document, even with single key"`
+ Document bool `short:"d"`
}
func init() {
- addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} })
+ addCommand("get", shortGetHelp, longGetHelp, func() flags.Commander { return &cmdGet{} },
+ map[string]string{
+ "d": i18n.G("Always return document, even with single key"),
+ }, []argDesc{
+ {
+ name: "<snap>",
+ desc: i18n.G("The snap whose conf is being requested"),
+ },
+ {
+ name: i18n.G("<key>"),
+ desc: i18n.G("Key of interest within the configuration"),
+ },
+ })
}
func (x *cmdGet) Execute(args []string) error {
diff --git a/cmd/snap/cmd_help.go b/cmd/snap/cmd_help.go
index 8470db02d6..033c91244e 100644
--- a/cmd/snap/cmd_help.go
+++ b/cmd/snap/cmd_help.go
@@ -32,12 +32,13 @@ var longHelpHelp = i18n.G(`
How help for the snap command.`)
type cmdHelp struct {
- Manpage bool `long:"man" description:"Generate the manpage"`
+ Manpage bool `long:"man"`
parser *flags.Parser
}
func init() {
- addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} })
+ addCommand("help", shortHelpHelp, longHelpHelp, func() flags.Commander { return &cmdHelp{} },
+ map[string]string{"man": i18n.G("Generate the manpage")}, nil)
}
func (cmd *cmdHelp) setParser(parser *flags.Parser) {
diff --git a/cmd/snap/cmd_help_test.go b/cmd/snap/cmd_help_test.go
index 47fbb59540..0ee3c01c77 100644
--- a/cmd/snap/cmd_help_test.go
+++ b/cmd/snap/cmd_help_test.go
@@ -48,7 +48,7 @@ platform.
Application Options:
- +--version +print the version and exit
+ +--version +Print the version and exit
Help Options:
+-h, --help +Show this help message
diff --git a/cmd/snap/cmd_interfaces.go b/cmd/snap/cmd_interfaces.go
index 7b3b1df9ca..72efec5751 100644
--- a/cmd/snap/cmd_interfaces.go
+++ b/cmd/snap/cmd_interfaces.go
@@ -28,9 +28,9 @@ import (
)
type cmdInterfaces struct {
- Interface string `short:"i" description:"constrain listing to specific interfaces"`
+ Interface string `short:"i"`
Positionals struct {
- Query SnapAndName `positional-arg-name:"<snap>:<slot or plug>" description:"snap or snap:name" skip-help:"true"`
+ Query SnapAndName `skip-help:"true"`
} `positional-args:"true"`
}
@@ -56,7 +56,12 @@ Filters the complete output so only plugs and/or slots matching the provided det
func init() {
addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander {
return &cmdInterfaces{}
- })
+ }, map[string]string{
+ "i": i18n.G("Constrain listing to specific interfaces"),
+ }, []argDesc{{
+ name: i18n.G("<snap>:<slot or plug>"),
+ desc: i18n.G("Constrain listing to a specific snap or snap:name"),
+ }})
}
func (x *cmdInterfaces) Execute(args []string) error {
diff --git a/cmd/snap/cmd_interfaces_test.go b/cmd/snap/cmd_interfaces_test.go
index aa5ac648a6..716395549d 100644
--- a/cmd/snap/cmd_interfaces_test.go
+++ b/cmd/snap/cmd_interfaces_test.go
@@ -51,16 +51,16 @@ Filters the complete output so only plugs and/or slots matching the provided
details are listed.
Application Options:
- --version print the version and exit
+ --version Print the version and exit
Help Options:
-h, --help Show this help message
[interfaces command options]
- -i= constrain listing to specific interfaces
+ -i= Constrain listing to specific interfaces
[interfaces command arguments]
- <snap>:<slot or plug>: snap or snap:name
+ <snap>:<slot or plug>: Constrain listing to a specific snap or snap:name
`
rest, err := Parser().ParseArgs([]string{"interfaces", "--help"})
c.Assert(err.Error(), Equals, msg)
diff --git a/cmd/snap/cmd_keys.go b/cmd/snap/cmd_keys.go
index aea0c7e6ba..e4206cc222 100644
--- a/cmd/snap/cmd_keys.go
+++ b/cmd/snap/cmd_keys.go
@@ -30,7 +30,7 @@ import (
)
type cmdKeys struct {
- JSON bool `long:"json" description:"output results in JSON format"`
+ JSON bool `long:"json"`
}
func init() {
@@ -39,7 +39,7 @@ func init() {
i18n.G("List cryptographic keys that can be used for signing assertions."),
func() flags.Commander {
return &cmdKeys{}
- })
+ }, map[string]string{"json": i18n.G("Output results in JSON format")}, nil)
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_keys_test.go b/cmd/snap/cmd_keys_test.go
index 12356e70b9..af20d05bf3 100644
--- a/cmd/snap/cmd_keys_test.go
+++ b/cmd/snap/cmd_keys_test.go
@@ -21,6 +21,7 @@ package main_test
import (
"encoding/json"
+ "fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -32,9 +33,30 @@ import (
type SnapKeysSuite struct {
BaseSnapSuite
+
+ GnupgCmd string
}
-var _ = Suite(&SnapKeysSuite{})
+// FIXME: Ideally we would just use gpg2 and remove the gnupg2_test.go file.
+// However currently there is LP: #1621839 which prevents us from
+// switching to gpg2 fully. Once this is resolved we should switch.
+var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg"})
+
+var fakePinentryData = []byte(`#!/bin/sh
+set -e
+echo "OK Pleased to meet you"
+while true; do
+ read line
+ case $line in
+ BYE)
+ exit 0
+ ;;
+ *)
+ echo "OK I agree to everything"
+ ;;
+esac
+done
+`)
func (s *SnapKeysSuite) SetUpTest(c *C) {
s.BaseSnapSuite.SetUpTest(c)
@@ -46,11 +68,20 @@ func (s *SnapKeysSuite) SetUpTest(c *C) {
err = ioutil.WriteFile(filepath.Join(tempdir, fileName), data, 0644)
c.Assert(err, IsNil)
}
+ fakePinentryFn := filepath.Join(tempdir, "pinentry-fake")
+ err := ioutil.WriteFile(fakePinentryFn, fakePinentryData, 0755)
+ c.Assert(err, IsNil)
+ gpgAgentConfFn := filepath.Join(tempdir, "gpg-agent.conf")
+ err = ioutil.WriteFile(gpgAgentConfFn, []byte(fmt.Sprintf(`pinentry-program %s`, fakePinentryFn)), 0644)
+ c.Assert(err, IsNil)
+
os.Setenv("SNAP_GNUPG_HOME", tempdir)
+ os.Setenv("SNAP_GNUPG_CMD", s.GnupgCmd)
}
func (s *SnapKeysSuite) TearDownTest(c *C) {
os.Unsetenv("SNAP_GNUPG_HOME")
+ os.Unsetenv("SNAP_GNUPG_CMD")
s.BaseSnapSuite.TearDownTest(c)
}
diff --git a/cmd/snap/cmd_known.go b/cmd/snap/cmd_known.go
index 54e49c1fb0..f40a33d7cb 100644
--- a/cmd/snap/cmd_known.go
+++ b/cmd/snap/cmd_known.go
@@ -31,8 +31,8 @@ import (
type cmdKnown struct {
KnownOptions struct {
- AssertTypeName string `positional-arg-name:"<assertion type>" description:"assertion type name" required:"true"`
- HeaderFilters []string `positional-arg-name:"<header filters>" description:"header=value" required:"0"`
+ AssertTypeName string `required:"true"`
+ HeaderFilters []string `required:"0"`
} `positional-args:"true" required:"true"`
}
@@ -46,6 +46,14 @@ shown must also have the specified headers matching the provided values.
func init() {
addCommand("known", shortKnownHelp, longKnownHelp, func() flags.Commander {
return &cmdKnown{}
+ }, nil, []argDesc{
+ {
+ name: i18n.G("<assertion type>"),
+ desc: i18n.G("Assertion type name"),
+ }, {
+ name: i18n.G("<header filter>"),
+ desc: i18n.G("Constrain listing to those matching header=value"),
+ },
})
}
diff --git a/cmd/snap/cmd_list.go b/cmd/snap/cmd_list.go
index 2617abc5ec..2df8accca6 100644
--- a/cmd/snap/cmd_list.go
+++ b/cmd/snap/cmd_list.go
@@ -42,7 +42,7 @@ type cmdList struct {
}
func init() {
- addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} })
+ addCommand("list", shortListHelp, longListHelp, func() flags.Commander { return &cmdList{} }, nil, nil)
}
type snapsByName []*client.Snap
diff --git a/cmd/snap/cmd_login.go b/cmd/snap/cmd_login.go
index 2d5bfa2dc6..00ed677210 100644
--- a/cmd/snap/cmd_login.go
+++ b/cmd/snap/cmd_login.go
@@ -32,9 +32,7 @@ import (
type cmdLogin struct {
Positional struct {
- // FIXME: add support for translated descriptions
- // (see cmd/snappy/common.go:addOptionDescription)
- UserName string `positional-arg-name:"email" description:"login.ubuntu.com email to login as"`
+ UserName string
} `positional-args:"yes" required:"yes"`
}
@@ -56,7 +54,11 @@ func init() {
longLoginHelp,
func() flags.Commander {
return &cmdLogin{}
- })
+ }, nil, []argDesc{{
+ // TRANSLATORS: noun
+ name: i18n.G("<email>"),
+ desc: i18n.G("The login.ubuntu.com email to login as"),
+ }})
}
func requestLoginWith2faRetry(username, password string) error {
diff --git a/cmd/snap/cmd_logout.go b/cmd/snap/cmd_logout.go
index 71298cb799..993012cb33 100644
--- a/cmd/snap/cmd_logout.go
+++ b/cmd/snap/cmd_logout.go
@@ -37,7 +37,7 @@ func init() {
longLogoutHelp,
func() flags.Commander {
return &cmdLogout{}
- })
+ }, nil, nil)
}
func (cmd *cmdLogout) Execute(args []string) error {
diff --git a/cmd/snap/cmd_prepare_image.go b/cmd/snap/cmd_prepare_image.go
index b1341377f5..6d42a001a6 100644
--- a/cmd/snap/cmd_prepare_image.go
+++ b/cmd/snap/cmd_prepare_image.go
@@ -30,12 +30,12 @@ import (
type cmdPrepareImage struct {
Positional struct {
- ModelAssertionFn string `positional-arg-name:"model-assertion" description:"the model assertion name"`
- Rootdir string `long:"root-dir" description:"the output directory" `
+ ModelAssertionFn string
+ Rootdir string
} `positional-args:"yes" required:"yes"`
- ExtraSnaps []string `long:"extra-snaps" description:"extra snaps to be installed"`
- Channel string `long:"channel" description:"the channel to use"`
+ ExtraSnaps []string `long:"extra-snaps"`
+ Channel string `long:"channel"`
}
func init() {
@@ -44,6 +44,17 @@ func init() {
i18n.G("Prepare a snappy image"),
func() flags.Commander {
return &cmdPrepareImage{}
+ }, map[string]string{
+ "extra-snaps": "Extra snaps to be installed",
+ "channel": "The channel to use",
+ }, []argDesc{
+ {
+ name: i18n.G("<model-assertion>"),
+ desc: i18n.G("The model assertion name"),
+ }, {
+ name: i18n.G("<root-dir>"),
+ desc: i18n.G("The output directory"),
+ },
})
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go
index f43ef5a6a5..717bada98f 100644
--- a/cmd/snap/cmd_run.go
+++ b/cmd/snap/cmd_run.go
@@ -40,10 +40,10 @@ var (
)
type cmdRun struct {
- Command string `long:"command" description:"alternative command to run" hidden:"yes"`
- Hook string `long:"hook" description:"hook to run" hidden:"yes"`
- Revision string `short:"r" description:"use a specific snap revision when running hook" default:"unset" hidden:"yes"`
- Shell bool `long:"shell" description:"run a shell instead of the command (useful for debugging)"`
+ Command string `long:"command" hidden:"yes"`
+ Hook string `long:"hook" hidden:"yes"`
+ Revision string `short:"r" default:"unset" hidden:"yes"`
+ Shell bool `long:"shell" `
}
func init() {
@@ -52,7 +52,12 @@ func init() {
i18n.G("Run the given snap command with the right confinement and environment"),
func() flags.Commander {
return &cmdRun{}
- })
+ }, map[string]string{
+ "command": i18n.G("Alternative command to run"),
+ "hook": i18n.G("Hook to run"),
+ "r": i18n.G("Use a specific snap revision when running hook"),
+ "shell": i18n.G("Run a shell instead of the command (useful for debugging)"),
+ }, nil)
}
func (x *cmdRun) Execute(args []string) error {
@@ -114,25 +119,6 @@ func getSnapInfo(snapName string, revision snap.Revision) (*snap.Info, error) {
return info, nil
}
-// returns the environment that is important for the later stages of execution
-// (like SNAP_REVISION that snap-exec requires to work)
-func snapExecEnv(info *snap.Info) []string {
- home := os.Getenv("HOME")
- // HOME is not set for systemd services, so pull it out of passwd
- if home == "" {
- user, err := user.Current()
- if err == nil {
- home = user.HomeDir
- }
- }
-
- env := snapenv.Basic(info)
- if home != "" {
- env = append(env, snapenv.User(info, home)...)
- }
- return env
-}
-
func createUserDataDirs(info *snap.Info) error {
usr, err := userCurrent()
if err != nil {
@@ -212,7 +198,5 @@ func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string,
cmd = append(cmd, snapApp)
cmd = append(cmd, args...)
- env := append(os.Environ(), snapExecEnv(info)...)
-
- return syscallExec(cmd[0], cmd, env)
+ return syscallExec(cmd[0], cmd, snapenv.ExecEnv(info))
}
diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go
index 1ee921b965..1d799e9de1 100644
--- a/cmd/snap/cmd_run_test.go
+++ b/cmd/snap/cmd_run_test.go
@@ -25,11 +25,9 @@ import (
"os"
"os/user"
"path/filepath"
- "sort"
"gopkg.in/check.v1"
- "github.com/snapcore/snapd/arch"
snaprun "github.com/snapcore/snapd/cmd/snap"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
@@ -65,41 +63,6 @@ func (s *SnapSuite) TestInvalidParameters(c *check.C) {
c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"apply-config\": bar.*")
}
-func (s *SnapSuite) TestSnapRunSnapExecEnv(c *check.C) {
- info, err := snap.InfoFromSnapYaml(mockYaml)
- c.Assert(err, check.IsNil)
- info.SideInfo.Revision = snap.R(42)
-
- usr, err := user.Current()
- c.Assert(err, check.IsNil)
-
- homeEnv := os.Getenv("HOME")
- defer os.Setenv("HOME", homeEnv)
-
- for _, withHomeEnv := range []bool{true, false} {
- if !withHomeEnv {
- os.Setenv("HOME", "")
- }
-
- env := snaprun.SnapExecEnv(info)
- sort.Strings(env)
- c.Check(env, check.DeepEquals, []string{
- fmt.Sprintf("HOME=%s/snap/snapname/42", usr.HomeDir),
- fmt.Sprintf("SNAP=%s/snapname/42", dirs.SnapMountDir),
- fmt.Sprintf("SNAP_ARCH=%s", arch.UbuntuArchitecture()),
- "SNAP_COMMON=/var/snap/snapname/common",
- "SNAP_DATA=/var/snap/snapname/42",
- "SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:",
- "SNAP_NAME=snapname",
- "SNAP_REEXEC=",
- "SNAP_REVISION=42",
- fmt.Sprintf("SNAP_USER_COMMON=%s/snap/snapname/common", usr.HomeDir),
- fmt.Sprintf("SNAP_USER_DATA=%s/snap/snapname/42", usr.HomeDir),
- "SNAP_VERSION=1.0",
- })
- }
-}
-
func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) {
// mock installed snap
dirs.SetRootDir(c.MkDir())
@@ -391,3 +354,42 @@ func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) {
_, err := snaprun.Parser().ParseArgs([]string{"run", "--command=shell"})
c.Assert(err, check.ErrorMatches, "need the application to run as argument")
}
+
+func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) {
+ // mock installed snap
+ dirs.SetRootDir(c.MkDir())
+ defer func() { dirs.SetRootDir("/") }()
+
+ snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // and mock the server
+ s.mockServer(c)
+
+ // redirect exec
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // set a SNAP{,_*} variable in the environment
+ os.Setenv("SNAP_NAME", "something-else")
+ os.Setenv("SNAP_ARCH", "PDP-7")
+ defer os.Unsetenv("SNAP_NAME")
+ defer os.Unsetenv("SNAP_ARCH")
+ // but unreleated stuff is ok
+ os.Setenv("SNAP_THE_WORLD", "YES")
+ defer os.Unsetenv("SNAP_THE_WORLD")
+
+ // and ensure those SNAP_ vars get overriden
+ rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+ c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else")
+ c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7")
+ c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES")
+}
diff --git a/cmd/snap/cmd_set.go b/cmd/snap/cmd_set.go
index f3762f3428..c7088e183e 100644
--- a/cmd/snap/cmd_set.go
+++ b/cmd/snap/cmd_set.go
@@ -36,13 +36,21 @@ accepts a number of key=value pairs of parameters.`)
type cmdSet struct {
Positional struct {
- Snap string `positional-arg-name:"<snap name>" description:"the snap to configure (e.g. hello-world)"`
- ConfValues []string `positional-arg-name:"<conf value>" description:"configuration value (key=value)" required:"1"`
+ Snap string
+ ConfValues []string `required:"1"`
} `positional-args:"yes" required:"yes"`
}
func init() {
- addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} })
+ addCommand("set", shortSetHelp, longSetHelp, func() flags.Commander { return &cmdSet{} }, nil, []argDesc{
+ {
+ name: "<snap>",
+ desc: i18n.G("The snap to configure (e.g. hello-world)"),
+ }, {
+ name: i18n.G("<conf value>"),
+ desc: i18n.G("Configuration value (key=value)"),
+ },
+ })
}
func (x *cmdSet) Execute(args []string) error {
diff --git a/cmd/snap/cmd_shell.go b/cmd/snap/cmd_shell.go
index 37c773c926..25578aacdc 100644
--- a/cmd/snap/cmd_shell.go
+++ b/cmd/snap/cmd_shell.go
@@ -32,7 +32,7 @@ import (
type cmdShell struct {
Positional struct {
- ShellType string `positional-arg-name:"shell-type" description:"The type of shell you want"`
+ ShellType string
} `positional-args:"yes"`
}
@@ -44,7 +44,10 @@ func init() {
i18n.G("Run snappy shell interface"),
func() flags.Commander {
return &cmdShell{}
- })
+ }, nil, []argDesc{{
+ name: i18n.G("<shell-type>"),
+ desc: i18n.G("The type of shell you want"),
+ }})
}
*/
diff --git a/cmd/snap/cmd_sign.go b/cmd/snap/cmd_sign.go
index 97811251cb..136ab3ee2d 100644
--- a/cmd/snap/cmd_sign.go
+++ b/cmd/snap/cmd_sign.go
@@ -35,13 +35,13 @@ var longSignHelp = i18n.G(`Sign an assertion using the specified key, using the
`)
type cmdSign struct {
- KeyName string `short:"k" description:"name of the key to use, otherwise use the default key" default:"default"`
+ KeyName string `short:"k" default:"default"`
}
func init() {
cmd := addCommand("sign", shortSignHelp, longSignHelp, func() flags.Commander {
return &cmdSign{}
- })
+ }, map[string]string{"k": i18n.G("Name of the key to use, otherwise use the default key")}, nil)
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_sign_build.go b/cmd/snap/cmd_sign_build.go
index 7e2b1fc18f..9a9aae4e2e 100644
--- a/cmd/snap/cmd_sign_build.go
+++ b/cmd/snap/cmd_sign_build.go
@@ -33,13 +33,13 @@ import (
type cmdSignBuild struct {
Positional struct {
- Filename string `positional-arg-name:"<filename>" description:"filename of the snap you want to assert a build for"`
+ Filename string
} `positional-args:"yes" required:"yes"`
- DeveloperID string `long:"developer-id" description:"identifier of the signer" required:"yes"`
- SnapID string `long:"snap-id" description:"identifier of the snap package associated with the build" required:"yes"`
- KeyName string `short:"k" default:"default" description:"name of the GnuPG key to use (defaults to 'default' as key name)"`
- Grade string `long:"grade" choice:"devel" choice:"stable" default:"stable" description:"grade states the build quality of the snap (defaults to 'stable')"`
+ DeveloperID string `long:"developer-id" required:"yes"`
+ SnapID string `long:"snap-id" required:"yes"`
+ KeyName string `short:"k" default:"default" `
+ Grade string `long:"grade" choice:"devel" choice:"stable" default:"stable"`
}
var shortSignBuildHelp = i18n.G("Create snap build assertion")
@@ -51,7 +51,15 @@ func init() {
longSignBuildHelp,
func() flags.Commander {
return &cmdSignBuild{}
- })
+ }, map[string]string{
+ "developer-id": i18n.G("Identifier of the signer"),
+ "snap-id": i18n.G("Identifier of the snap package associated with the build"),
+ "k": i18n.G("Name of the GnuPG key to use (defaults to 'default' as key name)"),
+ "grade": i18n.G("Grade states the build quality of the snap (defaults to 'stable')"),
+ }, []argDesc{{
+ name: i18n.G("<filename>"),
+ desc: i18n.G("Filename of the snap you want to assert a build for"),
+ }})
cmd.hidden = true
}
diff --git a/cmd/snap/cmd_sign_test.go b/cmd/snap/cmd_sign_test.go
index fa797542e7..341551e7e7 100644
--- a/cmd/snap/cmd_sign_test.go
+++ b/cmd/snap/cmd_sign_test.go
@@ -1,5 +1,4 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
-// +build !integrationcoverage
/*
* Copyright (C) 2016 Canonical Ltd
diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go
index 0dbb796cfe..3eaf6da5b0 100644
--- a/cmd/snap/cmd_snap_op.go
+++ b/cmd/snap/cmd_snap_op.go
@@ -159,7 +159,7 @@ and the snap can easily be enabled again.
`)
type cmdRemove struct {
- Revision string `long:"revision" description:"Remove only the given revision"`
+ Revision string `long:"revision"`
Positional struct {
Snap string `positional-arg-name:"<snap>"`
} `positional-args:"yes" required:"yes"`
@@ -182,13 +182,34 @@ func (x *cmdRemove) Execute([]string) error {
}
type channelMixin struct {
- Channel string `long:"channel" description:"Use this channel instead of stable"`
+ Channel string `long:"channel"`
// shortcuts
- EdgeChannel bool `long:"edge" description:"Install from the edge channel"`
- BetaChannel bool `long:"beta" description:"Install from the beta channel"`
- CandidateChannel bool `long:"candidate" description:"Install from the candidate channel"`
- StableChannel bool `long:"stable" description:"Install from the stable channel"`
+ EdgeChannel bool `long:"edge"`
+ BetaChannel bool `long:"beta"`
+ CandidateChannel bool `long:"candidate"`
+ StableChannel bool `long:"stable" `
+}
+
+type mixinDescs map[string]string
+
+func (mxd mixinDescs) also(m map[string]string) mixinDescs {
+ n := make(map[string]string, len(mxd)+len(m))
+ for k, v := range mxd {
+ n[k] = v
+ }
+ for k, v := range m {
+ n[k] = v
+ }
+ return n
+}
+
+var channelDescs = mixinDescs{
+ "channel": i18n.G("Use this channel instead of stable"),
+ "beta": i18n.G("Install from the beta channel"),
+ "edge": i18n.G("Install from the edge channel"),
+ "candidate": i18n.G("Install from the candidate channel"),
+ "stable": i18n.G("Install from the stable channel"),
}
func (mx *channelMixin) setChannelFromCommandline() error {
@@ -251,8 +272,13 @@ func (mx *channelMixin) asksForChannel() bool {
}
type modeMixin struct {
- DevMode bool `long:"devmode" description:"Request non-enforcing security"`
- JailMode bool `long:"jailmode" description:"Override a snap's request for non-enforcing security"`
+ DevMode bool `long:"devmode"`
+ JailMode bool `long:"jailmode"`
+}
+
+var modeDescs = mixinDescs{
+ "devmode": i18n.G("Request non-enforcing security"),
+ "jailmode": i18n.G("Override a snap's request for non-enforcing security"),
}
var errModeConflict = errors.New(i18n.G("cannot use devmode and jailmode flags together"))
@@ -271,9 +297,12 @@ func (mx modeMixin) asksForMode() bool {
type cmdInstall struct {
channelMixin
modeMixin
- Revision string `long:"revision" description:"Install the given revision of a snap, to which you must have developer access"`
+ Revision string `long:"revision"`
- Dangerous bool `long:"dangerous" description:"Install the given snap file even if there are no pre-acknowledged signatures for it, meaning it was not verified and could be dangerous (--devmode implies this)"`
+ Dangerous bool `long:"dangerous"`
+ // alias for --dangerous, deprecated but we need to support it
+ // because we released 2.14.2 with --force-dangerous
+ ForceDangerous bool `long:"force-dangerous" hidden:"yes"`
Positional struct {
Snap string `positional-arg-name:"<snap>"`
@@ -294,7 +323,8 @@ func (x *cmdInstall) Execute([]string) error {
cli := Client()
name := x.Positional.Snap
- opts := &client.SnapOptions{Channel: x.Channel, DevMode: x.DevMode, JailMode: x.JailMode, Revision: x.Revision, Dangerous: x.Dangerous}
+ dangerous := x.Dangerous || x.ForceDangerous
+ opts := &client.SnapOptions{Channel: x.Channel, DevMode: x.DevMode, JailMode: x.JailMode, Revision: x.Revision, Dangerous: dangerous}
if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") {
installFromFile = true
changeID, err = cli.InstallPath(name, opts)
@@ -327,7 +357,8 @@ type cmdRefresh struct {
channelMixin
modeMixin
- List bool `long:"list" description:"show available snaps for refresh"`
+ Revision string `long:"revision"`
+ List bool `long:"list"`
Positional struct {
Snaps []string `positional-arg-name:"<snap>"`
} `positional-args:"yes"`
@@ -423,6 +454,7 @@ func (x *cmdRefresh) Execute([]string) error {
Channel: x.Channel,
DevMode: x.DevMode,
JailMode: x.JailMode,
+ Revision: x.Revision,
})
}
@@ -540,6 +572,7 @@ func (x *cmdDisable) Execute([]string) error {
type cmdRevert struct {
modeMixin
+ Revision string `long:"revision"`
Positional struct {
Snap string `positional-arg-name:"<snap>"`
} `positional-args:"yes"`
@@ -566,7 +599,7 @@ func (x *cmdRevert) Execute(args []string) error {
cli := Client()
name := x.Positional.Snap
- opts := &client.SnapOptions{DevMode: x.DevMode, JailMode: x.JailMode}
+ opts := &client.SnapOptions{DevMode: x.DevMode, JailMode: x.JailMode, Revision: x.Revision}
changeID, err := cli.Revert(name, opts)
if err != nil {
return err
@@ -591,11 +624,23 @@ func (x *cmdRevert) Execute(args []string) error {
}
func init() {
- addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} })
- addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} })
- addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} })
- addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} })
- addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} })
- addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} })
- addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} })
+ addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} },
+ map[string]string{"revision": i18n.G("Remove only the given revision")}, nil)
+ addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} },
+ channelDescs.also(modeDescs).also(map[string]string{
+ "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"),
+ "dangerous": i18n.G("Install the given snap file even if there are no pre-acknowledged signatures for it, meaning it was not verified and could be dangerous (--devmode implies this)"),
+ "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"),
+ }), nil)
+ addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} },
+ channelDescs.also(modeDescs).also(map[string]string{
+ "revision": i18n.G("Refresh to the given revision"),
+ "list": i18n.G("Show available snaps for refresh"),
+ }), nil)
+ addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, modeDescs, nil)
+ addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, nil, nil)
+ addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, nil, nil)
+ addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, modeDescs.also(map[string]string{
+ "revision": "Revert to the given revision",
+ }), nil)
}
diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go
index e9d97eb6f1..8778edb6b6 100644
--- a/cmd/snap/export_test.go
+++ b/cmd/snap/export_test.go
@@ -27,7 +27,6 @@ import (
var RunMain = run
var (
- SnapExecEnv = snapExecEnv
CreateUserDataDirs = createUserDataDirs
SnapRunApp = snapRunApp
SnapRunHook = snapRunHook
diff --git a/cmd/snap/gnupg2_test.go b/cmd/snap/gnupg2_test.go
new file mode 100644
index 0000000000..aa14290677
--- /dev/null
+++ b/cmd/snap/gnupg2_test.go
@@ -0,0 +1,27 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main_test
+
+import (
+ . "gopkg.in/check.v1"
+)
+
+// FIXME: drop once gpg2 is the default
+var _ = Suite(&SnapKeysSuite{GnupgCmd: "/usr/bin/gpg2"})
diff --git a/cmd/snap/main.go b/cmd/snap/main.go
index 5f6841e768..e9fa4ed7cc 100644
--- a/cmd/snap/main.go
+++ b/cmd/snap/main.go
@@ -26,6 +26,7 @@ import (
"os/user"
"path/filepath"
"strings"
+ "unicode"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/cmd"
@@ -45,7 +46,12 @@ var (
)
type options struct {
- Version func() `long:"version" description:"print the version and exit"`
+ Version func() `long:"version"`
+}
+
+type argDesc struct {
+ name string
+ desc string
}
var optionsData options
@@ -58,6 +64,8 @@ type cmdInfo struct {
name, shortHelp, longHelp string
builder func() flags.Commander
hidden bool
+ optDescs map[string]string
+ argDescs []argDesc
}
// commands holds information about all non-experimental commands.
@@ -68,12 +76,14 @@ var experimentalCommands []*cmdInfo
// addCommand replaces parser.addCommand() in a way that is compatible with
// re-constructing a pristine parser.
-func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander) *cmdInfo {
+func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo {
info := &cmdInfo{
name: name,
shortHelp: shortHelp,
longHelp: longHelp,
builder: builder,
+ optDescs: optDescs,
+ argDescs: argDescs,
}
commands = append(commands, info)
return info
@@ -97,6 +107,27 @@ type parserSetter interface {
setParser(*flags.Parser)
}
+func lintDesc(cmdName, optName, desc, origDesc string) {
+ if len(optName) == 0 {
+ logger.Panicf("option on %q has no name", cmdName)
+ }
+ if len(origDesc) != 0 {
+ logger.Panicf("description of %s's %q of %q set from tag (=> no i18n)", cmdName, optName, origDesc)
+ }
+ if len(desc) > 0 {
+ if !unicode.IsUpper(([]rune)(desc)[0]) {
+ logger.Panicf("description of %s's %q not uppercase: %q", cmdName, optName, desc)
+ }
+ }
+}
+
+func lintArg(cmdName, optName, desc, origDesc string) {
+ lintDesc(cmdName, optName, desc, origDesc)
+ if optName[0] != '<' || optName[len(optName)-1] != '>' {
+ logger.Panicf("argument %q's %q should have <>s", cmdName, optName)
+ }
+}
+
// Parser creates and populates a fresh parser.
// Since commands have local state a fresh parser is required to isolate tests
// from each other.
@@ -129,6 +160,7 @@ func Parser() *flags.Parser {
parser.LongDescription = i18n.G(`
The snap tool interacts with the snapd daemon to control the snappy software platform.
`)
+ parser.FindOptionByLongName("version").Description = i18n.G("Print the version and exit")
// Add all regular commands
for _, c := range commands {
@@ -143,6 +175,40 @@ The snap tool interacts with the snapd daemon to control the snappy software pla
logger.Panicf("cannot add command %q: %v", c.name, err)
}
cmd.Hidden = c.hidden
+
+ opts := cmd.Options()
+ if c.optDescs != nil && len(opts) != len(c.optDescs) {
+ logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs))
+ }
+ for _, opt := range opts {
+ name := opt.LongName
+ if name == "" {
+ name = string(opt.ShortName)
+ }
+ desc, ok := c.optDescs[name]
+ if !(c.optDescs == nil || ok) {
+ logger.Panicf("%s missing description for %s", c.name, name)
+ }
+ lintDesc(c.name, name, desc, opt.Description)
+ if desc != "" {
+ opt.Description = desc
+ }
+ }
+
+ args := cmd.Args()
+ if c.argDescs != nil && len(args) != len(c.argDescs) {
+ logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs))
+ }
+ for i, arg := range args {
+ name, desc := arg.Name, ""
+ if c.argDescs != nil {
+ name = c.argDescs[i].name
+ desc = c.argDescs[i].desc
+ }
+ lintArg(c.name, name, desc, arg.Description)
+ arg.Name = name
+ arg.Description = desc
+ }
}
// Add the experimental command
experimentalCommand, err := parser.AddCommand("experimental", shortExperimentalHelp, longExperimentalHelp, &cmdExperimental{})
diff --git a/cmd/snap/main_test.go b/cmd/snap/main_test.go
index bbdd7100b4..09c305d68f 100644
--- a/cmd/snap/main_test.go
+++ b/cmd/snap/main_test.go
@@ -26,11 +26,13 @@ import (
"net/http"
"net/http/httptest"
"os"
+ "path/filepath"
"testing"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/cmd"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/testutil"
snap "github.com/snapcore/snapd/cmd/snap"
@@ -44,6 +46,8 @@ type BaseSnapSuite struct {
stdin *bytes.Buffer
stdout *bytes.Buffer
stderr *bytes.Buffer
+
+ AuthFile string
}
func (s *BaseSnapSuite) SetUpTest(c *C) {
@@ -54,12 +58,17 @@ func (s *BaseSnapSuite) SetUpTest(c *C) {
snap.Stdin = s.stdin
snap.Stdout = s.stdout
snap.Stderr = s.stderr
+ s.AuthFile = filepath.Join(c.MkDir(), "json")
+ os.Setenv(TestAuthFileEnvKey, s.AuthFile)
}
func (s *BaseSnapSuite) TearDownTest(c *C) {
snap.Stdin = os.Stdin
snap.Stdout = os.Stdout
snap.Stderr = os.Stderr
+ c.Assert(s.AuthFile == "", Equals, false)
+ err := os.Unsetenv(TestAuthFileEnvKey)
+ c.Assert(err, IsNil)
s.BaseTest.TearDownTest(c)
}
@@ -78,6 +87,18 @@ func (s *BaseSnapSuite) RedirectClientToTestServer(handler func(http.ResponseWri
s.BaseTest.AddCleanup(func() { snap.ClientConfig.BaseURL = "" })
}
+func (s *BaseSnapSuite) Login(c *C) {
+ err := osutil.AtomicWriteFile(s.AuthFile, []byte(TestAuthFileContents), 0600, 0)
+ c.Assert(err, IsNil)
+}
+
+func (s *BaseSnapSuite) Logout(c *C) {
+ c.Assert(s.AuthFile == "", Equals, false)
+ err := os.Remove(s.AuthFile)
+ c.Assert(err, IsNil)
+ c.Assert(osutil.FileExists(s.AuthFile), Equals, false)
+}
+
type SnapSuite struct {
BaseSnapSuite
}
@@ -112,6 +133,9 @@ func mockVersion(v string) (restore func()) {
return func() { cmd.Version = old }
}
+const TestAuthFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME"
+const TestAuthFileContents = `{"macaroon":"MDAzMWxvY2F0aW9uIG15YXBwcy5kZXZlbG9wZXIuc3RhZ2luZy51YnVudHUuY29tCjAwMTZpZGVudGlmaWVyIE15QXBwcwowMDUzY2lkIG15YXBwcy5kZXZlbG9wZXIuc3RhZ2luZy51YnVudHUuY29tfHZhbGlkX3NpbmNlfDIwMTYtMDktMTNUMTA6NDg6MDcuMjUxNzQ4CjAyZDFjaWQgeyJzZWNyZXQiOiAiVGF0dmE3VUJwYkZweHh3MHB2NTRrcS9yVjFnckUyZWt5QTkrQlZnTEhrbmpGam9tY0dGQ2lUZEI2cDJNVWNTZm0wUEFSRUxWZ3gzcG90Sm9MWWVjcmhsaWlFM2xGZGZUU1ZaSXNia0xZMXF1cFduVWZ3Y1RZOWRib1cwamNWV1EzL0RtcmFIV1NjQ0VsM3ZtenZOczJtV3dkRldxYlY1UEluNldMeFNBUy9PckdUOXk4YzZaZ0ZVbHZ2a2lGY3N3NHJjME45ZVhKcGxQZXV3NnZiU2tqQWlSYklFekc2N1IwRnhhc1JhZkUzd3NaTTJjeEdXQ0dmUitTeEh6dnV4Q1VtZm41d1liTVkxdnlCMmFNTEpTNE5rejJtdTEwbTZ1SFBGMnNsWmRNWklOUTZSRC9vTzBQMVpkZ1hmYSt6NnRYREEwcVFHSEJ3TkZPVE9MRDdKWXdUcG9DTy9BNzJPVHgxSUp2ellidzlYVVlQRFVUZ2MzSXBCc0NIRWI5UDJUMXdZRjJwdEhrcUdXZFMvSmk0UkQ4NDZLZTJuN1lCUmN5L0xJbzFURWNseEpOb3IzeDhBVzlRWlZwNWZHNWE2dElGT2pqZVlZbEw4a0wrcndkcURaWkxmOTZxZGdtTkRVQnF1V1BmcGd0VEE2U3Q3WFBHWnpMZzBoUTduWHhEVjVBT0NGMXhubXFseWYzTTdnQ0tIRzVqWmx2ZkRHVUk3OWg4THJvc00yaDZZVnk5ZllwczVOck0vdXJ1THpvSXlic1dtaWw0RnVmelFDbWl6YVZCMXpZSlMyRUVCTVNLVUJCdFZmL2owTThDMTdFajU5R1REeGZ6SW9rRkFHNzlQWG1ySUpJaDU4TWsrckM3ZDBabWo3QmNUS3dqUDQ1Uk5vRWFDamhJdEdXQkU9IiwgInZlcnNpb24iOiAxfQowMDUxdmlkIBlxMHnHn-0WPt1EvRG_z5C7s6JEAExK29jBHPTC1viEEdFLT-D5eZJhQIweP-q_vlKN1GtyVrAcLCbshbLxlIdP2-HS-uZriwowMDIwY2wgbG9naW4uc3RhZ2luZy51YnVudHUuY29tCjAwNTdjaWQgbXlhcHBzLmRldmVsb3Blci5zdGFnaW5nLnVidW50dS5jb218YWNsfFsicGFja2FnZV9hY2Nlc3MiLCAicGFja2FnZV9wdXJjaGFzZSJdCjAwMmZzaWduYXR1cmUgayfZk0IsVki5dqXN3HlDV0KApbES60t5pd1J5ERASJkK","discharges":["MDAyNmxvY2F0aW9uIGxvZ2luLnN0YWdpbmcudWJ1bnR1LmNvbQowMmQ4aWRlbnRpZmllciB7InNlY3JldCI6ICJUYXR2YTdVQnBiRnB4eHcwcHY1NGtxL3JWMWdyRTJla3lBOStCVmdMSGtuakZqb21jR0ZDaVRkQjZwMk1VY1NmbTBQQVJFTFZneDNwb3RKb0xZZWNyaGxpaUUzbEZkZlRTVlpJc2JrTFkxcXVwV25VZndjVFk5ZGJvVzBqY1ZXUTMvRG1yYUhXU2NDRWwzdm16dk5zMm1Xd2RGV3FiVjVQSW42V0x4U0FTL09yR1Q5eThjNlpnRlVsdnZraUZjc3c0cmMwTjllWEpwbFBldXc2dmJTa2pBaVJiSUV6RzY3UjBGeGFzUmFmRTN3c1pNMmN4R1dDR2ZSK1N4SHp2dXhDVW1mbjV3WWJNWTF2eUIyYU1MSlM0Tmt6Mm11MTBtNnVIUEYyc2xaZE1aSU5RNlJEL29PMFAxWmRnWGZhK3o2dFhEQTBxUUdIQndORk9UT0xEN0pZd1Rwb0NPL0E3Mk9UeDFJSnZ6WWJ3OVhVWVBEVVRnYzNJcEJzQ0hFYjlQMlQxd1lGMnB0SGtxR1dkUy9KaTRSRDg0NktlMm43WUJSY3kvTElvMVRFY2x4Sk5vcjN4OEFXOVFaVnA1Zkc1YTZ0SUZPamplWVlsTDhrTCtyd2RxRFpaTGY5NnFkZ21ORFVCcXVXUGZwZ3RUQTZTdDdYUEdaekxnMGhRN25YeERWNUFPQ0YxeG5tcWx5ZjNNN2dDS0hHNWpabHZmREdVSTc5aDhMcm9zTTJoNllWeTlmWXBzNU5yTS91cnVMem9JeWJzV21pbDRGdWZ6UUNtaXphVkIxellKUzJFRUJNU0tVQkJ0VmYvajBNOEMxN0VqNTlHVER4ZnpJb2tGQUc3OVBYbXJJSkloNThNaytyQzdkMFptajdCY1RLd2pQNDVSTm9FYUNqaEl0R1dCRT0iLCAidmVyc2lvbiI6IDF9CjAwZGVjaWQgbG9naW4uc3RhZ2luZy51YnVudHUuY29tfGFjY291bnR8ZXlKMWMyVnlibUZ0WlNJNklDSm9lbFJLUm5reklpd2dJbTl3Wlc1cFpDSTZJQ0pvZWxSS1Jua3pJaXdnSW1ScGMzQnNZWGx1WVcxbElqb2dJbEJsZEdVZ1YyOXZaSE1pTENBaVpXMWhhV3dpT2lBaWMzUmhaMmx1Wnl0bGJXRnBiRUJ3WlhSbExYZHZiMlJ6TG1OdmJTSXNJQ0pwYzE5MlpYSnBabWxsWkNJNklIUnlkV1Y5CjAwNDhjaWQgbG9naW4uc3RhZ2luZy51YnVudHUuY29tfHZhbGlkX3NpbmNlfDIwMTYtMDktMTNUMTA6NDg6MDguNTYzNjk0CjAwNDZjaWQgbG9naW4uc3RhZ2luZy51YnVudHUuY29tfGxhc3RfYXV0aHwyMDE2LTA5LTEzVDEwOjQ4OjA4LjU2MzY5NAowMDQ0Y2lkIGxvZ2luLnN0YWdpbmcudWJ1bnR1LmNvbXxleHBpcmVzfDIwMTctMDktMTNUMTA6NDg6MDguNTYzNzY5CjAwMmZzaWduYXR1cmUg_ADfFwfJjjN3Eorq2NAQVcNRwwAk5-jZQWUgRKRrii4K"]}`
+
func (s *SnapSuite) TestErrorResult(c *C) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"type": "error", "result": {"message": "cannot do something"}}`)
diff --git a/cmd/snap/notes.go b/cmd/snap/notes.go
index 74849dc64a..0801b3acb4 100644
--- a/cmd/snap/notes.go
+++ b/cmd/snap/notes.go
@@ -21,6 +21,8 @@ package main
import (
"strings"
+
+ "github.com/snapcore/snapd/i18n"
)
// Notes encapsulate everything that might be interesting about a
@@ -51,7 +53,8 @@ func (n *Notes) String() string {
}
if n.Private {
- ns = append(ns, "private")
+ // TRANSLATORS: if possible, a single short word
+ ns = append(ns, i18n.G("private"))
}
if n.TryMode {
@@ -59,11 +62,13 @@ func (n *Notes) String() string {
}
if n.Disabled {
- ns = append(ns, "disabled")
+ // TRANSLATORS: if possible, a single short word
+ ns = append(ns, i18n.G("disabled"))
}
if n.Broken {
- ns = append(ns, "broken")
+ // TRANSLATORS: if possible, a single short word
+ ns = append(ns, i18n.G("broken"))
}
if len(ns) == 0 {
diff --git a/daemon/api.go b/daemon/api.go
index 64d6fb5319..e04d4ac32f 100644
--- a/daemon/api.go
+++ b/daemon/api.go
@@ -660,6 +660,8 @@ var (
snapstateTryPath = snapstate.TryPath
snapstateUpdate = snapstate.Update
snapstateUpdateMany = snapstate.UpdateMany
+
+ assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations
)
func ensureStateSoonImpl(st *state.State) {
@@ -731,6 +733,11 @@ func modeFlags(devMode, jailMode bool) (snapstate.Flags, error) {
}
func snapUpdateMany(inst *snapInstruction, st *state.State) (msg string, updated []string, tasksets []*state.TaskSet, err error) {
+ // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly
+ if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
+ return "", nil, nil, err
+ }
+
updated, tasksets, err = snapstateUpdateMany(st, inst.Snaps, inst.userID)
if err != nil {
return "", nil, nil, err
@@ -785,7 +792,12 @@ func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSe
return "", nil, err
}
- ts, err := snapstateUpdate(st, inst.Snaps[0], inst.Channel, inst.userID, flags)
+ // we need refreshed snap-declarations to enforce refresh-control as best as we can
+ if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil {
+ return "", nil, err
+ }
+
+ ts, err := snapstateUpdate(st, inst.Snaps[0], inst.Channel, inst.Revision, inst.userID, flags)
if err != nil {
return "", nil, err
}
@@ -809,13 +821,18 @@ func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSe
}
func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) {
+ var ts *state.TaskSet
+
flags, err := modeFlags(inst.DevMode, inst.JailMode)
if err != nil {
return "", nil, err
}
- // TODO: bail if revision is given (and != current), or revert to that revision?
- ts, err := snapstate.Revert(st, inst.Snaps[0], flags)
+ if inst.Revision.Unset() {
+ ts, err = snapstate.Revert(st, inst.Snaps[0], flags)
+ } else {
+ ts, err = snapstate.RevertToRevision(st, inst.Snaps[0], inst.Revision, flags)
+ }
if err != nil {
return "", nil, err
}
diff --git a/daemon/api_test.go b/daemon/api_test.go
index a095173b3f..adae8a4e7b 100644
--- a/daemon/api_test.go
+++ b/daemon/api_test.go
@@ -39,7 +39,6 @@ import (
"gopkg.in/check.v1"
"gopkg.in/macaroon.v1"
- tomb "gopkg.in/tomb.v2"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
@@ -49,7 +48,6 @@ import (
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/auth"
- "github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/ifacestate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
@@ -173,6 +171,7 @@ func (s *apiSuite) TearDownTest(c *check.C) {
snapstateInstall = snapstate.Install
snapstateGet = snapstate.Get
snapstateInstallPath = snapstate.InstallPath
+ assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations
unsafeReadSnapInfo = unsafeReadSnapInfoImpl
ensureStateSoon = ensureStateSoonImpl
dirs.SetRootDir("")
@@ -397,6 +396,7 @@ func (s *apiSuite) TestListIncludesAll(c *check.C) {
"snapstateGet",
"snapstateUpdateMany",
"snapstateRefreshCandidates",
+ "assertstateRefreshSnapDeclarations",
"unsafeReadSnapInfo",
"osutilAddUser",
"storeUserInfo",
@@ -1766,19 +1766,9 @@ func (s *apiSuite) TestSetConf(c *check.C) {
d := s.daemon(c)
s.mockSnap(c, configYaml)
- oldHookRunner := hookstate.HookRunner
- defer func() { hookstate.HookRunner = oldHookRunner }()
-
// Mock the hook runner
- var hookSnap string
- var hookRevision snap.Revision
- var hookName string
- hookstate.HookRunner = func(snap string, revision snap.Revision, hook, _ string, _ *tomb.Tomb) ([]byte, error) {
- hookSnap = snap
- hookRevision = revision
- hookName = hook
- return nil, nil
- }
+ hookRunner := testutil.MockCommand(c, "snap", "")
+ defer hookRunner.Restore()
d.overlord.Loop()
defer d.overlord.Stop()
@@ -1815,9 +1805,9 @@ func (s *apiSuite) TestSetConf(c *check.C) {
c.Assert(err, check.IsNil)
// Check that the apply-config hook was run correctly
- c.Check(hookSnap, check.Equals, "config-snap")
- c.Check(hookRevision, check.Equals, snap.R(0))
- c.Check(hookName, check.Equals, "apply-config")
+ c.Check(hookRunner.Calls(), check.DeepEquals, [][]string{[]string{
+ "snap", "run", "--hook", "apply-config", "-r", "unset", "config-snap",
+ }})
}
func (s *apiSuite) TestAppIconGet(c *check.C) {
@@ -1897,6 +1887,12 @@ func (s *apiSuite) TestAppIconGetNoApp(c *check.C) {
c.Check(rec.Code, check.Equals, 404)
}
+func (s *apiSuite) TestNotInstalledSnapIcon(c *check.C) {
+ info := &snap.Info{SuggestedName: "notInstalledSnap", IconURL: "icon.svg"}
+ iconfile := snapIcon(info)
+ c.Check(iconfile, testutil.Contains, "icon.svg")
+}
+
func (s *apiSuite) TestInstallOnNonDevModeDistro(c *check.C) {
s.testInstall(c, &release.OS{ID: "ubuntu"}, snapstate.Flags(0), snap.R(0))
}
@@ -1969,12 +1965,13 @@ func (s *apiSuite) TestRefresh(c *check.C) {
calledFlags := snapstate.Flags(42)
calledUserID := 0
installQueue := []string{}
+ assertstateCalledUserID := 0
snapstateGet = func(s *state.State, name string, snapst *snapstate.SnapState) error {
// we have ubuntu-core
return nil
}
- snapstateUpdate = func(s *state.State, name, channel string, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledFlags = flags
calledUserID = userID
installQueue = append(installQueue, name)
@@ -1982,6 +1979,10 @@ func (s *apiSuite) TestRefresh(c *check.C) {
t := s.NewTask("fake-refresh-snap", "Doing a fake install")
return state.NewTaskSet(t), nil
}
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ assertstateCalledUserID = userID
+ return nil
+ }
d := s.daemon(c)
inst := &snapInstruction{
@@ -1996,6 +1997,7 @@ func (s *apiSuite) TestRefresh(c *check.C) {
summary, _, err := inst.dispatch()(inst, st)
c.Check(err, check.IsNil)
+ c.Check(assertstateCalledUserID, check.Equals, 17)
c.Check(calledFlags, check.Equals, snapstate.Flags(0))
c.Check(calledUserID, check.Equals, 17)
c.Check(err, check.IsNil)
@@ -2012,7 +2014,7 @@ func (s *apiSuite) TestRefreshDevMode(c *check.C) {
// we have ubuntu-core
return nil
}
- snapstateUpdate = func(s *state.State, name, channel string, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateUpdate = func(s *state.State, name, channel string, revision snap.Revision, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledFlags = flags
calledUserID = userID
installQueue = append(installQueue, name)
@@ -2020,6 +2022,9 @@ func (s *apiSuite) TestRefreshDevMode(c *check.C) {
t := s.NewTask("fake-refresh-snap", "Doing a fake install")
return state.NewTaskSet(t), nil
}
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ return nil
+ }
d := s.daemon(c)
inst := &snapInstruction{
@@ -2073,6 +2078,12 @@ func (s *apiSuite) TestPostSnapsOp(c *check.C) {
}
func (s *apiSuite) TestRefreshAll(c *check.C) {
+ refreshSnapDecls := false
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ refreshSnapDecls = true
+ return assertstate.RefreshSnapDeclarations(s, userID)
+ }
+
snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
c.Check(names, check.HasLen, 0)
t := s.NewTask("fake-refresh-all", "Refreshing everything")
@@ -2087,9 +2098,16 @@ func (s *apiSuite) TestRefreshAll(c *check.C) {
st.Unlock()
c.Assert(err, check.IsNil)
c.Check(summary, check.Equals, "Refresh all snaps in the system")
+ c.Check(refreshSnapDecls, check.Equals, true)
}
func (s *apiSuite) TestRefreshMany(c *check.C) {
+ refreshSnapDecls := false
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ refreshSnapDecls = true
+ return nil
+ }
+
snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
c.Check(names, check.HasLen, 2)
t := s.NewTask("fake-refresh-2", "Refreshing two")
@@ -2105,9 +2123,16 @@ func (s *apiSuite) TestRefreshMany(c *check.C) {
c.Assert(err, check.IsNil)
c.Check(summary, check.Equals, `Refresh snaps "foo", "bar"`)
c.Check(updates, check.DeepEquals, inst.Snaps)
+ c.Check(refreshSnapDecls, check.Equals, true)
}
func (s *apiSuite) TestRefreshMany1(c *check.C) {
+ refreshSnapDecls := false
+ assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error {
+ refreshSnapDecls = true
+ return nil
+ }
+
snapstateUpdateMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
c.Check(names, check.HasLen, 1)
t := s.NewTask("fake-refresh-1", "Refreshing one")
@@ -2123,6 +2148,7 @@ func (s *apiSuite) TestRefreshMany1(c *check.C) {
c.Assert(err, check.IsNil)
c.Check(summary, check.Equals, `Refresh snap "foo"`)
c.Check(updates, check.DeepEquals, inst.Snaps)
+ c.Check(refreshSnapDecls, check.Equals, true)
}
func (s *apiSuite) TestInstallMissingUbuntuCore(c *check.C) {
diff --git a/daemon/snap.go b/daemon/snap.go
index 15ac67c85f..20f1a3ede9 100644
--- a/daemon/snap.go
+++ b/daemon/snap.go
@@ -38,7 +38,7 @@ func snapIcon(info *snap.Info) string {
// XXX: copy of snap.Snap.Icon which will go away
found, _ := filepath.Glob(filepath.Join(info.MountDir(), "meta", "gui", "icon.*"))
if len(found) == 0 {
- return ""
+ return info.IconURL
}
return found[0]
diff --git a/debian/changelog b/debian/changelog
index 63136049d3..2401e99208 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,136 @@
+snapd (2.15.2) xenial; urgency=medium
+
+ * New upstream release, LP: #1623579
+ - asserts: define a bit less terse Ref.String
+ - interfaces: disable auto-connect in libvirt interface
+ - asserts: check that validation assertions are signed by the
+ publisher of the gating snap
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 19 Sep 2016 10:42:29 +0200
+
+snapd (2.15.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1623579
+ - image: ensure local snaps are put last in seed.yaml
+ - asserts: revert change that made the account-key's name mandatory.
+ - many: refresh all snap decls
+ - interfaces/apparmor: allow reading /etc/environment
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 19 Sep 2016 09:19:44 +0200
+
+snapd (2.15) xenial; urgency=medium
+
+ * New upstream release, LP: #1623579
+ - tests: disable prepare-image-grub test in autopkgtest
+ - interfaces: allow special casing for auto-connect until we have
+ assertions
+ - docs: add a little documentation on hooks.
+ - hookstate,daemon: don't mock HookRunner, mock command.
+ - tests: add http_proxy to /etc/environment in the autopkgtest
+ environment
+ - backends: first bits of kernel-module security backend
+ - tests: ensure openssh-server is installed in autopkgtest
+ - tests: make ubuntu-core tests more robust
+ - many: mostly work to support ABA upgrades
+ - cmd/snap: do runtime linting of descriptions
+ - spread.yaml: don't assume LANG is set
+ - snap: fix SNAP* environment merging in `snap run`
+ - CONTRIBUTING.md: remove integration-tests, include spread
+ - store: don't discard error body from request device session call
+ - docs: add create-user documentation
+ - cmd/snap: match UX document for message when buying without login
+ - firstboot: do not overwrite any existing netplan config
+ - tests: add debug output to ubuntu-core-update-rollback-
+ stresstest:
+ - tests/lib/prepare.sh: test that classic does not setting bootvars
+ - snap: run all tests with gpg2
+ - asserts: basic support for validation assertion and refresh-
+ control
+ - interfaces: miscellaneous policy updates for default, browser-
+ support and camera
+ - snap: (re)add --force-dangerous compat option
+ - tests: ensure SUDO_{USER,GID} is unset in the spread tests
+ - many: clean out left over references to integration tests
+ - overlord/auth,store: fix raciness in updating device/user in state
+ through authcontext and other issuesbonus fixes:
+ - tests: fix spread tests on yakkety
+ - store: refactor auth/refresh tests
+ - asserts: use gpg --fixed-list-mode to be compatible with both gpg1
+ and gpg2
+ - cmd/snap: i18n option descriptions
+ - asserts: required account key name header
+ - tests: add yakkety test host
+ - packaging: make sure debhelper-generated snippet is invoked on
+ postrm
+ - snap,store: capture newest digest from the store, make it
+ DownloadInfo only
+ - tests: add upower-observe spread test
+ - Merge github.com:snapcore/snapd
+ - tests: fixes to actually run the spread tests inside autopkgtest
+ - cmd/snap: make "snap find" error nicer.
+ - tests: get the gadget name from snap list
+ - cmd/snap: tweak help of 'snap download'
+ - cmd/snap,image: teach snap download to download also assertions
+ - interfaces/builtin: tweak opengl interface
+ - interfaces: serial-port use udevUsbDeviceSnippet
+ - store: ensure the payment methods method handles auth failure
+ - overlord/snapstate: support revert flags
+ - many: add snap configuration to REST API
+ - tests: use ubuntu-image for the ubuntu-core-16 image creation
+ - cmd/snap: serialise empty keys list as [] rather than null
+ - cmd/snap,client: add snap set and snap get commands
+ - asserts: update trusted account-key asserts with names
+ - overlord/snapstate: misc fixes/tweaks/cleanups
+ - image: have prepare-image set devmode correctly
+ - overlord/boot: have firstboot support assertion files with
+ multiple assertions
+ - daemon: bail from enable and disable if revision given, and from
+ multi-op if unsupported optons given
+ - osutil: call sync after cp if
+ requested.overlord/snapstate/backend: switch to use osutil instead
+ of another buggy call to cp
+ - cmd/snap: generate account-key-request "since" header in UTC
+ - many: use symlinks instead of wrappers
+ - tests: remove silly [Service] entry from snapd.socket.d/local.conf
+ - store: switch device session to use device-session-request
+ assertion
+ - snap: ensure that plug and slot names are unique
+ - cmd/snap: fix test suite (no Exit(0) on tests!)
+ - interfaces: add interface for hidraw devices
+ - tests: use the real model assertion when creating the core test
+ image
+ - interfaces/builtin: add udisks2 and removable-media interfaces
+ - interface: network_manager: enable resolvconf
+ - interfaces/builtin: usb serial-port support via udev
+ - interfaces/udev: support noneSecurityTag keyed snippets
+ - snap: switch to the new agreed regexp for snap names
+ - tests: adjust test setup after ubuntu user removal
+ - many: start services only after the snap is fully ready (link-snap
+ was run)
+ - asserts: don't have Add/Check panic in the face of unsupported no-
+ authority assertions
+ - asserts: initial support to generate/sign snap-build assertions
+ - asserts: support checking account-key-request assertions
+ - overlord: introduce AuthContext.DeviceSessionRequest with support
+ in devicestate
+ - overlord/state: fix for reloaded task/change crashing on Set if
+ checkpointed w. no custom data yet
+ - snapd.refresh.service: require snap.socket and /snap/*/current.
+ - many: spell --force-dangerous as just --dangerous, devmode should
+ imply it
+ - overlord/devicestate: try to fetch/refresh the signing key of
+ serial (also in case is not there yet)
+ - image,overlord/boot,snap: metadata from asserts for image snaps
+ - many: automatically restart all-snap devices after os/kernel
+ updates
+ - interfaces: modem-manager: ignore camera
+ - firstboot: only configure en* and eth* interfaces by default
+ - interfaces: fix interface handling on no-app snaps
+ - snap: set user variables even if HOME is unset (like with systemd
+ services)
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 16 Sep 2016 07:46:22 +0200
+
snapd (2.14.2~16.04) xenial; urgency=medium
* New upstream release: LP: #1618095
diff --git a/debian/control b/debian/control
index eedb489643..d8c10928c5 100644
--- a/debian/control
+++ b/debian/control
@@ -8,6 +8,7 @@ Build-Depends: bash-completion,
dh-systemd,
fakeroot,
gettext,
+ gnupg2,
golang-check.v1-dev,
golang-go,
golang-go-flags-dev,
diff --git a/debian/rules b/debian/rules
index 140b80de6f..ecc1743e52 100755
--- a/debian/rules
+++ b/debian/rules
@@ -5,7 +5,7 @@
export DH_OPTIONS
export DH_GOPKG := github.com/snapcore/snapd
#export DEB_BUILD_OPTIONS=nocheck
-export DH_GOLANG_EXCLUDES=integration-tests tests
+export DH_GOLANG_EXCLUDES=tests
export DH_GOLANG_GO_GENERATE=1
export PATH:=${PATH}:${CURDIR}
diff --git a/debian/snapd.postrm b/debian/snapd.postrm
index 82b218843f..84ce0501af 100644
--- a/debian/snapd.postrm
+++ b/debian/snapd.postrm
@@ -47,7 +47,7 @@ if [ "$1" = "purge" ]; then
fi
done
fi
-
+
echo "Removing $unit"
rm -f "/etc/systemd/system/$unit"
rm -f "/etc/systemd/system/multi-user.target.wants/$unit"
@@ -56,3 +56,5 @@ if [ "$1" = "purge" ]; then
echo "Removing snapd state"
rm -rf /var/lib/snapd
fi
+
+#DEBHELPER#
diff --git a/debian/tests/control b/debian/tests/control
index b9b5c4127d..eb3540b256 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -1,10 +1,11 @@
Tests: integrationtests
-Restrictions: allow-stderr, isolation-container, rw-build-tree
+Restrictions: allow-stderr, isolation-container, rw-build-tree, needs-root, breaks-testbed
Depends: @builddeps@,
bzr,
ca-certificates,
git,
golang-golang-x-net-dev,
+ openssh-server,
snapd,
unity,
x11-utils,
diff --git a/debian/tests/integrationtests b/debian/tests/integrationtests
index de20c5080e..2398a93f78 100644
--- a/debian/tests/integrationtests
+++ b/debian/tests/integrationtests
@@ -1,29 +1,33 @@
#!/bin/sh
-NEEDS_REBOOT=/tmp/needs-reboot
-
set -ex
# for these tests, run snap and snapd from outside of the core snap
-sudo mkdir -p /etc/systemd/system/snapd.service.d/
-cat <<EOF | sudo tee /etc/systemd/system/snapd.service.d/no-reexec.conf
+mkdir -p /etc/systemd/system/snapd.service.d/
+cat <<EOF | tee /etc/systemd/system/snapd.service.d/no-reexec.conf
[Service]
Environment=SNAP_REEXEC=0
EOF
# required for the debian adt host
if [ "$http_proxy" != "" ]; then
- cat <<EOF | sudo tee /etc/systemd/system/snapd.service.d/proxy.conf
+ cat <<EOF | tee /etc/systemd/system/snapd.service.d/proxy.conf
[Service]
Environment=http_proxy=$http_proxy
Environment=https_proxy=$http_proxy
EOF
+
+ # ensure environment is updated
+ echo "http_proxy=$http_proxy" >> /etc/environment
fi
-sudo systemctl daemon-reload
+systemctl daemon-reload
-# ensure our PATH is right
-. /etc/profile.d/apps-bin-path.sh
+# ensure we can do a connect to localhost
+echo ubuntu:ubuntu|chpasswd
+sed -i 's/\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' /etc/ssh/sshd_config
+systemctl reload sshd.service
+# and now run spread against localhost
export GOPATH=/tmp/go
go get -u github.com/snapcore/spread/cmd/spread
-/tmp/go/bin/spread -v adhoc:
+/tmp/go/bin/spread -v autopkgtest:
diff --git a/docs/hooks.md b/docs/hooks.md
new file mode 100644
index 0000000000..b4e879f1ac
--- /dev/null
+++ b/docs/hooks.md
@@ -0,0 +1,31 @@
+# Hooks
+
+There are a number of situations where snapd needs to notify a snap that
+something has happened. For example, when a snap is upgraded, it may need to run
+some sort of migration on the previous version's data in order to make it
+consumable by the new version. Or when an interface is connected or
+disconnected, the snap might need to obtain attributes specific to that
+connection. These types of situations are handled by hooks.
+
+A hook is defined as an executable contained within the `meta/hooks/` directory
+inside the snap. The file name of the executable is the name of the hook (e.g.
+the upgrade hook executable would be `meta/hooks/upgrade`).
+
+As long as the file name of the executable corresponds to a supported hook name,
+that's all one needs to do in order to utilize a hook within their snap. Note
+that hooks, like apps, are executed within a confined environment. By default
+hooks will run with no plugs; if a hook needs more privileges one can use the
+top-level attribute `hooks` in `snap.yaml` to request plugs, like so:
+
+ hooks: # Top-level YAML attribute, parallel to `apps`
+ upgrade: # Hook name, corresponds to executable name
+ plugs: [network] # Or any other plugs required by this hook
+
+Note that hooks will be called with no parameters. If they need more information
+from snapd they can utilize the `snapctl` command.
+
+
+## Supported Hooks
+
+**Note:** The development of specific hooks is ongoing. None are currently
+supported.
diff --git a/docs/interfaces.md b/docs/interfaces.md
index 9987209806..12d7868fce 100644
--- a/docs/interfaces.md
+++ b/docs/interfaces.md
@@ -313,6 +313,8 @@ Can access the libvirt control socket, which gives privileged access to control
libvirtd on the host. This is commonly used to create and manage QEMU/KVM
instances on the host.
+* Auto-Connect: no
+
### locale-control
Can manage locales directly separate from ``config core``.
diff --git a/docs/rest.md b/docs/rest.md
index ac103a4e4d..0a3a3da048 100644
--- a/docs/rest.md
+++ b/docs/rest.md
@@ -663,3 +663,29 @@ Generally the UUID of a background operation you are interested in.
]
}
```
+
+## /v2/create-user
+
+### POST
+
+* Description: Create a local user
+* Access: trusted
+* Operation: sync
+* Return: an object with the created username and the amount of imported ssh keys
+
+Sample input:
+
+```javascript
+{
+ "email":"michael@example.com",
+ "sudoer": false
+}
+```
+
+Sample return:
+```javascript
+{
+ "username":"mvo",
+ "ssk-key-count": 2
+}
+```
diff --git a/firstboot/firstboot.go b/firstboot/firstboot.go
index 9c6f423140..f54df6859e 100644
--- a/firstboot/firstboot.go
+++ b/firstboot/firstboot.go
@@ -69,6 +69,12 @@ network:
// be run as part of the config-changed hook and read the snap's
// config to determine the netplan config to write.
func InitialNetworkConfig() error {
+ // If the config is already present, don't overwrite it. See
+ // https://bugs.launchpad.net/snappy/+bug/1623119.
+ if _, err := os.Stat(netplanConfigFile); err == nil {
+ return nil
+ }
+
if err := osutil.AtomicWriteFile(netplanConfigFile, []byte(netplanConfigData), 0644, 0); err != nil {
return err
}
@@ -76,9 +82,6 @@ func InitialNetworkConfig() error {
enable := exec.Command(enableConfig[0], enableConfig[1:]...)
enable.Stdout = os.Stdout
enable.Stderr = os.Stderr
- if err := enable.Run(); err != nil {
- return err
- }
- return nil
+ return enable.Run()
}
diff --git a/image/helpers.go b/image/helpers.go
index 29d0b23472..2adc646371 100644
--- a/image/helpers.go
+++ b/image/helpers.go
@@ -85,8 +85,8 @@ func DownloadSnap(sto Store, name string, revision snap.Revision, opts *Download
return targetPath, snap, nil
}
-// StoreAssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will save assertions in the given database and after that also call save for each of them.
-func StoreAssertionFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database, save func(asserts.Assertion) error) *asserts.Fetcher {
+// StoreAssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them.
+func StoreAssertionFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher {
retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
return sto.Assertion(ref.Type, ref.PrimaryKey, dlOpts.User)
}
@@ -104,17 +104,14 @@ func StoreAssertionFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Datab
return asserts.NewFetcher(db, retrieve, save2)
}
-// FetchSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database.
-func FetchSnapAssertions(snapPath string, info *snap.Info, f *asserts.Fetcher, db asserts.RODatabase) error {
+// FetchAndCheckSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database.
+func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) error {
sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath)
if err != nil {
return err
}
- ref := &asserts.Ref{
- Type: asserts.SnapRevisionType,
- PrimaryKey: []string{sha3_384},
- }
- if err := f.Fetch(ref); err != nil {
+
+ if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil {
return fmt.Errorf("cannot fetch snap signatures/assertions: %v", err)
}
diff --git a/image/image.go b/image/image.go
index c19ecd9721..dd0c5e947a 100644
--- a/image/image.go
+++ b/image/image.go
@@ -66,6 +66,13 @@ func (li *localInfos) Name(pathOrName string) string {
return pathOrName
}
+func (li *localInfos) PreferLocal(name string) string {
+ if path := li.Path(name); path != "" {
+ return path
+ }
+ return name
+}
+
func (li *localInfos) Path(name string) string {
return li.nameToPath[name]
}
@@ -185,7 +192,7 @@ func acquireSnap(sto Store, name string, dlOpts *DownloadOptions, local *localIn
}
type addingFetcher struct {
- *asserts.Fetcher
+ asserts.Fetcher
addedRefs []*asserts.Ref
}
@@ -248,22 +255,23 @@ func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *l
DevMode: false, // XXX: should this be true?
}
- snaps := []string{}
- // opts.Snaps need to be considered first to support local overrides
- // of snaps mentioned in the model assertion whose fetching
- // from the store will be then skipped
- snaps = append(snaps, opts.Snaps...)
- snaps = append(snaps, model.Gadget())
- snaps = append(snaps, defaultCore)
- snaps = append(snaps, model.Kernel())
- snaps = append(snaps, model.RequiredSnaps()...)
-
for _, d := range []string{snapSeedDir, assertSeedDir} {
if err := os.MkdirAll(d, 0755); err != nil {
return err
}
}
+ snaps := []string{}
+ // core,kernel,gadget first
+ snaps = append(snaps, local.PreferLocal(defaultCore))
+ snaps = append(snaps, local.PreferLocal(model.Kernel()))
+ snaps = append(snaps, local.PreferLocal(model.Gadget()))
+ // then required and the user requested stuff
+ for _, snapName := range model.RequiredSnaps() {
+ snaps = append(snaps, local.PreferLocal(snapName))
+ }
+ snaps = append(snaps, opts.Snaps...)
+
seen := make(map[string]bool)
downloadedSnapsInfo := map[string]*snap.Info{}
var seedYaml snap.Seed
@@ -291,7 +299,7 @@ func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *l
// TODO: support somehow including available assertions
// also for local snaps
if info.SnapID != "" {
- err = FetchSnapAssertions(fn, info, f.Fetcher, db)
+ err = FetchAndCheckSnapAssertions(fn, info, f, db)
if err != nil {
return err
}
diff --git a/image/image_test.go b/image/image_test.go
index 19300c7e6f..cefd4a6d9f 100644
--- a/image/image_test.go
+++ b/image/image_test.go
@@ -91,14 +91,15 @@ func (s *imageSuite) SetUpTest(c *C) {
s.storeSigning.Add(brandAccKey)
model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
- "series": "16",
- "authority-id": "my-brand",
- "brand-id": "my-brand",
- "model": "my-model",
- "architecture": "amd64",
- "gadget": "pc",
- "kernel": "pc-kernel",
- "timestamp": time.Now().Format(time.RFC3339),
+ "series": "16",
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "architecture": "amd64",
+ "gadget": "pc",
+ "kernel": "pc-kernel",
+ "required-snaps": []interface{}{"required-snap1"},
+ "timestamp": time.Now().Format(time.RFC3339),
}, nil, "")
c.Assert(err, IsNil)
s.model = model.(*asserts.Model)
@@ -169,6 +170,7 @@ name: ubuntu-core
version: 16.04
type: os
`
+
const devmodeSnap = `
name: devmode-snap
version: 1.0
@@ -176,6 +178,11 @@ type: app
confinement: devmode
`
+const requiredSnap1 = `
+name: required-snap1
+version: 1.0
+`
+
func (s *imageSuite) TestMissingModelAssertions(c *C) {
_, err := image.DecodeModelAssertion(&image.Options{})
c.Assert(err, ErrorMatches, "cannot read model assertion: open : no such file or directory")
@@ -322,6 +329,10 @@ func (s *imageSuite) setupSnaps(c *C, gadgetUnpackDir string) {
s.downloadedSnaps["ubuntu-core"] = snaptest.MakeTestSnapWithFiles(c, packageCore, nil)
s.storeSnapInfo["ubuntu-core"] = infoFromSnapYaml(c, packageCore, snap.R(3))
s.addSystemSnapAssertions(c, "ubuntu-core")
+
+ s.downloadedSnaps["required-snap1"] = snaptest.MakeTestSnapWithFiles(c, requiredSnap1, nil)
+ s.storeSnapInfo["required-snap1"] = infoFromSnapYaml(c, requiredSnap1, snap.R(3))
+ s.addSystemSnapAssertions(c, "required-snap1")
}
func (s *imageSuite) TestBootstrapToRootDir(c *C) {
@@ -355,10 +366,10 @@ func (s *imageSuite) TestBootstrapToRootDir(c *C) {
seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 3)
+ c.Check(seed.Snaps, HasLen, 4)
// check the files are in place
- for i, name := range []string{"pc", "ubuntu-core", "pc-kernel"} {
+ for i, name := range []string{"ubuntu-core", "pc-kernel", "pc"} {
info := s.storeSnapInfo[name]
fn := filepath.Base(info.MountFile())
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
@@ -425,7 +436,10 @@ func (s *imageSuite) TestBootstrapToRootDirLocalCore(c *C) {
defer c2.Restore()
opts := &image.Options{
- Snaps: []string{s.downloadedSnaps["ubuntu-core"]},
+ Snaps: []string{
+ s.downloadedSnaps["ubuntu-core"],
+ s.downloadedSnaps["required-snap1"],
+ },
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
}
@@ -439,21 +453,31 @@ func (s *imageSuite) TestBootstrapToRootDirLocalCore(c *C) {
seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 3)
+ c.Check(seed.Snaps, HasLen, 4)
// check the files are in place
- for i, name := range []string{"ubuntu-core_x1.snap", "pc", "pc-kernel"} {
+ for i, name := range []string{"ubuntu-core_x1.snap", "pc-kernel", "pc", "required-snap1_x1.snap"} {
unasserted := false
info := s.storeSnapInfo[name]
if info == nil {
- // ubuntu-core
- info = &snap.Info{
- SideInfo: snap.SideInfo{
- RealName: "ubuntu-core",
- Revision: snap.R("x1"),
- },
+ switch name {
+ case "ubuntu-core_x1.snap":
+ info = &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "ubuntu-core",
+ Revision: snap.R("x1"),
+ },
+ }
+ unasserted = true
+ case "required-snap1_x1.snap":
+ info = &snap.Info{
+ SideInfo: snap.SideInfo{
+ RealName: "required-snap1",
+ Revision: snap.R("x1"),
+ },
+ }
+ unasserted = true
}
- unasserted = true
}
fn := filepath.Base(info.MountFile())
@@ -470,7 +494,7 @@ func (s *imageSuite) TestBootstrapToRootDirLocalCore(c *C) {
l, err := ioutil.ReadDir(filepath.Join(rootdir, "var/lib/snapd/seed/snaps"))
c.Assert(err, IsNil)
- c.Check(l, HasLen, 3)
+ c.Check(l, HasLen, 4)
storeAccountKey := s.storeSigning.StoreAccountKey("")
brandPubKey, err := s.brandSigning.PublicKey("")
@@ -548,7 +572,7 @@ func (s *imageSuite) TestBootstrapToRootDirDevmodeSnap(c *C) {
seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 4)
+ c.Check(seed.Snaps, HasLen, 5)
// check devmode-snap
info := &snap.Info{
@@ -560,7 +584,10 @@ func (s *imageSuite) TestBootstrapToRootDirDevmodeSnap(c *C) {
fn := filepath.Base(info.MountFile())
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
c.Check(osutil.FileExists(p), Equals, true)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+
+ // ensure local snaps are put last in seed.yaml
+ last := len(seed.Snaps) - 1
+ c.Check(seed.Snaps[last], DeepEquals, &snap.SeedSnap{
Name: "devmode-snap",
File: fn,
DevMode: true,
diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go
index 692e63de1f..774fef348a 100644
--- a/interfaces/apparmor/template.go
+++ b/interfaces/apparmor/template.go
@@ -86,6 +86,7 @@ var defaultTemplate = []byte(`
/etc/libnl-3/{classid,pktloc} r, # apps that use libnl
/var/lib/extrausers/{passwd,group} r,
/etc/profile r,
+ /etc/environment r,
/usr/share/terminfo/** r,
/etc/inputrc r,
# Common utilities for shell scripts
@@ -149,6 +150,7 @@ var defaultTemplate = []byte(`
/{,usr/}bin/rmdir ixr,
/{,usr/}bin/sed ixr,
/{,usr/}bin/seq ixr,
+ /{,usr/}bin/sha{1,224,256,384,512}sum ixr,
/{,usr/}bin/shuf ixr,
/{,usr/}bin/sleep ixr,
/{,usr/}bin/sort ixr,
@@ -166,6 +168,7 @@ var defaultTemplate = []byte(`
/{,usr/}bin/tput ixr,
/{,usr/}bin/tr ixr,
/{,usr/}bin/true ixr,
+ /{,usr/}bin/tty ixr,
/{,usr/}bin/uname ixr,
/{,usr/}bin/uniq ixr,
/{,usr/}bin/unlink ixr,
@@ -252,6 +255,7 @@ var defaultTemplate = []byte(`
@{PROC}/sys/fs/file-max r,
@{PROC}/sys/kernel/pid_max r,
@{PROC}/sys/kernel/random/uuid r,
+ /sys/devices/virtual/tty/{console,tty*}/active r,
/{,usr/}lib/ r,
# Reads of oom_adj and oom_score_adj are safe
diff --git a/interfaces/backends/backends.go b/interfaces/backends/backends.go
index 9163440e02..d5291966e7 100644
--- a/interfaces/backends/backends.go
+++ b/interfaces/backends/backends.go
@@ -23,6 +23,7 @@ import (
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
"github.com/snapcore/snapd/interfaces/dbus"
+ "github.com/snapcore/snapd/interfaces/kmod"
"github.com/snapcore/snapd/interfaces/mount"
"github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/interfaces/udev"
@@ -35,6 +36,7 @@ var All = []interfaces.SecurityBackend{
&dbus.Backend{},
&udev.Backend{},
&mount.Backend{},
+ &kmod.Backend{},
}
func init() {
diff --git a/interfaces/builtin/bluez.go b/interfaces/builtin/bluez.go
index 5526147e03..1f3cd661c2 100644
--- a/interfaces/builtin/bluez.go
+++ b/interfaces/builtin/bluez.go
@@ -194,7 +194,7 @@ func (iface *BluezInterface) Name() string {
func (iface *BluezInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -210,7 +210,7 @@ func (iface *BluezInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *i
return snippet, nil
case interfaces.SecuritySecComp:
return bluezConnectedPlugSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -225,7 +225,7 @@ func (iface *BluezInterface) PermanentSlotSnippet(slot *interfaces.Slot, securit
return bluezPermanentSlotSecComp, nil
case interfaces.SecurityDBus:
return bluezPermanentSlotDBus, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -234,7 +234,7 @@ func (iface *BluezInterface) PermanentSlotSnippet(slot *interfaces.Slot, securit
func (iface *BluezInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/bool_file.go b/interfaces/builtin/bool_file.go
index 4fea232847..85f45fea24 100644
--- a/interfaces/builtin/bool_file.go
+++ b/interfaces/builtin/bool_file.go
@@ -81,7 +81,7 @@ func (iface *BoolFileInterface) SanitizePlug(plug *interfaces.Plug) error {
// Applications associated with the slot don't gain any extra permissions.
func (iface *BoolFileInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -104,7 +104,7 @@ func (iface *BoolFileInterface) PermanentSlotSnippet(slot *interfaces.Slot, secu
return gpioSnippet, nil
}
return nil, nil
- case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -125,7 +125,7 @@ func (iface *BoolFileInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot
return nil, fmt.Errorf("cannot compute plug security snippet: %v", err)
}
return []byte(fmt.Sprintf("%s rwk,\n", path)), nil
- case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -136,7 +136,7 @@ func (iface *BoolFileInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot
// Applications associated with the plug don't gain any extra permissions.
func (iface *BoolFileInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/browser_support.go b/interfaces/builtin/browser_support.go
index b1c6443868..c591268263 100644
--- a/interfaces/builtin/browser_support.go
+++ b/interfaces/builtin/browser_support.go
@@ -43,8 +43,8 @@ owner /var/tmp/etilqs_* rw,
# Chrome/Chromium should be modified to use snap.$SNAP_NAME.* or the snap
# packaging adjusted to use LD_PRELOAD technique from LP: #1577514
-owner /{dev,run}/shm/.org.chromium.Chromium.* rw,
-owner /{dev,run}/shm/.com.google.Chrome.* rw,
+owner /{dev,run}/shm/{,.}org.chromium.Chromium.* rw,
+owner /{dev,run}/shm/{,.}com.google.Chrome.* rw,
# Chrome/Chromium should be adjusted to not use gconf. It is only used with
# legacy systems that don't have snapd
@@ -73,6 +73,69 @@ const browserSupportConnectedPlugAppArmorWithSandbox = `
/usr/share/applications/{,*} r,
/var/lib/snapd/desktop/applications/{,*} r,
+# Various files in /run/udev/data needed by Chrome Settings. Leaks device
+# information.
+# input
+/run/udev/data/c1:[0-9]* r, # /dev/psaux
+/run/udev/data/c10:[0-9]* r, # /dev/adbmouse
+/run/udev/data/c13:[0-9]* r, # /dev/input/*
+/run/udev/data/c180:[0-9]* r, # /dev/vrbuttons
+/run/udev/data/c4:[0-9]* r, # /dev/tty*, /dev/ttyS*
+/run/udev/data/c5:[0-9]* r, # /dev/tty, /dev/console, etc
+/run/udev/data/c7:[0-9]* r, # /dev/vcs*
+/run/udev/data/+hid:* r,
+/run/udev/data/+input:input[0-9]* r,
+
+# screen
+/run/udev/data/c29:[0-9]* r, # /dev/fb*
+/run/udev/data/+backlight:* r,
+/run/udev/data/+leds:* r,
+
+# sound
+/run/udev/data/c116:[0-9]* r, # alsa
+/run/udev/data/+sound:card[0-9]* r,
+
+# miscellaneous
+/run/udev/data/c108:[0-9]* r, # /dev/ppp
+/run/udev/data/c189:[0-9]* r, # USB serial converters
+/run/udev/data/c89:[0-9]* r, # /dev/i2c-*
+/run/udev/data/c81:[0-9]* r, # video4linux (/dev/video*, etc)
+/run/udev/data/+acpi:* r,
+/run/udev/data/+hwmon:hwmon[0-9]* r,
+/run/udev/data/+i2c:* r,
+/run/udev/data/+platform:* r,
+/sys/devices/**/bConfigurationValue r,
+/sys/devices/**/descriptors r,
+/sys/devices/**/manufacturer r,
+/sys/devices/**/product r,
+/sys/devices/**/serial r,
+
+# networking
+/run/udev/data/n[0-9]* r,
+/run/udev/data/+bluetooth:hci[0-9]* r,
+/run/udev/data/+rfkill:rfkill[0-9]* r,
+
+# storage
+/run/udev/data/b1:[0-9]* r, # /dev/ram*
+/run/udev/data/b7:[0-9]* r, # /dev/loop*
+/run/udev/data/b8:[0-9]* r, # /dev/sd*
+/run/udev/data/c21:[0-9]* r, # /dev/sg*
+/run/udev/data/+usb:[0-9]* r,
+
+# experimental
+/run/udev/data/c245:[0-9]* r,
+/run/udev/data/c246:[0-9]* r,
+/run/udev/data/c248:[0-9]* r,
+/run/udev/data/c249:[0-9]* r,
+/run/udev/data/c251:[0-9]* r,
+
+/sys/bus/**/devices/ r,
+
+# Google Cloud Print
+unix (bind)
+ type=stream
+ addr="@[0-9A-F]*._service_*",
+
# Policy needed only when using the chrome/chromium setuid sandbox
ptrace (trace) peer=snap.@{SNAP_NAME}.**,
unix (receive, send) peer=(label=snap.@{SNAP_NAME}.**),
@@ -117,6 +180,7 @@ const browserSupportConnectedPlugSecComp = `
# for anonymous sockets
bind
listen
+accept
# TODO: fine-tune when seccomp arg filtering available in stable distro
# releases
@@ -164,7 +228,7 @@ func (iface *BrowserSupportInterface) SanitizePlug(plug *interfaces.Plug) error
func (iface *BrowserSupportInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -174,7 +238,7 @@ func (iface *BrowserSupportInterface) ConnectedSlotSnippet(plug *interfaces.Plug
func (iface *BrowserSupportInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -199,7 +263,7 @@ func (iface *BrowserSupportInterface) ConnectedPlugSnippet(plug *interfaces.Plug
snippet = append(snippet, browserSupportConnectedPlugSecCompWithSandbox...)
}
return snippet, nil
- case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -208,7 +272,7 @@ func (iface *BrowserSupportInterface) ConnectedPlugSnippet(plug *interfaces.Plug
func (iface *BrowserSupportInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/camera.go b/interfaces/builtin/camera.go
index 9dae0c254e..93ff26e49c 100644
--- a/interfaces/builtin/camera.go
+++ b/interfaces/builtin/camera.go
@@ -31,6 +31,7 @@ const cameraConnectedPlugAppArmor = `
/sys/bus/usb/devices/ r,
/sys/devices/pci**/usb*/**/idVendor r,
/sys/devices/pci**/usb*/**/idProduct r,
+/run/udev/data/c81:[0-9]* r, # video4linux (/dev/video*, etc)
`
// NewCameraInterface returns a new "camera" interface.
diff --git a/interfaces/builtin/common.go b/interfaces/builtin/common.go
index b89ce08e97..57b76b2304 100644
--- a/interfaces/builtin/common.go
+++ b/interfaces/builtin/common.go
@@ -76,7 +76,7 @@ func (iface *commonInterface) SanitizePlug(plug *interfaces.Plug) error {
// Plugs don't get any permanent security snippets.
func (iface *commonInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -95,7 +95,7 @@ func (iface *commonInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *
return []byte(iface.connectedPlugAppArmor), nil
case interfaces.SecuritySecComp:
return []byte(iface.connectedPlugSecComp), nil
- case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -109,7 +109,7 @@ func (iface *commonInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *
// Slots don't get any permanent security snippets.
func (iface *commonInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -123,7 +123,7 @@ func (iface *commonInterface) PermanentSlotSnippet(slot *interfaces.Slot, securi
// Slots don't get any per-connection security snippets.
func (iface *commonInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/content.go b/interfaces/builtin/content.go
index 9399e707be..93378e78c1 100644
--- a/interfaces/builtin/content.go
+++ b/interfaces/builtin/content.go
@@ -80,7 +80,7 @@ func (iface *ContentInterface) SanitizePlug(plug *interfaces.Plug) error {
func (iface *ContentInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -90,7 +90,7 @@ func (iface *ContentInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot
func (iface *ContentInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -138,7 +138,7 @@ func (iface *ContentInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot
switch securitySystem {
case interfaces.SecurityMount:
return contentSnippet.Bytes(), nil
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -147,7 +147,7 @@ func (iface *ContentInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot
func (iface *ContentInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/fwupd.go b/interfaces/builtin/fwupd.go
index 96ad083ba0..3c83dab735 100644
--- a/interfaces/builtin/fwupd.go
+++ b/interfaces/builtin/fwupd.go
@@ -21,6 +21,7 @@ package builtin
import (
"bytes"
+
"github.com/snapcore/snapd/interfaces"
)
@@ -196,7 +197,7 @@ func (iface *FwupdInterface) Name() string {
// PermanentPlugSnippet - no slot snippets provided
func (iface *FwupdInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -213,7 +214,7 @@ func (iface *FwupdInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *i
return snippet, nil
case interfaces.SecuritySecComp:
return fwupdConnectedPlugSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -229,7 +230,7 @@ func (iface *FwupdInterface) PermanentSlotSnippet(slot *interfaces.Slot, securit
return fwupdPermanentSlotDBus, nil
case interfaces.SecuritySecComp:
return fwupdPermanentSlotSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -244,7 +245,7 @@ func (iface *FwupdInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *i
new := plugAppLabelExpr(plug)
snippet := bytes.Replace(fwupdConnectedSlotAppArmor, old, new, -1)
return snippet, nil
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/gpio.go b/interfaces/builtin/gpio.go
index d99ec891d0..8c438a8df4 100644
--- a/interfaces/builtin/gpio.go
+++ b/interfaces/builtin/gpio.go
@@ -84,7 +84,7 @@ func (iface *GpioInterface) SanitizePlug(plug *interfaces.Plug) error {
// PermanentPlugSnippet returns security snippets for plug at install
func (iface *GpioInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -94,7 +94,7 @@ func (iface *GpioInterface) PermanentPlugSnippet(plug *interfaces.Plug, security
// ConnectedPlugSnippet returns security snippets for plug at connection
func (iface *GpioInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
case interfaces.SecurityAppArmor:
path := fmt.Sprint(gpioSysfsGpioBase, slot.Attrs["number"])
@@ -115,7 +115,7 @@ func (iface *GpioInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *in
// PermanentSlotSnippet - no slot snippets provided
func (iface *GpioInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -147,7 +147,7 @@ func (iface *GpioInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *in
}
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
case interfaces.SecurityAppArmor:
return nil, nil
diff --git a/interfaces/builtin/hidraw.go b/interfaces/builtin/hidraw.go
index f3f067464f..5a8f0c7c5b 100644
--- a/interfaces/builtin/hidraw.go
+++ b/interfaces/builtin/hidraw.go
@@ -129,7 +129,7 @@ func (iface *HidrawInterface) PermanentSlotSnippet(slot *interfaces.Slot, securi
return nil, nil
}
return udevUsbDeviceSnippet("hidraw", usbVendor, usbProduct, "SYMLINK", strings.TrimPrefix(path, "/dev/")), nil
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -139,7 +139,7 @@ func (iface *HidrawInterface) PermanentSlotSnippet(slot *interfaces.Slot, securi
// ConnectedSlotSnippet no extra permissions granted on connection
func (iface *HidrawInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -149,7 +149,7 @@ func (iface *HidrawInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *
// PermanentPlugSnippet no permissions provided to plug permanently
func (iface *HidrawInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -188,7 +188,7 @@ func (iface *HidrawInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *
udevSnippet.Write(udevUsbDeviceSnippet("hidraw", usbVendor, usbProduct, "TAG", tag))
}
return udevSnippet.Bytes(), nil
- case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/libvirt.go b/interfaces/builtin/libvirt.go
index 7cc335e37c..4218bd8cb9 100644
--- a/interfaces/builtin/libvirt.go
+++ b/interfaces/builtin/libvirt.go
@@ -43,6 +43,7 @@ func NewLibvirtInterface() interfaces.Interface {
connectedPlugAppArmor: libvirtConnectedPlugAppArmor,
connectedPlugSecComp: libvirtConnectedPlugSecComp,
reservedForOS: true,
- autoConnect: true,
+ // cannot auto-connect, it grants too much power
+ autoConnect: false,
}
}
diff --git a/interfaces/builtin/libvirt_test.go b/interfaces/builtin/libvirt_test.go
index 7e08b3a139..2ceafebf98 100644
--- a/interfaces/builtin/libvirt_test.go
+++ b/interfaces/builtin/libvirt_test.go
@@ -121,3 +121,7 @@ func (s *LibvirtInterfaceSuite) TestUnexpectedSecuritySystems(c *C) {
snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityMount)
c.Assert(err, IsNil)
}
+
+func (s *LibvirtInterfaceSuite) TestAutoConnect(c *C) {
+ c.Check(s.iface.AutoConnect(), Equals, false)
+}
diff --git a/interfaces/builtin/location_control.go b/interfaces/builtin/location_control.go
index e7660a28ef..2fb86c7110 100644
--- a/interfaces/builtin/location_control.go
+++ b/interfaces/builtin/location_control.go
@@ -144,7 +144,7 @@ func (iface *LocationControlInterface) Name() string {
func (iface *LocationControlInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -162,7 +162,7 @@ func (iface *LocationControlInterface) ConnectedPlugSnippet(plug *interfaces.Plu
return locationControlConnectedPlugDBus, nil
case interfaces.SecuritySecComp:
return locationControlConnectedPlugSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -177,7 +177,7 @@ func (iface *LocationControlInterface) PermanentSlotSnippet(slot *interfaces.Slo
return locationControlPermanentSlotDBus, nil
case interfaces.SecuritySecComp:
return locationControlPermanentSlotSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -191,7 +191,7 @@ func (iface *LocationControlInterface) ConnectedSlotSnippet(plug *interfaces.Plu
new := plugAppLabelExpr(plug)
snippet := bytes.Replace(locationControlConnectedSlotAppArmor, old, new, -1)
return snippet, nil
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/location_observe.go b/interfaces/builtin/location_observe.go
index 7e1b1708fb..54b93340fa 100644
--- a/interfaces/builtin/location_observe.go
+++ b/interfaces/builtin/location_observe.go
@@ -230,7 +230,7 @@ func (iface *LocationObserveInterface) Name() string {
func (iface *LocationObserveInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -248,7 +248,7 @@ func (iface *LocationObserveInterface) ConnectedPlugSnippet(plug *interfaces.Plu
return locationObserveConnectedPlugDBus, nil
case interfaces.SecuritySecComp:
return locationObserveConnectedPlugSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -263,7 +263,7 @@ func (iface *LocationObserveInterface) PermanentSlotSnippet(slot *interfaces.Slo
return locationObservePermanentSlotDBus, nil
case interfaces.SecuritySecComp:
return locationObservePermanentSlotSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -277,7 +277,7 @@ func (iface *LocationObserveInterface) ConnectedSlotSnippet(plug *interfaces.Plu
new := plugAppLabelExpr(plug)
snippet := bytes.Replace(locationObserveConnectedSlotAppArmor, old, new, -1)
return snippet, nil
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/lxd_support.go b/interfaces/builtin/lxd_support.go
index ab5151b242..a3068f37ce 100644
--- a/interfaces/builtin/lxd_support.go
+++ b/interfaces/builtin/lxd_support.go
@@ -49,7 +49,7 @@ func (iface *LxdSupportInterface) PermanentPlugSnippet(plug *interfaces.Plug, se
switch securitySystem {
case interfaces.SecurityDBus, interfaces.SecurityAppArmor,
interfaces.SecuritySecComp, interfaces.SecurityUDev,
- interfaces.SecurityMount:
+ interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -63,7 +63,7 @@ func (iface *LxdSupportInterface) ConnectedPlugSnippet(plug *interfaces.Plug, sl
case interfaces.SecuritySecComp:
return []byte(lxdSupportConnectedPlugSecComp), nil
case interfaces.SecurityDBus, interfaces.SecurityUDev,
- interfaces.SecurityMount:
+ interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -74,7 +74,7 @@ func (iface *LxdSupportInterface) PermanentSlotSnippet(slot *interfaces.Slot, se
switch securitySystem {
case interfaces.SecurityDBus, interfaces.SecurityAppArmor,
interfaces.SecuritySecComp, interfaces.SecurityUDev,
- interfaces.SecurityMount:
+ interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -85,7 +85,7 @@ func (iface *LxdSupportInterface) ConnectedSlotSnippet(plug *interfaces.Plug, sl
switch securitySystem {
case interfaces.SecurityDBus, interfaces.SecurityAppArmor,
interfaces.SecuritySecComp, interfaces.SecurityUDev,
- interfaces.SecurityMount:
+ interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/mir.go b/interfaces/builtin/mir.go
index 6a04e6912b..194efe2fa6 100644
--- a/interfaces/builtin/mir.go
+++ b/interfaces/builtin/mir.go
@@ -21,6 +21,7 @@ package builtin
import (
"bytes"
+
"github.com/snapcore/snapd/interfaces"
)
@@ -90,7 +91,7 @@ func (iface *MirInterface) PermanentPlugSnippet(
switch securitySystem {
case interfaces.SecurityAppArmor, interfaces.SecuritySecComp,
interfaces.SecurityUDev, interfaces.SecurityDBus,
- interfaces.SecurityMount:
+ interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -109,7 +110,7 @@ func (iface *MirInterface) ConnectedPlugSnippet(
return snippet, nil
case interfaces.SecuritySecComp:
return mirConnectedPlugSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -124,7 +125,7 @@ func (iface *MirInterface) PermanentSlotSnippet(
return mirPermanentSlotAppArmor, nil
case interfaces.SecuritySecComp:
return mirPermanentSlotSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -138,7 +139,7 @@ func (iface *MirInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *int
new := plugAppLabelExpr(plug)
snippet := bytes.Replace(mirConnectedSlotAppArmor, old, new, -1)
return snippet, nil
- case interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/modem_manager.go b/interfaces/builtin/modem_manager.go
index 7bd72d2ce7..0761a97e33 100644
--- a/interfaces/builtin/modem_manager.go
+++ b/interfaces/builtin/modem_manager.go
@@ -1152,7 +1152,7 @@ func (iface *ModemManagerInterface) Name() string {
func (iface *ModemManagerInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -1177,7 +1177,7 @@ func (iface *ModemManagerInterface) ConnectedPlugSnippet(plug *interfaces.Plug,
return snippet, nil
case interfaces.SecuritySecComp:
return modemManagerConnectedPlugSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -1194,7 +1194,7 @@ func (iface *ModemManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot,
return modemManagerPermanentSlotUdev, nil
case interfaces.SecurityDBus:
return modemManagerPermanentSlotDBus, nil
- case interfaces.SecurityMount:
+ case interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -1208,7 +1208,7 @@ func (iface *ModemManagerInterface) ConnectedSlotSnippet(plug *interfaces.Plug,
new := plugAppLabelExpr(plug)
snippet := bytes.Replace(modemManagerConnectedSlotAppArmor, old, new, -1)
return snippet, nil
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/mpris.go b/interfaces/builtin/mpris.go
index 0b97288d5b..8cda9e3089 100644
--- a/interfaces/builtin/mpris.go
+++ b/interfaces/builtin/mpris.go
@@ -161,7 +161,7 @@ func (iface *MprisInterface) Name() string {
func (iface *MprisInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -177,7 +177,7 @@ func (iface *MprisInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *i
return snippet, nil
case interfaces.SecuritySecComp:
return mprisConnectedPlugSecComp, nil
- case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -203,7 +203,7 @@ func (iface *MprisInterface) PermanentSlotSnippet(slot *interfaces.Slot, securit
return snippet, nil
case interfaces.SecuritySecComp:
return mprisPermanentSlotSecComp, nil
- case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -217,7 +217,7 @@ func (iface *MprisInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *i
new := plugAppLabelExpr(plug)
snippet := bytes.Replace(mprisConnectedSlotAppArmor, old, new, -1)
return snippet, nil
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/network_manager.go b/interfaces/builtin/network_manager.go
index 2f879a46d1..b63fb1472b 100644
--- a/interfaces/builtin/network_manager.go
+++ b/interfaces/builtin/network_manager.go
@@ -380,7 +380,7 @@ func (iface *NetworkManagerInterface) Name() string {
func (iface *NetworkManagerInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -405,7 +405,7 @@ func (iface *NetworkManagerInterface) ConnectedPlugSnippet(plug *interfaces.Plug
return snippet, nil
case interfaces.SecuritySecComp:
return networkManagerConnectedPlugSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -418,7 +418,7 @@ func (iface *NetworkManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot
return networkManagerPermanentSlotAppArmor, nil
case interfaces.SecuritySecComp:
return networkManagerPermanentSlotSecComp, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
case interfaces.SecurityDBus:
return networkManagerPermanentSlotDBus, nil
@@ -429,7 +429,7 @@ func (iface *NetworkManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot
func (iface *NetworkManagerInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/ppp.go b/interfaces/builtin/ppp.go
index 4b48500051..9ca358b48e 100644
--- a/interfaces/builtin/ppp.go
+++ b/interfaces/builtin/ppp.go
@@ -56,7 +56,7 @@ func (iface *PppInterface) Name() string {
func (iface *PppInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -71,7 +71,7 @@ func (iface *PppInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *int
return pppConnectedPlugAppArmor, nil
case interfaces.SecuritySecComp:
return nil, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -84,7 +84,7 @@ func (iface *PppInterface) PermanentSlotSnippet(slot *interfaces.Slot, securityS
return nil, nil
case interfaces.SecuritySecComp:
return nil, nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
case interfaces.SecurityDBus:
return nil, nil
@@ -95,7 +95,7 @@ func (iface *PppInterface) PermanentSlotSnippet(slot *interfaces.Slot, securityS
func (iface *PppInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/pulseaudio.go b/interfaces/builtin/pulseaudio.go
index eba3fee1b8..78ac45c29e 100644
--- a/interfaces/builtin/pulseaudio.go
+++ b/interfaces/builtin/pulseaudio.go
@@ -27,7 +27,7 @@ import (
)
const pulseaudioConnectedPlugAppArmor = `
-/{run,dev}/shm/pulse-shm-* rwk,
+/{run,dev}/shm/pulse-shm-* rwkm,
owner /{,var/}run/pulse/ r,
owner /{,var/}run/pulse/native rwk,
@@ -120,7 +120,7 @@ func (iface *PulseAudioInterface) Name() string {
func (iface *PulseAudioInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -139,7 +139,7 @@ func (iface *PulseAudioInterface) ConnectedPlugSnippet(plug *interfaces.Plug, sl
}
case interfaces.SecuritySecComp:
return []byte(pulseaudioConnectedPlugSecComp), nil
- case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -152,7 +152,7 @@ func (iface *PulseAudioInterface) PermanentSlotSnippet(slot *interfaces.Slot, se
return []byte(pulseaudioPermanentSlotAppArmor), nil
case interfaces.SecuritySecComp:
return []byte(pulseaudioPermanentSlotSecComp), nil
- case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -161,7 +161,7 @@ func (iface *PulseAudioInterface) PermanentSlotSnippet(slot *interfaces.Slot, se
func (iface *PulseAudioInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/serial_port.go b/interfaces/builtin/serial_port.go
index 0c8a535a89..7641521182 100644
--- a/interfaces/builtin/serial_port.go
+++ b/interfaces/builtin/serial_port.go
@@ -129,7 +129,7 @@ func (iface *SerialPortInterface) PermanentSlotSnippet(slot *interfaces.Slot, se
return nil, nil
}
return udevUsbDeviceSnippet("tty", usbVendor, usbProduct, "SYMLINK", strings.TrimPrefix(path, "/dev/")), nil
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -139,7 +139,7 @@ func (iface *SerialPortInterface) PermanentSlotSnippet(slot *interfaces.Slot, se
// ConnectedSlotSnippet no extra permissions granted on connection
func (iface *SerialPortInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -149,7 +149,7 @@ func (iface *SerialPortInterface) ConnectedSlotSnippet(plug *interfaces.Plug, sl
// PermanentPlugSnippet no permissions provided to plug permanently
func (iface *SerialPortInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -188,7 +188,7 @@ func (iface *SerialPortInterface) ConnectedPlugSnippet(plug *interfaces.Plug, sl
udevSnippet.Write(udevUsbDeviceSnippet("tty", usbVendor, usbProduct, "TAG", tag))
}
return udevSnippet.Bytes(), nil
- case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount:
+ case interfaces.SecuritySecComp, interfaces.SecurityDBus, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/builtin/udisks2.go b/interfaces/builtin/udisks2.go
index b890fa1072..a1723e7546 100644
--- a/interfaces/builtin/udisks2.go
+++ b/interfaces/builtin/udisks2.go
@@ -356,7 +356,7 @@ func (iface *UDisks2Interface) Name() string {
func (iface *UDisks2Interface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
switch securitySystem {
- case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -374,7 +374,7 @@ func (iface *UDisks2Interface) ConnectedPlugSnippet(plug *interfaces.Plug, slot
return []byte(udisks2ConnectedPlugDBus), nil
case interfaces.SecuritySecComp:
return []byte(udisks2ConnectedPlugSecComp), nil
- case interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -391,7 +391,7 @@ func (iface *UDisks2Interface) PermanentSlotSnippet(slot *interfaces.Slot, secur
return []byte(udisks2PermanentSlotSecComp), nil
case interfaces.SecurityUDev:
return []byte(udisks2PermanentSlotUDev), nil
- case interfaces.SecurityMount:
+ case interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
@@ -405,7 +405,7 @@ func (iface *UDisks2Interface) ConnectedSlotSnippet(plug *interfaces.Plug, slot
new := plugAppLabelExpr(plug)
snippet := bytes.Replace(udisks2ConnectedSlotAppArmor, old, new, -1)
return snippet, nil
- case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount:
+ case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount, interfaces.SecurityKMod:
return nil, nil
default:
return nil, interfaces.ErrUnknownSecurity
diff --git a/interfaces/core.go b/interfaces/core.go
index 2c8d105ce8..7d8bca7e5f 100644
--- a/interfaces/core.go
+++ b/interfaces/core.go
@@ -154,6 +154,8 @@ const (
SecurityUDev SecuritySystem = "udev"
// SecurityMount identifies the mount security system.
SecurityMount SecuritySystem = "mount"
+ // SecurityKMod identifies the kernel modules security system
+ SecurityKMod SecuritySystem = "kmod"
)
var (
diff --git a/interfaces/kmod/backend.go b/interfaces/kmod/backend.go
new file mode 100644
index 0000000000..cca664c470
--- /dev/null
+++ b/interfaces/kmod/backend.go
@@ -0,0 +1,42 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package kmod
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/snap"
+)
+
+// Backend is responsible for maintaining kernel modules
+type Backend struct{}
+
+// Name returns the name of the backend.
+func (b *Backend) Name() string {
+ return "kmod"
+}
+
+func (b *Backend) Setup(snapInfo *snap.Info, devMode bool, repo *interfaces.Repository) error {
+ // TODO: get snippets, load modules, create /etc/modules-load.d/snap.modules.conf file
+ return nil
+}
+
+func (b *Backend) Remove(snapName string) error {
+ return nil
+}
diff --git a/interfaces/kmod/kmod.go b/interfaces/kmod/kmod.go
new file mode 100644
index 0000000000..f40849089b
--- /dev/null
+++ b/interfaces/kmod/kmod.go
@@ -0,0 +1,20 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package kmod
diff --git a/interfaces/repo.go b/interfaces/repo.go
index fcad4e3caf..6b83fab821 100644
--- a/interfaces/repo.go
+++ b/interfaces/repo.go
@@ -716,6 +716,17 @@ func (r *Repository) DisconnectSnap(snapName string) ([]string, error) {
return result, nil
}
+// isLivePatchSnap checks special Name/Developer combinations to see
+// if this particular snap's connections should be automatically connected even
+// if the interfaces are not autoconnect and the snap is not an OS snap.
+// FIXME: remove once we have assertions that provide this feature
+func isLivePatchSnap(snap *snap.Info) bool {
+ if snap.Name() == "canonical-livepatch" && snap.DeveloperID == "canonical" {
+ return true
+ }
+ return false
+}
+
// AutoConnectCandidates finds and returns viable auto-connection candidates
// for a given plug.
func (r *Repository) AutoConnectCandidates(plugSnapName, plugName string) []*Slot {
@@ -726,13 +737,11 @@ func (r *Repository) AutoConnectCandidates(plugSnapName, plugName string) []*Slo
if plug == nil {
return nil
}
- if r.ifaces[plug.Interface].AutoConnect() == false {
- return nil
- }
+
var candidates []*Slot
for _, slotsForSnap := range r.slots {
for _, slot := range slotsForSnap {
- if isAutoConnectCandidate(plug, slot) {
+ if r.isAutoConnectCandidate(plug, slot) {
candidates = append(candidates, slot)
}
}
@@ -742,7 +751,20 @@ func (r *Repository) AutoConnectCandidates(plugSnapName, plugName string) []*Slo
// isAutoConnectCandidate returns true if the plug is a candidate to
// automatically connect to the given slot.
-func isAutoConnectCandidate(plug *Plug, slot *Slot) bool {
+func (r *Repository) isAutoConnectCandidate(plug *Plug, slot *Slot) bool {
+ if slot.Interface != plug.Interface {
+ return false
+ }
+
+ // FIXME: remove once we have assertions that provide this feature
+ if isLivePatchSnap(plug.Snap) {
+ return true
+ }
+
+ if !r.ifaces[plug.Interface].AutoConnect() {
+ return false
+ }
+
// content sharing auto connect candidates
if slot.Interface == "content" {
if slot.Attrs["content"] == plug.Attrs["content"] && slot.Snap.Developer == plug.Snap.Developer {
@@ -754,7 +776,7 @@ func isAutoConnectCandidate(plug *Plug, slot *Slot) bool {
}
// OS snap auto connect candidates
- if slot.Snap.Type == snap.TypeOS && slot.Interface == plug.Interface {
+ if slot.Snap.Type == snap.TypeOS {
return true
}
diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go
index db369e6b9c..4cf90de0fa 100644
--- a/interfaces/repo_test.go
+++ b/interfaces/repo_test.go
@@ -1239,3 +1239,85 @@ func (s *RepositorySuite) TestAutoConnectContentInterfaceNoMatchingDeveloper(c *
candidateSlots := repo.AutoConnectCandidates("content-plug-snap", "import-content")
c.Check(candidateSlots, HasLen, 0)
}
+
+func makeLivepatchConnectionTestSnaps(c *C, name, developer string) (*Repository, *snap.Info, *snap.Info) {
+ repo := NewRepository()
+ err := repo.AddInterface(&TestInterface{InterfaceName: "restricted", AutoConnectFlag: false})
+ c.Assert(err, IsNil)
+
+ err = repo.AddInterface(&TestInterface{InterfaceName: "non-restricted", AutoConnectFlag: true})
+ c.Assert(err, IsNil)
+
+ plugSnap, err := snap.InfoFromSnapYaml([]byte(fmt.Sprintf(`
+name: %s
+plugs:
+ restricted:
+ interface: restricted
+ non-restricted:
+ interface: non-restricted
+`, name)))
+ c.Assert(err, IsNil)
+ slotSnap, err := snap.InfoFromSnapYaml([]byte(`
+name: ubuntu-core
+type: os
+slots:
+ restricted:
+ interface: restricted
+ non-restricted:
+ interface: non-restricted
+`))
+ c.Assert(err, IsNil)
+
+ plugSnap.DeveloperID = developer
+ slotSnap.DeveloperID = "canonical"
+ slotSnap.Type = snap.TypeOS
+
+ err = repo.AddSnap(plugSnap)
+ c.Assert(err, IsNil)
+ err = repo.AddSnap(slotSnap)
+ c.Assert(err, IsNil)
+
+ return repo, plugSnap, slotSnap
+}
+
+// test auto-connecting livepatch interfaces for special snaps
+func (s *RepositorySuite) TestAutoConnectLivepatchInterfaces(c *C) {
+ repo, _, _ := makeLivepatchConnectionTestSnaps(c, "canonical-livepatch", "canonical")
+ candidateSlots := repo.AutoConnectCandidates("canonical-livepatch", "restricted")
+ c.Check(candidateSlots, HasLen, 1)
+ c.Check(candidateSlots[0].Snap.Name(), Equals, "ubuntu-core")
+ c.Check(candidateSlots[0].Snap.DeveloperID, Equals, "canonical")
+ c.Check(candidateSlots[0].Name, Equals, "restricted")
+}
+
+// test auto-connecting unrestricted (auto-connect) interfaces for special snaps
+func (s *RepositorySuite) TestAutoConnectNonRestrictedInterfaces(c *C) {
+ repo, _, _ := makeLivepatchConnectionTestSnaps(c, "canonical-livepatch", "canonical")
+ candidateSlots := repo.AutoConnectCandidates("canonical-livepatch", "non-restricted")
+ c.Check(candidateSlots, HasLen, 1)
+ c.Check(candidateSlots[0].Snap.Name(), Equals, "ubuntu-core")
+ c.Check(candidateSlots[0].Snap.DeveloperID, Equals, "canonical")
+ c.Check(candidateSlots[0].Name, Equals, "non-restricted")
+}
+
+// test auto-connecting unrestricted (auto-connect) interfaces for non-special snaps
+func (s *RepositorySuite) TestAutoConnectNonRestrictedInterfacesNonSpecialSnap2(c *C) {
+ repo, _, _ := makeLivepatchConnectionTestSnaps(c, "canonical-livepatch", "someone-else")
+ candidateSlots := repo.AutoConnectCandidates("canonical-livepatch", "non-restricted")
+ c.Check(candidateSlots, HasLen, 1)
+ c.Check(candidateSlots[0].Snap.Name(), Equals, "ubuntu-core")
+ c.Check(candidateSlots[0].Snap.DeveloperID, Equals, "canonical")
+ c.Check(candidateSlots[0].Name, Equals, "non-restricted")
+}
+
+func (s *RepositorySuite) TestAutoConnectLivepatchWrongDeveloper(c *C) {
+ repo, _, _ := makeLivepatchConnectionTestSnaps(c, "canonical-livepatch", "somebody")
+ candidateSlots := repo.AutoConnectCandidates("canonical-livepatch", "restricted")
+ c.Check(candidateSlots, HasLen, 0)
+}
+
+func (s *RepositorySuite) TestAutoConnectLivepatchWrongName(c *C) {
+ repo, _, _ := makeLivepatchConnectionTestSnaps(c, "something", "canonical")
+ candidateSlots := repo.AutoConnectCandidates("canonical-livepatch", "restricted")
+ c.Check(candidateSlots, HasLen, 0)
+}
diff --git a/overlord/assertstate/assertmgr.go b/overlord/assertstate/assertmgr.go
index f291cd40f4..410bf5baf8 100644
--- a/overlord/assertstate/assertmgr.go
+++ b/overlord/assertstate/assertmgr.go
@@ -24,6 +24,7 @@ package assertstate
import (
"fmt"
+ "strings"
"gopkg.in/tomb.v2"
@@ -112,30 +113,18 @@ func userFromUserID(st *state.State, userID int) (*auth.UserState, error) {
return auth.User(st, userID)
}
-type assertionNotFoundError struct {
- ref *asserts.Ref
+type fetcher struct {
+ db *asserts.Database
+ asserts.Fetcher
+ fetched []asserts.Assertion
}
-func (e *assertionNotFoundError) Error() string {
- return fmt.Sprintf("%s %v not found", e.ref.Type.Name, e.ref.PrimaryKey)
-}
-
-// fetch fetches or updates the referenced assertion and all its prerequisites from the store and adds them to the system assertion database. It does not fail if required assertions were already present.
-func fetch(s *state.State, ref *asserts.Ref, userID int) error {
- // TODO: once we have a bulk assertion retrieval endpoint this approach will change
-
- user, err := userFromUserID(s, userID)
- if err != nil {
- return err
- }
-
+// newFetches creates a fetcher used to retrieve assertions from the store and later commit them to the system database in one go.
+func newFetcher(s *state.State, user *auth.UserState) *fetcher {
db := cachedDB(s)
sto := snapstate.Store(s)
- s.Unlock()
- defer s.Lock()
-
- got := []asserts.Assertion{}
+ f := &fetcher{db: db}
retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
// TODO: ignore errors if already in db?
@@ -143,21 +132,32 @@ func fetch(s *state.State, ref *asserts.Ref, userID int) error {
}
save := func(a asserts.Assertion) error {
- got = append(got, a)
+ f.fetched = append(f.fetched, a)
return nil
}
- f := asserts.NewFetcher(db, retrieve, save)
+ f.Fetcher = asserts.NewFetcher(db, retrieve, save)
- if err := f.Fetch(ref); err != nil {
- return err
- }
+ return f
+}
- s.Lock()
- defer s.Unlock()
+type commitError struct {
+ errs []error
+}
- for _, a := range got {
- err := db.Add(a)
+func (e *commitError) Error() string {
+ l := []string{""}
+ for _, e := range e.errs {
+ l = append(l, e.Error())
+ }
+ return fmt.Sprintf("cannot add some assertions to the system database:%s", strings.Join(l, "\n - "))
+}
+
+// commit does a best effort of adding all the fetched assertions to the system database.
+func (f *fetcher) commit() error {
+ var errs []error
+ for _, a := range f.fetched {
+ err := f.db.Add(a)
if revErr, ok := err.(*asserts.RevisionError); ok {
if revErr.Current >= a.Revision() {
// be idempotent
@@ -165,17 +165,39 @@ func fetch(s *state.State, ref *asserts.Ref, userID int) error {
continue
}
}
- // TODO: trigger w. caller a global sanity check if a is revoked
- // (but try to save as much possible still),
- // or err is a check error
if err != nil {
- return err
+ errs = append(errs, err)
}
}
-
+ if len(errs) != 0 {
+ return &commitError{errs: errs}
+ }
return nil
}
+func doFetch(s *state.State, userID int, fetching func(asserts.Fetcher) error) error {
+ // TODO: once we have a bulk assertion retrieval endpoint this approach will change
+
+ user, err := userFromUserID(s, userID)
+ if err != nil {
+ return err
+ }
+
+ f := newFetcher(s, user)
+
+ s.Unlock()
+ err = fetching(f)
+ s.Lock()
+ if err != nil {
+ return err
+ }
+
+ // TODO: trigger w. caller a global sanity check if a is revoked
+ // (but try to save as much possible still),
+ // or err is a check error
+ return f.commit()
+}
+
// doValidateSnap fetches the relevant assertions for the snap being installed and cross checks them with the snap.
func doValidateSnap(t *state.Task, _ *tomb.Tomb) error {
t.State().Lock()
@@ -191,13 +213,9 @@ func doValidateSnap(t *state.Task, _ *tomb.Tomb) error {
return err
}
- // for now starting from the snap-revision will get us all other relevant assertions
- ref := &asserts.Ref{
- Type: asserts.SnapRevisionType,
- PrimaryKey: []string{sha3_384},
- }
-
- err = fetch(t.State(), ref, ss.UserID)
+ err = doFetch(t.State(), ss.UserID, func(f asserts.Fetcher) error {
+ return snapasserts.FetchSnapAssertions(f, sha3_384)
+ })
if notFound, ok := err.(*store.AssertionNotFoundError); ok {
if notFound.Ref.Type == asserts.SnapRevisionType {
return fmt.Errorf("cannot verify snap %q, no matching signatures found", ss.Name())
@@ -221,3 +239,27 @@ func doValidateSnap(t *state.Task, _ *tomb.Tomb) error {
// TODO: set DeveloperID from assertions
return nil
}
+
+// RefreshSnapDeclarations refetches all the current snap declarations and their prerequisites.
+func RefreshSnapDeclarations(s *state.State, userID int) error {
+ snapStates, err := snapstate.All(s)
+ if err != nil {
+ return nil
+ }
+ fetching := func(f asserts.Fetcher) error {
+ for _, snapState := range snapStates {
+ info, err := snapState.CurrentInfo()
+ if err != nil {
+ return err
+ }
+ if info.SnapID == "" {
+ continue
+ }
+ if err := snapasserts.FetchSnapDeclaration(f, info.SnapID); err != nil {
+ return fmt.Errorf("cannot refresh snap-declaration for %q: %v", info.Name(), err)
+ }
+ }
+ return nil
+ }
+ return doFetch(s, userID, fetching)
+}
diff --git a/overlord/assertstate/assertmgr_test.go b/overlord/assertstate/assertmgr_test.go
index 6b7b73911d..3aece5336d 100644
--- a/overlord/assertstate/assertmgr_test.go
+++ b/overlord/assertstate/assertmgr_test.go
@@ -212,7 +212,7 @@ func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) {
}
}
-func (s *assertMgrSuite) TestFetch(c *C) {
+func (s *assertMgrSuite) TestDoFetch(c *C) {
s.prereqSnapAssertions(c, 10)
s.state.Lock()
@@ -223,7 +223,9 @@ func (s *assertMgrSuite) TestFetch(c *C) {
PrimaryKey: []string{makeDigest(10)},
}
- err := assertstate.Fetch(s.state, ref, 0)
+ err := assertstate.DoFetch(s.state, 0, func(f asserts.Fetcher) error {
+ return f.Fetch(ref)
+ })
c.Assert(err, IsNil)
snapRev, err := ref.Resolve(assertstate.DB(s.state).Find)
@@ -241,8 +243,11 @@ func (s *assertMgrSuite) TestFetchIdempotent(c *C) {
Type: asserts.SnapRevisionType,
PrimaryKey: []string{makeDigest(10)},
}
+ fetching := func(f asserts.Fetcher) error {
+ return f.Fetch(ref)
+ }
- err := assertstate.Fetch(s.state, ref, 0)
+ err := assertstate.DoFetch(s.state, 0, fetching)
c.Assert(err, IsNil)
ref = &asserts.Ref{
@@ -250,7 +255,7 @@ func (s *assertMgrSuite) TestFetchIdempotent(c *C) {
PrimaryKey: []string{makeDigest(11)},
}
- err = assertstate.Fetch(s.state, ref, 0)
+ err = assertstate.DoFetch(s.state, 0, fetching)
c.Assert(err, IsNil)
snapRev, err := ref.Resolve(assertstate.DB(s.state).Find)
@@ -369,3 +374,110 @@ func (s *assertMgrSuite) TestValidateSnapCrossCheckFail(c *C) {
c.Assert(chg.Err(), ErrorMatches, `(?s).*cannot install snap "f" that is undergoing a rename to "foo".*`)
}
+
+func (s *assertMgrSuite) TestRefreshSnapDeclarations(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ err := s.storeSigning.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": "foo-id",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDeclFoo, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
+
+ headers = map[string]interface{}{
+ "series": "16",
+ "snap-id": "bar-id",
+ "snap-name": "bar",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapDeclBar, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDeclBar)
+ c.Assert(err, IsNil)
+
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "foo", SnapID: "foo-id", Revision: snap.R(7)},
+ },
+ Current: snap.R(7),
+ })
+ snapstate.Set(s.state, "bar", &snapstate.SnapState{
+ Active: false,
+ Sequence: []*snap.SideInfo{
+ {RealName: "bar", SnapID: "bar-id", Revision: snap.R(3)},
+ },
+ Current: snap.R(3),
+ })
+ snapstate.Set(s.state, "local", &snapstate.SnapState{
+ Active: false,
+ Sequence: []*snap.SideInfo{
+ {RealName: "local", Revision: snap.R(-1)},
+ },
+ Current: snap.R(-1),
+ })
+
+ // previous state
+ err = assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclBar)
+ c.Assert(err, IsNil)
+
+ // one changed assertion
+ headers = map[string]interface{}{
+ "series": "16",
+ "snap-id": "foo-id",
+ "snap-name": "fo-o",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ "revision": "1",
+ }
+ snapDeclFoo1, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(snapDeclFoo1)
+ c.Assert(err, IsNil)
+
+ err = assertstate.RefreshSnapDeclarations(s.state, 0)
+ c.Assert(err, IsNil)
+
+ a, err := assertstate.DB(s.state).Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(err, IsNil)
+ c.Check(a.(*asserts.SnapDeclaration).SnapName(), Equals, "fo-o")
+
+ // another one
+ // one changed assertion
+ headers = s.dev1Acct.Headers()
+ headers["display-name"] = "Dev 1 edited display-name"
+ headers["revision"] = "1"
+ dev1Acct1, err := s.storeSigning.Sign(asserts.AccountType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(dev1Acct1)
+ c.Assert(err, IsNil)
+
+ err = assertstate.RefreshSnapDeclarations(s.state, 0)
+ c.Assert(err, IsNil)
+
+ a, err = assertstate.DB(s.state).Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(a.(*asserts.Account).DisplayName(), Equals, "Dev 1 edited display-name")
+}
diff --git a/overlord/assertstate/export_test.go b/overlord/assertstate/export_test.go
index ca343445d7..1646c05534 100644
--- a/overlord/assertstate/export_test.go
+++ b/overlord/assertstate/export_test.go
@@ -21,5 +21,5 @@ package assertstate
// expose for testing
var (
- Fetch = fetch
+ DoFetch = doFetch
)
diff --git a/overlord/auth/auth.go b/overlord/auth/auth.go
index 2f1fa46749..ad99a566a1 100644
--- a/overlord/auth/auth.go
+++ b/overlord/auth/auth.go
@@ -228,15 +228,17 @@ type DeviceAssertions interface {
}
var (
+ // ErrNoSerial indicates that a device serial is not set yet.
ErrNoSerial = errors.New("no device serial yet")
)
// An AuthContext exposes authorization data and handles its updates.
type AuthContext interface {
Device() (*DeviceState, error)
- UpdateDevice(device *DeviceState) error
- UpdateUser(user *UserState) error
+ UpdateDeviceAuth(device *DeviceState, sessionMacaroon string) (actual *DeviceState, err error)
+
+ UpdateUserAuth(user *UserState, discharges []string) (actual *UserState, err error)
StoreID(fallback string) (string, error)
@@ -265,20 +267,46 @@ func (ac *authContext) Device() (*DeviceState, error) {
return Device(ac.state)
}
-// UpdateDevice updates device in state.
-func (ac *authContext) UpdateDevice(device *DeviceState) error {
+// UpdateDeviceAuth updates the device auth details in state.
+// The last update wins but other device details are left unchanged.
+// It returns the updated device state value.
+func (ac *authContext) UpdateDeviceAuth(device *DeviceState, newSessionMacaroon string) (actual *DeviceState, err error) {
ac.state.Lock()
defer ac.state.Unlock()
- return SetDevice(ac.state, device)
+ cur, err := Device(ac.state)
+ if err != nil {
+ return nil, err
+ }
+
+ // just do it, last update wins
+ cur.SessionMacaroon = newSessionMacaroon
+ if err := SetDevice(ac.state, cur); err != nil {
+ return nil, fmt.Errorf("internal error: cannot update just read device state: %v", err)
+ }
+
+ return cur, nil
}
-// UpdateUser updates user in state.
-func (ac *authContext) UpdateUser(user *UserState) error {
+// UpdateUserAuth updates the user auth details in state.
+// The last update wins but other user details are left unchanged.
+// It returns the updated user state value.
+func (ac *authContext) UpdateUserAuth(user *UserState, newDischarges []string) (actual *UserState, err error) {
ac.state.Lock()
defer ac.state.Unlock()
- return UpdateUser(ac.state, user)
+ cur, err := User(ac.state, user.ID)
+ if err != nil {
+ return nil, err
+ }
+
+ // just do it, last update wins
+ cur.StoreDischarges = newDischarges
+ if err := UpdateUser(ac.state, cur); err != nil {
+ return nil, fmt.Errorf("internal error: cannot update just read user state: %v", err)
+ }
+
+ return cur, nil
}
// StoreID returns the store id according to system state or
diff --git a/overlord/auth/auth_test.go b/overlord/auth/auth_test.go
index 6b0d5ffdd6..4a44107c56 100644
--- a/overlord/auth/auth_test.go
+++ b/overlord/auth/auth_test.go
@@ -283,16 +283,15 @@ func (as *authSuite) TestSetDevice(c *C) {
c.Check(device, DeepEquals, &auth.DeviceState{Brand: "some-brand"})
}
-func (as *authSuite) TestAuthContextUpdateUser(c *C) {
+func (as *authSuite) TestAuthContextUpdateUserAuth(c *C) {
as.state.Lock()
user, _ := auth.NewUser(as.state, "username", "macaroon", []string{"discharge"})
as.state.Unlock()
- user.Username = "different"
- user.StoreDischarges = []string{"updated-discharge"}
+ newDischarges := []string{"updated-discharge"}
authContext := auth.NewAuthContext(as.state, nil)
- err := authContext.UpdateUser(user)
+ user, err := authContext.UpdateUserAuth(user, newDischarges)
c.Check(err, IsNil)
as.state.Lock()
@@ -300,9 +299,43 @@ func (as *authSuite) TestAuthContextUpdateUser(c *C) {
as.state.Unlock()
c.Check(err, IsNil)
c.Check(userFromState, DeepEquals, user)
+ c.Check(userFromState.Discharges, DeepEquals, []string{"discharge"})
+ c.Check(user.StoreDischarges, DeepEquals, newDischarges)
+}
+
+func (as *authSuite) TestAuthContextUpdateUserAuthOtherUpdate(c *C) {
+ as.state.Lock()
+ user, _ := auth.NewUser(as.state, "username", "macaroon", []string{"discharge"})
+ otherUpdateUser := *user
+ otherUpdateUser.Macaroon = "macaroon2"
+ otherUpdateUser.StoreDischarges = []string{"other-discharges"}
+ err := auth.UpdateUser(as.state, &otherUpdateUser)
+ as.state.Unlock()
+ c.Assert(err, IsNil)
+
+ newDischarges := []string{"updated-discharge"}
+
+ authContext := auth.NewAuthContext(as.state, nil)
+ // last discharges win
+ curUser, err := authContext.UpdateUserAuth(user, newDischarges)
+ c.Assert(err, IsNil)
+
+ as.state.Lock()
+ userFromState, err := auth.User(as.state, user.ID)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(userFromState, DeepEquals, curUser)
+ c.Check(curUser, DeepEquals, &auth.UserState{
+ ID: user.ID,
+ Username: "username",
+ Macaroon: "macaroon2",
+ Discharges: []string{"discharge"},
+ StoreMacaroon: "macaroon",
+ StoreDischarges: newDischarges,
+ })
}
-func (as *authSuite) TestAuthContextUpdateUserInvalid(c *C) {
+func (as *authSuite) TestAuthContextUpdateUserAuthInvalid(c *C) {
as.state.Lock()
_, _ = auth.NewUser(as.state, "username", "macaroon", []string{"discharge"})
as.state.Unlock()
@@ -314,7 +347,7 @@ func (as *authSuite) TestAuthContextUpdateUserInvalid(c *C) {
}
authContext := auth.NewAuthContext(as.state, nil)
- err := authContext.UpdateUser(user)
+ _, err := authContext.UpdateUserAuth(user, nil)
c.Assert(err, ErrorMatches, "invalid user")
}
@@ -340,21 +373,50 @@ func (as *authSuite) TestAuthContextDevice(c *C) {
c.Check(deviceFromState, DeepEquals, device)
}
-func (as *authSuite) TestAuthContextUpdateDevice(c *C) {
+func (as *authSuite) TestAuthContextUpdateDeviceAuth(c *C) {
as.state.Lock()
device, err := auth.Device(as.state)
as.state.Unlock()
c.Check(err, IsNil)
c.Check(device, DeepEquals, &auth.DeviceState{})
+ sessionMacaroon := "the-device-macaroon"
+
authContext := auth.NewAuthContext(as.state, nil)
- device.SessionMacaroon = "the-device-macaroon"
- err = authContext.UpdateDevice(device)
+ device, err = authContext.UpdateDeviceAuth(device, sessionMacaroon)
c.Check(err, IsNil)
deviceFromState, err := authContext.Device()
c.Check(err, IsNil)
c.Check(deviceFromState, DeepEquals, device)
+ c.Check(deviceFromState.SessionMacaroon, DeepEquals, sessionMacaroon)
+}
+
+func (as *authSuite) TestAuthContextUpdateDeviceAuthOtherUpdate(c *C) {
+ as.state.Lock()
+ device, _ := auth.Device(as.state)
+ otherUpdateDevice := *device
+ otherUpdateDevice.SessionMacaroon = "othe-session-macaroon"
+ otherUpdateDevice.KeyID = "KEYID"
+ err := auth.SetDevice(as.state, &otherUpdateDevice)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+
+ sessionMacaroon := "the-device-macaroon"
+
+ authContext := auth.NewAuthContext(as.state, nil)
+ curDevice, err := authContext.UpdateDeviceAuth(device, sessionMacaroon)
+ c.Assert(err, IsNil)
+
+ as.state.Lock()
+ deviceFromState, err := auth.Device(as.state)
+ as.state.Unlock()
+ c.Check(err, IsNil)
+ c.Check(deviceFromState, DeepEquals, curDevice)
+ c.Check(curDevice, DeepEquals, &auth.DeviceState{
+ KeyID: "KEYID",
+ SessionMacaroon: sessionMacaroon,
+ })
}
func (as *authSuite) TestAuthContextStoreIDFallback(c *C) {
diff --git a/overlord/hookstate/hookmgr.go b/overlord/hookstate/hookmgr.go
index 1c32cad56d..fdc8ffeb64 100644
--- a/overlord/hookstate/hookmgr.go
+++ b/overlord/hookstate/hookmgr.go
@@ -36,9 +36,6 @@ import (
"github.com/snapcore/snapd/snap"
)
-// HookRunner is the hook runner. Exported here for use in tests.
-var HookRunner = runHookAndWait
-
// HookManager is responsible for the maintenance of hooks in the system state.
// It runs hooks when they're requested, assuming they're present in the given
// snap. Otherwise they're skipped with no error.
@@ -185,7 +182,7 @@ func (m *HookManager) doRunHook(task *state.Task, tomb *tomb.Tomb) error {
}
// Actually run the hook
- output, err := HookRunner(setup.Snap, setup.Revision, setup.Hook, contextID, tomb)
+ output, err := runHookAndWait(setup.Snap, setup.Revision, setup.Hook, contextID, tomb)
if err != nil {
err = osutil.OutputErr(output, err)
if handlerErr := handler.Error(err); handlerErr != nil {
diff --git a/overlord/managers_test.go b/overlord/managers_test.go
index a82f7a715b..6fe7486dc2 100644
--- a/overlord/managers_test.go
+++ b/overlord/managers_test.go
@@ -424,7 +424,7 @@ apps:
snapR, err = os.Open(snapPath)
c.Assert(err, IsNil)
- ts, err = snapstate.Update(st, "foo", "stable", 0, 0)
+ ts, err = snapstate.Update(st, "foo", "stable", snap.R(0), 0, 0)
c.Assert(err, IsNil)
chg = st.NewChange("upgrade-snap", "...")
chg.AddAll(ts)
diff --git a/overlord/patch/patch.go b/overlord/patch/patch.go
index ab25fc0aea..3a7ff2623b 100644
--- a/overlord/patch/patch.go
+++ b/overlord/patch/patch.go
@@ -28,7 +28,7 @@ import (
)
// Level is the current implemented patch level of the state format and content.
-var Level = 3
+var Level = 4
// patches maps from patch level L to the function that moves from L-1 to L.
var patches = make(map[int]func(s *state.State) error)
diff --git a/overlord/patch/patch4.go b/overlord/patch/patch4.go
new file mode 100644
index 0000000000..5ba1bf7c40
--- /dev/null
+++ b/overlord/patch/patch4.go
@@ -0,0 +1,282 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+func init() {
+ patches[4] = patch4
+}
+
+type patch4Flags int
+
+type patch4DownloadInfo struct {
+ AnonDownloadURL string `json:"anon-download-url,omitempty"`
+ DownloadURL string `json:"download-url,omitempty"`
+
+ Size int64 `json:"size,omitempty"`
+ Sha3_384 string `json:"sha3-384,omitempty"`
+}
+
+type patch4SideInfo struct {
+ RealName string `yaml:"name,omitempty" json:"name,omitempty"`
+ SnapID string `yaml:"snap-id" json:"snap-id"`
+ Revision snap.Revision `yaml:"revision" json:"revision"`
+ Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
+ DeveloperID string `yaml:"developer-id,omitempty" json:"developer-id,omitempty"`
+ Developer string `yaml:"developer,omitempty" json:"developer,omitempty"`
+ EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
+ EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
+ Private bool `yaml:"private,omitempty" json:"private,omitempty"`
+}
+
+type patch4SnapSetup struct {
+ Channel string `json:"channel,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+ Flags patch4Flags `json:"flags,omitempty"`
+ SnapPath string `json:"snap-path,omitempty"`
+ DownloadInfo *patch4DownloadInfo `json:"download-info,omitempty"`
+ SideInfo *patch4SideInfo `json:"side-info,omitempty"`
+}
+
+func (ss *patch4SnapSetup) Name() string {
+ if ss.SideInfo.RealName == "" {
+ panic("SnapSetup.SideInfo.RealName not set")
+ }
+ return ss.SideInfo.RealName
+}
+
+func (ss *patch4SnapSetup) Revision() snap.Revision {
+ return ss.SideInfo.Revision
+}
+
+type patch4SnapState struct {
+ SnapType string `json:"type"` // Use Type and SetType
+ Sequence []*patch4SideInfo `json:"sequence"`
+ Active bool `json:"active,omitempty"`
+ Current snap.Revision `json:"current"`
+ Channel string `json:"channel,omitempty"`
+ Flags patch4Flags `json:"flags,omitempty"`
+}
+
+func (snapst *patch4SnapState) LastIndex(revision snap.Revision) int {
+ for i := len(snapst.Sequence) - 1; i >= 0; i-- {
+ if snapst.Sequence[i].Revision == revision {
+ return i
+ }
+ }
+ return -1
+}
+
+type patch4T struct{} // for namespacing of the helpers
+
+func (p4 patch4T) taskSnapSetup(task *state.Task) (*patch4SnapSetup, error) {
+ var ss patch4SnapSetup
+
+ switch err := p4.getMaybe(task, "snap-setup", &ss); err {
+ case state.ErrNoState:
+ // continue below
+ case nil:
+ return &ss, nil
+ default:
+ return nil, err
+ }
+
+ var id string
+ if err := p4.get(task, "snap-setup-task", &id); err != nil {
+ return nil, err
+ }
+
+ if err := p4.get(task.State().Task(id), "snap-setup", &ss); err != nil {
+ return nil, err
+ }
+
+ return &ss, nil
+}
+
+func (p4 patch4T) snapSetupAndState(task *state.Task) (*patch4SnapSetup, *patch4SnapState, error) {
+ var snapst patch4SnapState
+
+ ss, err := p4.taskSnapSetup(task)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var snaps map[string]*json.RawMessage
+ err = task.State().Get("snaps", &snaps)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot get snaps state: %v", err)
+ }
+ raw, ok := snaps[ss.Name()]
+ if !ok {
+ return nil, nil, fmt.Errorf("cannot get snap state for %q: %v", ss.Name(), err)
+ }
+ err = json.Unmarshal([]byte(*raw), &snapst)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot get state for snap %q: %v", ss.Name(), err)
+ }
+
+ return ss, &snapst, err
+}
+
+// getMaybe calls task.Get and wraps any non-ErrNoState error in an informative message
+func (p4 patch4T) getMaybe(task *state.Task, key string, value interface{}) error {
+ return p4.gget(task, key, true, value)
+}
+
+// get calls task.Get and wraps any error in an informative message
+func (p4 patch4T) get(task *state.Task, key string, value interface{}) error {
+ return p4.gget(task, key, false, value)
+}
+
+// gget does the actual work of get and getMaybe
+func (patch4T) gget(task *state.Task, key string, passThroughMissing bool, value interface{}) error {
+ err := task.Get(key, value)
+ if err == nil || (passThroughMissing && err == state.ErrNoState) {
+ return err
+ }
+ change := task.Change()
+
+ return fmt.Errorf("cannot get %q from task %s (%s) of change %s (%s): %v",
+ key, task.ID(), task.Kind(), change.ID(), change.Kind(), err)
+}
+
+func (p4 patch4T) addCleanup(task *state.Task) error {
+ // NOTE we could check for the status of the change itself, but
+ // copy-snap-data is the one creating the trash, so if it's run there's
+ // no sense in fiddling with the change.
+ if task.Status().Ready() {
+ return nil
+ }
+
+ ss, err := p4.taskSnapSetup(task)
+ if err != nil {
+ return err
+ }
+
+ var tid string
+ if err := p4.get(task, "snap-setup-task", &tid); err != nil {
+ return err
+ }
+
+ change := task.Change()
+ revisionStr := ""
+ if ss.SideInfo != nil {
+ revisionStr = fmt.Sprintf(" (%s)", ss.Revision())
+ }
+
+ tasks := change.Tasks()
+ last := tasks[len(tasks)-1]
+ newTask := task.State().NewTask("cleanup", fmt.Sprintf("Clean up %q%s install", ss.Name(), revisionStr))
+ newTask.Set("snap-setup-task", tid)
+ newTask.WaitFor(last)
+ change.AddTask(newTask)
+
+ return nil
+}
+
+func (p4 patch4T) mangle(task *state.Task) error {
+ ss, snapst, err := p4.snapSetupAndState(task)
+ if err != nil {
+ return err
+ }
+
+ var hadCandidate bool
+ if err := p4.getMaybe(task, "had-candidate", &hadCandidate); err != nil && err != state.ErrNoState {
+ return err
+ }
+
+ if hadCandidate {
+ change := task.Change()
+ if change.Kind() != "revert-snap" {
+ return fmt.Errorf("had-candidate true for task %s (%s) of non-revert change %s (%s)",
+ task.ID(), task.Kind(), change.ID(), change.Kind())
+ }
+ }
+
+ task.Clear("had-candidate")
+
+ task.Set("old-candidate-index", snapst.LastIndex(ss.SideInfo.Revision))
+
+ return nil
+}
+
+func (p4 patch4T) addRevertFlag(task *state.Task) error {
+ var ss patch4SnapSetup
+ err := p4.getMaybe(task, "snap-setup", &ss)
+ switch err {
+ case nil:
+ ss.Flags |= patch4Flags(snapstate.SnapSetupFlagRevert)
+
+ // save it back
+ task.Set("snap-setup", &ss)
+ return nil
+ case state.ErrNoState:
+ return nil
+ default:
+ return err
+ }
+}
+
+func patch4(s *state.State) error {
+ p4 := patch4T{}
+ for _, change := range s.Changes() {
+ // change is full done, take it easy
+ if change.Status().Ready() {
+ continue
+ }
+
+ if change.Kind() != "revert-snap" {
+ continue
+ }
+ for _, task := range change.Tasks() {
+ if err := p4.addRevertFlag(task); err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, task := range s.Tasks() {
+ // change is full done, take it easy
+ if task.Change().Status().Ready() {
+ continue
+ }
+
+ switch task.Kind() {
+ case "link-snap":
+ if err := p4.mangle(task); err != nil {
+ return err
+ }
+ case "copy-snap-data":
+ if err := p4.addCleanup(task); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/overlord/patch/patch4_test.go b/overlord/patch/patch4_test.go
new file mode 100644
index 0000000000..9f82ae527c
--- /dev/null
+++ b/overlord/patch/patch4_test.go
@@ -0,0 +1,420 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package patch_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type patch4Suite struct{}
+
+var _ = Suite(&patch4Suite{})
+
+var statePatch4JSON = []byte(`
+{
+ "last-task-id": 999,
+ "last-change-id": 99,
+
+ "data": {
+ "patch-level": 3,
+ "snaps": {
+ "a": {
+ "sequence": [
+ {"name": "", "revision": "1"},
+ {"name": "", "revision": "2"},
+ {"name": "", "revision": "3"}],
+ "current": "2"},
+ "b": {
+ "sequence": [
+ {"name": "", "revision": "1"},
+ {"name": "", "revision": "2"}],
+ "current": "2"}
+ }
+ },
+ "changes": {
+ "1": {
+ "id": "1",
+ "kind": "revert-snap",
+ "summary": "revert a snap",
+ "status": 2,
+ "data": {"snap-names": ["a"]},
+ "task-ids": ["1","2","3","4"]
+ },
+ "2": {
+ "id": "2",
+ "kind": "refresh-snap",
+ "summary": "refresh b snap",
+ "status": 2,
+ "data": {"snap-names": ["b"]},
+ "task-ids": ["10","11","12","13","14","15","16"]
+ }
+ },
+ "tasks": {
+ "1": {
+ "id": "1",
+ "kind": "prepare-snap",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup": {
+ "side-info": {"revision": "2", "name": "a"}
+ }
+ },
+ "halt-tasks": ["2"],
+ "change": "1"
+ },
+ "2": {
+ "id": "2",
+ "kind": "unlink-current-snap",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup-task": "1"
+ },
+ "wait-tasks": ["1"],
+ "halt-tasks": ["3"],
+ "change": "1"
+ },
+ "3": {
+ "id": "3",
+ "kind": "setup-profiles",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup-task": "1"
+ },
+ "wait-tasks": ["2"],
+ "halt-tasks": ["4"],
+ "change": "1"
+ },
+ "4": {
+ "id": "4",
+ "kind": "link-snap",
+ "summary": "make snap avaiblabla",
+ "status": 4,
+ "data": {
+ "had-candidate": true,
+ "snap-setup-task": "1"
+ },
+ "wait-tasks": ["3"],
+ "change": "1"
+ },
+
+ "10": {
+ "id": "10",
+ "kind": "download-snap",
+ "summary": "... download ...",
+ "status": 4,
+ "data": {"snap-setup": {"side-info": {"revision": "2", "name": "a"}}},
+ "halt-tasks": ["11"],
+ "change": "2"
+ }, "11": {
+ "id": "11",
+ "kind": "validate-snap",
+ "summary": "... check asserts...",
+ "status": 4,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["10"],
+ "halt-tasks": ["12"],
+ "change": "2"
+ }, "12": {
+ "id": "12",
+ "kind": "mount-snap",
+ "summary": "... mount...",
+ "status": 4,
+ "data": {"snap-setup-task": "10", "snap-type": "app"},
+ "wait-tasks": ["11"],
+ "halt-tasks": ["13"],
+ "change": "2"
+ }, "13": {
+ "id": "13",
+ "kind": "unlink-current-snap",
+ "summary": "... unlink...",
+ "status": 4,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["12"],
+ "halt-tasks": ["14"],
+ "change": "2"
+ }, "14": {
+ "id": "14",
+ "kind": "copy-snap-data",
+ "summary": "... copy...",
+ "status": 0,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["13"],
+ "halt-tasks": ["15"],
+ "change": "2"
+ }, "15": {
+ "id": "15",
+ "kind": "setup-profiles",
+ "summary": "... set up profile...",
+ "status": 0,
+ "data": {"snap-setup-task": "10"},
+ "wait-tasks": ["14"],
+ "halt-tasks": ["16"],
+ "change": "2"
+ }, "16": {
+ "id": "16",
+ "kind": "link-snap",
+ "summary": "... link...",
+ "status": 0,
+ "data": {"snap-setup-task": "10", "had-candidate": false},
+ "wait-tasks": ["15"],
+ "change": "2"
+ }
+ }
+}
+`)
+
+func (s *patch4Suite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+
+ err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(dirs.SnapStateFile, statePatch4JSON, 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *patch4Suite) TestPatch4OnReverts(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ // simulate that the task was running (but the change
+ // is not fully done yet)
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+ task.SetStatus(state.DoneStatus)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), IsNil)
+ c.Check(had, Equals, true)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, true)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+}
+
+func (s *patch4Suite) TestPatch4OnRevertsNoCandidateYet(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+ // its ready to run but has not run yet
+ task.Clear("had-candidate")
+ task.SetStatus(state.DoStatus)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("4")
+ c.Assert(task, NotNil)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, true)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ c.Check(len(task.Change().Tasks()), Equals, 4)
+}
+
+func (s *patch4Suite) TestPatch4OnRefreshes(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+ // simulate that the task was running (but the change
+ // is not fully done yet)
+ task.SetStatus(state.DoneStatus)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), IsNil)
+ c.Check(had, Equals, false)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 7)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ // we added cleanup
+ c.Check(len(task.Change().Tasks()), Equals, 7+1)
+}
+
+// This test simulates a link-snap task that is scheduled but has not
+// run yet. It has no "had-candidate" data set yet.
+func (s *patch4Suite) TestPatch4OnRefreshesNoHadCandidateYet(c *C) {
+ restorer := patch.MockLevel(4)
+ defer restorer()
+
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ defer r.Close()
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ func() {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+ // its ready to run but has not run yet
+ task.Clear("had-candidate")
+ task.SetStatus(state.DoStatus)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), Equals, state.ErrNoState)
+ c.Check(len(task.Change().Tasks()), Equals, 7)
+ }()
+
+ // go from patch level 3 -> 4
+ err = patch.Apply(st)
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task("16")
+ c.Assert(task, NotNil)
+
+ ss, err := snapstate.TaskSnapSetup(task)
+ c.Assert(err, IsNil)
+ c.Check(ss.Flags.Revert(), Equals, false)
+
+ var had bool
+ var idx int
+ c.Check(task.Get("had-candidate", &had), Equals, state.ErrNoState)
+ c.Check(task.Get("old-candidate-index", &idx), IsNil)
+ c.Check(idx, Equals, 1)
+ // we added cleanup
+ c.Check(len(task.Change().Tasks()), Equals, 7+1)
+}
diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go
index d8ea0a92c6..b0cc9d542b 100644
--- a/overlord/snapstate/backend.go
+++ b/overlord/snapstate/backend.go
@@ -53,6 +53,8 @@ type managerBackend interface {
// the undoers for install
UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, meter progress.Meter) error
UndoCopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error
+ // cleanup
+ ClearTrashedData(oldSnap *snap.Info)
// remove releated
UnlinkSnap(info *snap.Info, meter progress.Meter) error
diff --git a/overlord/snapstate/backend/copydata.go b/overlord/snapstate/backend/copydata.go
index e72ff04eb3..927f76a2c6 100644
--- a/overlord/snapstate/backend/copydata.go
+++ b/overlord/snapstate/backend/copydata.go
@@ -59,7 +59,27 @@ func (b Backend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, meter
if err2 != nil {
logger.Noticef("Cannot remove common data directories for %q: %v", newInfo.Name(), err2)
}
+ } else {
+ err2 = b.untrashData(newInfo)
+ if err2 != nil {
+ logger.Noticef("Cannot restore original data for %q while undoing: %v", newInfo.Name(), err2)
+ }
}
return firstErr(err1, err2)
}
+
+// ClearTrashedData removes the trash. It returns no errors on the assumption that it is called very late in the game.
+func (b Backend) ClearTrashedData(oldSnap *snap.Info) {
+ dirs, err := snapDataDirs(oldSnap)
+ if err != nil {
+ logger.Noticef("Cannot remove previous data for %q: %v", oldSnap.Name(), err)
+ return
+ }
+
+ for _, d := range dirs {
+ if err := clearTrash(d); err != nil {
+ logger.Noticef("Cannot remove %s: %v", d, err)
+ }
+ }
+}
diff --git a/overlord/snapstate/backend/copydata_test.go b/overlord/snapstate/backend/copydata_test.go
index b18cedb76e..50f9bf4cbd 100644
--- a/overlord/snapstate/backend/copydata_test.go
+++ b/overlord/snapstate/backend/copydata_test.go
@@ -169,16 +169,27 @@ func (s *copydataSuite) populateData(c *C, revision snap.Revision) {
subdir := filepath.Join(datadir, "random-subdir")
err := os.MkdirAll(subdir, 0755)
c.Assert(err, IsNil)
- err = ioutil.WriteFile(filepath.Join(subdir, "canary"), nil, 0644)
+ err = ioutil.WriteFile(filepath.Join(subdir, "canary"), []byte(fmt.Sprintln(revision)), 0644)
c.Assert(err, IsNil)
}
+func (s *copydataSuite) populatedData(d string) string {
+ bs, err := ioutil.ReadFile(filepath.Join(dirs.SnapDataDir, "hello", d, "random-subdir", "canary"))
+ if err == nil {
+ return string(bs)
+ }
+ if os.IsNotExist(err) {
+ return ""
+ }
+ panic(err)
+}
+
func (s copydataSuite) populateHomeData(c *C, user string, revision snap.Revision) (homedir string) {
homedir = filepath.Join(s.tempdir, "home", user, "snap")
homeData := filepath.Join(homedir, "hello", revision.String())
err := os.MkdirAll(homeData, 0755)
c.Assert(err, IsNil)
- err = ioutil.WriteFile(filepath.Join(homeData, "canary.home"), nil, 0644)
+ err = ioutil.WriteFile(filepath.Join(homeData, "canary.home"), []byte(fmt.Sprintln(revision)), 0644)
c.Assert(err, IsNil)
return
}
@@ -260,6 +271,58 @@ func (s *copydataSuite) TestCopyDataDoUndoFirstInstall(c *C) {
c.Check(os.IsNotExist(err), Equals, true)
}
+func (s *copydataSuite) TestCopyDataDoABA(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+ c.Check(s.populatedData("10"), Equals, "10\n")
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)})
+ // and write our own data to it
+ s.populateData(c, snap.R(20))
+ c.Check(s.populatedData("20"), Equals, "20\n")
+
+ // and now we pretend to refresh back to v1 (r10)
+ c.Check(s.be.CopySnapData(v1, v2, &s.nullProgress), IsNil)
+
+ // so 10 now has 20's data
+ c.Check(s.populatedData("10"), Equals, "20\n")
+
+ // but we still have the trash
+ c.Check(s.populatedData("10.old"), Equals, "10\n")
+
+ // but cleanup cleans it up, huzzah
+ s.be.ClearTrashedData(v1)
+ c.Check(s.populatedData("10.old"), Equals, "")
+}
+
+func (s *copydataSuite) TestCopyDataDoUndoABA(c *C) {
+ v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)})
+ s.populateData(c, snap.R(10))
+ c.Check(s.populatedData("10"), Equals, "10\n")
+
+ // pretend we install a new version
+ v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)})
+ // and write our own data to it
+ s.populateData(c, snap.R(20))
+ c.Check(s.populatedData("20"), Equals, "20\n")
+
+ // and now we pretend to refresh back to v1 (r10)
+ c.Check(s.be.CopySnapData(v1, v2, &s.nullProgress), IsNil)
+
+ // so v1 (r10) now has v2 (r20)'s data and we have trash
+ c.Check(s.populatedData("10"), Equals, "20\n")
+ c.Check(s.populatedData("10.old"), Equals, "10\n")
+
+ // but oh no! we have to undo it!
+ c.Check(s.be.UndoCopySnapData(v1, v2, &s.nullProgress), IsNil)
+
+ // so now v1 (r10) has v1 (r10)'s data and v2 (r20) has v2 (r20)'s data and we have no trash
+ c.Check(s.populatedData("10"), Equals, "10\n")
+ c.Check(s.populatedData("20"), Equals, "20\n")
+ c.Check(s.populatedData("10.old"), Equals, "")
+}
+
func (s *copydataSuite) TestCopyDataDoIdempotent(c *C) {
// make sure that a retry wouldn't stumble on partial work
diff --git a/overlord/snapstate/backend/mountunit.go b/overlord/snapstate/backend/mountunit.go
index 9f74324b8f..20b87715b6 100644
--- a/overlord/snapstate/backend/mountunit.go
+++ b/overlord/snapstate/backend/mountunit.go
@@ -59,9 +59,10 @@ func removeMountUnit(baseDir string, meter progress.Meter) error {
sysd := systemd.New(dirs.GlobalRootDir, meter)
unit := systemd.MountUnitPath(dirs.StripRootDir(baseDir), "mount")
if osutil.FileExists(unit) {
- // use umount --lazy to ensure that even busy mount points
- // can be unmounted
- if output, err := exec.Command("umount", "--lazy", baseDir).CombinedOutput(); err != nil {
+ // use umount -l (lazy) to ensure that even busy mount points
+ // can be unmounted.
+ // note that the long option --lazy is not supported on trusty.
+ if output, err := exec.Command("umount", "-l", baseDir).CombinedOutput(); err != nil {
return osutil.OutputErr(output, err)
}
diff --git a/overlord/snapstate/backend/snapdata.go b/overlord/snapstate/backend/snapdata.go
index 20f2f35fe8..d50d2a3d7d 100644
--- a/overlord/snapstate/backend/snapdata.go
+++ b/overlord/snapstate/backend/snapdata.go
@@ -23,6 +23,7 @@ import (
"fmt"
"os"
"path/filepath"
+ unix "syscall"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
@@ -48,6 +49,21 @@ func (b Backend) RemoveSnapCommonData(snap *snap.Info) error {
return removeDirs(dirs)
}
+func (b Backend) untrashData(snap *snap.Info) error {
+ dirs, err := snapDataDirs(snap)
+ if err != nil {
+ return err
+ }
+
+ for _, d := range dirs {
+ if e := untrash(d); e != nil {
+ err = e
+ }
+ }
+
+ return err
+}
+
func removeDirs(dirs []string) error {
for _, dir := range dirs {
if err := os.RemoveAll(dir); err != nil {
@@ -107,9 +123,72 @@ func copySnapData(oldSnap, newSnap *snap.Info) (err error) {
return nil
}
+// trashPath returns the trash path for the given path. This will
+// differ only in the last element.
+func trashPath(path string) string {
+ return path + ".old"
+}
+
+// trash moves path aside, if it exists. If the trash for the path
+// already exists and is not empty it will be removed first.
+func trash(path string) error {
+ trash := trashPath(path)
+ err := os.Rename(path, trash)
+ if err == nil {
+ return nil
+ }
+ // os.Rename says it always returns *os.LinkError. Be wary.
+ e, ok := err.(*os.LinkError)
+ if !ok {
+ return err
+ }
+
+ switch e.Err {
+ case unix.ENOENT:
+ // path does not exist (here we use that trashPath(path) and path differ only in the last element)
+ return nil
+ case unix.ENOTEMPTY, unix.EEXIST:
+ // path exists, but trash already exists and is non-empty
+ // (empirically always ENOTEMPTY but rename(2) says it can also be EEXIST)
+ // nuke the old trash and try again
+ if err := os.RemoveAll(trash); err != nil {
+ // well, that didn't work :-(
+ return err
+ }
+ return os.Rename(path, trash)
+ default:
+ // WAT
+ return err
+ }
+}
+
+// untrash moves the trash for path back in, if it exists.
+func untrash(path string) error {
+ err := os.Rename(trashPath(path), path)
+ if !os.IsNotExist(err) {
+ return err
+ }
+
+ return nil
+}
+
+// clearTrash removes the trash made for path, if it exists.
+func clearTrash(path string) error {
+ err := os.RemoveAll(trashPath(path))
+ if !os.IsNotExist(err) {
+ return err
+ }
+
+ return nil
+}
+
// Lowlevel copy the snap data (but never override existing data)
func copySnapDataDirectory(oldPath, newPath string) (err error) {
if _, err := os.Stat(oldPath); err == nil {
+ if err := trash(newPath); err != nil {
+ return err
+ }
+
if _, err := os.Stat(newPath); err != nil {
if err := osutil.CopyFile(oldPath, newPath, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync); err != nil {
return fmt.Errorf("cannot copy %q to %q: %v", oldPath, newPath, err)
diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go
index ff32994af2..888aae0fe3 100644
--- a/overlord/snapstate/backend_test.go
+++ b/overlord/snapstate/backend_test.go
@@ -277,6 +277,14 @@ func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info
return info, nil
}
+func (f *fakeSnappyBackend) ClearTrashedData(si *snap.Info) {
+ f.ops = append(f.ops, fakeOp{
+ op: "cleanup-trash",
+ name: si.Name(),
+ revno: si.Revision,
+ })
+}
+
func (f *fakeSnappyBackend) StoreInfo(st *state.State, name, channel string, userID int, flags snapstate.Flags) (*snap.Info, error) {
return f.ReadInfo(name, &snap.SideInfo{
RealName: name,
diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go
index ff4718059c..fd2a1bfc69 100644
--- a/overlord/snapstate/snapmgr.go
+++ b/overlord/snapstate/snapmgr.go
@@ -48,6 +48,17 @@ type SnapManager struct {
// SnapSetupFlags are flags stored in SnapSetup to control snap manager tasks.
type SnapSetupFlags Flags
+const (
+ // flags that are only used by SnapSetup grow downwards
+
+ // SnapSetupFlagRevert flags the SnapSetup as coming from a revert
+ SnapSetupFlagRevert SnapSetupFlags = 0x40000000 >> iota
+)
+
+func (f SnapSetupFlags) Revert() bool {
+ return f&SnapSetupFlagRevert != 0
+}
+
// SnapSetup holds the necessary snap details to perform most snap manager tasks.
type SnapSetup struct {
// FIXME: rename to RequestedChannel to convey the meaning better
@@ -359,6 +370,7 @@ func Manager(s *state.State) (*SnapManager, error) {
runner.AddHandler("copy-snap-data", m.doCopySnapData, m.undoCopySnapData)
runner.AddHandler("link-snap", m.doLinkSnap, m.undoLinkSnap)
runner.AddHandler("start-snap-services", m.startSnapServices, m.stopSnapServices)
+ runner.AddHandler("cleanup", m.cleanup, nil)
// FIXME: port to native tasks and rename
//runner.AddHandler("garbage-collect", m.doGarbageCollect, nil)
@@ -861,10 +873,14 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error {
cand := ss.SideInfo
m.backend.Candidate(cand)
- hadCandidate := true
- if snapst.LastIndex(cand.Revision) < 0 {
+ oldCandidateIndex := snapst.LastIndex(cand.Revision)
+
+ if oldCandidateIndex < 0 {
snapst.Sequence = append(snapst.Sequence, cand)
- hadCandidate = false
+ } else if !ss.Flags.Revert() {
+ // remove the old candidate from the sequence, add it at the end
+ copy(snapst.Sequence[oldCandidateIndex:len(snapst.Sequence)-1], snapst.Sequence[oldCandidateIndex+1:])
+ snapst.Sequence[len(snapst.Sequence)-1] = cand
}
oldCurrent := snapst.Current
@@ -912,7 +928,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error {
t.Set("old-jailmode", oldJailMode)
t.Set("old-channel", oldChannel)
t.Set("old-current", oldCurrent)
- t.Set("had-candidate", hadCandidate)
+ t.Set("old-candidate-index", oldCandidateIndex)
// Do at the end so we only preserve the new state if it worked.
Set(st, ss.Name(), snapst)
// Make sure if state commits and snapst is mutated we won't be rerun
@@ -972,19 +988,25 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error {
if err != nil {
return err
}
- var hadCandidate bool
- err = t.Get("had-candidate", &hadCandidate)
- if err != nil && err != state.ErrNoState {
+ var oldCandidateIndex int
+ if err := t.Get("old-candidate-index", &oldCandidateIndex); err != nil {
return err
}
+ isRevert := ss.Flags.Revert()
+
// relinking of the old snap is done in the undo of unlink-current-snap
currentIndex := snapst.LastIndex(snapst.Current)
if currentIndex < 0 {
return fmt.Errorf("internal error: cannot find revision %d in %v for undoing the added revision", ss.SideInfo.Revision, snapst.Sequence)
}
- if !hadCandidate {
+
+ if oldCandidateIndex < 0 {
snapst.Sequence = append(snapst.Sequence[:currentIndex], snapst.Sequence[currentIndex+1:]...)
+ } else if !isRevert {
+ oldCand := snapst.Sequence[currentIndex]
+ copy(snapst.Sequence[oldCandidateIndex+1:], snapst.Sequence[oldCandidateIndex:])
+ snapst.Sequence[oldCandidateIndex] = oldCand
}
snapst.Current = oldCurrent
snapst.Active = false
@@ -1059,3 +1081,26 @@ func (m *SnapManager) stopSnapServices(t *state.Task, _ *tomb.Tomb) error {
return err
}
+
+func (m *SnapManager) cleanup(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+
+ st.Lock()
+ defer st.Unlock()
+
+ _, snapst, err := snapSetupAndState(t)
+ if err != nil {
+ t.Errorf("cannot clean up: %v", err)
+ return nil // cleanup should not return error
+ }
+
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ t.Errorf("cannot clean up: %v", err)
+ return nil
+ }
+
+ m.backend.ClearTrashedData(info)
+
+ return nil
+}
diff --git a/overlord/snapstate/snapmgr_test.go b/overlord/snapstate/snapmgr_test.go
index 48b79a424c..45168643b1 100644
--- a/overlord/snapstate/snapmgr_test.go
+++ b/overlord/snapstate/snapmgr_test.go
@@ -208,12 +208,12 @@ func (s *snapmgrTestSuite) TestUpdateCreatesGCTasks(c *C) {
Current: snap.R(4),
})
- ts, err := snapstate.Update(s.state, "some-snap", "", 0, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), 0, 0)
c.Assert(err, IsNil)
i := verifyInstallUpdateTasks(c, true, ts, s.state)
- c.Assert(ts.Tasks(), HasLen, i+4)
- c.Assert(s.state.NumTask(), Equals, i+4)
+ c.Assert(ts.Tasks(), HasLen, i+5)
+ c.Assert(s.state.NumTask(), Equals, i+5)
c.Assert(ts.Tasks()[i].Kind(), Equals, "clear-snap")
i++
c.Assert(ts.Tasks()[i].Kind(), Equals, "discard-snap")
@@ -221,6 +221,8 @@ func (s *snapmgrTestSuite) TestUpdateCreatesGCTasks(c *C) {
c.Assert(ts.Tasks()[i].Kind(), Equals, "clear-snap")
i++
c.Assert(ts.Tasks()[i].Kind(), Equals, "discard-snap")
+ i++
+ c.Assert(ts.Tasks()[i].Kind(), Equals, "cleanup")
}
func (s *snapmgrTestSuite) TestUpdateCreatesDiscardAfterCurrentTasks(c *C) {
@@ -238,12 +240,12 @@ func (s *snapmgrTestSuite) TestUpdateCreatesDiscardAfterCurrentTasks(c *C) {
Current: snap.R(1),
})
- ts, err := snapstate.Update(s.state, "some-snap", "", 0, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), 0, 0)
c.Assert(err, IsNil)
i := verifyInstallUpdateTasks(c, true, ts, s.state)
- c.Assert(ts.Tasks(), HasLen, i+6)
- c.Assert(s.state.NumTask(), Equals, i+6)
+ c.Assert(ts.Tasks(), HasLen, i+7)
+ c.Assert(s.state.NumTask(), Equals, i+7)
c.Assert(ts.Tasks()[i].Kind(), Equals, "clear-snap")
i++
c.Assert(ts.Tasks()[i].Kind(), Equals, "discard-snap")
@@ -255,6 +257,8 @@ func (s *snapmgrTestSuite) TestUpdateCreatesDiscardAfterCurrentTasks(c *C) {
c.Assert(ts.Tasks()[i].Kind(), Equals, "clear-snap")
i++
c.Assert(ts.Tasks()[i].Kind(), Equals, "discard-snap")
+ i++
+ c.Assert(ts.Tasks()[i].Kind(), Equals, "cleanup")
}
func (s *snapmgrTestSuite) TestUpdateMany(c *C) {
@@ -279,8 +283,9 @@ func (s *snapmgrTestSuite) TestUpdateMany(c *C) {
ts := tts[0]
i := verifyInstallUpdateTasks(c, true, ts, s.state)
- c.Assert(ts.Tasks(), HasLen, i+6)
- c.Assert(s.state.NumTask(), Equals, i+6)
+ c.Assert(ts.Tasks(), HasLen, i+7)
+ c.Assert(s.state.NumTask(), Equals, i+7)
+ c.Check(ts.Tasks()[i+6].Kind(), Equals, "cleanup")
}
func (s *snapmgrTestSuite) TestRevertCreatesNoGCTasks(c *C) {
@@ -521,17 +526,19 @@ func (s *snapmgrTestSuite) TestUpdateTasks(c *C) {
Current: snap.R(7),
})
- ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
n := verifyInstallUpdateTasks(c, true, ts, s.state)
- c.Assert(ts.Tasks(), HasLen, n)
- c.Assert(s.state.NumTask(), Equals, n)
+ c.Assert(ts.Tasks(), HasLen, n+1)
+ c.Assert(s.state.NumTask(), Equals, n+1)
var ss snapstate.SnapSetup
err = ts.Tasks()[0].Get("snap-setup", &ss)
c.Assert(err, IsNil)
c.Check(ss.Channel, Equals, "some-channel")
+
+ c.Check(ts.Tasks()[n].Kind(), Equals, "cleanup")
}
func (s *snapmgrTestSuite) TestUpdateChannelFallback(c *C) {
@@ -545,7 +552,7 @@ func (s *snapmgrTestSuite) TestUpdateChannelFallback(c *C) {
Current: snap.R(7),
})
- ts, err := snapstate.Update(s.state, "some-snap", "", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
var ss snapstate.SnapSetup
@@ -565,7 +572,7 @@ func (s *snapmgrTestSuite) TestUpdatePassDevMode(c *C) {
Current: snap.R(7),
})
- _, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, snapstate.DevMode)
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.DevMode)
c.Assert(err, IsNil)
c.Assert(s.fakeBackend.ops, HasLen, 1)
@@ -592,12 +599,12 @@ func (s *snapmgrTestSuite) TestUpdateConflict(c *C) {
Current: snap.R(7),
})
- ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
// need a change to make the tasks visible
s.state.NewChange("refresh", "...").AddAll(ts)
- _, err = snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ _, err = snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
}
@@ -796,7 +803,7 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
})
chg := s.state.NewChange("install", "install a snap")
- ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -874,6 +881,11 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
op: "start-snap-services",
name: "/snap/some-snap/11",
},
+ {
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
}
// ensure all our tasks ran
@@ -895,7 +907,6 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
c.Assert(err, IsNil)
c.Assert(ss, DeepEquals, snapstate.SnapSetup{
Channel: "some-channel",
- Flags: 0,
UserID: s.user.ID,
SnapPath: "downloaded-snap-path",
@@ -950,7 +961,7 @@ func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) {
})
chg := s.state.NewChange("install", "install a snap")
- ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -1095,7 +1106,7 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) {
})
chg := s.state.NewChange("install", "install a snap")
- ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -1180,6 +1191,12 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) {
op: "start-snap-services",
name: "/snap/some-snap/11",
},
+ // only here because of how we triggered the error:
+ {
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
// undoing everything from here down...
{
op: "stop-snap-services",
@@ -1255,7 +1272,7 @@ func (s *snapmgrTestSuite) TestUpdateSameRevision(c *C) {
Current: si.Revision,
})
- _, err := snapstate.Update(s.state, "some-snap", "channel-for-7", s.user.ID, 0)
+ _, err := snapstate.Update(s.state, "some-snap", "channel-for-7", snap.R(0), s.user.ID, 0)
c.Assert(err, ErrorMatches, `snap "some-snap" has no updates available`)
}
@@ -1280,7 +1297,7 @@ func (s *snapmgrTestSuite) TestUpdateBlockedRevision(c *C) {
Current: si7.Revision,
})
- _, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Check(err, ErrorMatches, `snap "some-snap" has no updates available`)
c.Assert(s.fakeBackend.ops, HasLen, 1)
@@ -1313,7 +1330,7 @@ func (s *snapmgrTestSuite) TestUpdateLocalSnapFails(c *C) {
Current: si.Revision,
})
- _, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, ErrorMatches, `cannot refresh local snap "some-snap"`)
}
@@ -1333,7 +1350,7 @@ func (s *snapmgrTestSuite) TestUpdateDisabledUnsupported(c *C) {
Current: si.Revision,
})
- _, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ _, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, ErrorMatches, `refreshing disabled snap "some-snap" not supported`)
}
@@ -1434,38 +1451,45 @@ version: 1.0`)
s.settle()
s.state.Lock()
+ ops := s.fakeBackend.ops
// ensure only local install was run, i.e. first action is pseudo-action current
- c.Assert(s.fakeBackend.ops, HasLen, 9)
- c.Check(s.fakeBackend.ops[0].op, Equals, "current")
- c.Check(s.fakeBackend.ops[0].old, Equals, "/snap/mock/x2")
+ c.Assert(ops, HasLen, 10)
+ c.Check(ops[0].op, Equals, "current")
+ c.Check(ops[0].old, Equals, "/snap/mock/x2")
// and setup-snap
- c.Check(s.fakeBackend.ops[1].op, Equals, "setup-snap")
- c.Check(s.fakeBackend.ops[1].name, Matches, `.*/mock_1.0_all.snap`)
- c.Check(s.fakeBackend.ops[1].revno, Equals, snap.R("x3"))
+ c.Check(ops[1].op, Equals, "setup-snap")
+ c.Check(ops[1].name, Matches, `.*/mock_1.0_all.snap`)
+ c.Check(ops[1].revno, Equals, snap.R("x3"))
+ // and cleanup
+ c.Check(ops[len(ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "mock",
+ revno: snap.R("x3"),
+ })
- c.Check(s.fakeBackend.ops[2].op, Equals, "stop-snap-services")
- c.Check(s.fakeBackend.ops[2].name, Equals, "/snap/mock/x2")
+ c.Check(ops[2].op, Equals, "stop-snap-services")
+ c.Check(ops[2].name, Equals, "/snap/mock/x2")
- c.Check(s.fakeBackend.ops[3].op, Equals, "unlink-snap")
- c.Check(s.fakeBackend.ops[3].name, Equals, "/snap/mock/x2")
+ c.Check(ops[3].op, Equals, "unlink-snap")
+ c.Check(ops[3].name, Equals, "/snap/mock/x2")
- c.Check(s.fakeBackend.ops[4].op, Equals, "copy-data")
- c.Check(s.fakeBackend.ops[4].name, Equals, "/snap/mock/x3")
- c.Check(s.fakeBackend.ops[4].old, Equals, "/snap/mock/x2")
+ c.Check(ops[4].op, Equals, "copy-data")
+ c.Check(ops[4].name, Equals, "/snap/mock/x3")
+ c.Check(ops[4].old, Equals, "/snap/mock/x2")
- c.Check(s.fakeBackend.ops[5].op, Equals, "setup-profiles:Doing")
- c.Check(s.fakeBackend.ops[5].name, Equals, "mock")
- c.Check(s.fakeBackend.ops[5].revno, Equals, snap.R(-3))
+ c.Check(ops[5].op, Equals, "setup-profiles:Doing")
+ c.Check(ops[5].name, Equals, "mock")
+ c.Check(ops[5].revno, Equals, snap.R(-3))
- c.Check(s.fakeBackend.ops[6].op, Equals, "candidate")
- c.Check(s.fakeBackend.ops[6].sinfo, DeepEquals, snap.SideInfo{
+ c.Check(ops[6].op, Equals, "candidate")
+ c.Check(ops[6].sinfo, DeepEquals, snap.SideInfo{
RealName: "mock",
Revision: snap.R(-3),
})
- c.Check(s.fakeBackend.ops[7].op, Equals, "link-snap")
- c.Check(s.fakeBackend.ops[7].name, Equals, "/snap/mock/x3")
- c.Check(s.fakeBackend.ops[8].op, Equals, "start-snap-services")
- c.Check(s.fakeBackend.ops[8].name, Equals, "/snap/mock/x3")
+ c.Check(ops[7].op, Equals, "link-snap")
+ c.Check(ops[7].name, Equals, "/snap/mock/x3")
+ c.Check(ops[8].op, Equals, "start-snap-services")
+ c.Check(ops[8].name, Equals, "/snap/mock/x3")
// verify snapSetup info
var ss snapstate.SnapSetup
@@ -1525,13 +1549,19 @@ version: 1.0`)
// ensure only local install was run, i.e. first action is pseudo-action current
ops := s.fakeBackend.ops
- c.Assert(ops, HasLen, 9)
+ c.Assert(ops, HasLen, 10)
c.Check(ops[0].op, Equals, "current")
c.Check(ops[0].old, Equals, "/snap/mock/100001")
// and setup-snap
c.Check(ops[1].op, Equals, "setup-snap")
c.Check(ops[1].name, Matches, `.*/mock_1.0_all.snap`)
c.Check(ops[1].revno, Equals, snap.R("x1"))
+ // and cleanup
+ c.Check(ops[len(ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "mock",
+ revno: snap.R("x1"),
+ })
var snapst snapstate.SnapState
err = snapstate.Get(s.state, "mock", &snapst)
@@ -2071,7 +2101,7 @@ func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) {
})
chg := s.state.NewChange("update", "update a snap")
- ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -2082,32 +2112,37 @@ func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) {
// ensure garbage collection runs as the last tasks
ops := s.fakeBackend.ops
- c.Assert(ops[len(ops)-6], DeepEquals, fakeOp{
+ c.Assert(ops[len(ops)-7], DeepEquals, fakeOp{
op: "link-snap",
name: "/snap/some-snap/11",
})
- c.Assert(ops[len(ops)-5], DeepEquals, fakeOp{
+ c.Assert(ops[len(ops)-6], DeepEquals, fakeOp{
op: "start-snap-services",
name: "/snap/some-snap/11",
})
- c.Assert(ops[len(ops)-4], DeepEquals, fakeOp{
+ c.Assert(ops[len(ops)-5], DeepEquals, fakeOp{
op: "remove-snap-data",
name: "/snap/some-snap/1",
})
- c.Assert(ops[len(ops)-3], DeepEquals, fakeOp{
+ c.Assert(ops[len(ops)-4], DeepEquals, fakeOp{
op: "remove-snap-files",
name: "/snap/some-snap/1",
stype: "app",
})
- c.Assert(ops[len(ops)-2], DeepEquals, fakeOp{
+ c.Assert(ops[len(ops)-3], DeepEquals, fakeOp{
op: "remove-snap-data",
name: "/snap/some-snap/2",
})
- c.Assert(ops[len(ops)-1], DeepEquals, fakeOp{
+ c.Assert(ops[len(ops)-2], DeepEquals, fakeOp{
op: "remove-snap-files",
name: "/snap/some-snap/2",
stype: "app",
})
+ c.Assert(ops[len(ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(11),
+ })
}
@@ -3236,6 +3271,295 @@ func (s *canRemoveSuite) TestActiveOSAndKernelAreNotOK(c *C) {
c.Check(snapstate.CanRemove(kernel, true), Equals, false)
}
+func revs(seq []*snap.SideInfo) []int {
+ revs := make([]int, len(seq))
+ for i, si := range seq {
+ revs[i] = si.Revision.N
+ }
+
+ return revs
+}
+
+type opSeqOpts struct {
+ revert bool
+ fail bool
+ before []int
+ current int
+ via int
+ after []int
+}
+
+// build a SnapState with a revision sequence given by `before` and a
+// current revision of `current`. Then refresh --revision via. Then
+// check the revision sequence is as in `after`.
+func (s *snapmgrTestSuite) testOpSequence(c *C, opts *opSeqOpts) (*snapstate.SnapState, *state.TaskSet) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ seq := make([]*snap.SideInfo, len(opts.before))
+ for i, n := range opts.before {
+ seq[i] = &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(n)}
+ }
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Channel: "edge",
+ Sequence: seq,
+ Current: snap.R(opts.current),
+ })
+
+ var chg *state.Change
+ var ts *state.TaskSet
+ var err error
+ if opts.revert {
+ chg = s.state.NewChange("revert", "revert a snap")
+ ts, err = snapstate.RevertToRevision(s.state, "some-snap", snap.R(opts.via), snapstate.Flags(0))
+ } else {
+ chg = s.state.NewChange("refresh", "refresh a snap")
+ ts, err = snapstate.Update(s.state, "some-snap", "", snap.R(opts.via), s.user.ID, 0)
+ }
+ c.Assert(err, IsNil)
+ if opts.fail {
+ tasks := ts.Tasks()
+ last := tasks[len(tasks)-1]
+ terr := s.state.NewTask("error-trigger", "provoking total undo")
+ terr.WaitFor(last)
+ chg.AddTask(terr)
+ }
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, "some-snap", &snapst)
+ c.Assert(err, IsNil)
+ c.Check(revs(snapst.Sequence), DeepEquals, opts.after)
+
+ return &snapst, ts
+}
+
+func (s *snapmgrTestSuite) testUpdateSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = false
+ snapst, ts := s.testOpSequence(c, opts)
+ // update always ends with current==seq[-1]==via:
+ c.Check(snapst.Current.N, Equals, opts.after[len(opts.after)-1])
+ c.Check(snapst.Current.N, Equals, opts.via)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 1)
+ c.Check(s.fakeBackend.ops.First("copy-data"), DeepEquals, &fakeOp{
+ op: "copy-data",
+ name: fmt.Sprintf("/snap/some-snap/%d", opts.via),
+ old: fmt.Sprintf("/snap/some-snap/%d", opts.current),
+ })
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testUpdateFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = false
+ opts.after = opts.before
+ s.fakeBackend.linkSnapFailTrigger = fmt.Sprintf("/snap/some-snap/%d", opts.via)
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed update will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ ops := s.fakeBackend.ops
+ c.Check(ops.Count("copy-data"), Equals, 1)
+ do := ops.First("copy-data")
+
+ c.Check(ops.Count("undo-copy-snap-data"), Equals, 1)
+ undo := ops.First("undo-copy-snap-data")
+
+ do.op = undo.op
+ c.Check(do, DeepEquals, undo) // i.e. they only differed in the op
+
+ return ts
+}
+
+// testTotal*Failure fails *after* link-snap
+func (s *snapmgrTestSuite) testTotalUpdateFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = false
+ opts.fail = true
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed update will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ ops := s.fakeBackend.ops
+ c.Check(ops.Count("copy-data"), Equals, 1)
+ do := ops.First("copy-data")
+
+ c.Check(ops.Count("undo-copy-snap-data"), Equals, 1)
+ undo := ops.First("undo-copy-snap-data")
+
+ do.op = undo.op
+ c.Check(do, DeepEquals, undo) // i.e. they only differed in the op
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testRevertSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = true
+ opts.after = opts.before
+ snapst, ts := s.testOpSequence(c, opts)
+ // successful revert leaves current == via
+ c.Check(snapst.Current.N, Equals, opts.via)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 0)
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testRevertFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = true
+ opts.after = opts.before
+ s.fakeBackend.linkSnapFailTrigger = fmt.Sprintf("/snap/some-snap/%d", opts.via)
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed revert will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 0)
+ c.Check(s.fakeBackend.ops.Count("undo-copy-snap-data"), Equals, 0)
+
+ return ts
+}
+
+func (s *snapmgrTestSuite) testTotalRevertFailureSequence(c *C, opts *opSeqOpts) *state.TaskSet {
+ opts.revert = true
+ opts.fail = true
+ opts.after = opts.before
+ snapst, ts := s.testOpSequence(c, opts)
+ // a failed revert will always end with current unchanged
+ c.Check(snapst.Current.N, Equals, opts.current)
+
+ c.Check(s.fakeBackend.ops.Count("copy-data"), Equals, 0)
+ c.Check(s.fakeBackend.ops.Count("undo-copy-snap-data"), Equals, 0)
+
+ return ts
+}
+
+// *** sequence tests ***
+
+// 1. a boring update
+// 1a. ... that works
+func (s *snapmgrTestSuite) TestSeqNormal(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 4, after: []int{2, 3, 4}})
+}
+
+// 1b. that fails during link
+func (s *snapmgrTestSuite) TestSeqNormalFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 4})
+}
+
+// 1c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalNormalFailure(c *C) {
+ // total updates are failures after sequence trimming => we lose a rev
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 4, after: []int{2, 3}})
+}
+
+// 2. a boring revert
+// 2a. that works
+func (s *snapmgrTestSuite) TestSeqRevert(c *C) {
+ s.testRevertSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+}
+
+// 2b. that fails during link
+func (s *snapmgrTestSuite) TestSeqRevertFailure(c *C) {
+ s.testRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+}
+
+// 2c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalRevertFailure(c *C) {
+ s.testTotalRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+}
+
+// 3. a post-revert update
+// 3a. that works
+func (s *snapmgrTestSuite) TestSeqPostRevert(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 4, after: []int{1, 2, 4}})
+}
+
+// 3b. that fails during link
+func (s *snapmgrTestSuite) TestSeqPostRevertFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 4})
+}
+
+// 3c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalPostRevertFailure(c *C) {
+ // lose a rev here as well
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 4, after: []int{1, 2}})
+}
+
+// 4. a post-revert revert
+// 4a. that works
+func (s *snapmgrTestSuite) TestSeqRevertPostRevert(c *C) {
+ s.testRevertSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 1})
+}
+
+// 4b. that fails during link
+func (s *snapmgrTestSuite) TestSeqRevertPostRevertFailure(c *C) {
+ s.testRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 1})
+}
+
+// 4c. taht fails after link
+func (s *snapmgrTestSuite) TestSeqTotalRevertPostRevertFailure(c *C) {
+ s.testTotalRevertFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 2, via: 1})
+}
+
+// 5. an update that missed a rev
+// 5a. that works
+func (s *snapmgrTestSuite) TestSeqMissedOne(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2}, current: 2, via: 4, after: []int{1, 2, 4}})
+}
+
+// 5b. that fails during link
+func (s *snapmgrTestSuite) TestSeqMissedOneFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2}, current: 2, via: 4})
+}
+
+// 5c. that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalMissedOneFailure(c *C) {
+ // we don't lose a rev here because len(Seq) < 3 going in
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2}, current: 2, via: 4, after: []int{1, 2}})
+}
+
+// 6. an update that updates to a revision we already have ("ABA update")
+// 6a. that works
+func (s *snapmgrTestSuite) TestSeqABA(c *C) {
+ s.testUpdateSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2, after: []int{1, 3, 2}})
+ c.Check(s.fakeBackend.ops[len(s.fakeBackend.ops)-1], DeepEquals, fakeOp{
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(2),
+ })
+}
+
+// 6b. that fails during link
+func (s *snapmgrTestSuite) TestSeqABAFailure(c *C) {
+ s.testUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2})
+ c.Check(s.fakeBackend.ops.First("cleanup-trash"), IsNil)
+}
+
+// 6c that fails after link
+func (s *snapmgrTestSuite) TestSeqTotalABAFailure(c *C) {
+ // we don't lose a rev here because ABA
+ s.testTotalUpdateFailureSequence(c, &opSeqOpts{before: []int{1, 2, 3}, current: 3, via: 2, after: []int{1, 2, 3}})
+ // XXX: TODO: NOTE!! WARNING!! etc
+ //
+ // if this happens in real life, things will be weird. revno 2 will
+ // have data that has been copied from 3, instead of old 2's data,
+ // because the failure occurred *after* nuking the trash. This can
+ // happen when things are chained. Because of this, if it were to
+ // *actually* happen the correct end sequence would be [1, 3] and not
+ // [1, 2, 3]. IRL this scenario can happen if an update that works is
+ // chained to an update that fails. Detecting this case is rather hard,
+ // and the end result is not nice, and we want to move cleanup to a
+ // separate handler & status that will cope with this better (so trash
+ // gets nuked after all tasks succeeded).
+}
+
func (s *snapmgrTestSuite) TestUpdateTasksWithOldCurrent(c *C) {
s.state.Lock()
defer s.state.Unlock()
@@ -3252,30 +3576,40 @@ func (s *snapmgrTestSuite) TestUpdateTasksWithOldCurrent(c *C) {
})
// run the update
- ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0)
+ ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, 0)
c.Assert(err, IsNil)
+ n := verifyInstallUpdateTasks(c, true, ts, s.state)
+
// and ensure that it will remove the revisions after "current"
// (si3, si4)
var ss snapstate.SnapSetup
tasks := ts.Tasks()
- c.Check(tasks[len(tasks)-1].Kind(), Equals, "discard-snap")
- c.Check(tasks[len(tasks)-2].Kind(), Equals, "clear-snap")
- err = tasks[len(tasks)-2].Get("snap-setup", &ss)
+ c.Check(len(tasks), Equals, n+5)
+
+ c.Check(tasks[n+3].Kind(), Equals, "discard-snap")
+ c.Check(tasks[n+2].Kind(), Equals, "clear-snap")
+ err = tasks[n+2].Get("snap-setup", &ss)
c.Assert(err, IsNil)
c.Check(ss.Revision(), Equals, si4.Revision)
- c.Check(tasks[len(tasks)-3].Kind(), Equals, "discard-snap")
- c.Check(tasks[len(tasks)-4].Kind(), Equals, "clear-snap")
- err = tasks[len(tasks)-4].Get("snap-setup", &ss)
+ c.Check(tasks[n+1].Kind(), Equals, "discard-snap")
+ c.Check(tasks[n].Kind(), Equals, "clear-snap")
+ err = tasks[n].Get("snap-setup", &ss)
c.Assert(err, IsNil)
c.Check(ss.Revision(), Equals, si3.Revision)
// ensure only si3, si4 are removed
- c.Check(tasks[len(tasks)-5].Kind(), Not(Equals), "discard-snap")
+ numDiscards := 0
+ for _, task := range tasks {
+ if task.Kind() == "discard-snap" {
+ numDiscards++
+ }
+ }
+ c.Check(numDiscards, Equals, 2)
}
-func (s *snapmgrTestSuite) TestUpdateCanDoBackwardsNotQuiteYet(c *C) {
+func (s *snapmgrTestSuite) TestUpdateCanDoBackwards(c *C) {
si7 := snap.SideInfo{
RealName: "some-snap",
SnapID: "some-snap-id",
@@ -3296,8 +3630,59 @@ func (s *snapmgrTestSuite) TestUpdateCanDoBackwardsNotQuiteYet(c *C) {
Current: si11.Revision,
})
- _, err := snapstate.Update(s.state, "some-snap", "channel-for-7", s.user.ID, 0)
- c.Assert(err, ErrorMatches, `revision 7 of snap "some-snap" already installed`)
+ chg := s.state.NewChange("refresh", "refresh a snap")
+ ts, err := snapstate.Update(s.state, "some-snap", "", snap.R(7), s.user.ID, 0)
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+ expected := fakeOps{
+ {
+ op: "stop-snap-services",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "unlink-snap",
+ name: "/snap/some-snap/11",
+ },
+ {
+ op: "copy-data",
+ name: "/snap/some-snap/7",
+ old: "/snap/some-snap/11",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "",
+ Revision: snap.R(7),
+ },
+ },
+ {
+ op: "link-snap",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "start-snap-services",
+ name: "/snap/some-snap/7",
+ },
+ {
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ }
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
}
func (s *snapmgrTestSuite) TestSnapStateNoLocalRevision(c *C) {
diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go
index 8131fc5f60..6882645544 100644
--- a/overlord/snapstate/snapstate.go
+++ b/overlord/snapstate/snapstate.go
@@ -46,10 +46,6 @@ const (
// JailMode is set when the user has requested confinement
// always be enforcing, even if the snap requests otherwise.
JailMode
-
- // if we need flags for just SnapSetup it may be easier
- // to start a new sequence from the other end with:
- // 0x40000000 >> iota
)
func (f Flags) DevModeAllowed() bool {
@@ -126,7 +122,7 @@ func doInstall(s *state.State, snapst *SnapState, ss *SnapSetup) (*state.TaskSet
}
// copy-data (needs stopped services by unlink)
- if !revisionIsLocal {
+ if !ss.Flags.Revert() {
copyData := s.NewTask("copy-snap-data", fmt.Sprintf(i18n.G("Copy snap %q data"), ss.Name()))
addTask(copyData)
prev = copyData
@@ -148,7 +144,7 @@ func doInstall(s *state.State, snapst *SnapState, ss *SnapSetup) (*state.TaskSet
prev = startSnapServices
// Do not do that if we are reverting to a local revision
- if snapst.HasCurrent() && !revisionIsLocal {
+ if snapst.HasCurrent() && !ss.Flags.Revert() {
seq := snapst.Sequence
currentIndex := snapst.LastIndex(snapst.Current)
@@ -162,6 +158,19 @@ func doInstall(s *state.State, snapst *SnapState, ss *SnapSetup) (*state.TaskSet
prev = tasks[len(tasks)-1]
}
+ // make sure we're not scheduling the removal of the target
+ // revision in the case where the target revision is already in
+ // the sequence.
+ for i := 0; i < currentIndex; i++ {
+ si := seq[i]
+ if si.Revision == ss.Revision() {
+ // we do *not* want to removeInactiveRevision of this one
+ copy(seq[i:], seq[i+1:])
+ seq = seq[:len(seq)-1]
+ currentIndex--
+ }
+ }
+
// normal garbage collect
for i := 0; i <= currentIndex-2; i++ {
si := seq[i]
@@ -170,6 +179,8 @@ func doInstall(s *state.State, snapst *SnapState, ss *SnapSetup) (*state.TaskSet
tasks = append(tasks, ts.Tasks()...)
prev = tasks[len(tasks)-1]
}
+
+ addTask(s.NewTask("cleanup", fmt.Sprintf("Clean up %q%s install", ss.Name(), revisionStr)))
}
return state.NewTaskSet(tasks...), nil
@@ -411,7 +422,7 @@ func UpdateMany(st *state.State, names []string, userID int) ([]string, []*state
// Update initiates a change updating a snap.
// Note that the state must be locked by the caller.
-func Update(s *state.State, name, channel string, userID int, flags Flags) (*state.TaskSet, error) {
+func Update(s *state.State, name, channel string, revision snap.Revision, userID int, flags Flags) (*state.TaskSet, error) {
var snapst SnapState
err := Get(s, name, &snapst)
if err != nil && err != state.ErrNoState {
@@ -431,25 +442,43 @@ func Update(s *state.State, name, channel string, userID int, flags Flags) (*sta
channel = snapst.Channel
}
- updateInfo, err := updateInfo(s, &snapst, channel, userID, flags)
+ info, err := infoForUpdate(s, &snapst, name, channel, revision, userID, flags)
if err != nil {
return nil, err
}
- if err := checkRevisionIsNew(name, &snapst, updateInfo.Revision); err != nil {
- return nil, err
- }
ss := &SnapSetup{
Channel: channel,
UserID: userID,
Flags: SnapSetupFlags(flags),
- DownloadInfo: &updateInfo.DownloadInfo,
- SideInfo: &updateInfo.SideInfo,
+ DownloadInfo: &info.DownloadInfo,
+ SideInfo: &info.SideInfo,
}
return doInstall(s, &snapst, ss)
}
+func infoForUpdate(s *state.State, snapst *SnapState, name, channel string, revision snap.Revision, userID int, flags Flags) (*snap.Info, error) {
+ if revision.Unset() {
+ // good ol' refresh
+ return updateInfo(s, snapst, channel, userID, flags)
+ }
+ var sideInfo *snap.SideInfo
+ for _, si := range snapst.Sequence {
+ if si.Revision == revision {
+ sideInfo = si
+ break
+ }
+ }
+ if sideInfo == nil {
+ // refresh from given revision from store
+ return snapInfo(s, name, channel, revision, userID, flags)
+ }
+
+ // refresh-to-local
+ return readInfo(name, sideInfo)
+}
+
// Enable sets a snap to the active state
func Enable(s *state.State, name string) (*state.TaskSet, error) {
var snapst SnapState
@@ -706,7 +735,7 @@ func RevertToRevision(s *state.State, name string, rev snap.Revision, flags Flag
}
ss := &SnapSetup{
SideInfo: snapst.Sequence[i],
- Flags: SnapSetupFlags(flags),
+ Flags: SnapSetupFlags(flags) | SnapSetupFlagRevert,
}
return doInstall(s, &snapst, ss)
}
diff --git a/overlord/state/task.go b/overlord/state/task.go
index 22f3828bea..4110c9a41b 100644
--- a/overlord/state/task.go
+++ b/overlord/state/task.go
@@ -315,6 +315,12 @@ func (t *Task) Get(key string, value interface{}) error {
return t.data.get(key, value)
}
+// Clear disassociates the value from key.
+func (t *Task) Clear(key string) {
+ t.state.writing()
+ delete(t.data, key)
+}
+
func addOnce(set []string, s string) []string {
for _, cur := range set {
if s == cur {
diff --git a/overlord/state/task_test.go b/overlord/state/task_test.go
index d1bb2e74cd..20e774d386 100644
--- a/overlord/state/task_test.go
+++ b/overlord/state/task_test.go
@@ -82,6 +82,25 @@ func (ts *taskSuite) TestGetSet(c *C) {
c.Check(v, Equals, 1)
}
+func (ts *taskSuite) TestClear(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ t := st.NewTask("download", "1...")
+
+ t.Set("a", 1)
+
+ var v int
+ err := t.Get("a", &v)
+ c.Assert(err, IsNil)
+ c.Check(v, Equals, 1)
+
+ t.Clear("a")
+
+ c.Check(t.Get("a", &v), Equals, state.ErrNoState)
+}
+
func (ts *taskSuite) TestStatusAndSetStatus(c *C) {
st := state.New(nil)
st.Lock()
diff --git a/run-checks b/run-checks
index e89dd5b7b7..0e75ee457d 100755
--- a/run-checks
+++ b/run-checks
@@ -30,7 +30,7 @@ case "${1:-all}" in
SPREAD=1
;;
*)
- echo "Wrong flag ${1}. To run a single suite use --static, --unit, --spread, or --integration."
+ echo "Wrong flag ${1}. To run a single suite use --static, --unit, --spread."
exit 1
esac
@@ -116,12 +116,12 @@ if [ "$UNIT" = 1 ]; then
echo "mode: set" > .coverage/coverage.out
echo Building
- go build -tags=excludeintegration -v github.com/snapcore/snapd/...
+ go build -v github.com/snapcore/snapd/...
# tests
echo Running tests from $(pwd)
- for pkg in $(go list ./... | grep -v integration-tests); do
- $goctest -tags=excludeintegration -v -coverprofile=.coverage/profile.out $pkg
+ for pkg in $(go list ./...); do
+ $goctest -v -coverprofile=.coverage/profile.out $pkg
append_coverage .coverage/profile.out
done
diff --git a/snap/info.go b/snap/info.go
index be2316d75d..47b2c84769 100644
--- a/snap/info.go
+++ b/snap/info.go
@@ -110,8 +110,6 @@ type SideInfo struct {
Developer string `yaml:"developer,omitempty" json:"developer,omitempty"` // XXX: obsolete, will be retired after full backfilling of DeveloperID
EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
- Size int64 `yaml:"size,omitempty" json:"size,omitempty"`
- Sha512 string `yaml:"sha512,omitempty" json:"sha512,omitempty"`
Private bool `yaml:"private,omitempty" json:"private,omitempty"`
}
@@ -228,6 +226,9 @@ func (s *Info) NeedsDevMode() bool {
type DownloadInfo struct {
AnonDownloadURL string `json:"anon-download-url,omitempty"`
DownloadURL string `json:"download-url,omitempty"`
+
+ Size int64 `json:"size,omitempty"`
+ Sha3_384 string `json:"sha3-384,omitempty"`
}
// sanity check that Info is a PlaceInfo
diff --git a/snap/snapenv/snapenv.go b/snap/snapenv/snapenv.go
index befdada1e6..09283f8350 100644
--- a/snap/snapenv/snapenv.go
+++ b/snap/snapenv/snapenv.go
@@ -22,37 +22,102 @@ package snapenv
import (
"fmt"
"os"
+ "os/user"
+ "strings"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/snap"
)
-// Basic returns the app-level environment variables for a snap.
+// ExecEnv returns the full environment that is required for
+// snap-{confine,exec}(like SNAP_{NAME,REVISION} etc are all set).
+//
+// It merges it with the existing os.Environ() and ensures the SNAP_*
+// overrides the any pre-existing environment variables.
+func ExecEnv(info *snap.Info) []string {
+ // merge environment and the snap environment, note that the
+ // snap environment overrides pre-existing env entries
+ env := envMap(os.Environ())
+ snapEnv := snapEnv(info)
+ for k, v := range snapEnv {
+ env[k] = v
+ }
+ return envFromMap(env)
+}
+
+// snapEnv returns the extra environment that is required for
+// snap-{confine,exec} to work.
+func snapEnv(info *snap.Info) map[string]string {
+ home := os.Getenv("HOME")
+ // HOME is not set for systemd services, so pull it out of passwd
+ if home == "" {
+ user, err := user.Current()
+ if err == nil {
+ home = user.HomeDir
+ }
+ }
+
+ env := basicEnv(info)
+ if home != "" {
+ for k, v := range userEnv(info, home) {
+ env[k] = v
+ }
+ }
+ return env
+}
+
+// basicEnv returns the app-level environment variables for a snap.
// Despite this being a bit snap-specific, this is in helpers.go because it's
// used by so many other modules, we run into circular dependencies if it's
// somewhere more reasonable like the snappy module.
-func Basic(info *snap.Info) []string {
- return []string{
- fmt.Sprintf("SNAP=%s", info.MountDir()),
- fmt.Sprintf("SNAP_COMMON=%s", info.CommonDataDir()),
- fmt.Sprintf("SNAP_DATA=%s", info.DataDir()),
- fmt.Sprintf("SNAP_NAME=%s", info.Name()),
- fmt.Sprintf("SNAP_VERSION=%s", info.Version),
- fmt.Sprintf("SNAP_REVISION=%s", info.Revision),
- fmt.Sprintf("SNAP_ARCH=%s", arch.UbuntuArchitecture()),
- "SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:",
- fmt.Sprintf("SNAP_REEXEC=%s", os.Getenv("SNAP_REEXEC")),
+func basicEnv(info *snap.Info) map[string]string {
+ return map[string]string{
+ "SNAP": info.MountDir(),
+ "SNAP_COMMON": info.CommonDataDir(),
+ "SNAP_DATA": info.DataDir(),
+ "SNAP_NAME": info.Name(),
+ "SNAP_VERSION": info.Version,
+ "SNAP_REVISION": info.Revision.String(),
+ "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:",
+ "SNAP_REEXEC": os.Getenv("SNAP_REEXEC"),
}
}
-// User returns the user-level environment variables for a snap.
+// userEnv returns the user-level environment variables for a snap.
// Despite this being a bit snap-specific, this is in helpers.go because it's
// used by so many other modules, we run into circular dependencies if it's
// somewhere more reasonable like the snappy module.
-func User(info *snap.Info, home string) []string {
- return []string{
- fmt.Sprintf("HOME=%s", info.UserDataDir(home)),
- fmt.Sprintf("SNAP_USER_COMMON=%s", info.UserCommonDataDir(home)),
- fmt.Sprintf("SNAP_USER_DATA=%s", info.UserDataDir(home)),
+func userEnv(info *snap.Info, home string) map[string]string {
+ return map[string]string{
+ "HOME": info.UserDataDir(home),
+ "SNAP_USER_COMMON": info.UserCommonDataDir(home),
+ "SNAP_USER_DATA": info.UserDataDir(home),
+ }
+}
+
+// envMap creates a map from the given environment string list, e.g. the
+// list returned from os.Environ()
+func envMap(env []string) map[string]string {
+ envMap := map[string]string{}
+ for _, kv := range env {
+ l := strings.SplitN(kv, "=", 2)
+ if len(l) < 2 {
+ continue // strange
+ }
+ k, v := l[0], l[1]
+ envMap[k] = v
+ }
+ return envMap
+}
+
+// envFromMap creates a list of strings of the form k=v from a dict. This is
+// useful in combination with envMap to create an environment suitable to
+// pass to e.g. syscall.Exec()
+func envFromMap(em map[string]string) []string {
+ var out []string
+ for k, v := range em {
+ out = append(out, fmt.Sprintf("%s=%s", k, v))
}
+ return out
}
diff --git a/snap/snapenv/snapenv_test.go b/snap/snapenv/snapenv_test.go
index 87263d0823..9aadc097dd 100644
--- a/snap/snapenv/snapenv_test.go
+++ b/snap/snapenv/snapenv_test.go
@@ -21,7 +21,8 @@ package snapenv
import (
"fmt"
- "sort"
+ "os"
+ "os/user"
"testing"
. "gopkg.in/check.v1"
@@ -37,6 +38,15 @@ type HTestSuite struct{}
var _ = Suite(&HTestSuite{})
+var mockYaml = []byte(`name: snapname
+version: 1.0
+apps:
+ app:
+ command: run-app
+hooks:
+ apply-config:
+`)
+
var mockSnapInfo = &snap.Info{
SuggestedName: "foo",
Version: "1.0",
@@ -46,30 +56,62 @@ var mockSnapInfo = &snap.Info{
}
func (ts *HTestSuite) TestBasic(c *C) {
- env := Basic(mockSnapInfo)
- sort.Strings(env)
-
- c.Assert(env, DeepEquals, []string{
- fmt.Sprintf("SNAP=%s/foo/17", dirs.SnapMountDir),
- fmt.Sprintf("SNAP_ARCH=%s", arch.UbuntuArchitecture()),
- "SNAP_COMMON=/var/snap/foo/common",
- "SNAP_DATA=/var/snap/foo/17",
- "SNAP_LIBRARY_PATH=/var/lib/snapd/lib/gl:",
- "SNAP_NAME=foo",
- "SNAP_REEXEC=",
- "SNAP_REVISION=17",
- "SNAP_VERSION=1.0",
+ env := basicEnv(mockSnapInfo)
+
+ c.Assert(env, DeepEquals, map[string]string{
+ "SNAP": fmt.Sprintf("%s/foo/17", dirs.SnapMountDir),
+ "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_COMMON": "/var/snap/foo/common",
+ "SNAP_DATA": "/var/snap/foo/17",
+ "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:",
+ "SNAP_NAME": "foo",
+ "SNAP_REEXEC": "",
+ "SNAP_REVISION": "17",
+ "SNAP_VERSION": "1.0",
})
}
func (ts *HTestSuite) TestUser(c *C) {
- env := User(mockSnapInfo, "/root")
- sort.Strings(env)
+ env := userEnv(mockSnapInfo, "/root")
- c.Assert(env, DeepEquals, []string{
- "HOME=/root/snap/foo/17",
- "SNAP_USER_COMMON=/root/snap/foo/common",
- "SNAP_USER_DATA=/root/snap/foo/17",
+ c.Assert(env, DeepEquals, map[string]string{
+ "HOME": "/root/snap/foo/17",
+ "SNAP_USER_COMMON": "/root/snap/foo/common",
+ "SNAP_USER_DATA": "/root/snap/foo/17",
})
}
+
+func (s *HTestSuite) TestSnapRunSnapExecEnv(c *C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, IsNil)
+ info.SideInfo.Revision = snap.R(42)
+
+ usr, err := user.Current()
+ c.Assert(err, IsNil)
+
+ homeEnv := os.Getenv("HOME")
+ defer os.Setenv("HOME", homeEnv)
+
+ for _, withHomeEnv := range []bool{true, false} {
+ if !withHomeEnv {
+ os.Setenv("HOME", "")
+ }
+
+ env := snapEnv(info)
+ c.Check(env, DeepEquals, map[string]string{
+ "HOME": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir),
+ "SNAP": fmt.Sprintf("%s/snapname/42", dirs.SnapMountDir),
+ "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_COMMON": "/var/snap/snapname/common",
+ "SNAP_DATA": "/var/snap/snapname/42",
+ "SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:",
+ "SNAP_NAME": "snapname",
+ "SNAP_REEXEC": "",
+ "SNAP_REVISION": "42",
+ "SNAP_USER_COMMON": fmt.Sprintf("%s/snap/snapname/common", usr.HomeDir),
+ "SNAP_USER_DATA": fmt.Sprintf("%s/snap/snapname/42", usr.HomeDir),
+ "SNAP_VERSION": "1.0",
+ })
+ }
+}
diff --git a/spread.yaml b/spread.yaml
index ecbfb695b0..7364b2c560 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -9,7 +9,11 @@ environment:
SNAP_REEXEC: 0
SPREAD_STORE_USER: "$(HOST: echo $SPREAD_STORE_USER)"
SPREAD_STORE_PASSWORD: "$(HOST: echo $SPREAD_STORE_PASSWORD)"
- LANG: "$(echo $LANG)"
+ LANG: "$(echo ${LANG:-C.UTF-8})"
+ LANGUAGE: "$(echo ${LANGUAGE:-en})"
+ # important to ensure adhoc and linode/qemu behave the same
+ SUDO_USER: ""
+ SUDO_UID: ""
backends:
linode:
@@ -32,14 +36,18 @@ backends:
image: ubuntu-16.04-64
username: ubuntu
password: ubuntu
- adhoc:
+ - ubuntu-16.10-64:
+ username: ubuntu
+ password: ubuntu
+ autopkgtest:
+ type: adhoc
allocate: |
echo "Allocating ad-hoc $SPREAD_SYSTEM"
- if [ -z "$ADT_ARTIFACTS" ]; then
- echo "out adhoc only works inside autopkgtest"
+ if [ -z "${ADT_ARTIFACTS}" ]; then
+ FATAL "adhoc only works inside autopkgtest"
exit 1
fi
- echo "localhost:22"
+ ADDRESS localhost:22
discard: |
echo "Discarding ad-hoc $SPREAD_SYSTEM"
systems:
diff --git a/store/auth.go b/store/auth.go
index 3617704174..5f2c347fc5 100644
--- a/store/auth.go
+++ b/store/auth.go
@@ -24,6 +24,8 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ "io"
+ "io/ioutil"
"net/http"
"gopkg.in/macaroon.v1"
@@ -298,7 +300,8 @@ func RequestDeviceSession(serialAssertion, sessionRequest, previousSession strin
// check return code, error on anything !200
if resp.StatusCode != 200 {
- return "", fmt.Errorf(errorPrefix+"store server returned status %d", resp.StatusCode)
+ body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1e6)) // do our best to read the body
+ return "", fmt.Errorf(errorPrefix+"store server returned status %d and body %q", resp.StatusCode, body)
}
dec := json.NewDecoder(resp.Body)
diff --git a/store/auth_test.go b/store/auth_test.go
index e0c0705964..a4342a725b 100644
--- a/store/auth_test.go
+++ b/store/auth_test.go
@@ -378,11 +378,12 @@ func (s *authTestSuite) TestRequestDeviceSessionMissingData(c *C) {
func (s *authTestSuite) TestRequestDeviceSessionError(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
+ w.Write([]byte("error body"))
}))
defer mockServer.Close()
MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
macaroon, err := RequestDeviceSession("serial-assertion", "session-request", "")
- c.Assert(err, ErrorMatches, "cannot get device session from store: store server returned status 500")
+ c.Assert(err, ErrorMatches, `cannot get device session from store: store server returned status 500 and body "error body"`)
c.Assert(macaroon, Equals, "")
}
diff --git a/store/details.go b/store/details.go
index 2edc9c58b8..d3ee425134 100644
--- a/store/details.go
+++ b/store/details.go
@@ -25,26 +25,26 @@ import (
// snapDetails encapsulates the data sent to us from the store as JSON.
type snapDetails struct {
- AnonDownloadURL string `json:"anon_download_url,omitempty"`
- Architectures []string `json:"architecture"`
- Channel string `json:"channel,omitempty"`
- DownloadSha512 string `json:"download_sha512,omitempty"`
- Summary string `json:"summary,omitempty"`
- Description string `json:"description,omitempty"`
- DownloadSize int64 `json:"binary_filesize,omitempty"`
- DownloadURL string `json:"download_url,omitempty"`
- IconURL string `json:"icon_url"`
- LastUpdated string `json:"last_updated,omitempty"`
- Name string `json:"package_name"`
- Prices map[string]float64 `json:"prices,omitempty"`
- Publisher string `json:"publisher,omitempty"`
- RatingsAverage float64 `json:"ratings_average,omitempty"`
- Revision int `json:"revision"` // store revisions are ints starting at 1
- SnapID string `json:"snap_id"`
- SupportURL string `json:"support_url"`
- Title string `json:"title"`
- Type snap.Type `json:"content,omitempty"`
- Version string `json:"version"`
+ AnonDownloadURL string `json:"anon_download_url,omitempty"`
+ Architectures []string `json:"architecture"`
+ Channel string `json:"channel,omitempty"`
+ DownloadSha3_384 string `json:"download_sha3_384,omitempty"`
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ DownloadSize int64 `json:"binary_filesize,omitempty"`
+ DownloadURL string `json:"download_url,omitempty"`
+ IconURL string `json:"icon_url"`
+ LastUpdated string `json:"last_updated,omitempty"`
+ Name string `json:"package_name"`
+ Prices map[string]float64 `json:"prices,omitempty"`
+ Publisher string `json:"publisher,omitempty"`
+ RatingsAverage float64 `json:"ratings_average,omitempty"`
+ Revision int `json:"revision"` // store revisions are ints starting at 1
+ SnapID string `json:"snap_id"`
+ SupportURL string `json:"support_url"`
+ Title string `json:"title"`
+ Type snap.Type `json:"content,omitempty"`
+ Version string `json:"version"`
// FIXME: the store should return "developer" to us instead of
// origin
diff --git a/store/store.go b/store/store.go
index ce36a5a70d..9a715a649a 100644
--- a/store/store.go
+++ b/store/store.go
@@ -87,7 +87,7 @@ func infoFromRemote(d snapDetails) *snap.Info {
info.DeveloperID = d.DeveloperID
info.Developer = d.Developer // XXX: obsolete, will be retired after full backfilling of DeveloperID
info.Channel = d.Channel
- info.Sha512 = d.DownloadSha512
+ info.Sha3_384 = d.DownloadSha3_384
info.Size = d.DownloadSize
info.IconURL = d.IconURL
info.AnonDownloadURL = d.AnonDownloadURL
@@ -367,52 +367,53 @@ func authenticateUser(r *http.Request, user *auth.UserState) {
r.Header.Set("Authorization", buf.String())
}
-// refreshMacaroon will request a refreshed discharge macaroon for the user
-func refreshMacaroon(user *auth.UserState) error {
+// refreshDischarges will request refreshed discharge macaroons for the user
+func refreshDischarges(user *auth.UserState) ([]string, error) {
+ newDischarges := make([]string, len(user.StoreDischarges))
for i, d := range user.StoreDischarges {
discharge, err := MacaroonDeserialize(d)
if err != nil {
- return err
+ return nil, err
}
- if discharge.Location() == UbuntuoneLocation {
- refreshedDischarge, err := RefreshDischargeMacaroon(d)
- if err != nil {
- return err
- }
- user.StoreDischarges[i] = refreshedDischarge
+ if discharge.Location() != UbuntuoneLocation {
+ newDischarges[i] = d
+ continue
}
+
+ refreshedDischarge, err := RefreshDischargeMacaroon(d)
+ if err != nil {
+ return nil, err
+ }
+ newDischarges[i] = refreshedDischarge
}
- return nil
+ return newDischarges, nil
}
// refreshUser will refresh user discharge macaroon and update state
func (s *Store) refreshUser(user *auth.UserState) error {
- err := refreshMacaroon(user)
+ newDischarges, err := refreshDischarges(user)
if err != nil {
return err
}
if s.authContext != nil {
- err = s.authContext.UpdateUser(user)
+ curUser, err := s.authContext.UpdateUserAuth(user, newDischarges)
if err != nil {
return err
}
+ // update in place
+ *user = *curUser
}
return nil
}
// refreshDeviceSession will set or refresh the device session in the state
-func (s *Store) refreshDeviceSession() error {
+func (s *Store) refreshDeviceSession(device *auth.DeviceState) error {
if s.authContext == nil {
return fmt.Errorf("internal error: no authContext")
}
- device, err := s.authContext.Device()
- if err != nil {
- return err
- }
-
nonce, err := RequestStoreDeviceNonce()
if err != nil {
return err
@@ -428,11 +429,12 @@ func (s *Store) refreshDeviceSession() error {
return err
}
- device.SessionMacaroon = session
- err = s.authContext.UpdateDevice(device)
+ curDevice, err := s.authContext.UpdateDeviceAuth(device, session)
if err != nil {
return err
}
+ // update in place
+ *device = *curDevice
return nil
}
@@ -492,7 +494,15 @@ func (s *Store) doRequest(client *http.Client, reqOptions *requestOptions, user
}
if strings.Contains(wwwAuth, "refresh_device_session=1") {
// refresh device session
- err = s.refreshDeviceSession()
+ if s.authContext == nil {
+ return nil, fmt.Errorf("internal error: no authContext")
+ }
+ device, err := s.authContext.Device()
+ if err != nil {
+ return nil, err
+ }
+
+ err = s.refreshDeviceSession(device)
if err != nil {
return nil, err
}
@@ -526,8 +536,10 @@ func (s *Store) newRequest(reqOptions *requestOptions, user *auth.UserState) (*h
if err != nil {
return nil, err
}
- if device.SessionMacaroon == "" {
- err = s.refreshDeviceSession()
+ // we don't have a session yet but have a serial, try
+ // to get a session
+ if device.SessionMacaroon == "" && device.Serial != "" {
+ err = s.refreshDeviceSession(device)
if err == auth.ErrNoSerial {
// missing serial assertion, log and continue without device authentication
logger.Debugf("cannot set device session: %v", err)
diff --git a/store/store_test.go b/store/store_test.go
index 95d9453104..90f539b3e0 100644
--- a/store/store_test.go
+++ b/store/store_test.go
@@ -102,17 +102,23 @@ type testAuthContext struct {
}
func (ac *testAuthContext) Device() (*auth.DeviceState, error) {
- return ac.device, nil
+ freshDevice := *ac.device
+ return &freshDevice, nil
}
-func (ac *testAuthContext) UpdateDevice(d *auth.DeviceState) error {
- ac.device = d
- return nil
+func (ac *testAuthContext) UpdateDeviceAuth(d *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) {
+ ac.c.Assert(d, DeepEquals, ac.device)
+ updated := *ac.device
+ updated.SessionMacaroon = newSessionMacaroon
+ *ac.device = updated
+ return &updated, nil
}
-func (ac *testAuthContext) UpdateUser(u *auth.UserState) error {
+func (ac *testAuthContext) UpdateUserAuth(u *auth.UserState, newDischarges []string) (*auth.UserState, error) {
ac.c.Assert(u, DeepEquals, ac.user)
- return nil
+ updated := *ac.user
+ updated.StoreDischarges = newDischarges
+ return &updated, nil
}
func (ac *testAuthContext) StoreID(fallback string) (string, error) {
@@ -199,6 +205,7 @@ func createTestDevice() *auth.DeviceState {
return &auth.DeviceState{
Brand: "some-brand",
SessionMacaroon: "device-macaroon",
+ Serial: "9999",
}
}
@@ -336,6 +343,180 @@ func (t *remoteRepoTestSuite) TestDownloadSyncFails(c *C) {
c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
}
+func (t *remoteRepoTestSuite) TestDoRequestSetsAuth(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+ // check user authorization is set
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization is set
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ io.WriteString(w, "response-data")
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestAuthNoSerial(c *C) {
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+ // check user authorization is set
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization was not set
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, "")
+
+ io.WriteString(w, "response-data")
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ // no serial and no device macaroon => no device auth
+ t.device.Serial = ""
+ t.device.SessionMacaroon = ""
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestRefreshesAuth(c *C) {
+ refresh, err := makeTestRefreshDischargeResponse()
+ c.Assert(err, IsNil)
+ c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
+
+ // mock refresh response
+ refreshDischargeEndpointHit := false
+ mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
+ refreshDischargeEndpointHit = true
+ }))
+ defer mockSSOServer.Close()
+ UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
+
+ // mock store response (requiring auth refresh)
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+
+ authorization := r.Header.Get("Authorization")
+ c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
+ if t.user.StoreDischarges[0] == refresh {
+ io.WriteString(w, "response-data")
+ } else {
+ w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
+ w.WriteHeader(http.StatusUnauthorized)
+ }
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+ c.Check(refreshDischargeEndpointHit, Equals, true)
+}
+
+func (t *remoteRepoTestSuite) TestDoRequestSetsAndRefreshesDeviceAuth(c *C) {
+ deviceSessionRequested := false
+ refreshSessionRequested := false
+ expiredAuth := `Macaroon root="expired-session-macaroon"`
+ // mock store response
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.UserAgent(), Equals, userAgent)
+
+ switch r.URL.Path {
+ case "/":
+ authorization := r.Header.Get("X-Device-Authorization")
+ if authorization == "" {
+ c.Fatalf("device authentication missing")
+ } else if authorization == expiredAuth {
+ w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
+ w.WriteHeader(http.StatusUnauthorized)
+ } else {
+ c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
+ io.WriteString(w, "response-data")
+ }
+ case "/identity/api/v1/nonces":
+ io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
+ case "/identity/api/v1/sessions":
+ authorization := r.Header.Get("X-Device-Authorization")
+ if authorization == "" {
+ io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
+ deviceSessionRequested = true
+ } else {
+ c.Check(authorization, Equals, expiredAuth)
+ io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
+ refreshSessionRequested = true
+ }
+ default:
+ c.Fatalf("unexpected path %q", r.URL.Path)
+ }
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
+ MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
+
+ // make sure device session is not set
+ t.device.SessionMacaroon = ""
+ authContext := &testAuthContext{c: c, device: t.device, user: t.user}
+ repo := New(&Config{}, authContext)
+ c.Assert(repo, NotNil)
+
+ endpoint, _ := url.Parse(mockServer.URL)
+ reqOptions := &requestOptions{Method: "GET", URL: endpoint}
+
+ response, err := repo.doRequest(repo.client, reqOptions, t.user)
+ defer response.Body.Close()
+ c.Assert(err, IsNil)
+
+ responseData, err := ioutil.ReadAll(response.Body)
+ c.Assert(err, IsNil)
+ c.Check(string(responseData), Equals, "response-data")
+ c.Check(deviceSessionRequested, Equals, true)
+ c.Check(refreshSessionRequested, Equals, true)
+}
+
const (
funkyAppName = "8nzc1x4iim2xj1g2ul64"
funkyAppDeveloper = "chipaca"
@@ -347,7 +528,7 @@ const (
/* acquired via
-http --pretty=format --print b https://search.apps.ubuntu.com/api/v1/snaps/details/hello-world X-Ubuntu-Series:16 fields==anon_download_url,architecture,channel,download_sha512,summary,description,binary_filesize,download_url,icon_url,last_updated,package_name,prices,publisher,ratings_average,revision,snap_id,support_url,title,content,version,origin,developer_id,private,confinement channel==edge | xsel -b
+http --pretty=format --print b https://search.apps.ubuntu.com/api/v1/snaps/details/hello-world X-Ubuntu-Series:16 fields==anon_download_url,architecture,channel,download_sha3_384,summary,description,binary_filesize,download_url,icon_url,last_updated,package_name,prices,publisher,ratings_average,revision,snap_id,support_url,title,content,version,origin,developer_id,private,confinement channel==edge | xsel -b
on 2016-07-03. Then, by hand:
* set prices to {"EUR": 0.99, "USD": 1.23}.
@@ -366,10 +547,10 @@ const MockDetailsJSON = `{
}
],
"self": {
- "href": "https://search.apps.ubuntu.com/api/v1/snaps/details/hello-world?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement&channel=edge"
+ "href": "https://search.apps.ubuntu.com/api/v1/snaps/details/hello-world?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement&channel=edge"
}
},
- "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
+ "anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
"architecture": [
"all"
],
@@ -379,22 +560,23 @@ const MockDetailsJSON = `{
"content": "application",
"description": "This is a simple hello world example.",
"developer_id": "canonical",
- "download_sha512": "345f33c06373f799b64c497a778ef58931810dd7ae85279d6917d8b4f43d38abaf37e68239cb85914db276cb566a0ef83ea02b6f2fd064b54f9f2508fa4ca1f1",
- "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
+ "download_sha3_384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9",
+ "download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
"icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
- "last_updated": "2016-05-31T07:02:32.586839Z",
+ "last_updated": "2016-07-12T16:37:23.960632Z",
"origin": "canonical",
"package_name": "hello-world",
"prices": {"EUR": 0.99, "USD": 1.23},
"publisher": "Canonical",
"ratings_average": 0.0,
- "revision": 26,
+ "revision": 27,
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
- "summary": "Hello world example",
+ "summary": "The 'hello-world' of snaps",
"support_url": "mailto:snappy-devel@lists.ubuntu.com",
"title": "hello-world",
- "version": "6.1"
-}`
+ "version": "6.3"
+}
+`
const mockPurchasesJSON = `[
{
@@ -468,6 +650,9 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, userAgent)
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
// no store ID by default
storeID := r.Header.Get("X-Ubuntu-Store")
c.Check(storeID, Equals, "")
@@ -496,7 +681,8 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) {
cfg := Config{
DetailsURI: detailsURI,
}
- repo := New(&cfg, nil)
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
// the actual test
@@ -504,15 +690,15 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) {
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
c.Check(result.Architectures, DeepEquals, []string{"all"})
- c.Check(result.Revision, Equals, snap.R(26))
+ c.Check(result.Revision, Equals, snap.R(27))
c.Check(result.SnapID, Equals, helloWorldSnapID)
c.Check(result.Developer, Equals, "canonical")
- c.Check(result.Version, Equals, "6.1")
- c.Check(result.Sha512, Matches, `[[:xdigit:]]{128}`)
+ c.Check(result.Version, Equals, "6.3")
+ c.Check(result.Sha3_384, Matches, `[[:xdigit:]]{96}`)
c.Check(result.Size, Equals, int64(20480))
c.Check(result.Channel, Equals, "edge")
c.Check(result.Description(), Equals, "This is a simple hello world example.")
- c.Check(result.Summary(), Equals, "Hello world example")
+ c.Check(result.Summary(), Equals, "The 'hello-world' of snaps")
c.Assert(result.Prices, DeepEquals, map[string]float64{"EUR": 0.99, "USD": 1.23})
c.Check(result.MustBuy, Equals, true)
@@ -592,6 +778,10 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryStoreIDFromAuthContext(c
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRevision(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.URL.Path, "/dev/api/snap-purchases") {
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
c.Check(r.URL.Path, Equals, "/details/hello-world")
c.Check(r.URL.Query(), DeepEquals, url.Values{
"channel": []string{""},
@@ -604,11 +794,12 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRevision(c *C) {
c.Assert(mockServer, NotNil)
defer mockServer.Close()
-
+ purchasesURI, err := url.Parse(mockServer.URL + "/dev/api/snap-purchases/")
detailsURI, err := url.Parse(mockServer.URL + "/details/")
c.Assert(err, IsNil)
cfg := DefaultConfig()
cfg.DetailsURI = detailsURI
+ cfg.PurchasesURI = purchasesURI
repo := New(cfg, nil)
c.Assert(repo, NotNil)
@@ -616,7 +807,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRevision(c *C) {
result, err := repo.Snap("hello-world", "edge", true, snap.R(26), t.user)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
- c.Check(result.Revision, DeepEquals, snap.R(26))
+ c.Check(result.Revision, DeepEquals, snap.R(27))
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsDevmode(c *C) {
@@ -661,177 +852,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsDevmode(c *C) {
c.Check(snap.Validate(result), IsNil)
}
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsSetsAuth(c *C) {
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // check user authorization is set
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- // check device authorization is set
- c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
- c.Check(r.UserAgent(), Equals, userAgent)
-
- io.WriteString(w, MockDetailsJSON)
- }))
-
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
-
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
- // check user authorization is set
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- // check device authorization is set
- c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
-
- c.Check(r.URL.Path, Equals, "/dev/api/snap-purchases/"+helloWorldSnapID+"/")
- c.Check(r.URL.Query().Get("include_item_purchases"), Equals, "true")
- io.WriteString(w, mockPurchaseJSON)
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
-
- detailsURI, err := url.Parse(mockServer.URL + "/details/")
- c.Assert(err, IsNil)
- purchasesURI, err := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
- c.Assert(err, IsNil)
-
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- DetailsURI: detailsURI,
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- snap, err := repo.Snap("hello-world", "edge", false, snap.R(0), t.user)
- c.Assert(snap, NotNil)
- c.Assert(err, IsNil)
- c.Check(snap.MustBuy, Equals, false)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsRefreshesAuth(c *C) {
- refresh, err := makeTestRefreshDischargeResponse()
- c.Assert(err, IsNil)
- c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
-
- // mock refresh response
- refreshDischargeEndpointHit := false
- mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
- refreshDischargeEndpointHit = true
- }))
- defer mockSSOServer.Close()
- UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
-
- // mock purchases response
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, mockPurchaseJSON)
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- purchasesURI, _ := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
-
- // mock store response (requiring auth refresh)
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- if t.user.StoreDischarges[0] == refresh {
- io.WriteString(w, MockDetailsJSON)
- } else {
- w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
- w.WriteHeader(http.StatusUnauthorized)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
- detailsURI, _ := url.Parse(mockServer.URL + "/details/")
-
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- DetailsURI: detailsURI,
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- snap, err := repo.Snap("hello-world", "edge", false, snap.R(0), t.user)
- c.Assert(err, IsNil)
- c.Check(refreshDischargeEndpointHit, Equals, true)
- c.Assert(snap, NotNil)
- c.Check(snap.MustBuy, Equals, false)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsSetsAndRefreshesDeviceAuth(c *C) {
- // mock purchases response
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, mockPurchaseJSON)
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- purchasesURI, _ := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
-
- deviceSessionRequested := false
- refreshSessionRequested := false
- expiredAuth := `Macaroon root="expired-session-macaroon"`
- // mock store response
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- switch r.URL.Path {
- case "/details/hello-world":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- c.Fatalf("device authentication missing")
- } else if authorization == expiredAuth {
- w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
- w.WriteHeader(http.StatusUnauthorized)
- } else {
- c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
- io.WriteString(w, MockDetailsJSON)
- }
- case "/identity/api/v1/nonces":
- io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
- case "/identity/api/v1/sessions":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
- deviceSessionRequested = true
- } else {
- c.Check(authorization, Equals, expiredAuth)
- io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
- refreshSessionRequested = true
- }
- default:
- c.Fatalf("unexpected path %q", r.URL.Path)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
- MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
- MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
- detailsURI, _ := url.Parse(mockServer.URL + "/details/")
-
- // make sure device session is not set
- t.device.SessionMacaroon = ""
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- DetailsURI: detailsURI,
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- snap, err := repo.Snap("hello-world", "edge", false, snap.R(0), t.user)
- c.Assert(err, IsNil)
- c.Assert(snap, NotNil)
- c.Check(deviceSessionRequested, Equals, true)
- c.Check(refreshSessionRequested, Equals, true)
- c.Check(snap.MustBuy, Equals, false)
-}
-
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsOopses(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.URL.Path, Equals, "/details/hello-world")
@@ -974,6 +994,9 @@ const MockSearchJSON = `{
func (t *remoteRepoTestSuite) TestUbuntuStoreFindQueries(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
query := r.URL.Query()
name := query.Get("name")
@@ -1004,7 +1027,8 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindQueries(c *C) {
DetailsURI: detailsURI,
SearchURI: searchURI,
}
- repo := New(&cfg, nil)
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
for _, query := range []Search{
@@ -1142,193 +1166,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadBody(c *C) {
c.Check(snaps, HasLen, 0)
}
-func (t *remoteRepoTestSuite) TestUbuntuStoreFindSetsAuth(c *C) {
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
- // check user authorization is set
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- // check device authorization is set
- c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
-
- c.Check(r.URL.Query().Get("q"), Equals, "foo")
- w.Header().Set("Content-Type", "application/hal+json")
- io.WriteString(w, MockSearchJSON)
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
-
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- // check device authorization is set
- c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
- c.Check(r.URL.Path, Equals, "/dev/api/snap-purchases/"+helloWorldSnapID+"/")
- c.Check(r.URL.Query().Get("include_item_purchases"), Equals, "true")
- io.WriteString(w, mockPurchaseJSON)
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
-
- var err error
- searchURI, err := url.Parse(mockServer.URL)
- c.Assert(err, IsNil)
- purchasesURI, err := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
- c.Assert(err, IsNil)
-
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- SearchURI: searchURI,
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- snaps, err := repo.Find(&Search{Query: "foo"}, t.user)
- c.Assert(err, IsNil)
- c.Assert(snaps, HasLen, 1)
- c.Check(snaps[0].SnapID, Equals, helloWorldSnapID)
- c.Check(snaps[0].Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49})
- c.Check(snaps[0].MustBuy, Equals, false)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreFindRefreshesAuth(c *C) {
- refresh, err := makeTestRefreshDischargeResponse()
- c.Assert(err, IsNil)
- c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
-
- // mock refresh response
- refreshDischargeEndpointHit := false
- mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
- refreshDischargeEndpointHit = true
- }))
- defer mockSSOServer.Close()
- UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
-
- // mock purchases response
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
- io.WriteString(w, mockPurchaseJSON)
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- purchasesURI, err := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
- c.Assert(err, IsNil)
-
- // mock store response (requiring auth refresh)
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- if t.user.StoreDischarges[0] == refresh {
- c.Check(r.URL.Query().Get("q"), Equals, "foo")
- w.Header().Set("Content-Type", "application/hal+json")
- w.WriteHeader(http.StatusOK)
- io.WriteString(w, MockSearchJSON)
- } else {
- w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
- w.WriteHeader(http.StatusUnauthorized)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
-
- searchURI, _ := url.Parse(mockServer.URL)
- cfg := Config{
- SearchURI: searchURI,
- PurchasesURI: purchasesURI,
- }
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
-
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- snaps, err := repo.Find(&Search{Query: "foo"}, t.user)
- c.Assert(err, IsNil)
- c.Check(refreshDischargeEndpointHit, Equals, true)
- c.Assert(snaps, HasLen, 1)
- c.Check(snaps[0].SnapID, Equals, helloWorldSnapID)
- c.Check(snaps[0].Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49})
- c.Check(snaps[0].MustBuy, Equals, false)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreFindSetsAndRefreshesDeviceAuth(c *C) {
- // mock purchases response
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
- io.WriteString(w, mockPurchaseJSON)
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- purchasesURI, err := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
- c.Assert(err, IsNil)
-
- deviceSessionRequested := false
- refreshSessionRequested := false
- expiredAuth := `Macaroon root="expired-session-macaroon"`
- // mock store response
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- switch r.URL.Path {
- case "/":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- c.Fatalf("device authentication missing")
- } else if authorization == expiredAuth {
- w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
- w.WriteHeader(http.StatusUnauthorized)
- } else {
- c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
- c.Check(r.URL.Query().Get("q"), Equals, "foo")
- w.Header().Set("Content-Type", "application/hal+json")
- w.WriteHeader(http.StatusOK)
- io.WriteString(w, MockSearchJSON)
- }
- case "/identity/api/v1/nonces":
- io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
- case "/identity/api/v1/sessions":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
- deviceSessionRequested = true
- } else {
- c.Check(authorization, Equals, expiredAuth)
- io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
- refreshSessionRequested = true
- }
- default:
- c.Fatalf("unexpected path %q", r.URL.Path)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
- MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
- MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
- searchURI, _ := url.Parse(mockServer.URL)
-
- cfg := Config{
- SearchURI: searchURI,
- PurchasesURI: purchasesURI,
- }
- // make sure device session is not set
- t.device.SessionMacaroon = ""
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
-
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- snaps, err := repo.Find(&Search{Query: "foo"}, t.user)
- c.Assert(err, IsNil)
- c.Check(deviceSessionRequested, Equals, true)
- c.Check(refreshSessionRequested, Equals, true)
- c.Assert(snaps, HasLen, 1)
- c.Check(snaps[0].SnapID, Equals, helloWorldSnapID)
- c.Check(snaps[0].Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49})
- c.Check(snaps[0].MustBuy, Equals, false)
-}
-
func (t *remoteRepoTestSuite) TestUbuntuStoreFindAuthFailed(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// check authorization is set
@@ -1435,6 +1272,9 @@ var MockUpdatesJSON = `
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefresh(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // check device authorization is set, implicitly checking doRequest was used
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
@@ -1467,7 +1307,8 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefresh(c *C) {
cfg := Config{
BulkURI: bulkURI,
}
- repo := New(&cfg, nil)
+ authContext := &testAuthContext{c: c, device: t.device}
+ repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{
@@ -1632,159 +1473,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdateNotSendLocalRevs(c
c.Assert(err, IsNil)
}
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdatesSetsAuth(c *C) {
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // check user authorization is set
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- // check device authorization is set
- c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
-
- io.WriteString(w, MockUpdatesJSON)
- }))
-
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
-
- var err error
- bulkURI, err := url.Parse(mockServer.URL + "/updates/")
- c.Assert(err, IsNil)
-
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- BulkURI: bulkURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- _, err = repo.ListRefresh([]*RefreshCandidate{
- {
- SnapID: helloWorldSnapID,
- Channel: "stable",
- Revision: snap.R(1),
- Epoch: "0",
- DevMode: false,
- },
- }, t.user)
- c.Assert(err, IsNil)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdatesRefreshesAuth(c *C) {
- refresh, err := makeTestRefreshDischargeResponse()
- c.Assert(err, IsNil)
- c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
-
- // mock refresh response
- refreshDischargeEndpointHit := false
- mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
- refreshDischargeEndpointHit = true
- }))
- defer mockSSOServer.Close()
- UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
-
- // mock store response (requiring auth refresh)
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- if t.user.StoreDischarges[0] == refresh {
- io.WriteString(w, MockUpdatesJSON)
- } else {
- w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
- w.WriteHeader(http.StatusUnauthorized)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
- bulkURI, _ := url.Parse(mockServer.URL + "/updates/")
-
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- BulkURI: bulkURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- _, err = repo.ListRefresh([]*RefreshCandidate{
- {
- SnapID: helloWorldSnapID,
- Channel: "stable",
- Revision: snap.R(1),
- Epoch: "0",
- DevMode: false,
- },
- }, t.user)
- c.Assert(err, IsNil)
- c.Check(refreshDischargeEndpointHit, Equals, true)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdatesSetsAndRefreshesDeviceAuth(c *C) {
- deviceSessionRequested := false
- refreshSessionRequested := false
- expiredAuth := `Macaroon root="expired-session-macaroon"`
- // mock store response
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- switch r.URL.Path {
- case "/updates/":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- c.Fatalf("device authentication missing")
- } else if authorization == expiredAuth {
- w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
- w.WriteHeader(http.StatusUnauthorized)
- } else {
- c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
- io.WriteString(w, MockUpdatesJSON)
- }
- case "/identity/api/v1/nonces":
- io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
- case "/identity/api/v1/sessions":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
- deviceSessionRequested = true
- } else {
- c.Check(authorization, Equals, expiredAuth)
- io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
- refreshSessionRequested = true
- }
- default:
- c.Fatalf("unexpected path %q", r.URL.Path)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
- MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
- MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
- bulkURI, _ := url.Parse(mockServer.URL + "/updates/")
-
- // make sure device session is not set
- t.device.SessionMacaroon = ""
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- BulkURI: bulkURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- _, err := repo.ListRefresh([]*RefreshCandidate{
- {
- SnapID: helloWorldSnapID,
- Channel: "stable",
- Revision: snap.R(1),
- Epoch: "0",
- DevMode: false,
- },
- }, t.user)
- c.Assert(err, IsNil)
- c.Check(deviceSessionRequested, Equals, true)
- c.Check(refreshSessionRequested, Equals, true)
-}
-
func (t *remoteRepoTestSuite) TestStructFieldsSurvivesNoTag(c *C) {
type s struct {
Foo int `json:"hello"`
@@ -1872,34 +1560,7 @@ xS4u9rVT6UY=`
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
- c.Check(r.URL.Path, Equals, "/assertions/snap-declaration/16/snapidfoo")
- io.WriteString(w, testAssertion)
- }))
-
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
-
- var err error
- assertionsURI, err := url.Parse(mockServer.URL + "/assertions/")
- c.Assert(err, IsNil)
- cfg := Config{
- AssertionsURI: assertionsURI,
- }
- repo := New(&cfg, nil)
-
- a, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
- c.Assert(err, IsNil)
- c.Check(a, NotNil)
- c.Check(a.Type(), Equals, asserts.SnapDeclarationType)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionSetsAuth(c *C) {
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // check user authorization is set
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- // check device authorization is set
+ // check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
@@ -1914,115 +1575,16 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionSetsAuth(c *C) {
assertionsURI, err := url.Parse(mockServer.URL + "/assertions/")
c.Assert(err, IsNil)
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
AssertionsURI: assertionsURI,
}
+ authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
- _, err = repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, t.user)
- c.Assert(err, IsNil)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionRefreshesAuth(c *C) {
- refresh, err := makeTestRefreshDischargeResponse()
- c.Assert(err, IsNil)
- c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
-
- // mock refresh response
- refreshDischargeEndpointHit := false
- mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
- refreshDischargeEndpointHit = true
- }))
- defer mockSSOServer.Close()
- UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
-
- // mock store response (requiring auth refresh)
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- if t.user.StoreDischarges[0] == refresh {
- c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
- c.Check(r.URL.Path, Equals, "/assertions/snap-declaration/16/snapidfoo")
- io.WriteString(w, testAssertion)
- } else {
- w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
- w.WriteHeader(http.StatusUnauthorized)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
- assertionsURI, _ := url.Parse(mockServer.URL + "/assertions/")
-
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- AssertionsURI: assertionsURI,
- }
- repo := New(&cfg, authContext)
-
- _, err = repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, t.user)
- c.Assert(err, IsNil)
- c.Check(refreshDischargeEndpointHit, Equals, true)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionSetsAndRefreshesDeviceAuth(c *C) {
- deviceSessionRequested := false
- refreshSessionRequested := false
- expiredAuth := `Macaroon root="expired-session-macaroon"`
- // mock store response
- mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- switch r.URL.Path {
- case "/assertions/snap-declaration/16/snapidfoo":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- c.Fatalf("device authentication missing")
- } else if authorization == expiredAuth {
- w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
- w.WriteHeader(http.StatusUnauthorized)
- } else {
- c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
- c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
- io.WriteString(w, testAssertion)
- }
- case "/identity/api/v1/nonces":
- io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
- case "/identity/api/v1/sessions":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
- deviceSessionRequested = true
- } else {
- c.Check(authorization, Equals, expiredAuth)
- io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
- refreshSessionRequested = true
- }
- default:
- c.Fatalf("unexpected path %q", r.URL.Path)
- }
- }))
- c.Assert(mockServer, NotNil)
- defer mockServer.Close()
- MyAppsDeviceNonceAPI = mockServer.URL + "/identity/api/v1/nonces"
- MyAppsDeviceSessionAPI = mockServer.URL + "/identity/api/v1/sessions"
- assertionsURI, _ := url.Parse(mockServer.URL + "/assertions/")
-
- // make sure device session is not set
- t.device.SessionMacaroon = ""
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- AssertionsURI: assertionsURI,
- }
- repo := New(&cfg, authContext)
-
- _, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, t.user)
+ a, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
c.Assert(err, IsNil)
- c.Check(deviceSessionRequested, Equals, true)
- c.Check(refreshSessionRequested, Equals, true)
+ c.Check(a, NotNil)
+ c.Check(a.Type(), Equals, asserts.SnapDeclarationType)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNotFound(c *C) {
@@ -2095,8 +1657,10 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) {
func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchases(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.URL.Path, Equals, "/dev/api/snap-purchases/")
io.WriteString(w, mockPurchasesJSON)
}))
@@ -2141,147 +1705,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchases(c *C) {
c.Check(otherApp2.MustBuy, Equals, false)
}
-func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchasesRefreshesAuth(c *C) {
- refresh, err := makeTestRefreshDischargeResponse()
- c.Assert(err, IsNil)
- c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
-
- // mock refresh response
- refreshDischargeEndpointHit := false
- mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
- refreshDischargeEndpointHit = true
- }))
- defer mockSSOServer.Close()
- UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
-
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- if t.user.StoreDischarges[0] == refresh {
- io.WriteString(w, mockPurchasesJSON)
- } else {
- w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
- w.WriteHeader(http.StatusUnauthorized)
- }
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- purchasesURI, _ := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
-
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- helloWorld := &snap.Info{}
- helloWorld.SnapID = helloWorldSnapID
- helloWorld.Prices = map[string]float64{"USD": 1.23}
-
- funkyApp := &snap.Info{}
- funkyApp.SnapID = funkyAppSnapID
- funkyApp.Prices = map[string]float64{"USD": 2.34}
-
- otherApp := &snap.Info{}
- otherApp.SnapID = "other"
- otherApp.Prices = map[string]float64{"USD": 3.45}
-
- otherApp2 := &snap.Info{}
- otherApp2.SnapID = "other2"
-
- snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
-
- err = repo.decoratePurchases(snaps, "edge", t.user)
- c.Assert(err, IsNil)
- c.Check(refreshDischargeEndpointHit, Equals, true)
-
- c.Check(helloWorld.MustBuy, Equals, false)
- c.Check(funkyApp.MustBuy, Equals, false)
- c.Check(otherApp.MustBuy, Equals, true)
- c.Check(otherApp2.MustBuy, Equals, false)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchasesSetsAndRefreshesDeviceAuth(c *C) {
- deviceSessionRequested := false
- refreshSessionRequested := false
- expiredAuth := `Macaroon root="expired-session-macaroon"`
- // mock store response
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- switch r.URL.Path {
- case "/dev/api/snap-purchases/":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- c.Fatalf("device authentication missing")
- } else if authorization == expiredAuth {
- w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
- w.WriteHeader(http.StatusUnauthorized)
- } else {
- c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
- io.WriteString(w, mockPurchasesJSON)
- }
- case "/identity/api/v1/nonces":
- io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
- case "/identity/api/v1/sessions":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
- deviceSessionRequested = true
- } else {
- c.Check(authorization, Equals, expiredAuth)
- io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
- refreshSessionRequested = true
- }
- default:
- c.Fatalf("unexpected path %q", r.URL.Path)
- }
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- MyAppsDeviceNonceAPI = mockPurchasesServer.URL + "/identity/api/v1/nonces"
- MyAppsDeviceSessionAPI = mockPurchasesServer.URL + "/identity/api/v1/sessions"
- purchasesURI, _ := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
-
- // make sure device session is not set
- t.device.SessionMacaroon = ""
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- helloWorld := &snap.Info{}
- helloWorld.SnapID = helloWorldSnapID
- helloWorld.Prices = map[string]float64{"USD": 1.23}
-
- funkyApp := &snap.Info{}
- funkyApp.SnapID = funkyAppSnapID
- funkyApp.Prices = map[string]float64{"USD": 2.34}
-
- otherApp := &snap.Info{}
- otherApp.SnapID = "other"
- otherApp.Prices = map[string]float64{"USD": 3.45}
-
- otherApp2 := &snap.Info{}
- otherApp2.SnapID = "other2"
-
- snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
-
- err := repo.decoratePurchases(snaps, "edge", t.user)
- c.Assert(err, IsNil)
- c.Check(deviceSessionRequested, Equals, true)
- c.Check(refreshSessionRequested, Equals, true)
-
- c.Check(helloWorld.MustBuy, Equals, false)
- c.Check(funkyApp.MustBuy, Equals, false)
- c.Check(otherApp.MustBuy, Equals, true)
- c.Check(otherApp2.MustBuy, Equals, false)
-}
-
func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchasesFailedAccess(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
@@ -2558,15 +1981,19 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreBuySuccess(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
- c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.URL.Path, Equals, "/dev/api/snap-purchases/"+helloWorldSnapID+"/")
c.Check(r.URL.Query().Get("include_item_purchases"), Equals, "true")
io.WriteString(w, "{}")
purchaseServerGetCalled = true
case "POST":
- c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("Content-Type"), Equals, "application/json")
c.Check(r.URL.Path, Equals, "/dev/api/snap-purchases/")
jsonReq, err := ioutil.ReadAll(r.Body)
@@ -2699,123 +2126,6 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreBuyInteractive(c *C) {
c.Check(purchaseServerPostCalled, Equals, true)
}
-func (t *remoteRepoTestSuite) TestUbuntuStoreBuyRefreshesAuth(c *C) {
- refresh, err := makeTestRefreshDischargeResponse()
- c.Assert(err, IsNil)
- c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
-
- // mock refresh response
- refreshDischargeEndpointHit := false
- mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
- refreshDischargeEndpointHit = true
- }))
- defer mockSSOServer.Close()
- UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
-
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- if t.user.StoreDischarges[0] == refresh {
- io.WriteString(w, mockSinglePurchaseJSON)
- } else {
- w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
- w.WriteHeader(http.StatusUnauthorized)
- }
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
-
- purchasesURI, _ := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
- cfg := Config{
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, nil)
- c.Assert(repo, NotNil)
-
- result, err := repo.Buy(&BuyOptions{
- SnapID: helloWorldSnapID,
- SnapName: "hello-world",
- Currency: "EUR",
- Price: 0.99,
-
- BackendID: "123",
- MethodID: 234,
- }, t.user)
-
- c.Assert(result, NotNil)
- c.Check(result.State, Equals, "Complete")
- c.Check(refreshDischargeEndpointHit, Equals, true)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStoreBuySetsAndRefreshesDeviceAuth(c *C) {
- deviceSessionRequested := false
- refreshSessionRequested := false
- expiredAuth := `Macaroon root="expired-session-macaroon"`
- // mock store response
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- switch r.URL.Path {
- case "/dev/api/snap-purchases/":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- c.Fatalf("device authentication missing")
- } else if authorization == expiredAuth {
- w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
- w.WriteHeader(http.StatusUnauthorized)
- } else {
- c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
- io.WriteString(w, mockSinglePurchaseJSON)
- }
- case "/identity/api/v1/nonces":
- io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
- case "/identity/api/v1/sessions":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
- deviceSessionRequested = true
- } else {
- c.Check(authorization, Equals, expiredAuth)
- io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
- refreshSessionRequested = true
- }
- default:
- c.Fatalf("unexpected path %q", r.URL.Path)
- }
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- MyAppsDeviceNonceAPI = mockPurchasesServer.URL + "/identity/api/v1/nonces"
- MyAppsDeviceSessionAPI = mockPurchasesServer.URL + "/identity/api/v1/sessions"
- purchasesURI, _ := url.Parse(mockPurchasesServer.URL + "/dev/api/snap-purchases/")
-
- // make sure device session is not set
- t.device.SessionMacaroon = ""
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- PurchasesURI: purchasesURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- result, err := repo.Buy(&BuyOptions{
- SnapID: helloWorldSnapID,
- SnapName: "hello-world",
- Currency: "EUR",
- Price: 0.99,
-
- BackendID: "123",
- MethodID: 234,
- }, t.user)
-
- c.Assert(err, IsNil)
- c.Assert(result, NotNil)
- c.Check(result.State, Equals, "Complete")
- c.Check(deviceSessionRequested, Equals, true)
- c.Check(refreshSessionRequested, Equals, true)
-}
-
func (t *remoteRepoTestSuite) TestUbuntuStoreBuyFailWrongPrice(c *C) {
searchServerCalled := false
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -3074,8 +2384,10 @@ func (t *remoteRepoTestSuite) TestUbuntuStorePaymentMethods(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
- c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
+ // check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
+
+ c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.URL.Path, Equals, "/api/2.0/click/paymentmethods/")
io.WriteString(w, paymentMethodsJson)
purchaseServerGetCalled++
@@ -3174,101 +2486,3 @@ func (t *remoteRepoTestSuite) TestUbuntuStorePaymentMethodsHandles401(c *C) {
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "invalid credentials")
}
-
-func (t *remoteRepoTestSuite) TestUbuntuStorePaymentMethodsRefreshesAuth(c *C) {
- refresh, err := makeTestRefreshDischargeResponse()
- c.Assert(err, IsNil)
- c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
-
- // mock refresh response
- refreshDischargeEndpointHit := false
- mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
- refreshDischargeEndpointHit = true
- }))
- defer mockSSOServer.Close()
- UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
-
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- authorization := r.Header.Get("Authorization")
- c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
- if t.user.StoreDischarges[0] == refresh {
- io.WriteString(w, paymentMethodsJson)
- } else {
- w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
- w.WriteHeader(http.StatusUnauthorized)
- }
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
-
- paymentMethodsURI, _ := url.Parse(mockPurchasesServer.URL + "/api/2.0/click/paymentmethods/")
- cfg := Config{
- PaymentMethodsURI: paymentMethodsURI,
- }
- repo := New(&cfg, nil)
- c.Assert(repo, NotNil)
-
- result, err := repo.PaymentMethods(t.user)
- c.Assert(err, IsNil)
- c.Assert(result, NotNil)
- c.Check(refreshDischargeEndpointHit, Equals, true)
-}
-
-func (t *remoteRepoTestSuite) TestUbuntuStorePaymentMethodsSetsAndRefreshesDeviceAuth(c *C) {
- deviceSessionRequested := false
- refreshSessionRequested := false
- expiredAuth := `Macaroon root="expired-session-macaroon"`
- // mock store response
- mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- c.Check(r.UserAgent(), Equals, userAgent)
-
- switch r.URL.Path {
- case "/api/2.0/click/paymentmethods/":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- c.Fatalf("device authentication missing")
- } else if authorization == expiredAuth {
- w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
- w.WriteHeader(http.StatusUnauthorized)
- } else {
- c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
- io.WriteString(w, paymentMethodsJson)
- }
- case "/identity/api/v1/nonces":
- io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
- case "/identity/api/v1/sessions":
- authorization := r.Header.Get("X-Device-Authorization")
- if authorization == "" {
- io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
- deviceSessionRequested = true
- } else {
- c.Check(authorization, Equals, expiredAuth)
- io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
- refreshSessionRequested = true
- }
- default:
- c.Fatalf("unexpected path %q", r.URL.Path)
- }
- }))
- c.Assert(mockPurchasesServer, NotNil)
- defer mockPurchasesServer.Close()
- MyAppsDeviceNonceAPI = mockPurchasesServer.URL + "/identity/api/v1/nonces"
- MyAppsDeviceSessionAPI = mockPurchasesServer.URL + "/identity/api/v1/sessions"
- paymentMethodsURI, _ := url.Parse(mockPurchasesServer.URL + "/api/2.0/click/paymentmethods/")
-
- // make sure device session is not set
- t.device.SessionMacaroon = ""
- authContext := &testAuthContext{c: c, device: t.device, user: t.user}
- cfg := Config{
- PaymentMethodsURI: paymentMethodsURI,
- }
- repo := New(&cfg, authContext)
- c.Assert(repo, NotNil)
-
- result, err := repo.PaymentMethods(t.user)
- c.Assert(err, IsNil)
- c.Assert(result, NotNil)
- c.Check(deviceSessionRequested, Equals, true)
- c.Check(refreshSessionRequested, Equals, true)
-}
diff --git a/tests/lib/assertions/developer1.account-key b/tests/lib/assertions/developer1.account-key
index 57c256344d..302d42629a 100644
--- a/tests/lib/assertions/developer1.account-key
+++ b/tests/lib/assertions/developer1.account-key
@@ -2,8 +2,8 @@ type: account-key
authority-id: testrootorg
public-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
account-id: developer1
+name: default
since: 2016-08-19T15:49:45+02:00
-timestamp: 2016-08-11T18:46:02+02:00
body-length: 717
sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
@@ -18,13 +18,13 @@ Yy04Sf9LI148vJMsYenonkoWejWdMi8iCUTeaZydHJEUBU/RbNFLjCWa6NIUe9bfZgLiOOZkps54
+/AL078ri/tGjo/5UGvezSmwrEoWJyqrJt2M69N2oVDLJcHeo2bUYPtFC2Kfb2je58JrJ+llifdg
rAsxbnHXiXyVimUAEQEAAQ==
-AcLBUgQAAQoABgUCV7cOeQAAFqUQAE90GSklMLMqwudOcx2Za0xVHoNdmcE4yMI4gMdtzMM+I0EY
-rosU5XtObDSWog+7u1ojExWAEm01KwP4QGYJunHPYOKWb/72hS1u/xj3e3EUx04cwQw/MfhS+C5i
-D1FPtHe/ikaFBiM5grHhGFbcPWCZhzzSsDCEVsy59UnS4AR4I0ADFvLGJUgSRr+FIf8EYatGAxib
-u8gWHBnxhfgZ/Z7CWDEyZkFmPGoJ7NsJBxXpJ3w/LPvEvFpT03zdn3abIDvXEq0zpwoPper2+xUD
-RDvJYrNswxs7nWsZKIwLazTw470US5nEYyQ9JoVXilWUXXY1HukILIyzB0kMV1ncPo4ru982K0rf
-0JPHV8qpOBn5vZyH/ZZMl9KarlQbkG+bYhFmugD83f7quF/AwFUe/oeMtj7BiDwEV5BxENoFu8SR
-wbuKUswCaWNGhwnJ8l4pNBg5mnkleLVUtOCVVxsWP84lvAGlj9zeQB8G17GbzdAf5SiSkWNq4tMV
-b1UIoWjYvPb4+GPgo5zaqXWaGeiU2onU1UCRNE3UxIHim8u88mWAtxV8PvKqRkQC9ecJLQtI0nD4
-ubwvQeWP793+3nU47um7Dfs7CFD9aLjsgvshGSaBW90GQpK2WNL9hR5aMCzuL8uXXaKpvRKMGM1X
-hlym2K7K9yVpIumDPAYV8rmF9hD3
+AcLBXAQAAQoABgUCV9AFCQAKCRDdoJRfvd5vjf8OD/4nfa6dj+39OyxfBJYXCUgFj2qCPYUm66j+
+bwNY6YD96ZP4QKx4+Vhqrmu2RWhUISqHhJH/SPKxfil9nocWE16knSgE4HFUnb2omMBIq+wU1ThV
+JFvxdWXt9KFMKBlYeKr1BOoXuPJlQGf8PnsTM1oCqrzAyfGAbSFmsVrqzf8ujyQf551f5RovdSLX
+dqEZX1tYiyoIY9FJNGL5A74Q8CC9V1yJEPtEcQDEovoa368fD2S3rvyBYtaHnQ3tkrvZgZCPe0lJ
+4FwtljgGuAALAQPcZOepJRnNP60/eeYJGZPlQwwjD7akxy/FrDIjbqW/HRxHcroyKHGnMSD+DX5D
+GPSxClb1ChI+RqyNZJzubIh11lnLmidYsonNjTYZRmMDPn/iIo2Goso8hYKoF8ztJeD42otdLrMN
+omV7qZvf2bHZ5ADUPsK468CNOPnjQYL0AC5D01C+QxrkPfK8aJQiK0/rYcgCouSetzJHl0JrC0Fv
+6IfwpPxXz0/KHqDwsC20yGhmyCS8RjqzTX1pkn/hPeHD7qNJBP+4oXfyGdaEu+vsiAWIkgasv3Um
+pePncPNiBxTBHJEyYeRK6w5uy2M9iUe/QLT/1cHEYgsQDDr2mSV8rdpV9pn5JcyfiCL0nhIMPbTU
+DcIT7mkb48ulFZpKyVYubC5yDPGfaK621eRS5UfO1Q==
diff --git a/tests/lib/assertions/testrootorg-store.account-key b/tests/lib/assertions/testrootorg-store.account-key
index f3b0827f60..d04be39745 100644
--- a/tests/lib/assertions/testrootorg-store.account-key
+++ b/tests/lib/assertions/testrootorg-store.account-key
@@ -2,6 +2,7 @@ type: account-key
authority-id: testrootorg
public-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
account-id: testrootorg
+name: test-store
since: 2016-08-11T18:42:22+02:00
body-length: 717
sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
@@ -17,13 +18,13 @@ iAIwA4DpGMmFJ26maqVzJuiLvicri2FR/sJaSA24N8HbGne3gSS7WrSQS+jKe3IZPVy64NCoGvrW
o/HvTeqsIfihKPEpXm8QVtjNhtkVn3RdIUgOaNWyAfnZ4dW1TVIATe+OHDw2TNyImTjE0x75nL6B
1/Rrn+9VP9Swhv8AEQEAAQ==
-AcLBUgQAAQoABgUCV6yq7gAAhaYQAMIVYhta2uUvm5PXApdXmZFWr+iZYfkAZW8PEMOsuYVHbDoH
-oA7dpO0EwZXl/mCgGjNNc4nUmqQLBiIwwrcnmcYSRl2Xz+u+ssou4YMueXOD2tHo1N2J39SKpS72
-VqQsnF77Qylgdp2j7Q9lJrU0qHz0M195OJXNSppfdHYeWptfsO02cApPobU9s6KT5VggVg1ushNM
-1u97A+uvoClfJ53PPafC0kr1+vwFVPj+mko4gc7sIB42xwz+YeR47CgSaT8i8K1u5ouaHCNxe61+
-siQxAdIOv+hOAWAOMoOWZjxh5K7J9A1Dc18EMyggf716IUCBtkvDKfwcXFcoic1X6EVPO5kNhlh7
-aLFS8UVsBPaZKnJm3ZFudhYQUmZt22ntslgGrq9+NBeh9i6nswUPKdj2idHkyQsjnYgYyTpEH/xh
-sBqkqkedPkUtn+tP5dS4TP/L3Xq/q2tQbA4+gNVbZXIo8g8wfpsP8+sIOnEV5UcoxaO3oI3YUJrN
-oFGmC7No802XKG1ZNHhBtMSaan0pafWrBrHn+axT9Jbl/1B73TYg3zQWOpDuSpH3SU+gXJ/eukUb
-LvC8UWZM9YEhu/ZINSvSDQzvobV/NVrHWEJBXxrc3CwseFDgQq+Yz/CFGud79z2mS8lNHKSqQdFK
-3Bd0HG1HheYcX0nGIva0KIG0Sgf/
+AcLBXAQAAQoABgUCV866kwAKCRBMcZp594FxpHWHD/9AaZXqyT/Zsmq/VzmAMpd9JvCH4PHQKtAP
+bXfP2Dnpa2wk2wuzQuSWunR8NDRyVh/aNVeTEZ9dFm/B8LR+U2O4rsHmFSeicmsTmo9u/HouRdEU
+zeSc6cbAxMPpfNSjr5J+URLjGRT6oX5fEBmRPx/OC9pEIScMx7uKmTKEnuyMzLRNN/6HiGWKrFCo
+nJdKkwRXrkCHyXWAOv1GumT7NDuyFcjAqt/UdHliTZkDBImKOsBmBVXMUjg7HCSS2uq/5WjStJ+B
+JHQ4GSsXBvVINs6BncNWcvV6mCQ73D57MzGhqo997Zb4tSrn7UNGWK7GLCzV3e/pFlG7pw6HbgnQ
++rxU2Oj/TPVw0tcnUiRl2ttKpm+nua0Cl+MD+Gx0KXLAVp0ZGOQ9yGyP9AePFzcOR8SlRIgxi0EI
+iJkSeYilqoKo3AJhnICRiqvAca2TGJoiJUryEgZ8jbTOElfaF2p+y0xvXGlWbKZm1gzGyvFM5fV5
+hJTlp/am+2uVn6U8wPACir4PrbuXYo7L4MIXww2OEO0ruBIaLARbc5IutSWmw6AEYQUxtsa9bdHV
+Zin7LGbEj6lZm8GycWQwh4B6Vnt6dJRIyPc/9G7uM8Ds/2Wa7+yAxhiPqm8DwlbOYh1npw4X4TLD
+IMGnTv5N3zllI+Xz4rqJzNTzEbvOIcrqWxCedQe79A==
diff --git a/tests/lib/fakestore/refresh/refresh.go b/tests/lib/fakestore/refresh/refresh.go
index e571d0a46a..ce7d15cbba 100644
--- a/tests/lib/fakestore/refresh/refresh.go
+++ b/tests/lib/fakestore/refresh/refresh.go
@@ -30,6 +30,7 @@ import (
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/snapasserts"
"github.com/snapcore/snapd/asserts/sysdb"
"github.com/snapcore/snapd/asserts/systestkeys"
"github.com/snapcore/snapd/dirs"
@@ -85,7 +86,7 @@ func writeAssert(a asserts.Assertion, targetDir string) error {
return ioutil.WriteFile(filepath.Join(targetDir, "asserts", fn), asserts.Encode(a), 0644)
}
-func makeFakeRefreshForSnap(snap, targetDir string, db *asserts.Database, f *asserts.Fetcher) error {
+func makeFakeRefreshForSnap(snap, targetDir string, db *asserts.Database, f asserts.Fetcher) error {
// make a fake update snap in /var/tmp (which is not a tempfs)
fakeUpdateDir, err := ioutil.TempDir("/var/tmp", "snap-build-")
if err != nil {
@@ -198,11 +199,8 @@ func buildSnap(snapDir, targetDir string) (*info, error) {
return &info{digest: newDigest, size: size}, nil
}
-func copySnapAsserts(info *info, f *asserts.Fetcher) error {
- return f.Fetch(&asserts.Ref{
- Type: asserts.SnapRevisionType,
- PrimaryKey: []string{info.digest},
- })
+func copySnapAsserts(info *info, f asserts.Fetcher) error {
+ return snapasserts.FetchSnapAssertions(f, info.digest)
}
func makeNewSnapRevision(orig, new *info, targetDir string, db *asserts.Database) error {
diff --git a/tests/lib/fakestore/store/store_test.go b/tests/lib/fakestore/store/store_test.go
index d452766305..327fc79d48 100644
--- a/tests/lib/fakestore/store/store_test.go
+++ b/tests/lib/fakestore/store/store_test.go
@@ -1,5 +1,4 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
-// +build !excludeintegration
/*
* Copyright (C) 2014-2015 Canonical Ltd
diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh
index 75ca14518c..56db81ba1e 100755
--- a/tests/lib/prepare.sh
+++ b/tests/lib/prepare.sh
@@ -12,6 +12,14 @@ prepare_classic() {
snap install --candidate ubuntu-core
snap list | grep core
+ echo "Ensure that the grub-editenv list output is empty on classic"
+ output=$(grub-editenv list)
+ if [ -n "$output" ]; then
+ echo "Expected empty grub environment, got:"
+ echo "$output"
+ exit 1
+ fi
+
systemctl stop snapd.service snapd.socket
systemctl daemon-reload
mounts="$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ')"
@@ -205,4 +213,11 @@ prepare_all_snap() {
echo "Kernel has a store revision"
snap list|grep ^${gadget_name}-kernel|grep -E " [0-9]+\s+canonical"
+
+ # Snapshot the fresh state
+ if [ ! -f $SPREAD_PATH/snapd-state.tar.gz ]; then
+ systemctl stop snapd.service snapd.socket
+ tar czf $SPREAD_PATH/snapd-state.tar.gz /var/lib/snapd
+ systemctl start snapd.socket
+ fi
}
diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh
index 30e9ca991f..ef34eb500f 100755
--- a/tests/lib/reset.sh
+++ b/tests/lib/reset.sh
@@ -32,6 +32,7 @@ reset_classic() {
}
reset_all_snap() {
+ # remove all leftover snaps
. $TESTSLIB/gadget.sh
gadget_name=$(get_gadget_name)
for snap in $(ls /snap); do
@@ -40,6 +41,12 @@ reset_all_snap() {
fi
snap remove $snap
done
+
+ # ensure we have the same state as initially
+ systemctl stop snapd.service snapd.socket
+ $(cd / && tar xzf $SPREAD_PATH/snapd-state.tar.gz)
+ rm -rf /root/.snap
+ systemctl start snapd.service snapd.socket
}
if [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then
diff --git a/tests/lib/snaps/locale-control-consumer/bin/get b/tests/lib/snaps/locale-control-consumer/bin/get
index d82fdad948..ee4c740c66 100755
--- a/tests/lib/snaps/locale-control-consumer/bin/get
+++ b/tests/lib/snaps/locale-control-consumer/bin/get
@@ -4,11 +4,11 @@ import subprocess
import sys
def run(key):
+ prefix = key+'='
with open('/etc/default/locale') as input_data:
for line in input_data:
- if key in line:
- parts=line.split('=')
- print(parts[1].replace('"', ''), end='')
+ if line.startswith(prefix):
+ print(line[len(prefix):].strip().strip('"'), end='')
if __name__ == '__main__':
sys.exit(run(sys.argv[1]))
diff --git a/tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml b/tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml
new file mode 100644
index 0000000000..61db7f2837
--- /dev/null
+++ b/tests/lib/snaps/test-snapd-upower-observe-consumer/snapcraft.yaml
@@ -0,0 +1,14 @@
+name: test-snapd-upower-observe-consumer
+version: 1.0
+summary: Basic upower-observe consumer snap
+description: A basic snap declaring a plug on upower-observe
+
+apps:
+ upower:
+ command: upower
+ plugs: [upower-observe]
+
+parts:
+ upower:
+ plugin: nil
+ stage-packages: [upower]
diff --git a/tests/lib/store.sh b/tests/lib/store.sh
index 500281d3ce..54200ca4cb 100644
--- a/tests/lib/store.sh
+++ b/tests/lib/store.sh
@@ -52,8 +52,6 @@ setup_store(){
local store_type=$1
local top_dir=$2
if [ "$store_type" = "fake" ]; then
- setup_fake_store $top_dir
- elif [ "$store_type" = "fake-w-assert-fallback" ]; then
setup_fake_store $top_dir -assert-fallback
else
if [ "$store_type" = "staging" ]; then
diff --git a/tests/main/ack/developer1.account b/tests/main/ack/developer1.account
deleted file mode 100644
index 65e9c49180..0000000000
--- a/tests/main/ack/developer1.account
+++ /dev/null
@@ -1,19 +0,0 @@
-type: account
-authority-id: testrootorg
-account-id: developer1
-display-name: Developer 1
-timestamp: 2016-08-11T18:46:02+02:00
-username: developer1
-validation: unproven
-sign-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
-
-AcLBUgQAAQoABgUCV6yrygAAf0IQAFhUje+lWt6c2bmjUg17vo3tdowkpNYvXLma0ajCphULCmGg
-3qtS8sVCJa1rFXOeKr3whFc/iheFbZJyQh2lsIQsRE0Z587uxFLbs5ua0FU0yzU7va03PdfBIK9o
-BEC8uXdCx3yFlijnDibC3D6daqB/PkUUM+WT5ypvp4b4l/IY6Vf0s2p5IzYCToBcXzdXOuL6e8t8
-d2kC2q1P5WX+fK20UjSeGHSanR6sfr4Tw+FNgv/MtmaBAkhAbIHMKTOZaKd7sEjT0QLJVYRdWlTp
-dSpaJRqzTE2v7Ql7RjJtFO8+QKnjzNGMbRYj9yX9meBBeT+iDTqH4UrvyRBOmLlKVAkt8mXqjwWj
-IfuA+ISWb8Qc/aah/DO6wONt7oAD6AkXXrCFtinHyQCutD5/XB63eCeSJAfvxD2Nbx9xEWjwu4nE
-6D6a5URpU4Kzo05jk/OnCnjMHYgW/grn9wRt4yXAXWkENQfcAYH6ZZV4VTJqA+2fAO7q9v5WryA4
-z1WcX7073ORAFCviiUFEoX/3eUfsyom5AF45OKxs8bm8jfRemnfUjRrPbpV+auBZ+DG7NDKFAOVv
-zKKivSNac125adAcC3Xg1783eUMpDStylIOxziVYDB0e6nR4ekiccnvLv5GpiEOG0ZGa4sUYI6+V
-hWeNg9t8ydjCleMJcHt5WrgcBQuw
diff --git a/tests/main/ack/task.yaml b/tests/main/ack/task.yaml
index e4fc41af1b..b655ae3b26 100644
--- a/tests/main/ack/task.yaml
+++ b/tests/main/ack/task.yaml
@@ -11,14 +11,14 @@ prepare: |
systemctl start snapd.socket snapd.service
execute: |
echo "Ack when missing matching key fails"
- ! snap ack developer1.account
+ ! snap ack cp $TESTSLIB/assertions/developer1.account
echo "Ack the test store key"
- snap ack testrootorg-store.account-key
+ snap ack $TESTSLIB/assertions/testrootorg-store.account-key
snap known account-key public-key-sha3-384=XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y | grep "^sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR$"
echo "Ack a developer account signed by that"
- snap ack developer1.account
+ snap ack $TESTSLIB/assertions/developer1.account
snap known account account-id=developer1|grep "^username: developer1$"
diff --git a/tests/main/ack/testrootorg-store.account-key b/tests/main/ack/testrootorg-store.account-key
deleted file mode 100644
index f3b0827f60..0000000000
--- a/tests/main/ack/testrootorg-store.account-key
+++ /dev/null
@@ -1,29 +0,0 @@
-type: account-key
-authority-id: testrootorg
-public-key-sha3-384: XCIC_Wvj9_hiAt0b10sDon74oGr3a6xGODkMZqrj63ZzNYUD5N87-ojjPoeN7f1Y
-account-id: testrootorg
-since: 2016-08-11T18:42:22+02:00
-body-length: 717
-sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR
-
-AcbBTQRWhcGAARAAmJqmZvsS58INTs+UQ+jfo836vBS5tkU/hk7c0huZe3So2gc9kaJvjkjhZ6g0
-0/kGoidw3i2WkdMEp+JtvU9Ztfeu/Nn/OqkSc3Ap1KAmqL4OllPVII8H69w2zqvmo+PcqH0SvHAV
-EoOC2ToXP0wEHnAZsbVu56AKrwpHDppPEIvaS6glrsEX1AXpOeMHZLVRtfsBB6dlLXuula1UrSAL
-RFCEjXtqXqOto5Vo9C8p63lLBy19ifz4OriWAGBqZvFdItmo8VIPXSDdgMMHBlu2MSNJHVfo66yp
-buos5Qs6PlVARACLJgzgplI5sDbzXtVx5O9Q8YJjz4NfU0WOWYPWvmANXDeMNixWoWvuixYvsXaG
-x6mdD7Hh/gi8prkQmZ7gxW1MEOV9JThAqYjjs2ayGVD73EI2sKYxVwEg3iJToQ/cEz3O2U1HdmYj
-QfRDJiX3GEPBXXttDrbPM42SHElouldmJ+PkJDLdkGmA85xYUoEKHdEFIkjFStQcyO5CkyNZN7SH
-iAIwA4DpGMmFJ26maqVzJuiLvicri2FR/sJaSA24N8HbGne3gSS7WrSQS+jKe3IZPVy64NCoGvrW
-o/HvTeqsIfihKPEpXm8QVtjNhtkVn3RdIUgOaNWyAfnZ4dW1TVIATe+OHDw2TNyImTjE0x75nL6B
-1/Rrn+9VP9Swhv8AEQEAAQ==
-
-AcLBUgQAAQoABgUCV6yq7gAAhaYQAMIVYhta2uUvm5PXApdXmZFWr+iZYfkAZW8PEMOsuYVHbDoH
-oA7dpO0EwZXl/mCgGjNNc4nUmqQLBiIwwrcnmcYSRl2Xz+u+ssou4YMueXOD2tHo1N2J39SKpS72
-VqQsnF77Qylgdp2j7Q9lJrU0qHz0M195OJXNSppfdHYeWptfsO02cApPobU9s6KT5VggVg1ushNM
-1u97A+uvoClfJ53PPafC0kr1+vwFVPj+mko4gc7sIB42xwz+YeR47CgSaT8i8K1u5ouaHCNxe61+
-siQxAdIOv+hOAWAOMoOWZjxh5K7J9A1Dc18EMyggf716IUCBtkvDKfwcXFcoic1X6EVPO5kNhlh7
-aLFS8UVsBPaZKnJm3ZFudhYQUmZt22ntslgGrq9+NBeh9i6nswUPKdj2idHkyQsjnYgYyTpEH/xh
-sBqkqkedPkUtn+tP5dS4TP/L3Xq/q2tQbA4+gNVbZXIo8g8wfpsP8+sIOnEV5UcoxaO3oI3YUJrN
-oFGmC7No802XKG1ZNHhBtMSaan0pafWrBrHn+axT9Jbl/1B73TYg3zQWOpDuSpH3SU+gXJ/eukUb
-LvC8UWZM9YEhu/ZINSvSDQzvobV/NVrHWEJBXxrc3CwseFDgQq+Yz/CFGud79z2mS8lNHKSqQdFK
-3Bd0HG1HheYcX0nGIva0KIG0Sgf/
diff --git a/tests/main/create-key/successful_default.exp b/tests/main/create-key/successful_default.exp
index 2e5b9ae2af..9f4e01ab9a 100644
--- a/tests/main/create-key/successful_default.exp
+++ b/tests/main/create-key/successful_default.exp
@@ -28,10 +28,11 @@ if {[lindex $status 3] != 0} {
spawn snap export-key --account=developer default
-expect "Enter passphrase: "
-send "pass\n"
-
+# fun!
+# gpg1 asks for a passphrase on the terminal no matter what
+# gpg2 gets the passphrase via our fake pinentry
expect {
+ "Enter passphrase: " {send "pass\n"; exp_continue}
"account-id: developer" {}
timeout { exit 1 }
eof { exit 1 }
diff --git a/tests/main/create-key/successful_non_default.exp b/tests/main/create-key/successful_non_default.exp
index 5c7f815760..452f335a12 100644
--- a/tests/main/create-key/successful_non_default.exp
+++ b/tests/main/create-key/successful_non_default.exp
@@ -28,10 +28,11 @@ if {[lindex $status 3] != 0} {
spawn snap export-key --account=developer another
-expect "Enter passphrase: "
-send "pass\n"
-
+# fun!
+# gpg1 asks for a passphrase on the terminal no matter what
+# gpg2 gets the passphrase via our fake pinentry
expect {
+ "Enter passphrase: " {send "pass\n"; exp_continue}
"account-id: developer" {}
timeout { exit 1 }
eof { exit 1 }
diff --git a/tests/main/create-key/task.yaml b/tests/main/create-key/task.yaml
index b596af5b09..a7975f6fa0 100644
--- a/tests/main/create-key/task.yaml
+++ b/tests/main/create-key/task.yaml
@@ -5,6 +5,33 @@ systems: [-ubuntu-core-16-64]
restore: |
rm -rf $HOME/.snap/gnupg
+prepare: |
+ echo "setup fake gpg pinentry environment"
+ cat > /tmp/pinentry-fake <<'EOF'
+ #!/bin/sh
+ set -e
+ echo "OK Pleased to meet you"
+ while true; do
+ read line
+ case $line in
+ GETPIN)
+ echo "D pass"
+ echo "OK"
+ ;;
+ BYE)
+ exit 0
+ ;;
+ *)
+ echo "OK I'm not very smart"
+ ;;
+ esac
+ done
+ EOF
+ chmod +x /tmp/pinentry-fake
+ mkdir -p /root/.snap/gnupg/
+ chmod 0700 /root/.snap/gnupg/
+ echo pinentry-program /tmp/pinentry-fake > /root/.snap/gnupg/gpg-agent.conf
+
execute: |
echo "Checking passphrase mismatch error"
expect -f passphrase_mismatch.exp
diff --git a/tests/main/install-sideload/task.yaml b/tests/main/install-sideload/task.yaml
index 052267d49a..f8b4881d49 100644
--- a/tests/main/install-sideload/task.yaml
+++ b/tests/main/install-sideload/task.yaml
@@ -18,6 +18,10 @@ execute: |
.*"
snap install --dangerous ./basic_1.0_all.snap | grep -Pzq "$expected"
+ echo "Sideloaded snap with (deprecated) --force-dangerous option"
+ snap remove basic
+ snap install --force-dangerous ./basic_1.0_all.snap | grep -Pzq "$expected"
+
echo "Sideloaded snap executes commands"
snap install --dangerous ./test-snapd-tools_1.0_all.snap
test-snapd-tools.success
diff --git a/tests/main/interfaces-locale-control/task.yaml b/tests/main/interfaces-locale-control/task.yaml
index e0542bf516..95a6e43e48 100644
--- a/tests/main/interfaces-locale-control/task.yaml
+++ b/tests/main/interfaces-locale-control/task.yaml
@@ -16,7 +16,11 @@ prepare: |
echo "Given a snap declaring a plug on the locale-control interface is installed"
snapbuild $TESTSLIB/snaps/locale-control-consumer .
snap install --dangerous locale-control-consumer_1.0_all.snap
- cp /etc/default/locale locale.back
+ mv /etc/default/locale locale.back
+ cat > /etc/default/locale <<EOF
+ LANG="$LANG"
+ LANGUAGE="$LANGUAGE"
+ EOF
restore: |
rm -f locale-control-consumer_1.0_all.snap locale-read.error locale-write.error
diff --git a/tests/main/interfaces-upower-observe/task.yaml b/tests/main/interfaces-upower-observe/task.yaml
new file mode 100644
index 0000000000..6e3a763cde
--- /dev/null
+++ b/tests/main/interfaces-upower-observe/task.yaml
@@ -0,0 +1,54 @@
+summary: Ensure that the upower-observe interface works.
+
+systems: [-ubuntu-core-16-64]
+
+summary: |
+ The upower-observe interface allows a snap to query UPower for power devices, history
+ and statistics.
+
+ A snap which defines the upower-observe plug must be shown in the interfaces list.
+ The plug must be autoconnected on install and, as usual, must be able to be reconnected.
+
+ The test uses a snap wrapping the upower command line utility, and checks that it can query
+ it without error while the plug is connected.
+
+prepare: |
+ echo "Given a snap declaring a plug on the upower-observe interface is installed"
+ snap install --edge test-snapd-upower-observe-consumer
+
+ apt install -y upower
+
+restore: |
+ rm -f upower.error
+ apt remove -y upower
+ apt autoremove -y
+
+execute: |
+ CONNECTED_PATTERN=":upower-observe +test-snapd-upower-observe-consumer"
+ DISCONNECTED_PATTERN="(?s).*?\n- +test-snapd-upower-observe-consumer:upower-observe"
+
+ echo "Then it is connected by default"
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect test-snapd-upower-observe-consumer:upower-observe ubuntu-core:upower-observe
+ snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap is not able to dump info about the upower devices"
+ if su -l -c "test-snapd-upower-observe-consumer.upower --dump 2>${PWD}/upower.error" test; then
+ echo "Expected permission error accessing upower info with disconnected plug"
+ exit 1
+ fi
+ grep -q "Permission denied" upower.error
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect test-snapd-upower-observe-consumer:upower-observe ubuntu-core:upower-observe
+ snap interfaces | grep -Pzq "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to dump info about the upower devices"
+ expected="(?s)Device: +/org/freedesktop/UPower/devices/DisplayDevice.*Daemon:.*"
+ su -l -c 'test-snapd-upower-observe-consumer.upower --dump' test | grep -Pqz "$expected"
diff --git a/tests/main/login/missing_email_error.exp b/tests/main/login/missing_email_error.exp
index 8470b39d95..4bd1e1ff2e 100644
--- a/tests/main/login/missing_email_error.exp
+++ b/tests/main/login/missing_email_error.exp
@@ -1,7 +1,7 @@
spawn snap login
expect {
- "required argument `email` was not provided" {
+ "required argument `<email>` was not provided" {
exit 0
} default {
exit 1
diff --git a/tests/main/op-remove-retry/task.yaml b/tests/main/op-remove-retry/task.yaml
index df1cb4945c..1c85e17451 100644
--- a/tests/main/op-remove-retry/task.yaml
+++ b/tests/main/op-remove-retry/task.yaml
@@ -8,7 +8,7 @@ execute: |
wait_for_service(){
local service_name=$1
local state=$2
- while ! systemctl show -p ActiveState $service_name | grep -q "ActiveState=$state"; do sleep 1; done
+ while ! systemctl show -p ActiveState $service_name | grep -q "ActiveState=$state"; do systemctl status $service_name || true; sleep 1; done
}
wait_for_remove_state(){
local state=$1
@@ -41,4 +41,4 @@ execute: |
# cleanup umount blocker
systemctl stop unmount-blocker
- wait_for_service unmount-blocker inactive
+
diff --git a/tests/main/prepare-image-grub/task.yaml b/tests/main/prepare-image-grub/task.yaml
index 39817ae074..6dce94a255 100644
--- a/tests/main/prepare-image-grub/task.yaml
+++ b/tests/main/prepare-image-grub/task.yaml
@@ -1,5 +1,6 @@
summary: Check that prepare-image works for grub-systems
systems: [-ubuntu-core-16-64]
+backends: [-autopkgtest]
# TODO: use the real stores with proper assertions fully as well once possible
environment:
ROOT: /tmp/root
@@ -10,7 +11,7 @@ environment:
UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_SNAPS: 1
prepare: |
. $TESTSLIB/store.sh
- setup_store fake-w-assert-fallback $STORE_DIR
+ setup_store fake $STORE_DIR
restore: |
. $TESTSLIB/store.sh
teardown_store fake $STORE_DIR
diff --git a/tests/main/refresh-devmode/task.yaml b/tests/main/refresh-devmode/task.yaml
index 9f782a5c6b..bcac9db3d2 100644
--- a/tests/main/refresh-devmode/task.yaml
+++ b/tests/main/refresh-devmode/task.yaml
@@ -1,5 +1,5 @@
summary: Check that the refresh command works.
-
+systems: [-ubuntu-core-16-64]
details: |
These tests exercise the refresh command using different store backends.
The concrete store to be used is controlled with the STORE_TYPE environment
@@ -14,7 +14,7 @@ environment:
SNAP_VERSION_PATTERN: \d+\.\d+\+fake1
BLOB_DIR: $(pwd)/fake-store-blobdir
STORE_TYPE/fake: fake
- STORE_TYPE/staging: staging
+# STORE_TYPE/staging: staging
STORE_TYPE/production: production
prepare: |
diff --git a/tests/main/refresh/task.yaml b/tests/main/refresh/task.yaml
index 1f887643ed..3e929ba8b5 100644
--- a/tests/main/refresh/task.yaml
+++ b/tests/main/refresh/task.yaml
@@ -1,5 +1,5 @@
summary: Check that the refresh command works.
-
+systems: [-ubuntu-core-16-64]
details: |
These tests exercise the refresh command using different store backends.
The concrete store to be used is controlled with the STORE_TYPE environment
@@ -14,7 +14,7 @@ environment:
SNAP_VERSION_PATTERN: \d+\.\d+\+fake1
BLOB_DIR: $(pwd)/fake-store-blobdir
STORE_TYPE/fake: fake
- STORE_TYPE/staging: staging
+# STORE_TYPE/staging: staging
STORE_TYPE/production: production
prepare: |
diff --git a/tests/main/revert-devmode/task.yaml b/tests/main/revert-devmode/task.yaml
index b63b10587f..3f0c6a79fc 100644
--- a/tests/main/revert-devmode/task.yaml
+++ b/tests/main/revert-devmode/task.yaml
@@ -1,8 +1,8 @@
summary: Check that revert of a snap in devmode restores devmode
-
+systems: [-ubuntu-core-16-64]
environment:
STORE_TYPE/fake: fake
- STORE_TYPE/staging: staging
+# STORE_TYPE/staging: staging
STORE_TYPE/production: production
BLOB_DIR: $(pwd)/fake-store-blobdir
diff --git a/tests/main/revert/task.yaml b/tests/main/revert/task.yaml
index 4d4f6c8166..f11ba6a04b 100644
--- a/tests/main/revert/task.yaml
+++ b/tests/main/revert/task.yaml
@@ -1,8 +1,8 @@
summary: Check that revert works.
-
+systems: [-ubuntu-core-16-64]
environment:
STORE_TYPE/fake: fake
- STORE_TYPE/staging: staging
+# STORE_TYPE/staging: staging
STORE_TYPE/production: production
BLOB_DIR: $(pwd)/fake-store-blobdir
diff --git a/tests/main/snapctl/task.yaml b/tests/main/snapctl/task.yaml
index 5fe4bf0907..8d3f6267a6 100644
--- a/tests/main/snapctl/task.yaml
+++ b/tests/main/snapctl/task.yaml
@@ -72,6 +72,7 @@ execute: |
if ! printf "GET /v2/snaps HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket | grep "200 OK"; then
echo "Expected snapd API to be available on the snapd socket"
+ echo "Got: $(curl -s --unix-socket /run/snapd.socket http:/v2/snaps)"
exit 1
fi
diff --git a/tests/main/ubuntu-core-update-rollback-stresstest/task.yaml b/tests/main/ubuntu-core-update-rollback-stresstest/task.yaml
index 916e5d70d0..4d7acc2d69 100644
--- a/tests/main/ubuntu-core-update-rollback-stresstest/task.yaml
+++ b/tests/main/ubuntu-core-update-rollback-stresstest/task.yaml
@@ -6,18 +6,31 @@ execute: |
wait_boot_ok() {
echo "Waiting for boot-ok to finish"
while ! systemctl status snapd.boot-ok|grep SUCCESS; do
+ # debug output
+ systemctl status snapd.boot-ok || true
sleep 1
done
}
check_boot() {
+ # debug output
+ grub-editenv list
grub-editenv list | grep "snap_core=ubuntu-core_$(cat nextBoot).snap"
}
store_next_boot() {
snap list|grep ubuntu-core|tr -s " "|cut -f3 -d' ' > nextBoot
+ # debug output
+ snap list
+ echo "nextBoot:"
+ cat nextBoot
}
echo "Install/revert a couple of times and see if stuff breaks"
if [ "$SPREAD_REBOOT" = "0" ]; then
snap list|grep ubuntu-core|tr -s " "|cut -f3 -d' ' > firstBoot
+ # debug output
+ snap list
+ echo "firstBoot:"
+ cat firstBoot
+ echo "Install new ubuntu-core"
snap install --dangerous /var/lib/snapd/snaps/ubuntu-core_$(cat firstBoot).snap
store_next_boot
REBOOT
diff --git a/tests/main/writable-areas/task.yaml b/tests/main/writable-areas/task.yaml
index 112b79cd9d..4c93e18a04 100644
--- a/tests/main/writable-areas/task.yaml
+++ b/tests/main/writable-areas/task.yaml
@@ -20,18 +20,9 @@ execute: |
echo "Waiting for data writer service to finish..."
unit="snap.data-writer.service.service"
- while true; do
- code=$(sudo systemctl show -p ExecMainCode $unit | sed 's/.*=\([0-9]\+\)/\1/')
- # The main code will be 0 until the service is no longer running
- if [ $code -ne 0 ]; then
- status=$(sudo systemctl show -p ExecMainStatus $unit | sed 's/.*=\([0-9]\+\)/\1/')
- if [ $status -ne 0 ]; then
- echo "Service exited $status"
- exit 1
- fi
- break
- fi
- sleep 0.1
+ while ! systemctl status $unit | grep -q "Writing to SNAP_USER_DATA"; do
+ systemctl status $unit || true
+ sleep 1
done
echo "Services can write to writable areas"
diff --git a/integration-tests/manual-tests.md b/tests/manual-tests.md
index 787ad4a29d..0217c30057 100644
--- a/integration-tests/manual-tests.md
+++ b/tests/manual-tests.md
@@ -213,57 +213,6 @@ $ snap remove bluez
TPM Version: 01010000
Manufacturer Info: 57454300
-# Test upower-observe interface with upower
-
-1. Create a upower snap:
-
- name: test-upower-observe
- version: 0
- summary: Test upower-observe interface
- description: |
- Test upower-observe interface on classic
- architectures:
- - amd64
- confinement: strict
-
- apps:
- test-upower-observe:
- command: usr/bin/upower
- plugs: [ upower-observe ]
-
- parts:
- mypackages:
- plugin: nil
- stage-packages:
- - upower
-
-2. Install the snap:
-
- $ sudo snap install ./test-upower-observe_0_amd64.snap
-
-3. Dump upower information
-
- $ test-upower-observe -h
- Usage:
- upower [OPTION...] UPower tool
- ...
- $ test-upower-observe --dump # output here is from a VM
- Device: /org/freedesktop/UPower/devices/DisplayDevice
- power supply: no
- updated: Tue Aug 30 15:48:04 2016 (63174 seconds ago)
- has history: no
- has statistics: no
- unknown
- warning-level: none
- icon-name: ''
-
- Daemon:
- daemon-version: 0.99.4
- on-battery: no
- lid-is-closed: no
- lid-is-present: no
- critical-action: HybridSleep
-
# Test fwupd interface with uefi-fw-tools
1. Ensure your BIOS support UEFI firmware upgrading via UEFI capsule format