diff --git a/.github/labeler.yml b/.github/labeler.yml
index a08c77bdbd5..de544b4917f 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -3,11 +3,6 @@
# - https://github.com/actions/labeler/issues/112
# - https://github.com/actions/labeler/issues/104
-# Add 'Run nested -auto-' label to either any change on nested lib or nested test
-Run nested -auto-:
- - tests/lib/nested.sh
- - tests/nested/**/*
-
# Add 'Needs Documentation -auto-' label to indicate a change needs changes in the docs
Needs Documentation -auto-:
- cmd/snap/**/*"
diff --git a/.github/workflows/actions/download-install-debian-deps/action.yaml b/.github/workflows/actions/download-install-debian-deps/action.yaml
new file mode 100644
index 00000000000..b0d3883c648
--- /dev/null
+++ b/.github/workflows/actions/download-install-debian-deps/action.yaml
@@ -0,0 +1,28 @@
+name: 'Download cached dependencies and install Debian deps'
+description: 'Download and install Debian dependencies'
+inputs:
+ snapd-src-dir:
+ description: 'The snapd source code directory'
+ required: true
+ type: string
+
+runs:
+ using: "composite"
+ steps:
+ - name: Download Debian dependencies
+ uses: actions/download-artifact@v4
+ with:
+ name: debian-dependencies
+ path: ./debian-deps/
+
+ - name: Copy dependencies
+ shell: bash
+ run: |
+ test -f ./debian-deps/cached-apt.tar
+ sudo tar xvf ./debian-deps/cached-apt.tar -C /
+
+ - name: Install Debian dependencies
+ shell: bash
+ run: |
+ sudo apt update
+ sudo apt build-dep -y "${{ inputs.snapd-src-dir }}"
diff --git a/.github/workflows/snap-builds.yaml b/.github/workflows/snap-builds.yaml
new file mode 100644
index 00000000000..b66a9f180d9
--- /dev/null
+++ b/.github/workflows/snap-builds.yaml
@@ -0,0 +1,83 @@
+on:
+ workflow_call:
+ inputs:
+ runs-on:
+ description: 'A json list of tags to indicate which runner to use'
+ required: true
+ type: string
+ toolchain:
+ description: 'The go toolchain to use {default, FIPS}'
+ required: true
+ type: string
+ variant:
+ description: 'The type of snapd build {pristine, test}'
+ required: true
+ type: string
+
+jobs:
+ snap-builds:
+ runs-on: ${{ fromJSON(inputs.runs-on) }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set artifact name
+ id: set_artifact_name
+ run: |
+ postfix="${{ inputs.toolchain }}-${{ inputs.variant }}"
+ if grep -iq "arm64" <<<"${{ inputs.runs-on }}"; then
+ echo "artifact_name=snap-files-arm64-${postfix}" >> $GITHUB_OUTPUT
+ else
+ echo "artifact_name=snap-files-amd64-${postfix}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Select Go toolchain
+ run: |
+ case "${{ inputs.toolchain }}" in
+ default)
+ rm -f fips-build
+ ;;
+ FIPS)
+ touch fips-build
+ ;;
+ *)
+ echo "unknown toolchain ${{ inputs.toolchain }}"
+ exit 1
+ ;;
+ esac
+ case "${{ inputs.variant }}" in
+ pristine)
+ rm -f test-build
+ ;;
+ test)
+ touch test-build
+ ;;
+ esac
+
+ - name: Build snapd snap
+ uses: snapcore/action-build@v1
+ with:
+ snapcraft-channel: 8.x/stable
+ snapcraft-args: --verbose
+
+ - name: Check built artifact
+ run: |
+ unsquashfs snapd*.snap meta/snap.yaml usr/lib/snapd/
+ if cat squashfs-root/meta/snap.yaml | grep -q "version:.*dirty.*"; then
+ echo "PR produces dirty snapd snap version"
+ cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt
+ exit 1
+ elif cat squashfs-root/usr/lib/snapd/info | grep -q "VERSION=.*dirty.*"; then
+ echo "PR produces dirty internal snapd info version"
+ cat squashfs-root/usr/lib/snapd/info
+ cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt
+ exit 1
+ fi
+
+ - name: Uploading snapd snap artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ steps.set_artifact_name.outputs.artifact_name }}
+ path: "*.snap"
diff --git a/.github/workflows/spread-tests.yaml b/.github/workflows/spread-tests.yaml
new file mode 100644
index 00000000000..2fabd866bd7
--- /dev/null
+++ b/.github/workflows/spread-tests.yaml
@@ -0,0 +1,293 @@
+on:
+ workflow_call:
+ inputs:
+ runs-on:
+ description: 'A json list of tags to indicate which runner to use'
+ required: true
+ type: string
+ group:
+ description: 'The name of the group of backends, systems, tests, and rules'
+ required: true
+ type: string
+ backend:
+ description: 'The spread backend to use (for possible values, check spread.yaml)'
+ required: true
+ type: string
+ systems:
+ description: 'The spread system(s) to use (for possible values, check spread.yaml). If more than one, separate them with a space'
+ required: true
+ type: string
+ tasks:
+ description: 'The spread tasks to run. It may be a space-separated list and may contain directories of many tasks or individual ones'
+ required: true
+ type: string
+ rules:
+ description: 'The rule .yaml file to use (found under tests/lib/spread/rules) for test discovery'
+ required: true
+ type: string
+
+
+jobs:
+ run-spread:
+ runs-on: ${{ fromJSON(inputs.runs-on) }}
+ steps:
+ - name: Cleanup job workspace
+ id: cleanup-job-workspace
+ run: |
+ rm -rf "${{ github.workspace }}"
+ mkdir "${{ github.workspace }}"
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ # spread uses tags as delta reference
+ fetch-depth: 0
+
+ - name: Get previous attempt
+ id: get-previous-attempt
+ run: |
+ echo "previous_attempt=$(( ${{ github.run_attempt }} - 1 ))" >> $GITHUB_OUTPUT
+ shell: bash
+
+ - name: Get previous cache
+ uses: actions/cache@v4
+ with:
+ path: "${{ github.workspace }}/.test-results"
+ key: "${{ github.job }}-results-${{ github.run_id }}-${{ inputs.group }}-${{ steps.get-previous-attempt.outputs.previous_attempt }}"
+
+ - name: Prepare test results env and vars
+ id: prepare-test-results-env
+ run: |
+ # Create test results directories and save vars
+ TEST_RESULTS_DIR="${{ github.workspace }}/.test-results"
+ echo "TEST_RESULTS_DIR=$TEST_RESULTS_DIR" >> $GITHUB_ENV
+
+ # Save the var with the failed tests file
+ echo "FAILED_TESTS_FILE=$TEST_RESULTS_DIR/failed-tests" >> $GITHUB_ENV
+
+ # Make sure the test results dirs are created
+ # This step has to be after the cache is restored
+ mkdir -p "$TEST_RESULTS_DIR"
+
+ - name: Prepare nested env vars
+ if: contains(github.event.pull_request.labels.*.name, 'Run nested') && startsWith(matrix.group, 'nested-')
+ run: |
+ echo "RUN_NESTED=true" >> "$GITHUB_ENV"
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v41.0.0
+
+ - name: Save changes files
+ run: |
+ CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"
+ echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV
+ echo "The changed files found are: $CHANGED_FILES"
+
+ - name: Check failed tests to run
+ if: "!contains(github.event.pull_request.labels.*.name, 'Run all')"
+ run: |
+ # Save previous failed test results in FAILED_TESTS env var
+ FAILED_TESTS=""
+ if [ -f "$FAILED_TESTS_FILE" ]; then
+ echo "Failed tests file found"
+ FAILED_TESTS="$(cat $FAILED_TESTS_FILE)"
+ if [ -n "$FAILED_TESTS" ]; then
+ echo "Failed tests to run: $FAILED_TESTS"
+ echo "FAILED_TESTS=$FAILED_TESTS" >> $GITHUB_ENV
+ fi
+ fi
+
+ - name: Setup run tests variable
+ if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')"
+ run: |
+ RUN_TESTS=""
+ SUGGESTED_TESTS=""
+ # Save previous failed test results in FAILED_TESTS env var
+ if [ -n "$FAILED_TESTS" ]; then
+ RUN_TESTS="$FAILED_TESTS"
+ else
+ CHANGES_PARAM=""
+ for CHANGE in $CHANGED_FILES; do
+ CHANGES_PARAM="$CHANGES_PARAM -c $CHANGE"
+ done
+ for SYSTEM in ${{ inputs.systems }}; do
+ # Configure parameters to run tests based on current changes
+ # The tests are just filtered when the change is a PR
+ # When 'Run Nested' label is added in a PR, all the nested tests have to be executed
+ if [ -z "${{ github.event.number }}" ] || [ "$RUN_NESTED" = 'true' ]; then
+ for TESTS in ${{ inputs.tasks }}; do
+ RUN_TESTS="$RUN_TESTS ${{ inputs.backend }}:$SYSTEM:$TESTS"
+ done
+ else
+ NEW_TESTS="$(./tests/lib/external/snapd-testing-tools/utils/spread-filter -r ./tests/lib/spread/rules/${{ inputs.rules }}.yaml -p "${{ inputs.backend }}:$SYSTEM" $CHANGES_PARAM)"
+ if [ -z "$RUN_TESTS" ]; then
+ RUN_TESTS="$NEW_TESTS"
+ else
+ RUN_TESTS="$RUN_TESTS $NEW_TESTS"
+ fi
+ fi
+ done
+ fi
+ echo RUN_TESTS="$RUN_TESTS" >> $GITHUB_ENV
+
+ - name: Setup grafana parameters
+ if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')"
+ run: |
+ # Configure parameters to filter logs (these logs are sent read by grafana agent)
+ CHANGE_ID="${{ github.event.number }}"
+ if [ -z "$CHANGE_ID" ]; then
+ CHANGE_ID="main"
+ fi
+ FILTERED_LOG_FILE="spread_${CHANGE_ID}_n${{ github.run_attempt }}.filtered.log"
+ # The log-filter tool is used to filter the spread logs to be stored
+ echo FILTER_PARAMS="-o $FILTERED_LOG_FILE -e Debug -e WARNING: -f Failed=NO_LINES -f Error=NO_LINES" >> $GITHUB_ENV
+ echo FILTERED_LOG_FILE="$FILTERED_LOG_FILE" >> $GITHUB_ENV
+
+ # Add start line to filtered log
+ echo "GRAFANA START: pr ${CHANGE_ID} attempt ${{ github.run_attempt }} run ${{ github.run_id }} group ${{ inputs.group }}" > "$FILTERED_LOG_FILE"
+
+ - name: Download built snap (amd64)
+ uses: actions/download-artifact@v4
+ if: "!contains(inputs.group, '-arm64') && !endsWith(inputs.group, '-fips')"
+ with:
+ name: snap-files-amd64-default-test
+ # eg. snapd_1337.2.65.1+git97.gd35b459_amd64.snap
+ pattern: snapd_1337.*.snap
+ path: "${{ github.workspace }}/built-snap"
+
+ - name: Download built snap (arm64)
+ if: "contains(inputs.group, '-arm64') && !endsWith(inputs.group, '-fips')"
+ uses: actions/download-artifact@v4
+ with:
+ name: snap-files-arm64-default-test
+ pattern: snapd_1337.*.snap
+ # eg. snapd_1337.2.65.1+git97.gd35b459_amd64.snap
+ path: "${{ github.workspace }}/built-snap"
+
+ - name: Download built FIPS snap (amd64)
+ uses: actions/download-artifact@v4
+ # eg. ubuntu-fips
+ if: "!contains(inputs.group, '-arm64') && endsWith(inputs.group, '-fips')"
+ with:
+ name: snap-files-amd64-FIPS-test
+ # eg. snapd_1337.2.65.1+git97.gd35b459-fips_amd64.snap
+ pattern: snapd_1337.*-fips_*.snap
+ path: "${{ github.workspace }}/built-snap"
+
+ - name: Rename imported snap
+ run: |
+ for snap in built-snap/snapd_1337.*.snap; do
+ mv -v "${snap}" "${snap}.keep"
+ done
+
+ - name: Run spread tests
+ if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')"
+ env:
+ SPREAD_GOOGLE_KEY: ${{ secrets.SPREAD_GOOGLE_KEY }}
+ run: |
+ # Register a problem matcher to highlight spread failures
+ echo "::add-matcher::.github/spread-problem-matcher.json"
+ set -x
+ SPREAD=spread
+ if [[ "${{ inputs.group }}" =~ nested- ]]; then
+ export NESTED_BUILD_SNAPD_FROM_CURRENT=true
+ export NESTED_ENABLE_KVM=true
+ fi
+
+ export SPREAD_USE_PREBUILT_SNAPD_SNAP=true
+
+ if [[ "${{ inputs.systems }}" =~ amazon-linux-2023 ]]; then
+ # Amazon Linux 2023 has no xdelta, however we cannot disable
+ # xdelta on a per-target basis as it's used in the repack section
+ # of spread.yaml, which is shared by all targets, so all systems
+ # in this batch will not use delta for transferring project data
+ echo "Disabling xdelta support"
+ export NO_DELTA=1
+ fi
+
+ # Add openstack backend definition to spread.yaml
+ if [ "${{ inputs.backend }}" = openstack ]; then
+ ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml
+ fi
+
+ # This could be the case when either there are not systems for a group or
+ # the list of tests to run is empty
+ if [ -z "$RUN_TESTS" ]; then
+ echo "No tests to run, exiting..."
+ exit 0
+ fi
+
+ if "$SPREAD" -list $RUN_TESTS 2>&1 | grep -q "nothing matches provider filter"; then
+ echo "No tests to run, exiting..."
+ exit 0
+ fi
+
+ # Run spread tests
+ # "pipefail" ensures that a non-zero status from the spread is
+ # propagated; and we use a subshell as this option could trigger
+ # undesired changes elsewhere
+ echo "Running command: $SPREAD $RUN_TESTS"
+ (set -o pipefail; $SPREAD -no-debug-output -logs spread-logs $RUN_TESTS | PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-filter $FILTER_PARAMS | tee spread.log)
+
+ - name: Upload spread logs
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: spread-logs-${{ inputs.systems }}
+ path: "spread-logs/*.log"
+ if-no-files-found: ignore
+
+ - name: Discard spread workers
+ if: always()
+ run: |
+ shopt -s nullglob;
+ for r in .spread-reuse.*.yaml; do
+ spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')";
+ done
+
+ - name: Report spread errors
+ if: always()
+ run: |
+ if [ -e spread.log ]; then
+ echo "Running spread log analyzer"
+ ACTIONS_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}"
+ PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json --cut 1 >/dev/null
+ while IFS= read -r line; do
+ if [ ! -z "$line" ]; then
+ echo "Adding failed test line to filtered log"
+ echo "GRAFANA FAILED: $line $ACTIONS_URL" | tee -a "$FILTERED_LOG_FILE"
+ fi
+ done <<< $(jq -r '.[] | select( .type == "info" ) | select( .info_type == "Error" ) | "\(.verb) \(.task)"' spread-results.json)
+ else
+ echo "No spread log found, skipping errors reporting"
+ fi
+
+ - name: Analyze spread test results
+ if: always()
+ run: |
+ if [ -f spread.log ]; then
+ echo "Running spread log parser"
+ PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json
+
+ # Add openstack backend definition to spread.yaml
+ if [ "${{ inputs.backend }}" = openstack ]; then
+ ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml
+ fi
+
+ echo "Running spread log analyzer"
+ PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-analyzer list-reexecute-tasks "$RUN_TESTS" spread-results.json > "$FAILED_TESTS_FILE"
+
+ echo "List of failed tests saved"
+ cat "$FAILED_TESTS_FILE"
+ else
+ echo "No spread log found, saving empty list of failed tests"
+ touch "$FAILED_TESTS_FILE"
+ fi
+
+ - name: Save spread test results to cache
+ if: always()
+ uses: actions/cache/save@v4
+ with:
+ path: "${{ github.workspace }}/.test-results"
+ key: "${{ github.job }}-results-${{ github.run_id }}-${{ inputs.group }}-${{ github.run_attempt }}"
diff --git a/.github/workflows/static-checks.yaml b/.github/workflows/static-checks.yaml
new file mode 100644
index 00000000000..f1a2bd19837
--- /dev/null
+++ b/.github/workflows/static-checks.yaml
@@ -0,0 +1,126 @@
+on:
+ workflow_call:
+ inputs:
+ runs-on:
+ description: 'A tag to indicate which runner to use'
+ required: true
+ type: string
+ gochannel:
+ description: 'The snap store channel to use to install the go snap'
+ required: true
+ type: string
+
+jobs:
+ static-checks:
+ runs-on: ${{ inputs.runs-on }}
+ env:
+ GOPATH: ${{ github.workspace }}
+ # Set PATH to ignore the load of magic binaries from /usr/local/bin And
+ # to use the go snap automatically. Note that we install go from the
+ # snap in a step below. Without this we get the GitHub-controlled latest
+ # version of go.
+ PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin
+ GOROOT: ""
+ GITHUB_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }}
+ BASE_REF: ${{ github.base_ref }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ # needed for git commit history
+ fetch-depth: 0
+ # NOTE: checkout the code in a fixed location, even for forks, as this
+ # is relevant for go's import system.
+ path: ./src/github.com/snapcore/snapd
+
+ # Fetch base ref, needed for golangci-lint
+ - name: Fetching base ref ${{ github.base_ref }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd
+ git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
+
+ - name: Download and install Debian dependencies
+ # Github does not allow variables in "uses"; this has to be a hard-coded path
+ uses: ./src/github.com/snapcore/snapd/.github/workflows/actions/download-install-debian-deps
+ with:
+ snapd-src-dir: "${{ github.workspace }}/src/github.com/snapcore/snapd"
+
+ # golang latest ensures things work on the edge
+ - name: Install the go snap
+ run: |
+ sudo snap install --classic --channel=${{ inputs.gochannel }} go
+
+ - name: Install ShellCheck as a snap
+ run: |
+ sudo apt-get remove --purge shellcheck
+ sudo snap install shellcheck
+
+ - name: Get C vendoring
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd/c-vendor && ./vendor.sh
+
+ - name: Install golangci-lint snap
+ run: |
+ sudo snap install --classic golangci-lint
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v41.0.0
+ with:
+ path: ./src/github.com/snapcore/snapd
+
+ - name: Save changes files
+ run: |
+ CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"
+ echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV
+ echo "The changed files found are: $CHANGED_FILES"
+
+ - name: Run static checks
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ # run gofmt checks only with the latest stable Go
+ if [ "${{ matrix.gochannel }}" != "latest/stable" ]; then
+ export SKIP_GOFMT=1
+ echo "Formatting checks will be skipped due to the use of Go version ${{ inputs.gochannel }}"
+ fi
+ sudo apt-get install -y python3-yamlordereddictloader
+ ./run-checks --static
+
+ - name: Cache prebuilt indent
+ id: cache-indent-bin
+ uses: actions/cache@v4
+ with:
+ path: indent-bin
+ key: ${{ runner.os }}-indent-2.2.13
+
+ # build indent 2.2.13 which has this patch
+ # https://git.savannah.gnu.org/cgit/indent.git/commit/?id=22b83d68e9a8b429590f42920e9f473a236123cf
+ - name: Build indent 2.2.13
+ if: steps.cache-indent-bin.outputs.cache-hit != 'true'
+ run: |
+ sudo apt install texinfo autopoint
+ curl -O https://ftp.gnu.org/gnu/indent/indent-2.2.13.tar.xz
+ tar xvf indent-2.2.13.tar.xz
+ cd indent-2.2.13
+ autoreconf -if
+ # set prefix in case we want to pack to tar/extract into system
+ ./configure --prefix=/opt/indent
+ make -j
+ make install DESTDIR=${{ github.workspace }}/indent-bin
+ find ${{ github.workspace }}/indent-bin -ls
+
+ - name: Check C source code formatting
+ run: |
+ set -x
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/
+ ./autogen.sh
+ # apply formatting
+ PATH=${{ github.workspace }}/indent-bin/opt/indent/bin:$PATH make fmt
+ set +x
+ if [ -n "$(git diff --stat)" ]; then
+ git diff
+ echo "C files are not fomratted correctly, run 'make fmt'"
+ echo "make sure to have clang-format and indent 2.2.13+ installed"
+ exit 1
+ fi
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 1d3c24573a4..7f5d43669c6 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -11,72 +11,36 @@ concurrency:
jobs:
snap-builds:
- runs-on: ubuntu-22.04
+ uses: ./.github/workflows/snap-builds.yaml
+ with:
+ runs-on: ${{ matrix.runs-on }}
+ toolchain: ${{ matrix.toolchain }}
+ variant: ${{ matrix.variant }}
strategy:
matrix:
+ runs-on:
+ - '["ubuntu-22.04"]'
+ # Tags to identify the self-hosted runners to use from
+ # internal runner collection. See internal self-hosted
+ # runners doc for the complete list of options.
+ - '["self-hosted", "Linux", "jammy", "ARM64", "large"]'
toolchain:
- default
- FIPS
- version:
+ variant:
# test version is a build of snapd with test keys and should
# only be installed by test runners. The pristine versions
# are the build that should be installed by human users.
- pristine
- test
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Select Go toolchain
- run: |
- case "${{ matrix.toolchain }}" in
- default)
- rm -f fips-build
- ;;
- FIPS)
- touch fips-build
- ;;
- *)
- echo "unknown toolchain ${{ matrix.toolchain }}"
- exit 1
- ;;
- esac
- case "${{ matrix.version }}" in
- pristine)
- rm -f test-build
- ;;
- test)
- touch test-build
- ;;
- esac
-
- - name: Build snapd snap
- uses: snapcore/action-build@v1
- with:
- snapcraft-channel: 8.x/stable
- snapcraft-args: --verbose
-
- - name: Check built artifact
- run: |
- unsquashfs snapd*.snap meta/snap.yaml usr/lib/snapd/
- if cat squashfs-root/meta/snap.yaml | grep -q "version:.*dirty.*"; then
- echo "PR produces dirty snapd snap version"
- cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt
- exit 1
- elif cat squashfs-root/usr/lib/snapd/info | grep -q "VERSION=.*dirty.*"; then
- echo "PR produces dirty internal snapd info version"
- cat squashfs-root/usr/lib/snapd/info
- cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt
- exit 1
- fi
-
- - name: Uploading snapd snap artifact
- uses: actions/upload-artifact@v4
- with:
- name: snap-files-${{ matrix.toolchain }}-${{ matrix.version }}
- path: "*.snap"
+ # Exclude building everything for ARM but the version for testing
+ # to keep the number of builds down as we currently don't have a
+ # clear need for these excluded builds.
+ exclude:
+ - runs-on: '["self-hosted", "Linux", "jammy", "ARM64", "large"]'
+ toolchain: FIPS
+ - runs-on: '["self-hosted", "Linux", "jammy", "ARM64", "large"]'
+ variant: pristine
cache-build-deps:
runs-on: ubuntu-20.04
@@ -116,18 +80,11 @@ jobs:
path: ./cached-apt.tar
static-checks:
- runs-on: ubuntu-latest
+ uses: ./.github/workflows/static-checks.yaml
needs: [cache-build-deps]
- env:
- GOPATH: ${{ github.workspace }}
- # Set PATH to ignore the load of magic binaries from /usr/local/bin And
- # to use the go snap automatically. Note that we install go from the
- # snap in a step below. Without this we get the GitHub-controlled latest
- # version of go.
- PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin
- GOROOT: ""
- GITHUB_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }}
- BASE_REF: ${{ github.base_ref }}
+ with:
+ runs-on: ubuntu-latest
+ gochannel: ${{ matrix.gochannel }}
strategy:
# we cache successful runs so it's fine to keep going
@@ -137,117 +94,6 @@ jobs:
- 1.18
- latest/stable
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- # needed for git commit history
- fetch-depth: 0
- # NOTE: checkout the code in a fixed location, even for forks, as this
- # is relevant for go's import system.
- path: ./src/github.com/snapcore/snapd
-
- # Fetch base ref, needed for golangci-lint
- - name: Fetching base ref ${{ github.base_ref }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd
- git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
-
- - name: Download Debian dependencies
- uses: actions/download-artifact@v4
- with:
- name: debian-dependencies
- path: ./debian-deps/
-
- - name: Copy dependencies
- run: |
- test -f ./debian-deps/cached-apt.tar
- sudo tar xvf ./debian-deps/cached-apt.tar -C /
-
- - name: Install Debian dependencies
- run: |
- sudo apt update
- sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd
-
- # golang latest ensures things work on the edge
- - name: Install the go snap
- run: |
- sudo snap install --classic --channel=${{ matrix.gochannel }} go
-
- - name: Install ShellCheck as a snap
- run: |
- sudo apt-get remove --purge shellcheck
- sudo snap install shellcheck
-
- - name: Get C vendoring
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/c-vendor && ./vendor.sh
-
- - name: Install golangci-lint snap
- run: |
- sudo snap install --classic golangci-lint
-
- - name: Get changed files
- id: changed-files
- uses: tj-actions/changed-files@v41.0.0
- with:
- path: ./src/github.com/snapcore/snapd
-
- - name: Save changes files
- run: |
- CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"
- echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV
- echo "The changed files found are: $CHANGED_FILES"
-
- - name: Run static checks
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- # run gofmt checks only with the latest stable Go
- if [ "${{ matrix.gochannel }}" != "latest/stable" ]; then
- export SKIP_GOFMT=1
- echo "Formatting checks will be skipped due to the use of Go version ${{ matrix.gochannel }}"
- fi
- sudo apt-get install -y python3-yamlordereddictloader
- ./run-checks --static
-
- - name: Cache prebuilt indent
- id: cache-indent-bin
- uses: actions/cache@v4
- with:
- path: indent-bin
- key: ${{ runner.os }}-indent-2.2.13
-
- # build indent 2.2.13 which has this patch
- # https://git.savannah.gnu.org/cgit/indent.git/commit/?id=22b83d68e9a8b429590f42920e9f473a236123cf
- - name: Build indent 2.2.13
- if: steps.cache-indent-bin.outputs.cache-hit != 'true'
- run: |
- sudo apt install texinfo autopoint
- curl -O https://ftp.gnu.org/gnu/indent/indent-2.2.13.tar.xz
- tar xvf indent-2.2.13.tar.xz
- cd indent-2.2.13
- autoreconf -if
- # set prefix in case we want to pack to tar/extract into system
- ./configure --prefix=/opt/indent
- make -j
- make install DESTDIR=${{ github.workspace }}/indent-bin
- find ${{ github.workspace }}/indent-bin -ls
-
- - name: Check C source code formatting
- run: |
- set -x
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/
- ./autogen.sh
- # apply formatting
- PATH=${{ github.workspace }}/indent-bin/opt/indent/bin:$PATH make fmt
- set +x
- if [ -n "$(git diff --stat)" ]; then
- git diff
- echo "C files are not fomratted correctly, run 'make fmt'"
- echo "make sure to have clang-format and indent 2.2.13+ installed"
- exit 1
- fi
-
branch-static-checks:
runs-on: ubuntu-latest
needs: [cache-build-deps]
@@ -270,16 +116,12 @@ jobs:
shell: bash
unit-tests:
+ uses: ./.github/workflows/unit-tests.yaml
needs: [static-checks]
- runs-on: ubuntu-22.04
- env:
- GOPATH: ${{ github.workspace }}
- # Set PATH to ignore the load of magic binaries from /usr/local/bin And
- # to use the go snap automatically. Note that we install go from the
- # snap in a step below. Without this we get the GitHub-controlled latest
- # version of go.
- PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin
- GOROOT: ""
+ with:
+ runs-on: ubuntu-22.04
+ gochannel: ${{ matrix.gochannel }}
+ unit-scenario: ${{ matrix.unit-scenario }}
strategy:
# we cache successful runs so it's fine to keep going
fail-fast: false
@@ -290,93 +132,14 @@ jobs:
unit-scenario:
- normal
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- # needed for git commit history
- fetch-depth: 0
- # NOTE: checkout the code in a fixed location, even for forks, as this
- # is relevant for go's import system.
- path: ./src/github.com/snapcore/snapd
-
- # Fetch base ref, needed for golangci-lint
- - name: Fetching base ref ${{ github.base_ref }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd
- git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
-
- - name: Download Debian dependencies
- uses: actions/download-artifact@v4
- with:
- name: debian-dependencies
- path: ./debian-deps/
-
- - name: Copy dependencies
- run: |
- test -f ./debian-deps/cached-apt.tar
- sudo tar xvf ./debian-deps/cached-apt.tar -C /
-
- - name: Install Debian dependencies
- run: |
- sudo apt update
- sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd
-
- # golang latest ensures things work on the edge
- - name: Install the go snap
- run: |
- sudo snap install --classic --channel=${{ matrix.gochannel }} go
-
- - name: Get deps
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/ && ./get-deps.sh
-
- - name: Build C
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/
- ./autogen.sh
- make -j$(nproc)
-
- - name: Build Go
- run: |
- go build github.com/snapcore/snapd/...
-
- - name: Test C
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make distcheck
-
- - name: Reset code coverage data
- run: |
- rm -rf ${{ github.workspace }}/.coverage/
- COVERAGE_OUT="${{ github.workspace }}/coverage/coverage-${{ matrix.unit-scenario}}.cov"
- echo "COVERAGE_OUT=$COVERAGE_OUT" >> $GITHUB_ENV
-
- - name: Test Go
- if: ${{ matrix.unit-scenario == 'normal' }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- ./run-checks --unit
-
- - name: Upload the coverage results
- if: ${{ matrix.gochannel != 'latest/stable' }}
- uses: actions/upload-artifact@v4
- with:
- include-hidden-files: true
- name: "coverage-files-${{ matrix.unit-scenario }}"
- path: "${{ github.workspace }}/coverage/coverage*.cov"
-
# TODO run unit tests of C code
unit-tests-special:
+ uses: ./.github/workflows/unit-tests.yaml
needs: [static-checks]
- runs-on: ubuntu-22.04
- env:
- GOPATH: ${{ github.workspace }}
- # Set PATH to ignore the load of magic binaries from /usr/local/bin And
- # to use the go snap automatically. Note that we install go from the
- # snap in a step below. Without this we get the GitHub-controlled latest
- # version of go.
- PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin
- GOROOT: ""
+ with:
+ runs-on: ubuntu-22.04
+ gochannel: ${{ matrix.gochannel }}
+ unit-scenario: ${{ matrix.unit-scenario }}
strategy:
# we cache successful runs so it's fine to keep going
fail-fast: false
@@ -392,125 +155,12 @@ jobs:
- race
- snapdusergo
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- # needed for git commit history
- fetch-depth: 0
- # NOTE: checkout the code in a fixed location, even for forks, as this
- # is relevant for go's import system.
- path: ./src/github.com/snapcore/snapd
-
- # Fetch base ref, needed for golangci-lint
- - name: Fetching base ref ${{ github.base_ref }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd
- git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
-
- - name: Download Debian dependencies
- uses: actions/download-artifact@v4
- with:
- name: debian-dependencies
- path: ./debian-deps/
-
- - name: Copy dependencies
- run: |
- test -f ./debian-deps/cached-apt.tar
- sudo tar xvf ./debian-deps/cached-apt.tar -C /
-
- - name: Install Debian dependencies
- run: |
- sudo apt update
- sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd
-
- # golang latest ensures things work on the edge
- - name: Install the go snap
- run: |
- sudo snap install --classic --channel=${{ matrix.gochannel }} go
-
- - name: Get deps
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/ && ./get-deps.sh
-
- - name: Build C
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/
- ./autogen.sh
- make -j$(nproc)
-
- - name: Build Go
- run: |
- go build github.com/snapcore/snapd/...
-
- - name: Test C
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make distcheck
-
- - name: Reset code coverage data
- run: |
- rm -rf ${{ github.workspace }}/.coverage/
- COVERAGE_OUT="${{ github.workspace }}/coverage/coverage-${{ matrix.unit-scenario}}.cov"
- echo "COVERAGE_OUT=$COVERAGE_OUT" >> $GITHUB_ENV
-
- - name: Test Go (SNAPD_DEBUG=1)
- if: ${{ matrix.unit-scenario == 'snapd_debug' }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- SKIP_DIRTY_CHECK=1 SNAPD_DEBUG=1 ./run-checks --unit
-
- - name: Test Go (withbootassetstesting)
- if: ${{ matrix.unit-scenario == 'withbootassetstesting' }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=withbootassetstesting ./run-checks --unit
-
- - name: Test Go (nosecboot)
- if: ${{ matrix.unit-scenario == 'nosecboot' }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- echo "Dropping github.com/snapcore/secboot"
- # use govendor remove so that a subsequent govendor sync does not
- # install secboot again
- # ${{ github.workspace }}/bin/govendor remove github.com/snapcore/secboot
- # ${{ github.workspace }}/bin/govendor remove +unused
- SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=nosecboot ./run-checks --unit
-
- - name: Test Go (faultinject)
- if: ${{ matrix.unit-scenario == 'faultinject' }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=faultinject ./run-checks --unit
-
- - name: Test Go (-race)
- if: ${{ matrix.unit-scenario == 'race' }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- SKIP_DIRTY_CHECK=1 GO_TEST_RACE=1 SKIP_COVERAGE=1 ./run-checks --unit
-
- - name: Test Go (snapdusergo)
- if: ${{ matrix.unit-scenario == 'snapdusergo' }}
- run: |
- cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
- SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=snapdusergo ./run-checks --unit
-
- - name: Upload the coverage results
- if: ${{ matrix.gochannel != 'latest/stable' && matrix.unit-scenario != 'race' }}
- uses: actions/upload-artifact@v4
- with:
- include-hidden-files: true
- name: "coverage-files-${{ matrix.unit-scenario }}"
- path: "${{ github.workspace }}/coverage/coverage*.cov"
-
-
unit-tests-cross-distro:
+ uses: ./.github/workflows/unit-tests-cross-distro.yaml
needs: [static-checks]
- env:
- # Set PATH to ignore the load of magic binaries from /usr/local/bin And
- # to use the go snap automatically. Note that we install go from the
- # snap in a step below. Without this we get the GitHub-controlled latest
- # version of go.
- PATH: /usr/sbin:/usr/bin:/sbin:/bin
+ with:
+ runs-on: ubuntu-latest
+ distro: ${{ matrix.distro }}
strategy:
fail-fast: false
@@ -520,52 +170,6 @@ jobs:
- fedora:latest
- opensuse/tumbleweed
- runs-on: ubuntu-latest
- container: ${{ matrix.distro }}
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- # needed for git commit history
- fetch-depth: 0
-
- - name: Install dependencies
- run: |
- # approximation to handle both typical foo:bar (tagged) and foo/bar
- # (with implicit :latest)
- distroname="$(echo "${{ matrix.distro }}" | tr : - | tr / -)"
- case "${{ matrix.distro }}" in
- fedora:*)
- dnf install -y rpmdevtools
- dnf install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec")
- # TODO these are needed only by cmd/snap-seccomp unit tests, and
- # should be added to BuildRequires
- dnf install -y glibc-devel.i686 glibc-static.i686
- ;;
- opensuse/*)
- zypper --non-interactive install -y rpmdevtools rpm-build git
- zypper --non-interactive install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec")
- ;;
- *)
- echo "Unsupported distribution variant ${{ matrix.distro }}"
- exit 1
- ;;
- esac
- - name: Set up test user
- run: |
- useradd -U -m test-user
- chown -R test-user:test-user $PWD
-
- - name: Unit tests (Go)
- run: |
- su test-user sh -c "SKIP_DIRTY_CHECK=1 ./run-checks --unit"
-
- - name: Unit tests (C)
- run: |
- su test-user sh -c "./mkversion.sh 1337-git && cd ./cmd && ./autogen.sh && make -j && make distcheck"
-
-
code-coverage:
needs: [unit-tests, unit-tests-special]
runs-on: ubuntu-20.04
@@ -598,9 +202,20 @@ jobs:
verbose: true
spread:
+ uses: ./.github/workflows/spread-tests.yaml
needs: [unit-tests, snap-builds]
- name: ${{ matrix.group }}
- runs-on: [self-hosted, spread-enabled]
+ name: "spread ${{ matrix.group }}"
+ with:
+ # Github doesn't support passing sequences as parameters.
+ # Instead here we create a json array and pass it as a string.
+ # Then in the spread workflow it turns it into a sequence
+ # using the fromJSON expression.
+ runs-on: '["self-hosted", "spread-enabled"]'
+ group: ${{ matrix.group }}
+ backend: ${{ matrix.backend }}
+ systems: ${{ matrix.systems }}
+ tasks: ${{ matrix.tasks }}
+ rules: ${{ matrix.rules }}
strategy:
# FIXME: enable fail-fast mode once spread can cancel an executing job.
# Disable fail-fast mode as it doesn't function with spread. It seems
@@ -613,377 +228,120 @@ jobs:
- group: amazon-linux
backend: google-distro-1
systems: 'amazon-linux-2-64 amazon-linux-2023-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: arch-linux
backend: google-distro-2
systems: 'arch-linux-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: centos
backend: google-distro-2
systems: 'centos-9-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: debian-req
backend: google-distro-1
systems: 'debian-11-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: debian-not-req
backend: google-distro-1
systems: 'debian-12-64 debian-sid-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: fedora
- backend: google-distro-1
- systems: 'fedora-38-64 fedora-39-64'
- tests: 'tests/...'
- rules: 'main'
- - group: fedora-os
backend: openstack
- systems: 'fedora-40-64'
- tests: 'tests/...'
+ systems: 'fedora-40-64 fedora-41-64'
+ tasks: 'tests/...'
rules: 'main'
- group: opensuse
backend: google-distro-2
systems: 'opensuse-15.5-64 opensuse-15.6-64 opensuse-tumbleweed-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-trusty
backend: google
systems: 'ubuntu-14.04-64'
- tests: 'tests/smoke/ tests/main/canonical-livepatch tests/main/canonical-livepatch-14.04'
+ tasks: 'tests/smoke/ tests/main/canonical-livepatch tests/main/canonical-livepatch-14.04'
rules: 'trusty'
- group: ubuntu-xenial-bionic
backend: google
systems: 'ubuntu-16.04-64 ubuntu-18.04-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-focal-jammy
backend: google
systems: 'ubuntu-20.04-64 ubuntu-22.04-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-noble
backend: google
systems: 'ubuntu-24.04-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-no-lts
backend: google
systems: ''
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-daily
backend: google
systems: 'ubuntu-24.10-64'
- tests: 'tests/...'
- rules: 'main'
- - group: ubuntu-core-16
- backend: google-core
- systems: 'ubuntu-core-16-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-core-18
backend: google-core
systems: 'ubuntu-core-18-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-core-20
backend: google-core
systems: 'ubuntu-core-20-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-core-22
backend: google-core
systems: 'ubuntu-core-22-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-core-24
backend: google-core
systems: 'ubuntu-core-24-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- - group: ubuntu-arm
+ - group: ubuntu-arm64
backend: google-arm
systems: 'ubuntu-20.04-arm-64 ubuntu-core-22-arm-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-secboot
backend: google
systems: 'ubuntu-secboot-20.04-64'
- tests: 'tests/...'
+ tasks: 'tests/...'
rules: 'main'
- group: ubuntu-fips
backend: google-pro
systems: 'ubuntu-fips-22.04-64'
- tests: 'tests/fips/...'
+ tasks: 'tests/fips/...'
rules: 'main'
- - group: nested-ubuntu-16.04
- backend: google-nested
- systems: 'ubuntu-16.04-64'
- tests: 'tests/nested/...'
- rules: 'nested'
- group: nested-ubuntu-18.04
backend: google-nested
systems: 'ubuntu-18.04-64'
- tests: 'tests/nested/...'
+ tasks: 'tests/nested/...'
rules: 'nested'
- group: nested-ubuntu-20.04
backend: google-nested
systems: 'ubuntu-20.04-64'
- tests: 'tests/nested/...'
+ tasks: 'tests/nested/...'
rules: 'nested'
- group: nested-ubuntu-22.04
backend: google-nested
systems: 'ubuntu-22.04-64'
- tests: 'tests/nested/...'
+ tasks: 'tests/nested/...'
rules: 'nested'
- group: nested-ubuntu-24.04
backend: google-nested
systems: 'ubuntu-24.04-64'
- tests: 'tests/nested/...'
+ tasks: 'tests/nested/...'
rules: 'nested'
- steps:
- - name: Cleanup job workspace
- id: cleanup-job-workspace
- run: |
- rm -rf "${{ github.workspace }}"
- mkdir "${{ github.workspace }}"
-
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- # spread uses tags as delta reference
- fetch-depth: 0
-
- - name: Get previous attempt
- id: get-previous-attempt
- run: |
- echo "previous_attempt=$(( ${{ github.run_attempt }} - 1 ))" >> $GITHUB_OUTPUT
- shell: bash
-
- - name: Get previous cache
- uses: actions/cache@v4
- with:
- path: "${{ github.workspace }}/.test-results"
- key: "${{ github.job }}-results-${{ github.run_id }}-${{ matrix.group }}-${{ steps.get-previous-attempt.outputs.previous_attempt }}"
-
-
- - name: Prepare test results env and vars
- id: prepare-test-results-env
- run: |
- # Create test results directories and save vars
- TEST_RESULTS_DIR="${{ github.workspace }}/.test-results"
- echo "TEST_RESULTS_DIR=$TEST_RESULTS_DIR" >> $GITHUB_ENV
-
- # Save the var with the failed tests file
- echo "FAILED_TESTS_FILE=$TEST_RESULTS_DIR/failed-tests" >> $GITHUB_ENV
-
- # Make sure the test results dirs are created
- # This step has to be after the cache is restored
- mkdir -p "$TEST_RESULTS_DIR"
-
- - name: Get changed files
- id: changed-files
- uses: tj-actions/changed-files@v41.0.0
-
- - name: Save changes files
- run: |
- CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"
- echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV
- echo "The changed files found are: $CHANGED_FILES"
-
- - name: Check failed tests to run
- if: "!contains(github.event.pull_request.labels.*.name, 'Run all')"
- run: |
- # Save previous failed test results in FAILED_TESTS env var
- FAILED_TESTS=""
- if [ -f "$FAILED_TESTS_FILE" ]; then
- echo "Failed tests file found"
- FAILED_TESTS="$(cat $FAILED_TESTS_FILE)"
- if [ -n "$FAILED_TESTS" ]; then
- echo "Failed tests to run: $FAILED_TESTS"
- echo "FAILED_TESTS=$FAILED_TESTS" >> $GITHUB_ENV
- fi
- fi
-
- - name: Setup run tests variable
- if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')"
- run: |
- RUN_TESTS=""
- SUGGESTED_TESTS=""
- # Save previous failed test results in FAILED_TESTS env var
- if [ -n "$FAILED_TESTS" ]; then
- RUN_TESTS="$FAILED_TESTS"
- else
- for SYSTEM in ${{ matrix.systems }}; do
- for TESTS in ${{ matrix.tests }}; do
- RUN_TESTS="$RUN_TESTS ${{ matrix.backend }}:$SYSTEM:$TESTS"
- done
- CHANGES_PARAM=""
- for CHANGE in $CHANGED_FILES; do
- CHANGES_PARAM="$CHANGES_PARAM -c $CHANGE"
- done
- SUGGESTED_TESTS="$SUGGESTED_TESTS $(./tests/lib/external/snapd-testing-tools/utils/spread-filter -r ./tests/lib/spread/rules/${{ matrix.rules }}.yaml -p "${{ matrix.backend }}:$SYSTEM" $CHANGES_PARAM)"
- done
- fi
- echo RUN_TESTS="$RUN_TESTS" >> $GITHUB_ENV
- echo "Suggested tests by spread-filter tool"
- echo "$SUGGESTED_TESTS"
-
- - name: Setup grafana parameters
- if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')"
- run: |
- # Configure parameters to filter logs (these logs are sent read by grafana agent)
- CHANGE_ID="${{ github.event.number }}"
- if [ -z "$CHANGE_ID" ]; then
- CHANGE_ID="main"
- fi
- FILTERED_LOG_FILE="spread_${CHANGE_ID}_n${{ github.run_attempt }}.filtered.log"
- # The log-filter tool is used to filter the spread logs to be stored
- echo FILTER_PARAMS="-o $FILTERED_LOG_FILE -e Debug -e WARNING: -f Failed=NO_LINES -f Error=NO_LINES" >> $GITHUB_ENV
- echo FILTERED_LOG_FILE="$FILTERED_LOG_FILE" >> $GITHUB_ENV
-
- # Add start line to filtered log
- echo "GRAFANA START: pr ${CHANGE_ID} attempt ${{ github.run_attempt }} run ${{ github.run_id }} group ${{ matrix.group }}" > "$FILTERED_LOG_FILE"
-
- - name: Download built snap
- uses: actions/download-artifact@v4
- if: "!endsWith(matrix.group, '-fips')"
- with:
- name: snap-files-default-test
- # eg. snapd_1337.2.65.1+git97.gd35b459_amd64.snap
- pattern: snapd_1337.*.snap
- path: "${{ github.workspace }}/built-snap"
-
- - name: Download built FIPS snap
- uses: actions/download-artifact@v4
- # eg. ubuntu-fips
- if: "endsWith(matrix.group, '-fips')"
- with:
- name: snap-files-FIPS-test
- # eg. snapd_1337.2.65.1+git97.gd35b459-fips_amd64.snap
- pattern: snapd_1337.*-fips_*.snap
- path: "${{ github.workspace }}/built-snap"
-
- - name: Rename imported snap
- run: |
- for snap in built-snap/snapd_1337.*.snap; do
- mv -v "${snap}" "${snap}.keep"
- done
-
- - name: Run spread tests
- if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread') && ( !startsWith(matrix.group, 'nested-') || contains(github.event.pull_request.labels.*.name, 'Run nested') )"
- env:
- SPREAD_GOOGLE_KEY: ${{ secrets.SPREAD_GOOGLE_KEY }}
- run: |
- # Register a problem matcher to highlight spread failures
- echo "::add-matcher::.github/spread-problem-matcher.json"
- set -x
- SPREAD=spread
- if [[ "${{ matrix.group }}" =~ nested- ]]; then
- export NESTED_BUILD_SNAPD_FROM_CURRENT=true
- export NESTED_ENABLE_KVM=true
- fi
-
- case "${{ matrix.systems }}" in
- *-arm-*)
- SPREAD_USE_PREBUILT_SNAPD_SNAP=false
- ;;
- *)
- SPREAD_USE_PREBUILT_SNAPD_SNAP=true
- ;;
- esac
- export SPREAD_USE_PREBUILT_SNAPD_SNAP
-
- if [[ "${{ matrix.systems }}" =~ amazon-linux-2023 ]]; then
- # Amazon Linux 2023 has no xdelta, however we cannot disable
- # xdelta on a per-target basis as it's used in the repack section
- # of spread.yaml, which is shared by all targets, so all systems
- # in this batch will not use delta for transferring project data
- echo "Disabling xdelta support"
- export NO_DELTA=1
- fi
-
- # Add openstack backend definition to spread.yaml
- if [ "${{ matrix.backend }}" = openstack ]; then
- ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml
- fi
-
- # This coud be the case when either there are not systems for a group or
- # the list of tests to run is empty
- if [ -z "$RUN_TESTS" ]; then
- echo "No tests to run, skiping..."
- exit 0
- fi
-
- # Run spread tests
- # "pipefail" ensures that a non-zero status from the spread is
- # propagated; and we use a subshell as this option could trigger
- # undesired changes elsewhere
- echo "Running command: $SPREAD $RUN_TESTS"
- (set -o pipefail; $SPREAD -no-debug-output -logs spread-logs $RUN_TESTS | PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-filter $FILTER_PARAMS | tee spread.log)
-
- - name: Uploading spread logs
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: spread-logs-${{ matrix.systems }}
- path: "spread-logs/*.log"
- if-no-files-found: ignore
-
- - name: Discard spread workers
- if: always()
- run: |
- shopt -s nullglob;
- for r in .spread-reuse.*.yaml; do
- spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')";
- done
-
- - name: Report spread errors
- if: always()
- run: |
- if [ -e spread.log ]; then
- echo "Running spread log analyzer"
- ACTIONS_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}"
- PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json --cut 1 >/dev/null
- while IFS= read -r line; do
- if [ ! -z "$line" ]; then
- echo "Adding failed test line to filtered log"
- echo "GRAFANA FAILED: $line $ACTIONS_URL" | tee -a "$FILTERED_LOG_FILE"
- fi
- done <<< $(jq -r '.[] | select( .type == "info" ) | select( .info_type == "Error" ) | "\(.verb) \(.task)"' spread-results.json)
- else
- echo "No spread log found, skipping errors reporting"
- fi
-
- - name: Analyze spread test results
- if: always()
- run: |
- if [ -f spread.log ]; then
- echo "Running spread log parser"
- PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json
-
- # Add openstack backend definition to spread.yaml
- if [ "${{ matrix.backend }}" = openstack ]; then
- ./tests/lib/spread/add-backend tests/lib/spread/backend.openstack.yaml spread.yaml
- fi
-
- echo "Running spread log analyzer"
- PYTHONDONTWRITEBYTECODE=1 ./tests/lib/external/snapd-testing-tools/utils/log-analyzer list-reexecute-tasks "$RUN_TESTS" spread-results.json > "$FAILED_TESTS_FILE"
-
- echo "List of failed tests saved"
- cat "$FAILED_TESTS_FILE"
- else
- echo "No spread log found, saving empty list of failed tests"
- touch "$FAILED_TESTS_FILE"
- fi
-
- - name: Save spread test results to cache
- if: always()
- uses: actions/cache/save@v4
- with:
- path: "${{ github.workspace }}/.test-results"
- key: "${{ github.job }}-results-${{ github.run_id }}-${{ matrix.group }}-${{ github.run_attempt }}"
diff --git a/.github/workflows/unit-tests-cross-distro.yaml b/.github/workflows/unit-tests-cross-distro.yaml
new file mode 100644
index 00000000000..fcbd918a477
--- /dev/null
+++ b/.github/workflows/unit-tests-cross-distro.yaml
@@ -0,0 +1,65 @@
+on:
+ workflow_call:
+ inputs:
+ runs-on:
+ description: 'A tag to indicate which runner to use'
+ required: true
+ type: string
+ distro:
+ description: 'The name of the github container image to use to run the unit tests'
+ required: true
+ type: string
+
+
+jobs:
+ unit-tests-cross-distro:
+ runs-on: ${{ inputs.runs-on }}
+ container: ${{ inputs.distro }}
+ env:
+ # Set PATH to ignore the load of magic binaries from /usr/local/bin And
+ # to use the go snap automatically. Note that we install go from the
+ # snap in a step below. Without this we get the GitHub-controlled latest
+ # version of go.
+ PATH: /usr/sbin:/usr/bin:/sbin:/bin
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ # needed for git commit history
+ fetch-depth: 0
+
+ - name: Install dependencies
+ run: |
+ # approximation to handle both typical foo:bar (tagged) and foo/bar
+ # (with implicit :latest)
+ distroname="$(echo "${{ inputs.distro }}" | tr : - | tr / -)"
+ case "${{ inputs.distro }}" in
+ fedora:*)
+ dnf install -y rpmdevtools
+ dnf install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec")
+ # TODO these are needed only by cmd/snap-seccomp unit tests, and
+ # should be added to BuildRequires
+ dnf install -y glibc-devel.i686 glibc-static.i686
+ ;;
+ opensuse/*)
+ zypper --non-interactive install -y rpmdevtools rpm-build git
+ zypper --non-interactive install -y $(rpmspec -q --buildrequires "./packaging/$distroname/snapd.spec")
+ ;;
+ *)
+ echo "Unsupported distribution variant ${{ inputs.distro }}"
+ exit 1
+ ;;
+ esac
+ - name: Set up test user
+ run: |
+ useradd -U -m test-user
+ chown -R test-user:test-user $PWD
+
+ - name: Unit tests (Go)
+ run: |
+ su test-user sh -c "SKIP_DIRTY_CHECK=1 ./run-checks --unit"
+
+ - name: Unit tests (C)
+ run: |
+ su test-user sh -c "./mkversion.sh 1337-git && cd ./cmd && ./autogen.sh && make -j && make distcheck"
diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml
new file mode 100644
index 00000000000..21c54b2fa10
--- /dev/null
+++ b/.github/workflows/unit-tests.yaml
@@ -0,0 +1,133 @@
+on:
+ workflow_call:
+ inputs:
+ runs-on:
+ description: 'A tag to indicate which runner to use'
+ required: true
+ type: string
+ gochannel:
+ description: 'The snap store channel to use to install the go snap'
+ required: true
+ type: string
+ unit-scenario:
+ description: 'The name of the scenario being tested: {normal, snapd-debug, withbootassetstesting, nosecboot, faultinject, race, snapdusergo}'
+ required: true
+ type: string
+
+jobs:
+ unit-tests:
+ runs-on: ${{ inputs.runs-on }}
+ env:
+ GOPATH: ${{ github.workspace }}
+ # Set PATH to ignore the load of magic binaries from /usr/local/bin And
+ # to use the go snap automatically. Note that we install go from the
+ # snap in a step below. Without this we get the GitHub-controlled latest
+ # version of go.
+ PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin
+ GOROOT: ""
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ # needed for git commit history
+ fetch-depth: 0
+ # NOTE: checkout the code in a fixed location, even for forks, as this
+ # is relevant for go's import system.
+ path: ./src/github.com/snapcore/snapd
+
+ # Fetch base ref, needed for golangci-lint
+ - name: Fetching base ref ${{ github.base_ref }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd
+ git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
+
+ - name: Download and install Debian dependencies
+ # Github does not allow variables in "uses"; this has to be a hard-coded path
+ uses: ./src/github.com/snapcore/snapd/.github/workflows/actions/download-install-debian-deps
+ with:
+ snapd-src-dir: "${{ github.workspace }}/src/github.com/snapcore/snapd"
+
+ # golang latest ensures things work on the edge
+ - name: Install the go snap
+ run: |
+ sudo snap install --classic --channel=${{ inputs.gochannel }} go
+
+ - name: Get deps
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd/ && ./get-deps.sh
+
+ - name: Build C
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/
+ ./autogen.sh
+ make -j$(nproc)
+
+ - name: Build Go
+ run: |
+ go build github.com/snapcore/snapd/...
+
+ - name: Test C
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make distcheck
+
+ - name: Reset code coverage data
+ run: |
+ rm -rf ${{ github.workspace }}/.coverage/
+ COVERAGE_OUT="${{ github.workspace }}/coverage/coverage-${{ inputs.unit-scenario}}.cov"
+ echo "COVERAGE_OUT=$COVERAGE_OUT" >> $GITHUB_ENV
+
+ - name: Test Go
+ if: ${{ inputs.unit-scenario == 'normal' }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ ./run-checks --unit
+
+ - name: Test Go (SNAPD_DEBUG=1)
+ if: ${{ inputs.unit-scenario == 'snapd_debug' }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ SKIP_DIRTY_CHECK=1 SNAPD_DEBUG=1 ./run-checks --unit
+
+ - name: Test Go (withbootassetstesting)
+ if: ${{ inputs.unit-scenario == 'withbootassetstesting' }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=withbootassetstesting ./run-checks --unit
+
+ - name: Test Go (nosecboot)
+ if: ${{ inputs.unit-scenario == 'nosecboot' }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ echo "Dropping github.com/snapcore/secboot"
+ # use govendor remove so that a subsequent govendor sync does not
+ # install secboot again
+ # ${{ github.workspace }}/bin/govendor remove github.com/snapcore/secboot
+ # ${{ github.workspace }}/bin/govendor remove +unused
+ SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=nosecboot ./run-checks --unit
+
+ - name: Test Go (faultinject)
+ if: ${{ inputs.unit-scenario == 'faultinject' }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=faultinject ./run-checks --unit
+
+ - name: Test Go (-race)
+ if: ${{ inputs.unit-scenario == 'race' }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ SKIP_DIRTY_CHECK=1 GO_TEST_RACE=1 SKIP_COVERAGE=1 ./run-checks --unit
+
+ - name: Test Go (snapdusergo)
+ if: ${{ inputs.unit-scenario == 'snapdusergo' }}
+ run: |
+ cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1
+ SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=snapdusergo ./run-checks --unit
+
+ - name: Upload the coverage results
+ if: ${{ inputs.gochannel != 'latest/stable' && inputs.unit-scenario != 'race' }}
+ uses: actions/upload-artifact@v4
+ with:
+ include-hidden-files: true
+ name: "coverage-files-${{ inputs.unit-scenario }}"
+ path: "${{ github.workspace }}/coverage/coverage*.cov"
diff --git a/.golangci.yml b/.golangci.yml
index 71a0cecb78e..041a726f685 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -129,6 +129,14 @@ linters-settings:
deny:
- pkg: "os/user"
desc: "Please use osutil/user instead. See https://github.com/canonical/snapd/pull/13776"
+ testtime:
+ files:
+ - "!$test"
+ deny:
+ - pkg: "github.com/snapcore/snapd/testtime"
+ desc: "Cannot use testtime outside of test code"
+ - pkg: "github.com/canonical/snapd/testtime"
+ desc: "Cannot use testtime outside of test code"
misspell:
# Correct spellings using locale preferences for US or UK.
@@ -189,6 +197,7 @@ linters:
# disabling until https://github.com/daixiang0/gci/issues/54 is fixed
# - gci
- testpackage
+ - depguard
# disable everything else
disable-all: true
diff --git a/client/snap_op.go b/client/snap_op.go
index e5d274e5923..d3369f7d32d 100644
--- a/client/snap_op.go
+++ b/client/snap_op.go
@@ -162,12 +162,12 @@ func (client *Client) RemoveMany(names []string, components map[string][]string,
// Refresh refreshes the snap with the given name (switching it to track
// the given channel if given).
-func (client *Client) Refresh(name string, options *SnapOptions) (changeID string, err error) {
- return client.doSnapAction("refresh", name, nil, options)
+func (client *Client) Refresh(name string, components []string, options *SnapOptions) (changeID string, err error) {
+ return client.doSnapAction("refresh", name, components, options)
}
-func (client *Client) RefreshMany(names []string, options *SnapOptions) (changeID string, err error) {
- return client.doMultiSnapAction("refresh", names, nil, options)
+func (client *Client) RefreshMany(names []string, components map[string][]string, options *SnapOptions) (changeID string, err error) {
+ return client.doMultiSnapAction("refresh", names, components, options)
}
func (client *Client) HoldRefreshes(name string, options *SnapOptions) (changeID string, err error) {
diff --git a/client/snap_op_test.go b/client/snap_op_test.go
index e638cade2eb..5a8f604c27f 100644
--- a/client/snap_op_test.go
+++ b/client/snap_op_test.go
@@ -48,7 +48,12 @@ var ops = []struct {
},
action: "install",
},
- {(*client.Client).Refresh, "refresh"},
+ {
+ op: func(c *client.Client, name string, options *client.SnapOptions) (string, error) {
+ return c.Refresh(name, nil, options)
+ },
+ action: "refresh",
+ },
{
op: func(c *client.Client, name string, options *client.SnapOptions) (string, error) {
return c.Remove(name, nil, options)
@@ -67,7 +72,12 @@ var multiOps = []struct {
op func(*client.Client, []string, *client.SnapOptions) (string, error)
action string
}{
- {(*client.Client).RefreshMany, "refresh"},
+ {
+ op: func(c *client.Client, names []string, options *client.SnapOptions) (string, error) {
+ return c.RefreshMany(names, nil, options)
+ },
+ action: "refresh",
+ },
{
op: func(c *client.Client, names []string, options *client.SnapOptions) (string, error) {
return c.InstallMany(names, nil, options)
@@ -874,7 +884,7 @@ func (cs *clientSuite) TestClientRefreshWithValidationSets(c *check.C) {
}`
sets := []string{"foo/bar=2", "foo/baz"}
- chgID, err := cs.cli.RefreshMany(nil, &client.SnapOptions{
+ chgID, err := cs.cli.RefreshMany(nil, nil, &client.SnapOptions{
ValidationSets: sets,
})
c.Assert(err, check.IsNil)
@@ -957,6 +967,10 @@ func (cs *clientSuite) TestClientOpInstallWithComponents(c *check.C) {
cs.testClientOpWithComponents(c, cs.cli.Install)
}
+func (cs *clientSuite) TestClientOpRefreshWithComponents(c *check.C) {
+ cs.testClientOpWithComponents(c, cs.cli.Refresh)
+}
+
func (cs *clientSuite) TestClientOpRemoveWithComponents(c *check.C) {
cs.testClientOpWithComponents(c, cs.cli.Remove)
}
@@ -991,6 +1005,10 @@ func (cs *clientSuite) TestClientOpInstallManyWithComponents(c *check.C) {
cs.testClientOpManyWithComponents(c, cs.cli.InstallMany)
}
+func (cs *clientSuite) TestClientOpRefreshManyWithComponents(c *check.C) {
+ cs.testClientOpManyWithComponents(c, cs.cli.RefreshMany)
+}
+
func (cs *clientSuite) TestClientOpRemoveManyWithComponents(c *check.C) {
cs.testClientOpManyWithComponents(c, cs.cli.RemoveMany)
}
diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go
index 7f12943b699..edadde3fb86 100644
--- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go
+++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go
@@ -1361,6 +1361,9 @@ func (m *recoverModeStateMachine) mountSave() (stateFunc, error) {
// TODO: should we fsck ubuntu-save ?
mountOpts := &systemdMountOptions{
Private: true,
+ NoDev: true,
+ NoSuid: true,
+ NoExec: true,
}
mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, mountOpts)
if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil {
@@ -2012,7 +2015,7 @@ func generateMountsModeRun(mst *initramfsMountsState) error {
isRunMode := true
// 2. mount ubuntu-seed (optional for classic)
- systemdOpts := &systemdMountOptions{
+ seedMountOpts := &systemdMountOptions{
NeedsFsck: true,
Private: true,
}
@@ -2038,7 +2041,7 @@ func generateMountsModeRun(mst *initramfsMountsState) error {
// need it to be writable for i.e. transitioning to recover mode
if partUUID != "" {
if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID),
- boot.InitramfsUbuntuSeedDir, systemdOpts); err != nil {
+ boot.InitramfsUbuntuSeedDir, seedMountOpts); err != nil {
return err
}
}
@@ -2089,7 +2092,14 @@ func generateMountsModeRun(mst *initramfsMountsState) error {
rootfsDir := boot.InitramfsWritableDir(model, isRunMode)
// 3.2. mount ubuntu-save (if present)
- haveSave, err := maybeMountSave(disk, rootfsDir, isEncryptedDev, systemdOpts)
+ saveMountOpts := &systemdMountOptions{
+ NeedsFsck: true,
+ Private: true,
+ NoDev: true,
+ NoSuid: true,
+ NoExec: true,
+ }
+ haveSave, err := maybeMountSave(disk, rootfsDir, isEncryptedDev, saveMountOpts)
if err != nil {
return err
}
diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go
index 0d6b6ac48d5..91fa6daef6b 100644
--- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go
+++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go
@@ -97,6 +97,19 @@ var (
NoSuid: true,
Private: true,
}
+ needsNoSuidNoDevNoExecMountOpts = &main.SystemdMountOptions{
+ Private: true,
+ NoSuid: true,
+ NoExec: true,
+ NoDev: true,
+ }
+ needsFsckAndNoSuidNoDevNoExecMountOpts = &main.SystemdMountOptions{
+ Private: true,
+ NeedsFsck: true,
+ NoSuid: true,
+ NoExec: true,
+ NoDev: true,
+ }
needsFsckNoPrivateDiskMountOpts = &main.SystemdMountOptions{
NeedsFsck: true,
}
@@ -117,9 +130,6 @@ var (
ReadOnly: true,
Private: true,
}
- mountOpts = &main.SystemdMountOptions{
- Private: true,
- }
bindOpts = &main.SystemdMountOptions{
Bind: true,
}
@@ -615,6 +625,11 @@ func (s *baseInitramfsMountsSuite) ubuntuPartUUIDMount(partuuid string, mode str
}
case strings.Contains(partuuid, "ubuntu-save"):
mnt.where = boot.InitramfsUbuntuSaveDir
+ if mode == "run" {
+ mnt.opts = needsFsckAndNoSuidNoDevNoExecMountOpts
+ } else {
+ mnt.opts = needsNoSuidNoDevNoExecMountOpts
+ }
}
return mnt
@@ -1879,7 +1894,7 @@ Wants=%[1]s
"--no-pager",
"--no-ask-password",
"--fsck=no",
- "--options=private",
+ "--options=nodev,nosuid,noexec,private",
"--property=Before=initrd-fs.target",
},
})
@@ -2159,7 +2174,7 @@ Wants=%[1]s
"--no-pager",
"--no-ask-password",
"--fsck=yes",
- "--options=private",
+ "--options=nodev,nosuid,noexec,private",
"--property=Before=initrd-fs.target",
}, {
"systemd-mount",
@@ -2331,7 +2346,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- needsFsckDiskMountOpts,
+ needsFsckAndNoSuidNoDevNoExecMountOpts,
nil,
},
s.makeRunSnapSystemdMount(snap.TypeBase, s.core20),
@@ -3373,7 +3388,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappy(c *C) {
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -3462,7 +3477,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeTimeMovesForwardHap
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -3546,7 +3561,7 @@ defaults:
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -3631,7 +3646,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyBootedKernelPa
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -3751,7 +3766,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -3910,7 +3925,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedDa
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -4088,7 +4103,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedSa
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -4253,7 +4268,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAb
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -4416,7 +4431,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAb
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -4584,7 +4599,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedDa
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -4774,7 +4789,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeDegradedAbsentDataU
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -5234,7 +5249,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeUnencryptedDataUnen
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -5374,7 +5389,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAb
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -5755,7 +5770,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedMismatched
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -5967,7 +5982,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedAttackerFS
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -6033,7 +6048,7 @@ func (s *initramfsMountsSuite) testInitramfsMountsInstallRecoverModeMeasure(c *C
systemdMount{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
})
@@ -6157,7 +6172,7 @@ func (s *baseInitramfsMountsSuite) runInitramfsMountsUnencryptedTryRecovery(c *C
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -6802,7 +6817,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsFactoryResetModeHappyEncrypted
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -6911,7 +6926,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsFactoryResetModeHappyUnencrypt
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
@@ -7203,7 +7218,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsFactoryResetModeUnhappyMountEn
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
fmt.Errorf("mount failed"),
},
}, nil)
@@ -7598,7 +7613,7 @@ func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeEncryptedDataHap
{
"/dev/mapper/ubuntu-save-random",
boot.InitramfsUbuntuSaveDir,
- needsFsckDiskMountOpts,
+ needsFsckAndNoSuidNoDevNoExecMountOpts,
nil,
},
s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget),
@@ -8536,7 +8551,7 @@ func (s *initramfsClassicMountsSuite) TestInitramfsMountsRecoveryModeHybridSyste
{
"/dev/disk/by-partuuid/ubuntu-save-partuuid",
boot.InitramfsUbuntuSaveDir,
- mountOpts,
+ needsNoSuidNoDevNoExecMountOpts,
nil,
},
}, nil)
diff --git a/cmd/snap-seccomp/syscalls/syscalls.go b/cmd/snap-seccomp/syscalls/syscalls.go
index 7e60e3b6b2f..a5e16167534 100644
--- a/cmd/snap-seccomp/syscalls/syscalls.go
+++ b/cmd/snap-seccomp/syscalls/syscalls.go
@@ -46,6 +46,7 @@ var SeccompSyscalls = []string{
"brk",
"cachectl",
"cacheflush",
+ "cachestat",
"capget",
"capset",
"chdir",
@@ -99,6 +100,7 @@ var SeccompSyscalls = []string{
"fchdir",
"fchmod",
"fchmodat",
+ "fchmodat2",
"fchown",
"fchown32",
"fchownat",
@@ -126,8 +128,11 @@ var SeccompSyscalls = []string{
"ftruncate",
"ftruncate64",
"futex",
+ "futex_requeue",
"futex_time64",
+ "futex_wait",
"futex_waitv",
+ "futex_wake",
"futimesat",
"get_kernel_syms",
"get_mempolicy",
@@ -207,6 +212,7 @@ var SeccompSyscalls = []string{
"link",
"linkat",
"listen",
+ "listmount",
"listxattr",
"llistxattr",
"lock",
@@ -214,9 +220,13 @@ var SeccompSyscalls = []string{
"lremovexattr",
"lseek",
"lsetxattr",
+ "lsm_get_self_attr",
+ "lsm_list_modules",
+ "lsm_set_self_attr",
"lstat",
"lstat64",
"madvise",
+ "map_shadow_stack",
"mbind",
"membarrier",
"memfd_create",
@@ -248,6 +258,7 @@ var SeccompSyscalls = []string{
"mq_timedsend_time64",
"mq_unlink",
"mremap",
+ "mseal",
"msgctl",
"msgget",
"msgrcv",
@@ -331,6 +342,7 @@ var SeccompSyscalls = []string{
"request_key",
"restart_syscall",
"riscv_flush_icache",
+ "riscv_hwprobe",
"rmdir",
"rseq",
"rt_sigaction",
@@ -436,6 +448,7 @@ var SeccompSyscalls = []string{
"stat64",
"statfs",
"statfs64",
+ "statmount",
"statx",
"stime",
"stty",
@@ -486,6 +499,7 @@ var SeccompSyscalls = []string{
"unlink",
"unlinkat",
"unshare",
+ "uretprobe",
"uselib",
"userfaultfd",
"usr26",
diff --git a/cmd/snap/cmd_debug_api.go b/cmd/snap/cmd_debug_api.go
index b2fb7016634..01dcd093e4b 100644
--- a/cmd/snap/cmd_debug_api.go
+++ b/cmd/snap/cmd_debug_api.go
@@ -32,7 +32,7 @@ import (
"github.com/snapcore/snapd/logger"
)
-var longDebugAPIHelp = `
+const longDebugAPIHelp = `
Execute a raw query to snapd API. Complex input can be read from stdin, while
output is printed to stdout. See examples below:
diff --git a/cmd/snap/cmd_debug_seeding.go b/cmd/snap/cmd_debug_seeding.go
index 81b72191625..37a59f01e7b 100644
--- a/cmd/snap/cmd_debug_seeding.go
+++ b/cmd/snap/cmd_debug_seeding.go
@@ -36,13 +36,12 @@ type cmdSeeding struct {
}
func init() {
- cmd := addDebugCommand("seeding",
- "(internal) obtain seeding and preseeding details",
- "(internal) obtain seeding and preseeding details",
+ addDebugCommand("seeding",
+ "Obtain seeding and preseeding details",
+ "Obtain seeding and preseeding details",
func() flags.Commander {
return &cmdSeeding{}
}, nil, nil)
- cmd.hidden = true
}
func (x *cmdSeeding) Execute(args []string) error {
diff --git a/cmd/snap/cmd_debug_validate_seed.go b/cmd/snap/cmd_debug_validate_seed.go
index 68e8fa555cb..b60b69d7236 100644
--- a/cmd/snap/cmd_debug_validate_seed.go
+++ b/cmd/snap/cmd_debug_validate_seed.go
@@ -33,14 +33,18 @@ type cmdValidateSeed struct {
} `positional-args:"true" required:"true"`
}
+const longDebugValidateSeedHelp = `
+Validate correctness of snap seed located in the directory
+containing seed.yaml file.
+`
+
func init() {
- cmd := addDebugCommand("validate-seed",
- "(internal) validate seed.yaml",
- "(internal) validate seed.yaml",
+ addDebugCommand("validate-seed",
+ "Validate snap seed",
+ longDebugValidateSeedHelp,
func() flags.Commander {
return &cmdValidateSeed{}
}, nil, nil)
- cmd.hidden = true
}
func (x *cmdValidateSeed) Execute(args []string) error {
diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go
index 4418e53fa24..cc096a67e8c 100644
--- a/cmd/snap/cmd_run.go
+++ b/cmd/snap/cmd_run.go
@@ -501,7 +501,7 @@ func (x *cmdRun) straceOpts() (opts []string, raw bool, err error) {
func checkSnapRunInhibitionConflict(app *snap.AppInfo) error {
// Remove hint check takes precedence because we want to exit early
snapName := app.Snap.InstanceName()
- hint, _, err := runinhibit.IsLocked(snapName)
+ hint, _, err := runinhibit.IsLocked(snapName, nil)
if err != nil {
return err
}
diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go
index 5df4a2cc5c0..ee0b5eeb22d 100644
--- a/cmd/snap/cmd_run_test.go
+++ b/cmd/snap/cmd_run_test.go
@@ -281,7 +281,7 @@ func (s *RunSuite) TestSnapRunAppRunsChecksRefreshInhibitionLock(c *check.C) {
defer restorer()
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil)
c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil)
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
@@ -321,7 +321,7 @@ func (s *RunSuite) TestSnapRunAppRunsChecksRefreshInhibitionLock(c *check.C) {
func (s *RunSuite) testSnapRunAppRunsChecksRemoveInhibitionLock(c *check.C, svc bool) {
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo, nil), check.IsNil)
cmd := "snapname.app"
if svc {
@@ -355,7 +355,7 @@ func (s *RunSuite) TestSnapRunAppRefreshAppAwarenessUnsetSkipsInhibitionLockWait
// mark snap as inhibited
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil)
c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil)
// unset refresh-app-awareness flag
c.Assert(os.RemoveAll(features.RefreshAppAwareness.ControlFile()), check.IsNil)
@@ -379,7 +379,7 @@ func (s *RunSuite) TestSnapRunAppNewRevisionAfterInhibition(c *check.C) {
// mark snap as inhibited
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil)
// unset refresh-app-awareness flag
c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil)
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
@@ -498,7 +498,7 @@ func (s *RunSuite) TestSnapRunHookNoRuninhibit(c *check.C) {
defer restore()
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(42)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil)
c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil)
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
@@ -532,7 +532,7 @@ func (s *RunSuite) TestSnapRunAppRuninhibitSkipsServices(c *check.C) {
defer restorer()
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R("x2")}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), check.IsNil)
c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), check.IsNil)
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
@@ -718,7 +718,7 @@ func (s *RunSuite) testSnapRunAppRetryNoInhibitHintFileThenOngoingRefresh(c *che
// mock snap inhibited to trigger race condition detection
// i.e. we started without a hint lock file (snap on first install)
// then a refresh started which created the hint lock file.
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil)
// nil FileLock means no inhibit file exists
return nil, nil
@@ -847,7 +847,7 @@ func (s *RunSuite) testSnapRunAppRetryNoInhibitHintFileThenOngoingRemove(c *chec
// mock snap inhibited to trigger race condition detection
// i.e. we started without a hint lock file (snap on first install)
// then a remove started which created the hint lock file.
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil)
// nil FileLock means no inhibit file exists
return nil, nil
@@ -955,7 +955,7 @@ func (s *RunSuite) TestSnapRunAppRetryNoInhibitHintFileThenOngoingRefreshMissing
// and we have an ongoing refresh which removed current symlink.
c.Assert(err, testutil.ErrorIs, snaprun.ErrSnapRefreshConflict)
// and created the inhibition hint lock file.
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil)
return nil, err
} else {
var err error
@@ -1054,7 +1054,7 @@ func (s *RunSuite) TestSnapRunAppMaxRetry(c *check.C) {
// mock snap inhibited to trigger race condition detection
// i.e. we started without a hint lock file (snap on first install)
// then a refresh started which created the hint lock file.
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}), check.IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R("x2")}, nil), check.IsNil)
// nil FileLock means no inhibit file exists
return nil, nil
diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go
index 4a59c5ccb14..9cf67b46427 100644
--- a/cmd/snap/cmd_snap_op.go
+++ b/cmd/snap/cmd_snap_op.go
@@ -929,7 +929,13 @@ type cmdRefresh struct {
}
func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error {
- changeID, err := x.client.RefreshMany(snaps, opts)
+ const forInstall = true
+ names, compsBySnap, err := snapInstancesAndComponentsFromNames(snaps, forInstall)
+ if err != nil {
+ return err
+ }
+
+ changeID, err := x.client.RefreshMany(names, compsBySnap, opts)
if err != nil {
return err
}
@@ -958,9 +964,14 @@ func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error
}
func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error {
- changeID, err := x.client.Refresh(name, opts)
+ snapName, comps := snap.SplitSnapInstanceAndComponents(name)
+ if name == "" {
+ return errors.New(i18n.G("no snap for the component(s) was specified"))
+ }
+
+ changeID, err := x.client.Refresh(snapName, comps, opts)
if err != nil {
- msg, err := errorToCmdMessage(name, "refresh", err, opts)
+ msg, err := errorToCmdMessage(snapName, "refresh", err, opts)
if err != nil {
return err
}
@@ -978,7 +989,7 @@ func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error {
// TODO: this doesn't really tell about all the things you
// could set while refreshing (something switch does)
- return showDone(x.client, chg, &changedSnapsData{names: []string{name}, comps: nil}, "refresh", opts, x.getEscapes())
+ return showDone(x.client, chg, &changedSnapsData{names: []string{snapName}, comps: nil}, "refresh", opts, x.getEscapes())
}
func parseSysinfoTime(s string) time.Time {
diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go
index 9cba3ff9625..54ac7399531 100644
--- a/cmd/snap/cmd_snap_op_test.go
+++ b/cmd/snap/cmd_snap_op_test.go
@@ -144,6 +144,83 @@ func (t *snapOpTestServer) handle(w http.ResponseWriter, r *http.Request) {
t.n++
}
+func multiHandler(c *check.C, snaps []string, snapToComps map[string][]string, checker func(*http.Request), calls int) http.HandlerFunc {
+ var n int
+ return func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ checker(r)
+ method := "POST"
+ c.Check(r.Method, check.Equals, method)
+ w.WriteHeader(202)
+ fmt.Fprintln(w, `{"type":"async", "change": "42", "status-code": 202}`)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"status": "Doing"}}`)
+ case 2:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/changes/42")
+ var data struct {
+ SnapNames []string `json:"snap-names,omitempty"`
+ Components map[string][]string `json:"components,omitempty"`
+ }
+
+ data.Components = snapToComps
+ data.SnapNames = snaps
+
+ encoded, err := json.Marshal(data)
+ c.Assert(err, check.IsNil)
+
+ fmt.Fprintf(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": %s}}\n`, string(encoded))
+ case 3:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+
+ var results []interface{}
+ for _, snap := range snaps {
+ result := map[string]interface{}{
+ "name": snap,
+ "status": "active",
+ "version": "1.0",
+ "developer": "bar",
+ "publisher": map[string]interface{}{
+ "id": "bar-id",
+ "username": "bar",
+ "display-name": "Bar",
+ "validation": "unproven",
+ },
+ "revision": 42,
+ "channel": "latest/stable",
+ "tracking-channel": "latest/stable",
+ "confinement": "strict",
+ }
+
+ comps := make([]map[string]string, 0, len(snapToComps[snap]))
+ for _, comp := range snapToComps[snap] {
+ comps = append(comps, map[string]string{"name": comp, "version": "3.2"})
+ }
+
+ if len(comps) > 0 {
+ result["components"] = comps
+ }
+
+ results = append(results, result)
+ }
+
+ err := json.NewEncoder(w).Encode(map[string]interface{}{
+ "type": "sync",
+ "result": results,
+ })
+ c.Assert(err, check.IsNil)
+ default:
+ c.Fatalf("expected to get %d requests, now on %d", calls, n+1)
+ }
+
+ n++
+ }
+}
+
type SnapOpSuite struct {
BaseSnapSuite
@@ -2260,7 +2337,51 @@ func (s *SnapOpSuite) TestRefreshOne(c *check.C) {
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo"})
c.Assert(err, check.IsNil)
c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`)
+}
+
+func (s *SnapOpSuite) TestRefreshOneAdditionalComponents(c *check.C) {
+ s.RedirectClientToTestServer(s.srv.handle)
+ s.srv.checker = func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "transaction": string(client.TransactionPerSnap),
+ "components": []interface{}{"comp1", "comp2"},
+ })
+ }
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo+comp1+comp2"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`)
+}
+
+func (s *SnapOpSuite) TestRefreshManyAdditionalComponents(c *check.C) {
+ checker := func(r *http.Request) {
+ c.Check(r.Method, check.Equals, "POST")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps")
+ c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
+ "action": "refresh",
+ "transaction": string(client.TransactionPerSnap),
+ "snaps": []interface{}{"foo", "bar"},
+ "components": map[string]interface{}{
+ "foo": []interface{}{"comp1", "comp2"},
+ "bar": []interface{}{"comp3", "comp4"},
+ },
+ })
+ }
+ s.RedirectClientToTestServer(multiHandler(c, []string{"foo", "bar"}, map[string][]string{
+ "foo": {"comp1", "comp2"},
+ "bar": {"comp3", "comp4"},
+ }, checker, 3))
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "foo+comp1+comp2", "bar+comp3+comp4"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*bar 1.0 from Bar refreshed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*component comp1 3.2 for foo 1.0 refreshed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*component comp2 3.2 for foo 1.0 refreshed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*component comp3 3.2 for bar 1.0 refreshed`)
+ c.Check(s.Stdout(), check.Matches, `(?sm).*component comp4 3.2 for bar 1.0 refreshed`)
}
func (s *SnapOpSuite) TestRefreshOneSwitchChannel(c *check.C) {
diff --git a/cmd/snap/cmd_validate.go b/cmd/snap/cmd_validate.go
index d07f29345d2..f18b4467a78 100644
--- a/cmd/snap/cmd_validate.go
+++ b/cmd/snap/cmd_validate.go
@@ -126,7 +126,7 @@ func (cmd *cmdValidate) Execute(args []string) error {
}
if cmd.Refresh {
- changeID, err := cmd.client.RefreshMany(nil, &client.SnapOptions{
+ changeID, err := cmd.client.RefreshMany(nil, nil, &client.SnapOptions{
ValidationSets: []string{cmd.Positional.ValidationSet},
})
if err != nil {
diff --git a/cmd/snap/inhibit_test.go b/cmd/snap/inhibit_test.go
index ce1b6f76759..dbf7ca9d361 100644
--- a/cmd/snap/inhibit_test.go
+++ b/cmd/snap/inhibit_test.go
@@ -71,7 +71,7 @@ func (s *RunSuite) TestWaitWhileInhibitedRunThrough(c *C) {
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil)
var waitWhileInhibitedCalled int
restore := snaprun.MockWaitWhileInhibited(func(ctx context.Context, snapName string, notInhibited func(ctx context.Context) error, inhibited func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error), interval time.Duration) (flock *osutil.FileLock, retErr error) {
@@ -130,7 +130,7 @@ func (s *RunSuite) TestWaitWhileInhibitedErrorOnStartNotification(c *C) {
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil)
var startCalled, finishCalled int
inhibitionFlow := fakeInhibitionFlow{
@@ -166,7 +166,7 @@ func (s *RunSuite) TestWaitWhileInhibitedErrorOnFinishNotification(c *C) {
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil)
var waitWhileInhibitedCalled int
restore := snaprun.MockWaitWhileInhibited(func(ctx context.Context, snapName string, notInhibited func(ctx context.Context) error, inhibited func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error), interval time.Duration) (flock *osutil.FileLock, retErr error) {
@@ -226,7 +226,7 @@ func (s *RunSuite) TestWaitWhileInhibitedContextCancellationOnError(c *C) {
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil)
originalCtx, cancel := context.WithCancel(context.Background())
inhibitionFlow := fakeInhibitionFlow{
@@ -259,7 +259,7 @@ func (s *RunSuite) TestWaitWhileInhibitedGateRefreshNoNotification(c *C) {
c.Assert(os.WriteFile(features.RefreshAppAwareness.ControlFile(), []byte(nil), 0644), check.IsNil)
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedGateRefresh, inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedGateRefresh, inhibitInfo, nil), IsNil)
var called int
restore := snaprun.MockWaitWhileInhibited(func(ctx context.Context, snapName string, notInhibited func(ctx context.Context) error, inhibited func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error), interval time.Duration) (flock *osutil.FileLock, retErr error) {
@@ -310,7 +310,7 @@ func (s *RunSuite) testWaitWhileInhibitedRemoveInhibition(c *C, svc bool) {
snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{Revision: snap.R(11)})
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(11)}
- c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("snapname", runinhibit.HintInhibitedForRemove, inhibitInfo, nil), IsNil)
inhibitionFlow := fakeInhibitionFlow{
start: func(ctx context.Context) error {
diff --git a/cmd/snaplock/lock.go b/cmd/snaplock/lock.go
index d6ac6471414..f059db5be54 100644
--- a/cmd/snaplock/lock.go
+++ b/cmd/snaplock/lock.go
@@ -36,6 +36,11 @@ func lockFileName(snapName string) string {
}
// OpenLock creates and opens a lock file associated with a particular snap.
+//
+// NOTE: The snap lock is only accessible to root and is only intended to
+// synchronize operations between snapd and snap-confine (and snap-update-ns
+// in some cases). Any process holding the snap lock must not do any
+// interactions with snapd to avoid deadlocks due to locked snap state.
func OpenLock(snapName string) (*osutil.FileLock, error) {
if err := os.MkdirAll(dirs.SnapRunLockDir, 0700); err != nil {
return nil, fmt.Errorf("cannot create lock directory: %s", err)
diff --git a/cmd/snaplock/runinhibit/inhibit.go b/cmd/snaplock/runinhibit/inhibit.go
index 09223f46804..3bec5a86d7f 100644
--- a/cmd/snaplock/runinhibit/inhibit.go
+++ b/cmd/snaplock/runinhibit/inhibit.go
@@ -124,6 +124,12 @@ func removeInhibitInfoFiles(snapName string) error {
return nil
}
+// Unlocker functions are passed from code using runinhibit to indicate that global
+// state should be unlocked during file lock operations to avoid having deadlocks where
+// both inhibition hint file and state are locked and waiting for each other. Unlocker
+// being nil indicates not to do this.
+type Unlocker func() (relock func())
+
// LockWithHint sets a persistent "snap run" inhibition lock, for the given snap, with a given hint
// and saves given info that will be needed by "snap run" during inhibition (e.g. snap revision).
//
@@ -132,7 +138,17 @@ func removeInhibitInfoFiles(snapName string) error {
// start and will block, presenting a user interface if possible. Also
// info.Previous corresponding to the snap revision that was installed must be
// provided and cannot be unset.
-func LockWithHint(snapName string, hint Hint, info InhibitInfo) error {
+//
+// If unlocker is passed it indicates that the global state needs to be unlocked
+// before taking the inhibition hint file lock. It is the responsibility of the
+// caller to make sure state is locked if a non-nil unlocker is passed.
+func LockWithHint(snapName string, hint Hint, info InhibitInfo, unlocker Unlocker) error {
+ if unlocker != nil {
+ // unlock/relock global state
+ relock := unlocker()
+ defer relock()
+ }
+
if err := hint.validate(); err != nil {
return err
}
@@ -179,7 +195,17 @@ func LockWithHint(snapName string, hint Hint, info InhibitInfo) error {
// Unlock truncates the run inhibition lock, for the given snap.
//
// An empty inhibition lock means uninhibited "snap run".
-func Unlock(snapName string) error {
+//
+// If unlocker is passed it indicates that the global state needs to be unlocked
+// before taking the inhibition hint file lock. It is the responsibility of the
+// caller to make sure state is locked if a non-nil unlocker is passed.
+func Unlock(snapName string, unlocker Unlocker) error {
+ if unlocker != nil {
+ // unlock/relock global state
+ relock := unlocker()
+ defer relock()
+ }
+
flock, err := openHintFileLock(snapName)
if os.IsNotExist(err) {
return nil
@@ -214,7 +240,17 @@ func Unlock(snapName string) error {
//
// It returns the current, non-empty hint if inhibition is in place. Otherwise
// it returns an empty hint.
-func IsLocked(snapName string) (Hint, InhibitInfo, error) {
+//
+// If unlocker is passed it indicates that the global state needs to be unlocked
+// before taking the inhibition hint file lock. It is the responsibility of the
+// caller to make sure state is locked if a non-nil unlocker is passed.
+func IsLocked(snapName string, unlocker Unlocker) (Hint, InhibitInfo, error) {
+ if unlocker != nil {
+ // unlock/relock global state
+ relock := unlocker()
+ defer relock()
+ }
+
hintFlock, err := osutil.OpenExistingLockForReading(HintFile(snapName))
if os.IsNotExist(err) {
return "", InhibitInfo{}, nil
@@ -254,7 +290,17 @@ func IsLocked(snapName string) (Hint, InhibitInfo, error) {
// it.
//
// The function does not fail if the inhibition lock does not exist.
-func RemoveLockFile(snapName string) error {
+//
+// If unlocker is passed it indicates that the global state needs to be unlocked
+// before taking the inhibition hint file lock. It is the responsibility of the
+// caller to make sure state is locked if a non-nil unlocker is passed.
+func RemoveLockFile(snapName string, unlocker Unlocker) error {
+ if unlocker != nil {
+ // unlock/relock global state
+ relock := unlocker()
+ defer relock()
+ }
+
hintFlock, err := osutil.OpenExistingLockForReading(HintFile(snapName))
if os.IsNotExist(err) {
return nil
diff --git a/cmd/snaplock/runinhibit/inhibit_test.go b/cmd/snaplock/runinhibit/inhibit_test.go
index 841b191d904..7a51a808b92 100644
--- a/cmd/snaplock/runinhibit/inhibit_test.go
+++ b/cmd/snaplock/runinhibit/inhibit_test.go
@@ -61,7 +61,7 @@ func (s *runInhibitSuite) TestLockWithEmptyHint(c *C) {
_, err := os.Stat(runinhibit.InhibitDir)
c.Assert(os.IsNotExist(err), Equals, true)
- err = runinhibit.LockWithHint("pkg", runinhibit.HintNotInhibited, s.inhibitInfo)
+ err = runinhibit.LockWithHint("pkg", runinhibit.HintNotInhibited, s.inhibitInfo, nil)
c.Assert(err, ErrorMatches, "lock hint cannot be empty")
_, err = os.Stat(runinhibit.InhibitDir)
@@ -73,7 +73,7 @@ func (s *runInhibitSuite) TestLockWithUnsetRevision(c *C) {
_, err := os.Stat(runinhibit.InhibitDir)
c.Assert(os.IsNotExist(err), Equals, true)
- err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R(0)})
+ err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, runinhibit.InhibitInfo{Previous: snap.R(0)}, nil)
c.Assert(err, ErrorMatches, "snap revision cannot be unset")
_, err = os.Stat(runinhibit.InhibitDir)
@@ -95,9 +95,16 @@ func (s *runInhibitSuite) TestLockWithHint(c *C) {
_, err := os.Stat(runinhibit.InhibitDir)
c.Assert(os.IsNotExist(err), Equals, true)
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
expectedInfo := runinhibit.InhibitInfo{Previous: snap.R(42)}
- err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo)
+ err = runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo, fakeUnlocker)
c.Assert(err, IsNil)
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
fi, err := os.Stat(runinhibit.InhibitDir)
c.Assert(err, IsNil)
@@ -112,19 +119,19 @@ func (s *runInhibitSuite) TestLockWithHint(c *C) {
// The lock can be re-acquired to present a different hint.
func (s *runInhibitSuite) TestLockLocked(c *C) {
expectedInfo := runinhibit.InhibitInfo{Previous: snap.R(42)}
- err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo)
+ err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, expectedInfo, nil)
c.Assert(err, IsNil)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "refresh")
testInhibitInfo(c, "pkg", "refresh", expectedInfo)
expectedInfo = runinhibit.InhibitInfo{Previous: snap.R(43)}
- err = runinhibit.LockWithHint("pkg", runinhibit.Hint("just-testing"), expectedInfo)
+ err = runinhibit.LockWithHint("pkg", runinhibit.Hint("just-testing"), expectedInfo, nil)
c.Assert(err, IsNil)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "just-testing")
testInhibitInfo(c, "pkg", "just-testing", expectedInfo)
expectedInfo = runinhibit.InhibitInfo{Previous: snap.R(44)}
- err = runinhibit.LockWithHint("pkg", runinhibit.Hint("short"), expectedInfo)
+ err = runinhibit.LockWithHint("pkg", runinhibit.Hint("short"), expectedInfo, nil)
c.Assert(err, IsNil)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "short")
testInhibitInfo(c, "pkg", "short", expectedInfo)
@@ -132,18 +139,25 @@ func (s *runInhibitSuite) TestLockLocked(c *C) {
// Unlocking an unlocked lock doesn't break anything.
func (s *runInhibitSuite) TestUnlockUnlocked(c *C) {
- err := runinhibit.Unlock("pkg")
+ err := runinhibit.Unlock("pkg", nil)
c.Assert(err, IsNil)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileAbsent)
}
// Unlocking an locked lock truncates the hint and removes inhibit info file.
func (s *runInhibitSuite) TestUnlockLocked(c *C) {
- err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo)
+ err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil)
c.Assert(err, IsNil)
- err = runinhibit.Unlock("pkg")
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
+ err = runinhibit.Unlock("pkg", fakeUnlocker)
c.Assert(err, IsNil)
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileEquals, "")
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.refresh"), testutil.FileAbsent)
@@ -154,26 +168,36 @@ func (s *runInhibitSuite) TestIsLockedMissing(c *C) {
_, err := os.Stat(runinhibit.InhibitDir)
c.Assert(os.IsNotExist(err), Equals, true)
- hint, info, err := runinhibit.IsLocked("pkg")
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
+
+ hint, info, err := runinhibit.IsLocked("pkg", fakeUnlocker)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
err = os.MkdirAll(runinhibit.InhibitDir, 0755)
c.Assert(err, IsNil)
- hint, info, err = runinhibit.IsLocked("pkg")
+ hint, info, err = runinhibit.IsLocked("pkg", fakeUnlocker)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
+ c.Check(unlockerCalled, Equals, 2)
+ c.Check(relockCalled, Equals, 2)
}
// IsLocked returns the previously set hint/info.
func (s *runInhibitSuite) TestIsLockedLocked(c *C) {
- err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo)
+ err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil)
c.Assert(err, IsNil)
- hint, info, err := runinhibit.IsLocked("pkg")
+ hint, info, err := runinhibit.IsLocked("pkg", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh)
c.Check(info, Equals, s.inhibitInfo)
@@ -181,27 +205,37 @@ func (s *runInhibitSuite) TestIsLockedLocked(c *C) {
// IsLocked returns not-inhibited after unlocking.
func (s *runInhibitSuite) TestIsLockedUnlocked(c *C) {
- err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo)
+ err := runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil)
c.Assert(err, IsNil)
- err = runinhibit.Unlock("pkg")
+ err = runinhibit.Unlock("pkg", nil)
c.Assert(err, IsNil)
- hint, info, err := runinhibit.IsLocked("pkg")
+ hint, info, err := runinhibit.IsLocked("pkg", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
}
func (s *runInhibitSuite) TestRemoveLockFile(c *C) {
- c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FilePresent)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.refresh"), testutil.FilePresent)
- c.Assert(runinhibit.RemoveLockFile("pkg"), IsNil)
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
+
+ c.Assert(runinhibit.RemoveLockFile("pkg", fakeUnlocker), IsNil)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileAbsent)
c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.refresh"), testutil.FileAbsent)
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
// Removing an absent lock file is not an error.
- c.Assert(runinhibit.RemoveLockFile("pkg"), IsNil)
+ c.Assert(runinhibit.RemoveLockFile("pkg", fakeUnlocker), IsNil)
+ c.Check(unlockerCalled, Equals, 2)
+ c.Check(relockCalled, Equals, 2)
}
func checkFileLocked(c *C, path string) {
@@ -219,7 +253,7 @@ func checkFileNotLocked(c *C, path string) {
}
func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) {
- c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil)
notInhibitedCalled := 0
inhibitedCalled := 0
@@ -232,7 +266,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) {
if inhibitedCalled == 3 {
// let's remove run inhibtion
- c.Assert(runinhibit.Unlock("pkg"), IsNil)
+ c.Assert(runinhibit.Unlock("pkg", nil), IsNil)
}
return waitCh
@@ -242,7 +276,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) {
notInhibited := func(ctx context.Context) error {
notInhibitedCalled++
- hint, _, err := runinhibit.IsLocked("pkg")
+ hint, _, err := runinhibit.IsLocked("pkg", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
@@ -279,7 +313,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedWalkthrough(c *C) {
}
func (s *runInhibitSuite) TestWaitWhileInhibitedContextCancellation(c *C) {
- c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil)
ctx, cancel := context.WithCancel(context.Background())
@@ -324,7 +358,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedContextCancellation(c *C) {
}
func (s *runInhibitSuite) TestWaitWhileInhibitedNilCallbacks(c *C) {
- c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil)
waitCalled := 0
// closed channel returns immediately
@@ -335,7 +369,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedNilCallbacks(c *C) {
// lock should be released during wait interval
checkFileNotLocked(c, runinhibit.HintFile("pkg"))
// let's remove run inhibtion
- c.Assert(runinhibit.Unlock("pkg"), IsNil)
+ c.Assert(runinhibit.Unlock("pkg", nil), IsNil)
return waitCh
}
@@ -366,7 +400,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedCallbackError(c *C) {
// lock must be released on error
checkFileNotLocked(c, runinhibit.HintFile("pkg"))
- c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil)
inhibited := func(ctx context.Context, hint runinhibit.Hint, inhibitInfo *runinhibit.InhibitInfo) (cont bool, err error) {
return false, fmt.Errorf("inhibited error")
}
@@ -378,7 +412,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedCallbackError(c *C) {
}
func (s *runInhibitSuite) TestWaitWhileInhibitedCont(c *C) {
- c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo), IsNil)
+ c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh, s.inhibitInfo, nil), IsNil)
notInhibitedCalled := 0
inhibitedCalled := 0
@@ -406,7 +440,7 @@ func (s *runInhibitSuite) TestWaitWhileInhibitedCont(c *C) {
c.Check(notInhibitedCalled, Equals, 0)
c.Check(inhibitedCalled, Equals, 1)
- hint, inhibitInfo, err := runinhibit.IsLocked("pkg")
+ hint, inhibitInfo, err := runinhibit.IsLocked("pkg", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh)
c.Check(inhibitInfo, Equals, s.inhibitInfo)
diff --git a/daemon/api.go b/daemon/api.go
index f804c6156bc..ef1491f2497 100644
--- a/daemon/api.go
+++ b/daemon/api.go
@@ -143,6 +143,7 @@ func storeFrom(d *Daemon) snapstate.StoreService {
var (
snapstateStoreInstallGoal = snapstate.StoreInstallGoal
+ snapstatePathUpdateGoal = snapstate.PathUpdateGoal
snapstateInstallWithGoal = snapstate.InstallWithGoal
snapstateInstallPath = snapstate.InstallPath
snapstateInstallPathMany = snapstate.InstallPathMany
@@ -150,8 +151,9 @@ var (
snapstateInstallComponents = snapstate.InstallComponents
snapstateRefreshCandidates = snapstate.RefreshCandidates
snapstateTryPath = snapstate.TryPath
- snapstateUpdate = snapstate.Update
- snapstateUpdateMany = snapstate.UpdateMany
+ snapstateStoreUpdateGoal = snapstate.StoreUpdateGoal
+ snapstateUpdateWithGoal = snapstate.UpdateWithGoal
+ snapstateUpdateOne = snapstate.UpdateOne
snapstateRemove = snapstate.Remove
snapstateRemoveMany = snapstate.RemoveMany
snapstateResolveValSetsEnforcementError = snapstate.ResolveValidationSetsEnforcementError
diff --git a/daemon/api_base_test.go b/daemon/api_base_test.go
index 9ad98018ac1..0892b684e95 100644
--- a/daemon/api_base_test.go
+++ b/daemon/api_base_test.go
@@ -259,6 +259,8 @@ func (s *apiBaseSuite) SetUpTest(c *check.C) {
}))
s.AddCleanup(daemon.MockSnapstateStoreInstallGoal(newStoreInstallGoalRecorder))
+ s.AddCleanup(daemon.MockSnapstatePathUpdateGoal(newPathUpdateGoalRecorder))
+ s.AddCleanup(daemon.MockSnapstateStoreUpdateGoal(newStoreUpdateGoalRecorder))
}
type storeInstallGoalRecorder struct {
@@ -273,6 +275,38 @@ func newStoreInstallGoalRecorder(snaps ...snapstate.StoreSnap) snapstate.Install
}
}
+type pathUpdateGoalRecorder struct {
+ snapstate.UpdateGoal
+ snaps []snapstate.PathSnap
+}
+
+func newPathUpdateGoalRecorder(snaps ...snapstate.PathSnap) snapstate.UpdateGoal {
+ return &pathUpdateGoalRecorder{
+ snaps: snaps,
+ UpdateGoal: snapstate.PathUpdateGoal(snaps...),
+ }
+}
+
+type storeUpdateGoalRecorder struct {
+ snapstate.UpdateGoal
+ snaps []snapstate.StoreUpdate
+}
+
+func (s *storeUpdateGoalRecorder) names() []string {
+ names := make([]string, 0, len(s.snaps))
+ for _, snap := range s.snaps {
+ names = append(names, snap.InstanceName)
+ }
+ return names
+}
+
+func newStoreUpdateGoalRecorder(snaps ...snapstate.StoreUpdate) snapstate.UpdateGoal {
+ return &storeUpdateGoalRecorder{
+ snaps: snaps,
+ UpdateGoal: snapstate.StoreUpdateGoal(snaps...),
+ }
+}
+
func (s *apiBaseSuite) mockModel(st *state.State, model *asserts.Model) {
// realistic model setup
if model == nil {
@@ -402,6 +436,9 @@ func newFakeSnapManager(st *state.State, runner *state.TaskRunner) *fakeSnapMana
runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error {
return fmt.Errorf("fake-install-snap-error errored")
}, nil)
+ runner.AddHandler("fake-refresh-snap", func(t *state.Task, _ *tomb.Tomb) error {
+ return nil
+ }, nil)
return &fakeSnapManager{}
}
diff --git a/daemon/api_model.go b/daemon/api_model.go
index e13c7b5fb9e..540f7206c45 100644
--- a/daemon/api_model.go
+++ b/daemon/api_model.go
@@ -38,6 +38,7 @@ import (
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
)
var (
@@ -128,7 +129,7 @@ func remodelJSON(c *Command, r *http.Request) Response {
return AsyncResponse(nil, chg.ID())
}
-func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedSnap, *asserts.Batch, *apiError) {
+func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedContainer, *asserts.Batch, *apiError) {
// New model
model := form.Values["new-model"]
if len(model) != 1 {
@@ -141,7 +142,7 @@ func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedSnap, *asser
}
// Snap files
- var snapFiles []*uploadedSnap
+ var snapFiles []*uploadedContainer
if len(form.FileRefs["snap"]) > 0 {
snaps, errRsp := form.GetSnapFiles()
if errRsp != nil {
@@ -164,7 +165,7 @@ func readOfflineRemodelForm(form *Form) (*asserts.Model, []*uploadedSnap, *asser
}
func startOfflineRemodelChange(st *state.State, newModel *asserts.Model,
- snapFiles []*uploadedSnap, batch *asserts.Batch, pathsToNotRemove *[]string) (
+ snapFiles []*uploadedContainer, batch *asserts.Batch, pathsToNotRemove *[]string) (
*state.Change, *apiError) {
st.Lock()
@@ -185,19 +186,23 @@ func startOfflineRemodelChange(st *state.State, newModel *asserts.Model,
return nil, apiErr
}
- *pathsToNotRemove = make([]string, len(slInfo.sideInfos))
- for i, psi := range slInfo.sideInfos {
+ *pathsToNotRemove = make([]string, 0, len(slInfo.snaps))
+ sideInfos := make([]*snap.SideInfo, 0, len(slInfo.snaps))
+ paths := make([]string, 0, len(slInfo.snaps))
+ for _, psi := range slInfo.snaps {
// Move file to the same name of what a downloaded one would have
dest := filepath.Join(dirs.SnapBlobDir,
- fmt.Sprintf("%s_%s.snap", psi.RealName, psi.Revision))
- os.Rename(slInfo.tmpPaths[i], dest)
+ fmt.Sprintf("%s_%s.snap", psi.sideInfo.RealName, psi.sideInfo.Revision))
+ os.Rename(psi.tmpPath, dest)
// Avoid trying to remove a file that does not exist anymore
- (*pathsToNotRemove)[i] = slInfo.tmpPaths[i]
- slInfo.tmpPaths[i] = dest
+ *pathsToNotRemove = append(*pathsToNotRemove, psi.tmpPath)
+
+ sideInfos = append(sideInfos, psi.sideInfo)
+ paths = append(paths, dest)
}
// Now create and start the remodel change
- chg, err := devicestateRemodel(st, newModel, slInfo.sideInfos, slInfo.tmpPaths, devicestate.RemodelOptions{
+ chg, err := devicestateRemodel(st, newModel, sideInfos, paths, devicestate.RemodelOptions{
// since this is the codepath that parses the form, offline is implcit
// because local snaps are being provided.
Offline: true,
diff --git a/daemon/api_sideload_n_try.go b/daemon/api_sideload_n_try.go
index e5e4ad8c35f..55cf8ceb254 100644
--- a/daemon/api_sideload_n_try.go
+++ b/daemon/api_sideload_n_try.go
@@ -28,6 +28,7 @@ import (
"mime/multipart"
"os"
"path/filepath"
+ "strings"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/snapasserts"
@@ -79,10 +80,10 @@ func (f *Form) RemoveAllExcept(paths []string) {
}
}
-type uploadedSnap struct {
- // filename is the original name/path of the snap file.
+type uploadedContainer struct {
+ // filename is the original name/path of the container file.
filename string
- // tmpPath is the location where the temp snap file is stored.
+ // tmpPath is the location where the temp container file is stored.
tmpPath string
// instanceName is optional and can only be set if only one snap was uploaded.
instanceName string
@@ -91,14 +92,14 @@ type uploadedSnap struct {
// GetSnapFiles returns the original name and temp path for each snap file in
// the form. Optionally, it might include a requested instance name, but only
// if the was only one file in the form.
-func (f *Form) GetSnapFiles() ([]*uploadedSnap, *apiError) {
+func (f *Form) GetSnapFiles() ([]*uploadedContainer, *apiError) {
if len(f.FileRefs["snap"]) == 0 {
return nil, BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`)
}
refs := f.FileRefs["snap"]
if len(refs) == 1 && len(f.Values["snap-path"]) > 0 {
- uploaded := &uploadedSnap{
+ uploaded := &uploadedContainer{
filename: f.Values["snap-path"][0],
tmpPath: refs[0].TmpPath,
}
@@ -106,12 +107,12 @@ func (f *Form) GetSnapFiles() ([]*uploadedSnap, *apiError) {
if len(f.Values["name"]) > 0 {
uploaded.instanceName = f.Values["name"][0]
}
- return []*uploadedSnap{uploaded}, nil
+ return []*uploadedContainer{uploaded}, nil
}
- snapFiles := make([]*uploadedSnap, len(refs))
+ snapFiles := make([]*uploadedContainer, len(refs))
for i, ref := range refs {
- snapFiles[i] = &uploadedSnap{
+ snapFiles[i] = &uploadedContainer{
filename: ref.Filename,
tmpPath: ref.TmpPath,
}
@@ -218,39 +219,163 @@ func sideloadOrTrySnap(ctx context.Context, c *Command, body io.ReadCloser, boun
// sideloadedInfo contains information from a bunch of sideloaded snaps
type sideloadedInfo struct {
- sideInfos []*snap.SideInfo
- names, origPaths, tmpPaths []string
+ // snaps contains the set of snaps that should be sideloaded. Any components
+ // associated with these snaps that are being sideloaded will be inside of
+ // the sideloadSnapInfo for that snap.
+ snaps []sideloadSnapInfo
+ // components contains the set of components that should be sideloaded, but
+ // their associated snaps are not being sideloaded (they must already be
+ // installed).
+ components []sideloadComponentInfo
}
-func sideloadInfo(st *state.State, snapFiles []*uploadedSnap, flags sideloadFlags) (*sideloadedInfo, *apiError) {
+type sideloadSnapInfo struct {
+ sideInfo *snap.SideInfo
+ components []sideloadComponentInfo
+ origPath string
+ tmpPath string
+}
+
+type sideloadComponentInfo struct {
+ sideInfo *snap.ComponentSideInfo
+ origPath string
+ tmpPath string
+}
+
+func sideloadInfo(st *state.State, uploads []*uploadedContainer, flags sideloadFlags) (*sideloadedInfo, *apiError) {
deviceCtx, err := snapstate.DevicePastSeeding(st, nil)
if err != nil {
return nil, InternalError(err.Error())
}
- names := make([]string, len(snapFiles))
- origPaths := make([]string, len(snapFiles))
- tmpPaths := make([]string, len(snapFiles))
- sideInfos := make([]*snap.SideInfo, len(snapFiles))
+ var components []sideloadComponentInfo
+ var snaps []sideloadSnapInfo
+ for _, upload := range uploads {
+ si, snapErr := readSideInfo(st, upload.tmpPath, upload.filename, flags, deviceCtx.Model())
+ if snapErr != nil {
+ if !flags.dangerousOK {
+ // TODO:COMPS: read assertions for components
+ return nil, snapErr
+ }
+
+ ci, err := readComponentInfoFromCont(upload.tmpPath, nil)
+ if err != nil {
+ logger.Noticef("cannot sideload as a snap: %v", snapErr)
+ logger.Noticef("cannot sideload as a component: %v", err)
- for i, snapFile := range snapFiles {
- si, apiError := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags, deviceCtx.Model())
- if apiError != nil {
- return nil, apiError
+ // note that here we forward the error from reading the snap
+ // file, rather than the component file. this is consistent with
+ // what we do when installing one component from file. maybe
+ // something to change?
+ return nil, snapErr
+ }
+
+ components = append(components, sideloadComponentInfo{
+ sideInfo: &snap.ComponentSideInfo{
+ Component: ci.Component,
+ Revision: snap.Revision{},
+ },
+ origPath: upload.filename,
+ tmpPath: upload.tmpPath,
+ })
+ continue
}
- sideInfos[i] = si
- names[i] = si.RealName
- origPaths[i] = snapFile.filename
- tmpPaths[i] = snapFile.tmpPath
+ snaps = append(snaps, sideloadSnapInfo{
+ sideInfo: si,
+ origPath: upload.filename,
+ tmpPath: upload.tmpPath,
+ })
+ }
+
+ // we use this function here to get a pointer to an element in the snaps
+ // slice so that we can modify it. we can't just create a mapping in the
+ // above loop since the pointers could get invalidated the snaps slice
+ // growing.
+ snapByName := func(name string) (*sideloadSnapInfo, bool) {
+ for i := range snaps {
+ if snaps[i].sideInfo.RealName == name {
+ return &snaps[i], true
+ }
+ }
+ return nil, false
}
- return &sideloadedInfo{sideInfos: sideInfos, names: names,
- origPaths: origPaths, tmpPaths: tmpPaths}, nil
+ onlyComponents := make([]sideloadComponentInfo, 0)
+ for _, ci := range components {
+ snapName := ci.sideInfo.Component.SnapName
+
+ ssi, ok := snapByName(snapName)
+ if !ok {
+ onlyComponents = append(onlyComponents, ci)
+ continue
+ }
+
+ ssi.components = append(ssi.components, ci)
+ }
+
+ return &sideloadedInfo{
+ components: onlyComponents,
+ snaps: snaps,
+ }, nil
}
-func sideloadManySnaps(ctx context.Context, st *state.State, snapFiles []*uploadedSnap, flags sideloadFlags, user *auth.UserState) (*state.Change, *apiError) {
- slInfo, apiErr := sideloadInfo(st, snapFiles, flags)
+func sideloadTaskSets(ctx context.Context, st *state.State, sideload *sideloadedInfo, userID int, flags snapstate.Flags) ([]*state.TaskSet, *apiError) {
+ if flags.Transaction == client.TransactionAllSnaps && flags.Lane == 0 {
+ flags.Lane = st.NewLane()
+ }
+
+ var tss []*state.TaskSet
+
+ // handle all of the components whose snaps are not present in the set of
+ // files that are being sideloaded
+ for _, comp := range sideload.components {
+ snapName := comp.sideInfo.Component.SnapName
+ snapInfo, err := installedSnapInfo(st, snapName)
+ if err != nil {
+ if errors.Is(err, state.ErrNoState) {
+ return nil, SnapNotInstalled(snapName, fmt.Errorf("snap owning %q not installed", comp.sideInfo.Component))
+ }
+ return nil, BadRequest("cannot retrieve information for %q: %v", snapName, err)
+ }
+
+ ts, err := snapstateInstallComponentPath(st, comp.sideInfo, snapInfo, comp.tmpPath, snapstate.Options{
+ Flags: flags,
+ })
+ if err != nil {
+ return nil, errToResponse(err, nil, InternalError, "cannot install component %q for snap %q: %v", comp.sideInfo.Component, snapName, err)
+ }
+ tss = append(tss, ts)
+ }
+
+ // handle everything else
+ var pathSnaps []snapstate.PathSnap
+ for _, sn := range sideload.snaps {
+ comps := make(map[*snap.ComponentSideInfo]string, len(sn.components))
+ for _, ci := range sn.components {
+ comps[ci.sideInfo] = ci.tmpPath
+ }
+
+ pathSnaps = append(pathSnaps, snapstate.PathSnap{
+ Path: sn.tmpPath,
+ SideInfo: sn.sideInfo,
+ Components: comps,
+ })
+ }
+
+ _, uts, err := snapstateUpdateWithGoal(ctx, st, snapstatePathUpdateGoal(pathSnaps...), nil, snapstate.Options{
+ UserID: userID,
+ Flags: flags,
+ })
+ if err != nil {
+ return nil, errToResponse(err, nil, InternalError, "cannot install snap/component files: %v")
+ }
+
+ return append(tss, uts.Refresh...), nil
+}
+
+func sideloadManySnaps(ctx context.Context, st *state.State, uploads []*uploadedContainer, flags sideloadFlags, user *auth.UserState) (*state.Change, *apiError) {
+ slInfo, apiErr := sideloadInfo(st, uploads, flags)
if apiErr != nil {
return nil, apiErr
}
@@ -260,19 +385,109 @@ func sideloadManySnaps(ctx context.Context, st *state.State, snapFiles []*upload
userID = user.ID
}
- tss, err := snapstateInstallPathMany(ctx, st, slInfo.sideInfos, slInfo.tmpPaths, userID, &flags.Flags)
+ tss, err := sideloadTaskSets(ctx, st, slInfo, userID, flags.Flags)
if err != nil {
- return nil, errToResponse(err, slInfo.names, InternalError, "cannot install snap files: %v")
+ return nil, err
+ }
+
+ snapNames := make([]string, 0, len(slInfo.snaps))
+ snapToComps := make(map[string][]string, len(slInfo.components))
+ for _, sn := range slInfo.snaps {
+ snapName := sn.sideInfo.RealName
+ snapNames = append(snapNames, snapName)
+
+ if len(sn.components) == 0 {
+ continue
+ }
+
+ snapToComps[snapName] = make([]string, 0, len(sn.components))
+ for _, c := range sn.components {
+ snapToComps[snapName] = append(snapToComps[snapName], c.sideInfo.Component.ComponentName)
+ }
+ }
+
+ for _, ci := range slInfo.components {
+ snapToComps[ci.sideInfo.Component.SnapName] = append(snapToComps[ci.sideInfo.Component.SnapName], ci.sideInfo.Component.ComponentName)
+ }
+
+ msg := multiPathInstallMessage(slInfo)
+
+ chg := newChange(st, "install-snap", msg, tss, snapNames)
+ apiData := make(map[string]interface{}, 0)
+
+ if len(snapNames) > 0 {
+ apiData["snap-names"] = snapNames
}
- msg := fmt.Sprintf(i18n.G("Install snaps %s from files %s"), strutil.Quoted(slInfo.names), strutil.Quoted(slInfo.origPaths))
- chg := newChange(st, "install-snap", msg, tss, slInfo.names)
- chg.Set("api-data", map[string][]string{"snap-names": slInfo.names})
+ if len(snapToComps) > 0 {
+ apiData["components"] = snapToComps
+ }
+ chg.Set("api-data", apiData)
return chg, nil
}
-func sideloadSnap(_ context.Context, st *state.State, snapFile *uploadedSnap, flags sideloadFlags) (*state.Change, *apiError) {
+func multiPathInstallMessage(sli *sideloadedInfo) string {
+ var b strings.Builder
+ switch len(sli.snaps) {
+ case 0:
+ b.WriteString(i18n.G("Install"))
+ case 1:
+ b.WriteString(i18n.G("Install snap"))
+ default:
+ b.WriteString(i18n.G("Install snaps"))
+ }
+
+ var paths []string
+ for i, sn := range sli.snaps {
+ fmt.Fprintf(&b, " %q", sn.sideInfo.RealName)
+ paths = append(paths, sn.origPath)
+
+ comps := make([]string, 0, len(sn.components))
+ for _, c := range sn.components {
+ comps = append(comps, c.sideInfo.Component.ComponentName)
+ paths = append(paths, c.origPath)
+ }
+
+ if len(comps) > 0 {
+ b.WriteString(" (")
+ if len(sn.components) > 1 {
+ fmt.Fprintf(&b, i18n.G("with components %s"), strutil.Quoted(comps))
+ } else {
+ fmt.Fprintf(&b, i18n.G("with component %s"), strutil.Quoted(comps))
+ }
+ b.WriteRune(')')
+ }
+
+ if i < len(sli.snaps)-1 {
+ b.WriteRune(',')
+ }
+ }
+
+ compNames := make([]string, 0, len(sli.components))
+ for _, c := range sli.components {
+ compNames = append(compNames, c.sideInfo.Component.String())
+ paths = append(paths, c.origPath)
+ }
+
+ if len(sli.snaps) != 0 && len(sli.components) != 0 {
+ b.WriteString(i18n.G(" and"))
+ }
+
+ switch len(sli.components) {
+ case 0:
+ case 1:
+ fmt.Fprintf(&b, i18n.G(" component %s"), strutil.Quoted(compNames))
+ default:
+ fmt.Fprintf(&b, i18n.G(" components %s"), strutil.Quoted(compNames))
+ }
+
+ fmt.Fprintf(&b, " from files %s", strutil.Quoted(paths))
+
+ return b.String()
+}
+
+func sideloadSnap(_ context.Context, st *state.State, snapFile *uploadedContainer, flags sideloadFlags) (*state.Change, *apiError) {
var instanceName string
if snapFile.instanceName != "" {
// caller has specified desired instance name
diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go
index b97fb1a8eb0..0a9fb12ba19 100644
--- a/daemon/api_sideload_n_try_test.go
+++ b/daemon/api_sideload_n_try_test.go
@@ -941,21 +941,25 @@ func (s *sideloadSuite) TestSideloadCleanUpUnusedTempSnapFiles(c *check.C) {
func (s *sideloadSuite) TestSideloadManySnaps(c *check.C) {
d := s.daemonWithFakeSnapManager(c)
s.markSeeded(d)
- expectedFlags := &snapstate.Flags{RemoveSnapPath: true, DevMode: true, Transaction: client.TransactionAllSnaps}
+ expectedFlags := snapstate.Flags{RemoveSnapPath: true, DevMode: true, Transaction: client.TransactionAllSnaps, Lane: 1}
- restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, tmpPaths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) {
- c.Check(flags, check.DeepEquals, expectedFlags)
- c.Check(userID, check.Not(check.Equals), 0)
+ restore := daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*pathUpdateGoalRecorder)
+
+ c.Check(opts.Flags, check.DeepEquals, expectedFlags)
+ c.Check(opts.UserID, check.Not(check.Equals), 0)
var tss []*state.TaskSet
- for i, si := range infos {
- c.Check(tmpPaths[i], testutil.FileEquals, si.RealName)
+ var names []string
+ for _, sn := range goal.snaps {
+ c.Check(sn.Path, testutil.FileEquals, sn.SideInfo.RealName)
- ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName)))
+ ts := state.NewTaskSet(st.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", sn.SideInfo.RealName)))
tss = append(tss, ts)
+ names = append(names, sn.InstanceName)
}
- return tss, nil
+ return names, &snapstate.UpdateTaskSets{Refresh: tss}, nil
})
defer restore()
@@ -1005,10 +1009,278 @@ func (s *sideloadSuite) TestSideloadManySnaps(c *check.C) {
c.Check(data["snap-names"], check.DeepEquals, snaps)
}
+type sideloadSnapsAndComponentsOpts struct {
+ missingSnap bool
+}
+
+func (s *sideloadSuite) TestSideloadManySnapsAndComponentsMissingSnap(c *check.C) {
+ s.testSideloadManySnapsAndComponents(c, sideloadSnapsAndComponentsOpts{missingSnap: true})
+}
+
+func (s *sideloadSuite) TestSideloadManySnapsAndComponents(c *check.C) {
+ s.testSideloadManySnapsAndComponents(c, sideloadSnapsAndComponentsOpts{})
+}
+
+func (s *sideloadSuite) testSideloadManySnapsAndComponents(c *check.C, opts sideloadSnapsAndComponentsOpts) {
+ d := s.daemonWithFakeSnapManager(c)
+ s.markSeeded(d)
+ expectedFlags := snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionAllSnaps, Lane: 1}
+
+ restore := daemon.MockSnapstateInstallComponentPath(func(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info,
+ path string, opts snapstate.Options) (*state.TaskSet, error) {
+ c.Check(csi.Component.SnapName, check.Equals, "three")
+ c.Check(csi.Component.ComponentName, check.Equals, "comp-four")
+ c.Check(opts.Flags, check.DeepEquals, expectedFlags)
+ c.Check(path, testutil.FileEquals, "comp-four")
+
+ t := st.NewTask("fake-install-component", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ })
+ defer restore()
+
+ snaps := []string{"one", "two"}
+ expectedSnapsToComps := map[string][]string{
+ "one": {"comp-one"},
+ "two": {"comp-two", "comp-three"},
+ }
+ components := []string{"comp-one", "comp-two", "comp-three", "comp-four"}
+
+ st := d.Overlord().State()
+
+ if !opts.missingSnap {
+ ssi := &snap.SideInfo{
+ RealName: "three",
+ Revision: snap.R(1),
+ SnapID: "three-snap-id",
+ }
+ st.Lock()
+ snapstate.Set(d.Overlord().State(), "three", &snapstate.SnapState{
+ Active: true,
+ Sequence: snapstatetest.NewSequenceFromRevisionSideInfos([]*sequence.RevisionSideState{
+ sequence.NewRevisionSideState(ssi, nil),
+ }),
+ Current: snap.R(1),
+ })
+ st.Unlock()
+ }
+
+ restore = daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*pathUpdateGoalRecorder)
+
+ c.Check(opts.Flags, check.DeepEquals, expectedFlags)
+ c.Check(opts.UserID, check.Not(check.Equals), 0)
+
+ var tss []*state.TaskSet
+ var names []string
+ for _, sn := range goal.snaps {
+ comps, ok := expectedSnapsToComps[sn.SideInfo.RealName]
+ c.Assert(ok, check.Equals, true, check.Commentf("unexpected snap name %q", sn.SideInfo.RealName))
+ foundComps := make([]string, 0, len(comps))
+ for csi := range sn.Components {
+ foundComps = append(foundComps, csi.Component.ComponentName)
+ }
+ c.Check(foundComps, testutil.DeepUnsortedMatches, comps)
+
+ c.Check(sn.Path, testutil.FileEquals, sn.SideInfo.RealName)
+
+ ts := state.NewTaskSet(st.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", sn.SideInfo.RealName)))
+ tss = append(tss, ts)
+ names = append(names, sn.InstanceName)
+ }
+
+ return names, &snapstate.UpdateTaskSets{Refresh: tss}, nil
+ })
+ defer restore()
+
+ readComponentInfoCalled := -1
+ restore = daemon.MockReadComponentInfoFromCont(func(p string, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
+ readComponentInfoCalled++
+ var snapName string
+ switch readComponentInfoCalled {
+ case 0:
+ snapName = "one"
+ case 1, 2:
+ snapName = "two"
+ case 3:
+ snapName = "three"
+ }
+ return &snap.ComponentInfo{
+ Component: naming.NewComponentRef(snapName, components[readComponentInfoCalled]),
+ Type: snap.TestComponent,
+ CompVersion: "1.0",
+ }, nil
+ })
+ defer restore()
+
+ readSnapInfoCalled := -1
+ restore = daemon.MockUnsafeReadSnapInfo(func(p string) (*snap.Info, error) {
+ readSnapInfoCalled++
+ switch readSnapInfoCalled {
+ case 0, 1:
+ return &snap.Info{SuggestedName: snaps[readSnapInfoCalled]}, nil
+ default:
+ return nil, errors.New("no more snaps")
+ }
+ })
+ defer restore()
+
+ body := "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"dangerous\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n"
+ body += "Content-Disposition: form-data; name=\"transaction\"\r\n" +
+ "\r\n" +
+ "all-snaps\r\n" +
+ "----hello--\r\n"
+
+ containers := make([]string, len(snaps)+len(components))
+ copy(containers, snaps)
+ copy(containers[len(snaps):], components)
+ for _, c := range containers {
+ body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + c + "\"\r\n" +
+ "\r\n" +
+ c + "\r\n" +
+ "----hello--\r\n"
+ }
+
+ req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
+ s.asUserAuth(c, req)
+
+ if opts.missingSnap {
+ rsp := s.errorReq(c, req, s.authUser)
+ c.Check(rsp.Message, check.Equals, `snap owning "three+comp-four" not installed`)
+
+ return
+ }
+
+ rsp := s.asyncReq(c, req, s.authUser)
+
+ st.Lock()
+ defer st.Unlock()
+
+ expectedFileNames := []string{"file-one", "file-comp-one", "file-two", "file-comp-two", "file-comp-three", "file-comp-four"}
+
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+ c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps "one" (with component "comp-one"), "two" (with components "comp-two", "comp-three") and component "three+comp-four" from files %s`, strutil.Quoted(expectedFileNames)))
+
+ var data map[string]interface{}
+ c.Assert(chg.Get("api-data", &data), check.IsNil)
+ c.Check(data, check.DeepEquals, map[string]interface{}{
+ "snap-names": []interface{}{"one", "two"},
+ "components": map[string]interface{}{
+ "one": []interface{}{"comp-one"},
+ "two": []interface{}{"comp-two", "comp-three"},
+ "three": []interface{}{"comp-four"},
+ },
+ })
+}
+
+func (s *sideloadSuite) TestSideloadManyOnlyComponents(c *check.C) {
+ d := s.daemonWithFakeSnapManager(c)
+ s.markSeeded(d)
+ expectedFlags := snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionAllSnaps, Lane: 1}
+
+ components := []string{"comp-one", "comp-two", "comp-three", "comp-four"}
+ restore := daemon.MockSnapstateInstallComponentPath(func(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info,
+ path string, opts snapstate.Options) (*state.TaskSet, error) {
+ c.Check(csi.Component.SnapName, check.Equals, "one")
+ c.Check(components, testutil.Contains, csi.Component.ComponentName)
+ c.Check(opts.Flags, check.DeepEquals, expectedFlags)
+ c.Check(path, testutil.FileEquals, csi.Component.ComponentName)
+
+ t := st.NewTask("fake-install-component", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ })
+ defer restore()
+
+ st := d.Overlord().State()
+
+ ssi := &snap.SideInfo{
+ RealName: "one",
+ Revision: snap.R(1),
+ SnapID: "one-snap-id",
+ }
+ st.Lock()
+ snapstate.Set(d.Overlord().State(), "one", &snapstate.SnapState{
+ Active: true,
+ Sequence: snapstatetest.NewSequenceFromRevisionSideInfos([]*sequence.RevisionSideState{
+ sequence.NewRevisionSideState(ssi, nil),
+ }),
+ Current: snap.R(1),
+ })
+ st.Unlock()
+
+ readComponentInfoCalled := -1
+ restore = daemon.MockReadComponentInfoFromCont(func(p string, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
+ readComponentInfoCalled++
+ return &snap.ComponentInfo{
+ Component: naming.NewComponentRef("one", components[readComponentInfoCalled]),
+ Type: snap.TestComponent,
+ CompVersion: "1.0",
+ }, nil
+ })
+ defer restore()
+
+ restore = daemon.MockUnsafeReadSnapInfo(func(p string) (*snap.Info, error) {
+ return nil, errors.New("no more snaps")
+ })
+ defer restore()
+
+ body := "----hello--\r\n" +
+ "Content-Disposition: form-data; name=\"dangerous\"\r\n" +
+ "\r\n" +
+ "true\r\n" +
+ "----hello--\r\n"
+ body += "Content-Disposition: form-data; name=\"transaction\"\r\n" +
+ "\r\n" +
+ "all-snaps\r\n" +
+ "----hello--\r\n"
+
+ for _, c := range components {
+ body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + c + "\"\r\n" +
+ "\r\n" +
+ c + "\r\n" +
+ "----hello--\r\n"
+ }
+
+ req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
+ s.asUserAuth(c, req)
+
+ rsp := s.asyncReq(c, req, s.authUser)
+
+ st.Lock()
+ defer st.Unlock()
+
+ expectedFileNames := []string{"file-comp-one", "file-comp-two", "file-comp-three", "file-comp-four"}
+
+ fullComponentNames := make([]string, len(components))
+ for i, c := range components {
+ fullComponentNames[i] = "one+" + c
+ }
+
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+ c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install components %s from files %s`, strutil.Quoted(fullComponentNames), strutil.Quoted(expectedFileNames)))
+
+ var data map[string]interface{}
+ c.Assert(chg.Get("api-data", &data), check.IsNil)
+ c.Check(data, check.DeepEquals, map[string]interface{}{
+ "components": map[string]interface{}{
+ "one": []interface{}{"comp-one", "comp-two", "comp-three", "comp-four"},
+ },
+ })
+}
+
func (s *sideloadSuite) TestSideloadManyFailInstallPathMany(c *check.C) {
s.daemon(c)
- restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) {
- return nil, errors.New("expected")
+ restore := daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ return nil, nil, errors.New("expected")
})
defer restore()
@@ -1035,7 +1307,7 @@ func (s *sideloadSuite) TestSideloadManyFailInstallPathMany(c *check.C) {
apiErr := s.errorReq(c, req, nil)
c.Check(apiErr.JSON().Status, check.Equals, 500)
- c.Check(apiErr.Message, check.Equals, `cannot install snap files: expected`)
+ c.Check(apiErr.Message, check.Equals, `cannot install snap/component files: expected`)
}
func (s *sideloadSuite) TestSideloadManyFailUnsafeReadInfo(c *check.C) {
@@ -1115,22 +1387,26 @@ func (s *sideloadSuite) TestSideloadManySnapsAsserted(c *check.C) {
expectedFlags := snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionPerSnap}
- restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) {
- c.Check(*flags, check.DeepEquals, expectedFlags)
+ restore := daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*pathUpdateGoalRecorder)
+
+ c.Check(opts.Flags, check.DeepEquals, expectedFlags)
var tss []*state.TaskSet
- for i, si := range infos {
- c.Check(*si, check.DeepEquals, snap.SideInfo{
+ var names []string
+ for i, sn := range goal.snaps {
+ c.Check(*sn.SideInfo, check.DeepEquals, snap.SideInfo{
RealName: snaps[i],
SnapID: snaps[i] + "-id",
Revision: snap.R(41),
})
- ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName)))
+ ts := state.NewTaskSet(st.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", sn.SideInfo.RealName)))
tss = append(tss, ts)
+ names = append(names, sn.InstanceName)
}
- return tss, nil
+ return names, &snapstate.UpdateTaskSets{Refresh: tss}, nil
})
defer restore()
diff --git a/daemon/api_snaps.go b/daemon/api_snaps.go
index 342b38ac199..d2548a23d9e 100644
--- a/daemon/api_snaps.go
+++ b/daemon/api_snaps.go
@@ -416,7 +416,7 @@ func (inst *snapInstruction) validate() error {
if len(inst.CompsRaw) > 0 {
switch inst.Action {
- case "remove", "install":
+ case "remove", "install", "refresh":
default:
return fmt.Errorf("%q action is not supported for components", inst.Action)
}
@@ -463,12 +463,10 @@ func snapInstall(ctx context.Context, inst *snapInstruction, st *state.State) (*
return nil, errors.New(i18n.G("cannot install snap with empty name"))
}
- var ckey string
if inst.CohortKey == "" {
logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision)
} else {
- ckey = strutil.ElliptLeft(inst.CohortKey, 10)
- logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey)
+ logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10))
}
installedSnaps, installedComponents, tss, err := installationTaskSets(ctx, st, inst)
@@ -477,24 +475,29 @@ func snapInstall(ctx context.Context, inst *snapInstruction, st *state.State) (*
}
return &snapInstructionResult{
- Summary: installMessage(inst, ckey),
+ Summary: installRefreshMessage(inst.Snaps[0], inst),
Tasksets: tss,
Affected: installedSnaps,
AffectedComponents: installedComponents,
}, nil
}
-func installMessage(inst *snapInstruction, cohort string) string {
+func installRefreshMessage(snapName string, inst *snapInstruction) string {
var b strings.Builder
- fmt.Fprintf(&b, i18n.G("Install %q snap"), inst.Snaps[0])
+ if inst.Action == "install" {
+ fmt.Fprintf(&b, i18n.G("Install %q snap"), snapName)
+ } else {
+ fmt.Fprintf(&b, i18n.G("Refresh %q snap"), snapName)
+ }
+
if inst.Channel != "stable" && inst.Channel != "" {
fmt.Fprintf(&b, i18n.G(" from %q channel"), inst.Channel)
}
if inst.CohortKey != "" {
- fmt.Fprintf(&b, i18n.G(" from %q cohort"), cohort)
+ fmt.Fprintf(&b, i18n.G(" from %q cohort"), strutil.ElliptLeft(inst.CohortKey, 10))
}
- if comps := inst.CompsForSnaps[inst.Snaps[0]]; len(comps) > 0 {
+ if comps := inst.CompsForSnaps[snapName]; len(comps) > 0 {
if len(comps) > 1 {
fmt.Fprintf(&b, i18n.G(" with components %s"), strutil.Quoted(comps))
} else {
@@ -505,15 +508,19 @@ func installMessage(inst *snapInstruction, cohort string) string {
return b.String()
}
-func multiInstallMessage(inst *snapInstruction) string {
- if len(inst.Snaps) == 1 {
- return installMessage(inst, "")
+func multiInstallRefreshMessage(snaps []string, inst *snapInstruction) string {
+ if len(snaps) == 1 {
+ return installRefreshMessage(snaps[0], inst)
}
var b strings.Builder
- fmt.Fprint(&b, i18n.G("Install snaps"))
+ if inst.Action == "install" {
+ fmt.Fprint(&b, i18n.G("Install snaps"))
+ } else {
+ fmt.Fprint(&b, i18n.G("Refresh snaps"))
+ }
- for i, name := range inst.Snaps {
+ for i, name := range snaps {
fmt.Fprintf(&b, " %q", name)
if comps := inst.CompsForSnaps[name]; len(comps) > 0 {
@@ -526,14 +533,14 @@ func multiInstallMessage(inst *snapInstruction) string {
b.WriteRune(')')
}
- if i < len(inst.Snaps)-1 {
+ if i < len(snaps)-1 {
b.WriteRune(',')
}
}
return b.String()
}
-func snapUpdate(_ context.Context, inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
+func snapUpdate(ctx context.Context, inst *snapInstruction, st *state.State) (*snapInstructionResult, error) {
// TODO: bail if revision is given (and != current?), *or* behave as with install --revision?
flags, err := inst.modeFlags()
if err != nil {
@@ -554,18 +561,28 @@ func snapUpdate(_ context.Context, inst *snapInstruction, st *state.State) (*sna
return nil, err
}
- ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
- if err != nil {
- return nil, err
+ // TODO: once we completely move away from the old snapstate API, this
+ // backwards compatibility bit should be removed
+ if flags.Transaction == "" {
+ flags.Transaction = client.TransactionPerSnap
}
- msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0])
- if inst.Channel != "stable" && inst.Channel != "" {
- msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel)
+ goal := snapstateStoreUpdateGoal(snapstate.StoreUpdate{
+ InstanceName: inst.Snaps[0],
+ RevOpts: *inst.revnoOpts(),
+ AdditionalComponents: inst.CompsForSnaps[inst.Snaps[0]],
+ })
+
+ ts, err := snapstateUpdateOne(ctx, st, goal, nil, snapstate.Options{
+ Flags: flags,
+ UserID: inst.userID,
+ })
+ if err != nil {
+ return nil, err
}
return &snapInstructionResult{
- Summary: msg,
+ Summary: installRefreshMessage(inst.Snaps[0], inst),
Tasksets: []*state.TaskSet{ts},
Affected: inst.Snaps,
AffectedComponents: inst.CompsForSnaps,
@@ -949,7 +966,7 @@ func snapInstallMany(ctx context.Context, inst *snapInstruction, st *state.State
}
return &snapInstructionResult{
- Summary: multiInstallMessage(inst),
+ Summary: multiInstallRefreshMessage(inst.Snaps, inst),
Affected: installedSnaps,
AffectedComponents: installedComponents,
Tasksets: tasksets,
@@ -968,10 +985,28 @@ func snapUpdateMany(ctx context.Context, inst *snapInstruction, st *state.State)
return nil, err
}
- transaction := inst.Transaction
- updated, tasksets, err := snapstateUpdateMany(ctx, st, inst.Snaps, nil, inst.userID, &snapstate.Flags{
+ updates := make([]snapstate.StoreUpdate, 0, len(inst.Snaps))
+ for _, name := range inst.Snaps {
+ updates = append(updates, snapstate.StoreUpdate{
+ InstanceName: name,
+ AdditionalComponents: inst.CompsForSnaps[name],
+ })
+ }
+
+ flags := snapstate.Flags{
IgnoreRunning: inst.IgnoreRunning,
- Transaction: transaction,
+ Transaction: inst.Transaction,
+ }
+
+ // TODO: once we completely move away from the old snapstate API, this
+ // backwards compatibility bit should be removed
+ if flags.Transaction == "" {
+ flags.Transaction = client.TransactionPerSnap
+ }
+
+ goal := snapstateStoreUpdateGoal(updates...)
+ updated, uts, err := snapstateUpdateWithGoal(ctx, st, goal, nil, snapstate.Options{
+ Flags: flags,
})
if err != nil {
if opts.IsRefreshOfAllSnaps {
@@ -981,6 +1016,7 @@ func snapUpdateMany(ctx context.Context, inst *snapInstruction, st *state.State)
}
return nil, err
}
+ tasksets := uts.Refresh
var msg string
switch len(updated) {
@@ -992,11 +1028,9 @@ func snapUpdateMany(ctx context.Context, inst *snapInstruction, st *state.State)
msg = i18n.G("Refresh all snaps: no updates")
}
case 1:
- msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0])
+ msg = installRefreshMessage(updated[0], inst)
default:
- quoted := strutil.Quoted(updated)
- // TRANSLATORS: the %s is a comma-separated list of quoted snap names
- msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted)
+ msg = multiInstallRefreshMessage(updated, inst)
}
return &snapInstructionResult{
diff --git a/daemon/api_snaps_test.go b/daemon/api_snaps_test.go
index 776b8faae33..aeef7842062 100644
--- a/daemon/api_snaps_test.go
+++ b/daemon/api_snaps_test.go
@@ -674,10 +674,11 @@ func (s *snapsSuite) TestPostSnapsOpSystemRestartImmediate(c *check.C) {
func (s *snapsSuite) testPostSnapsOp(c *check.C, extraJSON, contentType string) (systemRestartImmediate bool) {
defer daemon.MockAssertstateRefreshSnapAssertions(func(*state.State, int, *assertstate.RefreshAssertionsOptions) error { return nil })()
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, _ int, _ *snapstate.Flags) ([]string, []*state.TaskSet, error) {
- c.Check(names, check.HasLen, 0)
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Check(goal.snaps, check.HasLen, 0)
t := s.NewTask("fake-refresh-all", "Refreshing everything")
- return []string{"fake1", "fake2"}, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ return []string{"fake1", "fake2"}, &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil
})()
d := s.daemonWithOverlordMockAndStore()
@@ -736,16 +737,17 @@ func (s *snapsSuite) TestRefreshAll(c *check.C) {
msg string
}{
{nil, "Refresh all snaps: no updates"},
- {[]string{"fake"}, `Refresh snap "fake"`},
+ {[]string{"fake"}, `Refresh "fake" snap`},
{[]string{"fake1", "fake2"}, `Refresh snaps "fake1", "fake2"`},
} {
refreshSnapAssertions = false
refreshAssertionsOpts = nil
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
- c.Check(names, check.HasLen, 0)
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Check(goal.snaps, check.HasLen, 0)
t := s.NewTask("fake-refresh-all", "Refreshing everything")
- return tst.snaps, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ return tst.snaps, &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil
})()
inst := &daemon.SnapInstruction{Action: "refresh"}
@@ -768,9 +770,10 @@ func (s *snapsSuite) TestRefreshAllNoChanges(c *check.C) {
return assertstate.RefreshSnapAssertions(s, userID, opts)
})()
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
- c.Check(names, check.HasLen, 0)
- return nil, nil, nil
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Check(goal.snaps, check.HasLen, 0)
+ return nil, &snapstate.UpdateTaskSets{Refresh: nil}, nil
})()
d := s.daemon(c)
@@ -797,7 +800,7 @@ func (s *snapsSuite) TestRefreshAllRestoresValidationSets(c *check.C) {
return nil
})()
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
return nil, nil, fmt.Errorf("boom")
})()
@@ -824,12 +827,13 @@ func (s *snapsSuite) TestRefreshManyTransactionally(c *check.C) {
return nil
})()
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
- calledFlags = flags
-
- c.Check(names, check.HasLen, 2)
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ calledFlags = &opts.Flags
+ c.Check(goal.snaps, check.HasLen, 2)
t := s.NewTask("fake-refresh-2", "Refreshing two")
- return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+
+ return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil
})()
d := s.daemon(c)
@@ -861,10 +865,11 @@ func (s *snapsSuite) TestRefreshMany(c *check.C) {
return nil
})()
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
- c.Check(names, check.HasLen, 2)
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Check(goal.snaps, check.HasLen, 2)
t := s.NewTask("fake-refresh-2", "Refreshing two")
- return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil
})()
d := s.daemon(c)
@@ -887,12 +892,13 @@ func (s *snapsSuite) TestRefreshManyIgnoreRunning(c *check.C) {
})()
var calledFlags *snapstate.Flags
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
- calledFlags = flags
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ calledFlags = &opts.Flags
- c.Check(names, check.HasLen, 2)
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Check(goal.snaps, check.HasLen, 2)
t := s.NewTask("fake-refresh-2", "Refreshing two")
- return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil
})()
d := s.daemon(c)
@@ -918,10 +924,11 @@ func (s *snapsSuite) TestRefreshMany1(c *check.C) {
return nil
})()
- defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, _ []*snapstate.RevisionOptions, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
- c.Check(names, check.HasLen, 1)
+ defer daemon.MockSnapstateUpdateWithGoal(func(_ context.Context, s *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Check(goal.snaps, check.HasLen, 1)
t := s.NewTask("fake-refresh-1", "Refreshing one")
- return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
+ return goal.names(), &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil
})()
d := s.daemon(c)
@@ -931,7 +938,7 @@ func (s *snapsSuite) TestRefreshMany1(c *check.C) {
res, err := inst.DispatchForMany()(context.Background(), inst, st)
st.Unlock()
c.Assert(err, check.IsNil)
- c.Check(res.Summary, check.Equals, `Refresh snap "foo"`)
+ c.Check(res.Summary, check.Equals, `Refresh "foo" snap`)
c.Check(res.Affected, check.DeepEquals, inst.Snaps)
c.Check(refreshSnapAssertions, check.Equals, true)
}
@@ -2619,12 +2626,13 @@ func (s *snapsSuite) TestRefresh(c *check.C) {
installQueue := []string{}
assertstateCalledUserID := 0
- defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
- calledFlags = flags
- calledUserID = userID
- installQueue = append(installQueue, name)
+ defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ calledFlags = opts.Flags
+ calledUserID = opts.UserID
+ installQueue = append(installQueue, goal.snaps[0].InstanceName)
- t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ t := st.NewTask("fake-refresh-snap", "Doing a fake refresh")
return state.NewTaskSet(t), nil
})()
defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
@@ -2646,7 +2654,9 @@ func (s *snapsSuite) TestRefresh(c *check.C) {
c.Check(err, check.IsNil)
c.Check(assertstateCalledUserID, check.Equals, 17)
- c.Check(calledFlags, check.DeepEquals, snapstate.Flags{})
+ c.Check(calledFlags, check.DeepEquals, snapstate.Flags{
+ Transaction: client.TransactionPerSnap,
+ })
c.Check(calledUserID, check.Equals, 17)
c.Check(err, check.IsNil)
c.Check(installQueue, check.DeepEquals, []string{"some-snap"})
@@ -2658,12 +2668,13 @@ func (s *snapsSuite) TestRefreshDevMode(c *check.C) {
calledUserID := 0
installQueue := []string{}
- defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
- calledFlags = flags
- calledUserID = userID
- installQueue = append(installQueue, name)
+ defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ calledFlags = opts.Flags
+ calledUserID = opts.UserID
+ installQueue = append(installQueue, goal.snaps[0].InstanceName)
- t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ t := st.NewTask("fake-refresh-snap", "Doing a fake install")
return state.NewTaskSet(t), nil
})()
defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
@@ -2684,8 +2695,10 @@ func (s *snapsSuite) TestRefreshDevMode(c *check.C) {
res, err := inst.Dispatch()(context.Background(), inst, st)
c.Check(err, check.IsNil)
- flags := snapstate.Flags{}
- flags.DevMode = true
+ flags := snapstate.Flags{
+ DevMode: true,
+ Transaction: client.TransactionPerSnap,
+ }
c.Check(calledFlags, check.DeepEquals, flags)
c.Check(calledUserID, check.Equals, 17)
c.Check(err, check.IsNil)
@@ -2696,8 +2709,8 @@ func (s *snapsSuite) TestRefreshDevMode(c *check.C) {
func (s *snapsSuite) TestRefreshClassic(c *check.C) {
var calledFlags snapstate.Flags
- defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
- calledFlags = flags
+ defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ calledFlags = opts.Flags
return nil, nil
})()
defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
@@ -2718,7 +2731,10 @@ func (s *snapsSuite) TestRefreshClassic(c *check.C) {
_, err := inst.Dispatch()(context.Background(), inst, st)
c.Check(err, check.IsNil)
- c.Check(calledFlags, check.DeepEquals, snapstate.Flags{Classic: true})
+ c.Check(calledFlags, check.DeepEquals, snapstate.Flags{
+ Classic: true,
+ Transaction: client.TransactionPerSnap,
+ })
}
func (s *snapsSuite) TestRefreshIgnoreValidation(c *check.C) {
@@ -2726,12 +2742,13 @@ func (s *snapsSuite) TestRefreshIgnoreValidation(c *check.C) {
calledUserID := 0
installQueue := []string{}
- defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
- calledFlags = flags
- calledUserID = userID
- installQueue = append(installQueue, name)
+ defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ calledFlags = opts.Flags
+ calledUserID = opts.UserID
+ installQueue = append(installQueue, goal.snaps[0].InstanceName)
- t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ t := st.NewTask("fake-refresh-snap", "Doing a fake install")
return state.NewTaskSet(t), nil
})()
defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
@@ -2752,8 +2769,10 @@ func (s *snapsSuite) TestRefreshIgnoreValidation(c *check.C) {
res, err := inst.Dispatch()(context.Background(), inst, st)
c.Check(err, check.IsNil)
- flags := snapstate.Flags{}
- flags.IgnoreValidation = true
+ flags := snapstate.Flags{
+ IgnoreValidation: true,
+ Transaction: client.TransactionPerSnap,
+ }
c.Check(calledFlags, check.DeepEquals, flags)
c.Check(calledUserID, check.Equals, 17)
@@ -2766,11 +2785,12 @@ func (s *snapsSuite) TestRefreshIgnoreRunning(c *check.C) {
var calledFlags snapstate.Flags
installQueue := []string{}
- defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
- calledFlags = flags
- installQueue = append(installQueue, name)
+ defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ calledFlags = opts.Flags
+ installQueue = append(installQueue, goal.snaps[0].InstanceName)
- t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ t := st.NewTask("fake-refresh-snap", "Doing a fake install")
return state.NewTaskSet(t), nil
})()
defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
@@ -2790,8 +2810,10 @@ func (s *snapsSuite) TestRefreshIgnoreRunning(c *check.C) {
res, err := inst.Dispatch()(context.Background(), inst, st)
c.Check(err, check.IsNil)
- flags := snapstate.Flags{}
- flags.IgnoreRunning = true
+ flags := snapstate.Flags{
+ IgnoreRunning: true,
+ Transaction: client.TransactionPerSnap,
+ }
c.Check(calledFlags, check.DeepEquals, flags)
c.Check(err, check.IsNil)
@@ -2802,10 +2824,11 @@ func (s *snapsSuite) TestRefreshIgnoreRunning(c *check.C) {
func (s *snapsSuite) TestRefreshCohort(c *check.C) {
cohort := ""
- defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
- cohort = opts.CohortKey
+ defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ cohort = goal.snaps[0].RevOpts.CohortKey
- t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ t := st.NewTask("fake-refresh-snap", "Doing a fake install")
return state.NewTaskSet(t), nil
})()
defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
@@ -2826,16 +2849,17 @@ func (s *snapsSuite) TestRefreshCohort(c *check.C) {
c.Check(err, check.IsNil)
c.Check(cohort, check.Equals, "xyzzy")
- c.Check(res.Summary, check.Equals, `Refresh "some-snap" snap`)
+ c.Check(res.Summary, check.Equals, `Refresh "some-snap" snap from "xyzzy" cohort`)
}
func (s *snapsSuite) TestRefreshLeaveCohort(c *check.C) {
var leave *bool
- defer daemon.MockSnapstateUpdate(func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
- leave = &opts.LeaveCohort
+ defer daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ leave = &goal.snaps[0].RevOpts.LeaveCohort
- t := s.NewTask("fake-refresh-snap", "Doing a fake install")
+ t := st.NewTask("fake-refresh-snap", "Doing a fake install")
return state.NewTaskSet(t), nil
})()
defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
@@ -3765,7 +3789,7 @@ func (s *snapsSuite) TestPostRemoveComponents(c *check.C) {
func (s *snapsSuite) TestPostComponentsWrongAction(c *check.C) {
s.daemonWithOverlordMockAndStore()
- for _, action := range []string{"refresh", "revert", "enable", "disable"} {
+ for _, action := range []string{"revert", "enable", "disable"} {
buf := strings.NewReader(fmt.Sprintf(`{"action": %q,"components":["comp1","comp2"]}`,
action))
req, err := http.NewRequest("POST", "/v2/snaps/foo", buf)
@@ -3892,7 +3916,7 @@ func (s *snapsSuite) TestPostComponentsRemoveManyWithSnaps(c *check.C) {
func (s *snapsSuite) TestPostComponentsManyWrongAction(c *check.C) {
s.daemonWithOverlordMockAndStore()
- for _, action := range []string{"refresh", "revert", "enable", "disable"} {
+ for _, action := range []string{"revert", "enable", "disable"} {
buf := strings.NewReader(fmt.Sprintf(`{"action": %q, "snaps":["foo", "bar"], "components": { "snap1": ["comp1", "comp2"], "snap2": ["comp3", "comp4"] }}`, action))
req, err := http.NewRequest("POST", "/v2/snaps", buf)
c.Assert(err, check.IsNil)
@@ -3969,6 +3993,60 @@ func (s *snapsSuite) TestInstallWithComponents(c *check.C) {
c.Check(chg.Summary(), check.Equals, `Install "some-snap" snap with components "comp1", "comp2"`)
}
+func (s *snapsSuite) TestUpdateWithAdditionalComponents(c *check.C) {
+ restore := daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error {
+ return nil
+ })
+ defer restore()
+
+ restore = daemon.MockSnapstateUpdateOne(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Check(goal.snaps, check.DeepEquals, []snapstate.StoreUpdate{{
+ InstanceName: "some-snap",
+ AdditionalComponents: []string{"comp1", "comp2"},
+ }})
+ t := st.NewTask("fake-refresh-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ })
+ defer restore()
+
+ d := s.daemonWithFakeSnapManager(c)
+
+ r := strings.NewReader(`{"action": "refresh", "components": ["comp1", "comp2"]}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/some-snap", r)
+ c.Assert(err, check.IsNil)
+
+ rsp := s.asyncReq(c, req, nil)
+
+ st := d.Overlord().State()
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+
+ c.Check(chg.Tasks(), check.HasLen, 1)
+
+ var data map[string]interface{}
+ err = chg.Get("api-data", &data)
+ c.Assert(err, check.IsNil)
+ c.Check(data, check.DeepEquals, map[string]interface{}{
+ "snap-names": []interface{}{"some-snap"},
+ "components": map[string]interface{}{
+ "some-snap": []interface{}{"comp1", "comp2"},
+ },
+ })
+
+ st.Unlock()
+ s.waitTrivialChange(c, chg)
+ st.Lock()
+
+ c.Check(chg.Status(), check.Equals, state.DoneStatus)
+ c.Check(err, check.IsNil)
+ c.Check(chg.Kind(), check.Equals, "refresh-snap")
+ c.Check(chg.Summary(), check.Equals, `Refresh "some-snap" snap with components "comp1", "comp2"`)
+}
+
func (s *snapsSuite) TestInstallManyWithComponents(c *check.C) {
defer daemon.MockSnapstateInstallWithGoal(func(ctx context.Context, st *state.State, g snapstate.InstallGoal, opts snapstate.Options) ([]*snap.Info, []*state.TaskSet, error) {
goal, ok := g.(*storeInstallGoalRecorder)
@@ -4018,6 +4096,60 @@ func (s *snapsSuite) TestInstallManyWithComponents(c *check.C) {
c.Check(chg.Summary(), check.Equals, `Install snaps "some-snap" (with components "some-comp1", "some-comp2"), "other-snap" (with component "other-comp1")`)
}
+func (s *snapsSuite) TestUpdateManyWithComponents(c *check.C) {
+ restore := daemon.MockAssertstateRefreshSnapAssertions(func(*state.State, int, *assertstate.RefreshAssertionsOptions) error {
+ return nil
+ })
+ defer restore()
+
+ restore = daemon.MockSnapstateUpdateWithGoal(func(ctx context.Context, st *state.State, g snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error) {
+ goal := g.(*storeUpdateGoalRecorder)
+ c.Assert(goal.snaps, check.HasLen, 2)
+
+ c.Check(goal.snaps, check.DeepEquals, []snapstate.StoreUpdate{
+ {
+ InstanceName: "some-snap",
+ AdditionalComponents: []string{"some-comp1", "some-comp2"},
+ },
+ {
+ InstanceName: "other-snap",
+ AdditionalComponents: []string{"other-comp1"},
+ },
+ })
+
+ t := st.NewTask("fake-refresh-snap", "Doing a fake refresh")
+ return []string{"some-snap", "other-snap"}, &snapstate.UpdateTaskSets{Refresh: []*state.TaskSet{state.NewTaskSet(t)}}, nil
+ })
+ defer restore()
+
+ d := s.daemonWithFakeSnapManager(c)
+
+ r := strings.NewReader(`{"action": "refresh", "snaps": ["some-snap", "other-snap"], "components": {"some-snap": ["some-comp1", "some-comp2"], "other-snap": ["other-comp1"]}}`)
+ req, err := http.NewRequest("POST", "/v2/snaps", r)
+ c.Assert(err, check.IsNil)
+ req.Header.Set("Content-Type", "application/json")
+
+ rsp := s.asyncReq(c, req, nil)
+
+ st := d.Overlord().State()
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.Change(rsp.Change)
+ c.Assert(chg, check.NotNil)
+
+ c.Check(chg.Tasks(), check.HasLen, 1)
+
+ st.Unlock()
+ s.waitTrivialChange(c, chg)
+ st.Lock()
+
+ c.Check(chg.Status(), check.Equals, state.DoneStatus)
+ c.Check(err, check.IsNil)
+ c.Check(chg.Kind(), check.Equals, "refresh-snap")
+ c.Check(chg.Summary(), check.Equals, `Refresh snaps "some-snap" (with components "some-comp1", "some-comp2"), "other-snap" (with component "other-comp1")`)
+}
+
func (s *snapsSuite) TestInstallWithComponentsSnapAlreadyInstalled(c *check.C) {
defer daemon.MockSnapstateInstallComponents(func(ctx context.Context, st *state.State, names []string, info *snap.Info, opts snapstate.Options) ([]*state.TaskSet, error) {
c.Check(names, check.DeepEquals, []string{"comp1", "comp2"})
diff --git a/daemon/api_systems.go b/daemon/api_systems.go
index 4ba32982c36..73f14376f5c 100644
--- a/daemon/api_systems.go
+++ b/daemon/api_systems.go
@@ -451,7 +451,7 @@ func postSystemActionCreateOffline(c *Command, form *Form) Response {
return BadRequest("cannot parse validation sets: %v", err)
}
- var snapFiles []*uploadedSnap
+ var snapFiles []*uploadedContainer
if len(form.FileRefs["snap"]) > 0 {
snaps, errRsp := form.GetSnapFiles()
if errRsp != nil {
@@ -488,15 +488,12 @@ func postSystemActionCreateOffline(c *Command, form *Form) Response {
return apiErr
}
- if len(slInfo.sideInfos) != len(slInfo.tmpPaths) {
- return InternalError("mismatch between number of snap side infos and temporary paths")
- }
-
- localSnaps := make([]devicestate.LocalSnap, 0, len(slInfo.sideInfos))
- for i := range slInfo.sideInfos {
+ // TODO:COMPS: support adding components to a recovery system
+ localSnaps := make([]devicestate.LocalSnap, 0, len(slInfo.snaps))
+ for _, sn := range slInfo.snaps {
localSnaps = append(localSnaps, devicestate.LocalSnap{
- SideInfo: slInfo.sideInfos[i],
- Path: slInfo.tmpPaths[i],
+ SideInfo: sn.sideInfo,
+ Path: sn.tmpPath,
})
}
diff --git a/daemon/export_test.go b/daemon/export_test.go
index 37bc96a2673..aa3a9019d36 100644
--- a/daemon/export_test.go
+++ b/daemon/export_test.go
@@ -150,6 +150,22 @@ func MockSnapstateInstallWithGoal(mock func(ctx context.Context, st *state.State
}
}
+func MockSnapstateUpdateWithGoal(mock func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) ([]string, *snapstate.UpdateTaskSets, error)) (restore func()) {
+ return testutil.Mock(&snapstateUpdateWithGoal, mock)
+}
+
+func MockSnapstatePathUpdateGoal(mock func(snaps ...snapstate.PathSnap) snapstate.UpdateGoal) (restore func()) {
+ return testutil.Mock(&snapstatePathUpdateGoal, mock)
+}
+
+func MockSnapstateUpdateOne(mock func(ctx context.Context, st *state.State, goal snapstate.UpdateGoal, filter func(*snap.Info, *snapstate.SnapState) bool, opts snapstate.Options) (*state.TaskSet, error)) (restore func()) {
+ old := snapstateUpdateOne
+ snapstateUpdateOne = mock
+ return func() {
+ snapstateUpdateOne = old
+ }
+}
+
func MockSnapstateInstallComponents(mock func(ctx context.Context, st *state.State, names []string, info *snap.Info, opts snapstate.Options) ([]*state.TaskSet, error)) (restore func()) {
old := snapstateInstallComponents
snapstateInstallComponents = mock
@@ -166,19 +182,19 @@ func MockSnapstateStoreInstallGoal(mock func(snaps ...snapstate.StoreSnap) snaps
}
}
-func MockSnapstateInstallPath(mock func(*state.State, *snap.SideInfo, string, string, string, snapstate.Flags, snapstate.PrereqTracker) (*state.TaskSet, *snap.Info, error)) (restore func()) {
- oldSnapstateInstallPath := snapstateInstallPath
- snapstateInstallPath = mock
+func MockSnapstateStoreUpdateGoal(mock func(snaps ...snapstate.StoreUpdate) snapstate.UpdateGoal) (restore func()) {
+ old := snapstateStoreUpdateGoal
+ snapstateStoreUpdateGoal = mock
return func() {
- snapstateInstallPath = oldSnapstateInstallPath
+ snapstateStoreUpdateGoal = old
}
}
-func MockSnapstateUpdate(mock func(*state.State, string, *snapstate.RevisionOptions, int, snapstate.Flags) (*state.TaskSet, error)) (restore func()) {
- oldSnapstateUpdate := snapstateUpdate
- snapstateUpdate = mock
+func MockSnapstateInstallPath(mock func(*state.State, *snap.SideInfo, string, string, string, snapstate.Flags, snapstate.PrereqTracker) (*state.TaskSet, *snap.Info, error)) (restore func()) {
+ oldSnapstateInstallPath := snapstateInstallPath
+ snapstateInstallPath = mock
return func() {
- snapstateUpdate = oldSnapstateUpdate
+ snapstateInstallPath = oldSnapstateInstallPath
}
}
@@ -214,14 +230,6 @@ func MockSnapstateRevertToRevision(mock func(*state.State, string, snap.Revision
}
}
-func MockSnapstateUpdateMany(mock func(context.Context, *state.State, []string, []*snapstate.RevisionOptions, int, *snapstate.Flags) ([]string, []*state.TaskSet, error)) (restore func()) {
- oldSnapstateUpdateMany := snapstateUpdateMany
- snapstateUpdateMany = mock
- return func() {
- snapstateUpdateMany = oldSnapstateUpdateMany
- }
-}
-
func MockSnapstateRemove(mock func(st *state.State, name string, revision snap.Revision, flags *snapstate.RemoveFlags) (*state.TaskSet, error)) (restore func()) {
oldSnapstateRemove := snapstateRemove
snapstateRemove = mock
@@ -325,21 +333,18 @@ func MockReboot(f func(boot.RebootAction, time.Duration, *boot.RebootInfo) error
func MockSideloadSnapsInfo(sis []*snap.SideInfo) (restore func()) {
r := testutil.Backup(&sideloadSnapsInfo)
- sideloadSnapsInfo = func(st *state.State, snapFiles []*uploadedSnap,
+ sideloadSnapsInfo = func(st *state.State, snapFiles []*uploadedContainer,
flags sideloadFlags) (*sideloadedInfo, *apiError) {
- names := make([]string, len(snapFiles))
- sideInfos := make([]*snap.SideInfo, len(snapFiles))
- origPaths := make([]string, len(snapFiles))
- tmpPaths := make([]string, len(snapFiles))
+ var snaps []sideloadSnapInfo
for i, snapFile := range snapFiles {
- sideInfos[i] = sis[i]
- names[i] = sis[i].RealName
- origPaths[i] = snapFile.filename
- tmpPaths[i] = snapFile.tmpPath
+ snaps = append(snaps, sideloadSnapInfo{
+ sideInfo: sis[i],
+ origPath: snapFile.filename,
+ tmpPath: snapFile.tmpPath,
+ })
}
- return &sideloadedInfo{sideInfos: sideInfos, names: names,
- origPaths: origPaths, tmpPaths: tmpPaths}, nil
+ return &sideloadedInfo{snaps: snaps}, nil
}
return r
}
diff --git a/gadget/gadget.go b/gadget/gadget.go
index 502f5c0afe1..202ac0215c5 100644
--- a/gadget/gadget.go
+++ b/gadget/gadget.go
@@ -55,6 +55,8 @@ const (
schemaMBR = "mbr"
// schemaGPT identifies a GUID Partition Table partitioning schema
schemaGPT = "gpt"
+ // schemaEMMC identifies a schema for eMMC
+ schemaEMMC = "emmc"
SystemBoot = "system-boot"
SystemData = "system-data"
@@ -1108,6 +1110,24 @@ func asOffsetPtr(offs quantity.Offset) *quantity.Offset {
return &offs
}
+func setVolumeStructureOffset(vs *VolumeStructure, startPtr *quantity.Offset) (next *quantity.Offset) {
+ if vs.Offset == nil && startPtr != nil {
+ var start quantity.Offset
+ if vs.Role != schemaMBR && *startPtr < NonMBRStartOffset {
+ start = NonMBRStartOffset
+ } else {
+ start = *startPtr
+ }
+ vs.Offset = &start
+ }
+ // We know the end of the structure only if we could define an offset
+ // and the size is fixed.
+ if vs.Offset != nil && vs.isFixedSize() {
+ return asOffsetPtr(*vs.Offset + quantity.Offset(vs.Size))
+ }
+ return nil
+}
+
func setImplicitForVolume(vol *Volume, model Model) error {
rs := whichVolRuleset(model)
if vol.HasPartial(PartialSchema) {
@@ -1132,41 +1152,34 @@ func setImplicitForVolume(vol *Volume, model Model) error {
previousEnd := asOffsetPtr(0)
for i := range vol.Structure {
+ vs := &vol.Structure[i]
+
// set the VolumeName for the structure from the volume itself
- vol.Structure[i].VolumeName = vol.Name
+ vs.VolumeName = vol.Name
// Store index as we will reorder later
- vol.Structure[i].YamlIndex = i
+ vs.YamlIndex = i
// MinSize is Size if not explicitly set
- if vol.Structure[i].MinSize == 0 {
- vol.Structure[i].MinSize = vol.Structure[i].Size
+ if vs.MinSize == 0 {
+ vs.MinSize = vs.Size
}
// Set the pointer to the volume
- vol.Structure[i].EnclosingVolume = vol
+ vs.EnclosingVolume = vol
// set other implicit data for the structure
- if err := setImplicitForVolumeStructure(&vol.Structure[i], rs, knownFsLabels, knownVfatFsLabels); err != nil {
+ if err := setImplicitForVolumeStructure(vs, rs, knownFsLabels, knownVfatFsLabels); err != nil {
return err
}
// Set offset if it was not set (must be after setImplicitForVolumeStructure
// so roles are good). This is possible only if the previous structure had
// a well-defined end.
- if vol.Structure[i].Offset == nil && previousEnd != nil {
- var start quantity.Offset
- if vol.Structure[i].Role != schemaMBR && *previousEnd < NonMBRStartOffset {
- start = NonMBRStartOffset
- } else {
- start = *previousEnd
- }
- vol.Structure[i].Offset = &start
- }
- // We know the end of the structure only if we could define an offset
- // and the size is fixed.
- if vol.Structure[i].Offset != nil && vol.Structure[i].isFixedSize() {
- previousEnd = asOffsetPtr(*vol.Structure[i].Offset +
- quantity.Offset(vol.Structure[i].Size))
- } else {
- previousEnd = nil
+ switch vol.Schema {
+ case schemaEMMC:
+ // For eMMC, we do not support partition offsets. The partitions
+ // are hardware partitions that act more like traditional disks.
+ vs.Offset = asOffsetPtr(0)
+ default:
+ previousEnd = setVolumeStructureOffset(vs, previousEnd)
}
}
@@ -1287,11 +1300,17 @@ func fmtIndexAndName(idx int, name string) string {
return fmt.Sprintf("#%v", idx)
}
+var validSchemaNames = []string{schemaMBR, schemaGPT, schemaEMMC}
+
+func isValidSchema(schema string) bool {
+ return strutil.ListContains(validSchemaNames, schema)
+}
+
func validateVolume(vol *Volume) error {
if !validVolumeName.MatchString(vol.Name) {
return errors.New("invalid name")
}
- if !vol.HasPartial(PartialSchema) && vol.Schema != schemaGPT && vol.Schema != schemaMBR {
+ if !vol.HasPartial(PartialSchema) && !isValidSchema(vol.Schema) {
return fmt.Errorf("invalid schema %q", vol.Schema)
}
@@ -1358,6 +1377,12 @@ func isMBR(vs *VolumeStructure) bool {
}
func validateCrossVolumeStructure(vol *Volume) error {
+ // eMMC have no traditional volumes, instead eMMC has the concept
+ // of hardware partitions that act like separate disks.
+ if vol.Schema == schemaEMMC {
+ return nil
+ }
+
previousEnd := quantity.Offset(0)
// cross structure validation:
// - relative offsets that reference other structures by name
@@ -1413,6 +1438,19 @@ func validateOffsetWrite(s, firstStruct *VolumeStructure, volSize quantity.Size)
return nil
}
+func contentCheckerCreate(vs *VolumeStructure, vol *Volume) func(*VolumeContent) error {
+ switch {
+ case vol.Schema == schemaEMMC:
+ return validateEMMCContent
+ case vs.HasFilesystem():
+ return validateFilesystemContent
+ default:
+ // default to bare content checker if no filesystem
+ // is present
+ return validateBareContent
+ }
+}
+
func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error {
if !vs.hasPartialSize() {
if vs.Size == 0 {
@@ -1439,20 +1477,14 @@ func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error {
return fmt.Errorf("invalid filesystem %q", vs.Filesystem)
}
- var contentChecker func(*VolumeContent) error
-
- if vs.HasFilesystem() {
- contentChecker = validateFilesystemContent
- } else {
- contentChecker = validateBareContent
- }
+ contentChecker := contentCheckerCreate(vs, vol)
for i, c := range vs.Content {
if err := contentChecker(&c); err != nil {
return fmt.Errorf("invalid content #%v: %v", i, err)
}
}
- if err := validateStructureUpdate(vs, vol); err != nil {
+ if err := validateStructureUpdate(vs); err != nil {
return err
}
@@ -1462,6 +1494,14 @@ func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error {
return nil
}
+func validateStructureTypeEMMC(s string) error {
+ // for eMMC we don't support the type being set
+ if s != "" {
+ return errors.New(`type is not supported for "emmc" schema`)
+ }
+ return nil
+}
+
func validateStructureType(s string, vol *Volume) error {
// Type can be one of:
// - "mbr" (backwards compatible)
@@ -1473,6 +1513,11 @@ func validateStructureType(s string, vol *Volume) error {
// Hybrid ID is 2 hex digits of MBR type, followed by 36 GUUID
// example: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
+ // eMMC volumes we treat differently
+ if vol.Schema == schemaEMMC {
+ return validateStructureTypeEMMC(s)
+ }
+
if s == "" {
return errors.New(`type is not specified`)
}
@@ -1582,6 +1627,20 @@ func validateBareContent(vc *VolumeContent) error {
return nil
}
+func validateEMMCContent(vc *VolumeContent) error {
+ if vc.Offset != nil || vc.Size != 0 {
+ return fmt.Errorf("cannot specify size or offset for content")
+ }
+
+ if vc.UnresolvedSource != "" || vc.Target != "" {
+ return fmt.Errorf("cannot use non-image content for hardware partitions")
+ }
+ if vc.Image == "" {
+ return fmt.Errorf("missing image file name")
+ }
+ return nil
+}
+
func validateFilesystemContent(vc *VolumeContent) error {
if vc.Image != "" || vc.Offset != nil || vc.Size != 0 {
return fmt.Errorf("cannot use image content for non-bare file system")
@@ -1595,7 +1654,7 @@ func validateFilesystemContent(vc *VolumeContent) error {
return nil
}
-func validateStructureUpdate(vs *VolumeStructure, v *Volume) error {
+func validateStructureUpdate(vs *VolumeStructure) error {
if !vs.HasFilesystem() && len(vs.Update.Preserve) > 0 {
return errors.New("preserving files during update is not supported for non-filesystem structures")
}
diff --git a/gadget/gadget_emmc_test.go b/gadget/gadget_emmc_test.go
new file mode 100644
index 00000000000..ad97fc3144d
--- /dev/null
+++ b/gadget/gadget_emmc_test.go
@@ -0,0 +1,387 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package gadget_test
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/gadget"
+ "github.com/snapcore/snapd/gadget/quantity"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type gadgetYamlEMMCSuite struct {
+ testutil.BaseTest
+
+ dir string
+ gadgetYamlPath string
+}
+
+var _ = Suite(&gadgetYamlEMMCSuite{})
+
+var mockEMMCGadgetYaml = []byte(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: u-boot
+ id: 0C
+ structure:
+ - filesystem-label: system-boot
+ offset: 12345
+ offset-write: 777
+ size: 88888
+ type: 0C
+ filesystem: vfat
+ content:
+ - source: subdir/
+ target: /
+ unpack: false
+ - source: foo
+ target: /
+ my-emmc:
+ schema: emmc
+ structure:
+ - name: boot0
+ size: 4M
+ content:
+ - image: boot0filename
+ - name: boot1
+ size: 4M
+ content:
+ - image: boot1filename
+`)
+
+func (s *gadgetYamlEMMCSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+
+ dirs.SetRootDir(c.MkDir())
+ s.dir = c.MkDir()
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "meta"), 0755), IsNil)
+ s.gadgetYamlPath = filepath.Join(s.dir, "meta", "gadget.yaml")
+}
+
+func (s *gadgetYamlEMMCSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+}
+
+func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlEMMCNoID(c *C) {
+ err := os.WriteFile(s.gadgetYamlPath, []byte(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: u-boot
+ my-emmc:
+ schema: emmc
+ id: test
+`), 0644)
+ c.Assert(err, IsNil)
+
+ info, err := gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, IsNil)
+
+ err = gadget.Validate(info, nil, nil)
+ c.Assert(err, ErrorMatches, `invalid volume "my-emmc": cannot set "id" for eMMC schemas`)
+}
+
+func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlEMMCNoBootloader(c *C) {
+ err := os.WriteFile(s.gadgetYamlPath, []byte(`
+volumes:
+ volumename:
+ schema: mbr
+ my-emmc:
+ schema: emmc
+ bootloader: u-boot
+`), 0644)
+ c.Assert(err, IsNil)
+
+ info, err := gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, IsNil)
+
+ err = gadget.Validate(info, nil, nil)
+ c.Assert(err, ErrorMatches, `invalid volume "my-emmc": cannot set "bootloader" for eMMC schemas`)
+}
+
+func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlEMMCNoPartial(c *C) {
+ err := os.WriteFile(s.gadgetYamlPath, []byte(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: u-boot
+ my-emmc:
+ schema: emmc
+ partial: [size]
+`), 0644)
+ c.Assert(err, IsNil)
+
+ info, err := gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, IsNil)
+
+ err = gadget.Validate(info, nil, nil)
+ c.Assert(err, ErrorMatches, `invalid volume "my-emmc": cannot set "partial" content for eMMC schemas`)
+}
+
+func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlOffsetNotSupportedForBoot(c *C) {
+ for _, t := range []string{"boot0", "boot1"} {
+ err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: u-boot
+ id: 0C
+ structure:
+ - filesystem-label: system-boot
+ offset: 12345
+ offset-write: 777
+ size: 88888
+ type: 0C
+ filesystem: vfat
+ content:
+ - source: subdir/
+ target: /
+ unpack: false
+ my-emmc:
+ schema: emmc
+ structure:
+ - name: %s
+ size: 4M
+ content:
+ - image: boot0filename
+ offset: 1000
+`, t)), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, ErrorMatches, `.*cannot specify size or offset for content`)
+ }
+}
+
+func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlSourceIsNotSupported(c *C) {
+ for _, t := range []string{"boot0", "boot1"} {
+ err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: u-boot
+ id: 0C
+ structure:
+ - filesystem-label: system-boot
+ offset: 12345
+ offset-write: 777
+ size: 88888
+ type: 0C
+ filesystem: vfat
+ content:
+ - source: subdir/
+ target: /
+ unpack: false
+ my-emmc:
+ schema: emmc
+ structure:
+ - name: %s
+ size: 4M
+ content:
+ - source: hello.bin
+`, t)), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, ErrorMatches, `.*cannot use non-image content for hardware partitions`)
+ }
+}
+
+func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlImageMustBeSet(c *C) {
+ for _, t := range []string{"boot0", "boot1"} {
+ err := os.WriteFile(s.gadgetYamlPath, []byte(fmt.Sprintf(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: u-boot
+ id: 0C
+ structure:
+ - filesystem-label: system-boot
+ offset: 12345
+ offset-write: 777
+ size: 88888
+ type: 0C
+ filesystem: vfat
+ content:
+ - source: subdir/
+ target: /
+ unpack: false
+ my-emmc:
+ schema: emmc
+ structure:
+ - name: %s
+ size: 4M
+ content:
+ - unpack: true
+`, t)), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, ErrorMatches, `.*missing image file name`)
+ }
+}
+
+func (s *gadgetYamlEMMCSuite) TestReadGadgetYamlHappy(c *C) {
+ err := os.WriteFile(s.gadgetYamlPath, mockEMMCGadgetYaml, 0644)
+ c.Assert(err, IsNil)
+
+ ginfo, err := gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, IsNil)
+ expected := &gadget.Info{
+ Volumes: map[string]*gadget.Volume{
+ "volumename": {
+ Name: "volumename",
+ Schema: "mbr",
+ Bootloader: "u-boot",
+ ID: "0C",
+ Structure: []gadget.VolumeStructure{
+ {
+ VolumeName: "volumename",
+ Label: "system-boot",
+ Role: "system-boot", // implicit
+ Offset: asOffsetPtr(12345),
+ OffsetWrite: mustParseGadgetRelativeOffset(c, "777"),
+ Size: 88888,
+ MinSize: 88888,
+ Type: "0C",
+ Filesystem: "vfat",
+ Content: []gadget.VolumeContent{
+ {
+ UnresolvedSource: "subdir/",
+ Target: "/",
+ Unpack: false,
+ },
+ {
+ UnresolvedSource: "foo",
+ Target: "/",
+ Unpack: false,
+ },
+ },
+ },
+ },
+ },
+ "my-emmc": {
+ Name: "my-emmc",
+ Schema: "emmc",
+ Structure: []gadget.VolumeStructure{
+ {
+ VolumeName: "my-emmc",
+ Name: "boot0",
+ Offset: asOffsetPtr(0),
+ Size: 4 * 1024 * 1024,
+ MinSize: 4 * 1024 * 1024,
+ Content: []gadget.VolumeContent{
+ {
+ Image: "boot0filename",
+ },
+ },
+ YamlIndex: 0,
+ }, {
+ VolumeName: "my-emmc",
+ Name: "boot1",
+ Offset: asOffsetPtr(0),
+ Size: 4 * 1024 * 1024,
+ MinSize: 4 * 1024 * 1024,
+ Content: []gadget.VolumeContent{
+ {
+ Image: "boot1filename",
+ },
+ },
+ YamlIndex: 1,
+ },
+ },
+ },
+ },
+ }
+ gadget.SetEnclosingVolumeInStructs(expected.Volumes)
+
+ c.Check(ginfo, DeepEquals, expected)
+}
+
+func (s *gadgetYamlEMMCSuite) TestUpdateApplyHappy(c *C) {
+ err := os.WriteFile(s.gadgetYamlPath, mockEMMCGadgetYaml, 0644)
+ c.Assert(err, IsNil)
+
+ oldInfo, err := gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, IsNil)
+ oldRootDir := c.MkDir()
+ makeSizedFile(c, filepath.Join(oldRootDir, "boot0filename"), 1*quantity.SizeMiB, nil)
+ makeSizedFile(c, filepath.Join(oldRootDir, "boot1filename"), 1*quantity.SizeMiB, nil)
+ oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir}
+
+ newInfo, err := gadget.ReadInfo(s.dir, coreMod)
+ c.Assert(err, IsNil)
+ // pretend we have an update
+ newInfo.Volumes["my-emmc"].Structure[1].Update.Edition = 1
+
+ newRootDir := c.MkDir()
+ makeSizedFile(c, filepath.Join(newRootDir, "boot0filename"), 1*quantity.SizeMiB, nil)
+ makeSizedFile(c, filepath.Join(newRootDir, "boot1filename"), 2*quantity.SizeMiB, nil)
+ newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir}
+
+ rollbackDir := c.MkDir()
+
+ restore := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, gm gadget.Model, gv map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) {
+ return map[string]map[int]gadget.StructureLocation{
+ "volumename": {
+ 0: {
+ Device: "/dev/emmcblk0",
+ Offset: quantity.OffsetMiB,
+ RootMountPoint: "/run/mnt/ubuntu-boot",
+ },
+ },
+ "my-emmc": {
+ 0: {
+ Device: "/dev/emmcblk0boot0",
+ },
+ 1: {
+ Device: "/dev/emmcblk0boot1",
+ },
+ },
+ }, map[string]map[int]*gadget.OnDiskStructure{
+ "volumename": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["volumename"]),
+ "my-emmc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["my-emmc"]),
+ }, nil
+ })
+ defer restore()
+
+ muo := &mockUpdateProcessObserver{}
+ updaterForStructureCalls := 0
+ restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, fromPs, ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) {
+ fmt.Println("update-for-structure", loc, ps, fromPs)
+ updaterForStructureCalls++
+ mu := &mockUpdater{}
+
+ return mu, nil
+ })
+ defer restore()
+
+ // go go go
+ err = gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo)
+ c.Assert(err, IsNil)
+ c.Assert(updaterForStructureCalls, Equals, 1)
+}
diff --git a/gadget/install/content.go b/gadget/install/content.go
index 9d72459d2ad..28a383ddfaa 100644
--- a/gadget/install/content.go
+++ b/gadget/install/content.go
@@ -62,16 +62,35 @@ func makeFilesystem(params mkfsParams) error {
return udevTrigger(params.Device)
}
+type mntfsParams struct {
+ NoExec bool
+ NoDev bool
+ NoSuid bool
+}
+
+func (p *mntfsParams) flags() uintptr {
+ var flags uintptr
+ if p.NoDev {
+ flags |= syscall.MS_NODEV
+ }
+ if p.NoExec {
+ flags |= syscall.MS_NOEXEC
+ }
+ if p.NoSuid {
+ flags |= syscall.MS_NOSUID
+ }
+ return flags
+}
+
// mountFilesystem mounts the filesystem on a given device with
// filesystem type fs under the provided mount point directory.
-func mountFilesystem(fsDevice, fs, mountpoint string) error {
+func mountFilesystem(fsDevice, fs, mountpoint string, params mntfsParams) error {
if err := os.MkdirAll(mountpoint, 0755); err != nil {
return fmt.Errorf("cannot create mountpoint: %v", err)
}
- if err := sysMount(fsDevice, mountpoint, fs, 0, ""); err != nil {
+ if err := sysMount(fsDevice, mountpoint, fs, params.flags(), ""); err != nil {
return fmt.Errorf("cannot mount filesystem %q at %q: %v", fsDevice, mountpoint, err)
}
-
return nil
}
diff --git a/gadget/install/content_test.go b/gadget/install/content_test.go
index d4dc65ca424..05593166d5f 100644
--- a/gadget/install/content_test.go
+++ b/gadget/install/content_test.go
@@ -49,8 +49,11 @@ type contentTestSuite struct {
gadgetRoot string
- mockMountPoint string
- mockMountCalls []struct{ source, target, fstype string }
+ mockMountPoint string
+ mockMountCalls []struct {
+ source, target, fstype string
+ flags uintptr
+ }
mockUnmountCalls []string
mockMountErr error
@@ -76,7 +79,10 @@ func (s *contentTestSuite) SetUpTest(c *C) {
s.mockMountPoint = c.MkDir()
restore := install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error {
- s.mockMountCalls = append(s.mockMountCalls, struct{ source, target, fstype string }{source, target, fstype})
+ s.mockMountCalls = append(s.mockMountCalls, struct {
+ source, target, fstype string
+ flags uintptr
+ }{source, target, fstype, flags})
return s.mockMountErr
})
s.AddCleanup(restore)
@@ -498,17 +504,52 @@ func (s *contentTestSuite) TestMountFilesystem(c *C) {
defer dirs.SetRootDir("")
// mount a filesystem...
- err := install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"))
+ err := install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"), install.MntfsParams{})
c.Assert(err, IsNil)
// ...and check if it was mounted at the right mount point
c.Check(s.mockMountCalls, HasLen, 1)
- c.Check(s.mockMountCalls, DeepEquals, []struct{ source, target, fstype string }{
- {"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat"},
+ c.Check(s.mockMountCalls, DeepEquals, []struct {
+ source, target, fstype string
+ flags uintptr
+ }{
+ {"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat", 0},
})
// try again with mocked error
s.mockMountErr = fmt.Errorf("mock mount error")
- err = install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"))
+ err = install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"), install.MntfsParams{})
c.Assert(err, ErrorMatches, `cannot mount filesystem "/dev/node2" at ".*/run/mnt/ubuntu-seed": mock mount error`)
}
+
+func (s *contentTestSuite) TestMountFilesystemOptions(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ defer dirs.SetRootDir("")
+
+ tests := []struct {
+ params install.MntfsParams
+ expectedFlags uintptr
+ }{
+ {install.MntfsParams{}, 0},
+ {install.MntfsParams{NoExec: true}, syscall.MS_NOEXEC},
+ {install.MntfsParams{NoDev: true}, syscall.MS_NODEV},
+ {install.MntfsParams{NoSuid: true}, syscall.MS_NOSUID},
+ }
+
+ for _, t := range tests {
+ // reset calls
+ s.mockMountCalls = nil
+
+ err := install.MountFilesystem("/dev/node2", "vfat", filepath.Join(boot.InitramfsRunMntDir, "ubuntu-seed"), t.params)
+ c.Assert(err, IsNil)
+
+ // .. verify flags
+ c.Check(s.mockMountCalls, HasLen, 1)
+ c.Check(s.mockMountCalls, DeepEquals, []struct {
+ source, target, fstype string
+ flags uintptr
+ }{
+ {"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat", t.expectedFlags},
+ })
+ }
+}
diff --git a/gadget/install/export_test.go b/gadget/install/export_test.go
index aae52eb3f93..c138e10daa4 100644
--- a/gadget/install/export_test.go
+++ b/gadget/install/export_test.go
@@ -29,6 +29,7 @@ import (
)
type MkfsParams = mkfsParams
+type MntfsParams = mntfsParams
var (
MakeFilesystem = makeFilesystem
diff --git a/gadget/install/install.go b/gadget/install/install.go
index 4603162ce25..0e2c266c9b3 100644
--- a/gadget/install/install.go
+++ b/gadget/install/install.go
@@ -400,7 +400,7 @@ func Run(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelSnapInfo,
if options.Mount && vs.Label != "" && vs.HasFilesystem() {
// fs is taken from gadget, as on disk one might be displayed as
// crypto_LUKS, which is not useful for formatting.
- if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs)); err != nil {
+ if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs), mntfsParams{}); err != nil {
return nil, err
}
}
@@ -592,7 +592,7 @@ func MountVolumes(onVolumes map[string]*gadget.Volume, encSetupData *EncryptionS
// Device might have been encrypted
device := deviceForMaybeEncryptedVolume(&part, encSetupData)
- if err := mountFilesystem(device, part.LinuxFilesystem(), mntPt); err != nil {
+ if err := mountFilesystem(device, part.LinuxFilesystem(), mntPt, mntfsParams{}); err != nil {
defer unmount()
return "", nil, fmt.Errorf("cannot mount %q at %q: %v", device, mntPt, err)
}
@@ -788,7 +788,7 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS
if options.Mount && vs.Label != "" && vs.HasFilesystem() {
// fs is taken from gadget, as on disk one might be displayed as
// crypto_LUKS, which is not useful for formatting.
- if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs)); err != nil {
+ if err := mountFilesystem(fsDevice, vs.LinuxFilesystem(), getMntPointForPart(vs), mntfsParams{}); err != nil {
return nil, err
}
}
diff --git a/gadget/validate.go b/gadget/validate.go
index 5fb5576f416..1996cc56945 100644
--- a/gadget/validate.go
+++ b/gadget/validate.go
@@ -141,6 +141,12 @@ func ruleValidateVolumes(vols map[string]*Volume, model Model, extra *Validation
}
func ruleValidateVolume(vol *Volume, hasModes bool) error {
+ if vol.Schema == schemaEMMC {
+ if err := ruleValidateEMMCVolume(vol); err != nil {
+ return err
+ }
+ }
+
for idx, s := range vol.Structure {
if err := ruleValidateVolumeStructure(&s, hasModes); err != nil {
return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err)
@@ -150,6 +156,30 @@ func ruleValidateVolume(vol *Volume, hasModes bool) error {
return nil
}
+func ruleValidateEMMCVolume(vol *Volume) error {
+ // Only content, schema and name can be set currently for eMMC
+ if vol.Bootloader != "" {
+ return fmt.Errorf(`cannot set "bootloader" for eMMC schemas`)
+ }
+ if vol.ID != "" {
+ return fmt.Errorf(`cannot set "id" for eMMC schemas`)
+ }
+ if len(vol.Partial) != 0 {
+ return fmt.Errorf(`cannot set "partial" content for eMMC schemas`)
+ }
+ return nil
+}
+
+func validateEMMCStructureName(vs *VolumeStructure) error {
+ if vs.EnclosingVolume.Schema != schemaEMMC {
+ return nil
+ }
+ if !strutil.ListContains(validEMMCVolumeNames, vs.Name) {
+ return fmt.Errorf("cannot use %q as emmc name, only %q is allowed", vs.Name, validEMMCVolumeNames)
+ }
+ return nil
+}
+
func ruleValidateVolumeStructure(vs *VolumeStructure, hasModes bool) error {
var reservedLabels []string
if hasModes {
@@ -160,6 +190,9 @@ func ruleValidateVolumeStructure(vs *VolumeStructure, hasModes bool) error {
if err := validateReservedLabels(vs, reservedLabels); err != nil {
return err
}
+ if err := validateEMMCStructureName(vs); err != nil {
+ return err
+ }
return nil
}
@@ -179,6 +212,9 @@ var (
ubuntuSeedLabel,
ubuntuDataLabel,
}
+
+ // valid names for volumes under an eMMC schema
+ validEMMCVolumeNames = []string{"boot0", "boot1"}
)
func validateReservedLabels(vs *VolumeStructure, reservedLabels []string) error {
diff --git a/gadget/validate_test.go b/gadget/validate_test.go
index 2e92e0dd098..e1022505d97 100644
--- a/gadget/validate_test.go
+++ b/gadget/validate_test.go
@@ -78,6 +78,7 @@ func (s *validateGadgetTestSuite) TestRuleValidateStructureReservedLabels(c *C)
},
},
}
+ gadget.SetEnclosingVolumeInStructs(gi.Volumes)
err := gadget.Validate(gi, tc.model, nil)
if tc.err == "" {
c.Check(err, IsNil)
@@ -88,6 +89,35 @@ func (s *validateGadgetTestSuite) TestRuleValidateStructureReservedLabels(c *C)
}
+func (s *validateGadgetTestSuite) TestRuleValidateStructureEmmcNames(c *C) {
+ for _, tc := range []struct {
+ name, err string
+ model gadget.Model
+ }{
+ {name: "some-name", err: `cannot use "some-name" as emmc name, only \["boot0" "boot1"\] is allowed`},
+ {name: "boot0", err: ""},
+ {name: "boot1", err: ""},
+ } {
+ gi := &gadget.Info{
+ Volumes: map[string]*gadget.Volume{
+ "emmc": {
+ Schema: "emmc",
+ Structure: []gadget.VolumeStructure{{
+ Name: tc.name,
+ }},
+ },
+ },
+ }
+ gadget.SetEnclosingVolumeInStructs(gi.Volumes)
+ err := gadget.Validate(gi, tc.model, nil)
+ if tc.err == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, ".*: "+tc.err)
+ }
+ }
+}
+
// rolesYaml produces gadget metadata with volumes with structure withs the given
// role if data, seed or save are != "-", and with their label set to the value
func rolesYaml(c *C, data, seed, save string) *gadget.Info {
diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go
index 219aa60d79e..0f519dd970e 100644
--- a/interfaces/builtin/common_files.go
+++ b/interfaces/builtin/common_files.go
@@ -104,6 +104,13 @@ func (iface *commonFilesInterface) validateSinglePath(np string) error {
if strings.HasSuffix(np, "/") {
return fmt.Errorf(`%q cannot end with "/"`, np)
}
+ if strings.HasSuffix(np, "@") {
+ // Variables in AppArmor profiles have the form `@{FOO}`. Since we're
+ // going to add `{,/,/**}` to the end of the path, we cannot have a
+ // trailing '@', else we'll end up with a path which ends with
+ // `@{,/,/**}`, which looks problematically like an AppArmor variable.
+ return fmt.Errorf(`%q cannot end with "@"`, np)
+ }
p := filepath.Clean(np)
if p != np {
return fmt.Errorf("cannot use %q: try %q", np, filepath.Clean(np))
diff --git a/interfaces/builtin/custom_device.go b/interfaces/builtin/custom_device.go
index e39abedc197..218b795ffa2 100644
--- a/interfaces/builtin/custom_device.go
+++ b/interfaces/builtin/custom_device.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2022 Canonical Ltd
+ * Copyright (C) 2022-2024 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
@@ -51,8 +51,14 @@ var (
// A cryptic, uninformative error message that we use only on impossible code paths
customDeviceInternalError = errors.New(`custom-device interface internal error`)
- // Validating regexp for filesystem paths
- customDevicePathRegexp = regexp.MustCompile(`^/[^"@]*$`)
+ // Validating regexp for filesystem paths. @ can appear in paths under
+ // /sys/devices for devices that are defined in the device tree (of the
+ // form device@address), so we need to support @ characters in paths.
+ // However, @{foo} is the format for variables in AppArmor, so we must
+ // disallow `@{`. For completeness, we allow paths with a trailing @ as
+ // well. This is not the case for common-files-derived interfaces, since
+ // these append {,/,/**} pattern to the end of filepath.
+ customDevicePathRegexp = regexp.MustCompile(`^/([^"@]|@[^{])*@?$`)
// Validating regexp for udev device names.
// We forbid:
diff --git a/interfaces/builtin/custom_device_test.go b/interfaces/builtin/custom_device_test.go
index 19de629758d..16bd01591d2 100644
--- a/interfaces/builtin/custom_device_test.go
+++ b/interfaces/builtin/custom_device_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2022 Canonical Ltd
+ * Copyright (C) 2022-2024 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
@@ -72,9 +72,11 @@ slots:
read-devices:
- /dev/js*
files:
- write: [ /bar ]
+ write: [ /bar, /baz@qux, /trailing@ ]
read:
- /dev/input/by-id/*
+ - /dev/dma_heap/qcom,qseecom
+ - /sys/devices/platform/soc@0/soc@0:bus@30000000/30350000.ocotp-ctrl/imx-ocotp0/nvmem
udev-tagging:
- kernel: input/mice
subsystem: input
@@ -209,8 +211,12 @@ apps:
`custom-device "devices" path contains invalid glob pattern "\*\*"`,
},
{
- "devices: [/dev/@foo]",
- `custom-device "devices" path must start with / and cannot contain special characters.*`,
+ `devices: ["/dev/@{foo}"]`,
+ `custom-device "devices" path must start with /dev/ and cannot contain special characters.*`,
+ },
+ {
+ `devices: ["/dev/@{foo"]`,
+ `custom-device "devices" path must start with /dev/ and cannot contain special characters.*`,
},
{
"devices: [/dev/foo|bar]",
@@ -405,7 +411,11 @@ func (s *CustomDeviceInterfaceSuite) TestAppArmorSpec(c *C) {
c.Check(plugSnippet, testutil.Contains, `"/dev/input/mice" rwk,`)
c.Check(plugSnippet, testutil.Contains, `"/dev/js*" r,`)
c.Check(plugSnippet, testutil.Contains, `"/bar" rw,`)
+ c.Check(plugSnippet, testutil.Contains, `"/baz@qux" rw,`)
+ c.Check(plugSnippet, testutil.Contains, `"/trailing@" rw,`)
c.Check(plugSnippet, testutil.Contains, `"/dev/input/by-id/*" r,`)
+ c.Check(plugSnippet, testutil.Contains, `"/dev/dma_heap/qcom,qseecom" r,`)
+ c.Check(plugSnippet, testutil.Contains, `"/sys/devices/platform/soc@0/soc@0:bus@30000000/30350000.ocotp-ctrl/imx-ocotp0/nvmem" r,`)
c.Check(slotSnippet, HasLen, 0)
}
diff --git a/interfaces/builtin/desktop_legacy.go b/interfaces/builtin/desktop_legacy.go
index 90a2eb0e742..d2a8ee5417e 100644
--- a/interfaces/builtin/desktop_legacy.go
+++ b/interfaces/builtin/desktop_legacy.go
@@ -273,6 +273,42 @@ dbus (send)
member=Lookup
peer=(label=unconfined),
+# dbusmenu
+dbus (send)
+ bus=session
+ path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}}
+ interface=com.canonical.dbusmenu
+ member="{LayoutUpdated,ItemsPropertiesUpdated}"
+ peer=(label="{plasmashell,unconfined}"),
+
+dbus (receive)
+ bus=session
+ path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}}
+ interface="{com.canonical.dbusmenu,org.freedesktop.DBus.Properties}"
+ member=Get*
+ peer=(label="{plasmashell,unconfined}"),
+
+dbus (receive)
+ bus=session
+ path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}}
+ interface=com.canonical.dbusmenu
+ member="{AboutTo*,Event*}"
+ peer=(label="{plasmashell,unconfined}"),
+
+dbus (receive)
+ bus=session
+ path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}}
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label="{plasmashell,unconfined}"),
+
+dbus (receive)
+ bus=session
+ path=/com/canonical/dbusmenu
+ interface=org.freedesktop.DBus.Properties
+ member=Get*
+ peer=(label="{plasmashell,unconfined}"),
+
# app-indicators
dbus (send)
bus=session
diff --git a/interfaces/builtin/fwupd.go b/interfaces/builtin/fwupd.go
index 22453148a38..70f25942180 100644
--- a/interfaces/builtin/fwupd.go
+++ b/interfaces/builtin/fwupd.go
@@ -154,6 +154,10 @@ const fwupdPermanentSlotAppArmor = `
/sys/devices/**/psp_vbflash rw,
/sys/devices/**/psp_vbflash_status r,
+ # Required by plugin thunderbolt
+ /sys/devices/**/nvm_non_active*/nvmem a,
+ /sys/devices/**/nvm_active*/nvmem r,
+
# DBus accesses
#include
dbus (send)
diff --git a/interfaces/builtin/mount_control.go b/interfaces/builtin/mount_control.go
index 6f6f4daced1..44464b5a9a1 100644
--- a/interfaces/builtin/mount_control.go
+++ b/interfaces/builtin/mount_control.go
@@ -125,7 +125,6 @@ var allowedFilesystemSpecificMountOptions = map[string][]string{
"jfs": {"iocharset=", "resize=", "nointegrity", "integrity", "errors=", "noquota", "quota", "usrquota", "grpquota"},
"msdos": {"blocksize=", "uid=", "gid=", "umask=", "dmask=", "fmask=", "allow_utime=", "check=", "codepage=", "conv=", "cvf_format=", "cvf_option", "debug", "discard", "dos1xfloppy", "errors=", "fat=", "iocharset=", "nfs=", "tz=", "time_offset=", "quiet", "rodir", "showexec", "sys_immutable", "flush", "usefree", "dots", "nodots", "dotsOK="},
"nfs": {"nfsvers=", "vers=", "soft", "hard", "softreval", "nosoftreval", "intr", "nointr", "timeo=", "retrans=", "rsize=", "wsize=", "ac", "noac", "acregmin=", "acregmax=", "acdirmin=", "acdirmax=", "actimeo=", "bg", "fg", "nconnect=", "max_connect=", "rdirplus", "nordirplus", "retry=", "sec=", "sharecache", "nosharecache", "revsport", "norevsport", "lookupcache=", "fsc", "nofsc", "sloppy", "proto=", "udp", "tcp", "rdma", "port=", "mountport=", "mountproto=", "mounthost=", "mountvers=", "namlen=", "lock", "nolock", "cto", "nocto", "acl", "noacl", "local_lock=", "minorversion=", "clientaddr=", "migration", "nomigration"},
- "nfs4": {"nfsvers=", "vers=", "soft", "hard", "softreval", "nosoftreval", "intr", "nointr", "timeo=", "retrans=", "rsize=", "wsize=", "ac", "noac", "acregmin=", "acregmax=", "acdirmin=", "acdirmax=", "actimeo=", "bg", "fg", "nconnect=", "max_connect=", "rdirplus", "nordirplus", "retry=", "sec=", "sharecache", "nosharecache", "revsport", "norevsport", "lookupcache=", "fsc", "nofsc", "sloppy", "proto=", "minorversion=", "port=", "cto", "nocto", "clientaddr=", "migration", "nomigration"},
"ntfs": {"iocharset=", "nls=", "utf8", "uni_xlate=", "posix=", "uid=", "gid=", "umask="},
"ntfs-3g": {"acl", "allow_other", "big_writes", "compression", "debug", "delay_mtime", "delay_mtime=", "dmask=", "efs_raw", "fmask=", "force", "hide_dot_files", "hide_hid_files", "inherit", "locale=", "max_read=", "no_def_opts", "no_detach", "nocompression", "norecover", "permissions", "posix_nlink", "recover", "remove_hiberfile", "show_sys_files", "silent", "special_files=", "streams_interface=", "uid=", "gid=", "umask=", "usermapping=", "user_xattr", "windows_names"},
"lowntfs-3g": {"acl", "allow_other", "big_writes", "compression", "debug", "delay_mtime", "delay_mtime=", "dmask=", "efs_raw", "fmask=", "force", "hide_dot_files", "hide_hid_files", "ignore_case", "inherit", "locale=", "max_read=", "no_def_opts", "no_detach", "nocompression", "norecover", "permissions", "posix_nlink", "recover", "remove_hiberfile", "show_sys_files", "silent", "special_files=", "streams_interface=", "uid=", "gid=", "umask=", "usermapping=", "user_xattr", "windows_names"},
@@ -212,6 +211,13 @@ var disallowedFSTypes = []string{
"tracefs",
}
+// THe filesystems which are considered deprecated and for which a better
+// alternative exists.
+var deprecatedFSTypes = []string{
+ // use "nfs"
+ "nfs4",
+}
+
// mountControlInterface allows creating transient and persistent mounts
type mountControlInterface struct {
commonInterface
@@ -244,6 +250,11 @@ type mountControlInterface struct {
// nearly any path, and due to the super-privileged nature of this interface it
// is expected that sensible values of what are enforced by the store manual
// review queue and security teams.
+//
+// Certain filesystem types impose additional restrictions on the allowed values
+// for "what" attribute:
+// - "tmpfs" - "what" must be set to "none"
+// - "nfs" - "what" must be unset
var (
whatRegexp = regexp.MustCompile(`^(none|/[^"@]*)$`)
whereRegexp = regexp.MustCompile(`^(\$SNAP_COMMON|\$SNAP_DATA)?/[^\$"@]+$`)
@@ -253,8 +264,16 @@ var (
// malicious string like
//
// auto) options=() /malicious/content /var/lib/snapd/hostfs/...,\n mount fstype=(
+//
+// The "type" attribute is an optional list of expected filesystem types. It is
+// most useful in situations when it is known upfront that only a handful of
+// types are accepted for a given mount.
var typeRegexp = regexp.MustCompile(`^[a-z0-9]+$`)
+// Because of additional rules imposed on mount attributes, some filesystems can
+// only be specified as a single "type" entry.
+var exclusiveFsTypes = []string{"tmpfs", "nfs"}
+
type MountInfo struct {
what string
where string
@@ -298,8 +317,18 @@ func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error
}
for _, mount := range mounts {
+ types, err := parseStringList(mount, "type")
+ if err != nil {
+ return err
+ }
+
+ disallowSource := false
+ if strutil.ListContains(types, "nfs") || strutil.ListContains(types, "nfs4") {
+ disallowSource = true
+ }
+
what, ok := mount["what"].(string)
- if !ok {
+ if !ok && !disallowSource {
return fmt.Errorf(`mount-control "what" must be a string`)
}
@@ -316,11 +345,6 @@ func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error
}
}
- types, err := parseStringList(mount, "type")
- if err != nil {
- return err
- }
-
options, err := parseStringList(mount, "options")
if err != nil {
return err
@@ -360,6 +384,14 @@ func validateWhatAttr(mountInfo *MountInfo) error {
return validateNoAppArmorRegexpWithError(`cannot use mount-control "what" attribute`, what)
}
+ if mountInfo.isType("nfs") {
+ if what != "" {
+ return fmt.Errorf(`mount-control "what" attribute must not be specified for nfs mounts`)
+ }
+ // that's it for nfs
+ return nil
+ }
+
if !whatRegexp.MatchString(what) {
return fmt.Errorf(`mount-control "what" attribute is invalid: must start with / and not contain special characters`)
}
@@ -404,22 +436,33 @@ func validateWhereAttr(where string) error {
}
func validateMountTypes(types []string) error {
- includesTmpfs := false
+ exclusiveFsType := ""
+
+ // multiple types specified in "type" are useful when the accepted
+ // filesystem type is known upfront or the mount uses one of the special
+ // types, such as "nfs" or "tmpfs"
for _, t := range types {
if !typeRegexp.MatchString(t) {
return fmt.Errorf(`mount-control filesystem type invalid: %q`, t)
}
+
if strutil.ListContains(disallowedFSTypes, t) {
return fmt.Errorf(`mount-control forbidden filesystem type: %q`, t)
}
- if t == "tmpfs" {
- includesTmpfs = true
+
+ if strutil.ListContains(deprecatedFSTypes, t) {
+ return fmt.Errorf(`mount-control deprecated filesystem type: %q`, t)
+ }
+
+ if exclusiveFsType == "" && strutil.ListContains(exclusiveFsTypes, t) {
+ exclusiveFsType = t
}
}
- if includesTmpfs && len(types) > 1 {
- return errors.New(`mount-control filesystem type "tmpfs" cannot be listed with other types`)
+ if exclusiveFsType != "" && len(types) > 1 {
+ return fmt.Errorf(`mount-control filesystem type %q cannot be listed with other types`, exclusiveFsType)
}
+
return nil
}
@@ -480,15 +523,15 @@ func isAllowedFilesystemSpecificMountOption(types []string, optionName string) b
}
func validateMountInfo(mountInfo *MountInfo) error {
- if err := validateWhatAttr(mountInfo); err != nil {
+ if err := validateMountTypes(mountInfo.types); err != nil {
return err
}
- if err := validateWhereAttr(mountInfo.where); err != nil {
+ if err := validateWhatAttr(mountInfo); err != nil {
return err
}
- if err := validateMountTypes(mountInfo.types); err != nil {
+ if err := validateWhereAttr(mountInfo.where); err != nil {
return err
}
@@ -583,6 +626,15 @@ func (iface *mountControlInterface) AppArmorConnectedPlug(spec *apparmor.Specifi
target = expanded + target[variableEnd:]
}
+ if mountInfo.isType("nfs") {
+ // override NFS share source, also see 'nfs-mount' interface
+ source = "*:**"
+
+ // emit additional rule required by NFS
+ emit(" # Allow lookup of RPC program numbers (due to mount-control)\n")
+ emit(" /etc/rpc r,\n")
+ }
+
var typeRule string
if optionIncompatibleWithFsType(mountInfo.options) != "" {
// In this rule the FS type will not match unless it's empty
diff --git a/interfaces/builtin/mount_control_test.go b/interfaces/builtin/mount_control_test.go
index b1824a52b51..45b8681aa70 100644
--- a/interfaces/builtin/mount_control_test.go
+++ b/interfaces/builtin/mount_control_test.go
@@ -78,6 +78,9 @@ plugs:
where: $SNAP_COMMON/mnt/**
type: [aufs]
options: [br:/mnt/a, add:0:/mnt/b, dirwh=1, rw]
+ - type: [nfs]
+ where: /media/foo/**
+ options: [rw]
apps:
app:
plugs: [mntctl]
@@ -307,6 +310,30 @@ func (s *MountControlInterfaceSuite) TestSanitizePlugUnhappy(c *C) {
"mount:\n - what: diag\n where: /dev/ffs-diag\n type: [functionfs]\n options: [rw,uid=*]",
`cannot use mount-control "option" attribute: "uid=\*" contains a reserved apparmor char from.*`,
},
+ {
+ "mount:\n - what: diag\n where: /media/foo\n type: [nfs]\n options: [rw]",
+ `mount-control "what" attribute must not be specified for nfs mounts.*`,
+ },
+ {
+ "mount:\n - where: /media/foo\n type: [nfs, ext4]\n options: [rw]",
+ `mount-control filesystem type "nfs" cannot be listed with other types`,
+ },
+ {
+ "mount:\n - where: /media/foo\n type: [tmpfs, nfs, ext4]\n options: [rw]",
+ `mount-control filesystem type "tmpfs" cannot be listed with other types`,
+ },
+ {
+ "mount:\n - what: 123\n where: /media/foo\n type: [ext4]\n options: [rw]",
+ `mount-control "what" must be a string`,
+ },
+ // the deprecated nfs4 isn't explicitly forbidden, but it is not
+ // possible construct a valid and useful specification using this
+ // type
+ {
+ // deprecated nfs4
+ "mount:\n - where: /media/foo\n type: [nfs4]\n options: [rw]",
+ `mount-control deprecated filesystem type: "nfs4"`,
+ },
}
for _, testData := range data {
@@ -376,6 +403,14 @@ func (s *MountControlInterfaceSuite) TestAppArmorSpec(c *C) {
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine7)
expectedUmountLine7 := `umount "/var/snap/consumer/common/mnt/**{,/}",`
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedUmountLine7)
+
+ expectedMountLine8 := `mount fstype=(nfs) options=(rw) ` +
+ `"*:**" -> "/media/foo/**{,/}",`
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine8)
+ expectedUmountLine8 := `umount "/media/foo/**{,/}",`
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedUmountLine8)
+ expectedExtraLine8 := ` /etc/rpc r,`
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedExtraLine8)
}
func (s *MountControlInterfaceSuite) TestStaticInfo(c *C) {
diff --git a/interfaces/builtin/mpris.go b/interfaces/builtin/mpris.go
index e3dc6814c20..2251c52b2d9 100644
--- a/interfaces/builtin/mpris.go
+++ b/interfaces/builtin/mpris.go
@@ -61,27 +61,27 @@ dbus (send)
path=/org/freedesktop/DBus
interface=org.freedesktop.DBus
member="{Request,Release}Name"
- peer=(name=org.freedesktop.DBus, label=unconfined),
+ peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"),
dbus (send)
bus=system
path=/org/freedesktop/DBus
interface=org.freedesktop.DBus
member="GetConnectionUnix{ProcessID,User}"
- peer=(name=org.freedesktop.DBus, label=unconfined),
+ peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"),
dbus (send)
bus=session
path=/org/mpris/MediaPlayer2
interface=org.freedesktop.DBus.Properties
member="{GetAll,PropertiesChanged}"
- peer=(name=org.freedesktop.DBus, label=unconfined),
+ peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"),
dbus (send)
bus=session
path=/org/mpris/MediaPlayer2
interface="org.mpris.MediaPlayer2{,.Player}"
- peer=(name=org.freedesktop.DBus, label=unconfined),
+ peer=(name=org.freedesktop.DBus, label="{plasmashell,unconfined}"),
# we can always connect to ourselves
dbus (receive)
@@ -122,11 +122,11 @@ const mprisConnectedSlotAppArmorClassic = `
dbus (receive)
bus=session
path=/org/mpris/MediaPlayer2
- peer=(label=unconfined),
+ peer=(label="{plasmashell,unconfined}"),
dbus (receive)
bus=session
interface=org.freedesktop.DBus.Introspectable
- peer=(label=unconfined),
+ peer=(label="{plasmashell,unconfined}"),
`
const mprisConnectedPlugAppArmor = `
@@ -139,19 +139,19 @@ dbus (send)
bus=session
path=/org/freedesktop/DBus
interface=org.freedesktop.DBus.Introspectable
- peer=(name="org.freedesktop.DBus", label="unconfined"),
+ peer=(name="org.freedesktop.DBus", label="{plasmashell,unconfined}"),
dbus (send)
bus=session
path=/{,org,org/mpris,org/mpris/MediaPlayer2}
interface=org.freedesktop.DBus.Introspectable
- peer=(name="org.freedesktop.DBus", label="unconfined"),
+ peer=(name="org.freedesktop.DBus", label="{plasmashell,unconfined}"),
# This reveals all names on the session bus
dbus (send)
bus=session
path=/
interface=org.freedesktop.DBus
member=ListNames
- peer=(name="org.freedesktop.DBus", label="unconfined"),
+ peer=(name="org.freedesktop.DBus", label="{plasmashell,unconfined}"),
# Communicate with the mpris player
dbus (send)
diff --git a/interfaces/builtin/opengl.go b/interfaces/builtin/opengl.go
index 71aecbbd0d2..c66fe38f7a7 100644
--- a/interfaces/builtin/opengl.go
+++ b/interfaces/builtin/opengl.go
@@ -193,6 +193,9 @@ unix (send, receive) type=dgram peer=(addr="@var/run/nvidia-xdriver-*"),
/dev/nvgpu/igpu[0-9]*/ctrl rw,
/dev/nvgpu/igpu[0-9]*/prof rw,
/dev/host1x-fence rw,
+
+# Kernel Fusion Driver for AMD GPUs
+/dev/kfd rw,
`
type openglInterface struct {
@@ -224,6 +227,9 @@ var openglConnectedPlugUDev = []string{
// Nvidia dma barrier
`SUBSYSTEM=="host1x-fence"`,
+
+ // Kernel Fusion Driver
+ `SUBSYSTEM=="kfd", KERNEL=="kfd"`,
}
// Those two are the same, but in theory they are separate and can move (or
diff --git a/interfaces/builtin/opengl_test.go b/interfaces/builtin/opengl_test.go
index ebefb3322de..ba6202915d8 100644
--- a/interfaces/builtin/opengl_test.go
+++ b/interfaces/builtin/opengl_test.go
@@ -93,6 +93,7 @@ func (s *OpenglInterfaceSuite) TestAppArmorSpec(c *C) {
spec := apparmor.NewSpecification(appSet)
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/kfd rw,`)
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/nvidia* rw,`)
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/dri/renderD[0-9]* rw,`)
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/mali[0-9]* rw,`)
@@ -118,7 +119,7 @@ func (s *OpenglInterfaceSuite) TestUDevSpec(c *C) {
c.Assert(err, IsNil)
spec := udev.NewSpecification(appSet)
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
- c.Assert(spec.Snippets(), HasLen, 19)
+ c.Assert(spec.Snippets(), HasLen, 20)
c.Assert(spec.Snippets(), testutil.Contains, `# opengl
SUBSYSTEM=="drm", KERNEL=="card[0-9]*", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# opengl
@@ -143,6 +144,8 @@ KERNEL=="mali[0-9]*", TAG+="snap_consumer_app"`)
KERNEL=="dma_buf_te", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# opengl
KERNEL=="galcore", TAG+="snap_consumer_app"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `# opengl
+SUBSYSTEM=="kfd", KERNEL=="kfd", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_consumer_app", SUBSYSTEM!="module", SUBSYSTEM!="subsystem", RUN+="%v/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`, dirs.DistroLibExecDir))
}
diff --git a/interfaces/builtin/personal_files_test.go b/interfaces/builtin/personal_files_test.go
index 5b8ff11fffd..3bd793b630c 100644
--- a/interfaces/builtin/personal_files_test.go
+++ b/interfaces/builtin/personal_files_test.go
@@ -244,6 +244,7 @@ plugs:
{`read: [ "$HOME/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "\$HOME/home/\$HOME/foo"`},
{`read: [ "$HOME/sweet/$HOME" ]`, `\$HOME must only be used at the start of the path of "\$HOME/sweet/\$HOME"`},
{`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`},
+ {`read: [ "/foo/bar@" ]`, `"/foo/bar@" cannot end with "@"`},
{`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`},
{`read: [ "${HOME}/foo" ]`, `"\${HOME}/foo" contains a reserved apparmor char from .*`},
{`read: [ "$HOME" ]`, `"\$HOME" must start with "\$HOME/"`},
diff --git a/interfaces/builtin/screen_inhibit_control.go b/interfaces/builtin/screen_inhibit_control.go
index 2509c856770..b0e263695d9 100644
--- a/interfaces/builtin/screen_inhibit_control.go
+++ b/interfaces/builtin/screen_inhibit_control.go
@@ -19,13 +19,27 @@
package builtin
+import (
+ "strings"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+)
+
const screenInhibitControlSummary = `allows inhibiting the screen saver`
const screenInhibitBaseDeclarationSlots = `
screen-inhibit-control:
allow-installation:
slot-snap-type:
+ - app
- core
+ deny-auto-connection:
+ slot-snap-type:
+ - app
+ deny-connection:
+ slot-snap-type:
+ - app
`
const screenInhibitControlConnectedPlugAppArmor = `
@@ -39,7 +53,7 @@ dbus (send)
path=/org/gnome/SessionManager
interface=org.gnome.SessionManager
member={Inhibit,Uninhibit}
- peer=(label=unconfined),
+ peer=(label=###SLOT_SECURITY_TAGS###),
# unity screen API
dbus (send)
@@ -47,13 +61,13 @@ dbus (send)
interface="org.freedesktop.DBus.Introspectable"
path="/com/canonical/Unity/Screen"
member="Introspect"
- peer=(label=unconfined),
+ peer=(label=###SLOT_SECURITY_TAGS###),
dbus (send)
bus=system
interface="com.canonical.Unity.Screen"
path="/com/canonical/Unity/Screen"
member={keepDisplayOn,removeDisplayOnRequest}
- peer=(label=unconfined),
+ peer=(label=###SLOT_SECURITY_TAGS###),
# freedesktop.org ScreenSaver
# compatibility rule
@@ -62,7 +76,7 @@ dbus (send)
path=/Screensaver
interface=org.freedesktop.ScreenSaver
member={Inhibit,UnInhibit,SimulateUserActivity}
- peer=(label=unconfined),
+ peer=(label=###SLOT_SECURITY_TAGS###),
# xfce4-power-manager -
# https://github.com/xfce-mirror/xfce4-power-manager/blob/0b3ad06ad4f51eae1aea3cdc26f434d8b5ce763e/src/org.freedesktop.PowerManagement.Inhibit.xml
@@ -71,7 +85,13 @@ dbus (send)
path=/org/freedesktop/PowerManagement/Inhibit
interface=org.freedesktop.PowerManagement.Inhibit
member={Inhibit,UnInhibit}
- peer=(label=unconfined),
+ peer=(label=###SLOT_SECURITY_TAGS###),
+dbus (receive)
+ bus=session
+ path=/org/freedesktop/PowerManagement/Inhibit
+ interface=org.freedesktop.PowerManagement.Inhibit
+ member=HasInhibitChanged
+ peer=(label=###SLOT_SECURITY_TAGS###),
# API rule
dbus (send)
@@ -79,7 +99,7 @@ dbus (send)
path=/{,org/freedesktop/,org/gnome/}ScreenSaver
interface=org.{freedesktop,gnome}.ScreenSaver
member={Inhibit,UnInhibit,SimulateUserActivity}
- peer=(label=unconfined),
+ peer=(label=###SLOT_SECURITY_TAGS###),
# gnome, kde and cinnamon screensaver
dbus (send)
@@ -87,15 +107,111 @@ dbus (send)
path=/{,ScreenSaver}
interface=org.{gnome.ScreenSaver,kde.screensaver,cinnamon.ScreenSaver}
member=SimulateUserActivity
- peer=(label=unconfined),
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`
+
+const screenInhibitControlConnectedSlotAppArmor = `
+# Description: Can inhibit and uninhibit screen savers in desktop sessions.
+#include
+#include
+
+# gnome-session
+dbus (receive)
+ bus=session
+ path=/org/gnome/SessionManager
+ interface=org.gnome.SessionManager
+ member={Inhibit,Uninhibit}
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# unity screen API
+dbus (receive)
+ bus=system
+ interface="org.freedesktop.DBus.Introspectable"
+ path="/com/canonical/Unity/Screen"
+ member="Introspect"
+ peer=(label=###PLUG_SECURITY_TAGS###),
+dbus (receive)
+ bus=system
+ interface="com.canonical.Unity.Screen"
+ path="/com/canonical/Unity/Screen"
+ member={keepDisplayOn,removeDisplayOnRequest}
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# freedesktop.org ScreenSaver
+# compatibility rule
+dbus (receive)
+ bus=session
+ path=/Screensaver
+ interface=org.freedesktop.ScreenSaver
+ member={Inhibit,UnInhibit,SimulateUserActivity}
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# xfce4-power-manager -
+# https://github.com/xfce-mirror/xfce4-power-manager/blob/0b3ad06ad4f51eae1aea3cdc26f434d8b5ce763e/src/org.freedesktop.PowerManagement.Inhibit.xml
+dbus (receive)
+ bus=session
+ path=/org/freedesktop/PowerManagement/Inhibit
+ interface=org.freedesktop.PowerManagement.Inhibit
+ member={Inhibit,UnInhibit}
+ peer=(label=###PLUG_SECURITY_TAGS###),
+dbus (send)
+ bus=session
+ path=/org/freedesktop/PowerManagement/Inhibit
+ interface=org.freedesktop.PowerManagement.Inhibit
+ member=HasInhibitChanged
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# API rule
+dbus (receive)
+ bus=session
+ path=/{,org/freedesktop/,org/gnome/}ScreenSaver
+ interface=org.{freedesktop,gnome}.ScreenSaver
+ member={Inhibit,UnInhibit,SimulateUserActivity}
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
+# gnome, kde and cinnamon screensaver
+dbus (receive)
+ bus=session
+ path=/{,ScreenSaver}
+ interface=org.{gnome.ScreenSaver,kde.screensaver,cinnamon.ScreenSaver}
+ member=SimulateUserActivity
+ peer=(label=###PLUG_SECURITY_TAGS###),
`
+type screenInhibitControlInterface struct {
+ commonInterface
+}
+
+func (iface *screenInhibitControlInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ old := "###SLOT_SECURITY_TAGS###"
+ var new string
+ if implicitSystemConnectedSlot(slot) {
+ // we are running on a system that has the screen-inhibit-control slot
+ // provided by the OS snap and so will run unconfined
+ new = "unconfined"
+ } else {
+ new = slot.LabelExpression()
+ }
+ snippet := strings.Replace(screenInhibitControlConnectedPlugAppArmor, old, new, -1)
+ spec.AddSnippet(snippet)
+ return nil
+}
+
+func (iface *screenInhibitControlInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ old := "###PLUG_SECURITY_TAGS###"
+ var new = plug.LabelExpression()
+ snippet := strings.Replace(screenInhibitControlConnectedSlotAppArmor, old, new, -1)
+ spec.AddSnippet(snippet)
+ return nil
+}
+
func init() {
- registerIface(&commonInterface{
- name: "screen-inhibit-control",
- summary: screenInhibitControlSummary,
- implicitOnClassic: true,
- baseDeclarationSlots: screenInhibitBaseDeclarationSlots,
- connectedPlugAppArmor: screenInhibitControlConnectedPlugAppArmor,
+ registerIface(&screenInhibitControlInterface{
+ commonInterface: commonInterface{
+ name: "screen-inhibit-control",
+ summary: screenInhibitControlSummary,
+ implicitOnClassic: true,
+ baseDeclarationSlots: screenInhibitBaseDeclarationSlots,
+ },
})
}
diff --git a/interfaces/builtin/system_files_test.go b/interfaces/builtin/system_files_test.go
index 7aa69ff954d..5ab02935498 100644
--- a/interfaces/builtin/system_files_test.go
+++ b/interfaces/builtin/system_files_test.go
@@ -50,7 +50,7 @@ version: 1.0
plugs:
system-files:
read: [/etc/read-dir2, /etc/read-file2]
- write: [/etc/write-dir2, /etc/write-file2]
+ write: [/etc/write-dir2, /etc/write-file2, /dev/foo@bar]
apps:
app:
command: foo
@@ -83,6 +83,7 @@ func (s *systemFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) {
"/etc/read-file2{,/,/**}" rk,
"/etc/write-dir2{,/,/**}" rwkl,
"/etc/write-file2{,/,/**}" rwkl,
+"/dev/foo@bar{,/,/**}" rwkl,
`)
}
@@ -133,6 +134,7 @@ plugs:
{`read: [ "$HOME/sweet/$HOME" ]`, `"\$HOME/sweet/\$HOME" must start with "/"`},
{`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`},
{`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`},
+ {`read: [ "/foo/bar@" ]`, `"/foo/bar@" cannot end with "@"`},
}
for _, t := range testCases {
diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go
index b4fbd7ec2ca..290e4be879d 100644
--- a/interfaces/policy/basedeclaration_test.go
+++ b/interfaces/policy/basedeclaration_test.go
@@ -143,19 +143,20 @@ func (s *baseDeclSuite) TestAutoConnection(c *C) {
// these have more complex or in flux policies and have their
// own separate tests
snowflakes := map[string]bool{
- "content": true,
- "core-support": true,
- "desktop": true,
- "home": true,
- "lxd-support": true,
- "microstack-support": true,
- "multipass-support": true,
- "packagekit-control": true,
- "pkcs11": true,
- "remoteproc": true,
- "snapd-control": true,
- "upower-observe": true,
- "empty": true,
+ "content": true,
+ "core-support": true,
+ "desktop": true,
+ "home": true,
+ "lxd-support": true,
+ "microstack-support": true,
+ "multipass-support": true,
+ "packagekit-control": true,
+ "pkcs11": true,
+ "remoteproc": true,
+ "screen-inhibit-control": true,
+ "snapd-control": true,
+ "upower-observe": true,
+ "empty": true,
}
// these simply auto-connect, anything else doesn't
@@ -173,7 +174,6 @@ func (s *baseDeclSuite) TestAutoConnection(c *C) {
"opengl": true,
"optical-drive": true,
"ros-opt-data": true,
- "screen-inhibit-control": true,
"ubuntu-download-manager": true,
"unity7": true,
"unity8": true,
@@ -205,8 +205,9 @@ func (s *baseDeclSuite) TestAutoConnectionImplicitSlotOnly(c *C) {
// these auto-connect only with an implicit slot
autoconnect := map[string]bool{
- "desktop": true,
- "upower-observe": true,
+ "desktop": true,
+ "screen-inhibit-control": true,
+ "upower-observe": true,
}
for _, iface := range all {
@@ -852,6 +853,7 @@ var (
"sd-control": {"core"},
"serial-port": {"core", "gadget"},
"spi": {"core", "gadget"},
+ "screen-inhibit-control": {"core", "app"},
"steam-support": {"core"},
"storage-framework-service": {"app"},
"thumbnailer-service": {"app"},
@@ -1109,6 +1111,7 @@ func (s *baseDeclSuite) TestConnection(c *C) {
"posix-mq": true,
"qualcomm-ipc-router": true,
"raw-volume": true,
+ "screen-inhibit-control": true,
"shared-memory": true,
"storage-framework-service": true,
"thumbnailer-service": true,
@@ -1139,9 +1142,10 @@ func (s *baseDeclSuite) TestConnectionImplicitSlotOnly(c *C) {
// these allow connect only with an implicit slot
autoconnect := map[string]bool{
- "desktop": true,
- "qualcomm-ipc-router": true,
- "upower-observe": true,
+ "desktop": true,
+ "qualcomm-ipc-router": true,
+ "screen-inhibit-control": true,
+ "upower-observe": true,
}
for _, iface := range all {
@@ -1334,6 +1338,7 @@ func (s *baseDeclSuite) TestValidity(c *C) {
"polkit-agent": true,
"remoteproc": true,
"qualcomm-ipc-router": true,
+ "screen-inhibit-control": true,
"sd-control": true,
"shutdown": true,
"shared-memory": true,
@@ -1884,3 +1889,35 @@ plugs:
err = ic.Check()
c.Assert(err, IsNil)
}
+
+func (s *baseDeclSuite) TestConnectionScreenInhibitControl(c *C) {
+ cand := s.connectCand(c, "screen-inhibit-control", "", "")
+ err := cand.Check()
+ c.Assert(err, ErrorMatches, `connection denied by slot rule of interface "screen-inhibit-control"`)
+
+ plugsSlots := `
+plugs:
+ screen-inhibit-control:
+ allow-connection: true
+`
+ snapDecl := s.mockSnapDecl(c, "some-snap", "some-snap", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = snapDecl
+ err = cand.Check()
+ c.Assert(err, IsNil)
+}
+
+func (s *baseDeclSuite) TestAutoConnectionScreenInhibitControl(c *C) {
+ cand := s.connectCand(c, "screen-inhibit-control", "", "")
+ _, err := cand.CheckAutoConnect()
+ c.Assert(err, ErrorMatches, "auto-connection denied by slot rule of interface \"screen-inhibit-control\"")
+
+ plugsSlots := `
+plugs:
+ screen-inhibit-control:
+ allow-auto-connection: true
+`
+ snapDecl := s.mockSnapDecl(c, "some-snap", "some-snap", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = snapDecl
+ _, err = cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+}
diff --git a/overlord/hookstate/ctlcmd/mount.go b/overlord/hookstate/ctlcmd/mount.go
index 65cb8437ce8..91daf84cb68 100644
--- a/overlord/hookstate/ctlcmd/mount.go
+++ b/overlord/hookstate/ctlcmd/mount.go
@@ -72,17 +72,29 @@ func matchMountPathAttribute(path string, attribute interface{}, snapInfo *snap.
return err == nil && pp.Matches(path)
}
-// matchConnection checks whether the given mount connection attributes give
-// the snap permission to execute the mount command
-func (m *mountCommand) matchConnection(attributes map[string]interface{}) bool {
- if !matchMountPathAttribute(m.Positional.What, attributes["what"], m.snapInfo) {
- return false
- }
+func matchMountSourceAttribute(path string, attribute interface{}, fsType string, snapInfo *snap.Info) bool {
+ if fsType == "nfs" {
+ // NFS mount source AppArmor profiles expects a match for "*:**", so
+ // make sure that the attribute is unset, and the path matches the
+ // format
+ if _, ok := attribute.(string); ok {
+ return false
+ }
- if !matchMountPathAttribute(m.Positional.Where, attributes["where"], m.snapInfo) {
- return false
+ host, share, found := strings.Cut(path, ":")
+ if !found || host == "" || strings.Contains(host, "/") || share == "" {
+ return false
+ }
+
+ return true
}
+ return matchMountPathAttribute(path, attribute, snapInfo)
+}
+
+// matchConnection checks whether the given mount connection attributes give
+// the snap permission to execute the mount command
+func (m *mountCommand) matchConnection(attributes map[string]interface{}) bool {
if m.Type != "" {
if types, ok := attributes["type"].([]interface{}); ok {
found := false
@@ -106,6 +118,14 @@ func (m *mountCommand) matchConnection(attributes map[string]interface{}) bool {
}
}
+ if !matchMountSourceAttribute(m.Positional.What, attributes["what"], m.Type, m.snapInfo) {
+ return false
+ }
+
+ if !matchMountPathAttribute(m.Positional.Where, attributes["where"], m.snapInfo) {
+ return false
+ }
+
if optionsIfaces, ok := attributes["options"].([]interface{}); ok {
var allowedOptions []string
for _, iface := range optionsIfaces {
diff --git a/overlord/hookstate/ctlcmd/mount_test.go b/overlord/hookstate/ctlcmd/mount_test.go
index ff7783c3463..44f24e55738 100644
--- a/overlord/hookstate/ctlcmd/mount_test.go
+++ b/overlord/hookstate/ctlcmd/mount_test.go
@@ -143,6 +143,11 @@ func (s *mountSuite) SetUpTest(c *C) {
"options": []string{"ro"},
"persistent": false,
},
+ map[string]interface{}{
+ "where": "/nfs-dest",
+ "options": []string{"rw"},
+ "type": []string{"nfs"},
+ },
},
},
}
@@ -270,6 +275,15 @@ func (s *mountSuite) TestMissingProperPlug(c *C) {
_, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "--persistent", "-o", "bind,rw", "/src", "/dest"}, 0)
c.Check(err, ErrorMatches, `.*no matching mount-control connection found`)
c.Check(s.sysd.EnsureMountUnitFileWithOptionsCalls, HasLen, 0)
+
+ // bad NFS source format
+ _, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", "/src", "/dest"}, 0)
+ c.Check(err, ErrorMatches, `.*no matching mount-control connection found`)
+ _, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", "/host:/src", "/dest"}, 0)
+ c.Check(err, ErrorMatches, `.*no matching mount-control connection found`)
+ _, _, err = ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", ":/share", "/dest"}, 0)
+ c.Check(err, ErrorMatches, `.*no matching mount-control connection found`)
+ c.Check(s.sysd.EnsureMountUnitFileWithOptionsCalls, HasLen, 0)
}
func (s *mountSuite) TestUnitCreationFailure(c *C) {
@@ -353,6 +367,27 @@ func (s *mountSuite) TestHappyWithCommasInPath(c *C) {
})
}
+func (s *mountSuite) TestHappyNFS(c *C) {
+ s.injectSnapWithProperPlug(c)
+
+ s.sysd.EnsureMountUnitFileWithOptionsResult = ResultForEnsureMountUnitFileWithOptions{"/path/unit.mount", nil}
+
+ // Now try with commas in the paths
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"mount", "-o", "rw", "-t", "nfs", "localhost:/var/share", "/nfs-dest"}, 0)
+ c.Check(err, IsNil)
+ c.Check(s.sysd.EnsureMountUnitFileWithOptionsCalls, DeepEquals, []*systemd.MountUnitOptions{
+ {
+ Lifetime: systemd.Transient,
+ Description: "Mount unit for snap1, revision 1 via mount-control",
+ What: "localhost:/var/share",
+ Where: "/nfs-dest",
+ Fstype: "nfs",
+ Options: []string{"rw"},
+ Origin: "mount-control",
+ },
+ })
+}
+
func (s *mountSuite) TestEnsureMountUnitFailed(c *C) {
s.injectSnapWithProperPlug(c)
diff --git a/overlord/hookstate/ctlcmd/refresh.go b/overlord/hookstate/ctlcmd/refresh.go
index 91af2d8ec31..8c33a75a93c 100644
--- a/overlord/hookstate/ctlcmd/refresh.go
+++ b/overlord/hookstate/ctlcmd/refresh.go
@@ -365,7 +365,7 @@ func (c *refreshCommand) printInhibitLockHint() error {
}
defer lock.Unlock()
- hint, _, err := runinhibit.IsLocked(snapName)
+ hint, _, err := runinhibit.IsLocked(snapName, nil)
if err != nil {
return err
}
diff --git a/overlord/hookstate/ctlcmd/refresh_test.go b/overlord/hookstate/ctlcmd/refresh_test.go
index 0e72badf4e6..f9713ed162e 100644
--- a/overlord/hookstate/ctlcmd/refresh_test.go
+++ b/overlord/hookstate/ctlcmd/refresh_test.go
@@ -511,7 +511,7 @@ func (s *refreshSuite) TestRefreshPrintInhibitHint(c *C) {
err = lock.Lock()
c.Assert(err, IsNil)
inhibitInfo := runinhibit.InhibitInfo{Previous: snap.R(1)}
- c.Check(runinhibit.LockWithHint("snap1", runinhibit.HintInhibitedForRefresh, inhibitInfo), IsNil)
+ c.Check(runinhibit.LockWithHint("snap1", runinhibit.HintInhibitedForRefresh, inhibitInfo, nil), IsNil)
lock.Unlock()
stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--show-lock"}, 0)
diff --git a/overlord/hookstate/hooks.go b/overlord/hookstate/hooks.go
index b2dd9844f00..d13a0298538 100644
--- a/overlord/hookstate/hooks.go
+++ b/overlord/hookstate/hooks.go
@@ -177,7 +177,7 @@ func (h *gateAutoRefreshHookHandler) Before() error {
defer lock.Unlock()
inhibitInfo := runinhibit.InhibitInfo{Previous: snapRev}
- if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedGateRefresh, inhibitInfo); err != nil {
+ if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedGateRefresh, inhibitInfo, st.Unlocker()); err != nil {
return err
}
@@ -214,7 +214,7 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) {
// invoking --hold/--proceed; this means proceed (except for respecting
// refresh inhibit).
if h.refreshAppAwareness {
- if err := runinhibit.Unlock(snapName); err != nil {
+ if err := runinhibit.Unlock(snapName, st.Unlocker()); err != nil {
return fmt.Errorf("cannot unlock inhibit lock for snap %s: %v", snapName, err)
}
}
@@ -232,7 +232,7 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) {
case snapstate.GateAutoRefreshHold:
// for action=hold the ctlcmd calls HoldRefresh; only unlock runinhibit.
if h.refreshAppAwareness {
- if err := runinhibit.Unlock(snapName); err != nil {
+ if err := runinhibit.Unlock(snapName, st.Unlocker()); err != nil {
return fmt.Errorf("cannot unlock inhibit lock of snap %s: %v", snapName, err)
}
}
@@ -243,14 +243,18 @@ func (h *gateAutoRefreshHookHandler) Done() (err error) {
return err
}
if h.refreshAppAwareness {
+ // Unlock global state once for IsLocked and LockWithHint instead
+ // of unlocking/locking state twice quickly.
+ st.Unlock()
+ defer st.Lock()
// we have HintInhibitedGateRefresh lock already when running the hook, change
// it to HintInhibitedForRefresh.
// Also let's reuse inhibit info that was saved in Before().
- _, inhibitInfo, err := runinhibit.IsLocked(snapName)
+ _, inhibitInfo, err := runinhibit.IsLocked(snapName, nil)
if err != nil {
return err
}
- if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRefresh, inhibitInfo); err != nil {
+ if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRefresh, inhibitInfo, nil); err != nil {
return fmt.Errorf("cannot set inhibit lock for snap %s: %v", snapName, err)
}
}
@@ -284,7 +288,7 @@ func (h *gateAutoRefreshHookHandler) Error(hookErr error) (ignoreHookErr bool, e
}
defer lock.Unlock()
- if err := runinhibit.Unlock(snapName); err != nil {
+ if err := runinhibit.Unlock(snapName, st.Unlocker()); err != nil {
return false, fmt.Errorf("cannot release inhibit lock of snap %s: %v", snapName, err)
}
}
diff --git a/overlord/hookstate/hooks_test.go b/overlord/hookstate/hooks_test.go
index 6530634d062..d162d5777b4 100644
--- a/overlord/hookstate/hooks_test.go
+++ b/overlord/hookstate/hooks_test.go
@@ -145,7 +145,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookProceedRuninhibitLock(
defer ctx.Unlock()
// check that runinhibit hint has been set by Before() hook handler.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)})
@@ -178,7 +178,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookProceedRuninhibitLock(
c.Assert(change.Err(), IsNil)
c.Assert(change.Status(), Equals, state.DoneStatus)
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedForRefresh)
c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)})
@@ -192,7 +192,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit(
defer ctx.Unlock()
// check that runinhibit hint has been set by Before() hook handler.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)})
@@ -226,7 +226,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit(
c.Assert(change.Status(), Equals, state.DoneStatus)
// runinhibit lock is released.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -237,7 +237,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookHoldUnlocksRuninhibit(
func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRuninhibit(c *C) {
hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
// validity, refresh is inhibited for snap-a.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)})
@@ -279,7 +279,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRunin
checkIsNotHeld(c, st, "snap-a")
// runinhibit lock is released.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -290,7 +290,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceedUnlocksRunin
func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) {
hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -327,7 +327,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) {
checkIsNotHeld(c, st, "snap-b")
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -338,7 +338,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshDefaultProceed(c *C) {
func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) {
hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -373,7 +373,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) {
checkIsHeld(c, st, "snap-a", "snap-a")
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -384,7 +384,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookError(c *C) {
func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C) {
hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -425,7 +425,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C
checkIsHeld(c, st, "snap-a", "snap-a")
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -436,7 +436,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorAfterProceed(c *C
func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock(c *C) {
hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedGateRefresh)
c.Check(info, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)})
@@ -476,7 +476,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock(
checkIsHeld(c, st, "snap-a", "snap-a")
// inhibit lock is unlocked
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -485,7 +485,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorRuninhibitUnlock(
func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorHoldErrorLogged(c *C) {
hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) {
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
@@ -532,7 +532,7 @@ func (s *gateAutoRefreshHookSuite) TestGateAutorefreshHookErrorHoldErrorLogged(c
c.Check(held, HasLen, 0)
// no runinhibit because the refresh-app-awareness feature is disabled.
- hint, info, err := runinhibit.IsLocked("snap-a")
+ hint, info, err := runinhibit.IsLocked("snap-a", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(info, Equals, runinhibit.InhibitInfo{})
diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go
index 08bb6142049..10fbf56272f 100644
--- a/overlord/ifacestate/handlers.go
+++ b/overlord/ifacestate/handlers.go
@@ -314,7 +314,28 @@ func (m *InterfaceManager) setupProfilesForAppSet(task *state.Task, appSet *inte
return err
}
- appSet, err := appSetForSnapRevision(st, snapInfo)
+ var appSet *interfaces.SnapAppSet
+ if snapst.PendingSecurity != nil {
+ // a content plug/slot may have already updated in this change, so the appSet
+ // should reflect in the revision (otherwise, we may regenerate the
+ // profile for the wrong revision)
+ snapInfo.SideInfo = *snapst.PendingSecurity.SideInfo
+
+ var comps []*snap.ComponentInfo
+ for _, csi := range snapst.PendingSecurity.Components {
+ ci, err := snapstate.ReadComponentInfo(snapInfo, csi)
+ if err != nil {
+ return fmt.Errorf("cannot read component info when building app set %q: %v", name, err)
+ }
+
+ comps = append(comps, ci)
+ }
+
+ appSet, err = interfaces.NewSnapAppSet(snapInfo, comps)
+ } else {
+ appSet, err = appSetForSnapRevision(st, snapInfo)
+ }
+
if err != nil {
return fmt.Errorf("building app set for snap %q: %v", name, err)
}
diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go
index b439e466f3c..98a78bf7297 100644
--- a/overlord/ifacestate/helpers.go
+++ b/overlord/ifacestate/helpers.go
@@ -43,7 +43,6 @@ import (
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/systemd"
"github.com/snapcore/snapd/timings"
)
@@ -1104,17 +1103,12 @@ func snapsWithSecurityProfiles(st *state.State) ([]*interfaces.SnapAppSet, error
components := make([]*snap.ComponentInfo, 0, len(snapst.PendingSecurity.Components))
for _, csi := range snapst.PendingSecurity.Components {
- cpi := snap.MinimalComponentContainerPlaceInfo(
- csi.Component.ComponentName,
- csi.Revision,
- instanceName,
- )
- container := snapdir.New(cpi.MountDir())
- ci, err := snap.ReadComponentInfoFromContainer(container, snapInfo, csi)
+ ci, err := snapstate.ReadComponentInfo(snapInfo, csi)
if err != nil {
logger.Noticef("cannot read component info for snap %q: %s", instanceName, err)
continue
}
+
components = append(components, ci)
}
diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go
index b633af53d24..c4abb9e66aa 100644
--- a/overlord/ifacestate/ifacestate_test.go
+++ b/overlord/ifacestate/ifacestate_test.go
@@ -10696,3 +10696,136 @@ func (s *interfaceManagerSuite) TestOnSnapLinkageChanged(c *C) {
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{&info.SideInfo}),
})
}
+
+func (s *interfaceManagerSuite) TestSetupProfilesAffectedSnapRegenUsesMostRecentRevision(c *C) {
+ // this test checks a bug fix where a consumer and producer snap (content interface)
+ // were refreshed together and sometimes the producing snap lost access to its own
+ // file. The issue was that, if the slot (producer) was refreshed first, then,
+ // when the plugging snap was refreshed it would regenerate the slot's profile
+ // but with the previous revision, not the revision that the current change was
+ // updating to.
+ s.mockIfaces(&ifacetest.TestInterface{InterfaceName: "test"})
+
+ const componentYaml = `
+component: producer2+comp1
+type: standard
+version: 1.0
+`
+ compProducerYaml := producer2Yaml +
+ `components:
+ comp1:
+ type: standard
+`
+
+ mgr := s.manager(c)
+ repo := mgr.Repository()
+
+ consumer := s.mockSnap(c, consumer2Yaml)
+ producer := s.mockSnap(c, compProducerYaml)
+
+ s.state.Lock()
+ var snapst snapstate.SnapState
+ c.Assert(snapstate.Get(s.state, "producer2", &snapst), IsNil)
+
+ compInfo := snaptest.MockComponent(c, componentYaml, producer, snap.ComponentSideInfo{
+ Revision: snap.R(1),
+ })
+
+ err := snapst.Sequence.AddComponentForRevision(snap.R(1), &sequence.ComponentState{
+ SideInfo: &compInfo.ComponentSideInfo,
+ CompType: snap.StandardComponent,
+ })
+ c.Assert(err, IsNil)
+ snapstate.Set(s.state, "producer2", &snapst)
+
+ for _, info := range []*snap.Info{consumer, producer} {
+ appSet, err := interfaces.NewSnapAppSet(info, nil)
+ c.Assert(err, IsNil)
+
+ err = repo.AddAppSet(appSet)
+ c.Assert(err, IsNil)
+ }
+
+ connRef := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{
+ Snap: "consumer2",
+ Name: "plug",
+ }, SlotRef: interfaces.SlotRef{
+ Snap: "producer2",
+ Name: "slot",
+ }}
+ _, err = repo.Connect(connRef, nil, nil, nil, nil, nil)
+ c.Assert(err, IsNil)
+
+ s.state.Set("conns", map[string]interface{}{"consumer2:plug producer2:slot": map[string]interface{}{"interface": "test"}})
+
+ // mock new snaps for the refresh
+ snaptest.MockSnap(c, consumer2Yaml, &snap.SideInfo{Revision: snap.R(2)})
+ snaptest.MockSnap(c, compProducerYaml, &snap.SideInfo{Revision: snap.R(2)})
+ snaptest.MockComponent(c, componentYaml, producer, snap.ComponentSideInfo{
+ Revision: snap.R(2),
+ })
+
+ // need to mark snap as inactive (which it would be during unlink)
+ for _, sn := range []string{"consumer2", "producer2"} {
+ var snapst snapstate.SnapState
+ err = snapstate.Get(s.state, sn, &snapst)
+ c.Assert(err, IsNil)
+
+ snapst.Active = false
+ snapstate.Set(s.state, sn, &snapst)
+ }
+
+ chg := s.state.NewChange("test", "")
+ slotTask := s.state.NewTask("setup-profiles", "")
+ slotTask.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "producer2",
+ Revision: snap.R(2),
+ }})
+ slotTask.Set("component-setup", &snapstate.ComponentSetup{
+ CompSideInfo: &snap.ComponentSideInfo{
+ Component: compInfo.Component,
+ Revision: snap.R(2),
+ }})
+ chg.AddTask(slotTask)
+
+ plugTask := s.state.NewTask("setup-profiles", "")
+ plugTask.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: "consumer2",
+ Revision: snap.R(2),
+ }})
+ chg.AddTask(plugTask)
+ plugTask.WaitFor(slotTask)
+ s.state.Unlock()
+
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // ensure that the task succeeded.
+ c.Assert(chg.Err(), IsNil)
+ c.Check(chg.Status(), Equals, state.DoneStatus)
+
+ calls := s.secBackend.SetupCalls
+ c.Assert(calls, HasLen, 4)
+
+ // we run setup-profiles for the slot first
+ c.Assert(calls[0].AppSet.InstanceName(), Equals, "producer2")
+ c.Assert(calls[0].AppSet.Info().Revision, Equals, snap.R(2))
+
+ // the connected plug is regenerated (but revision as we haven't setup its new profile yet)
+ c.Assert(calls[1].AppSet.InstanceName(), Equals, "consumer2")
+ c.Assert(calls[1].AppSet.Info().Revision, Equals, snap.R(1))
+
+ // then we run setup-profiles for the plug
+ c.Assert(calls[2].AppSet.InstanceName(), Equals, "consumer2")
+ c.Assert(calls[2].AppSet.Info().Revision, Equals, snap.R(2))
+
+ // the connected slot is also setup but we use the new revision
+ c.Assert(calls[3].AppSet.InstanceName(), Equals, "producer2")
+ c.Assert(calls[3].AppSet.Info().Revision, Equals, snap.R(2))
+ c.Assert(calls[3].AppSet.Components(), HasLen, 1)
+ c.Assert(calls[3].AppSet.Components()[0].Revision, Equals, snap.R(2))
+}
diff --git a/overlord/install/install_test.go b/overlord/install/install_test.go
index eb2ca305e47..709ece7bcf2 100644
--- a/overlord/install/install_test.go
+++ b/overlord/install/install_test.go
@@ -568,6 +568,7 @@ func (s *installSuite) TestEncryptionSupportInfoGadgetIncompatibleWithEncryption
"storage-safety": tc.storageSafety,
})
+ gadget.SetEnclosingVolumeInStructs(tc.gadgetInfo.Volumes)
res, err := install.GetEncryptionSupportInfo(mockModel, secboot.TPMProvisionFull, kernelInfo, tc.gadgetInfo, nil)
c.Assert(err, IsNil)
c.Check(res, DeepEquals, tc.expected, Commentf("%v", tc))
diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go
index 5dc7dd5d647..118b1e90902 100644
--- a/overlord/snapstate/backend.go
+++ b/overlord/snapstate/backend.go
@@ -79,11 +79,12 @@ type managerBackend interface {
SetupKernelModulesComponents(currentComps, finalComps []*snap.ComponentSideInfo, ksnapName string, ksnapRev snap.Revision, meter progress.Meter) (err error)
CopySnapData(newSnap, oldSnap *snap.Info, opts *dirs.SnapDirOptions, meter progress.Meter) error
SetupSnapSaveData(info *snap.Info, dev snap.Device, meter progress.Meter) error
- LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) (rebootInfo boot.RebootInfo, err error)
+ LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) error
LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revision) error
StartServices(svcs []*snap.AppInfo, disabledSvcs *wrappers.DisabledServices, meter progress.Meter, tm timings.Measurer) error
StopServices(svcs []*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error
QueryDisabledServices(info *snap.Info, pb progress.Meter) (*wrappers.DisabledServices, error)
+ MaybeSetNextBoot(info *snap.Info, dev snap.Device, isUndo bool) (boot.RebootInfo, error)
// the undoers for install
UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev snap.Device, meter progress.Meter) error
@@ -106,7 +107,7 @@ type managerBackend interface {
RemoveComponentDir(cpi snap.ContainerPlaceInfo) error
RemoveContainerMountUnits(cpi snap.ContainerPlaceInfo, meter progress.Meter) error
DiscardSnapNamespace(snapName string) error
- RemoveSnapInhibitLock(snapName string) error
+ RemoveSnapInhibitLock(snapName string, stateUnlocker runinhibit.Unlocker) error
RemoveAllSnapAppArmorProfiles() error
RemoveKernelSnapSetup(instanceName string, rev snap.Revision, meter progress.Meter) error
@@ -119,7 +120,7 @@ type managerBackend interface {
Candidate(sideInfo *snap.SideInfo)
// refresh related
- RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (*osutil.FileLock, error)
+ RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, stateUnlocker runinhibit.Unlocker, decision func() error) (*osutil.FileLock, error)
// (not a backend method because doInstall cannot access the backend)
// WithSnapLock(info *snap.Info, action func() error) error
diff --git a/overlord/snapstate/backend/link.go b/overlord/snapstate/backend/link.go
index 443f62b40ed..0d24e43ddfb 100644
--- a/overlord/snapstate/backend/link.go
+++ b/overlord/snapstate/backend/link.go
@@ -48,10 +48,6 @@ type LinkContext struct {
// installed
FirstInstall bool
- // IsUndo is set when we are installing the previous snap while
- // performing a revert of the latest one that was installed
- IsUndo bool
-
// ServiceOptions is used to configure services.
ServiceOptions *wrappers.SnapServiceOptions
@@ -59,6 +55,9 @@ type LinkContext struct {
// establish run inhibition lock for refresh operations.
RunInhibitHint runinhibit.Hint
+ // StateUnlocker is passed to inhibition lock operations.
+ StateUnlocker runinhibit.Unlocker
+
// RequireMountedSnapdSnap indicates that the apps and services
// generated when linking need to use tooling from the snapd snap mount.
RequireMountedSnapdSnap bool
@@ -153,10 +152,28 @@ func updateCurrentSymlinks(info *snap.Info) (revert func(), e error) {
return revertFunc, nil
}
+// MaybeSetNextBoot configures the system for a reboot if necesssary because
+// of a snap refresh. isUndo must be set when we are installing the previous
+// snap while performing a revert of the latest one that was installed
+func (b Backend) MaybeSetNextBoot(info *snap.Info, dev snap.Device, isUndo bool) (boot.RebootInfo, error) {
+ if b.preseed {
+ return boot.RebootInfo{}, nil
+ }
+
+ bootCtx := boot.NextBootContext{BootWithoutTry: isUndo}
+ return boot.Participant(info, info.Type(), dev).SetNextBoot(bootCtx)
+}
+
// LinkSnap makes the snap available by generating wrappers and setting the current symlinks.
-func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, tm timings.Measurer) (rebootRequired boot.RebootInfo, e error) {
+func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext, tm timings.Measurer) (e error) {
+ // explicitly prevent passing nil state unlocker to avoid internal errors of
+ // forgeting to pass the unlocker leading to deadlocks.
+ if linkCtx.StateUnlocker == nil {
+ return errors.New("internal error: LinkContext.StateUnlocker cannot be nil")
+ }
+
if info.Revision.Unset() {
- return boot.RebootInfo{}, fmt.Errorf("cannot link snap %q with unset revision", info.InstanceName())
+ return fmt.Errorf("cannot link snap %q with unset revision", info.InstanceName())
}
osutil.MaybeInjectFault("link-snap")
@@ -167,7 +184,7 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext,
restart, err = b.generateWrappers(info, linkCtx)
})
if err != nil {
- return boot.RebootInfo{}, err
+ return err
}
defer func() {
if e == nil {
@@ -178,21 +195,11 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext,
})
}()
- var rebootInfo boot.RebootInfo
- if !b.preseed {
- bootCtx := boot.NextBootContext{BootWithoutTry: linkCtx.IsUndo}
- rebootInfo, err = boot.Participant(
- info, info.Type(), dev).SetNextBoot(bootCtx)
- if err != nil {
- return boot.RebootInfo{}, err
- }
- }
-
// only after link snap it will be possible to execute snap
// applications, so ensure that the shared snap directory exists for
// parallel installed snaps
if err := createSharedSnapDirForParallelInstance(info); err != nil {
- return boot.RebootInfo{}, err
+ return err
}
cleanupSharedParallelInstanceDir := func() {
if !linkCtx.HasOtherInstances {
@@ -203,7 +210,7 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext,
revertSymlinks, err := updateCurrentSymlinks(info)
if err != nil {
cleanupSharedParallelInstanceDir()
- return boot.RebootInfo{}, err
+ return err
}
// if anything below here could return error, you need to
// somehow clean up whatever updateCurrentSymlinks did
@@ -214,17 +221,17 @@ func (b Backend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx LinkContext,
revertSymlinks()
cleanupSharedParallelInstanceDir()
- return boot.RebootInfo{}, err
+ return err
}
}
// Stop inhibiting application startup by removing the inhibitor file.
- if err := runinhibit.Unlock(info.InstanceName()); err != nil {
- return boot.RebootInfo{}, err
+ if err := runinhibit.Unlock(info.InstanceName(), linkCtx.StateUnlocker); err != nil {
+ return err
}
- return rebootInfo, nil
+ return nil
}
func (b Backend) LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revision) error {
@@ -379,9 +386,14 @@ func removeGeneratedSnapdWrappers(s *snap.Info, firstInstall bool, meter progres
func (b Backend) UnlinkSnap(info *snap.Info, linkCtx LinkContext, meter progress.Meter) error {
var err0 error
if hint := linkCtx.RunInhibitHint; hint != runinhibit.HintNotInhibited {
+ // explicitly prevent passing nil state unlocker to avoid internal errors of
+ // forgeting to pass the unlocker leading to deadlocks.
+ if linkCtx.StateUnlocker == nil {
+ return errors.New("internal error: LinkContext.StateUnlocker cannot be nil if LinkContext.RunInhibitHint is set")
+ }
// inhibit startup of new programs
inhibitInfo := runinhibit.InhibitInfo{Previous: info.SnapRevision()}
- err0 = runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo)
+ err0 = runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo, linkCtx.StateUnlocker)
}
// remove generated services, binaries etc
diff --git a/overlord/snapstate/backend/link_test.go b/overlord/snapstate/backend/link_test.go
index bb705e64397..754de204bb7 100644
--- a/overlord/snapstate/backend/link_test.go
+++ b/overlord/snapstate/backend/link_test.go
@@ -78,6 +78,13 @@ type linkSuite struct {
var _ = Suite(&linkSuite{})
+func mockLinkContextWithStateUnlocker() backend.LinkContext {
+ return backend.LinkContext{
+ // This is required for LinkSnap
+ StateUnlocker: func() (relock func()) { return func() {} },
+ }
+}
+
func (s *linkSuite) TestLinkDoUndoGenerateWrappers(c *C) {
const yaml = `name: hello
version: 1.0
@@ -110,7 +117,7 @@ apps:
`
info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)})
- _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, IsNil)
l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
@@ -177,7 +184,7 @@ Exec=foo
c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.png"), []byte(""), 0644), IsNil)
c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.svg"), []byte(""), 0644), IsNil)
- _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, IsNil)
l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
@@ -232,7 +239,7 @@ Exec=foo
c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.png"), []byte(""), 0644), IsNil)
c.Assert(os.WriteFile(filepath.Join(iconsDir, "snap.hello.svg"), []byte(""), 0644), IsNil)
- _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, IsNil)
l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
@@ -269,9 +276,12 @@ version: 1.0
`
info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)})
- reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, IsNil)
+ isUndo := false
+ reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo)
+ c.Assert(err, IsNil)
c.Check(reboot, Equals, boot.RebootInfo{RebootRequired: false})
mountDir := info.MountDir()
@@ -301,11 +311,16 @@ version: 1.0
`
info := snaptest.MockSnapInstance(c, "hello_foo", yaml, &snap.SideInfo{Revision: snap.R(11)})
- reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{
+ err := s.be.LinkSnap(info, mockDev, backend.LinkContext{
HasOtherInstances: false,
+ // This is required for LinkSnap
+ StateUnlocker: func() (relock func()) { return func() {} },
}, s.perfTimings)
c.Assert(err, IsNil)
+ isUndo := false
+ reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo)
+ c.Assert(err, IsNil)
c.Check(reboot, Equals, boot.RebootInfo{RebootRequired: false})
mountDir := info.MountDir()
@@ -360,7 +375,11 @@ type: base
`
info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)})
- reboot, err := s.be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
+ c.Assert(err, IsNil)
+
+ isUndo := false
+ reboot, err := s.be.MaybeSetNextBoot(info, coreDev, isUndo)
c.Assert(err, IsNil)
c.Check(reboot, Equals, boot.RebootInfo{RebootRequired: true})
}
@@ -382,7 +401,11 @@ type: kernel
`
info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)})
- reboot, err := be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings)
+ err := be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
+ c.Assert(err, IsNil)
+
+ isUndo := false
+ reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo)
c.Assert(err, IsNil)
c.Check(reboot, DeepEquals, boot.RebootInfo{})
}
@@ -412,7 +435,7 @@ type: snapd
`
info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)})
- _, err := be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings)
+ err := be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, IsNil)
c.Assert(called, Equals, true)
}
@@ -433,11 +456,23 @@ apps:
`
info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)})
- _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
+ linkCtx := backend.LinkContext{StateUnlocker: fakeUnlocker}
+
+ err := s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings)
c.Assert(err, IsNil)
+ // no hint file, no locking needed
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
- _, err = s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err = s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings)
c.Assert(err, IsNil)
+ c.Check(unlockerCalled, Equals, 2)
+ c.Check(relockCalled, Equals, 2)
l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*"))
c.Assert(err, IsNil)
@@ -475,7 +510,7 @@ apps:
`
info := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)})
- _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, IsNil)
err = s.be.UnlinkSnap(info, backend.LinkContext{}, progress.Null)
@@ -506,7 +541,7 @@ func (s *linkSuite) TestLinkFailsForUnsetRevision(c *C) {
info := &snap.Info{
SuggestedName: "foo",
}
- _, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, ErrorMatches, `cannot link snap "foo" with unset revision`)
}
@@ -545,7 +580,11 @@ func (s *linkSuite) TestLinkSnapdSnapOnCore(c *C) {
info, _ := mockSnapdSnapForLink(c)
- reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err = s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
+ c.Assert(err, IsNil)
+
+ isUndo := false
+ reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo)
c.Assert(err, IsNil)
c.Assert(reboot, Equals, boot.RebootInfo{RebootRequired: false})
@@ -654,7 +693,7 @@ func (s *linkCleanupSuite) testLinkCleanupDirOnFail(c *C, dir string) {
c.Assert(os.Chmod(dir, 0555), IsNil)
defer os.Chmod(dir, 0755)
- _, err := s.be.LinkSnap(s.info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(s.info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, NotNil)
_, isPathError := err.(*os.PathError)
_, isLinkError := err.(*os.LinkError)
@@ -699,7 +738,7 @@ func (s *linkCleanupSuite) TestLinkCleanupOnSystemctlFail(c *C) {
})
defer r()
- _, err := s.be.LinkSnap(s.info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(s.info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, ErrorMatches, "ouchie")
for _, d := range []string{dirs.SnapBinariesDir, dirs.SnapDesktopFilesDir, dirs.SnapServicesDir} {
@@ -719,7 +758,7 @@ func (s *linkCleanupSuite) TestLinkCleansUpDataDirAndSymlinksOnSymlinkFail(c *C)
c.Assert(os.Chmod(d, 0555), IsNil)
defer os.Chmod(d, 0755)
- _, err := s.be.LinkSnap(s.info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err := s.be.LinkSnap(s.info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, ErrorMatches, `(?i).*symlink.*permission denied.*`)
c.Check(s.info.DataDir(), testutil.FileAbsent)
@@ -756,9 +795,15 @@ func (s *linkCleanupSuite) testLinkCleanupFailedSnapdSnapOnCorePastWrappers(c *C
linkCtx := backend.LinkContext{
FirstInstall: firstInstall,
+ // This is required for LinkSnap
+ StateUnlocker: func() (relock func()) { return func() {} },
}
- reboot, err := s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings)
+ err = s.be.LinkSnap(info, mockDev, linkCtx, s.perfTimings)
c.Assert(err, ErrorMatches, fmt.Sprintf("symlink %s /.*/snapd/current.*: permission denied", info.Revision))
+
+ isUndo := false
+ reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo)
+ c.Assert(err, IsNil)
c.Assert(reboot, Equals, boot.RebootInfo{RebootRequired: false})
checker := testutil.FilePresent
@@ -814,9 +859,11 @@ version: 1.0
snapinstance := snaptest.MockSnapInstance(c, "instance-snap_foo", yaml, &snap.SideInfo{Revision: snap.R(11)})
- _, err := s.be.LinkSnap(snapinstance, mockDev,
+ err := s.be.LinkSnap(snapinstance, mockDev,
backend.LinkContext{
HasOtherInstances: tc.otherInstances,
+ // This is required for LinkSnap
+ StateUnlocker: func() (relock func()) { return func() {} },
},
s.perfTimings)
c.Assert(err, NotNil)
@@ -871,7 +918,11 @@ func (s *snapdOnCoreUnlinkSuite) TestUndoGeneratedWrappers(c *C) {
return filepath.Join(dirs.SnapServicesDir, filepath.Base(p))
}
- reboot, err := s.be.LinkSnap(info, mockDev, backend.LinkContext{}, s.perfTimings)
+ err = s.be.LinkSnap(info, mockDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
+ c.Assert(err, IsNil)
+
+ isUndo := false
+ reboot, err := s.be.MaybeSetNextBoot(info, mockDev, isUndo)
c.Assert(err, IsNil)
c.Assert(reboot, Equals, boot.RebootInfo{RebootRequired: false})
@@ -885,12 +936,20 @@ func (s *snapdOnCoreUnlinkSuite) TestUndoGeneratedWrappers(c *C) {
// linked snaps do not have a run inhibition lock
c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FileAbsent)
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
linkCtx := backend.LinkContext{
FirstInstall: true,
RunInhibitHint: runinhibit.HintInhibitedForRefresh,
+ StateUnlocker: fakeUnlocker,
}
err = s.be.UnlinkSnap(info, linkCtx, nil)
c.Assert(err, IsNil)
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
// generated wrappers should be gone now
for _, entry := range generatedSnapdUnits {
@@ -903,6 +962,8 @@ func (s *snapdOnCoreUnlinkSuite) TestUndoGeneratedWrappers(c *C) {
// unlink is idempotent
err = s.be.UnlinkSnap(info, linkCtx, nil)
c.Assert(err, IsNil)
+ c.Check(unlockerCalled, Equals, 2)
+ c.Check(relockCalled, Equals, 2)
c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FilePresent)
c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.refresh"), testutil.FilePresent)
}
@@ -928,12 +989,20 @@ func (s *snapdOnCoreUnlinkSuite) TestUnlinkNonFirstSnapdOnCoreDoesNothing(c *C)
}
// content list uses absolute paths already
snaptest.PopulateDir("/", units)
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
linkCtx := backend.LinkContext{
FirstInstall: false,
RunInhibitHint: runinhibit.HintInhibitedForRefresh,
+ StateUnlocker: fakeUnlocker,
}
err = s.be.UnlinkSnap(info, linkCtx, nil)
c.Assert(err, IsNil)
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
for _, unit := range units {
c.Check(unit[0], testutil.FileEquals, "precious")
}
@@ -963,8 +1032,10 @@ apps:
linkCtxWithTooling := backend.LinkContext{
RequireMountedSnapdSnap: true,
+ // This is required for LinkSnap
+ StateUnlocker: func() (relock func()) { return func() {} },
}
- _, err := s.be.LinkSnap(info, mockDev, linkCtxWithTooling, s.perfTimings)
+ err := s.be.LinkSnap(info, mockDev, linkCtxWithTooling, s.perfTimings)
c.Assert(err, IsNil)
c.Assert(filepath.Join(dirs.SnapServicesDir, "snap.hello.svc.service"), testutil.FileContains,
`Wants=usr-lib-snapd.mount
@@ -977,8 +1048,10 @@ After=usr-lib-snapd.mount`)
linkCtxNoTooling := backend.LinkContext{
RequireMountedSnapdSnap: false,
+ // This is required for LinkSnap
+ StateUnlocker: func() (relock func()) { return func() {} },
}
- _, err = s.be.LinkSnap(info, mockDev, linkCtxNoTooling, s.perfTimings)
+ err = s.be.LinkSnap(info, mockDev, linkCtxNoTooling, s.perfTimings)
c.Assert(err, IsNil)
c.Assert(filepath.Join(dirs.SnapServicesDir, "snap.hello.svc.service"), Not(testutil.FileContains), `usr-lib-snapd.mount`)
}
@@ -1001,8 +1074,10 @@ apps:
ServiceOptions: &wrappers.SnapServiceOptions{
QuotaGroup: grp,
},
+ // This is required for LinkSnap
+ StateUnlocker: func() (relock func()) { return func() {} },
}
- _, err = s.be.LinkSnap(info, mockDev, linkCtxWithGroup, s.perfTimings)
+ err = s.be.LinkSnap(info, mockDev, linkCtxWithGroup, s.perfTimings)
c.Assert(err, IsNil)
c.Assert(filepath.Join(dirs.SnapServicesDir, "snap.hello.svc.service"), testutil.FileContains,
"\nSlice=snap.foogroup.slice\n")
@@ -1055,7 +1130,7 @@ type: snapd
be := backend.NewForPreseedMode()
coreDev := boottest.MockUC20Device("run", nil)
- _, err = be.LinkSnap(info, coreDev, backend.LinkContext{}, s.perfTimings)
+ err = be.LinkSnap(info, coreDev, mockLinkContextWithStateUnlocker(), s.perfTimings)
c.Assert(err, ErrorMatches, `BROKEN`)
c.Assert(restartDone, Equals, true)
@@ -1194,3 +1269,13 @@ func (s *linkSuite) TestKillSnapApps(c *C) {
c.Assert(err, IsNil)
c.Assert(called, Equals, 1)
}
+
+func (s *linkSuite) TestLinkSnapNilStateUnlockerError(c *C) {
+ err := s.be.LinkSnap(nil, nil, backend.LinkContext{}, nil)
+ c.Assert(err, ErrorMatches, "internal error: LinkContext.StateUnlocker cannot be nil")
+}
+
+func (s *linkSuite) TestUnlinkSnapNilStateUnlockerError(c *C) {
+ err := s.be.UnlinkSnap(nil, backend.LinkContext{RunInhibitHint: "not-nil"}, nil)
+ c.Assert(err, ErrorMatches, "internal error: LinkContext.StateUnlocker cannot be nil if LinkContext.RunInhibitHint is set")
+}
diff --git a/overlord/snapstate/backend/locking.go b/overlord/snapstate/backend/locking.go
index 0bdb128e199..95064e45a8a 100644
--- a/overlord/snapstate/backend/locking.go
+++ b/overlord/snapstate/backend/locking.go
@@ -19,13 +19,19 @@
package backend
import (
+ "errors"
+
"github.com/snapcore/snapd/cmd/snaplock"
"github.com/snapcore/snapd/cmd/snaplock/runinhibit"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
-func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (lock *osutil.FileLock, err error) {
+func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, stateUnlocker runinhibit.Unlocker, decision func() error) (lock *osutil.FileLock, err error) {
+ if stateUnlocker == nil {
+ return nil, errors.New("internal error: stateUnlocker cannot be nil")
+ }
+
// A process may be created after the soft refresh done upon
// the request to refresh a snap. If such process is alive by
// the time this code is reached the refresh process is stopped.
@@ -65,7 +71,7 @@ func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint,
// and hard checks, as it would effectively make hard check a no-op,
// but it might provide a nicer user experience.
inhibitInfo := runinhibit.InhibitInfo{Previous: info.SnapRevision()}
- if err := runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo); err != nil {
+ if err := runinhibit.LockWithHint(info.InstanceName(), hint, inhibitInfo, stateUnlocker); err != nil {
return nil, err
}
return lock, nil
@@ -81,6 +87,7 @@ func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint,
// Note that this is not a method of the Backend type, so that it can be
// invoked from doInstall, which does not have access to a backend object.
func WithSnapLock(info *snap.Info, action func() error) error {
+ // XXX: Should we unlock state while holding snap lock? (ie. pass runinhibit.Unlocker)
lock, err := snaplock.OpenLock(info.InstanceName())
if err != nil {
return err
diff --git a/overlord/snapstate/backend/locking_test.go b/overlord/snapstate/backend/locking_test.go
index 350b5b9def0..982c9dc40f1 100644
--- a/overlord/snapstate/backend/locking_test.go
+++ b/overlord/snapstate/backend/locking_test.go
@@ -48,18 +48,27 @@ func (s *lockingSuite) TestRunInhibitSnapForUnlinkPositiveDecision(c *C) {
version: 1
`
info := snaptest.MockInfo(c, yaml, &snap.SideInfo{Revision: snap.R(1)})
- lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", func() error {
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
+ lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", fakeUnlocker, func() error {
// This decision function returns nil so the lock is established and
// the inhibition hint is set.
return nil
})
c.Assert(err, IsNil)
c.Assert(lock, NotNil)
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
lock.Close()
- hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName())
+ hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), fakeUnlocker)
c.Assert(err, IsNil)
c.Check(string(hint), Equals, "hint")
c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{Previous: snap.R(1)})
+ c.Check(unlockerCalled, Equals, 2)
+ c.Check(relockCalled, Equals, 2)
}
func (s *lockingSuite) TestRunInhibitSnapForUnlinkNegativeDecision(c *C) {
@@ -67,17 +76,31 @@ func (s *lockingSuite) TestRunInhibitSnapForUnlinkNegativeDecision(c *C) {
version: 1
`
info := snaptest.MockInfo(c, yaml, nil)
- lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", func() error {
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
+ lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", fakeUnlocker, func() error {
// This decision function returns an error so the lock is not
// established and the inhibition hint is not set.
return errors.New("propagated")
})
c.Assert(err, ErrorMatches, "propagated")
c.Assert(lock, IsNil)
- hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName())
+ c.Check(unlockerCalled, Equals, 0)
+ c.Check(relockCalled, Equals, 0)
+ hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), fakeUnlocker)
c.Assert(err, IsNil)
c.Check(string(hint), Equals, "")
c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{})
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
+}
+
+func (s *linkSuite) TestRunInhibitSnapForUnlinkNilStateUnlockerError(c *C) {
+ _, err := s.be.RunInhibitSnapForUnlink(nil, "not-nil", nil, nil)
+ c.Assert(err, ErrorMatches, "internal error: stateUnlocker cannot be nil")
}
func (s *lockingSuite) TestWithSnapLock(c *C) {
diff --git a/overlord/snapstate/backend/setup.go b/overlord/snapstate/backend/setup.go
index e845694f955..a37949565ee 100644
--- a/overlord/snapstate/backend/setup.go
+++ b/overlord/snapstate/backend/setup.go
@@ -20,6 +20,7 @@
package backend
import (
+ "errors"
"fmt"
"os"
"path/filepath"
@@ -302,8 +303,11 @@ func (b Backend) UndoSetupComponent(cpi snap.ContainerPlaceInfo, installRecord *
}
// RemoveSnapInhibitLock removes the file controlling inhibition of "snap run".
-func (b Backend) RemoveSnapInhibitLock(instanceName string) error {
- return runinhibit.RemoveLockFile(instanceName)
+func (b Backend) RemoveSnapInhibitLock(instanceName string, stateUnlocker runinhibit.Unlocker) error {
+ if stateUnlocker == nil {
+ return errors.New("internal error: stateUnlocker cannot be nil")
+ }
+ return runinhibit.RemoveLockFile(instanceName, stateUnlocker)
}
// SetupKernelModulesComponents changes kernel-modules configuration by
diff --git a/overlord/snapstate/backend/setup_test.go b/overlord/snapstate/backend/setup_test.go
index 63a4f6634f4..eed8d07b896 100644
--- a/overlord/snapstate/backend/setup_test.go
+++ b/overlord/snapstate/backend/setup_test.go
@@ -1057,3 +1057,20 @@ func (s *setupSuite) TestRemoveKernelModulesComponentsFails(c *C) {
s.testRemoveKernelModulesComponents(c, newComps, firstInstalled, ksnap, kernRev,
"cannot remove mount in .*: cannot disable comp3-32")
}
+
+func (s *linkSuite) TestRemoveSnapInhibitLock(c *C) {
+ var unlockerCalled, relockCalled int
+ fakeUnlocker := func() (relock func()) {
+ unlockerCalled++
+ return func() { relockCalled++ }
+ }
+ err := s.be.RemoveSnapInhibitLock("some-snap", fakeUnlocker)
+ c.Assert(err, IsNil)
+ c.Check(unlockerCalled, Equals, 1)
+ c.Check(relockCalled, Equals, 1)
+}
+
+func (s *linkSuite) TestRemoveSnapInhibitLockNilStateUnlockerError(c *C) {
+ err := s.be.RemoveSnapInhibitLock("some-snap", nil)
+ c.Assert(err, ErrorMatches, "internal error: stateUnlocker cannot be nil")
+}
diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go
index 58ab52ea88b..e638024dd52 100644
--- a/overlord/snapstate/backend_test.go
+++ b/overlord/snapstate/backend_test.go
@@ -101,6 +101,7 @@ type fakeOp struct {
containerFileName string
snapLocked bool
+ isUndo bool
}
type fakeOps []fakeOp
@@ -1266,7 +1267,25 @@ func (f *fakeSnappyBackend) SetupSnapSaveData(info *snap.Info, _ snap.Device, me
return f.maybeErrForLastOp()
}
-func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) (rebootInfo boot.RebootInfo, err error) {
+func (f *fakeSnappyBackend) MaybeSetNextBoot(
+ info *snap.Info, dev snap.Device, isUndo bool) (boot.RebootInfo, error) {
+
+ op := &fakeOp{
+ op: "maybe-set-next-boot",
+ isUndo: isUndo,
+ }
+ f.appendOp(op)
+
+ reboot := false
+ if f.linkSnapMaybeReboot {
+ reboot = info.InstanceName() == dev.Base() ||
+ (f.linkSnapRebootFor != nil && f.linkSnapRebootFor[info.InstanceName()])
+ }
+
+ return boot.RebootInfo{RebootRequired: reboot}, nil
+}
+
+func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx backend.LinkContext, tm timings.Measurer) (err error) {
if info.MountDir() == f.linkSnapWaitTrigger {
f.linkSnapWaitCh <- 1
<-f.linkSnapWaitCh
@@ -1290,18 +1309,12 @@ func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, dev snap.Device, linkCtx b
if info.MountDir() == f.linkSnapFailTrigger {
op.op = "link-snap.failed"
f.appendOp(op)
- return boot.RebootInfo{RebootRequired: false}, errors.New("fail")
+ return errors.New("fail")
}
f.appendOp(op)
- reboot := false
- if f.linkSnapMaybeReboot {
- reboot = info.InstanceName() == dev.Base() ||
- (f.linkSnapRebootFor != nil && f.linkSnapRebootFor[info.InstanceName()])
- }
-
- return boot.RebootInfo{RebootRequired: reboot}, nil
+ return nil
}
func (f *fakeSnappyBackend) LinkComponent(cpi snap.ContainerPlaceInfo, snapRev snap.Revision) error {
@@ -1551,7 +1564,7 @@ func (f *fakeSnappyBackend) RemoveAllSnapAppArmorProfiles() error {
return f.maybeErrForLastOp()
}
-func (f *fakeSnappyBackend) RemoveSnapInhibitLock(snapName string) error {
+func (f *fakeSnappyBackend) RemoveSnapInhibitLock(snapName string, stateUnlocker runinhibit.Unlocker) error {
f.appendOp(&fakeOp{
op: "remove-inhibit-lock",
name: snapName,
@@ -1632,7 +1645,7 @@ func (f *fakeSnappyBackend) RemoveSnapAliases(snapName string) error {
return f.maybeErrForLastOp()
}
-func (f *fakeSnappyBackend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (lock *osutil.FileLock, err error) {
+func (f *fakeSnappyBackend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, stateUnlocker runinhibit.Unlocker, decision func() error) (lock *osutil.FileLock, err error) {
f.appendOp(&fakeOp{
op: "run-inhibit-snap-for-unlink",
name: info.InstanceName(),
diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go
index 168e3a21da8..428b3d75945 100644
--- a/overlord/snapstate/export_test.go
+++ b/overlord/snapstate/export_test.go
@@ -73,9 +73,9 @@ func MockSnapReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, err
}
func MockReadComponentInfo(mock func(compMntDir string, snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error)) (restore func()) {
- old := readComponentInfo
- readComponentInfo = mock
- return func() { readComponentInfo = old }
+ old := readComponentInfoAt
+ readComponentInfoAt = mock
+ return func() { readComponentInfoAt = old }
}
func MockMountPollInterval(intv time.Duration) (restore func()) {
diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go
index e3b387c9870..4a23e626477 100644
--- a/overlord/snapstate/handlers.go
+++ b/overlord/snapstate/handlers.go
@@ -1341,11 +1341,12 @@ func (m *SnapManager) restoreUnlinkOnError(t *state.Task, info *snap.Info, other
}
linkCtx := backend.LinkContext{
FirstInstall: false,
- IsUndo: true,
ServiceOptions: opts,
HasOtherInstances: otherInstances,
+ // passed state must be locked
+ StateUnlocker: st.Unlocker(),
}
- _, err = m.backend.LinkSnap(info, deviceCtx, linkCtx, tm)
+ err = m.backend.LinkSnap(info, deviceCtx, linkCtx, tm)
return err
}
@@ -1461,6 +1462,7 @@ func (m *SnapManager) doUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) (err erro
// This task is only used for unlinking a snap during refreshes so we
// can safely hard-code this condition here.
RunInhibitHint: runinhibit.HintInhibitedForRefresh,
+ StateUnlocker: st.Unlocker(),
SkipBinaries: skipBinaries,
HasOtherInstances: otherInstances,
}
@@ -1604,11 +1606,16 @@ func (m *SnapManager) undoUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) error {
}
linkCtx := backend.LinkContext{
FirstInstall: false,
- IsUndo: true,
ServiceOptions: opts,
HasOtherInstances: otherInstances,
+ StateUnlocker: st.Unlocker(),
}
- reboot, err := m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings)
+ err = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings)
+ if err != nil {
+ return err
+ }
+ isUndo := true
+ reboot, err := m.backend.MaybeSetNextBoot(oldInfo, deviceCtx, isUndo)
if err != nil {
return err
}
@@ -2213,6 +2220,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) {
FirstInstall: firstInstall,
ServiceOptions: opts,
HasOtherInstances: otherInstances,
+ StateUnlocker: st.Unlocker(),
}
// on UC18+, snap tooling comes from the snapd snap so we need generated
// mount units to depend on the snapd snap mount units
@@ -2247,7 +2255,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) {
// links the new revision to current and ensures a shared base prefix
// directory for parallel installed snaps
- rebootInfo, err := m.backend.LinkSnap(newInfo, deviceCtx, linkCtx, perfTimings)
+ err = m.backend.LinkSnap(newInfo, deviceCtx, linkCtx, perfTimings)
// defer a cleanup helper which will unlink the snap if anything fails after
// this point
defer func() {
@@ -2261,7 +2269,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) {
// snapd snap is special in the sense that we always
// need the current symlink, so we restore the link to
// the old revision
- _, backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings)
+ backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings)
} else {
// snapd during first install and all other snaps
backendErr = m.backend.UnlinkSnap(newInfo, linkCtx, pb)
@@ -2274,6 +2282,19 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) {
if err != nil {
return err
}
+ // Prepare for rebooting when needed
+ // TODO we have to revert changes in bootloader config/modeenv if an
+ // error happens later in this method. This is not likely as possible
+ // errors after this would happen only due to internal errors or not
+ // being able to write to the filesystem, but still. There is also the
+ // question of what would happen if a restart happens when the boot
+ // configuration has been already written but DoneStatus in the state
+ // has not.
+ isUndo := false
+ rebootInfo, err := m.backend.MaybeSetNextBoot(newInfo, deviceCtx, isUndo)
+ if err != nil {
+ return err
+ }
// Restore configuration of the target revision (if available) on revert
if isInstalled {
@@ -2837,8 +2858,8 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error {
pb := NewTaskProgressAdapterLocked(t)
linkCtx := backend.LinkContext{
FirstInstall: firstInstall,
- IsUndo: true,
HasOtherInstances: otherInstances,
+ StateUnlocker: st.Unlocker(),
}
var backendErr error
@@ -2853,7 +2874,7 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error {
// the snapd snap is special in the sense that we need to make
// sure that a sensible version is always linked as current,
// also we never reboot when updating snapd snap
- _, backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings)
+ backendErr = m.backend.LinkSnap(oldInfo, deviceCtx, linkCtx, perfTimings)
} else {
// snapd during first install and all other snaps
backendErr = m.backend.UnlinkSnap(newInfo, linkCtx, pb)
@@ -3388,20 +3409,6 @@ func (m *SnapManager) doKillSnapApps(t *state.Task, _ *tomb.Tomb) (err error) {
lock.Lock()
defer lock.Unlock()
- inhibitInfo := runinhibit.InhibitInfo{Previous: snapsup.Revision()}
- if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRemove, inhibitInfo); err != nil {
- return err
- }
- // Note: The snap hint lock file is completely removed in “discard-snap”
- // so we only need to unlock it in case of an error here or during undo.
- defer func() {
- // Unlock snap inhibition if anything goes wrong afterwards to
- // avoid keeping the snap stuck at this inhibited state.
- if err != nil {
- runinhibit.Unlock(snapName)
- }
- }()
-
var reason snap.AppKillReason
if err := t.Get("kill-reason", &reason); err != nil && !errors.Is(err, state.ErrNoState) {
return err
@@ -3410,11 +3417,27 @@ func (m *SnapManager) doKillSnapApps(t *state.Task, _ *tomb.Tomb) (err error) {
perfTimings := state.TimingsForTask(t)
defer perfTimings.Save(st)
+ inhibitInfo := runinhibit.InhibitInfo{Previous: snapsup.Revision()}
+ if err := runinhibit.LockWithHint(snapName, runinhibit.HintInhibitedForRemove, inhibitInfo, st.Unlocker()); err != nil {
+ return err
+ }
+
// State lock is not needed for killing apps or stopping services and since those
// can take some time, let's unlock the state
st.Unlock()
defer st.Lock()
+ // Note: The snap hint lock file is completely removed in “discard-snap”
+ // so we only need to unlock it in case of an error here or during undo.
+ defer func() {
+ // Unlock snap inhibition if anything goes wrong afterwards to
+ // avoid keeping the snap stuck at this inhibited state.
+ if err != nil {
+ // state is unlocked, it is okay to pass nil here
+ runinhibit.Unlock(snapName, nil)
+ }
+ }()
+
if err := m.backend.KillSnapApps(snapName, reason, perfTimings); err != nil {
// Snap processes termination is best-effort and task should continue
// without returning an error. This is to avoid a maliciously crafted snap
@@ -3455,7 +3478,7 @@ func (m *SnapManager) undoKillSnapApps(t *state.Task, _ *tomb.Tomb) error {
return err
}
- if err := runinhibit.Unlock(snapsup.InstanceName()); err != nil {
+ if err := runinhibit.Unlock(snapsup.InstanceName(), st.Unlocker()); err != nil {
return err
}
@@ -3565,11 +3588,17 @@ func (m *SnapManager) undoUnlinkSnap(t *state.Task, _ *tomb.Tomb) error {
}
linkCtx := backend.LinkContext{
FirstInstall: false,
- IsUndo: true,
ServiceOptions: opts,
HasOtherInstances: otherInstances,
+ StateUnlocker: st.Unlocker(),
}
- reboot, err := m.backend.LinkSnap(info, deviceCtx, linkCtx, perfTimings)
+ err = m.backend.LinkSnap(info, deviceCtx, linkCtx, perfTimings)
+ if err != nil {
+ return err
+ }
+
+ isUndo := true
+ reboot, err := m.backend.MaybeSetNextBoot(info, deviceCtx, isUndo)
if err != nil {
return err
}
@@ -3720,7 +3749,7 @@ func (m *SnapManager) doDiscardSnap(t *state.Task, _ *tomb.Tomb) error {
t.Errorf("cannot discard snap namespace %q, will retry in 3 mins: %s", snapsup.InstanceName(), err)
return &state.Retry{After: 3 * time.Minute}
}
- err = m.backend.RemoveSnapInhibitLock(snapsup.InstanceName())
+ err = m.backend.RemoveSnapInhibitLock(snapsup.InstanceName(), st.Unlocker())
if err != nil {
return err
}
diff --git a/overlord/snapstate/handlers_components.go b/overlord/snapstate/handlers_components.go
index f956cff2749..3c7181a97f0 100644
--- a/overlord/snapstate/handlers_components.go
+++ b/overlord/snapstate/handlers_components.go
@@ -296,7 +296,7 @@ func (m *SnapManager) doMountComponent(t *state.Task, _ *tomb.Tomb) (err error)
var readInfoErr error
for i := 0; i < 10; i++ {
compMntDir := cpi.MountDir()
- _, readInfoErr = readComponentInfo(compMntDir, nil, csi)
+ _, readInfoErr = readComponentInfoAt(compMntDir, nil, csi)
if readInfoErr == nil {
logger.Debugf("component %q (%v) available at %q",
csi.Component, compSetup.Revision(), compMntDir)
@@ -342,8 +342,15 @@ func (m *SnapManager) doMountComponent(t *state.Task, _ *tomb.Tomb) (err error)
return nil
}
+// ReadComponentInfo reads the snap's component and returns a ComponentInfo.
+func ReadComponentInfo(snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
+ compName, compRev := csi.Component.ComponentName, csi.Revision
+ mountDir := snap.ComponentMountDir(compName, compRev, snapInfo.InstanceName())
+ return readComponentInfoAt(mountDir, snapInfo, csi)
+}
+
// Maybe we will need flags as in readInfo
-var readComponentInfo = func(compMntDir string, snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
+var readComponentInfoAt = func(compMntDir string, snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
cont := snapdir.New(compMntDir)
return snap.ReadComponentInfoFromContainer(cont, snapInfo, csi)
}
diff --git a/overlord/snapstate/handlers_link_test.go b/overlord/snapstate/handlers_link_test.go
index 4a1361de39f..63a37b46d46 100644
--- a/overlord/snapstate/handlers_link_test.go
+++ b/overlord/snapstate/handlers_link_test.go
@@ -714,6 +714,10 @@ func (s *linkSnapSuite) TestDoUndoUnlinkCurrentSnapWithVitalityScore(c *C) {
path: filepath.Join(dirs.SnapMountDir, "foo/11"),
vitalityRank: 2,
},
+ {
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
}
c.Check(s.fakeBackend.ops, DeepEquals, expected)
}
@@ -924,6 +928,9 @@ func (s *linkSnapSuite) TestDoLinkSnapWithVitalityScore(c *C) {
path: filepath.Join(dirs.SnapMountDir, "foo/33"),
vitalityRank: 2,
},
+ {
+ op: "maybe-set-next-boot",
+ },
}
c.Check(s.fakeBackend.ops, DeepEquals, expected)
}
@@ -1302,6 +1309,65 @@ func (s *linkSnapSuite) TestDoLinkSnapSuccessGadgetDoesRequestsRestart(c *C) {
c.Check(t.Log(), HasLen, 1)
}
+func (s *linkSnapSuite) TestDoLinkSnapFailGadgetDoesRequestsRestart(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ s.state.Lock()
+ si := &snap.SideInfo{
+ RealName: "pc",
+ SnapID: "pc-snap-id",
+ Revision: snap.R(1),
+ }
+ t := s.state.NewTask("link-snap", "test")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ Type: snap.TypeGadget,
+ })
+ chg := s.state.NewChange("sample", "...")
+ chg.AddTask(t)
+
+ // Force failure in a contrieved way by setting
+ // "gadget-restart-required" to a string (we want to make sure that we
+ // unlink on an error after setting next boot)
+ chg.Set("gadget-restart-required", "not-bool")
+
+ s.state.Unlock()
+
+ s.se.Ensure()
+ s.se.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ expected := fakeOps{
+ {
+ op: "candidate",
+ sinfo: *si,
+ },
+ {
+ op: "link-snap",
+ path: filepath.Join(dirs.SnapMountDir, "pc/1"),
+ },
+ {
+ op: "maybe-set-next-boot",
+ },
+ {
+ op: "unlink-snap",
+ path: filepath.Join(dirs.SnapMountDir, "pc/1"),
+
+ unlinkFirstInstallUndo: true,
+ },
+ }
+ c.Check(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Check(s.fakeBackend.ops, DeepEquals, expected)
+
+ // Error, no reboot has been requested
+ c.Check(t.Status(), Equals, state.ErrorStatus)
+ c.Check(s.restartRequested, HasLen, 0)
+ c.Check(t.Log(), HasLen, 2)
+}
+
func (s *linkSnapSuite) TestDoLinkSnapSuccessCoreAndSnapdNoCoreRestart(c *C) {
restore := release.MockOnClassic(true)
defer restore()
@@ -1536,6 +1602,9 @@ func (s *linkSnapSuite) TestDoLinkSnapdDiscardsNsOnDowngrade(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snapd/41"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
}
// start with an easier-to-read error if this fails:
@@ -1621,6 +1690,9 @@ func (s *linkSnapSuite) TestDoLinkSnapdRemovesAppArmorProfilesOnSnapdDowngrade(c
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snapd/41"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
}
// start with an easier-to-read error if this fails:
@@ -2272,6 +2344,9 @@ func (s *linkSnapSuite) TestUndoLinkSnapdFirstInstall(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snapd/22"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "discard-namespace",
name: "snapd",
@@ -2350,6 +2425,9 @@ func (s *linkSnapSuite) TestUndoLinkSnapdNthInstall(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snapd/22"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snapd/20"),
@@ -2602,6 +2680,9 @@ func (s *linkSnapSuite) testDoLinkSnapWithToolingDependency(c *C, classicOrBase
path: filepath.Join(dirs.SnapMountDir, "services-snap/11"),
requireSnapdTooling: needsTooling,
},
+ {
+ op: "maybe-set-next-boot",
+ },
}
// start with an easier-to-read error if this fails:
@@ -2683,7 +2764,7 @@ func (s *linkSnapSuite) testDoKillSnapApps(c *C, svc bool) {
c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
c.Check(s.fakeBackend.ops, DeepEquals, expected)
- hint, _, err := runinhibit.IsLocked("some-snap")
+ hint, _, err := runinhibit.IsLocked("some-snap", nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintInhibitedForRemove)
@@ -2737,7 +2818,7 @@ func (s *linkSnapSuite) TestDoKillSnapAppsUnlocksOnError(c *C) {
c.Assert(task.Status(), Equals, state.ErrorStatus)
- hint, _, err := runinhibit.IsLocked("some-snap")
+ hint, _, err := runinhibit.IsLocked("some-snap", nil)
c.Assert(err, IsNil)
// On error hint inhibition file is unlocked
c.Check(hint, Equals, runinhibit.HintNotInhibited)
@@ -2784,7 +2865,7 @@ func (s *linkSnapSuite) TestDoKillSnapAppsTerminateBestEffort(c *C) {
c.Assert(task.Status(), Equals, state.DoneStatus)
- hint, _, err := runinhibit.IsLocked("some-snap")
+ hint, _, err := runinhibit.IsLocked("some-snap", nil)
c.Assert(err, IsNil)
// Error is ignored, inhibition lock is held
c.Check(hint, Equals, runinhibit.HintInhibitedForRemove)
@@ -2863,7 +2944,7 @@ func (s *linkSnapSuite) testDoUndoKillSnapApps(c *C, svc bool) {
c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
c.Check(s.fakeBackend.ops, DeepEquals, expected)
- hint, _, err := runinhibit.IsLocked("some-snap")
+ hint, _, err := runinhibit.IsLocked("some-snap", nil)
c.Assert(err, IsNil)
// On undo hint inhibition file is unlocked
c.Check(hint, Equals, runinhibit.HintNotInhibited)
diff --git a/overlord/snapstate/refresh.go b/overlord/snapstate/refresh.go
index 86663ee1eeb..c722070419c 100644
--- a/overlord/snapstate/refresh.go
+++ b/overlord/snapstate/refresh.go
@@ -175,7 +175,7 @@ func (err BusySnapError) Pids() []int {
// the refresh change and continue running existing app processes.
func hardEnsureNothingRunningDuringRefresh(backend managerBackend, st *state.State, snapst *SnapState, snapsup *SnapSetup, info *snap.Info) (bool, *osutil.FileLock, error) {
var inhibitionTimeout bool
- lock, err := backend.RunInhibitSnapForUnlink(info, runinhibit.HintInhibitedForRefresh, func() error {
+ lock, err := backend.RunInhibitSnapForUnlink(info, runinhibit.HintInhibitedForRefresh, st.Unlocker(), func() error {
// In case of successful refresh inhibition the snap state is modified
// to indicate when the refresh was first inhibited. If the first
// refresh inhibition is outside of a grace period then refresh
diff --git a/overlord/snapstate/refresh_test.go b/overlord/snapstate/refresh_test.go
index 7fab8ce2bb4..8bddfd12afa 100644
--- a/overlord/snapstate/refresh_test.go
+++ b/overlord/snapstate/refresh_test.go
@@ -177,7 +177,7 @@ func (s *refreshSuite) TestDoSoftRefreshCheckAllowed(c *C) {
c.Assert(err, IsNil)
// In addition, the inhibition lock is not set.
- hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName())
+ hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{})
@@ -201,7 +201,7 @@ func (s *refreshSuite) TestDoSoftRefreshCheckDisallowed(c *C) {
c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks, pids: 123`)
// Validity check: the inhibition lock was not set.
- hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName())
+ hint, inhibitInfo, err := runinhibit.IsLocked(info.InstanceName(), nil)
c.Assert(err, IsNil)
c.Check(hint, Equals, runinhibit.HintNotInhibited)
c.Check(inhibitInfo, Equals, runinhibit.InhibitInfo{})
diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go
index 13efae7a2e4..dc96cecc2d7 100644
--- a/overlord/snapstate/snapmgr.go
+++ b/overlord/snapstate/snapmgr.go
@@ -665,10 +665,7 @@ func (snapst *SnapState) ComponentInfosForRevision(rev snap.Revision) ([]*snap.C
compInfos := make([]*snap.ComponentInfo, 0, len(revState.Components))
for _, comp := range revState.Components {
- cpi := snap.MinimalComponentContainerPlaceInfo(comp.SideInfo.Component.ComponentName,
- comp.SideInfo.Revision, si.InstanceName())
-
- compInfo, err := readComponentInfo(cpi.MountDir(), si, comp.SideInfo)
+ compInfo, err := ReadComponentInfo(si, comp.SideInfo)
if err != nil {
return nil, err
}
@@ -693,9 +690,7 @@ func (snapst *SnapState) CurrentComponentInfo(cref naming.ComponentRef) (*snap.C
return nil, err
}
- cpi := snap.MinimalComponentContainerPlaceInfo(csi.Component.ComponentName,
- csi.Revision, si.InstanceName())
- return readComponentInfo(cpi.MountDir(), si, csi)
+ return ReadComponentInfo(si, csi)
}
func (snapst *SnapState) InstanceName() string {
diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go
index d4fab1e5166..8ec4d555b97 100644
--- a/overlord/snapstate/snapstate.go
+++ b/overlord/snapstate/snapstate.go
@@ -1815,7 +1815,7 @@ func keys[K comparable, V any](m map[K]V) []K {
// If the filter returns true, the update for that snap proceeds. If
// it returns false, the snap is removed from the list of updates to
// consider.
-type updateFilter func(*snap.Info, *SnapState) bool
+type updateFilter = func(*snap.Info, *SnapState) bool
func updateManyFiltered(ctx context.Context, st *state.State, names []string, revOpts []*RevisionOptions, userID int, filter updateFilter, flags *Flags, fromChange string) ([]string, *UpdateTaskSets, error) {
if flags == nil {
diff --git a/overlord/snapstate/snapstate_install_test.go b/overlord/snapstate/snapstate_install_test.go
index c220b95cb71..ea67f17d277 100644
--- a/overlord/snapstate/snapstate_install_test.go
+++ b/overlord/snapstate/snapstate_install_test.go
@@ -1383,6 +1383,9 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -1583,6 +1586,9 @@ func (s *snapmgrTestSuite) testParallelInstanceInstallRunThrough(c *C, inputFlag
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap_instance",
@@ -1795,6 +1801,9 @@ func (s *snapmgrTestSuite) TestInstallUndoRunThroughJustOneSnap(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -1951,6 +1960,9 @@ func (s *snapmgrTestSuite) TestInstallWithCohortRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/666"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -2150,6 +2162,9 @@ func (s *snapmgrTestSuite) testInstallWithRevisionRunThrough(c *C, snapName, req
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, filepath.Join(snapName, "42")),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: snapName,
@@ -2350,6 +2365,9 @@ version: 1.0`)
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "mock/x1"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "mock",
@@ -2482,6 +2500,9 @@ epoch: 1*
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "mock/x3"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "mock",
@@ -2621,6 +2642,9 @@ epoch: 1*
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "mock/x1"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "mock",
@@ -2677,7 +2701,7 @@ version: 1.0`)
s.settle(c)
// ensure only local install was run, i.e. first actions are pseudo-action current
- c.Assert(s.fakeBackend.ops.Ops(), HasLen, 10)
+ c.Assert(s.fakeBackend.ops.Ops(), HasLen, 11)
c.Check(s.fakeBackend.ops[0].op, Equals, "current")
c.Check(s.fakeBackend.ops[0].old, Equals, "")
// and setup-snap
@@ -2690,6 +2714,7 @@ version: 1.0`)
c.Check(s.fakeBackend.ops[5].sinfo, DeepEquals, *si)
c.Check(s.fakeBackend.ops[6].op, Equals, "link-snap")
c.Check(s.fakeBackend.ops[6].path, Equals, filepath.Join(dirs.SnapMountDir, "some-snap/42"))
+ c.Check(s.fakeBackend.ops[7].op, Equals, "maybe-set-next-boot")
// verify snapSetup info
var snapsup snapstate.SnapSetup
@@ -2864,6 +2889,9 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "core/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "core",
@@ -2929,6 +2957,9 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/42"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -3323,6 +3354,8 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
}, {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snap-content-slot/11"),
+ }, {
+ op: "maybe-set-next-boot",
}, {
op: "auto-connect:Doing",
name: "snap-content-slot",
@@ -3375,6 +3408,8 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
}, {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snap-content-plug/42"),
+ }, {
+ op: "maybe-set-next-boot",
}, {
op: "auto-connect:Doing",
name: "snap-content-plug",
@@ -6436,10 +6471,16 @@ func undoOps(instanceName string, newSequence, prevSequence *sequence.RevisionSi
path: filepath.Join(dirs.SnapDataDir, instanceName),
})
} else {
- ops = append(ops, fakeOp{
- op: "link-snap",
- path: filepath.Join(dirs.SnapMountDir, instanceName, prevRevision.String()),
- })
+ ops = append(ops,
+ fakeOp{
+ op: "link-snap",
+ path: filepath.Join(dirs.SnapMountDir, instanceName, prevRevision.String()),
+ },
+ fakeOp{
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
+ )
}
for i := len(newComponents) - 1; i >= 0; i-- {
@@ -6702,6 +6743,8 @@ func (s *snapmgrTestSuite) testInstallComponentsRunThrough(c *C, snapName, insta
}, {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, filepath.Join(instanceName, snapRevision.String())),
+ }, {
+ op: "maybe-set-next-boot",
}}...)
// ops for linking components
@@ -7030,6 +7073,8 @@ components:
}, {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, filepath.Join(instanceName, snapRevision.String())),
+ }, {
+ op: "maybe-set-next-boot",
}}...)
for _, compName := range compNames {
diff --git a/overlord/snapstate/snapstate_remove_test.go b/overlord/snapstate/snapstate_remove_test.go
index 746805cfc59..1b9e0bccd41 100644
--- a/overlord/snapstate/snapstate_remove_test.go
+++ b/overlord/snapstate/snapstate_remove_test.go
@@ -1571,6 +1571,10 @@ func (s *snapmgrTestSuite) TestRemoveManyUndoRestoresCurrent(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/1"),
},
+ {
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
{
op: "update-aliases",
},
diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go
index 5a6cbd87bd1..f4444e912a1 100644
--- a/overlord/snapstate/snapstate_test.go
+++ b/overlord/snapstate/snapstate_test.go
@@ -1639,6 +1639,9 @@ func (s *snapmgrTestSuite) testRevertRunThrough(c *C, refreshAppAwarenessUX bool
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/2"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -1871,6 +1874,9 @@ func (s *snapmgrTestSuite) TestRevertWithBaseRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap-with-base/2"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap-with-base",
@@ -1961,6 +1967,9 @@ func (s *snapmgrTestSuite) TestParallelInstanceRevertRunThrough(c *C) {
path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/2"),
otherInstances: true,
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap_instance",
@@ -2030,7 +2039,7 @@ func (s *snapmgrTestSuite) TestRevertWithLocalRevisionRunThrough(c *C) {
s.settle(c)
- c.Assert(s.fakeBackend.ops.Ops(), HasLen, 8)
+ c.Assert(s.fakeBackend.ops.Ops(), HasLen, 9)
// verify that LocalRevision is still -7
var snapst snapstate.SnapState
@@ -2098,6 +2107,9 @@ func (s *snapmgrTestSuite) TestRevertToRevisionNewVersion(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -2189,6 +2201,9 @@ func (s *snapmgrTestSuite) TestRevertTotalUndoRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/1"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -2220,6 +2235,10 @@ func (s *snapmgrTestSuite) TestRevertTotalUndoRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/2"),
},
+ {
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
{
op: "update-aliases",
},
@@ -2311,6 +2330,10 @@ func (s *snapmgrTestSuite) TestRevertUndoRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/2"),
},
+ {
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
{
op: "update-aliases",
},
@@ -2996,6 +3019,9 @@ func (s *snapmgrTestSuite) TestEnableRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -3158,6 +3184,9 @@ func (s *snapmgrTestSuite) TestParallelInstanceEnableRunThrough(c *C) {
path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"),
otherInstances: true,
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap_instance",
@@ -5629,6 +5658,9 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "core/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "core",
@@ -8133,6 +8165,9 @@ func (s *snapmgrTestSuite) TestSnapdRefreshTasks(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "snapd/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "snapd",
diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go
index 1d8df6e0891..0605579c72a 100644
--- a/overlord/snapstate/snapstate_update_test.go
+++ b/overlord/snapstate/snapstate_update_test.go
@@ -129,6 +129,9 @@ func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -270,6 +273,7 @@ func (s *snapmgrTestSuite) testUpdateScenario(c *C, desc string, t switchScenari
"setup-profiles:Doing",
"candidate",
"link-snap",
+ "maybe-set-next-boot",
"auto-connect:Doing",
"update-aliases",
"cleanup-trash",
@@ -415,6 +419,9 @@ func (s *snapmgrTestSuite) testUpdateCanDoBackwards(c *C, refreshAppAwarenessUX
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -932,6 +939,7 @@ func (s *snapmgrTestSuite) testUpdateAmendRunThrough(c *C, tryMode bool, compone
"setup-profiles:Doing",
"candidate",
"link-snap",
+ "maybe-set-next-boot",
}...)
for range components {
ops = append(ops, "link-component")
@@ -1190,6 +1198,9 @@ func (s *snapmgrTestSuite) testUpdateRunThrough(c *C, refreshAppAwarenessUX bool
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "services-snap/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "services-snap",
@@ -1565,6 +1576,9 @@ func (s *snapmgrTestSuite) testParallelInstanceUpdateRunThrough(c *C, refreshApp
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "services-snap_instance/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "services-snap_instance",
@@ -2501,6 +2515,10 @@ func (s *snapmgrTestSuite) testUpdateUndoRunThrough(c *C, refreshAppAwarenessUX
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
},
+ {
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
}...)
// aliases removal undo is skipped when refresh-app-awareness-ux is enabled
if !refreshAppAwarenessUX {
@@ -2816,6 +2834,9 @@ func (s *snapmgrTestSuite) testUpdateTotalUndoRunThrough(c *C, refreshAppAwarene
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: "some-snap",
@@ -2866,6 +2887,10 @@ func (s *snapmgrTestSuite) testUpdateTotalUndoRunThrough(c *C, refreshAppAwarene
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
},
+ {
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
}...)
// aliases removal undo is skipped when refresh-app-awareness-ux is enabled
if !refreshAppAwarenessUX {
@@ -14382,6 +14407,9 @@ func (s *snapmgrTestSuite) TestUpdateBackToPrevRevision(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: instanceName,
@@ -14603,6 +14631,9 @@ func (s *snapmgrTestSuite) testRevertWithComponents(c *C, undo bool) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "auto-connect:Doing",
name: instanceName,
@@ -14647,6 +14678,10 @@ func (s *snapmgrTestSuite) testRevertWithComponents(c *C, undo bool) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ isUndo: true,
+ },
{
op: "update-aliases",
},
@@ -14999,6 +15034,9 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevision(c *C) {
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ },
}...)
for i, compName := range components {
@@ -15339,6 +15377,9 @@ func (s *snapmgrTestSuite) TestUpdateWithComponentsBackToPrevRevisionAddComponen
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, prevSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ },
{
op: "link-component",
path: snap.ComponentMountDir("kernel-modules-component", snap.R(2), instanceName),
@@ -15886,6 +15927,9 @@ func (s *snapmgrTestSuite) testUpdateWithComponentsRunThrough(c *C, opts updateW
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ },
}...)
for _, cs := range expectedComponentStates {
@@ -16531,6 +16575,9 @@ components:
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, newSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ },
}...)
for _, cs := range expectedComponentStates {
@@ -16977,6 +17024,9 @@ func (s *snapmgrTestSuite) testUpdateWithComponentsRunThroughOnlyComponentUpdate
op: "link-snap",
path: filepath.Join(dirs.SnapMountDir, instanceName, currentSnapRev.String()),
},
+ {
+ op: "maybe-set-next-boot",
+ },
}...)
for _, cs := range expectedComponentStates {
diff --git a/packaging/fedora-39 b/packaging/fedora-41
similarity index 100%
rename from packaging/fedora-39
rename to packaging/fedora-41
diff --git a/randutil/rand.go b/randutil/rand.go
index 1757afd571d..73d39abbe2b 100644
--- a/randutil/rand.go
+++ b/randutil/rand.go
@@ -83,6 +83,7 @@ func RandomString(length int) string {
var (
Intn = rand.Intn
Int63n = rand.Int63n
+ Perm = rand.Perm
)
// RandomDuration returns a random duration up to the given length.
diff --git a/spread.yaml b/spread.yaml
index 4741d84d588..5b75d1a5bff 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -245,6 +245,7 @@ backends:
workers: 6
- opensuse-tumbleweed-64:
workers: 6
+ storage: 12G
google-arm:
type: google
diff --git a/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh b/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh
index 73ab37f07be..0f4a8ce3a72 100755
--- a/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh
+++ b/tests/core/auto-refresh-backoff-after-reboot/check_auto_refresh_count.sh
@@ -4,4 +4,4 @@ LAST_CHANGE_ID=$1
CHANGES_COUNT=$2
#shellcheck disable=SC2086,SC2046
-test $(snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT
+test $(snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT
diff --git a/tests/core/auto-refresh-backoff-after-reboot/task.yaml b/tests/core/auto-refresh-backoff-after-reboot/task.yaml
index a280de50e3a..ce01dc863b8 100644
--- a/tests/core/auto-refresh-backoff-after-reboot/task.yaml
+++ b/tests/core/auto-refresh-backoff-after-reboot/task.yaml
@@ -21,15 +21,11 @@ prepare: |
exit
fi
- # Needed by make-snap-installable, Install before switching to fakestore
- snap install jq
- snap install remarshal
-
# Prevent refreshes until we have right snap revisions
snap set system refresh.hold=forever
# Record last change id before we start to avoid flakiness due to auto-refreshes in other tests
- snap debug api /v2/changes?select=all | jq '.result | sort_by(.id|tonumber) | .[-1].id' > last-change-id
+ snap debug api /v2/changes?select=all | gojq '.result | sort_by(.id|tonumber) | .[-1].id' > last-change-id
# Record current snap revision for reference
readlink "/snap/$SNAP_NAME/current" > snap.rev
@@ -58,7 +54,7 @@ restore: |
snap set system refresh.hold!
debug: |
- snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)"
+ snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)"
execute: |
if [ "$TRUST_TEST_KEYS" = "false" ]; then
@@ -169,17 +165,18 @@ execute: |
retry -n 50 --wait 1 "$(pwd)"/check_auto_refresh_count.sh "$LAST_CHANGE_ID" 2
echo "Check auto-refresh behaviour matches expectations for backoff algorithm"
- snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json
+ snap debug api /v2/changes?select=ready | \
+ gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json
# 1st auto-refresh
- jq '.[0].status' < changes.json | MATCH "Error"
- jq '.[0].data."snap-names" | length' < changes.json | MATCH "1"
- jq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_NAME"
- jq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_NAME"
+ gojq '.[0].status' < changes.json | MATCH "Error"
+ gojq '.[0].data."snap-names" | length' < changes.json | MATCH "1"
+ gojq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_NAME"
+ gojq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_NAME"
# 2nd auto-refresh
- jq '.[1].status' < changes.json | MATCH "Done"
- jq '.[1].data."snap-names" | length' < changes.json | MATCH "1"
- jq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_NAME"
- jq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_NAME"
+ gojq '.[1].status' < changes.json | MATCH "Done"
+ gojq '.[1].data."snap-names" | length' < changes.json | MATCH "1"
+ gojq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_NAME"
+ gojq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_NAME"
fi
diff --git a/tests/core/basic20plus/task.yaml b/tests/core/basic20plus/task.yaml
index 51225b939ff..38f67d792d9 100644
--- a/tests/core/basic20plus/task.yaml
+++ b/tests/core/basic20plus/task.yaml
@@ -8,17 +8,23 @@ details: |
properly
systems:
- - ubuntu-core-20-*
- - ubuntu-core-22-*
+ - -ubuntu-core-16-*
+ - -ubuntu-core-18-*
execute: |
case "$SPREAD_SYSTEM" in
+ ubuntu-core-24-*)
+ base_snap=core24
+ ;;
ubuntu-core-22-*)
base_snap=core22
;;
ubuntu-core-20-*)
base_snap=core20
;;
+ *)
+ echo "Unsupported ubuntu core system, add missing case here"
+ exit 1
esac
echo "Check that the system snaps are there"
snap list "${base_snap}"
@@ -32,12 +38,9 @@ execute: |
snap changes | MATCH "Done.*Initialize system state"
echo "Check that a simple shell snap"
- if os.query is-core22; then
- snap install --edge "test-snapd-sh-${base_snap}"
- else
- snap install "test-snapd-sh-${base_snap}"
- fi
- "test-snapd-sh-${base_snap}.sh" -c 'echo hello' | MATCH hello
+ SHELL_SNAP="test-snapd-sh-${base_snap}"
+ snap install "$SHELL_SNAP"
+ "${SHELL_SNAP}.sh" -c 'echo hello' | MATCH hello
if python3 -m json.tool < /var/lib/snapd/system-key | grep '"build-id": ""'; then
echo "The build-id of snapd must not be empty."
@@ -45,10 +48,10 @@ execute: |
fi
echo "Ensure passwd/group is available for snaps"
- "test-snapd-sh-${base_snap}.sh" -c 'cat /var/lib/extrausers/passwd' | MATCH test
+ "${SHELL_SNAP}.sh" -c 'cat /var/lib/extrausers/passwd' | MATCH test
# rpi devices don't use grub
- if ( os.query is-core20 || os.query is-core22 ) && not snap list pi-kernel &>/dev/null; then
+ if not snap list pi-kernel &>/dev/null; then
echo "Ensure extracted kernel.efi exists"
kernel_name="$(snaps.name kernel)"
test -e /boot/grub/"$kernel_name"*/kernel.efi
@@ -70,9 +73,13 @@ execute: |
# ensure that our the-tool (and thus our snap-bootstrap ran)
# for external backend the initramfs is not rebuilt
- echo "Check that we booted with the rebuilt initramfs in the kernel snap"
- if [ "$SPREAD_BACKEND" != "external" ] && [ "$SPREAD_BACKEND" != "testflinger" ]; then
- test -e /writable/system-data/the-tool-ran
+ # On core24+ we repack the kernel snap differently, and thus do not
+ # touch the /writable/system-data/the-tool-ran
+ if os.query is-core20 || os.query is-core22; then
+ echo "Check that we booted with the rebuilt initramfs in the kernel snap"
+ if [ "$SPREAD_BACKEND" != "external" ] && [ "$SPREAD_BACKEND" != "testflinger" ]; then
+ test -e /writable/system-data/the-tool-ran
+ fi
fi
# ensure we handled cloud-init, either we have:
@@ -117,6 +124,15 @@ execute: |
losetup -O ro -n --raw "${loop}" | MATCH "1"
done
+ # make sure that ubuntu-save is mounted with appropriate flags
+ findmnt -T /run/mnt/ubuntu-save > save-mount.info
+
+ # print it for debug purposes before we match flags
+ cat save-mount.info
+ MATCH nosuid < save-mount.info
+ MATCH noexec < save-mount.info
+ MATCH nodev < save-mount.info
+
# ensure apparmor works, see LP: 2024637
systemctl status apparmor.service
diff --git a/tests/core/kernel-snap-refresh-on-core/task.yaml b/tests/core/kernel-snap-refresh-on-core/task.yaml
index 95e889bbab4..d55a1382ce7 100644
--- a/tests/core/kernel-snap-refresh-on-core/task.yaml
+++ b/tests/core/kernel-snap-refresh-on-core/task.yaml
@@ -5,8 +5,7 @@ details: |
revision to a new one. It expects to find a new snap revision in the
channel pointed by the NEW_CORE_CHANNEL env var.
-# TODO:UC20 enable for UC20/UC18
-systems: [ubuntu-core-16-64]
+systems: [ubuntu-core-*-64]
manual: true
diff --git a/tests/core/snap-repair/task.yaml b/tests/core/snap-repair/task.yaml
index b3330741f12..5f768dc35fa 100644
--- a/tests/core/snap-repair/task.yaml
+++ b/tests/core/snap-repair/task.yaml
@@ -8,10 +8,6 @@ environment:
BLOB_DIR: $(pwd)/fake-store-blobdir
STORE_ADDR: localhost:11028
-prepare: |
- snap install jq
- tests.cleanup defer snap remove jq
-
restore: |
if [ "$TRUST_TEST_KEYS" = "false" ]; then
echo "This test needs test keys to be trusted"
@@ -93,7 +89,7 @@ execute: |
# note that the value "2" here _must_ be a string, otherwise we can't
# sign it as all values must be strings or lists of strings, etc.
- jq '."repair-id" = "2"' < "$PWD/$REPAIR_JSON" > "$PWD/$REPAIR_JSON.tmp"
+ gojq '."repair-id" = "2"' < "$PWD/$REPAIR_JSON" > "$PWD/$REPAIR_JSON.tmp"
mv "$PWD/$REPAIR_JSON.tmp" "$PWD/2-$REPAIR_JSON"
fakestore new-repair --dir "$BLOB_DIR" retry.sh --repair-json="$PWD/2-$REPAIR_JSON"
@@ -108,7 +104,7 @@ execute: |
echo "Add a new repair ID 2 revision that completes successfully"
- jq '."revision" = "1"' < "$PWD/2-$REPAIR_JSON" > "$PWD/2-$REPAIR_JSON.tmp"
+ gojq '."revision" = "1"' < "$PWD/2-$REPAIR_JSON" > "$PWD/2-$REPAIR_JSON.tmp"
mv "$PWD/2-$REPAIR_JSON.tmp" "$PWD/2-$REPAIR_JSON"
fakestore new-repair --dir "$BLOB_DIR" "$REPAIR_SCRIPT" --repair-json="$PWD/2-$REPAIR_JSON"
diff --git a/tests/core/snapd-maintenance-msg/task.yaml b/tests/core/snapd-maintenance-msg/task.yaml
index d882cd3f627..18dba16cd93 100644
--- a/tests/core/snapd-maintenance-msg/task.yaml
+++ b/tests/core/snapd-maintenance-msg/task.yaml
@@ -7,16 +7,12 @@ details: |
systems: [ubuntu-core-20-64]
prepare: |
- snap install jq
-
# make sure that the snapd daemon gives us time for comms before
# closing the socket
echo "SNAPD_SHUTDOWN_DELAY=1" >> /etc/environment
systemctl restart snapd
restore: |
- snap remove jq
-
# remove SNAPD_SHUTDOWN_DELAY from /etc/environment again
#shellcheck disable=SC2005
echo "$(grep -v 'SNAPD_SHUTDOWN_DELAY=1' /etc/environment)" > /etc/environment
@@ -31,7 +27,7 @@ execute: |
# closed so we need to catch it in that timeframe.
echo "Testing maintenance message for daemon restarts"
snap install --dangerous "$SNAPD_SNAP" &
- retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | jq ".maintenance" | MATCH "daemon is restarting"'
+ retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | gojq ".maintenance" | MATCH "daemon is restarting"'
wait
echo "Restoring the snapd snap"
@@ -39,7 +35,7 @@ execute: |
echo "Testing maintenance message for system reboots"
snap refresh core20 --channel=stable --amend &
- retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | jq ".maintenance" | MATCH "system is restarting"'
+ retry -n 20 --wait 0.5 sh -c 'snap debug api '/v2/changes?select=all' | gojq ".maintenance" | MATCH "system is restarting"'
wait
REBOOT
diff --git a/tests/core/snapd-refresh-vs-services/task.yaml b/tests/core/snapd-refresh-vs-services/task.yaml
index e81b35b280c..a1b16a8a613 100644
--- a/tests/core/snapd-refresh-vs-services/task.yaml
+++ b/tests/core/snapd-refresh-vs-services/task.yaml
@@ -42,11 +42,8 @@ environment:
SNAPD_2_49_2_ARMHF: https://storage.googleapis.com/snapd-spread-tests/snaps/snapd_2.49.2_11586.snap
prepare: |
- # install http snap to download files, jq + remarshal to simplify the check if
- # stable == 2.49.2 so we can skip that case automatically until a new version
- # is released to stable
+ # install http snap to download files
snap install test-snapd-curl --edge --devmode # devmode so it can save to any dir
- snap install jq remarshal
# save the current version of snapd for later
INITIAL_REV=$(snap list snapd | tail -n +2 | awk '{print $3}')
cp "/var/lib/snapd/snaps/snapd_$INITIAL_REV.snap" snapd-pr.snap
@@ -62,7 +59,7 @@ prepare: |
execute: |
# check if snapd 2.49.2 is the current latest/stable release as it simplifies
# some of the logic below
- if snap info snapd | yaml2json | jq -r '.channels."latest/stable"' | grep -q -Po '2.49.2\s+'; then
+ if snap info snapd | gojq --yaml-input -r '.channels."latest/stable"' | grep -q -Po '2.49.2\s+'; then
# skip the stable variant of the test
if [ "${SNAPD_VERSION_UNDER_TEST}" = "stable" ]; then
echo "Skipping duplicated test case"
diff --git a/tests/core/remodel-kernel/task.yaml b/tests/core/uc16-remodel-kernel/task.yaml
similarity index 98%
rename from tests/core/remodel-kernel/task.yaml
rename to tests/core/uc16-remodel-kernel/task.yaml
index f34e72d70fd..e8c4f6c519b 100644
--- a/tests/core/remodel-kernel/task.yaml
+++ b/tests/core/uc16-remodel-kernel/task.yaml
@@ -7,8 +7,6 @@ details: |
the new kernel snap. Finally check it is possible to remodel
back to the initial model.
-# FIXME: add core18 test as well
-# TODO:UC20: enable for UC20
systems: [ubuntu-core-16-64]
environment:
diff --git a/tests/core/uc20-recovery/task.yaml b/tests/core/uc20-recovery/task.yaml
index 411fd9f1a82..87bbe5200e4 100644
--- a/tests/core/uc20-recovery/task.yaml
+++ b/tests/core/uc20-recovery/task.yaml
@@ -34,22 +34,20 @@ execute: |
if [ "$SPREAD_REBOOT" == "0" ]; then
echo "In run mode"
- snap install --edge jq
-
MATCH 'snapd_recovery_mode=run' < /proc/cmdline
# verify we are in run mode via the API
snap debug api '/v2/system-info' > system-info
- jq -r '.result["system-mode"]' < system-info | MATCH 'run'
+ gojq -r '.result["system-mode"]' < system-info | MATCH 'run'
echo "Obtain available systems"
snap debug api '/v2/systems' > systems.json
# TODO:UC20: there is only one system for now
- jq .result.systems[0].current < systems.json | MATCH 'true'
- label="$(jq -r .result.systems[0].label < systems.json)"
+ gojq .result.systems[0].current < systems.json | MATCH 'true'
+ label="$(gojq -r .result.systems[0].label < systems.json)"
test -n "$label"
# make sure that the seed exists
test -d "/var/lib/snapd/seed/systems/$label"
- jq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install recover run'
+ gojq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install recover run'
# keep a copy of the systems dump for later reference
cp systems.json /writable/systems.json.run
@@ -66,7 +64,7 @@ execute: |
test -e /host/ubuntu-data/systems.json.run
snap debug api '/v2/systems' > systems.json
- jq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install run'
+ gojq -r .result.systems[0].actions[].mode < systems.json | sort | tr '\n' ' ' | MATCH 'install run'
label="$(cat /host/ubuntu-data/systems.label)"
test -n "$label"
@@ -79,11 +77,11 @@ execute: |
elif [ "$SPREAD_REBOOT" == "2" ]; then
echo "In run mode again"
snap debug api '/v2/system-info' > system-info
- jq -r '.result["system-mode"]' < system-info | MATCH 'run'
+ gojq -r '.result["system-mode"]' < system-info | MATCH 'run'
# now go back to recover mode so we can test that a simple reboot
# works to transition us back to run mode
- label="$(jq -r .result.systems[0].label < systems.json)"
+ label="$(gojq -r .result.systems[0].label < systems.json)"
transition_to_recover_mode "$label"
elif [ "$SPREAD_REBOOT" == "3" ]; then
echo "In recover mode again"
@@ -107,5 +105,5 @@ execute: |
elif [ "$SPREAD_REBOOT" == "4" ]; then
echo "In run mode again again"
snap debug api '/v2/system-info' > system-info
- jq -r '.result["system-mode"]' < system-info | MATCH 'run'
+ gojq -r '.result["system-mode"]' < system-info | MATCH 'run'
fi
diff --git a/tests/lib/external/snapd-testing-tools/spread.yaml b/tests/lib/external/snapd-testing-tools/spread.yaml
index 9282cc06ed4..1d966071faf 100644
--- a/tests/lib/external/snapd-testing-tools/spread.yaml
+++ b/tests/lib/external/snapd-testing-tools/spread.yaml
@@ -20,7 +20,6 @@ backends:
- debian-11-64:
- debian-12-64:
- debian-sid-64:
- - fedora-39-64:
- arch-linux-64:
- amazon-linux-2-64:
storage: preserve-size
diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh
index bf0c0ab14f6..dcd810e00f9 100755
--- a/tests/lib/nested.sh
+++ b/tests/lib/nested.sh
@@ -299,7 +299,7 @@ nested_get_snap_rev_for_channel() {
-H "Content-Type: application/json" \
--data "{\"context\": [], \"actions\": [{\"action\": \"install\", \"name\": \"$SNAP\", \"channel\": \"$CHANNEL\", \"instance-key\": \"1\"}]}" \
https://api.snapcraft.io/v2/snaps/refresh | \
- jq '.results[0].snap.revision'
+ gojq '.results[0].snap.revision'
}
nested_is_nested_system() {
diff --git a/tests/lib/pkgdb.sh b/tests/lib/pkgdb.sh
index 9bad51a6723..ecb46753415 100755
--- a/tests/lib/pkgdb.sh
+++ b/tests/lib/pkgdb.sh
@@ -589,7 +589,6 @@ pkg_dependencies_ubuntu_classic(){
echo "
dbus-user-session
gccgo-8
- gperf
evolution-data-server
fwupd
packagekit
@@ -617,6 +616,7 @@ pkg_dependencies_ubuntu_classic(){
dbus-user-session
fwupd
golang
+ gperf
libvirt-daemon-system
linux-tools-$(uname -r)
lz4
diff --git a/tests/lib/prepare-restore.sh b/tests/lib/prepare-restore.sh
index c9331bd4336..4b187aca9bb 100755
--- a/tests/lib/prepare-restore.sh
+++ b/tests/lib/prepare-restore.sh
@@ -608,6 +608,15 @@ prepare_project() {
disable_journald_rate_limiting
disable_journald_start_limiting
fi
+
+ # native jq replacement, but still with some incompatibilies, see
+ # https://github.com/itchyny/gojq
+ # major differences:
+ # - map keys are sorted by default
+ # - with --yaml-input, can parse YAML
+ GOBIN=$PROJECT_PATH/tests/bin \
+ CGO_ENABLED=0 \
+ go install github.com/itchyny/gojq/cmd/gojq@v0.12.16
}
prepare_project_each() {
@@ -772,6 +781,16 @@ restore_suite_each() {
"$TESTSLIB"/reset.sh --reuse-core
fi
+ # The ntp service randomly fails to create a socket on virbr0-nic,
+ # generating issues in actions like the auto-refresh (in Xenial).
+ # The errror lines are:
+ # ntpd: bind(23) AF_INET6 ... flags 0x11 failed: Cannot assign requested address
+ # ntpd: unable to create socket on virbr0-nic
+ # ntpd: kernel reports TIME_ERROR: 0x41: Clock Unsynchronized
+ if os.query is-xenial && systemctl status ntp | MATCH TIME_ERROR; then
+ systemctl restart ntp
+ fi
+
# Check for invariants late, in order to detect any bugs in the code above.
if [[ "$variant" = full ]]; then
"$TESTSTOOLS"/cleanup-state pre-invariant
diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh
index 799fb8f2c1f..3177c649247 100755
--- a/tests/lib/prepare.sh
+++ b/tests/lib/prepare.sh
@@ -97,9 +97,6 @@ ensure_jq() {
}
disable_refreshes() {
- echo "Ensure jq is available"
- ensure_jq
-
echo "Modify state to make it look like the last refresh just happened"
systemctl stop snapd.socket snapd.service
"$TESTSTOOLS"/snapd-state prevent-autorefresh
@@ -108,13 +105,6 @@ disable_refreshes() {
echo "Minimize risk of hitting refresh schedule"
snap set core refresh.schedule=00:00-23:59
snap refresh --time --abs-time | MATCH "last: 2[0-9]{3}"
-
- echo "Ensure jq is gone"
- snap remove --purge jq
- snap remove --purge jq-core18
- snap remove --purge jq-core20
- snap remove --purge jq-core22
- snap remove --purge test-snapd-jq-core24
}
setup_systemd_snapd_overrides() {
@@ -613,9 +603,10 @@ build_snapd_snap_with_run_mode_firstboot_tweaks() {
mv "${PROJECT_PATH}/snapd_from_snapcraft.snap" "/tmp/snapd_from_snapcraft.snap"
fi
- local UNPACK_DIR="/tmp/snapd-unpack"
- rm -rf "${UNPACK_DIR}"
- unsquashfs -no-progress -d "$UNPACK_DIR" /tmp/snapd_from_snapcraft.snap
+ # TODO set up a trap to clean this up properly?
+ local UNPACK_DIR
+ UNPACK_DIR="$(mktemp -d /tmp/snapd-unpack.XXXXXXXX)"
+ unsquashfs -no-progress -f -d "$UNPACK_DIR" /tmp/snapd_from_snapcraft.snap
# now install a unit that sets up enough so that we can connect
cat > "$UNPACK_DIR"/lib/systemd/system/snapd.spread-tests-run-mode-tweaks.service <<'EOF'
@@ -697,8 +688,10 @@ repack_core_snap_with_tweaks() {
local CORESNAP="$1"
local TARGET="$2"
- local UNPACK_DIR="/tmp/core-unpack"
- unsquashfs -no-progress -d "$UNPACK_DIR" "$CORESNAP"
+ local UNPACK_DIR
+ # TODO set up a trap to clean this up properly?
+ UNPACK_DIR="$(mktemp -d /tmp/core-unpack.XXXXXXXX)"
+ unsquashfs -no-progress -f -d "$UNPACK_DIR" "$CORESNAP"
mkdir -p "$UNPACK_DIR"/etc/systemd/journald.conf.d
cat < "$UNPACK_DIR"/etc/systemd/journald.conf.d/to-console.conf
@@ -736,9 +729,10 @@ repack_kernel_snap() {
fi
echo "Repacking kernel snap"
- UNPACK_DIR=/tmp/kernel-unpack
+ # TODO set up a trap to clean this up properly?
+ UNPACK_DIR="$(mktemp -d /tmp/kernel-unpack.XXXXXXXX)"
snap download --basename=pc-kernel --channel="$CHANNEL/${KERNEL_CHANNEL}" pc-kernel
- unsquashfs -no-progress -d "$UNPACK_DIR" pc-kernel.snap
+ unsquashfs -no-progress -f -d "$UNPACK_DIR" pc-kernel.snap
snap pack --filename="$TARGET" "$UNPACK_DIR"
rm -rf pc-kernel.snap "$UNPACK_DIR"
@@ -975,7 +969,7 @@ uc24_build_initramfs_kernel_snap() {
chmod +x ./initrd/main/usr/lib/snapd/snap-bootstrap
if [ "$injectKernelPanic" = "true" ]; then
# add a kernel panic to the end of the-tool execution
- echo "echo 'forcibly panicking'; echo c > /proc/sysrq-trigger" >> ./initrd/main/usr/lib/snapd/snap-bootstrap
+ echo "echo 'forcibly panicking'; echo c > /proc/sysrq-trigger" > ./initrd/main/usr/lib/snapd/snap-bootstrap
fi
(cd ./initrd/early; find . | cpio --create --quiet --format=newc --owner=0:0) >initrd.img
@@ -1170,8 +1164,10 @@ setup_reflash_magic() {
snap model --verbose
# remove the above debug lines once the mentioned bug is fixed
snap install "--channel=${CORE_CHANNEL}" "$core_name"
- UNPACK_DIR="/tmp/$core_name-snap"
- unsquashfs -no-progress -d "$UNPACK_DIR" /var/lib/snapd/snaps/${core_name}_*.snap
+ # TODO set up a trap to clean this up properly?
+ local UNPACK_DIR
+ UNPACK_DIR="$(mktemp -d "/tmp/$core_name-unpack.XXXXXXXX")"
+ unsquashfs -no-progress -f -d "$UNPACK_DIR" /var/lib/snapd/snaps/${core_name}_*.snap
if os.query is-arm; then
snap install ubuntu-image --channel="$UBUNTU_IMAGE_SNAP_CHANNEL" --classic
@@ -1242,7 +1238,7 @@ EOF
# Make /var/lib/systemd writable so that we can get linger enabled.
# This only applies to Ubuntu Core 16 where individual directories were
# writable. In Core 18 and beyond all of /var/lib/systemd is writable.
- mkdir -p $UNPACK_DIR/var/lib/systemd/{catalog,coredump,deb-systemd-helper-enabled,rfkill,linger}
+ mkdir -p "$UNPACK_DIR"/var/lib/systemd/{catalog,coredump,deb-systemd-helper-enabled,rfkill,linger}
touch "$UNPACK_DIR"/var/lib/systemd/random-seed
# build new core snap for the image
diff --git a/tests/lib/snaps/store/test-snapd-mount-control-nfs/bin/cmd b/tests/lib/snaps/store/test-snapd-mount-control-nfs/bin/cmd
new file mode 100755
index 00000000000..b49a7080b24
--- /dev/null
+++ b/tests/lib/snaps/store/test-snapd-mount-control-nfs/bin/cmd
@@ -0,0 +1,4 @@
+#!/bin/sh
+PS1='$ '
+
+exec "$@"
diff --git a/tests/lib/snaps/store/test-snapd-mount-control-nfs/snapcraft.yaml b/tests/lib/snaps/store/test-snapd-mount-control-nfs/snapcraft.yaml
new file mode 100644
index 00000000000..3eea84c6242
--- /dev/null
+++ b/tests/lib/snaps/store/test-snapd-mount-control-nfs/snapcraft.yaml
@@ -0,0 +1,34 @@
+name: test-snapd-mount-control-nfs
+summary: Snap for testing mount-control with NFS
+description: Snap for testing mount-control with NFS
+version: "1.0"
+base: core22
+confinement: strict
+
+apps:
+ cmd:
+ command: bin/cmd
+ plugs:
+ - mntctl
+ - network
+ - removable-media
+
+plugs:
+ mntctl:
+ interface: mount-control
+ mount:
+ - type: [nfs]
+ where: /media/**
+ options: [rw]
+
+parts:
+ apps:
+ plugin: dump
+ source: .
+
+ network-shares:
+ plugin: nil
+ stage-packages:
+ - nfs-common
+ stage:
+ - -lib/systemd/system/nfs-common.service
diff --git a/tests/lib/spread/backend.openstack.yaml b/tests/lib/spread/backend.openstack.yaml
index 45fb4f522fa..9f6b60667ec 100644
--- a/tests/lib/spread/backend.openstack.yaml
+++ b/tests/lib/spread/backend.openstack.yaml
@@ -1,6 +1,6 @@
openstack:
key: '$(HOST: echo "$SPREAD_OPENSTACK_ENV")'
- plan: staging-cpu2-ram4-disk50
+ plan: staging-cpu2-ram4-disk20
halt-timeout: 2h
groups: [default]
environment:
@@ -22,6 +22,9 @@
- fedora-40-64:
image: snapd-spread/fedora-40-64
workers: 6
+ - fedora-41-64:
+ image: snapd-spread/fedora-41-64
+ workers: 6
- opensuse-15.5-64:
image: snapd-spread/opensuse-15.5-64
diff --git a/tests/lib/spread/backend.testflinger.beta.yaml b/tests/lib/spread/backend.testflinger.beta.yaml
index 3203d4f7b18..651da380f40 100644
--- a/tests/lib/spread/backend.testflinger.beta.yaml
+++ b/tests/lib/spread/backend.testflinger.beta.yaml
@@ -1,4 +1,4 @@
- #this backend is used for edge validation
+ #this backend is used for beta validation
testflinger:
environment:
TRUST_TEST_KEYS: "false"
diff --git a/tests/lib/spread/rules/nested.yaml b/tests/lib/spread/rules/nested.yaml
index a4341187eef..71d463fbb70 100644
--- a/tests/lib/spread/rules/nested.yaml
+++ b/tests/lib/spread/rules/nested.yaml
@@ -4,6 +4,12 @@ rules:
- tests/nested/.*
to: [$SELF]
- rest:
- from: [.*]
+ nestedlib:
+ from:
+ - tests/lib/nested.sh
+ to: [tests/nested/]
+
+ assertions:
+ from:
+ - tests/lib/assertions/.*
to: [tests/nested/]
diff --git a/tests/lib/tools/snapd-state b/tests/lib/tools/snapd-state
index 4a5a118b5f0..9e7ce1b76ba 100755
--- a/tests/lib/tools/snapd-state
+++ b/tests/lib/tools/snapd-state
@@ -19,7 +19,7 @@ print_state() {
echo "snapd-state: jq-filter is a required parameter"
exit 1
fi
- jq -r "$JQ_FILTER" < /var/lib/snapd/state.json
+ gojq -r "$JQ_FILTER" < /var/lib/snapd/state.json
}
check_state() {
@@ -56,17 +56,17 @@ change_snap_channel() {
echo "snapd-state: snap and channel are required parameters"
exit 1
fi
- jq ".data.snaps[\"$SNAP\"].channel = \"$CHANNEL\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq ".data.snaps[\"$SNAP\"].channel = \"$CHANNEL\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
}
force_autorefresh() {
- jq ".data[\"last-refresh\"] = \"2007-08-22T09:30:44.449455783+01:00\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq ".data[\"last-refresh\"] = \"2007-08-22T09:30:44.449455783+01:00\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
}
prevent_autorefresh() {
- jq ".data[\"last-refresh\"] = \"$(date +%Y-%m-%dT%H:%M:%S%:z)\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq ".data[\"last-refresh\"] = \"$(date +%Y-%m-%dT%H:%M:%S%:z)\"" < /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
}
diff --git a/tests/lib/tools/store-state b/tests/lib/tools/store-state
index a4009e7140f..d49e59f72fa 100755
--- a/tests/lib/tools/store-state
+++ b/tests/lib/tools/store-state
@@ -1,5 +1,7 @@
#!/bin/bash
+set -e
+
STORE_CONFIG=/etc/systemd/system/snapd.service.d/store.conf
show_help() {
@@ -71,21 +73,9 @@ make_snap_installable(){
local snap_id="${3:-}"
if [ -n "$snap_id" ]; then
- if ! command -v yaml2json; then
- # FIXME: When fakestore is setup this snap cannot be installed
- # TODO: Install remarshal in setup_fake_store
- snap install remarshal
- fi
- if ! command -v jq; then
- # FIXME: When fakestore is setup this snap cannot be installed
- # TODO: Install jq in setup_fake_store or don't use snapped jq
- SUFFIX="$(snaps.name snap-suffix)"
- snap install "jq$SUFFIX"
- fi
-
# unsquash the snap to get its name
unsquashfs -d /tmp/snap-squashfs "$snap_path" meta/snap.yaml
- snap_name=$(yaml2json < /tmp/snap-squashfs/meta/snap.yaml | jq -r .name)
+ snap_name=$(gojq --yaml-input -r '.name' < /tmp/snap-squashfs/meta/snap.yaml)
rm -rf /tmp/snap-squashfs
cat >> /tmp/snap-decl.json << EOF
@@ -100,7 +90,7 @@ EOF
if [ -n "$extra_decl_json_file" ]; then
# then we need to combine the extra snap declaration json with the one
# we just wrote
- jq -s '.[0] * .[1]' <(cat /tmp/snap-decl.json) <(cat "$extra_decl_json_file") > /tmp/snap-decl.json.tmp
+ gojq -s '.[0] * .[1]' <(cat /tmp/snap-decl.json) <(cat "$extra_decl_json_file") > /tmp/snap-decl.json.tmp
mv /tmp/snap-decl.json.tmp /tmp/snap-decl.json
fi
@@ -141,8 +131,9 @@ setup_fake_store(){
return 1
fi
- # before switching make sure we have a session macaroon
- snap find test-snapd-tools
+ # before switching make sure we have a session macaroon, but keep it best
+ # effort
+ snap find test-snapd-tools || true
mkdir -p "$top_dir/asserts"
# debugging
diff --git a/tests/main/api-get-systems-label/task.yaml b/tests/main/api-get-systems-label/task.yaml
index 4423320e3e3..2ea1611b68e 100644
--- a/tests/main/api-get-systems-label/task.yaml
+++ b/tests/main/api-get-systems-label/task.yaml
@@ -8,27 +8,25 @@ systems:
- ubuntu-core-2*
execute: |
- snap install --edge jq
-
echo "Find what systems are available"
snap debug api /v2/systems > systems
- current_label=$(jq -r '.result.systems[0]["label"]' < systems)
+ current_label=$(gojq -r '.result.systems[0]["label"]' < systems)
echo "Get details for a specific system"
snap debug api "/v2/systems/$current_label" > current-system
echo "Ensure the result contains a model assertion"
- jq -r '.result.model.type' < current-system | MATCH model
- jq -r '.result.model.series' < current-system | MATCH 16
- jq -r '.result.model.base' < current-system | MATCH "core[0-9][0-9]"
+ gojq -r '.result.model.type' < current-system | MATCH model
+ gojq -r '.result.model.series' < current-system | MATCH 16
+ gojq -r '.result.model.base' < current-system | MATCH "core[0-9][0-9]"
echo "Ensure the result looks like a systems reply"
- jq -r '.result.brand.id' < current-system | MATCH "$(snap model --verbose|awk '/brand-id:/ {print $2}')"
- jq -r '.result.brand.validation' < current-system | MATCH '(verified|unproven|starred)'
- jq -r '.result.label' < current-system | MATCH "$current_label"
- jq -r '.result.current' < current-system | MATCH '(true|false)'
+ gojq -r '.result.brand.id' < current-system | MATCH "$(snap model --verbose|awk '/brand-id:/ {print $2}')"
+ gojq -r '.result.brand.validation' < current-system | MATCH '(verified|unproven|starred)'
+ gojq -r '.result.label' < current-system | MATCH "$current_label"
+ gojq -r '.result.current' < current-system | MATCH '(true|false)'
# we expect at least one current action to be available and
# each action always has a mode
- jq -r '.result.actions[0]' < current-system | MATCH 'mode'
+ gojq -r '.result.actions[0]' < current-system | MATCH 'mode'
echo "Ensure the result contains the gadget volumes"
- jq -r '.result.volumes' < current-system | MATCH bootloader
+ gojq -r '.result.volumes' < current-system | MATCH bootloader
# internal fields are not exported
- jq -r '.result.volumes' < current-system | NOMATCH VolumeName
+ gojq -r '.result.volumes' < current-system | NOMATCH VolumeName
diff --git a/tests/main/apparmor-prompting-flag-restart/task.yaml b/tests/main/apparmor-prompting-flag-restart/task.yaml
index 2dbb6b0db47..d2fe74115b9 100644
--- a/tests/main/apparmor-prompting-flag-restart/task.yaml
+++ b/tests/main/apparmor-prompting-flag-restart/task.yaml
@@ -14,7 +14,6 @@ systems:
- ubuntu-core-*
prepare: |
- snap install jq
# prerequisite for having a prompts handler service
snap set system experimental.user-daemons=true
"$TESTSTOOLS"/snaps-state install-local test-snapd-prompt-handler
@@ -93,9 +92,9 @@ execute: |
echo "Check that snap CLI reports prompting flag set correctly"
snap get system experimental.apparmor-prompting | MATCH "$1"
echo "Check that /v2/snaps/system/conf reports prompting flag set correctly"
- snap debug api /v2/snaps/system/conf | jq -r '.result.experimental."apparmor-prompting"' | MATCH "$1"
+ snap debug api /v2/snaps/system/conf | gojq -r '.result.experimental."apparmor-prompting"' | MATCH "$1"
echo "Check that /v2/system-info reports prompting correctly"
- snap debug api /v2/system-info | jq -r '.result.features."apparmor-prompting".enabled' | MATCH "$1"
+ snap debug api /v2/system-info | gojq -r '.result.features."apparmor-prompting".enabled' | MATCH "$1"
}
echo "Precondition check that snapd is active"
@@ -140,14 +139,18 @@ execute: |
echo "Enable prompting via API request"
- echo '{"experimental.apparmor-prompting": true}' | snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | jq -r '.status' | MATCH "Accepted" || reset_start_limit
+ echo '{"experimental.apparmor-prompting": true}' | \
+ snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | \
+ gojq -r '.status' | MATCH "Accepted" || reset_start_limit
echo "Check that snapd restarted after prompting set to true via api"
check_snapd_restarted
check_prompting_setting "true"
echo "Disable prompting via API request"
- echo '{"experimental.apparmor-prompting": false}' | snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | jq -r '.status' | MATCH "Accepted" || reset_start_limit
+ echo '{"experimental.apparmor-prompting": false}' | \
+ snap debug api -X PUT -H 'Content-Type: application/json' /v2/snaps/system/conf | \
+ gojq -r '.status' | MATCH "Accepted" || reset_start_limit
echo "Check that snapd restarted after prompting set to false via api"
check_snapd_restarted
diff --git a/tests/main/apparmor-prompting-snapd-startup/task.yaml b/tests/main/apparmor-prompting-snapd-startup/task.yaml
index 92befc7fd82..9d7ee489e51 100644
--- a/tests/main/apparmor-prompting-snapd-startup/task.yaml
+++ b/tests/main/apparmor-prompting-snapd-startup/task.yaml
@@ -33,7 +33,8 @@ execute: |
echo "Write two rules to disk, one of which is expired"
mkdir -p "$(dirname $RULES_PATH)"
- echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"},{"id":"0000000000000003","timestamp":"2004-10-20T16:47:32.138415627-05:00","user":1000,"snap":"firefox","interface":"home","constraints":{"path-pattern":"/home/test/Downloads/**","permissions":["read","write"]},"outcome":"allow","lifespan":"timespan","expiration":"2005-04-08T00:00:00Z"}]}' | tee "$RULES_PATH"
+ echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"},{"id":"0000000000000003","timestamp":"2004-10-20T16:47:32.138415627-05:00","user":1000,"snap":"firefox","interface":"home","constraints":{"path-pattern":"/home/test/Downloads/**","permissions":["read","write"]},"outcome":"allow","lifespan":"timespan","expiration":"2005-04-08T00:00:00Z"}]}' | \
+ tee "$RULES_PATH"
# Prompting is unsupported everywhere but the Ubuntu non-core systems with
# kernels which support apparmor prompting
@@ -65,25 +66,28 @@ execute: |
retry --wait 1 -n 60 systemctl is-active snapd
echo "Check that apparmor prompting is supported and enabled"
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true
# Write expected rules after the expired rule has been removed
- echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"}]}' | jq | tee expected.json
- # Parse existing rules through jq so they can be compared
- jq < "$RULES_PATH" > current.json
+ echo '{"rules":[{"id":"0000000000000002","timestamp":"2004-10-20T14:05:08.901174186-05:00","user":1000,"snap":"shellcheck","interface":"home","constraints":{"path-pattern":"/home/test/Projects/**","permissions":["read"]},"outcome":"allow","lifespan":"forever","expiration":"0001-01-01T00:00:00Z"}]}' | \
+ gojq | tee expected.json
+ # Parse existing rules through (go)jq so they can be compared
+ gojq < "$RULES_PATH" > current.json
echo "Check that rules on disk match what is expected"
diff expected.json current.json
echo "Check that we received two notices"
- snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq
- snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result | length' | MATCH 2
- snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result' | grep -c '"removed": "expired"' | MATCH 1
+ snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | gojq
+ snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \
+ gojq '.result | length' | MATCH 2
+ snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \
+ gojq '.result' | grep -c '"removed": "expired"' | MATCH 1
echo "Check that only the former rule is still valid (must be done with UID 1000)"
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result | length' | MATCH 1
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result[0].id' | MATCH "0000000000000002"
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result | length' | MATCH 1
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result[0].id' | MATCH "0000000000000002"
echo "Stop snapd and ensure it is not in failure mode"
systemctl stop snapd.service snapd.socket
@@ -98,20 +102,22 @@ execute: |
retry --wait 1 -n 60 systemctl is-active snapd.service snapd.socket
echo "Check that apparmor prompting is supported and enabled"
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true
echo "Check that rules on disk still match what is expected"
diff expected.json current.json
echo "Check that we received one notices for the non-expired rule"
- snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq
- snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result | length' | MATCH 1
- snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result[0].key' | MATCH "0000000000000002"
+ snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | gojq
+ snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \
+ gojq '.result | length' | MATCH 1
+ snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \
+ gojq '.result[0].key' | MATCH "0000000000000002"
echo "Check that only the non-expired rule is still valid (must be done with UID 1000)"
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result | length' | MATCH 1
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result[0].id' | MATCH "0000000000000002"
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result | length' | MATCH 1
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result[0].id' | MATCH "0000000000000002"
echo '### Simulate failure to open interfaces requests manager ###'
@@ -134,15 +140,16 @@ execute: |
echo "Check that apparmor prompting is supported and enabled"
# XXX: in the future, we should set enabled to be false if m.AppArmorPromptingRunning() is false,
# such as because creating the interfaces requests manager failed.
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true
echo "Check that rules on disk still match what is expected"
diff expected.json current.json
echo "Check that accessing a prompting endpoint results in an expected error"
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '."status-code"' | MATCH 500
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result.message' | MATCH -i "Apparmor Prompting is not running"
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '."status-code"' | MATCH 500
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | \
+ gojq '.result.message' | MATCH -i "Apparmor Prompting is not running"
echo '### Remove the corrupted max prompt ID file and check that prompting backends can start again ###'
@@ -162,17 +169,19 @@ execute: |
retry --wait 1 -n 60 systemctl is-active snapd.service snapd.socket
echo "Check that apparmor prompting is supported and enabled"
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".supported' | MATCH true
- snap debug api "/v2/system-info" | jq '.result.features."apparmor-prompting".enabled' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".supported' | MATCH true
+ snap debug api "/v2/system-info" | gojq '.result.features."apparmor-prompting".enabled' | MATCH true
echo "Check that rules on disk still match what is expected"
diff expected.json current.json
echo "Check that we received one notices for the non-expired rule"
- snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq
- snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result | length' | MATCH 1
- snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | jq '.result[0].key' | MATCH "0000000000000002"
+ snap debug api --fail "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | gojq
+ snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \
+ gojq '.result | length' | MATCH 1
+ snap debug api "/v2/notices?after=$CURRTIME&types=interfaces-requests-rule-update&user-id=1000" | \
+ gojq '.result[0].key' | MATCH "0000000000000002"
echo "Check that the non-expired rule is still valid (must be done with UID 1000)"
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result | length' | MATCH 1
- sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | jq '.result[0].id' | MATCH "0000000000000002"
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result | length' | MATCH 1
+ sudo -iu '#1000' snap debug api /v2/interfaces/requests/rules | gojq '.result[0].id' | MATCH "0000000000000002"
diff --git a/tests/main/appstream-id/task.yaml b/tests/main/appstream-id/task.yaml
index d91606c3cc0..6f4c5174583 100644
--- a/tests/main/appstream-id/task.yaml
+++ b/tests/main/appstream-id/task.yaml
@@ -7,9 +7,6 @@ details: |
# ubuntu-core: no jq
systems: [-ubuntu-core*]
-prepare: |
- snap install --edge jq
-
debug: |
cat response || true
@@ -17,7 +14,7 @@ execute: |
echo "Verify that search results contain common-ids"
timeout 5 snap debug api /v2/find?name=test-snapd-appstreamid > response
# shellcheck disable=SC2002
- cat response | jq -r ' .result[0]["common-ids"] | sort | join (",")' | \
+ cat response | gojq -r ' .result[0]["common-ids"] | sort | join (",")' | \
MATCH 'io.snapcraft.test-snapd-appstreamid.bar,io.snapcraft.test-snapd-appstreamid.foo'
snap install --edge test-snapd-appstreamid
@@ -25,11 +22,11 @@ execute: |
echo "Verify that installed snap info contains common-ids"
timeout 5 snap debug api /v2/snaps/test-snapd-appstreamid > response
# shellcheck disable=SC2002
- cat response | jq -r ' .result["common-ids"] | sort | join(",")' | \
+ cat response | gojq -r ' .result["common-ids"] | sort | join(",")' | \
MATCH 'io.snapcraft.test-snapd-appstreamid.bar,io.snapcraft.test-snapd-appstreamid.foo'
echo "Verify that apps have their common-id set"
timeout 5 snap debug api /v2/apps?names=test-snapd-appstreamid > response
# shellcheck disable=SC2002
- cat response | jq -r ' .result | sort_by(.name) | [.[]."common-id"] | join(",")' | \
+ cat response | gojq -r ' .result | sort_by(.name) | [.[]."common-id"] | join(",")' | \
MATCH 'io.snapcraft.test-snapd-appstreamid.bar,,io.snapcraft.test-snapd-appstreamid.foo'
diff --git a/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh b/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh
index 73ab37f07be..0f4a8ce3a72 100755
--- a/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh
+++ b/tests/main/auto-refresh-backoff/check_auto_refresh_count.sh
@@ -4,4 +4,4 @@ LAST_CHANGE_ID=$1
CHANGES_COUNT=$2
#shellcheck disable=SC2086,SC2046
-test $(snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT
+test $(snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | length") == $CHANGES_COUNT
diff --git a/tests/main/auto-refresh-backoff/task.yaml b/tests/main/auto-refresh-backoff/task.yaml
index f308ffe1b42..a513a8759a4 100644
--- a/tests/main/auto-refresh-backoff/task.yaml
+++ b/tests/main/auto-refresh-backoff/task.yaml
@@ -18,10 +18,6 @@ prepare: |
exit
fi
- # Needed by make-snap-installable, Install before switching to fakestore
- snap install jq
- snap install remarshal
-
# Install snaps as baseline since we want to test what happens in refreshes not installs
echo "Given installed snaps"
snap install "$SNAP_ONE" "$SNAP_TWO"
@@ -50,7 +46,7 @@ restore: |
rm -rf "$BLOB_DIR"
debug: |
- snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)"
+ snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\")] | sort_by(.id|tonumber)"
execute: |
if [ "$TRUST_TEST_KEYS" = "false" ]; then
@@ -94,7 +90,7 @@ execute: |
}
# Record last change id before we start to avoid flakiness due to auto-refreshes in other tests
- LAST_CHANGE_ID=$(snap debug api /v2/changes?select=all | jq '.result | sort_by(.id|tonumber) | .[-1].id')
+ LAST_CHANGE_ID=$(snap debug api /v2/changes?select=all | gojq '.result | sort_by(.id|tonumber) | .[-1].id')
# -------- FIRST AUTO REFRESH --------
@@ -162,26 +158,26 @@ execute: |
readlink "$SNAP_MOUNT_DIR/$SNAP_TWO/current" | MATCH 33
echo "Check auto-refresh behaviour matches expectations for backoff algorithm"
- snap debug api /v2/changes?select=ready | jq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json
+ snap debug api /v2/changes?select=ready | gojq "[.result[] | select(.kind == \"auto-refresh\" and (.id|tonumber) > ($LAST_CHANGE_ID|tonumber))] | sort_by(.id|tonumber)" > changes.json
# 1st auto-refresh
- jq '.[0].status' < changes.json | MATCH "Error"
- jq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_ONE"
- jq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_TWO"
- jq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_ONE"
- jq '.[0].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO"
+ gojq '.[0].status' < changes.json | MATCH "Error"
+ gojq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_ONE"
+ gojq '.[0].data."snap-names"' < changes.json | MATCH "$SNAP_TWO"
+ gojq '.[0].data."refresh-failed"' < changes.json | MATCH "$SNAP_ONE"
+ gojq '.[0].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO"
# 2nd auto-refresh
- jq '.[1].status' < changes.json | MATCH "Done"
+ gojq '.[1].status' < changes.json | MATCH "Done"
# Broken SNAP_ONE should have been skipped this time
- jq '.[1].data."snap-names"' < changes.json | NOMATCH "$SNAP_ONE"
- jq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_TWO"
- jq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE"
- jq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO"
+ gojq '.[1].data."snap-names"' < changes.json | NOMATCH "$SNAP_ONE"
+ gojq '.[1].data."snap-names"' < changes.json | MATCH "$SNAP_TWO"
+ gojq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE"
+ gojq '.[1].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO"
# 3rd auto-refresh
- jq '.[2].status' < changes.json | MATCH "Done"
- jq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_ONE"
- jq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_TWO"
- jq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE"
- jq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO"
+ gojq '.[2].status' < changes.json | MATCH "Done"
+ gojq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_ONE"
+ gojq '.[2].data."snap-names"' < changes.json | MATCH "$SNAP_TWO"
+ gojq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_ONE"
+ gojq '.[2].data."refresh-failed"' < changes.json | NOMATCH "$SNAP_TWO"
diff --git a/tests/main/auto-refresh-gating-from-snap/task.yaml b/tests/main/auto-refresh-gating-from-snap/task.yaml
index 4641971043c..72c7c53753c 100644
--- a/tests/main/auto-refresh-gating-from-snap/task.yaml
+++ b/tests/main/auto-refresh-gating-from-snap/task.yaml
@@ -16,7 +16,6 @@ environment:
DEBUG_LOG_FILE: /var/snap/test-snapd-refresh-control/common/debug.log
prepare: |
- snap install --devmode jq
snap set system experimental.gate-auto-refresh-hook=true
echo "Install test snaps"
@@ -25,7 +24,7 @@ prepare: |
"$TESTSTOOLS"/snaps-state install-local "$NOHOOK_SNAP"
debug: |
- jq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true
+ gojq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true
execute: |
LAST_REFRESH_CHANGE_ID=1
diff --git a/tests/main/auto-refresh-gating/task.yaml b/tests/main/auto-refresh-gating/task.yaml
index de3a18c905a..dea535b473f 100644
--- a/tests/main/auto-refresh-gating/task.yaml
+++ b/tests/main/auto-refresh-gating/task.yaml
@@ -14,11 +14,10 @@ environment:
DEBUG_LOG_FILE: /var/snap/test-snapd-refresh-control/common/debug.log
prepare: |
- snap install --devmode jq
snap set system experimental.gate-auto-refresh-hook=true
debug: |
- jq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true
+ gojq -r '.data["snaps-hold"]' < /var/lib/snapd/state.json || true
snap changes || true
snap refresh --time || true
diff --git a/tests/main/auto-refresh-pre-download/task.yaml b/tests/main/auto-refresh-pre-download/task.yaml
index 7c198541a61..69833520218 100644
--- a/tests/main/auto-refresh-pre-download/task.yaml
+++ b/tests/main/auto-refresh-pre-download/task.yaml
@@ -22,11 +22,9 @@ environment:
prepare: |
# ensure no other refreshes interfere with the test
snap refresh
- snap install --devmode jq
snap install test-snapd-sh
restore: |
- snap remove --purge jq || true
snap remove --purge test-snapd-sh || true
debug: |
diff --git a/tests/main/auto-refresh-private/task.yaml b/tests/main/auto-refresh-private/task.yaml
index 67350081b68..10dd684cb3c 100644
--- a/tests/main/auto-refresh-private/task.yaml
+++ b/tests/main/auto-refresh-private/task.yaml
@@ -85,7 +85,8 @@ execute: |
systemctl stop snapd.{service,socket}
- jq ".data.auth.users[0][\"store-macaroon\"] = \"$M\"|.data.auth.users[0][\"store-discharges\"][0] = \"$D\"" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq ".data.auth.users[0][\"store-macaroon\"] = \"$M\"|.data.auth.users[0][\"store-discharges\"][0] = \"$D\"" \
+ /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
"$TESTSTOOLS"/snapd-state force-autorefresh
systemctl start snapd.{service,socket}
diff --git a/tests/main/auto-refresh/task.yaml b/tests/main/auto-refresh/task.yaml
index 66f48baa71f..5c7979af617 100644
--- a/tests/main/auto-refresh/task.yaml
+++ b/tests/main/auto-refresh/task.yaml
@@ -10,7 +10,6 @@ environment:
SNAP_NAME/parallel: test-snapd-tools_instance
prepare: |
- snap install --devmode jq
if [[ "$SPREAD_VARIANT" =~ parallel ]]; then
snap set system experimental.parallel-instances=true
fi
@@ -66,7 +65,7 @@ execute: |
snap list|MATCH "$SNAP_NAME +[0-9]+\\.[0-9]+\\+fake1"
echo "Ensure refresh.last is set"
- jq ".data[\"last-refresh\"]" /var/lib/snapd/state.json | MATCH "$(date +%Y)"
+ gojq ".data[\"last-refresh\"]" /var/lib/snapd/state.json | MATCH "$(date +%Y)"
echo "No refresh hold at this point"
snap refresh --time | NOMATCH "^hold:"
diff --git a/tests/main/aux-info/task.yaml b/tests/main/aux-info/task.yaml
index 443754a616e..ff176b4e8ae 100644
--- a/tests/main/aux-info/task.yaml
+++ b/tests/main/aux-info/task.yaml
@@ -9,17 +9,16 @@ systems: [ubuntu-18.04-64, ubuntu-2*, ubuntu-core-*, fedora-*]
prepare: |
snap install snap-store
- snap install jq
execute: |
- snap_id=$(snap info snap-store | grep snap-id | awk '{ print $2 }')
- jq --sort-keys .media < "/var/cache/snapd/aux/${snap_id}.json" > media.json
+ snap_id=$(snap info snap-store | gojq -r --yaml-input '.["snap-id"]')
+ gojq .media < "/var/cache/snapd/aux/${snap_id}.json" > media.json
# don't depend on the exact number of media files, but there should be
# something here
- media_length=$(jq '. | length' < media.json)
+ media_length=$(gojq '. | length' < media.json)
test "${media_length}" -gt 0
- timeout 5 snap debug api /v2/snaps/snap-store | jq --sort-keys .result.media > snapd-media.json
+ timeout 5 snap debug api /v2/snaps/snap-store | gojq .result.media > snapd-media.json
diff media.json snapd-media.json
diff --git a/tests/main/cgroup-devices-v2/task.yaml b/tests/main/cgroup-devices-v2/task.yaml
index 07743752c4e..165bcf7ebd3 100644
--- a/tests/main/cgroup-devices-v2/task.yaml
+++ b/tests/main/cgroup-devices-v2/task.yaml
@@ -59,7 +59,7 @@ execute: |
dump_cgroup_device_progs() {
# dump all progs but those that are assigned to systemd, in which case
# they have the pids list non empty
- bpftool prog list -j | jq -r '.[] | select(.type == "cgroup_device") | select(.pids == null) | .id'
+ bpftool prog list -j | gojq -r '.[] | select(.type == "cgroup_device") | select(.pids == null) | .id'
}
echo "Dump BPF programs that are of type cgroup_device"
dump_cgroup_device_progs > cgroup_device.progs-1
diff --git a/tests/main/cloud-init/task.yaml b/tests/main/cloud-init/task.yaml
index 41454b51772..aca43d75930 100644
--- a/tests/main/cloud-init/task.yaml
+++ b/tests/main/cloud-init/task.yaml
@@ -8,11 +8,6 @@ details: |
# GCE backend sets instance data
backends: [google]
-prepare: |
- if ! command -v jq; then
- snap install --devmode jq
- fi
-
execute: |
if [[ ! -e /run/cloud-init/instance-data.json ]]; then
echo "cloud-init instance data is required to execute the test"
@@ -35,7 +30,7 @@ execute: |
kname=${kname/_/-}
fi
- jq -r ".[\"v1\"][\"$kname\"]" < /run/cloud-init/instance-data.json
+ gojq -r ".[\"v1\"][\"$kname\"]" < /run/cloud-init/instance-data.json
}
# GCE sets the following in Ubuntu images:
# {
diff --git a/tests/main/component-local-installs/comp-four/meta/component.yaml b/tests/main/component-local-installs/comp-four/meta/component.yaml
new file mode 100644
index 00000000000..7b00171df07
--- /dev/null
+++ b/tests/main/component-local-installs/comp-four/meta/component.yaml
@@ -0,0 +1,5 @@
+component: test-snap-two+comp-four
+type: standard
+version: '1.0'
+summary: test component
+description: test component
diff --git a/tests/main/component-local-installs/comp-one/meta/component.yaml b/tests/main/component-local-installs/comp-one/meta/component.yaml
new file mode 100644
index 00000000000..165fdb3b8e2
--- /dev/null
+++ b/tests/main/component-local-installs/comp-one/meta/component.yaml
@@ -0,0 +1,5 @@
+component: test-snap-one+comp-one
+type: standard
+version: '1.0'
+summary: test component
+description: test component
diff --git a/tests/main/component-local-installs/comp-three/meta/component.yaml b/tests/main/component-local-installs/comp-three/meta/component.yaml
new file mode 100644
index 00000000000..c4897d8cc51
--- /dev/null
+++ b/tests/main/component-local-installs/comp-three/meta/component.yaml
@@ -0,0 +1,5 @@
+component: test-snap-two+comp-three
+type: standard
+version: '1.0'
+summary: test component
+description: test component
diff --git a/tests/main/component-local-installs/comp-two/meta/component.yaml b/tests/main/component-local-installs/comp-two/meta/component.yaml
new file mode 100644
index 00000000000..c4772ef15ee
--- /dev/null
+++ b/tests/main/component-local-installs/comp-two/meta/component.yaml
@@ -0,0 +1,5 @@
+component: test-snap-one+comp-two
+type: standard
+version: '1.0'
+summary: test component
+description: test component
diff --git a/tests/main/component-local-installs/task.yaml b/tests/main/component-local-installs/task.yaml
new file mode 100644
index 00000000000..8f7d6d0b9dd
--- /dev/null
+++ b/tests/main/component-local-installs/task.yaml
@@ -0,0 +1,81 @@
+summary: Test installing components and snaps from local files.
+
+details: |
+ Test that we can install components and snaps from local files.
+ We test various combinations, like:
+ - installing a snap and some components for that snap
+ - installing a snap and some components for another snap (that is already installed)
+ - failing to install a snap and some components for another snap (that is not installed)
+ - installing multiple components for different snaps
+
+systems: [ubuntu-16.04-64, ubuntu-18.04-64, ubuntu-2*, ubuntu-core-*, fedora-*]
+
+execute: |
+ for container in test-snap-one/ test-snap-two/ comp-one/ comp-two/ comp-three/ comp-four/; do
+ snap pack $container
+ done
+
+ # install everything at once
+ snap install --dangerous \
+ ./test-snap-one_1.0_all.snap \
+ ./test-snap-two_1.0_all.snap \
+ ./test-snap-one+comp-one_1.0.comp \
+ ./test-snap-one+comp-two_1.0.comp \
+ ./test-snap-two+comp-three_1.0.comp \
+ ./test-snap-two+comp-four_1.0.comp
+
+ snap run test-snap-one comp-one
+ snap run test-snap-one comp-two
+ snap run test-snap-two comp-three
+ snap run test-snap-two comp-four
+
+ snap remove test-snap-one test-snap-two
+
+ # install only the snaps
+ snap install --dangerous \
+ ./test-snap-one_1.0_all.snap \
+ ./test-snap-two_1.0_all.snap
+ not snap run test-snap-one comp-one
+ not snap run test-snap-one comp-two
+ not snap run test-snap-two comp-three
+ not snap run test-snap-two comp-four
+
+ # install only the components
+ snap install --dangerous \
+ ./test-snap-one+comp-one_1.0.comp \
+ ./test-snap-one+comp-two_1.0.comp \
+ ./test-snap-two+comp-three_1.0.comp \
+ ./test-snap-two+comp-four_1.0.comp
+ snap run test-snap-one comp-one
+ snap run test-snap-one comp-two
+ snap run test-snap-two comp-three
+ snap run test-snap-two comp-four
+
+ snap remove test-snap-one test-snap-two
+
+ # install mixture of components for already installed snaps and a new snap
+ # with components
+ snap install --dangerous \
+ ./test-snap-one_1.0_all.snap \
+ ./test-snap-one+comp-one_1.0.comp
+ snap run test-snap-one comp-one
+ not snap run test-snap-one comp-two
+ snap install --dangerous \
+ ./test-snap-one+comp-two_1.0.comp \
+ ./test-snap-two_1.0_all.snap \
+ ./test-snap-two+comp-three_1.0.comp \
+ ./test-snap-two+comp-four_1.0.comp
+ snap run test-snap-one comp-one
+ snap run test-snap-one comp-two
+ snap run test-snap-two comp-three
+ snap run test-snap-two comp-four
+
+ snap remove test-snap-one test-snap-two
+
+ # fail to install a component for a snap that isn't installed. note that this
+ # isn't impacted by the separate lanes that the snaps are installed in, since
+ # the failure here doesn't occur from inside of a task.
+ not snap install --dangerous \
+ ./test-snap-one_1.0_all.snap \
+ ./test-snap-one+comp-one_1.0.comp \
+ ./test-snap-two+comp-three_1.0.comp
diff --git a/tests/main/component-local-installs/test-snap-one/meta/snap.yaml b/tests/main/component-local-installs/test-snap-one/meta/snap.yaml
new file mode 100644
index 00000000000..a757b367983
--- /dev/null
+++ b/tests/main/component-local-installs/test-snap-one/meta/snap.yaml
@@ -0,0 +1,22 @@
+name: test-snap-one
+version: '1.0'
+summary: A snap with components
+description: |
+ A snap with components used for testing snapd.
+architectures:
+- all
+base: core24
+apps:
+ test-snap-one:
+ command: test
+confinement: strict
+grade: stable
+components:
+ comp-one:
+ summary: test component
+ description: test component
+ type: standard
+ comp-two:
+ summary: test component
+ description: test component
+ type: standard
diff --git a/tests/main/component-local-installs/test-snap-one/test b/tests/main/component-local-installs/test-snap-one/test
new file mode 100755
index 00000000000..18106872b61
--- /dev/null
+++ b/tests/main/component-local-installs/test-snap-one/test
@@ -0,0 +1,15 @@
+#!/bin/sh -e
+
+if [ $# -ne 1 ]; then
+ echo "pass in a component name to check if it is installed"
+ exit 1
+fi
+
+if [ ! -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}/meta/component.yaml" ]; then
+ echo "component ${1} is not installed!"
+ exit 1
+fi
+
+comp_rev="$(basename "$(readlink -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}")")"
+
+echo "component ${1} is installed at revision ${comp_rev}"
diff --git a/tests/main/component-local-installs/test-snap-two/meta/snap.yaml b/tests/main/component-local-installs/test-snap-two/meta/snap.yaml
new file mode 100644
index 00000000000..8544f1e900f
--- /dev/null
+++ b/tests/main/component-local-installs/test-snap-two/meta/snap.yaml
@@ -0,0 +1,22 @@
+name: test-snap-two
+version: '1.0'
+summary: A snap with components
+description: |
+ A snap with components used for testing snapd.
+architectures:
+- all
+base: core24
+apps:
+ test-snap-two:
+ command: test
+confinement: strict
+grade: stable
+components:
+ comp-three:
+ summary: test component
+ description: test component
+ type: standard
+ comp-four:
+ summary: test component
+ description: test component
+ type: standard
diff --git a/tests/main/component-local-installs/test-snap-two/test b/tests/main/component-local-installs/test-snap-two/test
new file mode 100755
index 00000000000..18106872b61
--- /dev/null
+++ b/tests/main/component-local-installs/test-snap-two/test
@@ -0,0 +1,15 @@
+#!/bin/sh -e
+
+if [ $# -ne 1 ]; then
+ echo "pass in a component name to check if it is installed"
+ exit 1
+fi
+
+if [ ! -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}/meta/component.yaml" ]; then
+ echo "component ${1} is not installed!"
+ exit 1
+fi
+
+comp_rev="$(basename "$(readlink -f "/snap/${SNAP_NAME}/components/${SNAP_REVISION}/${1}")")"
+
+echo "component ${1} is installed at revision ${comp_rev}"
diff --git a/tests/main/component-refresh-and-revert/task.yaml b/tests/main/component-refresh-and-revert/task.yaml
index 82373959980..0064f14ebb0 100644
--- a/tests/main/component-refresh-and-revert/task.yaml
+++ b/tests/main/component-refresh-and-revert/task.yaml
@@ -70,3 +70,15 @@ execute: |
snap revert test-snap-component-refreshes --revision=5
test-snap-component-refreshes one | MATCH '.*revision 7$'
test-snap-component-refreshes two | MATCH '.*revision 6$'
+
+ snap remove test-snap-component-refreshes
+
+ snap install test-snap-component-refreshes+one --revision=3 --edge
+ test-snap-component-refreshes one | MATCH '.*revision 3$'
+ not test-snap-component-refreshes two
+
+ # this should refresh the snap to the latest in the edge channel, update the
+ # "one" component, and install the "two" component
+ snap refresh test-snap-component-refreshes+two
+ test-snap-component-refreshes one | MATCH '.*revision 5$'
+ test-snap-component-refreshes two | MATCH '.*revision 5$'
diff --git a/tests/main/document-portal-activation/task.yaml b/tests/main/document-portal-activation/task.yaml
index 398eae5cfbe..1ef25e3e063 100644
--- a/tests/main/document-portal-activation/task.yaml
+++ b/tests/main/document-portal-activation/task.yaml
@@ -102,6 +102,9 @@ execute: |
echo "No output on stderr when running without a session bus"
# NOTE: lack of session bus is emulated by unsetting DBUS_SESSION_BUS address
# and stopping dbus.socket
- tests.session -u test exec systemctl --user stop dbus.socket
- tests.session -u test exec sh -c "DBUS_SESSION_BUS_ADDRESS= test-snapd-desktop.check-dirs /home/test/snap/test-snapd-desktop/current" 2>stderr.log
- check_stderr stderr.log
+ # In opensuse tumbleweed the dbus.socket is configured to refuse manual start/stop
+ if ! os.query is-opensuse tumbleweed; then
+ tests.session -u test exec systemctl --user stop dbus.socket
+ tests.session -u test exec sh -c "DBUS_SESSION_BUS_ADDRESS= test-snapd-desktop.check-dirs /home/test/snap/test-snapd-desktop/current" 2>stderr.log
+ check_stderr stderr.log
+ fi
diff --git a/tests/main/interfaces-calendar-service/task.yaml b/tests/main/interfaces-calendar-service/task.yaml
index aa251061612..9abbe5ca706 100644
--- a/tests/main/interfaces-calendar-service/task.yaml
+++ b/tests/main/interfaces-calendar-service/task.yaml
@@ -25,9 +25,8 @@ systems:
- -arch-linux-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -centos-*
- -debian-*
- - -fedora-38-* # test-snapd-eds is incompatible with eds version shipped with the distro
- - -fedora-39-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -fedora-40-* # test-snapd-eds is incompatible with eds version shipped with the distro
+ - -fedora-41-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -opensuse-15.5-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -opensuse-15.6-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -opensuse-tumbleweed-* # test-snapd-eds is incompatible with eds version shipped with the distro
diff --git a/tests/main/interfaces-contacts-service/task.yaml b/tests/main/interfaces-contacts-service/task.yaml
index a54199c84ef..af7fda64a12 100644
--- a/tests/main/interfaces-contacts-service/task.yaml
+++ b/tests/main/interfaces-contacts-service/task.yaml
@@ -20,9 +20,8 @@ systems:
- -arch-linux-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -centos-*
- -debian-*
- - -fedora-38-* # test-snapd-eds is incompatible with eds version shipped with the distro
- - -fedora-39-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -fedora-40-* # test-snapd-eds is incompatible with eds version shipped with the distro
+ - -fedora-41-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -opensuse-15.5-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -opensuse-15.6-* # test-snapd-eds is incompatible with eds version shipped with the distro
- -opensuse-tumbleweed-* # test-snapd-eds is incompatible with eds version shipped with the distro
diff --git a/tests/main/interfaces-custom-device-app-slot/task.yaml b/tests/main/interfaces-custom-device-app-slot/task.yaml
index da763e1e964..2f614849238 100644
--- a/tests/main/interfaces-custom-device-app-slot/task.yaml
+++ b/tests/main/interfaces-custom-device-app-slot/task.yaml
@@ -25,9 +25,22 @@ prepare: |
snap install core
fi
+ # Install store-state dependencies
+ echo "Ensure jq is installed"
+ if ! command -v jq; then
+ snap install --devmode jq
+ fi
+
+ echo "Ensure yaml2json is installed"
+ if ! command -v yaml2json; then
+ snap install --devmode remarshal
+ fi
+
snap debug can-manage-refreshes | MATCH false
snap ack "$TESTSLIB/assertions/testrootorg-store.account-key"
+ snap ack "$TESTSLIB/assertions/developer1.account"
+ snap ack "$TESTSLIB/assertions/developer1.account-key"
"$TESTSTOOLS"/store-state setup-fake-store "$BLOB_DIR"
diff --git a/tests/main/interfaces-gpio-memory-control/task.yaml b/tests/main/interfaces-gpio-memory-control/task.yaml
index 7746a845868..36792f3b50b 100644
--- a/tests/main/interfaces-gpio-memory-control/task.yaml
+++ b/tests/main/interfaces-gpio-memory-control/task.yaml
@@ -3,7 +3,7 @@ summary: Ensure that the gpio physical memory control interface works.
details: |
The gpio-memory-control interface allows read/write access to all gpio memory.
-systems: [ubuntu-core-16-arm-*]
+systems: [ubuntu-core-18-arm-32*]
prepare: |
echo "Given the test-snapd-gpio-memory-control snap is installed"
diff --git a/tests/main/interfaces-hooks/task.yaml b/tests/main/interfaces-hooks/task.yaml
index 9446bb13f06..0899696c9cc 100644
--- a/tests/main/interfaces-hooks/task.yaml
+++ b/tests/main/interfaces-hooks/task.yaml
@@ -11,8 +11,6 @@ environment:
PRODUCER_DATA: /var/snap/basic-iface-hooks-producer/common
prepare: |
- snap install --devmode jq
-
echo "Install test hooks snaps"
"$TESTSTOOLS"/snaps-state install-local basic-iface-hooks-consumer
"$TESTSTOOLS"/snaps-state install-local basic-iface-hooks-producer
@@ -36,13 +34,19 @@ execute: |
}
check_attributes(){
# static values should have the values defined in snap's yaml
- jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-1"]' /var/lib/snapd/state.json | MATCH "consumer-value-1"
- jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-2"]' /var/lib/snapd/state.json | MATCH "consumer-value-2"
- jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-1"]' /var/lib/snapd/state.json | MATCH "producer-value-1"
- jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-2"]' /var/lib/snapd/state.json | MATCH "producer-value-2"
+ gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-1"]' \
+ /var/lib/snapd/state.json | MATCH "consumer-value-1"
+ gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-static"]["consumer-attr-2"]' \
+ /var/lib/snapd/state.json | MATCH "consumer-value-2"
+ gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-1"]' \
+ /var/lib/snapd/state.json | MATCH "producer-value-1"
+ gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-static"]["producer-attr-2"]' \
+ /var/lib/snapd/state.json | MATCH "producer-value-2"
# dynamic attributes have values created by the hooks, the "-validated" suffix is added by our test interface
- jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-dynamic"]["before-connect"]' /var/lib/snapd/state.json | MATCH 'plug-changed\(consumer-value\)'
- jq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-dynamic"]["before-connect"]' /var/lib/snapd/state.json | MATCH 'slot-changed\(producer-value\)'
+ gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["plug-dynamic"]["before-connect"]' \
+ /var/lib/snapd/state.json | MATCH 'plug-changed\(consumer-value\)'
+ gojq -r '.data["conns"]["basic-iface-hooks-consumer:consumer basic-iface-hooks-producer:producer"]["slot-dynamic"]["before-connect"]' \
+ /var/lib/snapd/state.json | MATCH 'slot-changed\(producer-value\)'
}
check_hooks_were_run(){
diff --git a/tests/main/interfaces-mount-control-nfs/task.yaml b/tests/main/interfaces-mount-control-nfs/task.yaml
new file mode 100644
index 00000000000..dc96b0aaa13
--- /dev/null
+++ b/tests/main/interfaces-mount-control-nfs/task.yaml
@@ -0,0 +1,85 @@
+summary: Test that with mount-control NFS shares can be mounted
+
+details: |
+ Verify that NFS mounts can be performed with mount-control
+
+# limit to systems where we know NFS works without problems
+# ubuntu-core: no required dependencies to export NFS shares
+systems:
+ - ubuntu-22.04-*
+ - ubuntu-24.04-*
+
+prepare: |
+ # Mount should not leak
+ tests.cleanup defer NOMATCH 'nfs-share' /proc/self/mountinfo
+
+ # Create directory which the test will share
+ if [ ! -d /var/nfs-share ]; then
+ mkdir /var/nfs-share
+ tests.cleanup defer rm -r /var/nfs-share
+ fi
+ echo 'hello from NFS share' > /var/nfs-share/hello
+
+ # Install a package with additional kernel modules
+ if ! tests.pkgs install "linux-modules-extra-$(uname -r)"; then
+ echo "SKIP: Kernel version and extras module mismatch"
+ exit 1
+ fi
+
+ # Install nfs with some precautions to undo the side-effects if we are
+ # really installing it and it was not pre-installed. If /proc/fs/nfsd
+ # is not initially mounted then ask the test to unmount it later
+ # without checking if it is mounted (hence okfail wrapper).
+ if not mountinfo.query /proc/fs/nfsd .fs_type=nfsd; then
+ tests.cleanup defer okfail umount /proc/fs/nfsd
+ fi
+ # If /var/lib/nfs/rpc_pipefs is not initially mounted then ask the test
+ # to unmount it later.
+ if not mountinfo.query /var/lib/nfs/rpc_pipefs .fs_type=rpc_pipefs; then
+ tests.cleanup defer okfail umount /proc/nfs/rpc_pipefs
+ fi
+
+ tests.pkgs install nfs-kernel-server
+
+ # Export /var/home/test-remote over NFS.
+ mkdir -p /etc/exports.d/
+ echo '/var/nfs-share localhost(rw,no_subtree_check)' > /etc/exports.d/test.exports
+ tests.cleanup defer rm -f /etc/exports.d/test.exports
+ retry -n 10 exportfs -r
+
+ # Later on remove the exports file and reload exported filesystems.
+ tests.cleanup defer retry -n 10 exportfs -r
+
+restore: |
+ # Run cleanup handlers registered earlier.
+ tests.cleanup restore
+
+execute: |
+ snap install test-snapd-mount-control-nfs
+ mkdir -p /media/mounted
+ tests.cleanup defer rm -rf /media/mounted
+
+ # Connect removable media first so that we can 'read' files.
+ snap connect test-snapd-mount-control-nfs:removable-media
+
+ # Blocked by seccomp, hence EPERM rather than EACCESS
+ test-snapd-mount-control-nfs.cmd mount.nfs localhost:/var/nfs-share /media/mounted 2>&1 | \
+ MATCH 'Operation not permitted'
+
+ test-snapd-mount-control-nfs.cmd snapctl mount -t nfs localhost:/var/nfs-share /media/mounted 2>&1 | \
+ MATCH 'no matching mount-control connection found'
+
+ echo "When the mount-control interface which lists NFS is connected"
+ snap connect test-snapd-mount-control-nfs:mntctl
+
+ echo "It is possible to mount the share"
+ test-snapd-mount-control-nfs.cmd mount.nfs localhost:/var/nfs-share /media/mounted
+ echo "Read the contents under the mount point"
+ test-snapd-mount-control-nfs.cmd cat /media/mounted/hello | MATCH 'hello from NFS share'
+ echo "And unmount it"
+ test-snapd-mount-control-nfs.cmd umount /media/mounted
+
+ echo "Same thing works through snapctl"
+ test-snapd-mount-control-nfs.cmd snapctl mount -t nfs localhost:/var/nfs-share /media/mounted
+ test-snapd-mount-control-nfs.cmd cat /media/mounted/hello | MATCH 'hello from NFS share'
+ test-snapd-mount-control-nfs.cmd snapctl umount /media/mounted
diff --git a/tests/main/interfaces-mount-control-nfs/test-snapd-sh/bin/sh b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/bin/sh
new file mode 100755
index 00000000000..0f845e07c5a
--- /dev/null
+++ b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/bin/sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+PS1='$ '
+exec /bin/sh "$@"
diff --git a/tests/main/interfaces-mount-control-nfs/test-snapd-sh/meta/snap.yaml b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/meta/snap.yaml
new file mode 100644
index 00000000000..7aa65c2db55
--- /dev/null
+++ b/tests/main/interfaces-mount-control-nfs/test-snapd-sh/meta/snap.yaml
@@ -0,0 +1,16 @@
+name: test-snapd-mount-control-nfs
+summary: A snap for testing mount-control with NFS
+version: 1.0
+
+apps:
+ cmd:
+ command: bin/sh
+ plugs: [mount-control]
+
+plugs:
+ mntctl:
+ interface: mount-control
+ mount:
+ - where: /media/**
+ type: [nfs]
+ options: [rw]
diff --git a/tests/main/interfaces-snap-interfaces-requests-control/task.yaml b/tests/main/interfaces-snap-interfaces-requests-control/task.yaml
index 74e044cc485..f360c152e51 100644
--- a/tests/main/interfaces-snap-interfaces-requests-control/task.yaml
+++ b/tests/main/interfaces-snap-interfaces-requests-control/task.yaml
@@ -14,11 +14,10 @@ details: |
environment:
# not all terminals support UTF-8, but Python tries to be smart and attempts
# to guess the encoding as if the output would go to the terminal, but in
- # fact all the test does is pipe the output to jq
+ # fact all the test does is pipe the output to (go)jq
PYTHONIOENCODING: utf-8
prepare: |
- snap install --edge jq
# prerequisite for having a prompts handler service
snap set system experimental.user-daemons=true
@@ -42,14 +41,17 @@ execute: |
snap connect api-client:snap-interfaces-requests-control
echo "Check snap can access interfaces-requests-prompt and interfaces-requests-rule-update notices under /v2/notices"
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | jq '."status-code"' | MATCH '^200$'
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | jq '."status-code"' | MATCH '^200$'
- api-client --socket /run/snapd-snap.socket "/v2/notices" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | \
+ gojq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | \
+ gojq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices" | gojq '."status-code"' | MATCH '^200$'
echo "But not other notice types"
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | jq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | \
+ gojq '."status-code"' | MATCH '^403$'
echo "Check snap can access system info via /v2/system-info"
- api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/system-info" | gojq '."status-code"' | MATCH '^200$'
SNAP_NAME="snapd"
if os.query is-core16; then
@@ -57,7 +59,7 @@ execute: |
fi
echo "Check snap can access snap info via /v2/snaps/{name}"
- api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | gojq '."status-code"' | MATCH '^200$'
echo "Ensure AppArmor Prompting experimental feature can be enabled where possible"
# Prompting is unsupported everywhere but the Ubuntu non-core systems with
@@ -79,10 +81,12 @@ execute: |
snap set system experimental.apparmor-prompting=true
echo 'Check "apparmor-prompting" is shown as enabled in /v2/system-info'
- api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."result"."features"."apparmor-prompting"."enabled"' | MATCH '^true$'
+ api-client --socket /run/snapd-snap.socket "/v2/system-info" | \
+ gojq '."result"."features"."apparmor-prompting"."enabled"' | MATCH '^true$'
EXPECTED_HTTP_CODE="200"
- if api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."result"."features"."apparmor-prompting"."supported"' | MATCH '^false$' ; then
+ if api-client --socket /run/snapd-snap.socket "/v2/system-info" | \
+ gojq '."result"."features"."apparmor-prompting"."supported"' | MATCH '^false$' ; then
# AppArmor prompting isn't supported, so rules and prompts backends are
# not active, and will return InternalError (500). We can at least check
# that we receive an InternalError instead of Forbidden (403).
@@ -91,29 +95,36 @@ execute: |
fi
echo "Check snap can access prompts via /v2/interfaces/requests/prompts"
- api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
+ api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | \
+ gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
# echo "Check snap can access a single prompt via /v2/interfaces/requests/prompts/"
# TODO: include the "home" interface and create a request prompt by attempting to list contents of $HOME
# PROMPT_ID=FIXME
- # api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/$PROMPT_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
+ # api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/$PROMPT_ID" | \
+ # gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
# echo "Check snap can reply to a prompt via /v2/interfaces/requests/prompts/
- # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"allow","lifespan":"forever","constraints":{"path-pattern":"/**","permissions":["read"]}}' "/v2/interfaces/requests/prompts/$PROMPT_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
+ # TODO: split this line more
+ # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"allow","lifespan":"forever","constraints":{"path-pattern":"/**","permissions":["read"]}}' "/v2/interfaces/requests/prompts/$PROMPT_ID" | \
+ # gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
# TODO: check that thread which triggered request completed successfully
echo "Check snap can access rules via /v2/interfaces/requests/rules"
- api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
+ api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | \
+ gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
# XXX: creating rules requires polkit authentication, so for now, use snap debug api instead of api-client
# echo "Check snap can create rule via /v2/interfaces/requests/rules"
# api-client --socket /run/snapd-snap.socket --method=POST '{"action":"add","rule":{"snap":"api-client","interface":"home","constraints":{"path-pattern":"/path/to/file","permissions":["read","write","execute"]},"outcome":"allow","lifespan":"forever"}}' "/v2/interfaces/requests/rules" > result.json
- echo '{"action":"add","rule":{"snap":"api-client","interface":"home","constraints":{"path-pattern":"/path/to/file","permissions":["read","write","execute"]},"outcome":"allow","lifespan":"forever"}}' | snap debug api -X POST -H 'Content-Type: application/json' "/v2/interfaces/requests/rules" | tee result.json
- jq '."status-code"' < result.json | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
- RULE_ID=$(jq '."result"."id"' < result.json | tr -d '"')
+ echo '{"action":"add","rule":{"snap":"api-client","interface":"home","constraints":{"path-pattern":"/path/to/file","permissions":["read","write","execute"]},"outcome":"allow","lifespan":"forever"}}' | snap debug api -X POST -H 'Content-Type: application/json' "/v2/interfaces/requests/rules" | \
+ tee result.json
+ gojq '."status-code"' < result.json | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
+ RULE_ID=$(gojq '."result"."id"' < result.json | tr -d '"')
echo "Check snap can view a single rule via /v2/interfaces/requests/rules/"
- api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
+ api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | \
+ gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
# XXX: modifying rules requires polkit authentication
# echo "Check snap can modify a single rule via /v2/interfaces/requests/rules/"
- # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"remove"}' "/v2/interfaces/requests/rules/$RULE_ID" | jq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
+ # api-client --socket /run/snapd-snap.socket --method=POST '{"action":"remove"}' "/v2/interfaces/requests/rules/$RULE_ID" | gojq '."status-code"' | MATCH '^'"$EXPECTED_HTTP_CODE"'$'
echo "Without snap-interfaces-requests-control the snap cannot access those API endpoints"
snap disconnect api-client:snap-interfaces-requests-control
@@ -122,12 +133,17 @@ execute: |
# the prerequisite of there being a snap with snap-interfaces-requests-control
# connected and a handler service running is no longer true. Otherwise, the error
# code would be 500 instead of 403.
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/system-info" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | jq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-prompt" | \
+ gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=interfaces-requests-rule-update" | \
+ gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/system-info" | gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/snaps/$SNAP_NAME" | gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts" | \
+ gojq '."status-code"' | MATCH '^403$'
# Try to access an arbitrary prompt ID, should fail with 403 rather than 404
- api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/1234123412341234" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | jq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/prompts/1234123412341234" | \
+ gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules" | gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/interfaces/requests/rules/$RULE_ID" | \
+ gojq '."status-code"' | MATCH '^403$'
diff --git a/tests/main/interfaces-snap-refresh-observe/task.yaml b/tests/main/interfaces-snap-refresh-observe/task.yaml
index 2f311d96f32..7e801157441 100644
--- a/tests/main/interfaces-snap-refresh-observe/task.yaml
+++ b/tests/main/interfaces-snap-refresh-observe/task.yaml
@@ -12,12 +12,9 @@ details: |
environment:
# not all terminals support UTF-8, but Python tries to be smart and attempts
# to guess the encoding as if the output would go to the terminal, but in
- # fact all the test does is pipe the output to jq
+ # fact all the test does is pipe the output to (go)jq
PYTHONIOENCODING: utf-8
-prepare: |
- snap install --edge jq
-
execute: |
"$TESTSTOOLS"/snaps-state install-local api-client
echo "The snap-refresh-observe plug on the api-client snap is initially disconnected"
@@ -26,29 +23,29 @@ execute: |
snap connect api-client:snap-refresh-observe
echo "Check snap can access change-update and refresh-inhibit notices under /v2/notices"
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | jq '."status-code"' | MATCH '^200$'
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=refresh-inhibit" | jq '."status-code"' | MATCH '^200$'
- api-client --socket /run/snapd-snap.socket "/v2/notices" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | gojq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=refresh-inhibit" | gojq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices" | gojq '."status-code"' | MATCH '^200$'
echo "But not other notice types"
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | jq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update,warning" | gojq '."status-code"' | MATCH '^403$'
echo "Check snap can access changes /v2/changes"
- api-client --socket /run/snapd-snap.socket "/v2/changes" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/changes" | gojq '."status-code"' | MATCH '^200$'
echo "Check snap can access a single change /v2/changes/"
CHANGE_ID=$(snap changes | tr -s '\n' | awk 'END{ print $1 }')
- api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | gojq '."status-code"' | MATCH '^200$'
# TODO: Check it can only access /v2/snaps?select=refresh-inhibited
echo "Check snap can access snaps /v2/snaps"
- api-client --socket /run/snapd-snap.socket "/v2/snaps" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/snaps" | gojq '."status-code"' | MATCH '^200$'
echo "And also a specific snap /v2/snaps/"
- api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | jq '."status-code"' | MATCH '^200$'
+ api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | gojq '."status-code"' | MATCH '^200$'
echo "Without snap-refresh-observe the snap cannot access those API endpoints"
snap disconnect api-client:snap-refresh-observe
- api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/changes" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/snaps" | jq '."status-code"' | MATCH '^403$'
- api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | jq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/notices?types=change-update" | gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/changes" | gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/changes/$CHANGE_ID" | gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/snaps" | gojq '."status-code"' | MATCH '^403$'
+ api-client --socket /run/snapd-snap.socket "/v2/snaps/api-client" | gojq '."status-code"' | MATCH '^403$'
diff --git a/tests/main/microk8s-smoke/task.yaml b/tests/main/microk8s-smoke/task.yaml
index 1b5aa6a5a41..77247279768 100644
--- a/tests/main/microk8s-smoke/task.yaml
+++ b/tests/main/microk8s-smoke/task.yaml
@@ -13,9 +13,8 @@ systems:
- -amazon-linux-2-* # fails to start service daemon-containerd
- -amazon-linux-2023-* # fails to start service daemon-containerd
- -centos-9-* # fails to start service daemon-containerd
- - -fedora-38-* # fails to start service daemon-containerd
- - -fedora-39-* # fails to start service daemon-containerd
- -fedora-40-* # fails to start service daemon-containerd
+ - -fedora-41-* # fails to start service daemon-containerd
- -ubuntu-14.04-* # doesn't have libseccomp >= 2.4
- -arch-linux-* # XXX: no curl to the pod for unknown reasons
- -ubuntu-*-arm* # not available on arm
diff --git a/tests/main/mounts-persist-refresh-content-snap/task.yaml b/tests/main/mounts-persist-refresh-content-snap/task.yaml
index 2fc2ba1f712..5b96e6ed5d2 100644
--- a/tests/main/mounts-persist-refresh-content-snap/task.yaml
+++ b/tests/main/mounts-persist-refresh-content-snap/task.yaml
@@ -18,7 +18,8 @@ kill-timeout: 10m
prepare: |
# make a font directory and restart snapd so it will see it when it goes to
# connect the desktop interface for the snap
- mkdir /usr/share/fonts/foo-font
+ mkdir -p /usr/share/fonts/foo-font
+ tests.cleanup defer rmdir /usr/share/fonts/foo-font
systemctl restart snapd
# install a snap which exposes some files via a content slot
@@ -35,26 +36,28 @@ prepare: |
tests.cleanup defer tests.session -u test restore
execute: |
- touch /run/keep-running
- # read a file continuously in the background until it fails - note that the
- # while loop here has to be inside the snap run shell, since the process must
- # persist during the refresh, if it is on the outside the mount namespace will
- # be rebuilt and the crash will not be reproduced
- tests.session -u test exec snap run --shell test-snapd-desktop-layout-with-content.cmd -c 'while test -f /run/keep-running && test -d /usr/share/fonts/foo-font; do true; done' &
+ # Construct the mount namespace once so that we are not racing construction
+ # from background job of snap run with update with snap install of the
+ # test-snapd-content-slot below.
+ test-snapd-desktop-layout-with-content.sh -c 'true'
+ # Read a file continuously in the background until it fails. Note that we are
+ # not using a service but a regular app running in the background since the
+ # process must persist during the refresh.
+ tests.session -u test exec test-snapd-desktop-layout-with-content.crash-foo-font &
pid=$!
- # refresh the content slot snap
+ # Wait for the script to start.
+ retry grep -xF 'started' ~test/snap/test-snapd-desktop-layout-with-content/common/status
+
+ # Refresh the content slot snap.
# TODO: when refresh app awareness is enabled, this will need to ignore running processes to
# check the behavior
"$TESTSTOOLS"/snaps-state install-local test-snapd-content-slot
- # ensure the process is still running
- if not ps -p "$pid"; then
- echo "process died, test failed"
- exit 1
- fi
-
- # signal to kill the loop
- rm /run/keep-running
+ # Signal to kill the loop
+ rm ~test/snap/test-snapd-desktop-layout-with-content/common/keep-running
+ wait "$pid"
- wait "$pid" || true
+ # Ensure that /usr/share/fonts/foo-font was never missing.
+ MATCH 'exited' ~test/snap/test-snapd-desktop-layout-with-content/common/status
+ NOMATCH 'foo-font missing' ~test/snap/test-snapd-desktop-layout-with-content/common/status
diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin.sh b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin.sh
deleted file mode 100755
index 901d49b98c7..00000000000
--- a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-/bin/bash "$@"
diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/crash-foo-font b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/crash-foo-font
new file mode 100755
index 00000000000..b4ca932de54
--- /dev/null
+++ b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/crash-foo-font
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+touch "$SNAP_USER_COMMON"/keep-running
+echo "started" > "$SNAP_USER_COMMON"/status
+while test -f "$SNAP_USER_COMMON"/keep-running; do
+ if ! [ -d /usr/share/fonts/foo-font ] ; then
+ echo "foo-font missing" >> "$SNAP_USER_COMMON"/status
+ exit 1
+ fi
+done
+echo "exited" >> "$SNAP_USER_COMMON"/status
+
diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/sh b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/sh
new file mode 100755
index 00000000000..0f845e07c5a
--- /dev/null
+++ b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/bin/sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+PS1='$ '
+exec /bin/sh "$@"
diff --git a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml
index d45a2e71520..53cfdc5a2eb 100644
--- a/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml
+++ b/tests/main/mounts-persist-refresh-content-snap/test-snapd-desktop-layout-with-content/meta/snap.yaml
@@ -2,8 +2,10 @@ name: test-snapd-desktop-layout-with-content
version: 0.1
apps:
- cmd:
- cmd: bin.sh
+ sh:
+ command: bin/sh
+ crash-foo-font:
+ command: bin/crash-foo-font
plugs:
desktop:
diff --git a/tests/main/quota-groups-systemd-accounting/task.yaml b/tests/main/quota-groups-systemd-accounting/task.yaml
index a18e01a0877..5dda4364f44 100644
--- a/tests/main/quota-groups-systemd-accounting/task.yaml
+++ b/tests/main/quota-groups-systemd-accounting/task.yaml
@@ -22,7 +22,7 @@ systems:
- -ubuntu-core-*-arm-*
prepare: |
- snap install hello-world go-example-webserver remarshal jq
+ snap install hello-world go-example-webserver
execute: |
# the bug mainly happens when we create a quota group with nothing in it,
@@ -48,12 +48,12 @@ execute: |
# that systemd returns
# TODO: change this to use the no unit flag when that is a thing so we can
# compare to actual value
- snap quota sub | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB"
- snap quota top | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB"
+ snap quota sub | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB"
+ snap quota top | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB"
# now trigger the bug by removing the sub21 group
snap remove-quota sub21
# usage should still be sensible
- snap quota sub | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB"
- snap quota top | yaml2json | jq -r '.current.memory' | NOMATCH "18.4EB"
+ snap quota sub | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB"
+ snap quota top | gojq --yaml-input -r '.current.memory' | NOMATCH "18.4EB"
diff --git a/tests/main/services-socket-activation/task.yaml b/tests/main/services-socket-activation/task.yaml
index 8508be36e67..5440e5559de 100644
--- a/tests/main/services-socket-activation/task.yaml
+++ b/tests/main/services-socket-activation/task.yaml
@@ -13,56 +13,68 @@ restore: |
execute: |
[ -f /etc/systemd/system/snap.socket-activation.sleep-daemon.sock.socket ]
[ -S /var/snap/socket-activation/common/socket ]
+
+ verify_status() {
+ local ENABLED="$1"
+ local MAIN_ACTIVE="$2"
+ local ACT_ACTIVE="$3"
+
+ echo "Checking that services are listed correctly"
+ snap services | cat -n > svcs.txt
+ MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt
+ MATCH " 2\s+socket-activation.sleep-daemon\s+${ENABLED}\s+${MAIN_ACTIVE}\s+socket-activated$" < svcs.txt
+
+ echo "Check that systemctl for the main unit is as expected"
+ systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.service | grep "static"
+ systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.service | grep "ActiveState=${MAIN_ACTIVE}"
+
+ echo "Check that systemctl for the socket is looking correct too"
+ systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "${ENABLED}"
+ systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=${ACT_ACTIVE}"
+ }
+
+ # verify default behavior on install is that the main service
+ # is inactive but enabled, and socket is active
+ verify_status "enabled" "inactive" "active"
+
+ # this will fail, but still start the service
+ echo "Start the primary unit, emulate that the trigger has run"
+ systemctl start snap.socket-activation.sleep-daemon.service
+
+ # verify that the main service is now active
+ verify_status "enabled" "active" "active"
+
+ # test normal restart
+ snap restart socket-activation
- echo "Checking that services are listed correctly"
- snap services | cat -n > svcs.txt
- MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt
- MATCH " 2\s+socket-activation.sleep-daemon\s+enabled\s+inactive\s+socket-activated$" < svcs.txt
+ verify_status "enabled" "active" "active"
- echo "Checking that the service is reported as static"
- systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.service | grep "static"
+ # test --reload restart, with --reload we expect different behavior
+ # because of systemd. Verify that systemd is acting like we expect
+ # as well
+ snap restart --reload socket-activation
- echo "Checking that service activation unit is reported as enabled and running"
- systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "enabled"
- systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=active"
+ verify_status "enabled" "active" "active"
+
+ systemctl reload-or-restart snap.socket-activation.sleep-daemon.sock.socket 2>&1 | MATCH "failed"
echo "Testing that we can stop will not disable the service"
snap stop socket-activation.sleep-daemon
- systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "enabled"
- systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=inactive"
+
+ verify_status "enabled" "inactive" "inactive"
echo "Testing that we can correctly disable activations"
snap stop --disable socket-activation.sleep-daemon
echo "Verifying that service is now listed as disabled"
- snap services | cat -n > svcs.txt
- MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt
- MATCH " 2\s+socket-activation.sleep-daemon\s+disabled\s+inactive\s+socket-activated$" < svcs.txt
-
- echo "Checking that service activation unit is reported as disabled and inactive"
- systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "disabled"
- systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=inactive"
+ verify_status "disabled" "inactive" "inactive"
echo "Starting the service will start the socket unit, but not enable"
snap start socket-activation.sleep-daemon
-
- echo "Checking that services are listed as expected"
- snap services | cat -n > svcs.txt
- MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt
- MATCH " 2\s+socket-activation.sleep-daemon\s+disabled\s+inactive\s+socket-activated$" < svcs.txt
-
- echo "Checking that service activation unit is reported as disabled and active"
- systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "disabled"
- systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=active"
+
+ verify_status "disabled" "inactive" "active"
echo "Enable service and verify its listed as enabled"
snap start --enable socket-activation.sleep-daemon
- echo "Checking that services are listed correctly"
- snap services | cat -n > svcs.txt
- MATCH " 1\s+Service\s+Startup\s+Current\s+Notes$" < svcs.txt
- MATCH " 2\s+socket-activation.sleep-daemon\s+enabled\s+inactive\s+socket-activated$" < svcs.txt
-
- echo "Checking that service activation unit is reported as enabled and active again"
- systemctl show --property=UnitFileState snap.socket-activation.sleep-daemon.sock.socket | grep "enabled"
- systemctl show --property=ActiveState snap.socket-activation.sleep-daemon.sock.socket | grep "ActiveState=active"
+ verify_status "enabled" "inactive" "active"
diff --git a/tests/main/set-proxy-store/task.yaml b/tests/main/set-proxy-store/task.yaml
index 182d18f7312..980379b331f 100644
--- a/tests/main/set-proxy-store/task.yaml
+++ b/tests/main/set-proxy-store/task.yaml
@@ -65,10 +65,10 @@ execute: |
systemctl stop snapd snapd.socket
- jq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|MATCH null
+ gojq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|MATCH null
# XXX the fakestore currently does not support faking session creation
- jq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
systemctl start snapd.socket
@@ -81,14 +81,14 @@ execute: |
snap refresh --list | not grep -Pzq "$expected"
echo "Ensure changing the proxy.store will clear out the session-macaroon"
- jq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|NOMATCH fake-session
+ gojq '.data.auth.device."session-macaroon"' /var/lib/snapd/state.json|NOMATCH fake-session
echo "Configure back to use fakestore"
snap set core proxy.store=fake
# XXX the fakestore currently does not support faking session creation
systemctl stop snapd snapd.socket
- jq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq '.data.auth.device."session-macaroon"="fake-session"' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
systemctl start snapd.socket
diff --git a/tests/main/snap-disconnect/task.yaml b/tests/main/snap-disconnect/task.yaml
index 6e2e74d812e..a8423c2ca9a 100644
--- a/tests/main/snap-disconnect/task.yaml
+++ b/tests/main/snap-disconnect/task.yaml
@@ -13,13 +13,12 @@ prepare: |
echo "Install a test snap"
snap pack "$TESTSLIB"/snaps/home-consumer
snap install --dangerous "$SNAP_FILE"
- snap install --edge jq
execute: |
inspect_connection() {
CONN="$1"
- # shellcheck disable=SC2002
- cat /var/lib/snapd/state.json | jq --arg CONN "$CONN" -r '.data["conns"] | has($CONN)'
+ # shellcheck disable=SC2002,SC2016
+ cat /var/lib/snapd/state.json | gojq --arg CONN "$CONN" -r '.data["conns"] | has($CONN)'
}
DISCONNECTED_PATTERN='-\s+home-consumer:home'
diff --git a/tests/main/snap-get/task.yaml b/tests/main/snap-get/task.yaml
index 56a1dbe5472..18c8784cfc9 100644
--- a/tests/main/snap-get/task.yaml
+++ b/tests/main/snap-get/task.yaml
@@ -10,8 +10,6 @@ details: |
the json output format and error scenarios.
prepare: |
- snap install --devmode jq
-
echo "Build basic test package (without hooks)"
snap pack "$TESTSLIB"/snaps/basic
snap install --dangerous basic_1.0_all.snap
@@ -105,7 +103,7 @@ execute: |
echo "$output" | MATCH ".*\"a\": 9876543210.*"
echo "Ensure config value has correct format"
- jq ".data[\"config\"][\"snapctl-hooks\"].intnumber" /var/lib/snapd/state.json | MATCH "1234567890"
+ gojq ".data[\"config\"][\"snapctl-hooks\"].intnumber" /var/lib/snapd/state.json | MATCH "1234567890"
echo "Test unsetting of root.key2 with exclamation mark via snapctl"
# precondition check
diff --git a/tests/main/snap-quota-memory/task.yaml b/tests/main/snap-quota-memory/task.yaml
index 51a7f5acf10..a6f7450d484 100644
--- a/tests/main/snap-quota-memory/task.yaml
+++ b/tests/main/snap-quota-memory/task.yaml
@@ -18,7 +18,7 @@ systems:
- -ubuntu-core-*-arm-*
prepare: |
- snap install go-example-webserver jq remarshal hello-world
+ snap install go-example-webserver hello-world
execute: |
echo "Create a group with a snap in it"
@@ -80,7 +80,7 @@ execute: |
exit 1
fi
- snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-one | jq -r '.result.current.memory')"
+ snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-one | gojq -r '.result.current.memory')"
kernelSaysMemUsage="$(cat "$cgroupMemFile")"
pyCmd="import math; print(math.ceil(abs($snapdSaysMemUsage - $kernelSaysMemUsage) / $snapdSaysMemUsage * 100))"
@@ -163,9 +163,9 @@ execute: |
echo "Removing a snap ensures that the snap is not in the quota group anymore"
snap set-quota group-three --memory=100MB go-example-webserver
- snap quota group-three | yaml2json | jq -r '.snaps | .[]' | MATCH go-example-webserver
+ snap quota group-three | gojq --yaml-input -r '.snaps | .[]' | MATCH go-example-webserver
snap remove go-example-webserver
- snap quota group-three | yaml2json | jq -r '.snaps' | MATCH null
+ snap quota group-three | gojq --yaml-input -r '.snaps' | MATCH null
snap remove-quota group-three
echo "Creating a quota group with no actual services in it still has logical memory usage reported for it"
@@ -175,7 +175,7 @@ execute: |
# in reporting it's memory usage on old systemd versions
snap set-quota group-five --memory=10MB --parent=group-four hello-world
- snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-five | jq -r '.result.current.memory')"
+ snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-five | gojq -r '.result.current.memory')"
# both 0 and up to 12KiB values are expected here, 0 is for older systemd/kernels
# where an empty cgroup has exactly 0, but on newer systems there is some
# minimum amount of accounting memory for an empty cgroup, which is observed
@@ -189,7 +189,7 @@ execute: |
exit 1
esac
- snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-four | jq -r '.result.current.memory')"
+ snapdSaysMemUsage="$(sudo snap debug api /v2/quotas/group-four | gojq -r '.result.current.memory')"
case "$snapdSaysMemUsage" in
null|0|4096|8192|12288)
# expected
diff --git a/tests/main/snap-seccomp-syscalls/task.yaml b/tests/main/snap-seccomp-syscalls/task.yaml
index fd19bbf4a14..1b5a1d7e492 100644
--- a/tests/main/snap-seccomp-syscalls/task.yaml
+++ b/tests/main/snap-seccomp-syscalls/task.yaml
@@ -8,7 +8,7 @@ details: |
more details.
# one system is enough
-systems: [ubuntu-18.04-64]
+systems: [ubuntu-24.04-64]
# Start early as it takes a long time.
priority: 100
diff --git a/tests/main/snapctl-from-snap/task.yaml b/tests/main/snapctl-from-snap/task.yaml
index 15fd2885213..32a4965bd61 100644
--- a/tests/main/snapctl-from-snap/task.yaml
+++ b/tests/main/snapctl-from-snap/task.yaml
@@ -12,13 +12,12 @@ environment:
SNAP/wcore18: snapctl-from-snap-core18
prepare: |
- snap install --devmode jq
echo "Build basic test package"
snap pack snapctl-from-snap
execute: |
check_single_cookie() {
- cnt=$(jq -r '.data["snap-cookies"]' /var/lib/snapd/state.json | grep -c "$1" || true)
+ cnt=$(gojq -r '.data["snap-cookies"]' /var/lib/snapd/state.json | grep -c "$1" || true)
if [ "$cnt" -ne 1 ]; then
echo "Expected single cookie for snap $1, found $cnt"
exit 1
@@ -61,7 +60,7 @@ execute: |
echo "Simulate upgrade from old snapd with no cookie support"
systemctl stop snapd.{service,socket}
rm -f "$COOKIE_FILE"
- jq -c 'del(.data["snap-cookies"])' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq -c 'del(.data["snap-cookies"])' /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
systemctl start snapd.{service,socket}
diff --git a/tests/main/snapd-reexec/task.yaml b/tests/main/snapd-reexec/task.yaml
index 76a9bb2d596..8e0c5b81f69 100644
--- a/tests/main/snapd-reexec/task.yaml
+++ b/tests/main/snapd-reexec/task.yaml
@@ -27,7 +27,7 @@ prepare: |
# without .data.snaps.snapd.sequence and .data.snaps.snapd.current="unset"
# snapd is not considered installed and core install will request restart
cp -a /var/lib/snapd/state.json /tmp/backup_state.json
- jq 'del(.data.snaps.snapd)' /tmp/backup_state.json > /tmp/modified_state.json
+ gojq 'del(.data.snaps.snapd)' /tmp/backup_state.json > /tmp/modified_state.json
cp /tmp/modified_state.json /var/lib/snapd/state.json && rm /tmp/modified_state.json
systemctl start snapd.service
fi
diff --git a/tests/main/snapshot-exclusions-dynamic/task.yaml b/tests/main/snapshot-exclusions-dynamic/task.yaml
index ed7ad471d13..4832db10d28 100644
--- a/tests/main/snapshot-exclusions-dynamic/task.yaml
+++ b/tests/main/snapshot-exclusions-dynamic/task.yaml
@@ -16,7 +16,6 @@ environment:
prepare: |
"$TESTSTOOLS"/snaps-state install-local test-snap
- snap install jq
debug: |
snap saved || true
@@ -52,9 +51,9 @@ execute: |
# Create snapshot that will apply dynamic exclusions, and grab the set ID
#shellcheck disable=SC2016
RESPONSE=$( echo '{"action": "snapshot", "snaps": ["test-snap"], "snapshot-options": {"test-snap":{"exclude":["$SNAP_DATA/dynamic-exclude.txt", "$SNAP_COMMON/dynamic-exclude.txt", "$SNAP_USER_COMMON/dynamic-exclude.txt", "$SNAP_USER_DATA/dynamic-exclude.txt"]}}}' | timeout 5 snap debug api -X POST -H 'Content-Type: application/json' /v2/snaps )
- SET_ID=$( echo "$RESPONSE" | jq '.result."set-id"' )
+ SET_ID=$( echo "$RESPONSE" | gojq '.result."set-id"' )
echo "$SET_ID" | MATCH "^[0-9]+$"
- CHANGE=$( echo "$RESPONSE" | jq ".change" | grep -o "[0-9]*" )
+ CHANGE=$( echo "$RESPONSE" | gojq ".change" | grep -o "[0-9]*" )
# Wait for completion of async change
retry -n 20 sh -c "snap change \"$CHANGE\" | tail -n2 | MATCH \"Done\".*"
@@ -78,7 +77,7 @@ execute: |
"\$SNAP_USER_COMMON/dynamic-exclude.txt"
"\$SNAP_USER_DATA/dynamic-exclude.txt"
EOF
- timeout 5 snap debug api /v2/snapshots?set="$SET_ID" | jq .result[0].snapshots[0].options.exclude[] > actual_options_entry
+ timeout 5 snap debug api /v2/snapshots?set="$SET_ID" | gojq .result[0].snapshots[0].options.exclude[] > actual_options_entry
diff -u expected_options_entry actual_options_entry
# Remove the canaries to test restore
diff --git a/tests/main/store-state/snap/meta/snap.yaml.in b/tests/main/store-state/snap/meta/snap.yaml.in
index 3bea62b9d08..e71c9488903 100644
--- a/tests/main/store-state/snap/meta/snap.yaml.in
+++ b/tests/main/store-state/snap/meta/snap.yaml.in
@@ -1,6 +1,7 @@
name: SNAPNAME
summary: generic snap
version: '1.0'
+base: core22
apps:
test-app:
diff --git a/tests/main/store-state/task.yaml b/tests/main/store-state/task.yaml
index dd61ccf3bfa..f847ae17d93 100644
--- a/tests/main/store-state/task.yaml
+++ b/tests/main/store-state/task.yaml
@@ -12,15 +12,8 @@ backends: [-external]
systems: [-ubuntu-14.04-64]
prepare: |
- echo "Ensure jq is installed"
- if ! command -v jq; then
- snap install --devmode jq
- fi
-
- echo "Ensure yaml2json is installed"
- if ! command -v yaml2json; then
- snap install --devmode remarshal
- fi
+ # acquire session macaroon
+ snap find core
execute: |
# Check help
@@ -40,6 +33,10 @@ execute: |
snap info core | MATCH "store-url:.*https://snapcraft.io"
fi
+ # install test snap dependency before switching to fake store
+ base_dep="$(gojq -r --yaml-input '.base' < snap/meta/snap.yaml.in)"
+ snap install "$base_dep"
+
# Setup fakestore
STORE_DIR="$(pwd)/fake-store-blobdir"
snap ack "$TESTSLIB/assertions/testrootorg-store.account-key"
@@ -50,8 +47,8 @@ execute: |
# Check make-snap-installable command with snap-id
create_snap() {
- yaml2json -i snap/meta/snap.yaml.in > snap/meta/snap.json
- jq ".name = \"$1\"" snap/meta/snap.json | json2yaml -o snap/meta/snap.yaml
+ gojq --yaml-input --yaml-output \
+ ".name = \"$1\"" snap/meta/snap.yaml.in > snap/meta/snap.yaml
"$TESTSTOOLS"/snaps-state pack-local snap
}
diff --git a/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in b/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in
index d40315d6245..eb83a56ce73 100644
--- a/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in
+++ b/tests/main/system-usernames-snap-scoped/snap/meta/snap.yaml.in
@@ -1,6 +1,7 @@
name: SNAPNAME
summary: Snap requesting snap-scoped system users
version: '1.0'
+base: core22
apps:
test-app:
diff --git a/tests/main/system-usernames-snap-scoped/task.yaml b/tests/main/system-usernames-snap-scoped/task.yaml
index d395ccd2673..cc3e46bf33d 100644
--- a/tests/main/system-usernames-snap-scoped/task.yaml
+++ b/tests/main/system-usernames-snap-scoped/task.yaml
@@ -34,18 +34,12 @@ prepare: |
exit
fi
- echo "Ensure jq is installed"
- if ! command -v jq; then
- snap install --devmode jq
- fi
-
- echo "Ensure yaml2json is installed"
- if ! command -v yaml2json; then
- snap install --devmode remarshal
- fi
-
snap debug can-manage-refreshes | MATCH false
+ # install test snap dependencies before switching to fake store
+ base_dep="$(gojq -r --yaml-input '.base' < snap/meta/snap.yaml.in)"
+ snap install "$base_dep"
+
snap ack "$TESTSLIB/assertions/testrootorg-store.account-key"
"$TESTSTOOLS"/store-state setup-fake-store "$STORE_DIR"
@@ -57,13 +51,14 @@ prepare: |
snap ack "$TESTSLIB/assertions/developer1.account-key"
create_snap() {
- yaml2json -i snap/meta/snap.yaml.in > snap/meta/snap.json
+ gojq --yaml-input --yaml-output ".name = \"$1\"" < snap/meta/snap.yaml.in > snap/meta/snap.yaml
for user in $TESTED_USERS
do
- jq ".\"system-usernames\" += { \"$user\" : \"shared\"}" snap/meta/snap.json > snap/meta/snap.json.tmp
- mv snap/meta/snap.json.tmp snap/meta/snap.json
+ gojq --yaml-input --yaml-output \
+ ".\"system-usernames\" += { \"$user\" : \"shared\"}" \
+ snap/meta/snap.yaml > snap/meta/snap.yaml.tmp
+ mv snap/meta/snap.yaml.tmp snap/meta/snap.yaml
done
- jq ".name = \"$1\"" snap/meta/snap.json | json2yaml -o snap/meta/snap.yaml
"$TESTSTOOLS"/snaps-state pack-local snap
}
diff --git a/tests/main/theme-install/task.yaml b/tests/main/theme-install/task.yaml
index c1baaf08a30..14e0f14672f 100644
--- a/tests/main/theme-install/task.yaml
+++ b/tests/main/theme-install/task.yaml
@@ -16,12 +16,9 @@ details: |
environment:
# not all terminals support UTF-8, but Python tries to be smart and attempts
# to guess the encoding as if the output would go to the terminal, but in
- # fact all the test does is pipe the output to jq
+ # fact all the test does is pipe the output to gojq
PYTHONIOENCODING: utf-8
-prepare: |
- snap install --edge jq
-
execute: |
"$TESTSTOOLS"/snaps-state install-local api-client
echo "The snapd*-control plugs on the api-client snap are initially disconnected"
@@ -35,21 +32,21 @@ execute: |
echo "Check for presence of a collection of themes"
api-client '/v2/accessories/themes?gtk-theme=Yaru>k-theme=TraditionalHumanized&icon-theme=Yaru&icon-theme=Adwaita&sound-theme=Yaru&sound-theme=No-Such-Theme' > response.txt
- jq . < response.txt
+ gojq . < response.txt
- jq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed'
- jq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^available'
- jq -r '.result."icon-themes".Yaru' < response.txt | MATCH '^installed'
- jq -r '.result."icon-themes".Adwaita' < response.txt | MATCH '^installed'
- jq -r '.result."sound-themes".Yaru' < response.txt | MATCH '^installed'
- jq -r '.result."sound-themes"."No-Such-Theme"' < response.txt | MATCH '^unavailable'
+ gojq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed'
+ gojq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^available'
+ gojq -r '.result."icon-themes".Yaru' < response.txt | MATCH '^installed'
+ gojq -r '.result."icon-themes".Adwaita' < response.txt | MATCH '^installed'
+ gojq -r '.result."sound-themes".Yaru' < response.txt | MATCH '^installed'
+ gojq -r '.result."sound-themes"."No-Such-Theme"' < response.txt | MATCH '^unavailable'
echo "We can request installation of a snap to satisfy a theme"
api-client --method=POST /v2/accessories/themes '{"gtk-themes":["TraditionalHumanized"]}' > response.txt
- jq . < response.txt
+ gojq . < response.txt
echo "Wait for change to complete"
- change_id="$(jq -r .change < response.txt)"
+ change_id="$(gojq -r .change < response.txt)"
snap watch "$change_id"
echo "The snap providing the theme is now installed"
@@ -57,23 +54,23 @@ execute: |
echo "The theme now reports as installed"
api-client '/v2/accessories/themes?gtk-theme=TraditionalHumanized' > response.txt
- jq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^installed'
+ gojq -r '.result."gtk-themes".TraditionalHumanized' < response.txt | MATCH '^installed'
echo "The API is also available to snaps via snapd-snap.socket, provided they have snap-themes-control plugged"
snap disconnect api-client:snapd-control
not api-client --socket /run/snapd-snap.socket '/v2/accessories/themes?gtk-theme=Yaru' > response.txt
- jq -r '."status-code"' < response.txt | MATCH '^403$'
+ gojq -r '."status-code"' < response.txt | MATCH '^403$'
snap connect api-client:snap-themes-control
api-client --socket /run/snapd-snap.socket '/v2/accessories/themes?gtk-theme=Yaru' > response.txt
- jq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed'
+ gojq -r '.result."gtk-themes".Yaru' < response.txt | MATCH '^installed'
echo "POST requests are also accepted on snapd-snap.socket"
not api-client --socket /run/snapd-snap.socket --method=POST /v2/accessories/themes '{"gtk-themes":["TraditionalHumanized"]}' > response.txt
- jq -r '.result.message' < response.txt | MATCH '^no snaps to install'
+ gojq -r '.result.message' < response.txt | MATCH '^no snaps to install'
echo "Information about install-themes changes can also be accessed"
api-client --socket /run/snapd-snap.socket "/v2/accessories/changes/$change_id" > response.txt
- jq -r .result.status < response.txt | MATCH '^Done$'
- jq -r .result.kind < response.txt | MATCH '^install-themes$'
+ gojq -r .result.status < response.txt | MATCH '^Done$'
+ gojq -r .result.kind < response.txt | MATCH '^install-themes$'
diff --git a/tests/main/uc20-create-partitions-encrypt/task.yaml b/tests/main/uc20-create-partitions-encrypt/task.yaml
index 2eb81067518..681ae645560 100644
--- a/tests/main/uc20-create-partitions-encrypt/task.yaml
+++ b/tests/main/uc20-create-partitions-encrypt/task.yaml
@@ -306,75 +306,75 @@ execute: |
LOOP_BASENAME="$(basename "$LOOP")"
# disk things
- jq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 10000000000
- jq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512
- jq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME"
- jq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP"
- jq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt
- jq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5
- jq -r '.pc."structure-encryption" | length' < "$DISK_MAPPING_JSON" | MATCH 2
- jq -r '.pc."structure-encryption"."ubuntu-data" | length' < "$DISK_MAPPING_JSON" | MATCH 1
- jq -r '.pc."structure-encryption"."ubuntu-save" | length' < "$DISK_MAPPING_JSON" | MATCH 1
- jq -r '.pc."structure-encryption"."ubuntu-data"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS
- jq -r '.pc."structure-encryption"."ubuntu-save"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS
+ gojq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 10000000000
+ gojq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512
+ gojq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME"
+ gojq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP"
+ gojq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt
+ gojq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5
+ gojq -r '.pc."structure-encryption" | length' < "$DISK_MAPPING_JSON" | MATCH 2
+ gojq -r '.pc."structure-encryption"."ubuntu-data" | length' < "$DISK_MAPPING_JSON" | MATCH 1
+ gojq -r '.pc."structure-encryption"."ubuntu-save" | length' < "$DISK_MAPPING_JSON" | MATCH 1
+ gojq -r '.pc."structure-encryption"."ubuntu-data"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS
+ gojq -r '.pc."structure-encryption"."ubuntu-save"."method"' < "$DISK_MAPPING_JSON" | MATCH LUKS
# note: no partition "id" for gpt disks
# first structure - "BIOS Boot"
# note: no filesystem for the BIOS Boot structure
- jq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1"
- jq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1"
- jq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot"
- jq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576
- jq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576
+ gojq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1"
+ gojq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1"
+ gojq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot"
+ gojq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576
+ gojq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576
# second structure - ubuntu-seed
# TODO: for some reason udev does not identify ubuntu-seed as having a
# filesystem label, I think this has something to do with how we create it
# artificially above
- jq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2"
- jq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2"
- jq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed
- jq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152
- jq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200
+ gojq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2"
+ gojq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2"
+ gojq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed
+ gojq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152
+ gojq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200
# third structure - ubuntu-boot
- jq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3"
- jq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3"
- jq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
- jq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
- jq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352
- jq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000
+ gojq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3"
+ gojq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3"
+ gojq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
+ gojq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
+ gojq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352
+ gojq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000
# fourth structure - ubuntu-save-enc
- jq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4"
- jq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4"
- jq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save-enc
- jq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save
- jq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4"
+ gojq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4"
+ gojq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save-enc
+ gojq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save
+ gojq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH ""
if [ "$channel" = "20" ]; then
- jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
- jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216
+ gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
+ gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216
else
- jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
- jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432
+ gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
+ gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432
fi
# fifth structure - ubuntu-data-enc
- jq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5"
- jq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5"
- jq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data-enc
- jq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data
- jq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5"
+ gojq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5"
+ gojq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data-enc
+ gojq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data
+ gojq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH ""
if [ "$channel" = "20" ]; then
- jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568
- jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7936385536
+ gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568
+ gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7936385536
else
- jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784
- jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7919608320
+ gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784
+ gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 7919608320
fi
diff --git a/tests/main/uc20-create-partitions/task.yaml b/tests/main/uc20-create-partitions/task.yaml
index 4d84b709f6d..87013d05634 100644
--- a/tests/main/uc20-create-partitions/task.yaml
+++ b/tests/main/uc20-create-partitions/task.yaml
@@ -217,70 +217,70 @@ execute: |
LOOP_BASENAME="$(basename "$LOOP")"
# disk things
- jq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 20000000000
- jq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512
- jq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME"
- jq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP"
- jq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt
- jq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5
+ gojq -r '.pc.size' < "$DISK_MAPPING_JSON" | MATCH 20000000000
+ gojq -r '.pc."sector-size"' < "$DISK_MAPPING_JSON" | MATCH 512
+ gojq -r '.pc."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME"
+ gojq -r '.pc."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "$LOOP"
+ gojq -r '.pc.schema' < "$DISK_MAPPING_JSON" | MATCH gpt
+ gojq -r '.pc.structure | length' < "$DISK_MAPPING_JSON" | MATCH 5
# note: no partition "id" for gpt disks
# first structure - "BIOS Boot"
# note: no filesystem for the BIOS Boot structure
- jq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1"
- jq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1"
- jq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot"
- jq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576
- jq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576
+ gojq -r '.pc.structure[0]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p1"
+ gojq -r '.pc.structure[0]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p1"
+ gojq -r '.pc.structure[0]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[0]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH "BIOS\\\x20Boot"
+ gojq -r '.pc.structure[0].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[0].offset' < "$DISK_MAPPING_JSON" | MATCH 1048576
+ gojq -r '.pc.structure[0].size' < "$DISK_MAPPING_JSON" | MATCH 1048576
# second structure - ubuntu-seed
# TODO: for some reason udev does not identify ubuntu-seed as having a
# filesystem label, I think this has something to do with how we create it
# artificially above
- jq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2"
- jq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2"
- jq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed
- jq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152
- jq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200
+ gojq -r '.pc.structure[1]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p2"
+ gojq -r '.pc.structure[1]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p2"
+ gojq -r '.pc.structure[1]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[1]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-seed
+ gojq -r '.pc.structure[1].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[1].offset' < "$DISK_MAPPING_JSON" | MATCH 2097152
+ gojq -r '.pc.structure[1].size' < "$DISK_MAPPING_JSON" | MATCH 1258291200
# third structure - ubuntu-boot
- jq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3"
- jq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3"
- jq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
- jq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
- jq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH ""
- jq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352
- jq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000
+ gojq -r '.pc.structure[2]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p3"
+ gojq -r '.pc.structure[2]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p3"
+ gojq -r '.pc.structure[2]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
+ gojq -r '.pc.structure[2]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-boot
+ gojq -r '.pc.structure[2].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[2].offset' < "$DISK_MAPPING_JSON" | MATCH 1260388352
+ gojq -r '.pc.structure[2].size' < "$DISK_MAPPING_JSON" | MATCH 786432000
# fourth structure - ubuntu-save
- jq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4"
- jq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4"
- jq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save
- jq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save
- jq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[3]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p4"
+ gojq -r '.pc.structure[3]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p4"
+ gojq -r '.pc.structure[3]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save
+ gojq -r '.pc.structure[3]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-save
+ gojq -r '.pc.structure[3].id' < "$DISK_MAPPING_JSON" | MATCH ""
if [ "$channel" = "20" ]; then
- jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
- jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216
+ gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
+ gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 16777216
else
- jq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
- jq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432
+ gojq -r '.pc.structure[3].offset' < "$DISK_MAPPING_JSON" | MATCH 2046820352
+ gojq -r '.pc.structure[3].size' < "$DISK_MAPPING_JSON" | MATCH 33554432
fi
# fifth structure - ubuntu-data
- jq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5"
- jq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5"
- jq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data
- jq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data
- jq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH ""
+ gojq -r '.pc.structure[4]."device-path"' < "$DISK_MAPPING_JSON" | MATCH "/sys/devices/virtual/block/$LOOP_BASENAME/${LOOP_BASENAME}p5"
+ gojq -r '.pc.structure[4]."kernel-path"' < "$DISK_MAPPING_JSON" | MATCH "${LOOP}p5"
+ gojq -r '.pc.structure[4]."filesystem-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data
+ gojq -r '.pc.structure[4]."partition-label"' < "$DISK_MAPPING_JSON" | MATCH ubuntu-data
+ gojq -r '.pc.structure[4].id' < "$DISK_MAPPING_JSON" | MATCH ""
if [ "$channel" = "20" ]; then
- jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568
- jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17936385536
+ gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2063597568
+ gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17936385536
else
- jq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784
- jq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17919608320
+ gojq -r '.pc.structure[4].offset' < "$DISK_MAPPING_JSON" | MATCH 2080374784
+ gojq -r '.pc.structure[4].size' < "$DISK_MAPPING_JSON" | MATCH 17919608320
fi
diff --git a/tests/main/unhandled-task/task.yaml b/tests/main/unhandled-task/task.yaml
index dccb907c82c..ac4583db1f4 100644
--- a/tests/main/unhandled-task/task.yaml
+++ b/tests/main/unhandled-task/task.yaml
@@ -8,23 +8,20 @@ details: |
unknown handler name in order to check that such tasks do not cause snapd to
malfunction entirely, which would make rollback impossible.
-prepare: |
- snap install --devmode jq
-
execute: |
echo "Stop snapd"
systemctl stop snapd.{service,socket}
- LAST_LANE_ID=$(jq ".[\"last-lane-id\"]" /var/lib/snapd/state.json)
+ LAST_LANE_ID=$(gojq ".[\"last-lane-id\"]" /var/lib/snapd/state.json)
TASK_SNIPPET="{\"id\":\"90999\",\"kind\":\"alien-task\",\"summary\":\"alien task\",\"status\":0,\"data\":{},\"wait-tasks\":[],\"lanes\":[$LAST_LANE_ID],\"change\":\"80999\",\"spawn-time\":\"2010-11-09T22:04:10.320985653Z\"}"
CHANGE_SNIPPET="{\"id\":\"80999\",\"kind\":\"some-change\",\"summary\":\"...\",\"status\":0,\"clean\":true,\"data\":{},\"task-ids\":[\"90999\"],\"spawn-time\":\"2010-11-09T22:04:10.320985653Z\"}"
echo "Add unknown task to the state"
- jq ".changes[\"80999\"]=$CHANGE_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq ".changes[\"80999\"]=$CHANGE_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
- jq ".tasks[\"90999\"]=$TASK_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
+ gojq ".tasks[\"90999\"]=$TASK_SNIPPET" /var/lib/snapd/state.json > /var/lib/snapd/state.json.new
mv /var/lib/snapd/state.json.new /var/lib/snapd/state.json
systemctl start snapd.{service,socket}
diff --git a/tests/nested/manual/core20-create-recovery/task.yaml b/tests/nested/manual/core20-create-recovery/task.yaml
index 3a8857da6a3..4caa4562bf4 100644
--- a/tests/nested/manual/core20-create-recovery/task.yaml
+++ b/tests/nested/manual/core20-create-recovery/task.yaml
@@ -21,7 +21,7 @@ execute: |
boot_id="$( tests.nested boot-id )"
echo '{"action":"create-recovery-system","params":{"recovery-system-label":"1234"}}' | \
remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/debug" > change.out
- REMOTE_CHG_ID=$(jq -r .change < change.out)
+ REMOTE_CHG_ID=$(gojq -r .change < change.out)
remote.wait-for reboot "${boot_id}"
remote.exec sudo snap watch "${REMOTE_CHG_ID}"
@@ -37,7 +37,7 @@ execute: |
boot_id="$( tests.nested boot-id )"
echo '{"action":"create-recovery-system","params":{"recovery-system-label":"1234-1"}}' | \
remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/debug" > change.out
- REMOTE_CHG_ID=$(jq -r .change < change.out)
+ REMOTE_CHG_ID=$(gojq -r .change < change.out)
remote.wait-for reboot "${boot_id}"
remote.exec sudo snap watch "${REMOTE_CHG_ID}"
diff --git a/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml b/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml
index 8d535c90a77..68d8e8bca65 100644
--- a/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml
+++ b/tests/nested/manual/core20-install-device-file-install-ubuntu-save-via-hook/task.yaml
@@ -41,11 +41,6 @@ prepare: |
exit
fi
- # install pre-reqs which we need to adjust various bits
- snap install jq remarshal
- tests.cleanup defer snap remove remarshal
- tests.cleanup defer snap remove jq
-
# Setup the fake-store for ubuntu-image to use when creating our core image.
# We immediately tear down the staging store, to make sure snapd is not pointed
# towards this once we invoke ubuntu-image.
@@ -78,12 +73,11 @@ prepare: |
cp prepare-device pc-gadget/meta/hooks/prepare-device
echo "Add the extra hooks definition to the snap.yaml"
- # convert our yaml files to json (yaml is easier to write and maintain in VCS)
- yaml2json < pc-gadget/meta/snap.yaml | tee pc-gadget/meta/snap.json > /dev/null
- yaml2json < snap-yaml-extras.yaml | tee snap-yaml-extras.json > /dev/null
- # slurp our two json files together into one json document, then convert
- # back to yaml and write out to the snap.yaml in the unpacked gadget snap
- jq -s '.[0] * .[1]' <(cat snap-yaml-extras.json) <(cat pc-gadget/meta/snap.json) | json2yaml | tee pc-gadget/meta/snap.yaml
+ # slurp our two yaml files together into one document, then convert write
+ # out to the snap.yaml in the unpacked gadget snap
+ gojq -s --yaml-input --yaml-output '.[0] * .[1]' <(cat snap-yaml-extras.yaml) <(cat pc-gadget/meta/snap.yaml) | \
+ tee snap.yaml.tmp
+ cp -v snap.yaml.tmp pc-gadget/meta/snap.yaml
# delay all refreshes for a week from now, as otherwise refreshes for our
# snaps (which are asserted by the testrootorg authority-id) may happen, which
diff --git a/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml b/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml
index 0366cb40f61..050fc038c18 100644
--- a/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml
+++ b/tests/nested/manual/core20-install-device-file-install-via-hook-hack/task.yaml
@@ -42,11 +42,6 @@ prepare: |
exit
fi
- # install pre-reqs which we need to adjust various bits
- snap install jq remarshal
- tests.cleanup defer snap remove remarshal
- tests.cleanup defer snap remove jq
-
# setup the fakestore, but don't use it for our snapd here on the host VM, so
# tear down the staging_store immediately afterwards so that only the SAS is
# running and our snapd is not pointed at it, ubuntu-image is the only thing
@@ -79,12 +74,11 @@ prepare: |
cp prepare-device pc-gadget/meta/hooks/prepare-device
echo "Add the extra hooks definition to the snap.yaml"
- # convert our yaml files to json (yaml is easier to write and maintain in VCS)
- yaml2json < pc-gadget/meta/snap.yaml | tee pc-gadget/meta/snap.json > /dev/null
- yaml2json < snap-yaml-extras.yaml | tee snap-yaml-extras.json > /dev/null
- # slurp our two json files together into one json document, then convert
- # back to yaml and write out to the snap.yaml in the unpacked gadget snap
- jq -s '.[0] * .[1]' <(cat snap-yaml-extras.json) <(cat pc-gadget/meta/snap.json) | json2yaml | tee pc-gadget/meta/snap.yaml
+ # slurp our two yaml files together into one document, then convert write
+ # out to the snap.yaml in the unpacked gadget snap
+ gojq -s --yaml-output --yaml-input '.[0] * .[1]' <(cat snap-yaml-extras.yaml) <(cat pc-gadget/meta/snap.yaml) | \
+ tee snap.yaml.tmp
+ cp -v snap.yaml.tmp pc-gadget/meta/snap.yaml
# delay all refreshes for a week from now, as otherwise refreshes for our
# snaps (which are asserted by the testrootorg authority-id) may happen, which
diff --git a/tests/nested/manual/core20-validation-sets/task.yaml b/tests/nested/manual/core20-validation-sets/task.yaml
index d76abd7abab..96733df879c 100644
--- a/tests/nested/manual/core20-validation-sets/task.yaml
+++ b/tests/nested/manual/core20-validation-sets/task.yaml
@@ -40,11 +40,8 @@ prepare: |
}
# install pre-reqs which we need to adjust various bits
- snap install jq remarshal
snap install test-snapd-swtpm --edge
tests.cleanup defer snap remove test-snapd-swtpm
- tests.cleanup defer snap remove remarshal
- tests.cleanup defer snap remove jq
# download snaps for the model
snap download core
diff --git a/tests/nested/manual/split-refresh/task.yaml b/tests/nested/manual/split-refresh/task.yaml
index 0ea0aec1210..68846fc57af 100644
--- a/tests/nested/manual/split-refresh/task.yaml
+++ b/tests/nested/manual/split-refresh/task.yaml
@@ -24,8 +24,6 @@ prepare: |
echo "This test needs test keys to be trusted"
exit
fi
- echo "Install used snaps"
- snap install jq --devmode --edge
if [ -d /var/lib/snapd/seed ]; then
mv /var/lib/snapd/seed /var/lib/snapd/seed.orig
fi
@@ -100,9 +98,9 @@ execute: |
cp -a ./classic-seed/system-seed/ /var/lib/snapd/seed
# do some light checking that the system is valid
- snap debug api /v2/systems | jq '.result.systems[0].label' | MATCH "$LABEL"
+ snap debug api /v2/systems | gojq '.result.systems[0].label' | MATCH "$LABEL"
snap debug api "/v2/systems/$LABEL" > system
- jq '.result.model.distribution' system | MATCH "ubuntu"
+ gojq '.result.model.distribution' system | MATCH "ubuntu"
# build muinstaller and put in place
go build -o muinstaller "$TESTSLIB"/muinstaller/main.go
diff --git a/tests/regression/lp-2084730/task.yaml b/tests/regression/lp-2084730/task.yaml
new file mode 100644
index 00000000000..b40dec348a1
--- /dev/null
+++ b/tests/regression/lp-2084730/task.yaml
@@ -0,0 +1,63 @@
+summary: Ensure that snapd state doesn't get stuck if inhibition hint file is locked.
+
+details: |
+ Check that snapd state will not stay locked when inhibition hint file lock is held.
+ For context: In LP #2084730, A problem was discovered related to refresh app awareness
+ feature which, in a very specific scenario, can cause snapd to enter a deadlock state
+ because snapd was tring to hold an exclusive lock on the inhibition hint lock file
+ while locking global state which was already locked by snap run. This results in all API
+ calls hanging which was made worse by snap run trying to call the snapd API causing
+ a deadlock.
+
+environment:
+ INHIBITION_LOCK_FILE: /var/lib/snapd/inhibit/test-snapd-sh.lock
+
+prepare: |
+ snap install --stable test-snapd-sh
+ mkdir -p /var/lib/snapd/inhibit
+ touch "$INHIBITION_LOCK_FILE"
+
+ snap set core experimental.parallel-instances=true
+
+restore: |
+ # release inhibition hint lock
+ systemctl stop inhibition-file-locker.service
+
+ # Wait for refresh to finish to be able to remove the snap
+ snap debug ensure-state-soon
+ retry -n 10 sh -c "snap changes | NOMATCH Doing"
+
+ snap set core experimental.parallel-instances!
+
+execute: |
+ systemd-run --unit inhibition-file-locker.service flock "$INHIBITION_LOCK_FILE" --command "sleep 10000"
+ # Wait for the inhibition file lock to be held
+ retry -n 10 not flock --timeout 0 "$INHIBITION_LOCK_FILE" --command "true"
+
+ # This refresh will block because it tries to hold the inhibition file lock for test-snapd-sh
+ snap refresh --no-wait --edge test-snapd-sh > locked-change-id
+
+ # wait until snapd is blocked in link-snap
+ # avoid using the API directly to not take the state lock
+ locked_id="$(cat locked-change-id)"
+ retry -n 50 --wait 1 sh -c "snap debug state /var/lib/snapd/state.json --change=$locked_id | MATCH 'Doing .* Make current revision for snap .* unavailable'"
+
+ # and still waiting
+ snap debug state /var/lib/snapd/state.json --change="$locked_id" | MATCH 'Doing .* Make current revision for snap .* unavailable'
+
+ # Check snapd has the inhibition hint lock file open
+ if command -v lsof; then
+ lsof "$INHIBITION_LOCK_FILE" | MATCH "snapd"
+ fi
+
+ # Check that snapd state is not locked when trying to hold the inhibition file lock above
+ timeout 5 snap debug api /v2/snaps
+
+ # Check that operations changing the state of the snap (remove, or another refresh) fail the conflict check
+ snap refresh --beta test-snapd-sh 2>&1 | MATCH 'snap "test-snapd-sh" has "refresh-snap" change in progress'
+ snap remove test-snapd-sh 2>&1 | MATCH 'snap "test-snapd-sh" has "refresh-snap" change in progress'
+
+ # Check that parallel instance of the blocked snap is not affected
+ snap install --stable test-snapd-sh_instance
+ snap refresh --edge test-snapd-sh_instance
+ snap remove test-snapd-sh_instance
diff --git a/tests/upgrade/basic/task.yaml b/tests/upgrade/basic/task.yaml
index 8854ac92726..bd3325bcb39 100644
--- a/tests/upgrade/basic/task.yaml
+++ b/tests/upgrade/basic/task.yaml
@@ -173,7 +173,7 @@ execute: |
snap aliases|MATCH "test-snapd-auto-aliases.wellknown2 +test_snapd_wellknown2 +-"
echo "Check migrating to types in state"
- coreType=$(jq -r '.data.snaps["core"].type' /var/lib/snapd/state.json)
- testSnapType=$(jq -r '.data.snaps["test-snapd-sh"].type' /var/lib/snapd/state.json)
+ coreType=$(gojq -r '.data.snaps["core"].type' /var/lib/snapd/state.json)
+ testSnapType=$(gojq -r '.data.snaps["test-snapd-sh"].type' /var/lib/snapd/state.json)
[ "$coreType" = "os" ]
[ "$testSnapType" = "app" ]
diff --git a/testtime/export_test.go b/testtime/export_test.go
new file mode 100644
index 00000000000..acabafc55b3
--- /dev/null
+++ b/testtime/export_test.go
@@ -0,0 +1,28 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package testtime
+
+import (
+ "time"
+)
+
+func (t *TestTimer) SetCChan(c chan time.Time) {
+ t.c = c
+}
diff --git a/testtime/testtime.go b/testtime/testtime.go
new file mode 100644
index 00000000000..cfb943e92d3
--- /dev/null
+++ b/testtime/testtime.go
@@ -0,0 +1,213 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+// Package testtime provides a mocked version of time.Timer for use in tests.
+package testtime
+
+import (
+ "sync"
+ "time"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/timeutil"
+)
+
+// TestTimer is a mocked version of time.Timer for which the passage of time or
+// the direct expiration of the timer is controlled manually.
+//
+// TestTimer implements timeutil.Timer.
+//
+// TestTimer also provides methods to introspect whether the timer is active or
+// how many times it has fired.
+type TestTimer struct {
+ mu sync.Mutex
+ duration time.Duration
+ elapsed time.Duration
+ active bool
+ fireCount int
+ callback func()
+ c chan time.Time
+}
+
+var _ timeutil.Timer = (*TestTimer)(nil)
+
+// AfterFunc waits for the timer to fire and then calls f in its own goroutine.
+// It returns a Timer that can be used to cancel the call using its Stop method.
+// The returned Timer's C field is not used and will be nil.
+//
+// AfterFunc returns a TestTimer which simulates the behavior of a timer which
+// was created via time.AfterFunc.
+//
+// See here for more details: https://pkg.go.dev/time#AfterFunc
+func AfterFunc(d time.Duration, f func()) *TestTimer {
+ osutil.MustBeTestBinary("testtime timers cannot be used outside of tests")
+ timer := &TestTimer{
+ duration: d,
+ active: true,
+ callback: f,
+ }
+ // If duration is 0 or negative, ensure timer fires
+ defer timer.maybeFire()
+ return timer
+}
+
+// NewTimer creates a new Timer that will send the current time on its channel
+// after the timer fires.
+//
+// NewTimer returns a TestTimer which simulates the behavior of a timer which
+// was created via time.NewTimer.
+//
+// See here for more details: https://pkg.go.dev/time#NewTimer
+func NewTimer(d time.Duration) *TestTimer {
+ osutil.MustBeTestBinary("testtime timers cannot be used outside of tests")
+ c := make(chan time.Time, 1)
+ timer := &TestTimer{
+ duration: d,
+ active: true,
+ c: c,
+ }
+ // If duration is 0 or negative, ensure timer fires
+ defer timer.maybeFire()
+ return timer
+}
+
+// ExpiredC returns the underlying C channel of the timer.
+func (t *TestTimer) ExpiredC() <-chan time.Time {
+ return t.c
+}
+
+// Reset changes the timer to expire after duration d. It returns true if the
+// timer had been active, false if the timer had expired or been stopped.
+//
+// As the test timer does not actually count down, Reset sets the timer's
+// elapsed time to 0 and set its duration to the given duration. The elapsed
+// time must be advanced manually using Elapse.
+//
+// This simulates the behavior of Timer.Reset() from the time package.
+// See here fore more details: https://pkg.go.dev/time#Timer.Reset
+func (t *TestTimer) Reset(d time.Duration) bool {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ active := t.active
+ t.active = true
+ t.duration = d
+ t.elapsed = 0
+ if t.c != nil {
+ // Drain the channel, guaranteeing that a receive after Reset will
+ // block until the timer fires again, and not receive a time value
+ // from the timer firing before the reset occurred.
+ // This complies with the new behavior of Reset as of Go 1.23.
+ // See: https://pkg.go.dev/time#Timer.Reset
+ select {
+ case <-t.c:
+ default:
+ }
+ }
+ // If duration is 0 or negative, ensure timer fires
+ defer t.maybeFire()
+ return active
+}
+
+// Stop prevents the timer from firing. It returns true if the call stops the
+// timer, false if the timer has already expired or been stopped.
+//
+// This simulates the behavior of Timer.Stop() from the time package.
+// See here for more details: https://pkg.go.dev/time#Timer.Stop
+func (t *TestTimer) Stop() bool {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ wasActive := t.active
+ t.active = false
+ if t.c != nil {
+ // Drain the channel, guaranteeing that a receive after Stop will block
+ // and not receive a time value from the timer firing before the stop
+ // occurred. This complies with the new behavior of Stop as of Go 1.23.
+ // See: https://pkg.go.dev/time#Timer.Stop
+ select {
+ case <-t.c:
+ default:
+ }
+ }
+ return wasActive
+}
+
+// Active returns true if the timer is active, false if the timer has expired
+// or been stopped.
+func (t *TestTimer) Active() bool {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ return t.active
+}
+
+// FireCount returns the number of times the timer has fired.
+func (t *TestTimer) FireCount() int {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ return t.fireCount
+}
+
+// Elapse simulates time advancing by the given duration, which potentially
+// causes the timer to fire.
+//
+// The timer will fire if the total elapsed time since the timer was created
+// or reset is greater than the timer's duration and the timer has not yet
+// fired.
+func (t *TestTimer) Elapse(duration time.Duration) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ t.elapsed += duration
+ t.maybeFire()
+}
+
+// maybeFire fires the timer if the elapsed time is greater than the timer's
+// duration. The caller must hold the timer lock.
+func (t *TestTimer) maybeFire() {
+ if t.elapsed >= t.duration {
+ t.doFire(time.Now())
+ }
+}
+
+// Fire causes the timer to fire. If the timer was created via NewTimer, then
+// sends the given current time over the C channel.
+//
+// To avoid accidental misuse, panics if the timer is not active (if it has
+// already fired or been stopped).
+func (t *TestTimer) Fire(currTime time.Time) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ if !t.active {
+ panic("cannot fire timer which is not active")
+ }
+ t.doFire(currTime)
+}
+
+// doFire carries out the timer firing. The caller must hold the timer lock.
+func (t *TestTimer) doFire(currTime time.Time) {
+ if !t.active {
+ return
+ }
+ t.active = false
+ t.fireCount++
+ // Either t.callback or t.C should be non-nil, and the other should be nil.
+ if t.callback != nil {
+ go t.callback()
+ } else if t.c != nil {
+ t.c <- currTime
+ }
+}
diff --git a/testtime/testtime_test.go b/testtime/testtime_test.go
new file mode 100644
index 00000000000..df31fadca67
--- /dev/null
+++ b/testtime/testtime_test.go
@@ -0,0 +1,761 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package testtime_test
+
+import (
+ "errors"
+ "runtime"
+ "sync"
+ "testing"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/randutil"
+ "github.com/snapcore/snapd/testtime"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type testtimeSuite struct{}
+
+var _ = Suite(&testtimeSuite{})
+
+func (s *testtimeSuite) TestTimerInterfaceCompatibility(c *C) {
+ t := testtime.NewTimer(time.Second)
+ active := t.Reset(time.Second)
+ c.Check(active, Equals, true)
+ active = t.Stop()
+ c.Check(active, Equals, true)
+ c.Check(t.ExpiredC(), NotNil)
+ t = testtime.AfterFunc(time.Second, func() { return })
+ active = t.Reset(time.Second)
+ c.Check(active, Equals, true)
+ active = t.Stop()
+ c.Check(active, Equals, true)
+ c.Check(t.ExpiredC(), IsNil)
+}
+
+func (s *testtimeSuite) TestAfterFunc(c *C) {
+ // Create a non-buffered channel on which a message will be sent when the
+ // callback is called. Use a non-buffered channel so that we ensure that
+ // the callback runs in its own goroutine.
+ callbackChan := make(chan string)
+
+ timer := testtime.AfterFunc(time.Hour, func() {
+ callbackChan <- "called"
+ })
+
+ c.Check(timer.ExpiredC(), IsNil)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 0)
+ select {
+ case <-callbackChan:
+ c.Fatal("callback fired early")
+ default:
+ }
+
+ // Manually advance the timer so that it will fire
+ timer.Elapse(time.Hour)
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case msg := <-callbackChan:
+ c.Assert(msg, Equals, "called")
+ case <-time.NewTimer(time.Minute).C:
+ // Goroutine may not start immediately, so allow some grace period
+ c.Fatal("callback did not complete")
+ }
+
+ // Reset timer to check that if it fires again, the callback will be called again
+ active := timer.Reset(time.Nanosecond)
+ c.Check(active, Equals, false)
+
+ c.Check(timer.ExpiredC(), IsNil)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-callbackChan:
+ c.Fatal("callback fired early")
+ default:
+ }
+
+ // Manually fire the timer with the current time, though the time doesn't matter here
+ timer.Fire(time.Now())
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 2)
+ select {
+ case msg := <-callbackChan:
+ c.Assert(msg, Equals, "called")
+ case <-time.NewTimer(time.Minute).C:
+ // Goroutine may not start immediately, so allow some grace period
+ c.Fatal("callback did not complete")
+ }
+
+ // Firing inactive timer panics
+ c.Check(func() { timer.Fire(time.Now()) }, PanicMatches, "cannot fire timer which is not active")
+}
+
+func (s *testtimeSuite) TestNewTimer(c *C) {
+ timer := testtime.NewTimer(time.Second)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 0)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired early")
+ default:
+ }
+
+ // Manually advance the timer so that it will fire
+ timer.Elapse(time.Second)
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-timer.ExpiredC():
+ default:
+ c.Fatal("timer did not fire")
+ }
+
+ // Reset timer to check that if it fires again, the callback will be called again
+ active := timer.Reset(time.Nanosecond)
+ c.Check(active, Equals, false)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired early")
+ default:
+ }
+
+ // Manually fire the timer with the current time
+ currTime := time.Now()
+ timer.Fire(currTime)
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 2)
+ select {
+ case t := <-timer.ExpiredC():
+ c.Assert(t.Equal(currTime), Equals, true)
+ default:
+ c.Fatal("timer did not fire")
+ }
+
+ // Firing inactive timer panics
+ c.Check(func() { timer.Fire(currTime) }, PanicMatches, "cannot fire timer which is not active")
+}
+
+func (s *testtimeSuite) TestReset(c *C) {
+ timer := testtime.NewTimer(time.Millisecond)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 0)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired early")
+ default:
+ }
+
+ timer.Fire(time.Now())
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 1)
+
+ active := timer.Reset(time.Millisecond)
+ c.Check(active, Equals, false)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 1)
+ // Check that receiving from the timer channel blocks after reset, even
+ // though the timer previously fired and write time to channel.
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired after reset")
+ default:
+ }
+
+ // Reset the timer
+ active = timer.Reset(3 * time.Second)
+ c.Check(active, Equals, true)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired early")
+ default:
+ }
+
+ // Elapse more than half the time
+ timer.Elapse(2 * time.Second)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired early")
+ default:
+ }
+
+ // Reset the timer
+ active = timer.Reset(3 * time.Second)
+ c.Check(active, Equals, true)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired after reset")
+ default:
+ }
+
+ // Elapse more than half the time again
+ timer.Elapse(2 * time.Second)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired after time elapsed following reset")
+ default:
+ }
+
+ // Elapse the remaining time
+ timer.Elapse(time.Second)
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 2)
+ select {
+ case <-timer.ExpiredC():
+ default:
+ c.Fatal("timer did not fire")
+ }
+
+ active = timer.Reset(time.Second)
+ c.Check(active, Equals, false)
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 2)
+}
+
+func (s *testtimeSuite) TestStop(c *C) {
+ timer := testtime.NewTimer(time.Millisecond)
+
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 0)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired early")
+ default:
+ }
+
+ active := timer.Stop()
+ c.Check(active, Equals, true)
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 0)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("timer fired after Stop")
+ default:
+ }
+
+ // Elapse time so the timer would have fired if it were not stopped
+ timer.Elapse(time.Millisecond)
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 0)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("received from timer chan after Stop and Elapse")
+ default:
+ }
+
+ // Reset the timer, and check that the timer was not previously active
+ active = timer.Reset(time.Second)
+ c.Check(active, Equals, false)
+ c.Check(timer.Active(), Equals, true)
+ c.Check(timer.FireCount(), Equals, 0)
+
+ // Elapse time so that the timer fires
+ timer.Elapse(1500 * time.Millisecond)
+
+ c.Check(active, Equals, false)
+
+ // Stop the timer after it has fired
+ active = timer.Stop()
+ c.Check(active, Equals, false)
+
+ c.Check(timer.Active(), Equals, false)
+ c.Check(timer.FireCount(), Equals, 1)
+ select {
+ case <-timer.ExpiredC():
+ c.Fatal("received from timer chan after Stop called after firing")
+ default:
+ }
+}
+
+// Tests from the Go standard library which relate to timers
+
+// Adapted from src/time/time_test.go as of go1.23.3.
+//
+// Issue 25686: hard crash on concurrent timer access.
+// Issue 37400: panic with "racy use of timers"
+// This test deliberately invokes a race condition.
+// We are testing that we don't crash with "fatal error: panic holding locks",
+// and that we also don't panic.
+func (s *testtimeSuite) TestStdlibConcurrentTimerReset(c *C) {
+ const goroutines = 8
+ const tries = 1000
+ var wg sync.WaitGroup
+ wg.Add(goroutines)
+ timer := testtime.NewTimer(time.Hour)
+ for i := 0; i < goroutines; i++ {
+ go func(i int) {
+ defer wg.Done()
+ for j := 0; j < tries; j++ {
+ timer.Reset(time.Hour + time.Duration(i*j))
+ }
+ }(i)
+ }
+ wg.Wait()
+}
+
+// Adapted from src/time/time_test.go as of go1.23.3.
+//
+// Issue 37400: panic with "racy use of timers".
+func (s *testtimeSuite) TestStdlibConcurrentTimerResetStop(c *C) {
+ const goroutines = 8
+ const tries = 1000
+ var wg sync.WaitGroup
+ wg.Add(goroutines * 2)
+ timer := testtime.NewTimer(time.Hour)
+ for i := 0; i < goroutines; i++ {
+ go func(i int) {
+ defer wg.Done()
+ for j := 0; j < tries; j++ {
+ timer.Reset(time.Hour + time.Duration(i*j))
+ }
+ }(i)
+ go func(i int) {
+ defer wg.Done()
+ timer.Stop()
+ }(i)
+ }
+ wg.Wait()
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// newTimerFunc simulates NewTimer using AfterFunc,
+// but this version will not hit the special cases for channels
+// that are used when calling NewTimer.
+// This makes it easy to test both paths.
+func newTimerFunc(d time.Duration) *testtime.TestTimer {
+ c := make(chan time.Time, 1)
+ t := testtime.AfterFunc(d, func() { c <- time.Now() })
+ t.SetCChan(c)
+ return t
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func (s *testtimeSuite) TestStdlibAfterStopNewTimer(c *C) {
+ testAfterStop(c, testtime.NewTimer)
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func (s *testtimeSuite) TestStdlibAfterStopAfterFunc(c *C) {
+ testAfterStop(c, newTimerFunc)
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func testAfterStop(c *C, newTimer func(time.Duration) *testtime.TestTimer) {
+ // We want to test that we stop a timer before it runs.
+ // We also want to test that it didn't run after a longer timer.
+ // Since we don't want the test to run for too long, we don't
+ // want to use lengthy times. That makes the test inherently flaky.
+ // So only report an error if it fails five times in a row.
+
+ var errs []string
+ logErrs := func() {
+ for _, e := range errs {
+ c.Log(e)
+ }
+ }
+
+ for i := 0; i < 5; i++ {
+ tInitial := testtime.AfterFunc(100*time.Millisecond, func() {})
+ t0 := newTimer(50 * time.Millisecond)
+ c1 := make(chan bool, 1)
+ t1 := testtime.AfterFunc(150*time.Millisecond, func() { c1 <- true })
+ if !t0.Stop() {
+ errs = append(errs, "failed to stop event 0")
+ continue
+ }
+ if !t1.Stop() {
+ errs = append(errs, "failed to stop event 1")
+ continue
+ }
+ for _, timer := range []*testtime.TestTimer{tInitial, t0, t1} {
+ timer.Elapse(200 * time.Millisecond)
+ }
+ select {
+ case <-t0.ExpiredC():
+ errs = append(errs, "event 0 was not stopped")
+ continue
+ case <-c1:
+ errs = append(errs, "event 1 was not stopped")
+ continue
+ default:
+ }
+ if t1.Stop() {
+ errs = append(errs, "Stop returned true twice")
+ continue
+ }
+
+ // Test passed, so all done.
+ if len(errs) > 0 {
+ c.Logf("saw %d errors, ignoring to avoid flakiness", len(errs))
+ logErrs()
+ }
+
+ return
+ }
+
+ c.Errorf("saw %d errors", len(errs))
+ logErrs()
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func (s *testtimeSuite) TestStdlibTimerStopStress(c *C) {
+ for i := 0; i < 100; i++ {
+ go func(i int) {
+ timer := testtime.AfterFunc(2*time.Second, func() {
+ c.Errorf("timer %d was not stopped", i)
+ })
+ timer.Elapse(1 * time.Second)
+ timer.Stop()
+ timer.Elapse(1 * time.Second)
+ }(i)
+ }
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func testReset(d time.Duration) error {
+ t0 := testtime.NewTimer(2 * d)
+ t0.Elapse(d)
+ if !t0.Reset(3 * d) {
+ return errors.New("resetting unfired timer returned false")
+ }
+ t0.Elapse(2 * d)
+ select {
+ case <-t0.ExpiredC():
+ return errors.New("timer fired early")
+ default:
+ }
+ t0.Elapse(2 * d)
+ select {
+ case <-t0.ExpiredC():
+ default:
+ return errors.New("reset timer did not fire")
+ }
+
+ if t0.Reset(50 * time.Millisecond) {
+ return errors.New("resetting expired timer returned true")
+ }
+ return nil
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func (s *testtimeSuite) TestStdlibReset(c *C) {
+ // We try to run this test with increasingly larger multiples
+ // until one works so slow, loaded hardware isn't as flaky,
+ // but without slowing down fast machines unnecessarily.
+ //
+ // (maxDuration is several orders of magnitude longer than we
+ // expect this test to actually take on a fast, unloaded machine.)
+ d := 1 * time.Millisecond
+ const maxDuration = 10 * time.Second
+ for {
+ err := testReset(d)
+ if err == nil {
+ break
+ }
+ d *= 2
+ if d > maxDuration {
+ c.Error(err)
+ }
+ c.Logf("%v; trying duration %v", err, d)
+ }
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// Test that zero duration timers aren't missed by the scheduler. Regression test for issue 44868.
+func (s *testtimeSuite) TestStdlibZeroTimerNewTimer(c *C) {
+ testZeroTimer(c, testtime.NewTimer)
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// Test that zero duration timers aren't missed by the scheduler. Regression test for issue 44868.
+func (s *testtimeSuite) TestStdlibZeroTimerAfterFunc(c *C) {
+ testZeroTimer(c, newTimerFunc)
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// Test that zero duration timers aren't missed by the scheduler. Regression test for issue 44868.
+func (s *testtimeSuite) TestStdlibZeroTimerAfterFuncReset(c *C) {
+ timer := newTimerFunc(time.Hour)
+ testZeroTimer(c, func(d time.Duration) *testtime.TestTimer {
+ timer.Reset(d)
+ return timer
+ })
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func testZeroTimer(c *C, newTimer func(time.Duration) *testtime.TestTimer) {
+ // XXX: stdlib does 1000000, but that's really slow, so do 1/10 that
+ for i := 0; i < 100000; i++ {
+ s := time.Now()
+ ti := newTimer(0)
+ <-ti.ExpiredC()
+ if diff := time.Since(s); diff > 2*time.Second {
+ c.Errorf("Expected time to get value from Timer channel in less than 2 sec, took %v", diff)
+ }
+ }
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// Test that rapidly moving a timer earlier doesn't cause it to get dropped.
+// Issue 47329.
+func (s *testtimeSuite) TestStdlibTimerModifiedEarlier(c *C) {
+ past := time.Until(time.Unix(0, 0))
+ count := 1000
+ fail := 0
+ for i := 0; i < count; i++ {
+ timer := newTimerFunc(time.Hour)
+ for j := 0; j < 10; j++ {
+ if !timer.Stop() {
+ select {
+ case <-timer.ExpiredC():
+ // This shouldn't be necessary since we comply with 1.23
+ // behavior:
+ // "as of Go 1.23, any receive from t.C after Stop has
+ // returned is guaranteed to block rather than receive a
+ // stale time value from before the Stop"
+ //
+ // See: https://cs.opensource.google/go/go/+/refs/tags/go1.23.3:src/time/sleep.go;l=105
+ default:
+ }
+ }
+ timer.Reset(past)
+ }
+
+ deadline := time.NewTimer(10 * time.Second)
+ defer deadline.Stop()
+ now := time.Now()
+ select {
+ case <-timer.ExpiredC():
+ if since := time.Since(now); since > 8*time.Second {
+ c.Errorf("timer took too long (%v)", since)
+ fail++
+ }
+ case <-deadline.C:
+ c.Error("deadline expired")
+ }
+ }
+
+ if fail > 0 {
+ c.Errorf("%d failures", fail)
+ }
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// Test that rapidly moving timers earlier and later doesn't cause
+// some of the sleep times to be lost.
+// Issue 47762
+func (s *testtimeSuite) TestStdlibAdjustTimers(c *C) {
+ timers := make([]*testtime.TestTimer, 100)
+ states := make([]int, len(timers))
+ indices := randutil.Perm(len(timers))
+
+ for len(indices) != 0 {
+ var ii = randutil.Intn(len(indices))
+ var i = indices[ii]
+
+ var timer = timers[i]
+ var state = states[i]
+ states[i]++
+
+ switch state {
+ case 0:
+ timers[i] = newTimerFunc(0)
+
+ case 1:
+ <-timer.ExpiredC() // Timer is now idle.
+
+ // Reset to various long durations, which we'll cancel.
+ case 2:
+ if timer.Reset(1 * time.Minute) {
+ panic("shouldn't be active (1)")
+ }
+ case 4:
+ if timer.Reset(3 * time.Minute) {
+ panic("shouldn't be active (3)")
+ }
+ case 6:
+ if timer.Reset(2 * time.Minute) {
+ panic("shouldn't be active (2)")
+ }
+
+ // Stop and drain a long-duration timer.
+ case 3, 5, 7:
+ if !timer.Stop() {
+ c.Logf("timer %d state %d Stop returned false", i, state)
+ <-timer.ExpiredC()
+ }
+
+ // Start a short-duration timer we expect to select without blocking.
+ case 8:
+ if timer.Reset(0) {
+ c.Fatal("timer.Reset returned true")
+ }
+ case 9:
+ now := time.Now()
+ <-timer.ExpiredC()
+ dur := time.Since(now)
+ if dur > 750*time.Millisecond {
+ c.Errorf("timer %d took %v to complete", i, dur)
+ }
+
+ // Timer is done. Swap with tail and remove.
+ case 10:
+ indices[ii] = indices[len(indices)-1]
+ indices = indices[:len(indices)-1]
+ }
+ }
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func (s *testtimeSuite) TestStdlibStopResult(c *C) {
+ testStopResetResult(c, true)
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+func (s *testtimeSuite) TestStdlibResetResult(c *C) {
+ testStopResetResult(c, false)
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// Test that when racing between running a timer and stopping a timer Stop
+// consistently indicates whether a value can be read from the channel.
+// Issue #69312.
+func testStopResetResult(c *C, testStop bool) {
+ stopOrReset := func(timer *testtime.TestTimer) bool {
+ if testStop {
+ return timer.Stop()
+ } else {
+ return timer.Reset(1 * time.Hour)
+ }
+ }
+
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+ const N = 1000
+ wg.Add(N)
+ for i := 0; i < N; i++ {
+ go func() {
+ defer wg.Done()
+ <-start
+ for j := 0; j < 100; j++ {
+ timer1 := testtime.NewTimer(1 * time.Millisecond)
+ timer2 := testtime.NewTimer(1 * time.Millisecond)
+ if randutil.Intn(2) == 0 {
+ timer1.Elapse(time.Millisecond)
+ } else {
+ timer2.Elapse(time.Millisecond)
+ }
+ select {
+ case <-timer1.ExpiredC():
+ if !stopOrReset(timer2) {
+ // The test fails if this
+ // channel read times out.
+ <-timer2.ExpiredC()
+ }
+ case <-timer2.ExpiredC():
+ if !stopOrReset(timer1) {
+ // The test fails if this
+ // channel read times out.
+ <-timer1.ExpiredC()
+ }
+ }
+ }
+ }()
+ }
+ close(start)
+ wg.Wait()
+}
+
+// Adapted from src/time/sleep_test.go as of go1.23.3.
+//
+// Test having a large number of goroutines wake up a timer simultaneously.
+// This used to trigger a crash when run under x/tools/cmd/stress.
+func (s *testtimeSuite) TestStdlibMultiWakeupTimer(c *C) {
+ goroutines := runtime.GOMAXPROCS(0)
+ timer := testtime.NewTimer(time.Nanosecond)
+ var wg sync.WaitGroup
+ wg.Add(goroutines)
+ for i := 0; i < goroutines; i++ {
+ go func() {
+ defer wg.Done()
+ for j := 0; j < 10000; j++ {
+ select {
+ case <-timer.ExpiredC():
+ default:
+ }
+ timer.Reset(time.Nanosecond)
+ }
+ }()
+ }
+ doneChan := make(chan struct{})
+ go func() {
+ // Time won't elapse on its own, so we do it manually
+ for {
+ select {
+ case <-doneChan:
+ return
+ default:
+ timer.Elapse(time.Nanosecond)
+ }
+ }
+ }()
+ wg.Wait()
+ close(doneChan)
+}
diff --git a/timeutil/timer.go b/timeutil/timer.go
new file mode 100644
index 00000000000..0332f22dccd
--- /dev/null
+++ b/timeutil/timer.go
@@ -0,0 +1,69 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package timeutil
+
+import (
+ "time"
+)
+
+// Timer is an interface which wraps time.Timer so that it may be mocked.
+//
+// Timer is fully compatible with time.Timer, except that since interfaces
+// cannot have instance variables, we must expose the C channel as a method.
+// Therefore, when replacing a time.Timer with timeutil.Timer, any accesses of C
+// must be replaced with ExpireC().
+//
+// For more information about time.Timer, see: https://pkg.go.dev/time#Timer
+type Timer interface {
+ Reset(d time.Duration) bool
+ Stop() bool
+ // ExpiredC is equivalent to t.C for StdlibTimer and time.Timer.
+ ExpiredC() <-chan time.Time
+}
+
+type StdlibTimer struct {
+ *time.Timer
+}
+
+// AfterFunc waits for the duration to elapse and then calls f in its own
+// goroutine. It returns a Timer that can be used to cancel the call using its
+// Stop method. The returned Timer's C field is not used and will be nil.
+//
+// See here for more information: https://pkg.go.dev/time#AfterFunc
+func AfterFunc(d time.Duration, f func()) StdlibTimer {
+ return StdlibTimer{time.AfterFunc(d, f)}
+}
+
+// NewTimer creates a new Timer that will send the current time on its channel
+// after at least duration d.
+//
+// See here for more information: https://pkg.go.dev/time#NewTimer
+func NewTimer(d time.Duration) StdlibTimer {
+ return StdlibTimer{time.NewTimer(d)}
+}
+
+// ExpiredC returns the channel t.C over which the current time will be sent
+// when the timer expires, assuming the timer was created via NewTimer.
+//
+// If the timer was created via AfterFunc, then t.C is nil, so this function
+// returns nil.
+func (t StdlibTimer) ExpiredC() <-chan time.Time {
+ return t.C
+}
diff --git a/timeutil/timer_test.go b/timeutil/timer_test.go
new file mode 100644
index 00000000000..0c916f43b5f
--- /dev/null
+++ b/timeutil/timer_test.go
@@ -0,0 +1,57 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2024 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package timeutil_test
+
+import (
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/timeutil"
+)
+
+type timerSuite struct{}
+
+var _ = Suite(&timerSuite{})
+
+var _ timeutil.Timer = timeutil.StdlibTimer{}
+
+func (s *timerSuite) TestAfterFuncExpiredC(c *C) {
+ var timer timeutil.Timer = timeutil.AfterFunc(time.Second, func() {})
+ c.Assert(timer, NotNil)
+ c.Assert(timer.ExpiredC(), IsNil)
+ active := timer.Stop()
+ c.Assert(active, Equals, true)
+}
+
+func (s *timerSuite) TestNewTimerExpiredC(c *C) {
+ before := time.Now()
+ var timer timeutil.Timer = timeutil.NewTimer(time.Nanosecond)
+ c.Assert(timer, NotNil)
+ c.Assert(timer.ExpiredC(), NotNil)
+ fired := <-timer.ExpiredC()
+ after := time.Now()
+ c.Check(before.Before(fired), Equals, true)
+ c.Check(after.After(fired), Equals, true)
+ active := timer.Reset(time.Second)
+ c.Check(active, Equals, false)
+ active = timer.Stop()
+ c.Check(active, Equals, true)
+}
diff --git a/wrappers/services.go b/wrappers/services.go
index 8db7f1a1a67..0d526478f69 100644
--- a/wrappers/services.go
+++ b/wrappers/services.go
@@ -1231,7 +1231,10 @@ func restartServicesByStatus(svcsSts []*internal.ServiceStatus, explicitServices
var unitsToRestart []string
// If the service is activated, then we must also consider it's activators
- if len(st.ActivatorUnitStatuses()) != 0 {
+ // as long as we are not requesting a reload. For activated units reload
+ // is not a supported action. In that case treat it like a non-activated
+ // service.
+ if len(st.ActivatorUnitStatuses()) != 0 && !opts.Reload {
// Restart any activators first and operate normally on these
for _, act := range st.ActivatorUnitStatuses() {
// Use the primary name here for shouldRestart, as the caller
diff --git a/wrappers/services_test.go b/wrappers/services_test.go
index 8f78ecde167..ca5c1eb8bed 100644
--- a/wrappers/services_test.go
+++ b/wrappers/services_test.go
@@ -5352,6 +5352,19 @@ apps:
{"show", "--property=ActiveState", srvFile2Sock2},
{"start", srvFile2Sock1, srvFile2Sock2},
})
+
+ // Restart but also reload. When reloading we expect to see different behaviour.
+ // Reloading activated units is not supported by systemd, and for that reason they must
+ // not appear in the list of systemctl calls.
+ // The only call we expect to appear here is the primary service of svc1 as
+ // that one is the only one reported as active here.
+ s.sysdLog = nil
+ c.Assert(wrappers.RestartServices(services, nil, &wrappers.RestartServicesOptions{Reload: true}, progress.Null, s.perfTimings), IsNil)
+ c.Check(s.sysdLog, DeepEquals, [][]string{
+ {"show", "--property=Id,ActiveState,UnitFileState,Type,Names,NeedDaemonReload", srvFile1, srvFile2},
+ {"show", "--property=Id,ActiveState,UnitFileState,Names", srvFile2Sock1, srvFile2Sock2},
+ {"reload-or-restart", srvFile1},
+ })
}
func (s *servicesTestSuite) TestRestartWithActivatedServicesActive(c *C) {
@@ -5428,6 +5441,17 @@ apps:
{"show", "--property=ActiveState", srvFile2Sock2},
{"start", srvFile2Sock1, srvFile2Sock2},
})
+
+ // Restart but also reload. We do not expect any services to be restarted here as
+ // both the primary units are reported inactive. (Only the activation units are
+ // reported active). The reason we don't expect to see the activation units restarted
+ // when reloading, is that these units do not support reloading by systemd.
+ s.sysdLog = nil
+ c.Assert(wrappers.RestartServices(services, nil, &wrappers.RestartServicesOptions{Reload: true}, progress.Null, s.perfTimings), IsNil)
+ c.Check(s.sysdLog, DeepEquals, [][]string{
+ {"show", "--property=Id,ActiveState,UnitFileState,Type,Names,NeedDaemonReload", srvFile1, srvFile2},
+ {"show", "--property=Id,ActiveState,UnitFileState,Names", srvFile2Sock1, srvFile2Sock2},
+ })
}
func (s *servicesTestSuite) TestRestartWithActivatedServicesActivePrimaryUnit(c *C) {