-
Notifications
You must be signed in to change notification settings - Fork 13
feat: snap prepare-image plugins #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
306e116
feat: add classic preseed plugin
smethnani 6e4c0f2
feat: add uc preseed plugin
smethnani 45b35c6
chore(merge): 'main' into work/classic-snap-prepare-image-plugin
smethnani 44d37bb
refactor: update plugins + add spread tasks
smethnani eb11511
test: add unit tests
smethnani ae49010
fix: linting
smethnani dcc030b
fix: add model validators
smethnani 683a3b0
fix: add build-package for cross-building
smethnani 83a8c7b
fix: condition build-package on preseed key
smethnani 6300f79
fix: update test_volume.py
smethnani 72b08d9
test: filter out gadget, kernel + resolved-content directories
smethnani 86a6b02
test: fix filter
smethnani aa1ce0e
chore(merge): 'main' into work/classic-snap-prepare-image-plugin
smethnani 4c517a0
fix: pr feedback
smethnani 86190fa
fix: extract shared helper fn (#324)
Copilot 60e09f8
test: add unit tests for snap-preseed-channel and snap-preseed-valida…
Copilot cbd661f
fix: pr feedback
smethnani 71f47d0
fix: validation message
smethnani efaa248
test: fix asserts
smethnani 07df7d2
test: add channel and validation option tests
smethnani 7841aa6
test: add uc-prepare tests for assertions and snaps options
smethnani 565781a
fix: add build-for
smethnani 5227330
test: check arch of installed core snap
smethnani d05c876
fix: add snap ref validator
smethnani 51af373
chore(merge): 'main' into work/classic-snap-prepare-image-plugin
smethnani e8f1268
fix: add max length contraint
smethnani e1dfa3b
test: extract snap.yaml file
smethnani 081c72e
fix: add --write-revisions key
smethnani c1565d0
fix: add max / in channel constraint
smethnani 0daf2bd
chore(merge): 'main' into work/classic-snap-prepare-image-plugin
smethnani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # This file is part of imagecraft. | ||
| # | ||
| # Copyright 2026 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 warranties of MERCHANTABILITY, | ||
| # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| # General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License along | ||
| # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| """Shared plugin utilities.""" | ||
|
|
||
| from craft_application.models.constraints import PROJECT_NAME_COMPILED_REGEX | ||
|
|
||
| VALID_RISKS = ["stable", "candidate", "beta", "edge"] | ||
| MAX_LEN = 40 | ||
| MAX_CHANNEL_PARTS = 3 | ||
|
|
||
|
|
||
| def validate_snap_refs(snaps: list[str]) -> list[str]: | ||
| """Validate a list of snap references.""" | ||
| for snap in snaps: | ||
| if snap.endswith(".snap"): | ||
| continue | ||
|
|
||
| parts = snap.split("/") | ||
|
|
||
| name = parts[0] | ||
| if not (len(name) <= MAX_LEN and PROJECT_NAME_COMPILED_REGEX.match(name)): | ||
| raise ValueError(f"Invalid snap reference {snap}") | ||
|
|
||
| if (channel_parts := parts[1:]) and ( | ||
| len(channel_parts) > 1 | ||
| and not any(p in VALID_RISKS for p in channel_parts) | ||
| or len(channel_parts) > MAX_CHANNEL_PARTS | ||
| ): | ||
| raise ValueError(f"Invalid snap reference {snap}") | ||
|
|
||
| return snaps | ||
|
|
||
|
|
||
| def resolve_snap(snap: str) -> str: | ||
| """Resolve a snap reference to the format expected by snap prepare-image. | ||
|
|
||
| If the snap reference contains a channel (e.g. ``name/track/risk``), it is | ||
| converted to the ``name=track/risk`` form that ``snap prepare-image`` | ||
| expects. Plain snap names and local ``.snap`` file paths are returned | ||
| unchanged. | ||
| """ | ||
| snap = snap.strip() | ||
| if "/" in snap and not snap.endswith(".snap"): | ||
| name, channel = snap.split("/", 1) | ||
| snap = f"{name}={channel}" | ||
| return snap |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # This file is part of imagecraft. | ||
| # | ||
| # Copyright 2026 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 warranties of MERCHANTABILITY, | ||
| # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| # General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License along | ||
| # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| """The snap-preseed plugin.""" | ||
|
|
||
| import shlex | ||
| from typing import Literal, cast | ||
|
|
||
| from craft_parts.plugins import Plugin, PluginProperties | ||
|
lengau marked this conversation as resolved.
|
||
| from pydantic import field_validator | ||
| from typing_extensions import override | ||
|
|
||
| from ._utils import resolve_snap, validate_snap_refs | ||
|
|
||
|
|
||
| class SnapPreseedPluginProperties(PluginProperties, frozen=True): | ||
| """Properties for the 'snap-preseed' plugin.""" | ||
|
|
||
| plugin: Literal["snap-preseed"] = "snap-preseed" | ||
|
|
||
| snap_preseed_snaps: list[str] | ||
|
lengau marked this conversation as resolved.
|
||
| snap_preseed_channel: str | None = None | ||
| snap_preseed_model_assert: str = "" | ||
| snap_preseed_validation: Literal["ignore", "enforce"] = "ignore" | ||
| snap_preseed_assertions: list[str] = [] | ||
| snap_preseed_revisions: str | None = None | ||
| snap_preseed_write_revisions: str | bool = False | ||
|
|
||
| @field_validator("snap_preseed_snaps") | ||
| @classmethod | ||
| def _validate_snap_refs(cls, snaps: list[str]) -> list[str]: | ||
| return validate_snap_refs(snaps) | ||
|
|
||
|
smethnani marked this conversation as resolved.
|
||
|
|
||
| class SnapPreseedPlugin(Plugin): | ||
| """Prepare snaps for Ubuntu Classic images using 'snap prepare-image'.""" | ||
|
|
||
| properties_class = SnapPreseedPluginProperties | ||
|
|
||
| @override | ||
| def get_build_snaps(self) -> set[str]: | ||
| """Return a set of required snaps to install in the build environment.""" | ||
| return set() | ||
|
|
||
| @override | ||
| def get_build_packages(self) -> set[str]: | ||
| """Return a set of required packages to install in the build environment.""" | ||
| return set() | ||
|
|
||
| @override | ||
| def get_build_environment(self) -> dict[str, str]: | ||
| """Return a dictionary with the environment to use in the build step.""" | ||
| return {} | ||
|
|
||
| @override | ||
| def get_build_commands(self) -> list[str]: | ||
| """Return a list of commands to run during the build step.""" | ||
| options = cast(SnapPreseedPluginProperties, self._options) | ||
| cmd = [ | ||
| "snap", | ||
| "prepare-image", | ||
| "--classic", | ||
| f"--arch={self._part_info.target_arch}", | ||
| f"--validation={options.snap_preseed_validation}", | ||
| ] | ||
| if options.snap_preseed_channel: | ||
| cmd.append(f"--channel={options.snap_preseed_channel}") | ||
|
|
||
| if options.snap_preseed_revisions: | ||
| cmd.append(f"--revisions={options.snap_preseed_revisions}") | ||
|
|
||
| if options.snap_preseed_write_revisions: | ||
| revisions_path = self._part_info.part_install_dir / ( | ||
| "seed.manifest" | ||
| if isinstance(options.snap_preseed_write_revisions, bool) | ||
| else options.snap_preseed_write_revisions.lstrip("/") | ||
| ) | ||
|
|
||
| revisions_path.parent.mkdir(parents=True, exist_ok=True) | ||
| cmd.append(f"--write-revisions={revisions_path}") | ||
|
|
||
| cmd.extend( | ||
| f"--assert={assertion}" for assertion in options.snap_preseed_assertions | ||
| ) | ||
|
|
||
| cmd.extend( | ||
| f"--snap={resolve_snap(snap)}" for snap in options.snap_preseed_snaps | ||
| ) | ||
|
|
||
| cmd.append(options.snap_preseed_model_assert) | ||
| cmd.append(str(self._part_info.part_install_dir)) | ||
| return [shlex.join(cmd)] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| # This file is part of imagecraft. | ||
| # | ||
| # Copyright 2026 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 warranties of MERCHANTABILITY, | ||
| # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| # General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License along | ||
| # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| """The uc-prepare plugin.""" | ||
|
|
||
| from typing import Literal, cast | ||
|
|
||
| from craft_parts.plugins import Plugin, PluginProperties | ||
| from pydantic import field_validator, model_validator | ||
| from typing_extensions import Self, override | ||
|
|
||
| from ._utils import resolve_snap, validate_snap_refs | ||
|
|
||
|
|
||
| class UcPreparePluginProperties(PluginProperties, frozen=True): | ||
| """Properties for the uc-prepare plugin.""" | ||
|
|
||
| plugin: Literal["uc-prepare"] = "uc-prepare" | ||
|
|
||
| uc_prepare_model_assert: str | ||
| uc_prepare_snaps: list[str] = [] | ||
| uc_prepare_channel: str | None = None | ||
| uc_prepare_validation: Literal["ignore", "enforce"] = "ignore" | ||
| uc_prepare_assertions: list[str] = [] | ||
| uc_prepare_revisions: str | None = None | ||
| uc_prepare_write_revisions: str | bool = False | ||
| uc_prepare_preseed: bool = False | ||
| uc_prepare_preseed_sign_key: str | None = None | ||
| uc_prepare_apparmor_features_dir: str | None = None | ||
| uc_prepare_sysfs_overlay: str | None = None | ||
|
|
||
| @model_validator(mode="after") | ||
| def sign_key_requires_preseed(self) -> Self: | ||
| """preseed-sign-key requires preseed to be enabled.""" | ||
| if self.uc_prepare_preseed_sign_key and not self.uc_prepare_preseed: | ||
| raise ValueError( | ||
| "uc-prepare-preseed-sign-key requires uc-prepare-preseed to be set" | ||
| ) | ||
| return self | ||
|
smethnani marked this conversation as resolved.
|
||
|
|
||
| @model_validator(mode="after") | ||
| def sysfs_overlay_requires_preseed(self) -> Self: | ||
| """sysfs-overlay requires preseed to be enabled.""" | ||
| if self.uc_prepare_sysfs_overlay and not self.uc_prepare_preseed: | ||
| raise ValueError( | ||
| "uc-prepare-sysfs-overlay requires uc-prepare-preseed to be set" | ||
| ) | ||
| return self | ||
|
smethnani marked this conversation as resolved.
|
||
|
|
||
| @field_validator("uc_prepare_snaps") | ||
| @classmethod | ||
| def _validate_snap_refs(cls, snaps: list[str]) -> list[str]: | ||
| return validate_snap_refs(snaps) | ||
|
|
||
|
|
||
| class UcPreparePlugin(Plugin): | ||
| """Prepare snaps for Ubuntu Core using 'snap prepare-image'.""" | ||
|
|
||
| properties_class = UcPreparePluginProperties | ||
|
|
||
| @override | ||
| def get_build_snaps(self) -> set[str]: | ||
| """Return a set of required snaps to install in the build environment.""" | ||
| return set() | ||
|
|
||
| @override | ||
| def get_build_packages(self) -> set[str]: | ||
| """Return a set of required packages to install in the build environment.""" | ||
| options = cast(UcPreparePluginProperties, self._options) | ||
| if ( | ||
| options.uc_prepare_preseed | ||
| and self._part_info.host_arch != self._part_info.target_arch | ||
| ): | ||
| return {"qemu-user-static"} | ||
| return set() | ||
|
smethnani marked this conversation as resolved.
|
||
|
|
||
| @override | ||
| def get_build_environment(self) -> dict[str, str]: | ||
| """Return a dictionary with the environment to use in the build step.""" | ||
| return {} | ||
|
|
||
| @override | ||
| def get_build_commands(self) -> list[str]: | ||
| """Return a list of commands to run during the build step.""" | ||
| options = cast(UcPreparePluginProperties, self._options) | ||
|
|
||
| cmd = ["snap", "prepare-image"] | ||
|
|
||
| if options.uc_prepare_preseed: | ||
| cmd.append("--preseed") | ||
|
|
||
| if options.uc_prepare_preseed_sign_key: | ||
| cmd.append(f"--preseed-sign-key={options.uc_prepare_preseed_sign_key}") | ||
|
|
||
| if options.uc_prepare_apparmor_features_dir: | ||
| cmd.append( | ||
| f"--apparmor-features-dir={options.uc_prepare_apparmor_features_dir}" | ||
| ) | ||
|
|
||
| if options.uc_prepare_sysfs_overlay: | ||
| cmd.append(f"--sysfs-overlay={options.uc_prepare_sysfs_overlay}") | ||
|
|
||
| cmd.append(f"--validation={options.uc_prepare_validation}") | ||
|
|
||
| if options.uc_prepare_channel: | ||
| cmd.append(f"--channel={options.uc_prepare_channel}") | ||
|
|
||
| if options.uc_prepare_revisions: | ||
| cmd.append(f"--revisions={options.uc_prepare_revisions}") | ||
|
|
||
| if options.uc_prepare_write_revisions: | ||
| revisions_path = self._part_info.part_install_dir / ( | ||
| "seed.manifest" | ||
| if isinstance(options.uc_prepare_write_revisions, bool) | ||
| else options.uc_prepare_write_revisions.lstrip("/") | ||
| ) | ||
|
|
||
| revisions_path.parent.mkdir(parents=True, exist_ok=True) | ||
| cmd.append(f"--write-revisions={revisions_path}") | ||
|
|
||
| cmd.extend( | ||
| f"--assert={assertion}" for assertion in options.uc_prepare_assertions | ||
| ) | ||
|
|
||
| cmd.extend(f"--snap={resolve_snap(snap)}" for snap in options.uc_prepare_snaps) | ||
|
|
||
| cmd.append( | ||
| f"{options.uc_prepare_model_assert} {self._part_info.part_install_dir}" | ||
| ) | ||
|
smethnani marked this conversation as resolved.
|
||
|
|
||
| return [" ".join(cmd)] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| name: snap-preseed-test | ||
| version: "0.1" | ||
| summary: Test snap-preseed plugin | ||
| description: Test snap-preseed plugin | ||
| base: bare | ||
| build-base: ubuntu@24.04 | ||
|
|
||
| platforms: | ||
| armhf: | ||
| build-on: [amd64] | ||
| build-for: [armhf] | ||
|
|
||
| volumes: | ||
| disk: | ||
| schema: gpt | ||
| structure: | ||
| - name: rootfs | ||
| role: system-data | ||
| type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4 | ||
| filesystem: ext4 | ||
| filesystem-label: writable | ||
| size: 2G | ||
|
|
||
| filesystems: | ||
| default: | ||
| - mount: / | ||
| device: (volume/disk/rootfs) | ||
|
|
||
| parts: | ||
| snaps: | ||
| plugin: snap-preseed | ||
| snap-preseed-snaps: | ||
| - hello-world/latest/stable | ||
| organize: | ||
| "var/*": (overlay)/var/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| summary: Test snap-preseed plugin | ||
|
|
||
| systems: [ubuntu-24.04-64] | ||
|
|
||
| execute: | | ||
| imagecraft pack --verbose --destructive-mode | ||
| test -f disk.img | ||
| test -d prime/var/lib/snapd/seed/ | ||
| test -f prime/var/lib/snapd/seed/snaps/hello-world_*.snap | ||
|
lengau marked this conversation as resolved.
|
||
| test -f prime/var/lib/snapd/seed/snaps/core_*.snap | ||
| unsquashfs -d core-snap prime/var/lib/snapd/seed/snaps/core_*.snap --extract-file meta/snap.yaml | ||
| grep -q "armhf" core-snap/meta/snap.yaml | ||
| restore: | | ||
| rm -rf disk.img core-snap | ||
| imagecraft clean --destructive-mode | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.