├── .fmf └── version ├── .claude └── CLAUDE.md ├── crates ├── lib │ ├── README.md │ ├── LICENSE-MIT │ ├── LICENSE-APACHE │ └── src │ │ ├── systemglue │ │ └── mod.rs │ │ ├── parsers │ │ └── mod.rs │ │ ├── fixtures │ │ ├── spec-v1-null.json │ │ ├── spec-rfe-ostree-deployment.yaml │ │ ├── spec-via-local-oci.yaml │ │ ├── spec-v1a1.yaml │ │ ├── spec-only-booted.yaml │ │ ├── spec-ostree-remote.yaml │ │ ├── spec-ostree-to-bootc.yaml │ │ ├── spec-v1a1-orig.yaml │ │ ├── spec-staged-rollback.yaml │ │ ├── spec-booted-pinned.yaml │ │ └── spec-staged-booted.yaml │ │ ├── metadata.rs │ │ ├── bootc_composefs │ │ ├── mod.rs │ │ └── service.rs │ │ ├── glyph.rs │ │ ├── lib.rs │ │ ├── k8sapitypes.rs │ │ ├── reboot.rs │ │ ├── journal.rs │ │ ├── composefs_consts.rs │ │ ├── podman.rs │ │ └── containerenv.rs ├── ostree-ext │ ├── .gitignore │ ├── tests │ │ └── it │ │ │ └── fixtures │ │ │ └── hlinks.tar.gz │ ├── src │ │ ├── fixtures │ │ │ ├── ostree-gpg-test-home.tar.gz │ │ │ └── fedora-coreos-contentmeta.json.gz │ │ ├── container │ │ │ └── tests │ │ │ │ └── it │ │ │ │ └── fixtures │ │ │ │ └── exampleos.tar.zst │ │ ├── objgv.rs │ │ ├── utils.rs │ │ ├── ostree_manual.rs │ │ ├── logging.rs │ │ ├── selinux.rs │ │ ├── docgen.rs │ │ ├── isolation.rs │ │ ├── keyfileext.rs │ │ └── tar │ │ │ └── mod.rs │ ├── ci │ │ ├── lints.sh │ │ ├── ima.sh │ │ ├── integration.sh │ │ ├── installdeps.sh │ │ ├── container-build-integration.sh │ │ └── priv-test-cockpit-selinux.sh │ ├── deny.toml │ ├── man │ │ └── ostree-container-auth.md │ └── LICENSE-MIT ├── system-reinstall-bootc │ ├── sample_config.yaml │ ├── src │ │ ├── config │ │ │ ├── cli.rs │ │ │ └── mod.rs │ │ └── btrfs.rs │ ├── tests │ │ └── fixtures │ │ │ └── loginctl.json │ └── Cargo.toml ├── cli │ ├── bootc-generator-stub │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── sysusers │ ├── src │ │ └── nameservice │ │ │ └── mod.rs │ └── Cargo.toml ├── initramfs │ ├── src │ │ └── main.rs │ ├── bootc-root-setup.service │ ├── dracut │ │ └── module-setup.sh │ └── Cargo.toml ├── utils │ ├── src │ │ ├── timestamp.rs │ │ ├── lib.rs │ │ ├── result_ext.rs │ │ ├── tracing_util.rs │ │ └── reexec.rs │ └── Cargo.toml ├── kernel_cmdline │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── etc-merge │ └── Cargo.toml ├── tmpfiles │ └── Cargo.toml ├── mount │ └── Cargo.toml ├── blockdev │ └── Cargo.toml ├── xtask │ └── Cargo.toml └── tests-integration │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── selinux.rs │ └── hostpriv.rs ├── baseimage ├── base │ ├── ostree │ ├── usr │ │ └── lib │ │ │ └── ostree │ │ │ └── prepare-root.conf │ └── sysroot │ │ └── .gitignore ├── systemd │ └── usr │ │ └── lib │ │ └── kernel │ │ └── install.conf ├── dracut │ └── usr │ │ └── lib │ │ └── dracut.conf.d │ │ └── 10-bootc-base.conf └── README.md ├── docs ├── .gitignore ├── src │ ├── related.md │ ├── experimental-fsck.md │ ├── man │ │ ├── bootc-composefs-finalize-staged.8.md │ │ ├── bootc-config-diff.8.md │ │ ├── bootc-status-updated.path.5.md │ │ ├── bootc-status-updated.target.5.md │ │ ├── bootc-container.8.md │ │ ├── bootc-install-finalize.8.md │ │ ├── bootc-edit.8.md │ │ ├── bootc-fetch-apply-updates.service.5.md │ │ ├── bootc-install-ensure-completion.8.md │ │ ├── bootc-install-print-configuration.8.md │ │ ├── bootc-usr-overlay.8.md │ │ ├── bootc-config.5.md │ │ ├── bootc-container-lint.8.md │ │ ├── bootc.8.md │ │ ├── system-reinstall-bootc.8.md │ │ ├── bootc-install-config.5.md │ │ ├── bootc-install.8.md │ │ ├── bootc-rollback.8.md │ │ └── bootc-root-setup.service.5.md │ ├── relationship-oci-artifacts.md │ ├── experimental-bootc-image.md │ ├── bootc-in-container.md │ ├── bootloaders.md │ ├── installation.md │ ├── intro.md │ ├── bootc-via-api.md │ ├── booting-local-builds.md │ ├── experimental-progress-fd.md │ └── upgrades.md ├── _sass │ └── color_schemes │ │ └── coreos.scss ├── book.toml ├── Dockerfile.mdbook └── _config.yml ├── tmt ├── tests │ ├── booted │ │ ├── .gitignore │ │ ├── readonly │ │ │ ├── tap.nu │ │ │ ├── 011-hostname.nu │ │ │ ├── 002-test-cli.nu │ │ │ ├── 015-test-fsck.nu │ │ │ ├── 020-test-relabel.nu │ │ │ ├── 010-test-bootc-container-store.nu │ │ │ ├── 051-test-initramfs.nu │ │ │ ├── 017-test-bound-storage.nu │ │ │ ├── 012-test-unit-status.nu │ │ │ ├── 030-test-locking-read.nu │ │ │ ├── 011-test-ostree-ext-cli.nu │ │ │ ├── 011-test-resolvconf.nu │ │ │ ├── 030-test-composefs.nu │ │ │ └── 001-test-status.nu │ │ ├── README.md │ │ ├── tap.nu │ │ ├── test-01-readonly.nu │ │ ├── test-26-examples-build.sh │ │ ├── bootc_testlib.nu │ │ ├── test-usroverlay.nu │ │ ├── test-install-outside-container.nu │ │ └── test-image-upgrade-reboot.nu │ ├── lbi │ │ └── usr │ │ │ └── share │ │ │ └── containers │ │ │ └── systemd │ │ │ ├── curl-base.image │ │ │ ├── podman.image │ │ │ ├── curl.container │ │ │ └── jboss-webserver-5.image │ ├── Dockerfile.upgrade │ ├── examples │ │ ├── bootc-bls │ │ │ ├── extra │ │ │ │ └── usr │ │ │ │ │ └── lib │ │ │ │ │ └── dracut │ │ │ │ │ ├── dracut.conf.d │ │ │ │ │ └── 37composefs.conf │ │ │ │ │ └── modules.d │ │ │ │ │ └── 37bootc │ │ │ │ │ ├── module-setup.sh │ │ │ │ │ └── bootc-initramfs-setup.service │ │ │ ├── build │ │ │ └── Containerfile │ │ └── bootc-uki │ │ │ ├── extra │ │ │ └── usr │ │ │ │ └── lib │ │ │ │ └── dracut │ │ │ │ ├── dracut.conf.d │ │ │ │ └── 37composefs.conf │ │ │ │ └── modules.d │ │ │ │ └── 37bootc │ │ │ │ ├── module-setup.sh │ │ │ │ └── bootc-initramfs-setup.service │ │ │ ├── Containerfile.stage1 │ │ │ ├── build.base │ │ │ ├── build_vars │ │ │ ├── install-grub.sh │ │ │ ├── install-systemd-boot.sh │ │ │ ├── Containerfile.stage2 │ │ │ └── build.final │ └── tests.fmf ├── README.md └── bug-soft-reboot.md ├── hack ├── test-kargs │ ├── 20-test2.toml │ └── 10-test.toml ├── packages.txt ├── lbi │ └── usr │ │ └── share │ │ └── containers │ │ └── systemd │ │ ├── curl-base.image │ │ ├── podman.image │ │ ├── curl.container │ │ └── jboss-webserver-5.image ├── lldb │ ├── etc │ │ ├── sysctl.conf │ │ ├── sudoers.d │ │ │ └── wheel-nopasswd │ │ └── systemd │ │ │ └── system │ │ │ └── lldb-server.service │ ├── Containerfile │ └── deploy.sh ├── install-test-configs │ └── 50-test-kargs.toml ├── Containerfile.drop-lbis ├── os-image-map.json ├── packit-reboot.yml ├── system-reinstall-bootc.exp ├── Containerfile └── Containerfile.packit ├── .cargo └── config.toml ├── .bootc-dev-infra-commit.txt ├── renovate.json ├── .gitignore ├── contrib ├── packaging │ ├── usr-extras │ │ └── lib │ │ │ └── bootc │ │ │ └── kargs.d │ │ │ └── 21-console-hvc0.toml │ ├── README-usr-extras.md │ ├── fedora-extra.txt │ ├── install-buildroot │ ├── configure-variant │ ├── install-rpm-and-setup │ ├── build-rpm │ └── configure-rootfs └── scripts │ └── fedora-bootc-destructive-cleanup ├── clippy.toml ├── .github ├── auto-review-config.yml ├── workflows │ ├── labeler.yml │ ├── autovendor.yml │ ├── docs.yml │ └── auto-review.yml └── labeler.yml ├── systemd ├── bootc-status-updated.target ├── bootc-status-updated-onboot.target ├── bootc-fetch-apply-updates.service ├── bootc-status-updated.path ├── bootc-destructive-cleanup.service ├── bootc-publish-rhsm-facts.service ├── bootc-fetch-apply-updates.timer └── bootc-finalize-staged.service ├── tests ├── container │ ├── run │ └── reboot │ │ ├── bootc-test-reboot.service │ │ ├── bootc-finish-test-reboot.service │ │ └── run └── build-sealed ├── ci ├── Containerfile.install-fsverity ├── Dockerfile.ci ├── installdeps.sh └── run-kola.sh ├── deny.toml ├── .gemini └── config.yaml ├── .dockerignore ├── .devcontainer └── devcontainer.json ├── SECURITY.md ├── AGENTS.md ├── LICENSE-MIT ├── MAINTAINERS.md └── ADOPTERS.md /.fmf/version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /.claude/CLAUDE.md: -------------------------------------------------------------------------------- 1 | ../AGENTS.md -------------------------------------------------------------------------------- /crates/lib/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /baseimage/base/ostree: -------------------------------------------------------------------------------- 1 | sysroot/ostree -------------------------------------------------------------------------------- /crates/lib/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /crates/lib/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | mermaid*.js 3 | -------------------------------------------------------------------------------- /docs/src/related.md: -------------------------------------------------------------------------------- 1 | # Related projects 2 | -------------------------------------------------------------------------------- /tmt/tests/booted/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/tap.nu: -------------------------------------------------------------------------------- 1 | ../tap.nu -------------------------------------------------------------------------------- /crates/lib/src/systemglue/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod generator; 2 | -------------------------------------------------------------------------------- /docs/_sass/color_schemes/coreos.scss: -------------------------------------------------------------------------------- 1 | $link-color: #53a3da; 2 | -------------------------------------------------------------------------------- /hack/test-kargs/20-test2.toml: -------------------------------------------------------------------------------- 1 | kargs = ["testing-kargsd=3"] 2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | -------------------------------------------------------------------------------- /.bootc-dev-infra-commit.txt: -------------------------------------------------------------------------------- 1 | b23aa64010d014befa5adc5bc54363b6fb60a3e4 2 | -------------------------------------------------------------------------------- /baseimage/base/usr/lib/ostree/prepare-root.conf: -------------------------------------------------------------------------------- 1 | [composefs] 2 | enabled = true 3 | -------------------------------------------------------------------------------- /hack/test-kargs/10-test.toml: -------------------------------------------------------------------------------- 1 | kargs = ["kargsd-test=1", "kargsd-othertest=2"] 2 | -------------------------------------------------------------------------------- /tmt/tests/booted/README.md: -------------------------------------------------------------------------------- 1 | # Booted tests 2 | 3 | These are intended to run via tmt. 4 | -------------------------------------------------------------------------------- /crates/lib/src/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod bls_config; 2 | pub(crate) mod grub_menuconfig; 3 | -------------------------------------------------------------------------------- /hack/packages.txt: -------------------------------------------------------------------------------- 1 | # Needed by tmt 2 | rsync 3 | cloud-init 4 | /usr/bin/flock 5 | /usr/bin/awk 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | example 2 | .cosa 3 | _kola_temp 4 | bootc.tar.zst 5 | 6 | # Added by cargo 7 | /target 8 | -------------------------------------------------------------------------------- /baseimage/base/sysroot/.gitignore: -------------------------------------------------------------------------------- 1 | # A trick to keep an empty directory in git 2 | * 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /crates/ostree-ext/.gitignore: -------------------------------------------------------------------------------- 1 | example 2 | 3 | 4 | # Added by cargo 5 | 6 | /target 7 | Cargo.lock 8 | -------------------------------------------------------------------------------- /hack/lbi/usr/share/containers/systemd/curl-base.image: -------------------------------------------------------------------------------- 1 | [Image] 2 | Image=quay.io/curl/curl-base:latest 3 | -------------------------------------------------------------------------------- /hack/lldb/etc/sysctl.conf: -------------------------------------------------------------------------------- 1 | net.ipv6.conf.all.disable_ipv6 = 1 2 | net.ipv6.conf.default.disable_ipv6 = 1 3 | -------------------------------------------------------------------------------- /tmt/tests/lbi/usr/share/containers/systemd/curl-base.image: -------------------------------------------------------------------------------- 1 | [Image] 2 | Image=quay.io/curl/curl-base:latest 3 | -------------------------------------------------------------------------------- /hack/lbi/usr/share/containers/systemd/podman.image: -------------------------------------------------------------------------------- 1 | [Image] 2 | Image=registry.access.redhat.com/ubi9/podman:latest 3 | -------------------------------------------------------------------------------- /hack/install-test-configs/50-test-kargs.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | kargs = ["localtestkarg=somevalue", "otherlocalkarg=42"] 3 | -------------------------------------------------------------------------------- /tmt/tests/lbi/usr/share/containers/systemd/podman.image: -------------------------------------------------------------------------------- 1 | [Image] 2 | Image=registry.access.redhat.com/ubi9/podman:latest 3 | -------------------------------------------------------------------------------- /hack/lldb/etc/sudoers.d/wheel-nopasswd: -------------------------------------------------------------------------------- 1 | # Enable passwordless sudo for the wheel group 2 | %wheel ALL=(ALL) NOPASSWD: ALL 3 | -------------------------------------------------------------------------------- /crates/system-reinstall-bootc/sample_config.yaml: -------------------------------------------------------------------------------- 1 | # The bootc container image to install 2 | bootc_image: quay.io/fedora/fedora-bootc:41 3 | -------------------------------------------------------------------------------- /contrib/packaging/usr-extras/lib/bootc/kargs.d/21-console-hvc0.toml: -------------------------------------------------------------------------------- 1 | # https://bugzilla.redhat.com/show_bug.cgi?id=2353887 2 | kargs = ["console=hvc0"] 3 | -------------------------------------------------------------------------------- /crates/ostree-ext/tests/it/fixtures/hlinks.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/ci-sandbox/main/crates/ostree-ext/tests/it/fixtures/hlinks.tar.gz -------------------------------------------------------------------------------- /crates/ostree-ext/src/fixtures/ostree-gpg-test-home.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/ci-sandbox/main/crates/ostree-ext/src/fixtures/ostree-gpg-test-home.tar.gz -------------------------------------------------------------------------------- /hack/lbi/usr/share/containers/systemd/curl.container: -------------------------------------------------------------------------------- 1 | [Container] 2 | Image=quay.io/curl/curl:latest 3 | GlobalArgs=--storage-opt=additionalimagestore=/usr/lib/bootc/storage 4 | -------------------------------------------------------------------------------- /hack/Containerfile.drop-lbis: -------------------------------------------------------------------------------- 1 | FROM localhost/bootc-integration 2 | # Workaround for https://github.com/bootc-dev/bootc/issues/1618 3 | RUN rm -rf /usr/lib/bootc/bound-images.d/* 4 | -------------------------------------------------------------------------------- /tmt/tests/lbi/usr/share/containers/systemd/curl.container: -------------------------------------------------------------------------------- 1 | [Container] 2 | Image=quay.io/curl/curl:latest 3 | GlobalArgs=--storage-opt=additionalimagestore=/usr/lib/bootc/storage 4 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/fixtures/fedora-coreos-contentmeta.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/ci-sandbox/main/crates/ostree-ext/src/fixtures/fedora-coreos-contentmeta.json.gz -------------------------------------------------------------------------------- /crates/ostree-ext/src/container/tests/it/fixtures/exampleos.tar.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/ci-sandbox/main/crates/ostree-ext/src/container/tests/it/fixtures/exampleos.tar.zst -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-methods = [ 2 | # https://github.com/rust-lang/rust-clippy/issues/13434#issuecomment-2484292914 3 | { path = "str::len", reason = "use .as_bytes().len() instead" } 4 | ] 5 | -------------------------------------------------------------------------------- /.github/auto-review-config.yml: -------------------------------------------------------------------------------- 1 | # Auto-reviewer configuration 2 | # Start date for the rotation cycle (YYYY-MM-DD format) 3 | start_date: "2025-08-04" 4 | 5 | # Rotation cycle in weeks 6 | rotation_cycle_weeks: 3 7 | -------------------------------------------------------------------------------- /systemd/bootc-status-updated.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Target for bootc status changes 3 | Documentation=man:bootc-status-updated.target(8) 4 | StopWhenUnneeded=true 5 | ConditionPathExists=/run/ostree-booted 6 | -------------------------------------------------------------------------------- /tmt/tests/Dockerfile.upgrade: -------------------------------------------------------------------------------- 1 | # Just creates a file as a new layer for a synthetic upgrade test 2 | FROM localhost/bootc-integration 3 | RUN touch --reference=/usr/bin/bash /usr/share/testing-bootc-upgrade-apply 4 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-bls/extra/usr/lib/dracut/dracut.conf.d/37composefs.conf: -------------------------------------------------------------------------------- 1 | # we need to force these in via the initramfs because we don't have modules in 2 | # the base image 3 | force_drivers+=" virtio_net vfat " 4 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/extra/usr/lib/dracut/dracut.conf.d/37composefs.conf: -------------------------------------------------------------------------------- 1 | # we need to force these in via the initramfs because we don't have modules in 2 | # the base image 3 | force_drivers+=" virtio_net vfat " 4 | -------------------------------------------------------------------------------- /crates/ostree-ext/ci/lints.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | tmpf=$(mktemp) 4 | git grep 'dbg!' '*.rs' > ${tmpf} || true 5 | if test -s ${tmpf}; then 6 | echo "Found dbg!" 1>&2 7 | cat "${tmpf}" 8 | exit 1 9 | fi -------------------------------------------------------------------------------- /systemd/bootc-status-updated-onboot.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Bootc status trigger state sync 3 | Documentation=man:bootc-status-updated.target(8) 4 | After=sysinit.target 5 | 6 | # No [Install] section, this is enabled via generator 7 | -------------------------------------------------------------------------------- /systemd/bootc-fetch-apply-updates.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Apply bootc updates 3 | Documentation=man:bootc(8) 4 | ConditionPathExists=/run/ostree-booted 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/bin/bootc upgrade --apply --quiet 9 | -------------------------------------------------------------------------------- /crates/system-reinstall-bootc/src/config/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | pub(crate) struct Cli { 5 | /// The bootc container image to install, e.g. quay.io/fedora/fedora-bootc:41 6 | pub(crate) bootc_image: String, 7 | } 8 | -------------------------------------------------------------------------------- /crates/cli/bootc-generator-stub: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # We can't actually hardlink because in Fedora (+derivatives) 3 | # these have different SELinux labels. xref 4 | # https://issues.redhat.com/browse/RHEL-76188 5 | exec bootc internals systemd-generator "$@" 6 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-v1-null.json: -------------------------------------------------------------------------------- 1 | {"apiVersion":"org.containers.bootc/v1","kind":"BootcHost","metadata":{"name":"host"},"spec":{"image":null,"bootOrder":"default"},"status":{"staged":null,"booted":null,"rollback":null,"rollbackQueued":false,"type":null}} -------------------------------------------------------------------------------- /crates/lib/src/metadata.rs: -------------------------------------------------------------------------------- 1 | /// This label is expected to be present on compatible base images. 2 | pub(crate) const BOOTC_COMPAT_LABEL: &str = "containers.bootc"; 3 | /// The current single well-known value for the label. 4 | pub(crate) const COMPAT_LABEL_V1: &str = "1"; 5 | -------------------------------------------------------------------------------- /crates/sysusers/src/nameservice/mod.rs: -------------------------------------------------------------------------------- 1 | //! Linux name-service information helpers. 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | // TODO(lucab): consider moving this to its own crate. 4 | 5 | pub(crate) mod group; 6 | pub(crate) mod passwd; 7 | pub(crate) mod shadow; 8 | -------------------------------------------------------------------------------- /baseimage/systemd/usr/lib/kernel/install.conf: -------------------------------------------------------------------------------- 1 | # kernel-install will not try to run dracut and allow rpm-ostree to 2 | # take over. Rpm-ostree will use this to know that it is responsible 3 | # to run dracut and ensure that there is only one kernel in the image 4 | layout=ostree 5 | 6 | -------------------------------------------------------------------------------- /contrib/packaging/README-usr-extras.md: -------------------------------------------------------------------------------- 1 | # Understanding usr-extras 2 | 3 | The usr-extras directory contains 4 | content we inject into all container images 5 | built from this project. 6 | 7 | It is likely though that some of this will 8 | end up in downstream operating systems instead. 9 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/011-hostname.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "verify /etc/hostname is not zero sized" 5 | 6 | let hostname = try { ls /etc/hostname | first } 7 | if $hostname != null { 8 | assert not equal $hostname.size 0B 9 | } 10 | 11 | tap ok 12 | -------------------------------------------------------------------------------- /systemd/bootc-status-updated.path: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Monitor bootc for status changes 3 | Documentation=man:bootc-status-updated.path(8) 4 | 5 | [Path] 6 | PathChanged=/ostree/bootc 7 | Unit=bootc-status-updated.target 8 | 9 | # No [Install] section, this is enabled via generator 10 | -------------------------------------------------------------------------------- /crates/ostree-ext/deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | unlicensed = "deny" 3 | allow = ["Apache-2.0", "Apache-2.0 WITH LLVM-exception", "MIT", "BSD-3-Clause", "BSD-2-Clause", "Unicode-DFS-2016"] 4 | 5 | [bans] 6 | 7 | [sources] 8 | unknown-registry = "deny" 9 | unknown-git = "deny" 10 | allow-git = [] 11 | -------------------------------------------------------------------------------- /crates/system-reinstall-bootc/tests/fixtures/loginctl.json: -------------------------------------------------------------------------------- 1 | [{"session":"2","uid":1000,"user":"foo-doe","seat":"seat0","leader":3045,"class":"user","tty":"tty1","idle":false,"since":null},{"session":"3","uid":1000,"user":"foo-doe","seat":null,"leader":3148,"class":"manager","tty":null,"idle":false,"since":null}] 2 | -------------------------------------------------------------------------------- /docs/src/experimental-fsck.md: -------------------------------------------------------------------------------- 1 | # bootc internals fsck 2 | 3 | Experimental features are subject to change or removal. Please 4 | do provide feedback on them. 5 | 6 | ## Using `bootc internals fsck` 7 | 8 | This command expects a booted system, and performs consistency checks 9 | in a read-only fashion. 10 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/002-test-cli.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "verify bootc status output formats" 5 | 6 | assert equal (bootc switch blah:// e>| find "\u{1B}") [] 7 | 8 | # Verify soft-reboot is in help 9 | bootc upgrade --help | grep -qF -e '--soft-reboot' 10 | 11 | tap ok 12 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | triage: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: actions/labeler@v6 14 | -------------------------------------------------------------------------------- /crates/lib/src/bootc_composefs/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod boot; 2 | pub(crate) mod delete; 3 | pub(crate) mod finalize; 4 | pub(crate) mod gc; 5 | pub(crate) mod repo; 6 | pub(crate) mod rollback; 7 | pub(crate) mod service; 8 | pub(crate) mod state; 9 | pub(crate) mod status; 10 | pub(crate) mod switch; 11 | pub(crate) mod update; 12 | -------------------------------------------------------------------------------- /tests/container/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | image=$1 4 | shift 5 | 6 | cd $(dirname $0) 7 | 8 | tests=$(find . -maxdepth 1 -type d) 9 | for case in $tests; do 10 | if test $case = .; then continue; fi 11 | echo "Running: $case" 12 | cd $case 13 | ./run $image 14 | cd - 15 | echo "ok $case" 16 | done 17 | -------------------------------------------------------------------------------- /contrib/packaging/fedora-extra.txt: -------------------------------------------------------------------------------- 1 | # Extra packages needed in the build container for Fedora derivatives 2 | # that aren't actually used to build the code necessarily, but 3 | # are used by targets in the Dockerfile. 4 | rustfmt 5 | clippy 6 | git-core 7 | jq 8 | # We now always build a package in the container build 9 | rpm-build 10 | -------------------------------------------------------------------------------- /hack/lbi/usr/share/containers/systemd/jboss-webserver-5.image: -------------------------------------------------------------------------------- 1 | # This is not symlinked to bound-images.d so it should not be pulled. 2 | # It's here to represent an app image that exists 3 | # in a bootc image but is not logically bound. 4 | [Image] 5 | Image=registry.redhat.io/jboss-webserver-5/jws5-rhel8-operator:latest 6 | AuthFile=/root/auth.json 7 | -------------------------------------------------------------------------------- /tests/container/reboot/bootc-test-reboot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | ConditionPathExists=!/etc/initrd-release 3 | Requires=bootc-finish-test-reboot.service 4 | After=bootc-finish-test-reboot.service 5 | 6 | [Service] 7 | Type=oneshot 8 | RemainAfterExit=yes 9 | ExecStart=bootc internals reboot 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /systemd/bootc-destructive-cleanup.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cleanup previous the installation after an alongside installation 3 | Documentation=man:bootc(8) 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/usr/lib/bootc/fedora-bootc-destructive-cleanup 8 | PrivateMounts=true 9 | 10 | # No [Install] section, this is enabled via generator 11 | -------------------------------------------------------------------------------- /tmt/tests/lbi/usr/share/containers/systemd/jboss-webserver-5.image: -------------------------------------------------------------------------------- 1 | # This is not symlinked to bound-images.d so it should not be pulled. 2 | # It's here to represent an app image that exists 3 | # in a bootc image but is not logically bound. 4 | [Image] 5 | Image=registry.redhat.io/jboss-webserver-5/jws5-rhel8-operator:latest 6 | AuthFile=/root/auth.json 7 | -------------------------------------------------------------------------------- /tests/container/reboot/bootc-finish-test-reboot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | ConditionPathExists=!/etc/initrd-release 3 | After=local-fs.target 4 | RequiresMountsFor=/run/bootc-test-reboot 5 | Before=bootc-test-reboot.service 6 | PartOf=bootc-test-reboot.service 7 | 8 | [Service] 9 | Type=oneshot 10 | RemainAfterExit=yes 11 | ExecStop=touch /run/bootc-test-reboot/success 12 | -------------------------------------------------------------------------------- /tmt/tests/booted/tap.nu: -------------------------------------------------------------------------------- 1 | # A simple nushell "library" for the 2 | # "Test anything protocol": 3 | # https://testanything.org/tap-version-14-specification.html 4 | export def begin [description] { 5 | print "TAP version 14" 6 | print $description 7 | } 8 | 9 | export def ok [] { 10 | print "ok" 11 | } 12 | 13 | export def fail [] { 14 | print "not ok" 15 | } 16 | -------------------------------------------------------------------------------- /ci/Containerfile.install-fsverity: -------------------------------------------------------------------------------- 1 | # Enable fsverity at install time 2 | FROM localhost/bootc 3 | RUN < /usr/lib/ostree/prepare-root.conf < /usr/lib/bootc/install/90-ext4.toml < Result<()> { 10 | let args = Args::parse(); 11 | gpt_workaround()?; 12 | setup_root(args) 13 | } 14 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-bls/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | cd "${0%/*}" 6 | 7 | cp /usr/bin/bootc . 8 | cp /usr/lib/bootc/initramfs-setup extra/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup 9 | 10 | mkdir -p tmp 11 | 12 | podman build \ 13 | -t quay.io/fedora/fedora-bootc-bls:42 \ 14 | -f Containerfile \ 15 | --iidfile=tmp/iid \ 16 | . 17 | -------------------------------------------------------------------------------- /systemd/bootc-publish-rhsm-facts.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Publish bootc facts to Red Hat Subscription Manager 3 | Documentation=man:bootc(8) 4 | ConditionPathExists=/etc/rhsm/facts 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/bin/bootc internals publish-rhsm-facts 9 | 10 | [Install] 11 | WantedBy=bootc-status-updated.target 12 | WantedBy=bootc-status-updated-onboot.target 13 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-bls/Containerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/fedora/fedora-bootc:42 2 | COPY extra / 3 | COPY bootc /usr/bin 4 | 5 | RUN passwd -d root 6 | 7 | # need to have bootc-initramfs-setup in the initramfs so we need this 8 | RUN set -x; \ 9 | kver=$(cd /usr/lib/modules && echo *); \ 10 | dracut -vf --install "/etc/passwd /etc/group" /usr/lib/modules/$kver/initramfs.img $kver; 11 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/Containerfile.stage1: -------------------------------------------------------------------------------- 1 | FROM quay.io/fedora/fedora-bootc:42 2 | COPY extra / 3 | COPY bootc /usr/bin 4 | 5 | RUN passwd -d root 6 | 7 | # need to have composefs setup root in the initramfs so we need this 8 | RUN set -x; \ 9 | kver=$(cd /usr/lib/modules && echo *); \ 10 | dracut -vf --install "/etc/passwd /etc/group" /usr/lib/modules/$kver/initramfs.img $kver; 11 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/build.base: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | cd "${0%/*}" 6 | 7 | cp /usr/bin/bootc . 8 | cp /usr/lib/bootc/initramfs-setup extra/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup 9 | 10 | mkdir -p tmp 11 | 12 | podman build \ 13 | -t quay.io/fedora/fedora-bootc-base-uki:42 \ 14 | -f Containerfile.stage1 \ 15 | --iidfile=tmp/iid \ 16 | . 17 | -------------------------------------------------------------------------------- /crates/utils/src/timestamp.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | 3 | /// Try to parse an RFC 3339, warn on error. 4 | pub fn try_deserialize_timestamp(t: &str) -> Option> { 5 | match chrono::DateTime::parse_from_rfc3339(t).context("Parsing timestamp") { 6 | Ok(t) => Some(t.into()), 7 | Err(e) => { 8 | tracing::warn!("Invalid timestamp in image: {:#}", e); 9 | None 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /hack/os-image-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "rhel-9.8": "images.paas.redhat.com/bootc/rhel-bootc:latest-9.8", 3 | "rhel-10.2": "images.paas.redhat.com/bootc/rhel-bootc:latest-10.2", 4 | "centos-9": "quay.io/centos-bootc/centos-bootc:stream9", 5 | "centos-10": "quay.io/centos-bootc/centos-bootc:stream10", 6 | "fedora-42": "quay.io/fedora/fedora-bootc:42", 7 | "fedora-43": "quay.io/fedora/fedora-bootc:43", 8 | "fedora-44": "quay.io/fedora/fedora-bootc:rawhide" 9 | } 10 | -------------------------------------------------------------------------------- /crates/ostree-ext/ci/ima.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Assumes that the current environment is a mutable ostree-container 3 | # with ostree-ext-cli installed in /usr/bin. 4 | # Runs IMA tests. 5 | set -xeuo pipefail 6 | 7 | # https://github.com/ostreedev/ostree-rs-ext/issues/417 8 | mkdir -p /var/tmp 9 | 10 | if test '!' -x /usr/bin/evmctl; then 11 | rpm-ostree install ima-evm-utils 12 | fi 13 | 14 | ostree-ext-cli internal-only-for-testing run-ima 15 | echo ok "ima" 16 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-01-readonly.nu: -------------------------------------------------------------------------------- 1 | # number: 1 2 | # tmt: 3 | # summary: Execute booted readonly/nondestructive tests 4 | # duration: 30m 5 | # 6 | # Run all readonly tests in sequence 7 | use tap.nu 8 | 9 | tap begin "readonly tests" 10 | 11 | # Get all readonly test files and run them in order 12 | let tests = (ls booted/readonly/*-test-*.nu | get name | sort) 13 | 14 | for test_file in $tests { 15 | print $"Running ($test_file)..." 16 | nu $test_file 17 | } 18 | 19 | tap ok -------------------------------------------------------------------------------- /ci/Dockerfile.ci: -------------------------------------------------------------------------------- 1 | # This really just depends on `cosa run`, which we could 2 | # in theory split out separately at some point later. 3 | FROM quay.io/coreos-assembler/coreos-assembler:latest 4 | WORKDIR /srv 5 | USER root 6 | # Grab all of our ci scripts 7 | COPY /ci/ /ci/ 8 | # And install our tests 9 | COPY /tests/kolainst/ /usr/lib/coreos-assembler/tests/kola/bootc/ 10 | RUN ln -sr /ci/run-kola.sh /usr/bin/bootc-run-kola 11 | USER builder 12 | ENTRYPOINT [] 13 | CMD ["/usr/bin/bootc-run-kola"] 14 | -------------------------------------------------------------------------------- /systemd/bootc-fetch-apply-updates.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Apply bootc updates 3 | Documentation=man:bootc(8) 4 | ConditionPathExists=/run/ostree-booted 5 | 6 | [Timer] 7 | OnBootSec=1h 8 | # This time is relatively arbitrary and obviously expected to be overridden/changed 9 | OnUnitInactiveSec=8h 10 | # When deploying a large number of systems, it may be beneficial to increase this value to help with load on the registry. 11 | RandomizedDelaySec=2h 12 | 13 | [Install] 14 | WantedBy=timers.target 15 | -------------------------------------------------------------------------------- /hack/packit-reboot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # tmt-reboot and reboot do not work in this case 4 | # reboot in ansible is the only way to reboot in tmt prepare 5 | - name: Reboot after system-reinstall-bootc 6 | hosts: all 7 | tasks: 8 | - name: Reboot system to image mode 9 | reboot: 10 | reboot_timeout: 1200 11 | connect_timeout: 30 12 | pre_reboot_delay: 15 13 | 14 | - name: Wait for connection to become reachable/usable 15 | wait_for_connection: 16 | delay: 30 17 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | allow = ["Apache-2.0", "Apache-2.0 WITH LLVM-exception", "MIT", 3 | "BSD-3-Clause", "BSD-2-Clause", "Zlib", 4 | "Unlicense", "CC0-1.0", "BSL-1.0", 5 | "Unicode-DFS-2016", "Unicode-3.0"] 6 | private = { ignore = true } 7 | 8 | [[bans.deny]] 9 | # We want to require FIPS validation downstream, so we use openssl 10 | name = "ring" 11 | 12 | [sources] 13 | unknown-registry = "deny" 14 | unknown-git = "deny" 15 | allow-git = ["https://github.com/containers/composefs-rs"] 16 | -------------------------------------------------------------------------------- /docs/src/man/bootc-composefs-finalize-staged.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-composefs-finalize-staged - TODO: Add description 4 | 5 | # SYNOPSIS 6 | 7 | bootc composefs-finalize-staged 8 | 9 | # DESCRIPTION 10 | 11 | TODO: Add description 12 | 13 | 14 | 15 | 16 | # EXAMPLES 17 | 18 | TODO: Add practical examples showing how to use this command. 19 | 20 | # SEE ALSO 21 | 22 | **bootc**(8) 23 | 24 | # VERSION 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/src/man/bootc-config-diff.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-config-diff - Diff current /etc configuration versus default 4 | 5 | # SYNOPSIS 6 | 7 | bootc config-diff 8 | 9 | # DESCRIPTION 10 | 11 | Diff current /etc configuration versus default 12 | 13 | 14 | 15 | 16 | # EXAMPLES 17 | 18 | TODO: Add practical examples showing how to use this command. 19 | 20 | # SEE ALSO 21 | 22 | **bootc**(8) 23 | 24 | # VERSION 25 | 26 | 27 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/015-test-fsck.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "Run fsck" 5 | 6 | # Detect composefs by checking if composefs field is present 7 | let st = bootc status --json | from json 8 | let is_composefs = ($st.status.booted.composefs? != null) 9 | 10 | if $is_composefs { 11 | print "# TODO composefs: skipping test - fsck requires ostree-booted host" 12 | } else { 13 | # That's it, just ensure we've run a fsck on our basic install. 14 | bootc internals fsck 15 | } 16 | 17 | tap ok 18 | -------------------------------------------------------------------------------- /crates/kernel_cmdline/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootc-kernel-cmdline" 3 | description = "Kernel command line parsing utilities for bootc" 4 | version = "0.0.0" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/bootc-dev/bootc" 8 | 9 | [dependencies] 10 | # Workspace dependencies 11 | anyhow = { workspace = true } 12 | serde = { workspace = true, features = ["derive"] } 13 | 14 | [dev-dependencies] 15 | similar-asserts = { workspace = true } 16 | static_assertions = { workspace = true } 17 | 18 | [lints] 19 | workspace = true -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/module-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | check() { 4 | return 0 5 | } 6 | 7 | depends() { 8 | return 0 9 | } 10 | 11 | install() { 12 | inst \ 13 | "${moddir}/bootc-initramfs-setup" /usr/bin/bootc-initramfs-setup 14 | inst \ 15 | "${moddir}/bootc-initramfs-setup.service" \ 16 | "${systemdsystemunitdir}/bootc-initramfs-setup.service" 17 | 18 | $SYSTEMCTL -q --root "${initdir}" add-wants \ 19 | 'initrd-root-fs.target' 'bootc-initramfs-setup.service' 20 | } 21 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/extra/usr/lib/dracut/modules.d/37bootc/module-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | check() { 4 | return 0 5 | } 6 | 7 | depends() { 8 | return 0 9 | } 10 | 11 | install() { 12 | inst \ 13 | "${moddir}/bootc-initramfs-setup" /usr/bin/bootc-initramfs-setup 14 | inst \ 15 | "${moddir}/bootc-initramfs-setup.service" \ 16 | "${systemdsystemunitdir}/bootc-initramfs-setup.service" 17 | 18 | $SYSTEMCTL -q --root "${initdir}" add-wants \ 19 | 'initrd-root-fs.target' 'bootc-initramfs-setup.service' 20 | } 21 | -------------------------------------------------------------------------------- /crates/etc-merge/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "etc-merge" 3 | version = "0.1.0" 4 | edition = "2024" 5 | license = "MIT OR Apache-2.0" 6 | publish = false 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | cap-std-ext = { workspace = true } 11 | rustix = { workspace = true } 12 | openssl = { workspace = true } 13 | hex = { workspace = true } 14 | tracing = { workspace = true } 15 | composefs = { workspace = true } 16 | fn-error-context = { workspace = true } 17 | owo-colors = { workspace = true } 18 | anstream = { workspace = true } 19 | 20 | [lints] 21 | workspace = true 22 | -------------------------------------------------------------------------------- /docs/src/man/bootc-status-updated.path.5.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-status-updated.path 4 | 5 | # DESCRIPTION 6 | 7 | This unit watches the `bootc` root directory (/ostree/bootc) for 8 | modification, and triggers the companion `bootc-status-updated.target` 9 | systemd unit. 10 | 11 | The `bootc` program updates the mtime on its root directory when the 12 | contents of `bootc status` changes as a result of an 13 | update/upgrade/edit/switch/rollback operation. 14 | 15 | # SEE ALSO 16 | 17 | **bootc**(1), **bootc-status-updated.target**(5) 18 | 19 | # VERSION 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Colin Walters"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "bootc" 7 | 8 | [preprocessor.mermaid] 9 | command = "mdbook-mermaid" 10 | 11 | [preprocessor.header-footer] 12 | headers = [] 13 | footers = [ 14 | { padding = "\n---\n

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

" } 15 | ] 16 | 17 | [output.html] 18 | additional-js = ["mermaid.min.js", "mermaid-init.js"] 19 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/build_vars: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | cd "${0%/*}" 6 | 7 | if [[ ! -d "secureboot" ]]; then 8 | echo "fail" 9 | exit 1 10 | fi 11 | 12 | # See: https://github.com/rhuefi/qemu-ovmf-secureboot 13 | # $ dnf install -y python3-virt-firmware 14 | GUID=$(cat secureboot/GUID.txt) 15 | virt-fw-vars --input "/usr/share/edk2/ovmf/OVMF_VARS_4M.secboot.qcow2" \ 16 | --secure-boot \ 17 | --set-pk $GUID "secureboot/PK.crt" \ 18 | --add-kek $GUID "secureboot/KEK.crt" \ 19 | --add-db $GUID "secureboot/db.crt" \ 20 | -o "VARS_CUSTOM.secboot.qcow2.template" 21 | -------------------------------------------------------------------------------- /crates/initramfs/bootc-root-setup.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=bootc setup root 3 | Documentation=man:bootc(1) 4 | DefaultDependencies=no 5 | ConditionKernelCommandLine=composefs 6 | ConditionPathExists=/etc/initrd-release 7 | After=sysroot.mount 8 | After=ostree-prepare-root.service 9 | Requires=sysroot.mount 10 | Before=initrd-root-fs.target 11 | 12 | OnFailure=emergency.target 13 | OnFailureJobMode=isolate 14 | 15 | [Service] 16 | Type=oneshot 17 | ExecStart=/usr/lib/bootc/initramfs-setup setup-root 18 | StandardInput=null 19 | StandardOutput=journal 20 | StandardError=journal+console 21 | RemainAfterExit=yes 22 | -------------------------------------------------------------------------------- /contrib/packaging/install-buildroot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Install buildroot requirements for Fedora derivatives 3 | set -xeuo pipefail 4 | cd $(dirname $0) 5 | . /usr/lib/os-release 6 | case $ID in 7 | centos|rhel) dnf config-manager --set-enabled crb;; 8 | fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';; 9 | esac 10 | # Handle version skew, xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 11 | dnf -y distro-sync ostree{,-libs} systemd 12 | # Install base build requirements 13 | dnf -y builddep bootc.spec 14 | # And extra packages 15 | grep -Ev -e '^#' fedora-extra.txt | xargs dnf -y install 16 | -------------------------------------------------------------------------------- /docs/src/man/bootc-status-updated.target.5.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-status-updated.target 4 | 5 | # DESCRIPTION 6 | 7 | This unit is triggered by the companion `bootc-status-updated.path` 8 | systemd unit. This target is intended to enable users to add custom 9 | services to trigger as a result of `bootc status` changing. 10 | 11 | Add the following to your unit configuration to active it when `bootc 12 | status` changes: 13 | 14 | ``` 15 | [Install] 16 | WantedBy=bootc-status-updated.target 17 | ``` 18 | 19 | # SEE ALSO 20 | 21 | **bootc**(1), **bootc-status-updated.path**(5) 22 | 23 | # VERSION 24 | 25 | 26 | -------------------------------------------------------------------------------- /crates/ostree-ext/ci/integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Assumes that the current environment is a mutable ostree-container 3 | # with ostree-ext-cli installed in /usr/bin. 4 | # Runs integration tests. 5 | set -xeuo pipefail 6 | 7 | # Output an ok message for TAP 8 | n_tap_tests=0 9 | tap_ok() { 10 | echo "ok" "$@" 11 | n_tap_tests=$(($n_tap_tests+1)) 12 | } 13 | 14 | tap_end() { 15 | echo "1..${n_tap_tests}" 16 | } 17 | 18 | env=$(ostree-ext-cli internal-only-for-testing detect-env) 19 | test "${env}" = ostree-container 20 | tap_ok environment 21 | 22 | ostree-ext-cli internal-only-for-testing run 23 | tap_ok integrationtests 24 | 25 | tap_end 26 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/020-test-relabel.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "Relabel" 5 | 6 | let td = mktemp -d -p /var/tmp 7 | cd $td 8 | 9 | mkdir etc/ssh 10 | touch etc/shadow etc/ssh/ssh_config 11 | bootc internals relabel --as-path /etc $"(pwd)/etc" 12 | 13 | def assert_labels_equal [p] { 14 | let base = (getfattr --only-values -n security.selinux $"/($p)") 15 | let target = (getfattr --only-values -n security.selinux $p) 16 | assert equal $base $target 17 | } 18 | 19 | for path in ["etc", "etc/shadow", "etc/ssh/ssh_config"] { 20 | assert_labels_equal $path 21 | } 22 | 23 | cd / 24 | rm -rf $td 25 | 26 | tap ok 27 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Maybe in the future we have a label-per-crate, but 2 | # for now this is just a rough cut. 3 | 4 | area/documentation: 5 | - changed-files: 6 | - any-glob-to-any-file: 7 | - 'docs/**' 8 | - README.md 9 | - CONTRIBUTING.md 10 | 11 | area/install: 12 | - changed-files: 13 | - any-glob-to-any-file: 14 | - 'crates/lib/src/install.rs' 15 | - 'crates/lib/src/install/**' 16 | 17 | area/system-reinstall-bootc: 18 | - changed-files: 19 | - any-glob-to-any-file: 20 | - 'crates/system-reinstall-bootc/**' 21 | 22 | area/ostree: 23 | - changed-files: 24 | - any-glob-to-any-file: 25 | - 'crates/ostree-ext/**' 26 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-26-examples-build.sh: -------------------------------------------------------------------------------- 1 | # number: 26 2 | # tmt: 3 | # summary: Test bootc examples build scripts 4 | # duration: 45m 5 | # adjust: 6 | # - when: running_env != image_mode 7 | # enabled: false 8 | # because: packit tests use RPM bootc and does not install /usr/lib/bootc/initramfs-setup 9 | # 10 | #!/bin/bash 11 | set -eux 12 | 13 | # Test bootc-bls example 14 | echo "Testing bootc-bls example..." 15 | cd examples/bootc-bls 16 | ./build 17 | 18 | # Test bootc-uki example 19 | echo "Testing bootc-uki example..." 20 | cd ../bootc-uki 21 | ./build.base 22 | ./build.final 23 | 24 | echo "All example builds completed successfully" 25 | -------------------------------------------------------------------------------- /.gemini/config.yaml: -------------------------------------------------------------------------------- 1 | # NOTE: This file is canonically maintained in 2 | # 3 | # DO NOT EDIT 4 | # 5 | # This config mainly overrides `summary: false` by default 6 | # as it's really noisy. 7 | have_fun: true 8 | code_review: 9 | disable: false 10 | # Even medium level can be quite noisy, I don't think 11 | # we need LOW. Anyone who wants that type of stuff should 12 | # be able to get it locally or before review. 13 | comment_severity_threshold: MEDIUM 14 | max_review_comments: -1 15 | pull_request_opened: 16 | help: false 17 | summary: false # turned off by default 18 | code_review: true 19 | ignore_patterns: [] 20 | -------------------------------------------------------------------------------- /crates/initramfs/dracut/module-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | installkernel() { 3 | instmods erofs overlay 4 | } 5 | check() { 6 | # We are never installed by default; see 10-bootc-base.conf 7 | # for how base images can opt in. 8 | return 255 9 | } 10 | depends() { 11 | return 0 12 | } 13 | install() { 14 | local service=bootc-root-setup.service 15 | dracut_install /usr/lib/bootc/initramfs-setup 16 | inst_simple "${systemdsystemunitdir}/${service}" 17 | mkdir -p "${initdir}${systemdsystemconfdir}/initrd-root-fs.target.wants" 18 | ln_r "${systemdsystemunitdir}/${service}" \ 19 | "${systemdsystemconfdir}/initrd-root-fs.target.wants/${service}" 20 | } 21 | -------------------------------------------------------------------------------- /crates/initramfs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootc-initramfs-setup" 3 | version = "0.1.0" 4 | license = "MIT OR Apache-2.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | anyhow.workspace = true 10 | clap = { workspace = true, features = ["std", "help", "usage", "derive"] } 11 | libc.workspace = true 12 | rustix.workspace = true 13 | serde = { workspace = true, features = ["derive"] } 14 | composefs.workspace = true 15 | composefs-boot.workspace = true 16 | toml.workspace = true 17 | fn-error-context.workspace = true 18 | 19 | [lints] 20 | workspace = true 21 | 22 | [features] 23 | default = ['pre-6.15'] 24 | rhel9 = ['composefs/rhel9'] 25 | 'pre-6.15' = ['composefs/pre-6.15'] 26 | -------------------------------------------------------------------------------- /contrib/scripts/fedora-bootc-destructive-cleanup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # An implementation of --cleanup for bootc installs on Fedora derivatives 3 | 4 | set -xeuo pipefail 5 | 6 | # Remove all RPMs installed in the physical root (i.e. the previous OS) 7 | mount -o remount,rw /sysroot 8 | rpm -qa --root=/sysroot --dbpath=/usr/lib/sysimage/rpm | xargs rpm -e --root=/sysroot --dbpath=/usr/lib/sysimage/rpm 9 | 10 | # Remove all container images (including the one that was used to install) 11 | # Note that this does not remove stopped containers, and so some storage 12 | # may leak. This may change in the future. 13 | mount --bind -o rw /sysroot/var/lib/containers /var/lib/containers 14 | podman system prune --all -f 15 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/010-test-bootc-container-store.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "verify bootc-owned container storage" 5 | 6 | # Detect composefs by checking if composefs field is present 7 | let st = bootc status --json | from json 8 | let is_composefs = ($st.status.booted.composefs? != null) 9 | 10 | if $is_composefs { 11 | print "# TODO composefs: skipping test - /usr/lib/bootc/storage doesn't exist with composefs" 12 | } else { 13 | # Just verifying that the additional store works 14 | podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage images 15 | 16 | # And verify this works 17 | bootc image cmd list -q o>/dev/null 18 | } 19 | 20 | tap ok 21 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/051-test-initramfs.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "initramfs" 5 | 6 | if (not ("/usr/lib/bootc/initramfs-setup" | path exists)) { 7 | print "No initramfs support" 8 | } else if (not (open /proc/cmdline | str contains composefs)) { 9 | print "No composefs in cmdline" 10 | } else { 11 | # journalctl --grep exits with 1 if no entries found, so we need to handle that 12 | let result = (do { journalctl -b -t bootc-root-setup.service --grep=OK } | complete) 13 | if $result.exit_code == 0 { 14 | print $result.stdout 15 | } else { 16 | print "# TODO composefs: No bootc-root-setup.service journal entries found" 17 | } 18 | } 19 | 20 | tap ok 21 | -------------------------------------------------------------------------------- /docs/src/man/bootc-container.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-container - Operations which can be executed as part of a 4 | container build 5 | 6 | # SYNOPSIS 7 | 8 | **bootc container** \[*OPTIONS...*\] <*SUBCOMMAND*> 9 | 10 | # DESCRIPTION 11 | 12 | Operations which can be executed as part of a container build 13 | 14 | 15 | 16 | 17 | # SUBCOMMANDS 18 | 19 | 20 | | Command | Description | 21 | |---------|-------------| 22 | | **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build | 23 | 24 | 25 | 26 | # VERSION 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tmt/tests/booted/bootc_testlib.nu: -------------------------------------------------------------------------------- 1 | # A simple nushell "library" for the 2 | 3 | # This is a workaround for what must be a systemd bug 4 | # that seems to have appeared in C10S 5 | # TODO diagnose and fill in here 6 | export def reboot [] { 7 | # Sometimes systemd daemons are still running old binaries and response "Access denied" when send reboot request 8 | # Force a full sync before reboot 9 | sync 10 | # Allow more delay for bootc to settle 11 | sleep 30sec 12 | 13 | tmt-reboot 14 | } 15 | 16 | # True if we're running in bcvk with `--bind-storage-ro` and 17 | # we can expect to be able to pull container images from the host. 18 | # See xtask.rs 19 | export def have_hostexports [] { 20 | $env.BCVK_EXPORT? == "1" 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/man/bootc-install-finalize.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-install-finalize - Execute this as the penultimate step of an 4 | installation using `install to-filesystem` 5 | 6 | # SYNOPSIS 7 | 8 | **bootc install finalize** \[*OPTIONS...*\] <*ROOT_PATH*> 9 | 10 | # DESCRIPTION 11 | 12 | Execute this as the penultimate step of an installation using `install 13 | to-filesystem` 14 | 15 | # OPTIONS 16 | 17 | 18 | **ROOT_PATH** 19 | 20 | Path to the mounted root filesystem 21 | 22 | This argument is required. 23 | 24 | 25 | 26 | # ARGUMENTS 27 | 28 | \<*ROOT_PATH*\> 29 | 30 | : Path to the mounted root filesystem 31 | 32 | # VERSION 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Exclude everything by default, then include just what we need 2 | # Especially note this means that .git is not included, and not tests/ 3 | # to avoid spurious rebuilds. 4 | * 5 | 6 | # This one signals we're in a bootc toplevel 7 | !ADOPTERS.md 8 | # Toplevel build bits 9 | !Makefile 10 | !Cargo.* 11 | # License and doc files needed for RPM 12 | !LICENSE-* 13 | !README.md 14 | # We do build manpages from markdown 15 | !docs/ 16 | # We use the spec file 17 | !contrib/ 18 | # The systemd units and baseimage bits end up in installs 19 | !systemd/ 20 | !baseimage/ 21 | # Workaround for podman bug with secrets + remote 22 | # https://github.com/containers/podman/issues/25314 23 | !podman-build-secret* 24 | # And finally of course all the Rust sources 25 | !crates/ 26 | -------------------------------------------------------------------------------- /.github/workflows/autovendor.yml: -------------------------------------------------------------------------------- 1 | # Automatically generate a vendor.tar.zstd on pushes to git main. 2 | name: Auto-vendor artifact 3 | 4 | permissions: 5 | actions: read 6 | 7 | on: 8 | push: 9 | branches: [main] 10 | 11 | jobs: 12 | vendor: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: dtolnay/rust-toolchain@stable 17 | - name: Install vendor tool 18 | run: cargo install cargo-vendor-filterer 19 | - name: Run 20 | run: mkdir -p target && cd crates/cli && cargo vendor-filterer --format=tar.zstd --prefix=vendor/ ../../target/vendor.tar.zst 21 | - uses: actions/upload-artifact@v5 22 | with: 23 | name: vendor.tar.zst 24 | path: target/vendor.tar.zst 25 | -------------------------------------------------------------------------------- /crates/tmpfiles/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootc-tmpfiles" 3 | version = "0.1.0" 4 | license = "MIT OR Apache-2.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | # Internal crates 10 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" } 11 | 12 | # Workspace dependencies 13 | camino = { workspace = true } 14 | cap-std-ext = { workspace = true } 15 | fn-error-context = { workspace = true } 16 | rustix = { workspace = true } 17 | tempfile = { workspace = true } 18 | thiserror = { workspace = true } 19 | uzers = { workspace = true } 20 | 21 | [dev-dependencies] 22 | anyhow = { workspace = true } 23 | indoc = { workspace = true } 24 | similar-asserts = { workspace = true } 25 | 26 | [lints] 27 | workspace = true 28 | -------------------------------------------------------------------------------- /hack/lldb/Containerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/centos-bootc/centos-bootc-dev:stream9 2 | 3 | COPY ./etc/sysctl.conf /etc/sysctl.conf 4 | COPY ./etc/systemd/system/lldb-server.service /etc/systemd/system/lldb-server.service 5 | COPY ./etc/sudoers.d/wheel-nopasswd /etc/sudoers.d 6 | ARG sshpubkey 7 | 8 | RUN dnf -y install lldb firewalld vim && \ 9 | firewall-offline-cmd --add-port 1025-65535/tcp && \ 10 | systemctl enable lldb-server.service && \ 11 | 12 | # add test user 13 | if test -z "$sshpubkey"; then echo "must provide sshpubkey"; exit 1; fi; \ 14 | useradd -G wheel test && \ 15 | mkdir -m 0700 -p /home/test/.ssh && \ 16 | echo $sshpubkey > /home/test/.ssh/authorized_keys && \ 17 | chmod 0600 /home/test/.ssh/authorized_keys && \ 18 | chown -R test: /home/test 19 | -------------------------------------------------------------------------------- /baseimage/README.md: -------------------------------------------------------------------------------- 1 | # Recommended image content 2 | 3 | The subdirectories here are recommended to be installed alongside 4 | bootc in `/usr/share/doc/bootc/baseimage` - they act as reference 5 | sources of content. 6 | 7 | - [base](base): At the current time the content here is effectively 8 | a hard requirement. It's not much, just an ostree configuration 9 | enabling composefs, plus the default `sysroot` directory (which 10 | may go away in the future) and the `ostree` symlink into `sysroot`. 11 | - [dracut](dracut): Default/basic dracut configuration; at the current 12 | time this basically just enables ostree in the initramfs. 13 | - [systemd](systemd): Optional configuration for systemd, currently 14 | this has configuration for kernel-install enabling rpm-ostree integration. 15 | -------------------------------------------------------------------------------- /contrib/packaging/configure-variant: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Configure system for a specific bootc variant 3 | set -xeuo pipefail 4 | 5 | VARIANT="${1:-}" 6 | 7 | if [ -z "$VARIANT" ]; then 8 | # No variant specified, nothing to do 9 | exit 0 10 | fi 11 | 12 | # Handle variant-specific configuration 13 | case "${VARIANT}" in 14 | *-sdboot) 15 | # Install systemd-boot and remove bootupd 16 | dnf -y install systemd-boot-unsigned 17 | # Uninstall bootupd 18 | rpm -e bootupd 19 | rm -rf /usr/lib/bootupd/updates 20 | # Clean up package manager caches 21 | dnf clean all 22 | rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/* 23 | ;; 24 | # Future variants can be added here 25 | # For Debian support, this could check package manager type and use apt instead 26 | esac 27 | -------------------------------------------------------------------------------- /crates/ostree-ext/ci/installdeps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | 4 | # For some reason dnf copr enable -y says there are no builds? 5 | cat >/etc/yum.repos.d/coreos-continuous.repo << 'EOF' 6 | [copr:copr.fedorainfracloud.org:group_CoreOS:continuous] 7 | name=Copr repo for continuous owned by @CoreOS 8 | baseurl=https://download.copr.fedorainfracloud.org/results/@CoreOS/continuous/fedora-$releasever-$basearch/ 9 | type=rpm-md 10 | skip_if_unavailable=True 11 | gpgcheck=1 12 | gpgkey=https://download.copr.fedorainfracloud.org/results/@CoreOS/continuous/pubkey.gpg 13 | repo_gpgcheck=0 14 | enabled=1 15 | enabled_metadata=1 16 | EOF 17 | 18 | # Our tests depend on this 19 | dnf -y install skopeo 20 | 21 | # Always pull ostree from updates-testing to avoid the bodhi wait 22 | dnf -y update ostree 23 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootc-devenv-debian", 3 | // TODO override this back to prod image 4 | "image": "ghcr.io/bootc-dev/devenv-debian", 5 | "customizations": { 6 | "vscode": { 7 | // Abitrary, but most of our code is in one of these two 8 | "extensions": [ 9 | "rust-lang.rust-analyzer", 10 | "golang.Go" 11 | ] 12 | } 13 | }, 14 | "features": {}, 15 | "runArgs": [ 16 | // Because we want to be able to run podman and also use e.g. /dev/kvm 17 | // among other things 18 | "--privileged" 19 | ], 20 | "postCreateCommand": { 21 | // Our init script 22 | "devenv-init": "sudo /usr/local/bin/devenv-init.sh" 23 | }, 24 | "remoteEnv": { 25 | "PATH": "${containerEnv:PATH}:/usr/local/cargo/bin" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/objgv.rs: -------------------------------------------------------------------------------- 1 | /// Type representing an ostree commit object. 2 | macro_rules! gv_commit { 3 | () => { 4 | gvariant::gv!("(a{sv}aya(say)sstayay)") 5 | }; 6 | } 7 | pub(crate) use gv_commit; 8 | 9 | /// Type representing an ostree DIRTREE object. 10 | macro_rules! gv_dirtree { 11 | () => { 12 | gvariant::gv!("(a(say)a(sayay))") 13 | }; 14 | } 15 | pub(crate) use gv_dirtree; 16 | 17 | #[cfg(test)] 18 | mod tests { 19 | use gvariant::aligned_bytes::TryAsAligned; 20 | use gvariant::Marker; 21 | 22 | use super::*; 23 | #[test] 24 | fn test_dirtree() { 25 | // Just a compilation test 26 | let data = b"".try_as_aligned().ok(); 27 | if let Some(data) = data { 28 | let _t = gv_dirtree!().cast(data); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/sysusers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootc-sysusers" 3 | version = "0.1.0" 4 | license = "MIT OR Apache-2.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | # Internal crates 10 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" } 11 | 12 | # Workspace dependencies 13 | anyhow = { workspace = true } 14 | camino = { workspace = true } 15 | cap-std-ext = { workspace = true, features = ["fs_utf8"] } 16 | fn-error-context = { workspace = true } 17 | hex = { workspace = true } 18 | rustix = { workspace = true } 19 | tempfile = { workspace = true } 20 | thiserror = { workspace = true } 21 | uzers = { workspace = true } 22 | 23 | [dev-dependencies] 24 | indoc = { workspace = true } 25 | similar-asserts = { workspace = true } 26 | 27 | [lints] 28 | workspace = true 29 | -------------------------------------------------------------------------------- /tests/container/reboot/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Verify that invoking `bootc internals reboot` actually invokes a reboot, when 3 | # running inside systemd. 4 | # xref: 5 | # - https://github.com/bootc-dev/bootc/issues/1416 6 | # - https://github.com/bootc-dev/bootc/issues/1419 7 | set -euo pipefail 8 | image=$1 9 | tmpd=$(mktemp -d) 10 | log() { 11 | echo "$@" 12 | "$@" 13 | } 14 | log timeout 120 podman run --rm --systemd=always --privileged -v /sys:/sys:ro --label bootc.test=reboot --net=none -v $(pwd):/src:ro -v $tmpd:/run/bootc-test-reboot $image /bin/sh -c 'cp /src/*.service /etc/systemd/system && systemctl enable bootc-test-reboot && exec /sbin/init' || true 15 | ls -al $tmpd 16 | if test '!' -f $tmpd/success; then 17 | echo "reboot failed" 1>&2 18 | rm -rf "$tmpd" 19 | exit 1 20 | fi 21 | rm -rf "$tmpd" 22 | echo "ok reboot" 23 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/017-test-bound-storage.nu: -------------------------------------------------------------------------------- 1 | # Verify that we have host container storage with bcvk 2 | use std assert 3 | use tap.nu 4 | use ../bootc_testlib.nu 5 | 6 | if not (bootc_testlib have_hostexports) { 7 | print "No host exports, skipping" 8 | exit 0 9 | } 10 | 11 | bootc status 12 | let st = bootc status --json | from json 13 | let is_composefs = ($st.status.booted.composefs? != null) 14 | if $is_composefs { 15 | # TODO we don't have imageDigest yet in status 16 | exit 0 17 | } 18 | 19 | let booted = $st.status.booted 20 | let imgref = $booted.image.image.image 21 | let digest = $booted.image.imageDigest 22 | let imgref_untagged = $imgref | split row ':' | first 23 | let digested_imgref = $"($imgref_untagged)@($digest)" 24 | systemd-run -dqP /bin/env 25 | podman inspect $digested_imgref 26 | 27 | tap ok 28 | -------------------------------------------------------------------------------- /crates/lib/src/bootc_composefs/service.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use fn_error_context::context; 3 | use std::process::Command; 4 | 5 | use crate::composefs_consts::BOOTC_FINALIZE_STAGED_SERVICE; 6 | 7 | /// Starts the finaize staged service which will "unstage" the deployment 8 | /// This is called before an upgrade or switch operation, as these create a staged 9 | /// deployment 10 | #[context("Starting finalize staged service")] 11 | pub(crate) fn start_finalize_stated_svc() -> Result<()> { 12 | let cmd_status = Command::new("systemctl") 13 | .args(["start", "--quiet", BOOTC_FINALIZE_STAGED_SERVICE]) 14 | .status() 15 | .context("Starting finalize service")?; 16 | 17 | if !cmd_status.success() { 18 | anyhow::bail!("systemctl exited with status {cmd_status}") 19 | } 20 | 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-rfe-ostree-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: org.containers.bootc/v1alpha1 2 | kind: BootcHost 3 | metadata: 4 | name: host 5 | spec: 6 | image: null 7 | bootOrder: default 8 | status: 9 | staged: 10 | image: null 11 | cachedUpdate: null 12 | incompatible: true 13 | pinned: false 14 | store: null 15 | ostree: 16 | checksum: 1c24260fdd1be20f72a4a97a75c582834ee3431fbb0fa8e4f482bb219d633a45 17 | deploySerial: 0 18 | stateroot: default 19 | booted: 20 | image: null 21 | cachedUpdate: null 22 | incompatible: false 23 | pinned: false 24 | store: null 25 | ostree: 26 | checksum: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 27 | deploySerial: 0 28 | stateroot: default 29 | rollback: null 30 | rollbackQueued: false 31 | type: null 32 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/install-grub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | curl http://192.168.122.1:8000/bootc -o bootc 6 | chmod +x bootc 7 | 8 | IMAGE=quay.io/fedora/fedora-bootc-uki:42 9 | 10 | # --env RUST_LOG=debug \ 11 | # --env RUST_BACKTRACE=1 \ 12 | podman run \ 13 | --rm --privileged \ 14 | --pid=host \ 15 | -v /dev:/dev \ 16 | -v /var/lib/containers:/var/lib/containers \ 17 | -v /srv/bootc:/usr/bin/bootc:ro,Z \ 18 | -v /var/tmp:/var/tmp \ 19 | --security-opt label=type:unconfined_t \ 20 | "${IMAGE}" \ 21 | bootc install to-disk \ 22 | --composefs-backend \ 23 | --boot=uki \ 24 | --source-imgref="containers-storage:${IMAGE}" \ 25 | --target-imgref="${IMAGE}" \ 26 | --target-transport="docker" \ 27 | /dev/vdb \ 28 | --filesystem=ext4 \ 29 | --wipe 30 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-via-local-oci.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: org.containers.bootc/v1alpha1 2 | kind: BootcHost 3 | metadata: 4 | name: host 5 | spec: 6 | image: 7 | image: /var/mnt/osupdate:latest 8 | transport: oci 9 | bootOrder: default 10 | status: 11 | staged: null 12 | booted: 13 | image: 14 | image: 15 | image: /var/mnt/osupdate 16 | transport: oci 17 | architecture: amd64 18 | version: stream9.20240807.0 19 | timestamp: null 20 | imageDigest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 21 | cachedUpdate: null 22 | incompatible: false 23 | pinned: false 24 | ostree: 25 | checksum: 439f6bd2e2361bee292c1f31840d798c5ac5ba76483b8021dc9f7b0164ac0f48 26 | deploySerial: 0 27 | stateroot: default 28 | rollback: null 29 | rollbackQueued: false 30 | type: bootcHost -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-v1a1.yaml: -------------------------------------------------------------------------------- 1 | # This one drops the now-optional signature schema 2 | apiVersion: org.containers.bootc/v1alpha1 3 | kind: BootcHost 4 | metadata: 5 | name: host 6 | spec: 7 | image: 8 | image: quay.io/otherexample/otherimage:latest 9 | transport: registry 10 | status: 11 | booted: 12 | image: 13 | image: 14 | image: quay.io/otherexample/otherimage:latest 15 | transport: registry 16 | architecture: s390x 17 | version: 20231230.1 18 | timestamp: 2023-12-30T16:10:11Z 19 | imageDigest: sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c 20 | incompatible: false 21 | pinned: false 22 | ostree: 23 | checksum: 41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3 24 | deploySerial: 0 25 | stateroot: default 26 | rollback: null 27 | isContainer: false 28 | -------------------------------------------------------------------------------- /docs/src/relationship-oci-artifacts.md: -------------------------------------------------------------------------------- 1 | # How does the use of OCI artifacts intersect with this effort? 2 | 3 | The "bootc compatible" images are OCI container images; they do not rely on the [OCI artifact specification](https://github.com/opencontainers/image-spec/blob/main/artifacts-guidance.md) or [OCI referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#enabling-the-referrers-api). 4 | 5 | It is foreseeable that users will need to produce "traditional" disk images (i.e. raw disk images, qcow2 disk images, Amazon AMIs, etc.) from the "bootc compatible" container images using additional tools. Therefore, it is reasonable that some users may want to encapsulate those disk images as an OCI artifact for storage and distribution. However, it is not a goal to use `bootc` to produce these "traditional" disk images nor to facilitate the encapsulation of those disk images as OCI artifacts. 6 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/012-test-unit-status.nu: -------------------------------------------------------------------------------- 1 | # Verify our systemd units are enabled 2 | use std assert 3 | use tap.nu 4 | 5 | tap begin "verify our systemd units" 6 | 7 | # Detect composefs by checking if composefs field is present 8 | let st = bootc status --json | from json 9 | let is_composefs = ($st.status.booted.composefs? != null) 10 | 11 | if $is_composefs { 12 | print "# TODO composefs: skipping test - bootc-status-updated.path watches /ostree/bootc which doesn't exist with composefs" 13 | } else { 14 | let units = [ 15 | ["unit", "status"]; 16 | # This one should be always enabled by our install logic 17 | ["bootc-status-updated.path", "active"] 18 | ] 19 | 20 | for elt in $units { 21 | let found_status = systemctl show -P ActiveState $elt.unit | str trim 22 | assert equal $elt.status $found_status 23 | } 24 | } 25 | 26 | tap ok 27 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/030-test-locking-read.nu: -------------------------------------------------------------------------------- 1 | # Verify we can spawn multiple bootc status at the same time 2 | use std assert 3 | use tap.nu 4 | 5 | tap begin "concurrent bootc status" 6 | 7 | # Fork via systemd-run 8 | let n = 10 9 | 0..$n | each { |v| 10 | # Clean up prior runs 11 | systemctl stop $"bootc-status-($v)" | complete 12 | } 13 | # Fork off a concurrent bootc status 14 | 0..$n | each { |v| 15 | systemd-run --no-block -qr -u $"bootc-status-($v)" bootc status 16 | } 17 | 18 | # Await completion 19 | 0..$n | each { |v| 20 | loop { 21 | let r = systemctl is-active $"bootc-status-($v)" | complete 22 | if $r.exit_code == 0 { 23 | break 24 | } 25 | # check status 26 | systemctl status $"bootc-status-($v)" out> /dev/null 27 | # Clean it up 28 | systemctl reset-failed $"bootc-status-($v)" 29 | } 30 | } 31 | 32 | tap ok 33 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-only-booted.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: org.containers.bootc/v1alpha1 2 | kind: BootcHost 3 | metadata: 4 | name: host 5 | spec: 6 | image: 7 | image: quay.io/centos-bootc/centos-bootc:stream9 8 | transport: registry 9 | bootOrder: default 10 | status: 11 | staged: null 12 | booted: 13 | image: 14 | image: 15 | image: quay.io/centos-bootc/centos-bootc:stream9 16 | transport: registry 17 | architecture: arm64 18 | version: stream9.20240807.0 19 | timestamp: null 20 | imageDigest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 21 | cachedUpdate: null 22 | incompatible: false 23 | pinned: false 24 | ostree: 25 | checksum: 439f6bd2e2361bee292c1f31840d798c5ac5ba76483b8021dc9f7b0164ac0f48 26 | deploySerial: 0 27 | stateroot: default 28 | rollback: null 29 | rollbackQueued: false 30 | type: bootcHost -------------------------------------------------------------------------------- /docs/src/man/bootc-edit.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-edit - Apply full changes to the host specification 4 | 5 | # SYNOPSIS 6 | 7 | **bootc edit** \[*OPTIONS...*\] 8 | 9 | # DESCRIPTION 10 | 11 | Apply full changes to the host specification. 12 | 13 | This command operates very similarly to `kubectl apply`; if invoked 14 | interactively, then the current host specification will be presented in 15 | the system default `\$EDITOR` for interactive changes. 16 | 17 | It is also possible to directly provide new contents via `bootc edit 18 | \--filename`. 19 | 20 | Only changes to the `spec` section are honored. 21 | 22 | # OPTIONS 23 | 24 | 25 | **-f**, **--filename**=*FILENAME* 26 | 27 | Use filename to edit system specification 28 | 29 | **--quiet** 30 | 31 | Don't display progress 32 | 33 | 34 | 35 | # VERSION 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/src/man/bootc-fetch-apply-updates.service.5.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-fetch-apply-updates.service 4 | 5 | # DESCRIPTION 6 | 7 | This service causes `bootc` to perform the following steps: 8 | 9 | - Check the source registry for an updated container image 10 | - If one is found, download it 11 | - Reboot 12 | 13 | This service also comes with a companion `bootc-fetch-apply-updates.timer` 14 | systemd unit. The current default systemd timer shipped in the upstream 15 | project is enabled for daily updates. 16 | 17 | However, it is fully expected that different operating systems 18 | and distributions choose different defaults. 19 | 20 | # CUSTOMIZING UPDATES 21 | 22 | Note that all three of these steps can be decoupled; they 23 | are: 24 | 25 | - `bootc upgrade --check` 26 | - `bootc upgrade` 27 | - `bootc upgrade --apply` 28 | 29 | # SEE ALSO 30 | 31 | **bootc(1)** 32 | 33 | # VERSION 34 | 35 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-ostree-remote.yaml: -------------------------------------------------------------------------------- 1 | # This one drops the now-optional signature schema 2 | apiVersion: org.containers.bootc/v1alpha1 3 | kind: BootcHost 4 | metadata: 5 | name: host 6 | spec: 7 | image: 8 | image: quay.io/fedora/fedora-coreos:stable 9 | transport: registry 10 | signature: !ostreeRemote "fedora" 11 | status: 12 | booted: 13 | image: 14 | image: 15 | image: quay.io/otherexample/otherimage:latest 16 | transport: registry 17 | architecture: arm64 18 | version: 20231230.1 19 | timestamp: 2023-12-30T16:10:11Z 20 | imageDigest: sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c 21 | incompatible: false 22 | pinned: false 23 | ostree: 24 | checksum: 41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3 25 | deploySerial: 0 26 | stateroot: default 27 | rollback: null 28 | isContainer: false 29 | -------------------------------------------------------------------------------- /crates/mount/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Internal mount code" 3 | # Should never be published to crates.io 4 | publish = false 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | name = "bootc-mount" 8 | repository = "https://github.com/bootc-dev/bootc" 9 | version = "0.0.0" 10 | 11 | [dependencies] 12 | # Internal crates 13 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" } 14 | 15 | # Workspace dependencies 16 | anyhow = { workspace = true } 17 | camino = { workspace = true, features = ["serde1"] } 18 | fn-error-context = { workspace = true } 19 | libc = { workspace = true } 20 | rustix = { workspace = true } 21 | serde = { workspace = true, features = ["derive"] } 22 | tracing = { workspace = true } 23 | tempfile = { workspace = true } 24 | cap-std-ext = { workspace = true } 25 | 26 | [dev-dependencies] 27 | indoc = { workspace = true } 28 | 29 | [lib] 30 | path = "src/mount.rs" 31 | -------------------------------------------------------------------------------- /tmt/README.md: -------------------------------------------------------------------------------- 1 | # Run integration test locally 2 | 3 | In the bootc CI, integration tests are executed via Packit on the Testing Farm. In addition, the integration tests can also be run locally on a developer's machine, which is especially valuable for debugging purposes. 4 | 5 | To run integration tests locally, you need to [install tmt](https://tmt.readthedocs.io/en/stable/guide.html#the-first-steps) and `provision-virtual` plugin in this case. Be ready with `dnf install -y tmt+provision-virtual`. Then, use `tmt run -vvvvv plans -n integration` command to run the all integration tests. 6 | 7 | To run integration tests on different distros, just change `image: fedora-rawhide` in https://github.com/bootc-dev/bootc/blob/9d15eedea0d54a4dbc15d267dbdb055817336254/tmt/plans/integration.fmf#L6. 8 | 9 | The available images value can be found from https://tmt.readthedocs.io/en/stable/plugins/provision.html#images. 10 | 11 | Enjoy integration test local running! 12 | -------------------------------------------------------------------------------- /hack/lldb/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # connect to the VM using https://libvirt.org/nss.html 4 | 5 | set -e 6 | 7 | # build the container image 8 | sudo podman build --build-arg "sshpubkey=$(cat ~/.ssh/id_rsa.pub)" -f Containerfile -t localhost/bootc-lldb . 9 | 10 | # build the disk image 11 | mkdir -p ~/.cache/bootc-dev/disks 12 | rm -f ~/.cache/bootc-dev/disks/lldb.raw 13 | truncate -s 10G ~/.cache/bootc-dev/disks/lldb.raw 14 | sudo podman run --pid=host --network=host --privileged --security-opt label=type:unconfined_t -v ~/.cache/bootc-dev/disks:/output localhost/bootc-lldb bootc install to-disk --via-loopback --generic-image /output/lldb.raw 15 | 16 | # create a new VM in libvirt 17 | set +e 18 | virsh -c qemu:///system destroy bootc-lldb 19 | virsh -c qemu:///system undefine --nvram bootc-lldb 20 | set -e 21 | sudo virt-install --name bootc-lldb --cpu host --vcpus 8 --memory 8192 --import --disk ~/.cache/bootc-dev/disks/lldb.raw --os-variant rhel9-unknown 22 | -------------------------------------------------------------------------------- /docs/src/man/bootc-install-ensure-completion.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-install-ensure-completion - Intended for use in environments that 4 | are performing an ostree-based installation, not bootc 5 | 6 | # SYNOPSIS 7 | 8 | **bootc install ensure-completion** \[*OPTIONS...*\] 9 | 10 | # DESCRIPTION 11 | 12 | Intended for use in environments that are performing an ostree-based 13 | installation, not bootc. 14 | 15 | In this scenario the installation may be missing bootc specific features 16 | such as kernel arguments, logically bound images and more. This command 17 | can be used to attempt to reconcile. At the current time, the only 18 | tested environment is Anaconda using `ostreecontainer` and it is 19 | recommended to avoid usage outside of that environment. Instead, ensure 20 | your code is using `bootc install to-filesystem` from the start. 21 | 22 | 23 | 24 | 25 | # VERSION 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/011-test-ostree-ext-cli.nu: -------------------------------------------------------------------------------- 1 | # Verify our wrapped "bootc internals ostree-container" calling into 2 | # the legacy ostree-ext CLI. 3 | use std assert 4 | use tap.nu 5 | 6 | tap begin "verify bootc wrapping ostree-ext" 7 | 8 | # Parse the status and get the booted image 9 | let st = bootc status --json | from json 10 | # Detect composefs by checking if composefs field is present 11 | let is_composefs = ($st.status.booted.composefs? != null) 12 | if $is_composefs { 13 | print "# TODO composefs: skipping test - ostree-container commands don't work with composefs" 14 | } else { 15 | let booted = $st.status.booted.image 16 | # Then verify we can extract its metadata via the ostree-container code. 17 | let metadata = bootc internals ostree-container image metadata --repo=/ostree/repo $"($booted.image.transport):($booted.image.image)" | from json 18 | assert equal $metadata.mediaType "application/vnd.oci.image.manifest.v1+json" 19 | } 20 | 21 | tap ok 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you find a potential security vulnerability in bootc, please report it by following these steps: 6 | 7 | ### 1. **Use the GitHub Security Tab** 8 | This repository is set up to allow vulnerability reports through GitHub's Security Advisories feature. To report a vulnerability: 9 | 10 | 1. Navigate to the repository's main page. 11 | 2. Select the [**Security**](https://github.com/bootc-dev/bootc/security) tab. 12 | 3. Select **Advisories** from the left-hand sidebar. 13 | 4. Click on **Report a vulnerability**. 14 | 5. Fill in the required details and submit the report. 15 | 16 | Following this process will create a private advisory for our maintainers to review. 17 | 18 | ### 2. **Do Not Open Public Pull Requests, Issues, or Discussions** 19 | Please **do not** discuss the issue, create PRs, or start discussions about the vulnerability. This ensures the vulnerability is not widely exploited before a fix is provided. 20 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootc" 3 | # This is a stub, the real version is from the lib crate 4 | version = "0.0.0" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/bootc-dev/bootc" 8 | publish = false 9 | default-run = "bootc" 10 | 11 | # See https://github.com/coreos/cargo-vendor-filterer 12 | [package.metadata.vendor-filter] 13 | # For now we only care about tier 1+2 Linux. (In practice, it's unlikely there is a tier3-only Linux dependency) 14 | platforms = ["*-unknown-linux-gnu"] 15 | 16 | [dependencies] 17 | # Internal crates 18 | bootc-lib = { version = "1.10", path = "../lib" } 19 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" } 20 | 21 | # Workspace dependencies 22 | anstream = { workspace = true } 23 | anyhow = { workspace = true } 24 | log = { workspace = true } 25 | tokio = { workspace = true, features = ["macros"] } 26 | tracing = { workspace = true } 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /docs/src/experimental-bootc-image.md: -------------------------------------------------------------------------------- 1 | # bootc image 2 | 3 | Experimental features are subject to change or removal. Please 4 | do provide feedback on them. 5 | 6 | Tracking issue: 7 | 8 | ## Using `bootc image copy-to-storage` 9 | 10 | This experimental command is intended to aid in [booting local builds](booting-local-builds.md). 11 | 12 | Invoking this command will default to copying the booted container image into the `containers-storage:` 13 | area as used by e.g. `podman`, under the image tag `localhost/bootc` by default. It can 14 | then be managed independently; used as a base image, pushed to a registry, etc. 15 | 16 | Run `bootc image copy-to-storage --help` for more options. 17 | 18 | Example workflow: 19 | 20 | ``` 21 | $ bootc image copy-to-storage 22 | $ cat Containerfile 23 | FROM localhost/bootc 24 | ... 25 | $ podman build -t localhost/bootc-custom . 26 | $ bootc switch --transport containers-storage localhost/bootc-custom 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /hack/system-reinstall-bootc.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | # Set a timeout 4 | set timeout 600 5 | 6 | spawn system-reinstall-bootc localhost/bootc-integration 7 | 8 | expect { 9 | "Then you can login as * using those keys. \\\[Y/n\\\]" { 10 | send "\r" 11 | exp_continue 12 | } 13 | "NOTICE: This will replace the installed operating system and reboot. Are you sure you want to continue? \\\[y/N\\\]" { 14 | send "y" 15 | exp_continue 16 | } 17 | "NOTICE: This will replace the installed operating system and reboot. Are you sure you want to continue? \\\[Y/n\\\]" { 18 | send "\r" 19 | exp_continue 20 | } 21 | "Press \\\ to continue" { 22 | send "\r" 23 | exp_continue 24 | } 25 | "Operation complete, rebooting in 10 seconds. Press Ctrl-C to cancel reboot, or press enter to continue immediately" { 26 | send "\x03" 27 | } 28 | } 29 | 30 | # Wait for the program to complete 31 | expect eof 32 | -------------------------------------------------------------------------------- /crates/blockdev/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Internal implementation component of bootc; do not use" 3 | edition = "2021" 4 | license = "MIT OR Apache-2.0" 5 | name = "bootc-internal-blockdev" 6 | repository = "https://github.com/bootc-dev/bootc" 7 | version = "0.0.0" 8 | 9 | [dependencies] 10 | # Internal crates 11 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" } 12 | 13 | # Workspace dependencies 14 | anyhow = { workspace = true } 15 | camino = { workspace = true, features = ["serde1"] } 16 | fn-error-context = { workspace = true } 17 | libc = { workspace = true } 18 | regex = { workspace = true } 19 | rustix = { workspace = true } 20 | serde = { workspace = true, features = ["derive"] } 21 | serde_json = { workspace = true } 22 | tempfile = { workspace = true } 23 | tokio = { workspace = true, features = ["signal"] } 24 | tracing = { workspace = true } 25 | 26 | [dev-dependencies] 27 | indoc = { workspace = true } 28 | 29 | [lib] 30 | path = "src/blockdev.rs" -------------------------------------------------------------------------------- /contrib/packaging/install-rpm-and-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Install bootc RPM and perform post-installation setup 3 | set -xeuo pipefail 4 | 5 | RPM_DIR="${1:-/tmp}" 6 | 7 | # Install the RPM package 8 | # Use rpm -Uvh with --oldpackage to allow replacing with dev version 9 | rpm -Uvh --oldpackage "${RPM_DIR}"/*.rpm 10 | rm -f "${RPM_DIR}"/*.rpm 11 | 12 | # Regenerate initramfs if we have initramfs-setup 13 | kver=$(cd /usr/lib/modules && echo *) 14 | # DRACUT_NO_XATTR=1 is the default in newer base images, and 15 | # we have --add bootc here until the change to add the module in base 16 | # images lands. 17 | env DRACUT_NO_XATTR=1 dracut --add bootc -vf /usr/lib/modules/$kver/initramfs.img $kver 18 | 19 | # Only in this containerfile, inject a file which signifies 20 | # this comes from this development image. This can be used in 21 | # tests to know we're doing upstream CI. 22 | touch /usr/lib/.bootc-dev-stamp 23 | 24 | # Workaround for https://github.com/bootc-dev/bootc/issues/1546 25 | rm -rf /root/buildinfo 26 | -------------------------------------------------------------------------------- /crates/system-reinstall-bootc/src/btrfs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bootc_mount::Filesystem; 3 | use fn_error_context::context; 4 | 5 | #[context("check_root_siblings")] 6 | pub(crate) fn check_root_siblings() -> Result> { 7 | let mounts = bootc_mount::run_findmnt(&[], None, None)?; 8 | let problem_filesystems: Vec = mounts 9 | .filesystems 10 | .iter() 11 | .filter(|fs| fs.target == "/") 12 | .flat_map(|root| { 13 | let children: Vec<&Filesystem> = root 14 | .children 15 | .iter() 16 | .flatten() 17 | .filter(|child| child.source == root.source) 18 | .collect(); 19 | children 20 | }) 21 | .map(|zs| { 22 | format!( 23 | "Type: {}, Mount Point: {}, Source: {}", 24 | zs.fstype, zs.target, zs.source 25 | ) 26 | }) 27 | .collect(); 28 | Ok(problem_filesystems) 29 | } 30 | -------------------------------------------------------------------------------- /crates/xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | # See https://github.com/matklad/cargo-xtask 2 | # This is an implementation detail of bootc 3 | [package] 4 | name = "xtask" 5 | version = "0.1.0" 6 | license = "MIT OR Apache-2.0" 7 | edition = "2021" 8 | publish = false 9 | 10 | [[bin]] 11 | name = "xtask" 12 | path = "src/xtask.rs" 13 | 14 | [dependencies] 15 | # Workspace dependencies 16 | anyhow = { workspace = true } 17 | anstream = { workspace = true } 18 | camino = { workspace = true } 19 | chrono = { workspace = true, features = ["std"] } 20 | clap = { workspace = true, features = ["derive"] } 21 | fn-error-context = { workspace = true } 22 | owo-colors = { workspace = true } 23 | serde = { workspace = true, features = ["derive"] } 24 | serde_json = { workspace = true } 25 | tempfile = { workspace = true } 26 | toml = { workspace = true } 27 | xshell = { workspace = true } 28 | 29 | # Crate-specific dependencies 30 | mandown = "1.1.0" 31 | rand = "0.9" 32 | serde_yaml = "0.9" 33 | tar = "0.4" 34 | 35 | [lints] 36 | workspace = true 37 | -------------------------------------------------------------------------------- /crates/tests-integration/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Our integration tests 2 | [package] 3 | name = "tests-integration" 4 | version = "0.1.0" 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | publish = false 8 | 9 | [[bin]] 10 | name = "tests-integration" 11 | path = "src/tests-integration.rs" 12 | 13 | [dependencies] 14 | # Workspace dependencies 15 | anyhow = { workspace = true } 16 | camino = { workspace = true } 17 | cap-std-ext = { workspace = true } 18 | clap = { workspace = true, features = ["derive","cargo"] } 19 | fn-error-context = { workspace = true } 20 | indoc = { workspace = true } 21 | rustix = { workspace = true } 22 | serde = { workspace = true, features = ["derive"] } 23 | serde_json = { workspace = true } 24 | tempfile = { workspace = true } 25 | xshell = { workspace = true } 26 | bootc-kernel-cmdline = { path = "../kernel_cmdline", version = "0.0.0" } 27 | 28 | # Crate-specific dependencies 29 | libtest-mimic = "0.8.0" 30 | oci-spec = "0.8.0" 31 | rexpect = "0.6" 32 | 33 | [lints] 34 | workspace = true 35 | -------------------------------------------------------------------------------- /docs/src/bootc-in-container.md: -------------------------------------------------------------------------------- 1 | # bootc is read-only when run in a default container 2 | 3 | Currently, running e.g. `podman run bootc upgrade` will not work. 4 | There are a variety of reasons for this, such as the basic fact that by 5 | default a `docker|podman run ` doesn't know where to update itself; 6 | the image reference is not exposed into the target image (for security/operational 7 | reasons). 8 | 9 | ## Supported operations 10 | 11 | There are only two supported operations in a container environment today: 12 | 13 | - `bootc status`: This can reliably be used to detect whether the system is 14 | actually booted via bootc or not. 15 | - `bootc container lint`: See [man/bootc-container-lint.8.md](man/bootc-container-lint.8.md). 16 | 17 | ### Testing bootc in a container 18 | 19 | Eventually we would like to support having bootc run inside a container environment 20 | primarily for testing purposes. For this, please see the [tracking issue](https://github.com/bootc-dev/bootc/issues/400). 21 | -------------------------------------------------------------------------------- /docs/src/man/bootc-install-print-configuration.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-install-print-configuration - Output JSON to stdout that contains 4 | the merged installation configuration as it may be relevant to calling 5 | processes using `install to-filesystem` that in particular want to 6 | discover the desired root filesystem type from the container image 7 | 8 | # SYNOPSIS 9 | 10 | **bootc install print-configuration** \[*OPTIONS...*\] 11 | 12 | # DESCRIPTION 13 | 14 | Output JSON to stdout that contains the merged installation 15 | configuration as it may be relevant to calling processes using `install 16 | to-filesystem` that in particular want to discover the desired root 17 | filesystem type from the container image. 18 | 19 | At the current time, the only output key is `root-fs-type` which is a 20 | string-valued filesystem name suitable for passing to `mkfs.\$type`. 21 | 22 | 23 | 24 | 25 | # VERSION 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /crates/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootc-internal-utils" 3 | description = "Internal implementation component of bootc; do not use" 4 | version = "0.0.0" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/bootc-dev/bootc" 8 | 9 | [dependencies] 10 | # Workspace dependencies 11 | anstream = { workspace = true } 12 | anyhow = { workspace = true } 13 | chrono = { workspace = true, features = ["std"] } 14 | owo-colors = { workspace = true } 15 | rustix = { workspace = true } 16 | serde = { workspace = true, features = ["derive"] } 17 | serde_json = { workspace = true } 18 | shlex = { workspace = true } 19 | tempfile = { workspace = true } 20 | tokio = { workspace = true, features = ["process", "rt", "macros"] } 21 | tracing = { workspace = true } 22 | tracing-subscriber = { workspace = true } 23 | tracing-journald = { workspace = true } 24 | 25 | [dev-dependencies] 26 | similar-asserts = { workspace = true } 27 | static_assertions = { workspace = true } 28 | 29 | [lints] 30 | workspace = true 31 | -------------------------------------------------------------------------------- /crates/lib/src/glyph.rs: -------------------------------------------------------------------------------- 1 | //! Special Unicode characters used for display with ASCII fallbacks 2 | //! in case we're not in a UTF-8 locale. 3 | 4 | use std::fmt::Display; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 7 | pub(crate) enum Glyph { 8 | BlackCircle, 9 | } 10 | 11 | impl Glyph { 12 | // TODO: Add support for non-Unicode output 13 | #[allow(dead_code)] 14 | pub(crate) fn as_ascii(&self) -> &'static str { 15 | match self { 16 | Glyph::BlackCircle => "*", 17 | } 18 | } 19 | 20 | pub(crate) fn as_utf8(&self) -> &'static str { 21 | match self { 22 | Glyph::BlackCircle => "●", 23 | } 24 | } 25 | } 26 | 27 | impl Display for Glyph { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | f.write_str(self.as_utf8()) 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | 37 | #[test] 38 | fn test_glyph() { 39 | assert_eq!(Glyph::BlackCircle.as_utf8(), "●"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, time::Duration}; 2 | 3 | /// Call an async task function, and write a message to stdout 4 | /// with an automatic spinner to show that we're not blocked. 5 | /// Note that generally the called function should not output 6 | /// anything to stdout as this will interfere with the spinner. 7 | pub(crate) async fn async_task_with_spinner(msg: &str, f: F) -> T 8 | where 9 | F: Future, 10 | { 11 | let pb = indicatif::ProgressBar::new_spinner(); 12 | let style = indicatif::ProgressStyle::default_bar(); 13 | pb.set_style(style.template("{spinner} {msg}").unwrap()); 14 | pb.set_message(msg.to_string()); 15 | pb.enable_steady_tick(Duration::from_millis(150)); 16 | let r = f.await; 17 | pb.finish_and_clear(); 18 | r 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::*; 24 | 25 | #[tokio::test] 26 | async fn test_spinner() { 27 | async_task_with_spinner("Testing...", tokio::time::sleep(Duration::from_secs(5))).await 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Instructions for AI agents 4 | 5 | ## CRITICAL instructions for generating commits 6 | 7 | ### Signed-off-by 8 | 9 | Human review is required for all code that is generated 10 | or assisted by a large language model. If you 11 | are a LLM, you MUST NOT include a `Signed-off-by` 12 | on any automatically generated git commits. Only explicit 13 | human action or request should include a Signed-off-by. 14 | If for example you automatically create a pull request 15 | and the DCO check fails, tell the human to review 16 | the code and give them instructions on how to add 17 | a signoff. 18 | 19 | ### Attribution 20 | 21 | When generating substantial amounts of code, you SHOULD 22 | include an `Assisted-by: TOOLNAME (MODELNAME)`. For example, 23 | `Assisted-by: Goose (Sonnet 4.5)`. 24 | 25 | ## Follow other guidelines 26 | 27 | Look at the project README.md and look for guidelines 28 | related to contribution, such as a CONTRIBUTING.md 29 | and follow those. 30 | -------------------------------------------------------------------------------- /crates/lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Bootable container tool 2 | //! 3 | //! This crate builds on top of ostree's container functionality 4 | //! to provide a fully "container native" tool for using 5 | //! bootable container images. 6 | 7 | mod bootc_composefs; 8 | pub(crate) mod bootc_kargs; 9 | mod bootloader; 10 | mod boundimage; 11 | mod cfsctl; 12 | pub mod cli; 13 | mod composefs_consts; 14 | mod containerenv; 15 | pub(crate) mod deploy; 16 | mod discoverable_partition_specification; 17 | pub(crate) mod fsck; 18 | pub(crate) mod generator; 19 | mod glyph; 20 | mod image; 21 | mod install; 22 | pub(crate) mod journal; 23 | mod k8sapitypes; 24 | mod lints; 25 | mod lsm; 26 | pub(crate) mod metadata; 27 | mod parsers; 28 | mod podman; 29 | mod podstorage; 30 | mod progress_jsonl; 31 | mod reboot; 32 | pub mod spec; 33 | mod status; 34 | mod store; 35 | mod task; 36 | mod utils; 37 | 38 | #[cfg(feature = "docgen")] 39 | mod cli_json; 40 | 41 | #[cfg(feature = "rhsm")] 42 | mod rhsm; 43 | 44 | // Re-export blockdev crate for internal use 45 | pub(crate) use bootc_blockdev as blockdev; 46 | -------------------------------------------------------------------------------- /docs/Dockerfile.mdbook: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi10/ubi:latest 2 | # An intermediate layer which caches the RPMS 3 | RUN <(f: F) 26 | where 27 | F: FnOnce() -> anyhow::Result<()>, 28 | { 29 | use std::io::Write as _; 30 | 31 | use owo_colors::OwoColorize; 32 | 33 | if let Err(e) = f() { 34 | let mut stderr = anstream::stderr(); 35 | // Don't panic if writing fails. 36 | let _ = writeln!(stderr, "{}{:#}", "error: ".red(), e); 37 | std::process::exit(1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-usroverlay.nu: -------------------------------------------------------------------------------- 1 | # number: 23 2 | # tmt: 3 | # summary: Execute tests for bootc usrover 4 | # duration: 30m 5 | # 6 | # Verify that bootc usroverlay works 7 | use std assert 8 | use tap.nu 9 | use bootc_testlib.nu 10 | 11 | bootc status 12 | 13 | # We should start out in a non-writable state on each boot 14 | let is_writable = (do -i { /bin/test -w /usr } | complete | get exit_code) == 0 15 | assert (not $is_writable) 16 | 17 | def initial_run [] { 18 | bootc usroverlay 19 | let is_writable = (do -i { /bin/test -w /usr } | complete | get exit_code) == 0 20 | assert ($is_writable) 21 | 22 | bootc_testlib reboot 23 | } 24 | 25 | # The second boot; verify we're in the derived image 26 | def second_boot [] { 27 | # Nothing, we already verified non-writability above 28 | } 29 | 30 | def main [] { 31 | # See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test 32 | match $env.TMT_REBOOT_COUNT? { 33 | null | "0" => initial_run, 34 | "1" => second_boot, 35 | $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs to pages 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - uses: actions/checkout@v6 22 | - name: Bootc Ubuntu Setup 23 | uses: ./.github/actions/bootc-ubuntu-setup 24 | - name: Build mdbook 25 | run: mkdir target && just build-mdbook-to target/docs 26 | - name: Setup Pages 27 | id: pages 28 | uses: actions/configure-pages@v5 29 | - name: Upload artifact 30 | uses: actions/upload-pages-artifact@v4 31 | with: 32 | path: ./target/docs 33 | 34 | deploy: 35 | environment: 36 | name: github-pages 37 | url: ${{ steps.deployment.outputs.page_url }} 38 | runs-on: ubuntu-latest 39 | needs: build 40 | steps: 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /crates/lib/src/k8sapitypes.rs: -------------------------------------------------------------------------------- 1 | //! Subset of API definitions for selected Kubernetes API types. 2 | //! We avoid dragging in all of k8s-openapi because it's *huge*. 3 | 4 | use std::collections::BTreeMap; 5 | 6 | use schemars::JsonSchema; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct Resource { 12 | pub api_version: String, 13 | pub kind: String, 14 | #[serde(default)] 15 | pub metadata: ObjectMeta, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct ObjectMeta { 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub annotations: Option>, 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub labels: Option>, 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub name: Option, 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub namespace: Option, 29 | } 30 | -------------------------------------------------------------------------------- /crates/utils/src/result_ext.rs: -------------------------------------------------------------------------------- 1 | /// Extension trait for Result types that provides logging capabilities 2 | pub trait ResultExt { 3 | /// Return the Ok value unchanged. In the err case, log it, and call the closure to compute the default 4 | fn log_err_or_else(self, default: F) -> T 5 | where 6 | F: FnOnce() -> T; 7 | /// Return the Ok value unchanged. In the err case, log it, and return the default value 8 | fn log_err_default(self) -> T 9 | where 10 | T: Default; 11 | } 12 | 13 | impl ResultExt for Result { 14 | #[track_caller] 15 | fn log_err_or_else(self, default: F) -> T 16 | where 17 | F: FnOnce() -> T, 18 | { 19 | match self { 20 | Ok(r) => r, 21 | Err(e) => { 22 | tracing::debug!("{e}"); 23 | default() 24 | } 25 | } 26 | } 27 | 28 | #[track_caller] 29 | fn log_err_default(self) -> T 30 | where 31 | T: Default, 32 | { 33 | self.log_err_or_else(|| Default::default()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/src/bootloaders.md: -------------------------------------------------------------------------------- 1 | # Bootloaders in `bootc` 2 | 3 | `bootc` supports two ways to manage bootloaders. 4 | 5 | ## bootupd 6 | 7 | [bootupd](https://github.com/coreos/bootupd/) is a project explicitly designed to abstract over and manage bootloader installation and configuration. 8 | Today it primarily supports GRUB+shim. There are pending patches for it to support systemd-boot as well. 9 | 10 | When you run `bootc install`, it invokes `bootupctl backend install` to install the bootloader to the target disk or filesystem. The specific bootloader configuration is determined by the container image and the target system's hardware. 11 | 12 | Currently, `bootc` only runs `bootupd` during the installation process. It does **not** automatically run `bootupctl update` to update the bootloader after installation. This means that bootloader updates must be handled separately, typically by the user or an automated system update process. 13 | 14 | ## systemd-boot 15 | 16 | If bootupd is not present in the input container image, then systemd-boot will be used 17 | by default (except on s390x). 18 | 19 | ## s390x 20 | 21 | bootc uses `zipl`. 22 | -------------------------------------------------------------------------------- /crates/ostree-ext/ci/container-build-integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Verify `ostree container commit` 3 | set -euo pipefail 4 | 5 | image=quay.io/fedora/fedora-coreos:stable 6 | example=coreos-layering-examples/tailscale 7 | set -x 8 | 9 | chmod a+x ostree-ext-cli 10 | workdir=${PWD} 11 | cd ${example} 12 | cp ${workdir}/ostree-ext-cli . 13 | sed -ie 's,ostree container commit,ostree-ext-cli container commit,' Containerfile 14 | sed -ie 's,^\(FROM .*\),\1\nADD ostree-ext-cli /usr/bin/,' Containerfile 15 | git diff 16 | 17 | for runtime in podman docker; do 18 | $runtime build -t localhost/fcos-tailscale -f Containerfile . 19 | $runtime run --rm localhost/fcos-tailscale rpm -q tailscale 20 | done 21 | 22 | cd $(mktemp -d) 23 | cp ${workdir}/ostree-ext-cli . 24 | cat > Containerfile << EOF 25 | FROM $image 26 | ADD ostree-ext-cli /usr/bin/ 27 | RUN set -x; test \$(ostree-ext-cli internal-only-for-testing detect-env) = ostree-container 28 | EOF 29 | # Also verify docker buildx, which apparently doesn't have /.dockerenv 30 | docker buildx build -t localhost/fcos-tailscale -f Containerfile . 31 | 32 | echo ok container image integration 33 | -------------------------------------------------------------------------------- /crates/ostree-ext/man/ostree-container-auth.md: -------------------------------------------------------------------------------- 1 | % ostree-container-auth 5 2 | 3 | # NAME 4 | ostree-container-auth description of the registry authentication file 5 | 6 | # DESCRIPTION 7 | 8 | The OSTree container stack uses the same file formats as **containers-auth(5)** but 9 | not the same locations. 10 | 11 | When running as uid 0 (root), the tooling uses `/etc/ostree/auth.json` first, then looks 12 | in `/run/ostree/auth.json`, and finally checks `/usr/lib/ostree/auth.json`. 13 | For any other uid, the file paths used are in `${XDG_RUNTIME_DIR}/ostree/auth.json`. 14 | 15 | In the future, it is likely that a path that is supported for both "system podman" 16 | usage and ostree will be added. 17 | 18 | ## FORMAT 19 | 20 | The auth.json file stores, or references, credentials that allow the user to authenticate 21 | to container image registries. 22 | It is primarily managed by a `login` command from a container tool such as `podman login`, 23 | `buildah login`, or `skopeo login`. 24 | 25 | For more information, see **containers-auth(5)**. 26 | 27 | # SEE ALSO 28 | 29 | **containers-auth(5)**, **skopeo-login(1)**, **skopeo-logout(1)** 30 | -------------------------------------------------------------------------------- /crates/lib/src/reboot.rs: -------------------------------------------------------------------------------- 1 | //! Handling of system restarts/reboot 2 | 3 | use std::{io::Write, process::Command}; 4 | 5 | use bootc_utils::CommandRunExt; 6 | use fn_error_context::context; 7 | 8 | /// Initiate a system reboot. 9 | /// This function will only return in case of error. 10 | #[context("Initiating reboot")] 11 | pub(crate) fn reboot() -> anyhow::Result<()> { 12 | // Flush output streams 13 | let _ = std::io::stdout().flush(); 14 | let _ = std::io::stderr().flush(); 15 | Command::new("systemd-run") 16 | .args([ 17 | "--quiet", 18 | "--", 19 | "systemctl", 20 | "reboot", 21 | "--message=Initiated by bootc", 22 | ]) 23 | .run_capture_stderr()?; 24 | // We expect to be terminated via SIGTERM here. We sleep 25 | // instead of exiting an exit would necessarily appear 26 | // racy to calling processes in that sometimes we'd 27 | // win the race to exit, other times might get killed 28 | // via SIGTERM. 29 | tracing::debug!("Initiated reboot, sleeping"); 30 | loop { 31 | std::thread::park(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/ostree-ext/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The openat Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/src/man/bootc-usr-overlay.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-usr-overlay - Adds a transient writable overlayfs on `/usr` that 4 | will be discarded on reboot 5 | 6 | # SYNOPSIS 7 | 8 | **bootc usr-overlay** \[*OPTIONS...*\] 9 | 10 | # DESCRIPTION 11 | 12 | Adds a transient writable overlayfs on `/usr` that will be discarded 13 | on reboot. 14 | 15 | ## USE CASES 16 | 17 | A common pattern is wanting to use tracing/debugging tools, such as 18 | `strace` that may not be in the base image. A system package manager 19 | such as `apt` or `dnf` can apply changes into this transient overlay 20 | that will be discarded on reboot. 21 | 22 | ## /ETC AND /VAR 23 | 24 | However, this command has no effect on `/etc` and `/var` - changes 25 | written there will persist. It is common for package installations to 26 | modify these directories. 27 | 28 | ## UNMOUNTING 29 | 30 | Almost always, a system process will hold a reference to the open mount 31 | point. You can however invoke `umount -l /usr` to perform a "lazy 32 | unmount". 33 | 34 | 35 | 36 | 37 | # VERSION 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/src/installation.md: -------------------------------------------------------------------------------- 1 | # Base images 2 | 3 | Many users will be more interested in base (container) images. 4 | 5 | ## Fedora/CentOS 6 | 7 | Currently, the [Fedora/CentOS bootc project](https://docs.fedoraproject.org/en-US/bootc/) 8 | is the most closely aligned upstream project. 9 | 10 | For pre-built base images; any Fedora derivative already using `ostree` can be seamlessly converted into using bootc; 11 | for example, [Fedora CoreOS](https://quay.io/repository/fedora/fedora-coreos) can be used as a 12 | base image; you will want to also `rpm-ostree install bootc` in your image builds currently. 13 | There are some overlaps between `bootc` and `ignition` and `zincati` however; see 14 | [this pull request](https://github.com/coreos/fedora-coreos-docs/pull/540) for more information. 15 | 16 | For other derivatives such as the ["Atomic desktops"](https://gitlab.com/fedora/ostree), see 17 | discussion of [relationships](relationships.md) which particularly covers interactions with rpm-ostree. 18 | 19 | ## Other 20 | 21 | However, bootc itself is not tied to Fedora derivatives; 22 | [this issue](https://github.com/coreos/bootupd/issues/468) tracks the main blocker for other distributions. 23 | -------------------------------------------------------------------------------- /crates/tests-integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests crate 2 | 3 | This crate holds integration tests (as distinct from the regular 4 | Rust unit tests run as part of `cargo test`). 5 | 6 | ## Building and running 7 | 8 | `cargo run -p tests-integration` 9 | will work. Note that at the current time all test suites target 10 | an externally built bootc-compatible container image. See 11 | how things are set up in e.g. Github Actions, where we first 12 | run a `podman build` with the bootc git sources. 13 | 14 | ## Available suites 15 | 16 | ### `composefs-bcvk` 17 | 18 | Intended only right now to be used with a sealed UKI image, 19 | and sanity checks the composefs backend. 20 | 21 | ### `host-privileged` 22 | 23 | This suite will run the target container image in a way that expects 24 | full privileges, but is *not* destructive. 25 | 26 | ### `install-alongside` 27 | 28 | This suite is *DESTRUCTIVE*, executing the bootc `install to-existing-root` 29 | style flow using the host root. Run it in a transient virtual machine. 30 | 31 | ### `system-reinstall` 32 | 33 | This suite is *DESTRUCTIVE*, executing the `system-reinstall-bootc` 34 | tests. Run it in a transient virtual machine. 35 | -------------------------------------------------------------------------------- /.github/workflows/auto-review.yml: -------------------------------------------------------------------------------- 1 | name: Auto Assign Reviewer 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, ready_for_review] 6 | 7 | jobs: 8 | assign-reviewer: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pull-requests: write 12 | contents: read 13 | issues: write 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v6 18 | 19 | - name: Setup Python 20 | uses: actions/setup-python@v6 21 | with: 22 | python-version: '3.14' 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install pyyaml 28 | 29 | - name: Generate Bootc Actions Token 30 | id: bootc_token 31 | uses: actions/create-github-app-token@v2 32 | with: 33 | app-id: ${{ secrets.APP_ID }} 34 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 35 | 36 | - name: Assign reviewer 37 | env: 38 | PR_NUMBER: ${{ github.event.pull_request.number }} 39 | GH_TOKEN: ${{ steps.bootc_token.outputs.token }} 40 | run: | 41 | python .github/scripts/assign_reviewer.py 42 | -------------------------------------------------------------------------------- /docs/src/man/bootc-config.5.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-config - Configuration file format for bootc 4 | 5 | # SYNOPSIS 6 | 7 | **/etc/bootc/config.toml** 8 | 9 | # DESCRIPTION 10 | 11 | The bootc configuration file uses TOML format to specify various 12 | settings for bootc operation. 13 | 14 | # FILE FORMAT 15 | 16 | The configuration file is in TOML format with the following sections: 17 | 18 | ## [core] 19 | 20 | Core configuration options. 21 | 22 | **auto_updates** = *boolean* 23 | Enable or disable automatic updates. Default: false 24 | 25 | **update_interval** = *string* 26 | Update check interval (e.g., "daily", "weekly"). Default: "weekly" 27 | 28 | ## [storage] 29 | 30 | Storage-related configuration. 31 | 32 | **root** = *path* 33 | Root storage path. Default: "/sysroot/ostree" 34 | 35 | # EXAMPLES 36 | 37 | A basic configuration file: 38 | 39 | [core] 40 | auto_updates = true 41 | update_interval = "daily" 42 | 43 | [storage] 44 | root = "/var/lib/bootc" 45 | 46 | # FILES 47 | 48 | **/etc/bootc/config.toml** 49 | System-wide configuration file 50 | 51 | # SEE ALSO 52 | 53 | **bootc**(8), **toml**(5) 54 | 55 | # VERSION 56 | 57 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/ostree_manual.rs: -------------------------------------------------------------------------------- 1 | //! Manual workarounds for ostree bugs 2 | 3 | use std::io::Read; 4 | use std::ptr; 5 | 6 | use ostree::prelude::{Cast, InputStreamExtManual}; 7 | use ostree::{gio, glib}; 8 | 9 | /// Equivalent of `g_file_read()` for ostree::RepoFile to work around https://github.com/ostreedev/ostree/issues/2703 10 | #[allow(unsafe_code)] 11 | pub fn repo_file_read(f: &ostree::RepoFile) -> Result { 12 | use glib::translate::*; 13 | let stream = unsafe { 14 | let f = f.upcast_ref::(); 15 | let mut error = ptr::null_mut(); 16 | let stream = gio::ffi::g_file_read(f.to_glib_none().0, ptr::null_mut(), &mut error); 17 | if !error.is_null() { 18 | return Err(from_glib_full(error)); 19 | } 20 | // Upcast to GInputStream here 21 | from_glib_full(stream as *mut gio::ffi::GInputStream) 22 | }; 23 | 24 | Ok(stream) 25 | } 26 | 27 | /// Read a repo file to a string. 28 | pub fn repo_file_read_to_string(f: &ostree::RepoFile) -> anyhow::Result { 29 | let mut r = String::new(); 30 | let mut s = repo_file_read(f)?.into_read(); 31 | s.read_to_string(&mut r)?; 32 | Ok(r) 33 | } 34 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-ostree-to-bootc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: org.containers.bootc/v1alpha1 2 | kind: BootcHost 3 | metadata: 4 | name: host 5 | spec: 6 | image: 7 | image: quay.io/centos-bootc/centos-bootc:stream9 8 | transport: registry 9 | bootOrder: default 10 | status: 11 | staged: 12 | image: 13 | image: 14 | image: quay.io/centos-bootc/centos-bootc:stream9 15 | transport: registry 16 | architecture: s390x 17 | version: stream9.20240807.0 18 | timestamp: null 19 | imageDigest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b38 20 | cachedUpdate: null 21 | incompatible: false 22 | pinned: false 23 | store: ostreeContainer 24 | ostree: 25 | checksum: 05cbf6dcae32e7a1c5a0774a648a073a5834a305ca92204b53fb6c281fe49db1 26 | deploySerial: 0 27 | stateroot: default 28 | booted: 29 | image: null 30 | cachedUpdate: null 31 | incompatible: false 32 | pinned: false 33 | store: null 34 | ostree: 35 | checksum: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 36 | deploySerial: 0 37 | stateroot: default 38 | rollback: null 39 | rollbackQueued: false 40 | type: null 41 | -------------------------------------------------------------------------------- /crates/system-reinstall-bootc/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::BufReader}; 2 | 3 | use anyhow::{Context, Result}; 4 | use bootc_utils::PathQuotedDisplay; 5 | use fn_error_context::context; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | mod cli; 9 | 10 | /// The environment variable that can be used to specify an image. 11 | const CONFIG_VAR: &str = "BOOTC_REINSTALL_CONFIG"; 12 | 13 | #[derive(Debug, Deserialize, Serialize)] 14 | #[serde(deny_unknown_fields)] 15 | pub(crate) struct ReinstallConfig { 16 | /// The bootc image to install on the system. 17 | pub(crate) bootc_image: String, 18 | pub(crate) composefs_backend: bool, 19 | } 20 | 21 | impl ReinstallConfig { 22 | #[context("load")] 23 | pub fn load() -> Result> { 24 | let Some(config) = std::env::var_os(CONFIG_VAR) else { 25 | return Ok(None); 26 | }; 27 | let f = File::open(&config) 28 | .with_context(|| format!("Opening {}", PathQuotedDisplay::new(&config))) 29 | .map(BufReader::new)?; 30 | let r = serde_yaml::from_reader(f) 31 | .with_context(|| format!("Parsing config from {}", PathQuotedDisplay::new(&config)))?; 32 | Ok(Some(r)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/011-test-resolvconf.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "verify there's not an empty /etc/resolv.conf in the image" 5 | 6 | let st = bootc status --json | from json 7 | 8 | # Detect composefs by checking if composefs field is present 9 | let is_composefs = ($st.status.booted.composefs? != null) 10 | if $is_composefs { 11 | print "# TODO composefs: skipping test - ostree commands don't work with composefs" 12 | } else { 13 | let booted_ostree = $st.status.booted.ostree.checksum; 14 | 15 | # ostree ls should probably have --json and a clean way to not error on ENOENT 16 | let resolvconf = ostree ls $booted_ostree /usr/etc | split row (char newline) | find resolv.conf 17 | if ($resolvconf | length) > 0 { 18 | let parts = $resolvconf | first | split row -r '\s+' 19 | let ty = $parts | first | split chars | first 20 | # If resolv.conf exists in the image, currently require it in our 21 | # test suite to be a symlink (which is hopefully to the systemd/stub-resolv.conf) 22 | assert equal $ty 'l' 23 | print "resolv.conf is a symlink" 24 | } else { 25 | print "No resolv.conf found in commit" 26 | } 27 | } 28 | 29 | tap ok 30 | -------------------------------------------------------------------------------- /crates/ostree-ext/ci/priv-test-cockpit-selinux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Assumes that the current environment is a privileged container 3 | # with the host mounted at /run/host. We can basically write 4 | # whatever we want, however we can't actually *reboot* the host. 5 | set -euo pipefail 6 | 7 | sysroot=/run/host 8 | stateroot=test-cockpit 9 | repo=$sysroot/ostree/repo 10 | image=registry.gitlab.com/fedora/bootc/tests/container-fixtures/cockpit 11 | imgref=ostree-unverified-registry:${image} 12 | 13 | cd $(mktemp -d -p /var/tmp) 14 | 15 | set -x 16 | 17 | if test '!' -e "${sysroot}/ostree"; then 18 | ostree admin init-fs --epoch=1 "${sysroot}" 19 | ostree config --repo $repo set sysroot.bootloader none 20 | fi 21 | ostree admin stateroot-init "${stateroot}" --sysroot "${sysroot}" 22 | ostree-ext-cli container image deploy --sysroot "${sysroot}" \ 23 | --stateroot "${stateroot}" --imgref "${imgref}" 24 | ref=$(ostree refs --repo $repo ostree/container/image | head -1) 25 | commit=$(ostree rev-parse --repo $repo ostree/container/image/$ref) 26 | ostree ls --repo $repo -X ${commit} /usr/lib/systemd/system|grep -i cockpit >out.txt 27 | if ! grep -q :cockpit_unit_file_t:s0 out.txt; then 28 | echo "failed to find cockpit_unit_file_t" 1>&2 29 | exit 1 30 | fi 31 | 32 | echo ok "derived selinux" 33 | -------------------------------------------------------------------------------- /docs/src/intro.md: -------------------------------------------------------------------------------- 1 | # bootc 2 | 3 | Transactional, in-place operating system updates using OCI/Docker container images. 4 | bootc is the key component in a broader mission of [bootable containers](https://containers.github.io/bootable/). 5 | 6 | The original Docker container model of using "layers" to model 7 | applications has been extremely successful. This project 8 | aims to apply the same technique for bootable host systems - using 9 | standard OCI/Docker containers as a transport and delivery format 10 | for base operating system updates. 11 | 12 | The container image includes a Linux kernel (in e.g. `/usr/lib/modules`), 13 | which is used to boot. At runtime on a target system, the base userspace is 14 | *not* itself running in a container by default. For example, assuming 15 | systemd is in use, systemd acts as pid1 as usual - there's no "outer" process. 16 | 17 | # Status 18 | 19 | The CLI and API for bootc are now considered stable. Every existing system 20 | can be upgraded in place seamlessly across any future changes. 21 | 22 | However, the core underlying code uses the [ostree](https://github.com/ostreedev/ostree) 23 | project which has been powering stable operating system updates for 24 | many years. The stability here generally refers to the surface 25 | APIs, not the underlying logic. 26 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/install-systemd-boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | curl http://192.168.122.1:8000/bootc -o bootc 6 | chmod +x bootc 7 | 8 | IMAGE=quay.io/fedora/fedora-bootc-uki:42 9 | 10 | if [[ ! -f /srv/systemd-bootx64.efi ]]; then 11 | echo "Needs /srv/systemd-bootx64.efi to exists for now" 12 | exit 1 13 | fi 14 | 15 | # --env RUST_LOG=debug \ 16 | # --env RUST_BACKTRACE=1 \ 17 | podman run \ 18 | --rm --privileged \ 19 | --pid=host \ 20 | -v /dev:/dev \ 21 | -v /var/lib/containers:/var/lib/containers \ 22 | -v /srv/bootc:/usr/bin/bootc:ro,Z \ 23 | -v /var/tmp:/var/tmp \ 24 | --security-opt label=type:unconfined_t \ 25 | "${IMAGE}" \ 26 | bootc install to-disk \ 27 | --composefs-backend \ 28 | --boot=uki \ 29 | --source-imgref="containers-storage:${IMAGE}" \ 30 | --target-imgref="${IMAGE}" \ 31 | --target-transport="docker" \ 32 | /dev/vdb \ 33 | --filesystem=ext4 \ 34 | --wipe 35 | 36 | mkdir -p efi 37 | mount /dev/vdb2 /srv/efi 38 | 39 | # Manual systemd-boot installation 40 | cp /srv/systemd-bootx64.efi /srv/efi/EFI/fedora/grubx64.efi 41 | mkdir -p /srv/efi/loader 42 | echo "timeout 5" > /srv/efi/loader/loader.conf 43 | rm -rf /srv/efi/EFI/fedora/grub.cfg 44 | 45 | umount efi 46 | -------------------------------------------------------------------------------- /docs/src/bootc-via-api.md: -------------------------------------------------------------------------------- 1 | # Using bootc via API 2 | 3 | At the current time, bootc is primarily intended to be 4 | driven via a fork/exec model. The core CLI verbs 5 | are stable and will not change. 6 | 7 | ## Using `bootc edit` and `bootc status --json` 8 | 9 | While bootc does not depend on Kubernetes, it does currently 10 | also offer a Kubernetes *style* API, especially oriented 11 | towards the [spec and status and other conventions](https://kubernetes.io/docs/reference/using-api/api-concepts/). 12 | 13 | In general, most use cases of driving bootc via API are probably 14 | most easily done by forking off `bootc upgrade` when desired, 15 | and viewing `bootc status --json --format-version=1`. 16 | 17 | ## JSON Schema 18 | 19 | The current API `org.containers.bootc/v1` is stable. 20 | In order to support the future introduction of a v2 21 | or newer format, please change your code now to explicitly 22 | request `--format-version=1` as referenced above. (Available 23 | since bootc 0.1.15, `--format-version=0` in bootc 0.1.14). 24 | 25 | There is a [JSON schema](https://json-schema.org/) generated from 26 | the Rust source code available here: [host-v1.schema.json](host-v1.schema.json). 27 | 28 | A common way to use this is to run a code generator such as 29 | [go-jsonschema](https://github.com/omissis/go-jsonschema) on the 30 | input schema. 31 | -------------------------------------------------------------------------------- /ci/installdeps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | 4 | OS_ID=$(. /usr/lib/os-release && echo $ID) 5 | 6 | baseurl= 7 | case $OS_ID in 8 | fedora) baseurl=https://download.copr.fedorainfracloud.org/results/@CoreOS/continuous/fedora-\$releasever-\$basearch/ ;; 9 | # Default to c9s (also covers all variants/derivatives) 10 | *) baseurl=https://download.copr.fedorainfracloud.org/results/@CoreOS/continuous/centos-stream-\$releasever-\$basearch/ ;; 11 | esac 12 | 13 | # For some reason dnf copr enable -y says there are no builds? 14 | cat >/etc/yum.repos.d/coreos-continuous.repo << EOF 15 | [copr:copr.fedorainfracloud.org:group_CoreOS:continuous] 16 | name=Copr repo for continuous owned by @CoreOS 17 | baseurl=$baseurl 18 | type=rpm-md 19 | skip_if_unavailable=True 20 | gpgcheck=1 21 | gpgkey=https://download.copr.fedorainfracloud.org/results/@CoreOS/continuous/pubkey.gpg 22 | repo_gpgcheck=0 23 | enabled=1 24 | enabled_metadata=1 25 | EOF 26 | 27 | # TODO: Recursively extract this from the existing cargo system-deps metadata 28 | case $OS_ID in 29 | fedora) dnf -y builddep bootc ;; 30 | *) dnf -y install libzstd-devel openssl-devel ostree-devel cargo ;; 31 | esac 32 | 33 | bindeps=$(cargo metadata --format-version 1 --no-deps | jq -r '.metadata.["binary-dependencies"].bins | map("/usr/bin/" + .) | join(" ")') 34 | dnf -y install $bindeps 35 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-bls/extra/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Colin Walters 2 | # 3 | # This library is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU Lesser General Public 5 | # License as published by the Free Software Foundation; either 6 | # version 2 of the License, or (at your option) any later version. 7 | # 8 | # This library is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public 14 | # License along with this library. If not, see . 15 | 16 | [Unit] 17 | DefaultDependencies=no 18 | ConditionKernelCommandLine=composefs 19 | ConditionPathExists=/etc/initrd-release 20 | After=sysroot.mount 21 | Requires=sysroot.mount 22 | Before=initrd-root-fs.target 23 | Before=initrd-switch-root.target 24 | 25 | OnFailure=emergency.target 26 | OnFailureJobMode=isolate 27 | 28 | [Service] 29 | Type=oneshot 30 | ExecStart=/usr/bin/bootc-initramfs-setup 31 | StandardInput=null 32 | StandardOutput=journal 33 | StandardError=journal+console 34 | RemainAfterExit=yes 35 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/extra/usr/lib/dracut/modules.d/37bootc/bootc-initramfs-setup.service: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Colin Walters 2 | # 3 | # This library is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU Lesser General Public 5 | # License as published by the Free Software Foundation; either 6 | # version 2 of the License, or (at your option) any later version. 7 | # 8 | # This library is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public 14 | # License along with this library. If not, see . 15 | 16 | [Unit] 17 | DefaultDependencies=no 18 | ConditionKernelCommandLine=composefs 19 | ConditionPathExists=/etc/initrd-release 20 | After=sysroot.mount 21 | Requires=sysroot.mount 22 | Before=initrd-root-fs.target 23 | Before=initrd-switch-root.target 24 | 25 | OnFailure=emergency.target 26 | OnFailureJobMode=isolate 27 | 28 | [Service] 29 | Type=oneshot 30 | ExecStart=/usr/bin/bootc-initramfs-setup 31 | StandardInput=null 32 | StandardOutput=journal 33 | StandardError=journal+console 34 | RemainAfterExit=yes 35 | -------------------------------------------------------------------------------- /crates/kernel_cmdline/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Kernel command line parsing utilities. 2 | //! 3 | //! This module provides functionality for parsing and working with kernel command line 4 | //! arguments, supporting both key-only switches and key-value pairs with proper quote handling. 5 | //! 6 | //! The kernel command line is not required to be UTF-8. The `bytes` 7 | //! module works on arbitrary byte data and attempts to parse the 8 | //! command line in the same manner as the kernel itself. 9 | //! 10 | //! The `utf8` module performs the same functionality, but requires 11 | //! all data to be valid UTF-8. 12 | 13 | pub mod bytes; 14 | pub mod utf8; 15 | 16 | /// This is used by dracut. 17 | pub const INITRD_ARG_PREFIX: &str = "rd."; 18 | /// The kernel argument for configuring the rootfs flags. 19 | pub const ROOTFLAGS: &str = "rootflags"; 20 | 21 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 22 | /// Possible outcomes for `add_or_modify` operations. 23 | pub enum Action { 24 | /// The parameter did not exist before and was added 25 | Added, 26 | /// The parameter existed before, but contained a different value. 27 | /// The value was updated to the newly-requested value. 28 | Modified, 29 | /// The parameter existed before, and contained the same value as 30 | /// the newly-requested value. No modification was made. 31 | Existed, 32 | } 33 | -------------------------------------------------------------------------------- /hack/Containerfile: -------------------------------------------------------------------------------- 1 | # Build a container image that has extra testing stuff in it, such 2 | # as nushell, some preset logically bound images, etc. This expects 3 | # to create an image derived FROM localhost/bootc which was created 4 | # by the Dockerfile at top. 5 | 6 | FROM scratch as context 7 | # We only need this stuff in the initial context 8 | COPY . / 9 | 10 | # An intermediate layer which caches the extended RPMS 11 | FROM localhost/bootc as extended 12 | # And this layer has additional stuff for testing, such as nushell etc. 13 | RUN --mount=type=bind,from=context,target=/run/context <( 13 | priority: libsystemd::logging::Priority, 14 | msg: &str, 15 | vars: impl Iterator, 16 | ) where 17 | K: AsRef, 18 | V: AsRef, 19 | { 20 | if !libsystemd::daemon::booted() { 21 | return; 22 | } 23 | if let Err(e) = libsystemd::logging::journal_send(priority, msg, vars) { 24 | if !EMITTED_JOURNAL_ERROR.swap(true, Ordering::SeqCst) { 25 | eprintln!("failed to write to journal: {e}"); 26 | } 27 | } 28 | } 29 | 30 | /// Wrapper for writing to systemd journal which is an explicit no-op 31 | /// when systemd is not in use (e.g. in a container). 32 | #[allow(dead_code)] 33 | pub(crate) fn journal_print(priority: libsystemd::logging::Priority, msg: &str) { 34 | let vars: HashMap<&str, &str> = HashMap::new(); 35 | journal_send(priority, msg, vars.into_iter()) 36 | } 37 | -------------------------------------------------------------------------------- /contrib/packaging/build-rpm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Build bootc RPM package from source 3 | set -xeuo pipefail 4 | 5 | # Version can be passed via RPM_VERSION env var (set by Dockerfile ARG) 6 | # or defaults to the hardcoded value in the spec file 7 | VERSION="${RPM_VERSION:-}" 8 | 9 | # Determine output directory (defaults to /out) 10 | OUTPUT_DIR="${1:-/out}" 11 | SRC_DIR="${2:-/src}" 12 | 13 | if [ -n "${VERSION}" ]; then 14 | echo "Building RPM with version: ${VERSION}" 15 | else 16 | echo "Building RPM with version from spec file" 17 | fi 18 | 19 | # Create temporary rpmbuild directories 20 | mkdir -p /tmp/rpmbuild/{RPMS,BUILDROOT,SPECS} 21 | 22 | # If version is provided, create modified spec file; otherwise use original 23 | if [ -n "${VERSION}" ]; then 24 | sed "s/^Version:.*/Version: ${VERSION}/" \ 25 | "${SRC_DIR}/contrib/packaging/bootc.spec" > /tmp/rpmbuild/SPECS/bootc.spec 26 | SPEC_FILE=/tmp/rpmbuild/SPECS/bootc.spec 27 | else 28 | SPEC_FILE="${SRC_DIR}/contrib/packaging/bootc.spec" 29 | fi 30 | 31 | # Build RPM 32 | rpmbuild -bb \ 33 | --define "_topdir /tmp/rpmbuild" \ 34 | --define "_builddir ${SRC_DIR}" \ 35 | --define "container_build 1" \ 36 | --with tests \ 37 | --nocheck \ 38 | "${SPEC_FILE}" 39 | 40 | # Copy built RPMs to output directory 41 | ARCH=$(uname -m) 42 | mkdir -p "${OUTPUT_DIR}" 43 | cp /tmp/rpmbuild/RPMS/${ARCH}/*.rpm "${OUTPUT_DIR}/" 44 | rm -rf /tmp/rpmbuild 45 | -------------------------------------------------------------------------------- /docs/src/man/bootc-container-lint.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-container-lint - Perform relatively inexpensive static analysis 4 | checks as part of a container build 5 | 6 | # SYNOPSIS 7 | 8 | **bootc container lint** \[*OPTIONS...*\] 9 | 10 | # DESCRIPTION 11 | 12 | Perform relatively inexpensive static analysis checks as part of a 13 | container build. 14 | 15 | This is intended to be invoked via e.g. `RUN bootc container lint` as 16 | part of a build process; it will error if any problems are detected. 17 | 18 | # OPTIONS 19 | 20 | 21 | **--rootfs**=*ROOTFS* 22 | 23 | Operate on the provided rootfs 24 | 25 | Default: / 26 | 27 | **--fatal-warnings** 28 | 29 | Make warnings fatal 30 | 31 | **--list** 32 | 33 | Instead of executing the lints, just print all available lints. At the current time, this will output in YAML format because it's reasonably human friendly. However, there is no commitment to maintaining this exact format; do not parse it via code or scripts 34 | 35 | **--skip**=*SKIP* 36 | 37 | Skip checking the targeted lints, by name. Use `--list` to discover the set of available lints 38 | 39 | **--no-truncate** 40 | 41 | Don't truncate the output. By default, only a limited number of entries are shown for each lint, followed by a count of remaining entries 42 | 43 | 44 | 45 | # VERSION 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-v1a1-orig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: org.containers.bootc/v1alpha1 2 | kind: BootcHost 3 | metadata: 4 | name: host 5 | spec: 6 | image: 7 | image: quay.io/example/someimage:latest 8 | transport: registry 9 | signature: insecure 10 | status: 11 | staged: 12 | image: 13 | image: 14 | image: quay.io/example/someimage:latest 15 | transport: registry 16 | signature: insecure 17 | version: nightly 18 | timestamp: 2023-10-14T19:22:15Z 19 | imageDigest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 20 | architecture: amd64 21 | incompatible: false 22 | pinned: false 23 | ostree: 24 | checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d 25 | deploySerial: 0 26 | stateroot: default 27 | booted: 28 | image: 29 | image: 30 | image: quay.io/example/someimage:latest 31 | transport: registry 32 | signature: insecure 33 | version: nightly 34 | timestamp: 2023-09-30T19:22:16Z 35 | imageDigest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 36 | architecture: amd64 37 | incompatible: false 38 | pinned: false 39 | ostree: 40 | checksum: 26836632adf6228d64ef07a26fd3efaf177104efd1f341a2cf7909a3e4e2c72c 41 | deploySerial: 0 42 | stateroot: default 43 | rollback: null 44 | isContainer: false 45 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-staged-rollback.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: org.containers.bootc/v1alpha1 2 | kind: BootcHost 3 | metadata: 4 | name: host 5 | spec: 6 | image: 7 | image: quay.io/example/someimage:latest 8 | transport: registry 9 | signature: insecure 10 | status: 11 | staged: 12 | image: 13 | image: 14 | image: quay.io/example/someimage:latest 15 | transport: registry 16 | signature: insecure 17 | architecture: s390x 18 | version: nightly 19 | timestamp: 2023-10-14T19:22:15Z 20 | imageDigest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 21 | incompatible: false 22 | pinned: false 23 | ostree: 24 | checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d 25 | deploySerial: 0 26 | stateroot: default 27 | booted: null 28 | rollback: 29 | image: 30 | image: 31 | image: quay.io/example/someimage:latest 32 | transport: registry 33 | signature: insecure 34 | architecture: s390x 35 | version: nightly 36 | timestamp: 2023-09-30T19:22:16Z 37 | imageDigest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 38 | incompatible: false 39 | pinned: false 40 | ostree: 41 | checksum: 26836632adf6228d64ef07a26fd3efaf177104efd1f341a2cf7909a3e4e2c72c 42 | deploySerial: 0 43 | stateroot: default 44 | isContainer: false 45 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | 4 | /// Set to true if we failed to write to the journal once 5 | static EMITTED_JOURNAL_ERROR: AtomicBool = AtomicBool::new(false); 6 | 7 | /// Wrapper for systemd structured logging which only emits a message 8 | /// if we're targeting the system repository, and it's booted. 9 | pub(crate) fn system_repo_journal_send( 10 | repo: &ostree::Repo, 11 | priority: libsystemd::logging::Priority, 12 | msg: &str, 13 | vars: impl Iterator, 14 | ) where 15 | K: AsRef, 16 | V: AsRef, 17 | { 18 | if !libsystemd::daemon::booted() { 19 | return; 20 | } 21 | if !repo.is_system() { 22 | return; 23 | } 24 | if let Err(e) = libsystemd::logging::journal_send(priority, msg, vars) { 25 | if !EMITTED_JOURNAL_ERROR.swap(true, Ordering::SeqCst) { 26 | eprintln!("failed to write to journal: {e}"); 27 | } 28 | } 29 | } 30 | 31 | /// Wrapper for systemd structured logging which only emits a message 32 | /// if we're targeting the system repository, and it's booted. 33 | pub(crate) fn system_repo_journal_print( 34 | repo: &ostree::Repo, 35 | priority: libsystemd::logging::Priority, 36 | msg: &str, 37 | ) { 38 | let vars: HashMap<&str, &str> = HashMap::new(); 39 | system_repo_journal_send(repo, priority, msg, vars.into_iter()) 40 | } 41 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/selinux.rs: -------------------------------------------------------------------------------- 1 | //! SELinux-related helper APIs. 2 | 3 | use anyhow::Result; 4 | use fn_error_context::context; 5 | use std::path::Path; 6 | 7 | /// The well-known selinuxfs mount point 8 | const SELINUX_MNT: &str = "/sys/fs/selinux"; 9 | /// Hardcoded value for SELinux domain capable of setting unknown contexts. 10 | const INSTALL_T: &str = "install_t"; 11 | 12 | /// Query for whether or not SELinux is enabled. 13 | pub fn is_selinux_enabled() -> bool { 14 | Path::new(SELINUX_MNT).join("access").exists() 15 | } 16 | 17 | /// Return an error If the current process is not running in the `install_t` domain. 18 | #[context("Verifying self is install_t SELinux domain")] 19 | pub fn verify_install_domain() -> Result<()> { 20 | // If it doesn't look like SELinux is enabled, then nothing to do. 21 | if !is_selinux_enabled() { 22 | return Ok(()); 23 | } 24 | 25 | // If we're not root, there's no need to try to warn because we can only 26 | // do read-only operations anyways. 27 | if !rustix::process::getuid().is_root() { 28 | return Ok(()); 29 | } 30 | 31 | let self_domain = std::fs::read_to_string("/proc/self/attr/current")?; 32 | let is_install_t = self_domain.split(':').any(|x| x == INSTALL_T); 33 | if !is_install_t { 34 | anyhow::bail!( 35 | "Detected SELinux enabled system, but the executing binary is not labeled install_exec_t" 36 | ); 37 | } 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /crates/system-reinstall-bootc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "system-reinstall-bootc" 3 | version = "0.1.9" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/bootc-dev/bootc" 7 | publish = false 8 | # For now don't bump this above what is currently shipped in RHEL9. 9 | rust-version = "1.75.0" 10 | 11 | # See https://github.com/coreos/cargo-vendor-filterer 12 | [package.metadata.vendor-filter] 13 | # For now we only care about tier 1+2 Linux. (In practice, it's unlikely there is a tier3-only Linux dependency) 14 | platforms = ["*-unknown-linux-gnu"] 15 | 16 | [dependencies] 17 | # Internal crates 18 | bootc-mount = { path = "../mount" } 19 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.0.0" } 20 | 21 | # Workspace dependencies 22 | anstream = { workspace = true } 23 | anyhow = { workspace = true } 24 | clap = { workspace = true, features = ["derive"] } 25 | fn-error-context = { workspace = true } 26 | indoc = { workspace = true } 27 | log = { workspace = true } 28 | rustix = { workspace = true } 29 | serde = { workspace = true, features = ["derive"] } 30 | serde_json = { workspace = true } 31 | tempfile = { workspace = true } 32 | tracing = { workspace = true } 33 | uzers = { workspace = true } 34 | 35 | # Crate-specific dependencies 36 | crossterm = "0.29.0" 37 | dialoguer = "0.12.0" 38 | openssh-keys = "0.6.4" 39 | serde_yaml = "0.9.22" 40 | which = "8.0.0" 41 | 42 | [lints] 43 | workspace = true 44 | -------------------------------------------------------------------------------- /contrib/packaging/configure-rootfs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Configure rootfs type for bootc installation 3 | set -xeuo pipefail 4 | 5 | VARIANT="${1:-}" 6 | ROOTFS="${2:-}" 7 | 8 | # Support overriding the rootfs at build time 9 | CONFIG_DIR="/usr/lib/bootc/install" 10 | mkdir -p "${CONFIG_DIR}" 11 | 12 | # Do we have an explicit build-time override? Then write it. 13 | if [ -n "$ROOTFS" ]; then 14 | cat > "${CONFIG_DIR}/80-rootfs-override.toml" < "${CONFIG_DIR}/80-ext4-composefs.toml" < Result { 12 | let mut buf = [0u8; 1024]; 13 | let mut fdpath = PathBuf::from("/proc/self/fd"); 14 | fdpath.push(DecInt::new(d.as_raw_fd())); 15 | fdpath.push(filename); 16 | match rustix::fs::lgetxattr(fdpath, "security.selinux", &mut buf) { 17 | // Ignore EOPNOTSUPPORTED 18 | Ok(_) | Err(rustix::io::Errno::OPNOTSUPP) => Ok(true), 19 | Err(rustix::io::Errno::NODATA) => Ok(false), 20 | Err(e) => Err(e.into()), 21 | } 22 | } 23 | 24 | pub(crate) fn verify_selinux_recurse(root: &Dir, warn: bool) -> Result<()> { 25 | root.walk(&WalkConfiguration::default().noxdev(), |e| { 26 | let exists = verify_selinux_label_exists(e.dir, e.filename) 27 | .with_context(|| format!("Failed to look up context for {:?}", e.path))?; 28 | if !exists { 29 | if warn { 30 | eprintln!("No SELinux label found for: {:?}", e.path); 31 | } else { 32 | anyhow::bail!("No SELinux label found for: {:?}", e.path); 33 | } 34 | } 35 | anyhow::Ok(ControlFlow::Continue(())) 36 | })?; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /ci/run-kola.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | 4 | # We require the an image containing bootc-under-test to have been injected 5 | # by an external system, e.g. Prow 6 | # https://docs.ci.openshift.org/docs/architecture/ci-operator/#referring-to-images-in-tests 7 | if test -z "${TARGET_IMAGE:-}"; then 8 | echo "fatal: Must set TARGET_IMAGE" 1>&2; exit 1 9 | fi 10 | echo "Test base image: ${TARGET_IMAGE}" 11 | 12 | 13 | tmpdir="$(mktemp -d -p /var/tmp)" 14 | cd "${tmpdir}" 15 | 16 | # Detect Prow; if we find it, assume the image requires a pull secret 17 | kola_args=() 18 | if test -n "${JOB_NAME_HASH:-}"; then 19 | oc registry login --to auth.json 20 | cat > pull-secret.bu << 'EOF' 21 | variant: fcos 22 | version: 1.1.0 23 | storage: 24 | files: 25 | - path: /etc/ostree/auth.json 26 | contents: 27 | local: auth.json 28 | systemd: 29 | units: 30 | - name: zincati.service 31 | dropins: 32 | - name: disabled.conf 33 | contents: | 34 | [Unit] 35 | ConditionPathExists=/enoent 36 | 37 | EOF 38 | butane -d . < pull-secret.bu > pull-secret.ign 39 | kola_args+=("--append-ignition" "pull-secret.ign") 40 | fi 41 | 42 | if test -z "${BASE_QEMU_IMAGE:-}"; then 43 | coreos-installer download -p qemu -f qcow2.xz --decompress 44 | BASE_QEMU_IMAGE="$(echo *.qcow2)" 45 | fi 46 | cosa kola run --oscontainer ostree-unverified-registry:${TARGET_IMAGE} --qemu-image "./${BASE_QEMU_IMAGE}" "${kola_args[@]}" ext.bootc.'*' 47 | 48 | echo "ok kola bootc" 49 | -------------------------------------------------------------------------------- /crates/lib/src/fixtures/spec-staged-booted.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: org.containers.bootc/v1alpha1 2 | kind: BootcHost 3 | metadata: 4 | name: host 5 | spec: 6 | image: 7 | image: quay.io/example/someimage:latest 8 | transport: registry 9 | signature: insecure 10 | status: 11 | staged: 12 | image: 13 | image: 14 | image: quay.io/example/someimage:latest 15 | transport: registry 16 | signature: insecure 17 | architecture: arm64 18 | version: nightly 19 | # This one has nanoseconds, which should be dropped for human consumption 20 | timestamp: 2023-10-14T19:22:15.42Z 21 | imageDigest: sha256:16dc2b6256b4ff0d2ec18d2dbfb06d117904010c8cf9732cdb022818cf7a7566 22 | incompatible: false 23 | pinned: false 24 | ostree: 25 | checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d 26 | deploySerial: 0 27 | stateroot: default 28 | booted: 29 | image: 30 | image: 31 | image: quay.io/example/someimage:latest 32 | transport: registry 33 | signature: insecure 34 | architecture: arm64 35 | version: nightly 36 | timestamp: 2023-09-30T19:22:16Z 37 | imageDigest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 38 | incompatible: false 39 | pinned: false 40 | ostree: 41 | checksum: 26836632adf6228d64ef07a26fd3efaf177104efd1f341a2cf7909a3e4e2c72c 42 | deploySerial: 0 43 | stateroot: default 44 | rollback: null 45 | isContainer: false 46 | -------------------------------------------------------------------------------- /crates/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | //! The main entrypoint for bootc, which just performs global initialization, and then 2 | //! calls out into the library. 3 | //! 4 | use anyhow::Result; 5 | 6 | /// The code called after we've done process global init and created 7 | /// an async runtime. 8 | async fn async_main() -> Result<()> { 9 | bootc_utils::initialize_tracing(); 10 | 11 | tracing::trace!("starting bootc"); 12 | 13 | // As you can see, the role of this file is mostly to just be a shim 14 | // to call into the code that lives in the internal shared library. 15 | bootc_lib::cli::run_from_iter(std::env::args()).await 16 | } 17 | 18 | /// Perform process global initialization, then create an async runtime 19 | /// and do the rest of the work there. 20 | fn run() -> Result<()> { 21 | // Initialize global state before we've possibly created other threads, etc. 22 | bootc_lib::cli::global_init()?; 23 | // We only use the "current thread" runtime because we don't perform 24 | // a lot of CPU heavy work in async tasks. Where we do work on the CPU, 25 | // or we do want explicit concurrency, we typically use 26 | // tokio::task::spawn_blocking to create a new OS thread explicitly. 27 | let runtime = tokio::runtime::Builder::new_current_thread() 28 | .enable_all() 29 | .build() 30 | .expect("Failed to build tokio runtime"); 31 | // And invoke the async_main 32 | runtime.block_on(async move { async_main().await }) 33 | } 34 | 35 | fn main() { 36 | bootc_utils::run_main(run) 37 | } 38 | -------------------------------------------------------------------------------- /docs/src/booting-local-builds.md: -------------------------------------------------------------------------------- 1 | # Booting local builds 2 | 3 | In some scenarios, you may want to boot a *locally* built 4 | container image, in order to apply a persistent hotfix 5 | to a specific server, or as part of a development/testing 6 | scenario. 7 | 8 | ## Building a new local image 9 | 10 | At the current time, the bootc host container storage is distinct 11 | from that of the `podman` container runtime storage (default 12 | configuration in `/var/lib/containers`). 13 | 14 | It not currently streamlined to export the booted host container 15 | storage into the podman storage. 16 | 17 | Hence today, to replicate the exact container image the 18 | host has booted, take the container image referenced 19 | in `bootc status` and turn it into a `podman pull` 20 | invocation. 21 | 22 | Next, craft a container build file with your desired changes: 23 | ``` 24 | FROM 25 | RUN apt|dnf upgrade https://example.com/systemd-hotfix.package 26 | ``` 27 | 28 | ## Copying an updated image into the bootc storage 29 | 30 | This command is straightforward; we just need to tell bootc 31 | to fetch updates from `containers-storage`, which is the 32 | local "application" container runtime (podman) storage: 33 | 34 | ``` 35 | $ bootc switch --transport containers-storage quay.io/fedora/fedora-bootc:40 36 | ``` 37 | 38 | From there, the new image will be queued for the next boot 39 | and a `reboot` will apply it. 40 | 41 | For more on valid transports, see [containers-transports](https://github.com/containers/image/blob/main/docs/containers-transports.5.md). 42 | -------------------------------------------------------------------------------- /tmt/bug-soft-reboot.md: -------------------------------------------------------------------------------- 1 | # TMT soft-reboot limitation 2 | 3 | TMT does not currently support systemd soft-reboots. It detects reboots by checking 4 | if the `/proc/stat` btime (boot time) field changes, which does not happen during 5 | a systemd soft-reboot. 6 | 7 | See: 8 | 9 | Note: This same issue affects Testing Farm as documented in `plans/integration.fmf` 10 | where `test-27-custom-selinux-policy` is disabled for Packit (AWS) testing. 11 | 12 | ## Impact on bootc testing 13 | 14 | This means that when testing `bootc switch --soft-reboot=auto` or `bootc upgrade --soft-reboot=auto`: 15 | 16 | 1. The bootc commands will correctly prepare for a soft-reboot (staging the deployment in `/run/nextroot`) 17 | 2. However, TMT cannot detect or properly handle the soft-reboot 18 | 3. Tests must explicitly reset the soft-reboot preparation before calling `tmt-reboot` 19 | 20 | ## Workaround 21 | 22 | After calling bootc with `--soft-reboot=auto`, use: 23 | 24 | ```nushell 25 | ostree admin prepare-soft-reboot --reset 26 | tmt-reboot 27 | ``` 28 | 29 | This forces a full reboot instead of a soft-reboot, which TMT can properly detect. 30 | 31 | ## Testing environments 32 | 33 | - **testcloud**: Accidentally worked because libvirt forced a full VM power cycle, overriding systemd's soft-reboot attempt 34 | - **bcvk**: Exposes the real issue because it allows actual systemd soft-reboots 35 | - **Production (AWS, bare metal, etc.)**: Not affected - TMT is purely a testing framework; soft-reboots work correctly in production 36 | -------------------------------------------------------------------------------- /crates/utils/src/tracing_util.rs: -------------------------------------------------------------------------------- 1 | //! Helpers related to tracing, used by main entrypoints 2 | 3 | use tracing_subscriber::prelude::*; 4 | 5 | /// Initialize tracing with the default configuration. 6 | pub fn initialize_tracing() { 7 | // Always try to use journald subscriber if we're running as root; 8 | // This ensures key messages (info, warn, error) go to the journal 9 | let journald_layer = if rustix::process::getuid().is_root() { 10 | tracing_journald::layer() 11 | .ok() 12 | .map(|layer| layer.with_filter(tracing_subscriber::filter::LevelFilter::INFO)) 13 | } else { 14 | None 15 | }; 16 | 17 | // Always add the stdout/stderr layer for RUST_LOG support 18 | // This preserves the existing workflow for users 19 | let format = tracing_subscriber::fmt::format() 20 | .without_time() 21 | .with_target(false) 22 | .compact(); 23 | 24 | let fmt_layer = tracing_subscriber::fmt::layer() 25 | .event_format(format) 26 | .with_writer(std::io::stderr) 27 | .with_filter(tracing_subscriber::EnvFilter::from_default_env()); 28 | 29 | // Build the registry with layers, handling the journald layer conditionally 30 | match journald_layer { 31 | Some(journald) => { 32 | tracing_subscriber::registry() 33 | .with(fmt_layer) 34 | .with(journald) 35 | .init(); 36 | } 37 | None => { 38 | tracing_subscriber::registry().with(fmt_layer).init(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/utils/src/reexec.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::process::CommandExt; 2 | use std::path::PathBuf; 3 | use std::process::Command; 4 | 5 | use anyhow::Result; 6 | 7 | /// Environment variable holding a reference to our original binary 8 | pub const ORIG: &str = "_BOOTC_ORIG_EXE"; 9 | 10 | /// Return the path to our own executable. In some cases (SELinux) we may have 11 | /// performed a re-exec with a temporary copy of the binary and 12 | /// this environment variable will hold the path to the original binary. 13 | pub fn executable_path() -> Result { 14 | if let Some(p) = std::env::var_os(ORIG) { 15 | Ok(p.into()) 16 | } else { 17 | std::env::current_exe().map_err(Into::into) 18 | } 19 | } 20 | 21 | /// Re-execute the current process if the provided environment variable is not set. 22 | pub fn reexec_with_guardenv(k: &str, prefix_args: &[&str]) -> Result<()> { 23 | if std::env::var_os(k).is_some() { 24 | tracing::trace!("Skipping re-exec due to env var {k}"); 25 | return Ok(()); 26 | } 27 | let self_exe = executable_path()?; 28 | let mut prefix_args = prefix_args.iter(); 29 | let mut cmd = if let Some(p) = prefix_args.next() { 30 | let mut c = Command::new(p); 31 | c.args(prefix_args); 32 | c.arg(self_exe); 33 | c 34 | } else { 35 | Command::new(self_exe) 36 | }; 37 | cmd.env(k, "1"); 38 | cmd.args(std::env::args_os().skip(1)); 39 | cmd.arg0(crate::NAME); 40 | tracing::debug!("Re-executing current process for {k}"); 41 | Err(cmd.exec().into()) 42 | } 43 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: containers/bootc 2 | description: bootc documentation 3 | baseurl: "/bootc" 4 | url: "https://bootc-dev.github.io" 5 | # Comment above and use below for local development 6 | # url: "http://localhost:4000" 7 | permalink: /:title/ 8 | markdown: kramdown 9 | kramdown: 10 | typographic_symbols: 11 | ndash: "--" 12 | mdash: "---" 13 | 14 | # Exclude the README and the bundler files that would normally be 15 | # ignored by default. 16 | exclude: 17 | - README.md 18 | - Gemfile 19 | - Gemfile.lock 20 | - prep-docs.sh 21 | - vendor/ 22 | 23 | # These are copies of the apidoc/html and man/html directories. Run 24 | # prep-docs.sh before jekyll to put it in place. 25 | include: [reference, man] 26 | 27 | remote_theme: just-the-docs/just-the-docs@v0.4.1 28 | plugins: 29 | - jekyll-remote-theme 30 | 31 | color_scheme: coreos 32 | 33 | # Aux links for the upper right navigation 34 | aux_links: 35 | "bootc on GitHub": 36 | - "https://github.com/bootc-dev/bootc" 37 | 38 | footer_content: "Copyright © Red Hat, Inc. and others." 39 | 40 | # Footer last edited timestamp 41 | last_edit_timestamp: true 42 | last_edit_time_format: "%b %e %Y at %I:%M %p" 43 | 44 | # Footer "Edit this page on GitHub" link text 45 | gh_edit_link: true 46 | gh_edit_link_text: "Edit this page on GitHub" 47 | gh_edit_repository: "https://github.com/bootc-dev/bootc" 48 | gh_edit_branch: "main" 49 | gh_edit_source: docs 50 | gh_edit_view_mode: "tree" 51 | 52 | compress_html: 53 | clippings: all 54 | comments: all 55 | endings: all 56 | startings: [] 57 | blanklines: false 58 | profile: false 59 | -------------------------------------------------------------------------------- /hack/Containerfile.packit: -------------------------------------------------------------------------------- 1 | # Build image for system-reinstall-bootc test 2 | 3 | # Use centos-bootc:stream10 as default 4 | FROM quay.io/centos-bootc/centos-bootc:stream10 5 | 6 | WORKDIR /bootc-test 7 | 8 | # Save testing farm run files 9 | COPY ARTIFACTS /var/ARTIFACTS 10 | # Copy bootc repo 11 | COPY test-artifacts /var/share/test-artifacts 12 | 13 | ARG GATING 14 | RUN < 9 | 10 | # DESCRIPTION 11 | 12 | Deploy and transactionally in-place with bootable container images. 13 | 14 | The `bootc` project currently uses ostree-containers as a backend to 15 | support a model of bootable container images. Once installed, whether 16 | directly via `bootc install` (executed as part of a container) or via 17 | another mechanism such as an OS installer tool, further updates can be 18 | pulled and `bootc upgrade`. 19 | 20 | 21 | 22 | 23 | # SUBCOMMANDS 24 | 25 | 26 | | Command | Description | 27 | |---------|-------------| 28 | | **bootc upgrade** | Download and queue an updated container image to apply | 29 | | **bootc switch** | Target a new container image reference to boot | 30 | | **bootc rollback** | Change the bootloader entry ordering; the deployment under `rollback` will be queued for the next boot, and the current will become rollback. If there is a `staged` entry (an unapplied, queued upgrade) then it will be discarded | 31 | | **bootc edit** | Apply full changes to the host specification | 32 | | **bootc status** | Display status | 33 | | **bootc usr-overlay** | Add a transient writable overlayfs on `/usr` | 34 | | **bootc install** | Install the running container to a target | 35 | | **bootc container** | Operations which can be executed as part of a container build | 36 | | **bootc composefs-finalize-staged** | | 37 | 38 | 39 | 40 | # VERSION 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/src/experimental-progress-fd.md: -------------------------------------------------------------------------------- 1 | 2 | # Interactive progress with `--progress-fd` 3 | 4 | This is an experimental feature; tracking issue: 5 | 6 | While the `bootc status` tooling allows a client to discover the state 7 | of the system, during interactive changes such as `bootc upgrade` 8 | or `bootc switch` it is possible to monitor the status of downloads 9 | or other operations at a fine-grained level with `--progress-fd`. 10 | 11 | The format of data output over `--progress-fd` is [JSON Lines](https://jsonlines.org) 12 | which is a series of JSON objects separated by newlines (the intermediate 13 | JSON content is guaranteed not to contain a literal newline). 14 | 15 | You can find the JSON schema describing this version here: 16 | [progress-v0.schema.json](progress-v0.schema.json). 17 | 18 | Deploying a new image with either switch or upgrade consists 19 | of three stages: `pulling`, `importing`, and `staging`. The `pulling` step 20 | downloads the image from the registry, offering per-layer and progress in 21 | each message. The `importing` step imports the image into storage and consists 22 | of a single step. Finally, `staging` runs a variety of staging 23 | tasks. Currently, they are staging the image to disk, pulling bound images, 24 | and removing old images. 25 | 26 | Note that new stages or fields may be added at any time. 27 | 28 | Importing and staging are affected by disk speed and the total image size. Pulling 29 | is affected by network speed and how many layers invalidate between pulls. 30 | Therefore, a large image with a good caching strategy will have longer 31 | importing and staging times, and a small bespoke container image will have 32 | negligible importing and staging times. 33 | -------------------------------------------------------------------------------- /docs/src/man/system-reinstall-bootc.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | system-reinstall-bootc - Reinstall the current system with a bootc image 4 | 5 | # SYNOPSIS 6 | 7 | **system-reinstall-bootc** <*BOOTC_IMAGE*> 8 | 9 | # DESCRIPTION 10 | 11 | **system-reinstall-bootc** is a utility that allows you to reinstall your current system using a bootc container image. This tool provides an interactive way to replace your existing system with a new bootc-based system while preserving SSH access and making the previous root filesystem (including user data) available in `/sysroot`. 12 | 13 | The utility will: 14 | - Pull the specified bootc container image 15 | - Collect SSH keys for root access after reinstall 16 | - Execute a bootc install to replace the current system 17 | - Reboot into the new system 18 | 19 | After reboot, the previous root filesystem will be available in `/sysroot`, and some automatic cleanup of the previous root will be performed. Note that existing mounts will not be automatically mounted by the bootc system unless they are defined in the bootc image. 20 | 21 | This is primarily intended as a way to "take over" cloud virtual machine images, effectively using them as an installer environment. 22 | 23 | # ARGUMENTS 24 | 25 | **BOOTC_IMAGE** 26 | 27 | The bootc container image to install (e.g., quay.io/fedora/fedora-bootc:41) 28 | 29 | This argument is required. 30 | 31 | # EXAMPLES 32 | 33 | Reinstall with a custom bootc image: 34 | ``` 35 | system-reinstall-bootc registry.example.com/my-bootc:latest 36 | ``` 37 | 38 | # ENVIRONMENT 39 | 40 | **BOOTC_REINSTALL_CONFIG** 41 | 42 | This variable is deprecated. 43 | 44 | # SEE ALSO 45 | 46 | **bootc**(8), **bootc-install**(8) 47 | 48 | # VERSION 49 | 50 | 51 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/docgen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Red Hat, Inc. 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 OR MIT 4 | 5 | use anyhow::{Context, Result}; 6 | use camino::Utf8Path; 7 | use clap::{Command, CommandFactory}; 8 | use std::fs::OpenOptions; 9 | use std::io::Write; 10 | 11 | pub fn generate_manpages(directory: &Utf8Path) -> Result<()> { 12 | generate_one(directory, crate::cli::Opt::command()) 13 | } 14 | 15 | fn generate_one(directory: &Utf8Path, cmd: Command) -> Result<()> { 16 | let version = env!("CARGO_PKG_VERSION"); 17 | let name = cmd.get_name(); 18 | let path = directory.join(format!("{name}.8")); 19 | println!("Generating {path}..."); 20 | 21 | let mut out = OpenOptions::new() 22 | .create(true) 23 | .write(true) 24 | .truncate(true) 25 | .open(&path) 26 | .with_context(|| format!("opening {path}")) 27 | .map(std::io::BufWriter::new)?; 28 | clap_mangen::Man::new(cmd.clone()) 29 | .title("ostree-ext") 30 | .section("8") 31 | .source(format!("ostree-ext {version}")) 32 | .render(&mut out) 33 | .with_context(|| format!("rendering {name}.8"))?; 34 | out.flush().context("flushing man page")?; 35 | drop(out); 36 | 37 | for subcmd in cmd.get_subcommands().filter(|c| !c.is_hide_set()) { 38 | let subname = format!("{}-{}", name, subcmd.get_name()); 39 | // SAFETY: Latest clap 4 requires names are &'static - this is 40 | // not long-running production code, so we just leak the names here. 41 | let subname = &*std::boxed::Box::leak(subname.into_boxed_str()); 42 | let subcmd = subcmd.clone().name(subname).alias(subname).version(version); 43 | generate_one(directory, subcmd)?; 44 | } 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /crates/tests-integration/src/hostpriv.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use fn_error_context::context; 3 | use libtest_mimic::Trial; 4 | use xshell::cmd; 5 | 6 | struct TestState { 7 | image: String, 8 | } 9 | 10 | fn new_test( 11 | state: &'static TestState, 12 | description: &'static str, 13 | f: fn(&'static str) -> anyhow::Result<()>, 14 | ) -> libtest_mimic::Trial { 15 | Trial::test(description, move || f(&state.image).map_err(Into::into)) 16 | } 17 | 18 | fn test_loopback_install(image: &'static str) -> Result<()> { 19 | let base_args = super::install::BASE_ARGS; 20 | let sh = &xshell::Shell::new()?; 21 | let size = 10 * 1000 * 1000 * 1000; 22 | let mut tmpdisk = tempfile::NamedTempFile::new_in("/var/tmp")?; 23 | tmpdisk.as_file_mut().set_len(size)?; 24 | let tmpdisk = tmpdisk.into_temp_path(); 25 | let tmpdisk = tmpdisk.to_str().unwrap(); 26 | cmd!( 27 | sh, 28 | "sudo {base_args...} -v {tmpdisk}:/disk {image} bootc install to-disk --via-loopback /disk" 29 | ) 30 | .run()?; 31 | Ok(()) 32 | } 33 | 34 | /// Tests that require real root (e.g. CAP_SYS_ADMIN) to do things like 35 | /// create loopback devices, but are *not* destructive. At the current time 36 | /// these tests are defined to reference a bootc container image. 37 | #[context("Hostpriv tests")] 38 | pub(crate) fn run_hostpriv(image: &str, testargs: libtest_mimic::Arguments) -> Result<()> { 39 | let state = Box::new(TestState { 40 | image: image.to_string(), 41 | }); 42 | // Make this static because the tests require it 43 | let state: &'static TestState = Box::leak(state); 44 | 45 | let tests = [new_test(&state, "loopback install", test_loopback_install)]; 46 | 47 | libtest_mimic::run(&testargs, tests.into()).exit() 48 | } 49 | -------------------------------------------------------------------------------- /docs/src/man/bootc-install-config.5.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-install-config.toml 4 | 5 | # DESCRIPTION 6 | 7 | The `bootc install` process supports some basic customization. This configuration file 8 | is in TOML format, and will be discovered by the installation process in via "drop-in" 9 | files in `/usr/lib/bootc/install` that are processed in alphanumerical order. 10 | 11 | The individual files are merged into a single final installation config, so it is 12 | supported for e.g. a container base image to provide a default root filesystem type, 13 | that can be overridden in a derived container image. 14 | 15 | # install 16 | 17 | This is the only defined toplevel table. 18 | 19 | The `install` section supports two subfields: 20 | 21 | - `block`: An array of supported `to-disk` backends enabled by this base container image; 22 | if not specified, this will just be `direct`. The only other supported value is `tpm2-luks`. 23 | The first value specified will be the default. To enable both, use `block = ["direct", "tpm2-luks"]`. 24 | - `filesystem`: See below. 25 | - `kargs`: An array of strings; this will be appended to the set of kernel arguments. 26 | - `match_architectures`: An array of strings; this filters the install config. 27 | 28 | # filesystem 29 | 30 | There is one valid field: 31 | 32 | - `root`: An instance of "filesystem-root"; see below 33 | 34 | # filesystem-root 35 | 36 | There is one valid field: 37 | 38 | `type`: This can be any basic Linux filesystem with a `mkfs.$fstype`. For example, `ext4`, `xfs`, etc. 39 | 40 | # Examples 41 | 42 | ```toml 43 | [install.filesystem.root] 44 | type = "xfs" 45 | [install] 46 | kargs = ["nosmt", "console=tty0"] 47 | ``` 48 | 49 | # SEE ALSO 50 | 51 | **bootc(1)** 52 | 53 | # VERSION 54 | 55 | 56 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/Containerfile.stage2: -------------------------------------------------------------------------------- 1 | FROM quay.io/fedora/fedora-bootc-base-uki:42 AS base 2 | 3 | FROM base as kernel 4 | 5 | ARG COMPOSEFS_FSVERITY 6 | 7 | RUN --mount=type=secret,id=key \ 8 | --mount=type=secret,id=cert < /etc/kernel/cmdline 13 | 14 | dnf install -y systemd-ukify sbsigntools systemd-boot-unsigned 15 | kver=$(cd /usr/lib/modules && echo *) 16 | ukify build \ 17 | --linux "/usr/lib/modules/$kver/vmlinuz" \ 18 | --initrd "/usr/lib/modules/$kver/initramfs.img" \ 19 | --uname="${kver}" \ 20 | --cmdline "@/etc/kernel/cmdline" \ 21 | --os-release "@/etc/os-release" \ 22 | --signtool sbsign \ 23 | --secureboot-private-key "/run/secrets/key" \ 24 | --secureboot-certificate "/run/secrets/cert" \ 25 | --measure \ 26 | --json pretty \ 27 | --output "/boot/$kver.efi" 28 | sbsign \ 29 | --key "/run/secrets/key" \ 30 | --cert "/run/secrets/cert" \ 31 | "/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \ 32 | --output "/boot/systemd-bootx64.efi" 33 | EOF 34 | 35 | FROM base as final 36 | 37 | RUN --mount=type=bind,from=kernel,target=/_mount/kernel < bool { 10 | static RUNNING_IN_SYSTEMD: OnceLock = OnceLock::new(); 11 | *RUNNING_IN_SYSTEMD.get_or_init(|| { 12 | // See https://www.freedesktop.org/software/systemd/man/systemd.exec.html#%24INVOCATION_ID 13 | std::env::var_os("INVOCATION_ID") 14 | .filter(|s| !s.is_empty()) 15 | .is_some() 16 | }) 17 | } 18 | 19 | /// Return a prepared subprocess configuration that will run as an unprivileged user if possible. 20 | /// 21 | /// This currently only drops privileges when run under systemd with DynamicUser. 22 | pub(crate) fn unprivileged_subprocess(binary: &str, user: &str) -> Command { 23 | // TODO: if we detect we're running in a container as uid 0, perhaps at least switch to the 24 | // "bin" user if we can? 25 | if !running_in_systemd() { 26 | return Command::new(binary); 27 | } 28 | let mut cmd = Command::new("setpriv"); 29 | // Clear some strategic environment variables that may cause the containers/image stack 30 | // to look in the wrong places for things. 31 | cmd.env_remove("HOME"); 32 | cmd.env_remove("XDG_DATA_DIR"); 33 | cmd.env_remove("USER"); 34 | cmd.args([ 35 | "--no-new-privs", 36 | "--init-groups", 37 | "--reuid", 38 | user, 39 | "--bounding-set", 40 | "-all", 41 | "--pdeathsig", 42 | "TERM", 43 | "--", 44 | binary, 45 | ]); 46 | cmd 47 | } 48 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/030-test-composefs.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "composefs integration smoke test" 5 | 6 | def parse_cmdline [] { 7 | open /proc/cmdline | str trim | split row " " 8 | } 9 | 10 | # Detect composefs by checking if composefs field is present 11 | let st = bootc status --json | from json 12 | let is_composefs = ($st.status.booted.composefs? != null) 13 | let expecting_composefs = ($env.BOOTC_variant? | default "" | find "composefs") != null 14 | if $expecting_composefs { 15 | assert $is_composefs 16 | # When using systemd-boot with DPS (Discoverable Partition Specification), 17 | # /proc/cmdline should NOT contain a root= parameter because systemd-gpt-auto-generator 18 | # discovers the root partition automatically 19 | # Note that there is `bootctl --json=pretty` but it doesn't actually output JSON 20 | let bootctl_output = (bootctl) 21 | if ($bootctl_output | str contains 'Product: systemd-boot') { 22 | let cmdline = parse_cmdline 23 | let has_root_param = ($cmdline | any { |param| $param | str starts-with 'root=' }) 24 | assert (not $has_root_param) "systemd-boot image should not have root= in kernel cmdline; systemd-gpt-auto-generator should discover the root partition via DPS" 25 | } 26 | } 27 | 28 | if $is_composefs { 29 | # When already on composefs, we can only test read-only operations 30 | print "# TODO composefs: skipping pull test - cfs oci pull requires write access to sysroot" 31 | bootc internals cfs --help 32 | } else { 33 | # When not on composefs, run the full test including initialization 34 | bootc internals test-composefs 35 | bootc internals cfs --help 36 | bootc internals cfs oci pull docker://busybox busybox 37 | test -L /sysroot/composefs/streams/refs/busybox 38 | } 39 | 40 | tap ok 41 | -------------------------------------------------------------------------------- /tmt/tests/examples/bootc-uki/build.final: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | cd "${0%/*}" 6 | 7 | cp /usr/bin/bootc . 8 | 9 | rm -rf tmp/sysroot 10 | mkdir -p tmp/sysroot/composefs 11 | 12 | # TODO port this over to container compute-composefs-digest 13 | IMAGE_ID="$(sed s/sha256:// tmp/iid)" 14 | ./bootc internals cfs --repo tmp/sysroot/composefs --insecure oci pull containers-storage:"${IMAGE_ID}" 15 | COMPOSEFS_FSVERITY="$(./bootc internals cfs --repo tmp/sysroot/composefs --insecure oci compute-id --bootable "${IMAGE_ID}")" 16 | 17 | # See: https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot 18 | # Alternative to generate keys for testing: `sbctl create-keys` 19 | if [[ ! -d "secureboot" ]]; then 20 | echo "Generating test Secure Boot keys" 21 | mkdir secureboot 22 | pushd secureboot > /dev/null 23 | systemd-id128 new -u > GUID.txt 24 | openssl req -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj "/CN=Test Platform Key/" -out PK.crt 25 | openssl x509 -outform DER -in PK.crt -out PK.cer 26 | openssl req -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj "/CN=Test Key Exchange Key/" -out KEK.crt 27 | openssl x509 -outform DER -in KEK.crt -out KEK.cer 28 | openssl req -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj "/CN=Test Signature Database key/" -out db.crt 29 | openssl x509 -outform DER -in db.crt -out db.cer 30 | popd > /dev/null 31 | fi 32 | 33 | # For debugging, add --no-cache to podman command 34 | sudo podman build \ 35 | -t quay.io/fedora/fedora-bootc-uki:42 \ 36 | --build-arg=COMPOSEFS_FSVERITY="${COMPOSEFS_FSVERITY}" \ 37 | -f Containerfile.stage2 \ 38 | --secret=id=key,src=secureboot/db.key \ 39 | --secret=id=cert,src=secureboot/db.crt \ 40 | --iidfile=tmp/iid2 41 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-install-outside-container.nu: -------------------------------------------------------------------------------- 1 | # number: 23 2 | # tmt: 3 | # summary: Execute tests for installing outside of a container 4 | # duration: 30m 5 | # 6 | use std assert 7 | use tap.nu 8 | 9 | # In this test we install a generic image mainly because it keeps 10 | # this test in theory independent of starting from a bootc host, 11 | # but also because it's useful to test "skew" between the bootc binary 12 | # doing the install and the target image. 13 | let target_image = "docker://quay.io/centos-bootc/centos-bootc:stream10" 14 | 15 | # setup filesystem 16 | mkdir /var/mnt 17 | truncate -s 10G disk.img 18 | mkfs.ext4 disk.img 19 | mount -o loop disk.img /var/mnt 20 | 21 | # attempt to install to filesystem without specifying a source-imgref 22 | let result = bootc install to-filesystem /var/mnt e>| find "--source-imgref must be defined" 23 | assert not equal $result null 24 | umount /var/mnt 25 | 26 | # Mask off the bootupd state to reproduce https://github.com/bootc-dev/bootc/issues/1778 27 | # Also it turns out that installation outside of containers dies due to `error: Multiple commit objects found` 28 | # so we mask off /sysroot/ostree 29 | # And using systemd-run here breaks our install_t so we disable SELinux enforcement 30 | setenforce 0 31 | systemd-run -p MountFlags=slave -qdPG -- /bin/sh -c $" 32 | set -xeuo pipefail 33 | if test -d /sysroot/ostree; then mount --bind /usr/share/empty /sysroot/ostree; fi 34 | mkdir -p /tmp/ovl/{upper,work} 35 | mount -t overlay -olowerdir=/usr,workdir=/tmp/ovl/work,upperdir=/tmp/ovl/upper overlay /usr 36 | # Note we do keep the other bootupd state 37 | rm -vrf /usr/lib/bootupd/updates 38 | # Another bootc install bug, we should not look at this in outside-of-container flows 39 | rm -vrf /usr/lib/bootc/bound-images.d 40 | bootc install to-disk --disable-selinux --via-loopback --filesystem xfs --source-imgref ($target_image) ./disk.img 41 | " 42 | 43 | tap ok 44 | -------------------------------------------------------------------------------- /docs/src/upgrades.md: -------------------------------------------------------------------------------- 1 | # Managing upgrades 2 | 3 | Right now, bootc is a quite simple tool that is designed to do just 4 | a few things well. One of those is transactionally fetching new operating system 5 | updates from a registry and booting into them, while supporting rollback. 6 | 7 | ## The `bootc upgrade` verb 8 | 9 | This will query the registry and queue an updated container image for the next boot. 10 | 11 | This is backed today by ostree, implementing an A/B style upgrade system. 12 | Changes to the base image are staged, and the running system is not 13 | changed by default. 14 | 15 | Use `bootc upgrade --apply` to auto-apply if there are queued changes. 16 | 17 | There is also an opinionated `bootc-fetch-apply-updates.timer` and corresponding 18 | service available in upstream for operating systems and distributions 19 | to enable. 20 | 21 | Man page: [bootc-upgrade](man/bootc-upgrade.8.md). 22 | 23 | ## Changing the container image source 24 | 25 | Another useful pattern to implement can be to use a management agent 26 | to invoke `bootc switch` (or declaratively via `bootc edit`) 27 | to implement e.g. blue/green deployments, 28 | where some hosts are rolled onto a new image independently of others. 29 | 30 | ```shell 31 | bootc switch quay.io/examplecorp/os-prod-blue:latest 32 | ``` 33 | 34 | `bootc switch` has the same effect as `bootc upgrade`; there is no 35 | semantic difference between the two other than changing the 36 | container image being tracked. 37 | 38 | This will preserve existing state in `/etc` and `/var` - for example, 39 | host SSH keys and home directories. 40 | 41 | Man page: [bootc-switch](man/bootc-switch.8.md). 42 | 43 | ## Rollback 44 | 45 | There is a `bootc rollback` verb, and associated declarative interface 46 | accessible to tools via `bootc edit`. This will swap the bootloader 47 | ordering to the previous boot entry. 48 | 49 | Man page: [bootc-rollback](man/bootc-rollback.8.md). 50 | 51 | 52 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | The current Maintainers Group for the bootc Project consists of: 4 | 5 | | Name | GitHub ID | Employer | Responsibilities | 6 | | ---- | ---- | ---- | ---- | 7 | | Chris Kyrouac | [ckyrouac](https://github.com/orgs/bootc-dev/people/ckyrouac) | Red Hat | Approver | 8 | | Colin Walters | [cgwalters](https://github.com/orgs/bootc-dev/people/cgwalters) | Red Hat | Approver | 9 | | John Eckersberg | [jeckersb](https://github.com/orgs/bootc-dev/people/jeckersb) | Red Hat | Approver | 10 | | Xiaofeng Wang | [henrywang](https://github.com/orgs/bootc-dev/people/henrywang) | Red Hat | Approver | 11 | | Gursewak Mangat | [gursewak1997](https://github.com/orgs/bootc-dev/people/gursewak1997)| Red Hat | Approver | 12 | | Joseph Marrero | [jmarrero](https://github.com/orgs/bootc-dev/people/jmarrero) | Red Hat | Approver | 13 | 14 | # Community Managers 15 | 16 | This group can represent the project for administrative and program manager duties. Examples: CNCF Service Desk tickets, coordinating with CNCF Project and Events teams, and LFX Administration. No code or code review rights. 17 | 18 | | Name | GitHub ID | Employer | Responsibilities | 19 | | ---- | ---- | ---- | ---- | 20 | | Laura Santamaria | [nimbinatus](https://github.com/nimbinatus) | Red Hat | Representative | 21 | | Mohan Shash | [mohan-shash](https://github.com/mohan-shash) | Red Hat | Representative | 22 | -------------------------------------------------------------------------------- /crates/lib/src/composefs_consts.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | /// composefs= paramter in kernel cmdline 4 | pub const COMPOSEFS_CMDLINE: &str = "composefs"; 5 | 6 | /// Directory to store transient state, such as staged deployemnts etc 7 | pub(crate) const COMPOSEFS_TRANSIENT_STATE_DIR: &str = "/run/composefs"; 8 | /// File created in /run/composefs to record a staged-deployment 9 | pub(crate) const COMPOSEFS_STAGED_DEPLOYMENT_FNAME: &str = "staged-deployment"; 10 | 11 | /// Absolute path to composefs-backend state directory 12 | pub(crate) const STATE_DIR_ABS: &str = "/sysroot/state/deploy"; 13 | /// Relative path to composefs-backend state directory. Relative to /sysroot 14 | pub(crate) const STATE_DIR_RELATIVE: &str = "state/deploy"; 15 | /// Relative path to the shared 'var' directory. Relative to /sysroot 16 | pub(crate) const SHARED_VAR_PATH: &str = "state/os/default/var"; 17 | 18 | /// Section in .origin file to store boot related metadata 19 | pub(crate) const ORIGIN_KEY_BOOT: &str = "boot"; 20 | /// Whether the deployment was booted with BLS or UKI 21 | pub(crate) const ORIGIN_KEY_BOOT_TYPE: &str = "boot_type"; 22 | /// Key to store the SHA256 sum of vmlinuz + initrd for a deployment 23 | pub(crate) const ORIGIN_KEY_BOOT_DIGEST: &str = "digest"; 24 | 25 | /// Filename for `loader/entries` 26 | pub(crate) const BOOT_LOADER_ENTRIES: &str = "entries"; 27 | /// Filename for staged boot loader entries 28 | pub(crate) const STAGED_BOOT_LOADER_ENTRIES: &str = "entries.staged"; 29 | 30 | /// Filename for grub user config 31 | pub(crate) const USER_CFG: &str = "user.cfg"; 32 | /// Filename for staged grub user config 33 | pub(crate) const USER_CFG_STAGED: &str = "user.cfg.staged"; 34 | 35 | /// Path to the config files directory for Type1 boot entries 36 | /// This is relative to the boot/efi directory 37 | pub(crate) const TYPE1_ENT_PATH: &str = "loader/entries"; 38 | pub(crate) const TYPE1_ENT_PATH_STAGED: &str = "loader/entries.staged"; 39 | 40 | pub(crate) const BOOTC_FINALIZE_STAGED_SERVICE: &str = "bootc-finalize-staged.service"; 41 | -------------------------------------------------------------------------------- /systemd/bootc-finalize-staged.service: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 Red Hat, Inc. 2 | # 3 | # This library is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU Lesser General Public 5 | # License as published by the Free Software Foundation; either 6 | # version 2 of the License, or (at your option) any later version. 7 | # 8 | # This library is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public 14 | # License along with this library. If not, see . 15 | 16 | # For some implementation discussion, see: 17 | # https://lists.freedesktop.org/archives/systemd-devel/2018-March/040557.html 18 | [Unit] 19 | Description=Composefs Finalize Staged Deployment 20 | Documentation=man:bootc(1) 21 | DefaultDependencies=no 22 | 23 | RequiresMountsFor=/sysroot 24 | After=local-fs.target 25 | Before=basic.target final.target 26 | # We want to make sure the transaction logs are persisted to disk: 27 | # https://bugzilla.redhat.com/show_bug.cgi?id=1751272 28 | After=systemd-journal-flush.service 29 | Conflicts=final.target 30 | 31 | [Service] 32 | Type=oneshot 33 | RemainAfterExit=yes 34 | ExecStop=/usr/bin/bootc composefs-finalize-staged 35 | # This is a quite long timeout intentionally; the failure mode 36 | # here is that people don't get an upgrade. We need to handle 37 | # cases with slow rotational media, etc. 38 | TimeoutStopSec=5m 39 | # Bootc should never touch /var at all...except, we need to remove 40 | # the /var/.updated flag, so we can't just `InaccessiblePaths=/var` right now. 41 | # For now, let's at least use ProtectHome just so we have some sandboxing 42 | # of that. 43 | ProtectHome=yes 44 | # And we shouldn't affect the current deployment's /etc. 45 | ReadOnlyPaths=/etc 46 | # We write to /sysroot and /boot of course. 47 | -------------------------------------------------------------------------------- /tmt/tests/booted/readonly/001-test-status.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "verify bootc status output formats" 5 | 6 | # Verify /sysroot is not writable initially (read-only operations should not make it writable) 7 | let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0 8 | assert (not $is_writable) "/sysroot should not be writable initially" 9 | 10 | # Double-check with findmnt 11 | let mnt = (findmnt /sysroot -J | from json) 12 | let opts = ($mnt.filesystems.0.options | split row ",") 13 | assert ($opts | any { |o| $o == "ro" }) "/sysroot should be mounted read-only" 14 | 15 | let st = bootc status --json | from json 16 | # Detect composefs by checking if composefs field is present 17 | let is_composefs = ($st.status.booted.composefs? != null) 18 | 19 | assert equal $st.apiVersion org.containers.bootc/v1 20 | 21 | let st = bootc status --json --format-version=0 | from json 22 | assert equal $st.apiVersion org.containers.bootc/v1 23 | 24 | let st = bootc status --format=yaml | from yaml 25 | assert equal $st.apiVersion org.containers.bootc/v1 26 | if not $is_composefs { 27 | assert ($st.status.booted.image.timestamp != null) 28 | } # else { TODO composefs: timestamp is not populated with composefs } 29 | let ostree = $st.status.booted.ostree 30 | if $ostree != null { 31 | assert ($ostree.stateroot != null) 32 | } 33 | 34 | let st = bootc status --json --booted | from json 35 | assert equal $st.apiVersion org.containers.bootc/v1 36 | if not $is_composefs { 37 | assert ($st.status.booted.image.timestamp != null) 38 | } # else { TODO composefs: timestamp is not populated with composefs } 39 | assert (($st.status | get rollback | default null) == null) 40 | assert (($st.status | get staged | default null) == null) 41 | 42 | # Verify /sysroot is still not writable after bootc status (regression test for PR #1718) 43 | let is_writable = (do -i { /bin/test -w /sysroot } | complete | get exit_code) == 0 44 | assert (not $is_writable) "/sysroot should remain read-only after bootc status" 45 | 46 | tap ok 47 | -------------------------------------------------------------------------------- /crates/lib/src/podman.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use camino::Utf8Path; 3 | use cap_std_ext::cap_std::fs::Dir; 4 | use serde::Deserialize; 5 | 6 | /// Where we look inside our container to find our own image 7 | /// for use with `bootc install`. 8 | pub(crate) const CONTAINER_STORAGE: &str = "/var/lib/containers"; 9 | 10 | #[derive(Deserialize)] 11 | #[serde(rename_all = "PascalCase")] 12 | pub(crate) struct Inspect { 13 | pub(crate) digest: String, 14 | } 15 | 16 | /// This is output from `podman image list --format=json`. 17 | #[derive(Deserialize)] 18 | #[serde(rename_all = "PascalCase")] 19 | pub(crate) struct ImageListEntry { 20 | pub(crate) id: String, 21 | pub(crate) names: Option>, 22 | } 23 | 24 | /// Given an image ID, return its manifest digest 25 | pub(crate) fn imageid_to_digest(imgid: &str) -> Result { 26 | use bootc_utils::CommandRunExt; 27 | let o: Vec = crate::install::run_in_host_mountns("podman")? 28 | .args(["inspect", imgid]) 29 | .run_and_parse_json()?; 30 | let i = o 31 | .into_iter() 32 | .next() 33 | .ok_or_else(|| anyhow::anyhow!("No images returned for inspect"))?; 34 | Ok(i.digest) 35 | } 36 | 37 | /// Return true if there is apparently an active container store at the target path. 38 | pub(crate) fn storage_exists(root: &Dir, path: impl AsRef) -> Result { 39 | fn impl_storage_exists(root: &Dir, path: &Utf8Path) -> Result { 40 | let lock = "storage.lock"; 41 | root.try_exists(path.join(lock)).map_err(Into::into) 42 | } 43 | impl_storage_exists(root, path.as_ref()) 44 | } 45 | 46 | /// Return true if there is apparently an active container store in the default path 47 | /// for the target root. 48 | /// 49 | /// Note this does not attempt to parse the root filesystem's container storage configuration, 50 | /// this uses a hardcoded default path. 51 | pub(crate) fn storage_exists_default(root: &Dir) -> Result { 52 | storage_exists(root, CONTAINER_STORAGE.trim_start_matches('/')) 53 | } 54 | -------------------------------------------------------------------------------- /crates/lib/src/containerenv.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for parsing the `/run/.containerenv` file generated by podman. 2 | 3 | use std::io::{BufRead, BufReader}; 4 | 5 | use anyhow::Result; 6 | use cap_std_ext::cap_std::fs::Dir; 7 | use cap_std_ext::prelude::CapStdExtDirExt; 8 | use fn_error_context::context; 9 | 10 | /// Path is relative to container rootfs (assumed to be /) 11 | pub(crate) const PATH: &str = "run/.containerenv"; 12 | 13 | #[derive(Debug, Default)] 14 | pub(crate) struct ContainerExecutionInfo { 15 | pub(crate) engine: String, 16 | pub(crate) name: String, 17 | pub(crate) id: String, 18 | pub(crate) image: String, 19 | pub(crate) imageid: String, 20 | pub(crate) rootless: Option, 21 | } 22 | 23 | pub(crate) fn is_container(rootfs: &Dir) -> bool { 24 | rootfs.exists(PATH) 25 | } 26 | 27 | /// Load and parse the `/run/.containerenv` file. 28 | #[context("Querying container")] 29 | pub(crate) fn get_container_execution_info(rootfs: &Dir) -> Result { 30 | let f = match rootfs.open_optional(PATH)? { 31 | Some(f) => BufReader::new(f), 32 | None => { 33 | anyhow::bail!( 34 | "This command must be executed inside a podman container (missing /{PATH})" 35 | ) 36 | } 37 | }; 38 | let mut r = ContainerExecutionInfo::default(); 39 | for line in f.lines() { 40 | let line = line?; 41 | let line = line.trim(); 42 | let Some((k, v)) = line.split_once('=') else { 43 | continue; 44 | }; 45 | // Assuming there's no quotes here 46 | let v = v.trim_start_matches('"').trim_end_matches('"'); 47 | match k { 48 | "engine" => r.engine = v.to_string(), 49 | "name" => r.name = v.to_string(), 50 | "id" => r.id = v.to_string(), 51 | "image" => r.image = v.to_string(), 52 | "imageid" => r.imageid = v.to_string(), 53 | "rootless" => r.rootless = Some(v.to_string()), 54 | _ => {} 55 | } 56 | } 57 | Ok(r) 58 | } 59 | -------------------------------------------------------------------------------- /docs/src/man/bootc-install.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-install - Install the running container to a target 4 | 5 | # SYNOPSIS 6 | 7 | **bootc install** \[*OPTIONS...*\] <*SUBCOMMAND*> 8 | 9 | # DESCRIPTION 10 | 11 | Install the running container to a target. 12 | 13 | ## Understanding installations 14 | 15 | OCI containers are effectively layers of tarballs with JSON for 16 | metadata; they cannot be booted directly. The `bootc install` flow is 17 | a highly opinionated method to take the contents of the container image 18 | and install it to a target block device (or an existing filesystem) in 19 | such a way that it can be booted. 20 | 21 | For example, a Linux partition table and filesystem is used, and the 22 | bootloader and kernel embedded in the container image are also prepared. 23 | 24 | A bootc installed container currently uses OSTree as a backend, and this 25 | sets it up such that a subsequent `bootc upgrade` can perform in-place 26 | updates. 27 | 28 | An installation is not simply a copy of the container filesystem, but 29 | includes other setup and metadata. 30 | 31 | 32 | 33 | 34 | # SUBCOMMANDS 35 | 36 | 37 | | Command | Description | 38 | |---------|-------------| 39 | | **bootc install to-disk** | Install to the target block device | 40 | | **bootc install to-filesystem** | Install to an externally created filesystem structure | 41 | | **bootc install to-existing-root** | Install to the host root filesystem | 42 | | **bootc install finalize** | Execute this as the penultimate step of an installation using `install to-filesystem` | 43 | | **bootc install ensure-completion** | Intended for use in environments that are performing an ostree-based installation, not bootc | 44 | | **bootc install print-configuration** | Output JSON to stdout that contains the merged installation configuration as it may be relevant to calling processes using `install to-filesystem` that in particular want to discover the desired root filesystem type from the container image | 45 | 46 | 47 | 48 | # VERSION 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/src/man/bootc-rollback.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-rollback - Change the bootloader entry ordering 4 | 5 | # SYNOPSIS 6 | 7 | **bootc rollback** \[*OPTIONS...*\] 8 | 9 | # DESCRIPTION 10 | 11 | Change the bootloader entry ordering; the deployment under `rollback` will be queued for the next boot, 12 | and the current will become rollback. If there is a `staged` entry (an unapplied, queued upgrade) 13 | then it will be discarded. 14 | 15 | Note that absent any additional control logic, if there is an active agent doing automated upgrades 16 | (such as the default `bootc-fetch-apply-updates.timer` and associated `.service`) the 17 | change here may be reverted. It's recommended to only use this in concert with an agent that 18 | is in active control. 19 | 20 | A systemd journal message will be logged with `MESSAGE_ID=26f3b1eb24464d12aa5e7b544a6b5468` in 21 | order to detect a rollback invocation. 22 | 23 | ## Note on Rollbacks and the `/etc` Directory 24 | 25 | When you perform a rollback (e.g., with `bootc rollback`), any 26 | changes made to files in the `/etc` directory won't carry over 27 | to the rolled-back deployment. The `/etc` files will revert 28 | to their state from that previous deployment instead. 29 | 30 | This is because `bootc rollback` just reorders the existing 31 | deployments. It doesn't create new deployments. The `/etc` 32 | merges happen when new deployments are created. 33 | 34 | # OPTIONS 35 | 36 | 37 | **--apply** 38 | 39 | Restart or reboot into the rollback image 40 | 41 | **--soft-reboot**=*SOFT_REBOOT* 42 | 43 | Configure soft reboot behavior 44 | 45 | Possible values: 46 | - required 47 | - auto 48 | 49 | 50 | 51 | # EXAMPLES 52 | 53 | Rollback to the previous deployment: 54 | 55 | bootc rollback 56 | 57 | Rollback and immediately apply the changes: 58 | 59 | bootc rollback --apply 60 | 61 | Rollback with soft reboot if possible: 62 | 63 | bootc rollback --apply --soft-reboot=auto 64 | 65 | # SEE ALSO 66 | 67 | **bootc**(8), **bootc-upgrade**(8), **bootc-switch**(8), **bootc-status**(8) 68 | 69 | # VERSION 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/src/man/bootc-root-setup.service.5.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-root-setup.service 4 | 5 | # DESCRIPTION 6 | 7 | This service runs in the initramfs to set up the root filesystem when composefs is enabled. 8 | It is only activated when the `composefs` kernel command line parameter is present. 9 | 10 | The service performs the following operations: 11 | 12 | - Mounts the composefs image specified in the kernel command line 13 | - Sets up `/etc` and `/var` directories from the deployment state 14 | - Optionally configures transient overlays based on the configuration file 15 | - Prepares the root filesystem for switch-root 16 | 17 | This service runs after `sysroot.mount` and `ostree-prepare-root.service`, and before 18 | `initrd-root-fs.target`. 19 | 20 | # CONFIGURATION FILE 21 | 22 | The service reads an optional configuration file at `/usr/lib/composefs/setup-root-conf.toml`. 23 | If this file does not exist, default settings are used. 24 | 25 | **WARNING**: The configuration file format and composefs integration are experimental 26 | and subject to change. 27 | 28 | ## Configuration Options 29 | 30 | The configuration file uses TOML format with the following sections: 31 | 32 | ### `[root]` 33 | 34 | - `transient` (boolean): If true, mounts the root filesystem as a transient overlay. 35 | This makes all changes to `/` ephemeral and lost on reboot. Default: false. 36 | 37 | ### `[etc]` 38 | 39 | - `mount` (string): Mount type for `/etc`. Options: "none", "bind", "overlay", "transient". 40 | Default: "bind". 41 | - `transient` (boolean): Shorthand for `mount = "transient"`. Default: false. 42 | 43 | ### `[var]` 44 | 45 | - `mount` (string): Mount type for `/var`. Options: "none", "bind", "overlay", "transient". 46 | Default: "bind". 47 | - `transient` (boolean): Shorthand for `mount = "transient"`. Default: false. 48 | 49 | ## Example Configuration 50 | 51 | ```toml 52 | [root] 53 | transient = false 54 | 55 | [etc] 56 | mount = "bind" 57 | 58 | [var] 59 | mount = "overlay" 60 | ``` 61 | 62 | # EXPERIMENTAL STATUS 63 | 64 | The composefs integration, including this service and its configuration file format, 65 | is experimental and subject to change. 66 | 67 | # SEE ALSO 68 | 69 | **bootc(8)** 70 | 71 | # VERSION 72 | 73 | 74 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-image-upgrade-reboot.nu: -------------------------------------------------------------------------------- 1 | # number: 24 2 | # tmt: 3 | # summary: Execute local upgrade tests 4 | # duration: 30m 5 | # 6 | # This test does: 7 | # bootc image copy-to-storage 8 | # podman build 9 | # bootc switch --apply 10 | # Verify we boot into the new image 11 | # 12 | use std assert 13 | use tap.nu 14 | 15 | # This code runs on *each* boot. 16 | # Here we just capture information. 17 | bootc status 18 | journalctl --list-boots 19 | 20 | let st = bootc status --json | from json 21 | let booted = $st.status.booted.image 22 | 23 | # Parse the kernel commandline into a list. 24 | # This is not a proper parser, but good enough 25 | # for what we need here. 26 | def parse_cmdline [] { 27 | open /proc/cmdline | str trim | split row " " 28 | } 29 | 30 | def imgsrc [] { 31 | $env.BOOTC_upgrade_image? | default "localhost/bootc-derived-local" 32 | } 33 | 34 | # Run on the first boot 35 | def initial_build [] { 36 | tap begin "local image push + pull + upgrade" 37 | 38 | let imgsrc = imgsrc 39 | # For the packit case, we build locally right now 40 | if ($imgsrc | str ends-with "-local") { 41 | bootc image copy-to-storage 42 | 43 | # A simple derived container that adds a file 44 | "FROM localhost/bootc 45 | RUN touch /usr/share/testing-bootc-upgrade-apply 46 | " | save Dockerfile 47 | # Build it 48 | podman build -t $imgsrc . 49 | } 50 | 51 | # Now, switch into the new image 52 | print $"Applying ($imgsrc)" 53 | bootc switch --transport containers-storage ($imgsrc) 54 | tmt-reboot 55 | } 56 | 57 | # Check we have the updated image 58 | def second_boot [] { 59 | print "verifying second boot" 60 | assert equal $booted.image.transport containers-storage 61 | assert equal $booted.image.image $"(imgsrc)" 62 | 63 | # Verify the new file exists 64 | "/usr/share/testing-bootc-upgrade-apply" | path exists 65 | 66 | tap ok 67 | } 68 | 69 | def main [] { 70 | # See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test 71 | match $env.TMT_REBOOT_COUNT? { 72 | null | "0" => initial_build, 73 | "1" => second_boot, 74 | $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/keyfileext.rs: -------------------------------------------------------------------------------- 1 | //! Helper methods for [`glib::KeyFile`]. 2 | 3 | use glib::GString; 4 | use ostree::glib; 5 | 6 | /// Helper methods for [`glib::KeyFile`]. 7 | pub trait KeyFileExt { 8 | /// Get a string value, but return `None` if the key does not exist. 9 | fn optional_string(&self, group: &str, key: &str) -> Result, glib::Error>; 10 | /// Get a boolean value, but return `None` if the key does not exist. 11 | fn optional_bool(&self, group: &str, key: &str) -> Result, glib::Error>; 12 | } 13 | 14 | /// Consume a keyfile error, mapping the case where group or key is not found to `Ok(None)`. 15 | pub fn map_keyfile_optional(res: Result) -> Result, glib::Error> { 16 | match res { 17 | Ok(v) => Ok(Some(v)), 18 | Err(e) => { 19 | if let Some(t) = e.kind::() { 20 | match t { 21 | glib::KeyFileError::GroupNotFound | glib::KeyFileError::KeyNotFound => Ok(None), 22 | _ => Err(e), 23 | } 24 | } else { 25 | Err(e) 26 | } 27 | } 28 | } 29 | } 30 | 31 | impl KeyFileExt for glib::KeyFile { 32 | fn optional_string(&self, group: &str, key: &str) -> Result, glib::Error> { 33 | map_keyfile_optional(self.string(group, key)) 34 | } 35 | 36 | fn optional_bool(&self, group: &str, key: &str) -> Result, glib::Error> { 37 | map_keyfile_optional(self.boolean(group, key)) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_optional() { 47 | let kf = glib::KeyFile::new(); 48 | assert_eq!(kf.optional_string("foo", "bar").unwrap(), None); 49 | kf.set_string("foo", "baz", "someval"); 50 | assert_eq!(kf.optional_string("foo", "bar").unwrap(), None); 51 | assert_eq!( 52 | kf.optional_string("foo", "baz").unwrap().unwrap(), 53 | "someval" 54 | ); 55 | 56 | assert!(kf.optional_bool("foo", "baz").is_err()); 57 | assert_eq!(kf.optional_bool("foo", "bar").unwrap(), None); 58 | kf.set_boolean("foo", "somebool", false); 59 | assert_eq!(kf.optional_bool("foo", "somebool").unwrap(), Some(false)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/build-sealed: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | # This should turn into https://github.com/bootc-dev/bootc/issues/1498 4 | 5 | variant=$1 6 | shift 7 | # The un-sealed container image we want to use 8 | input_image=$1 9 | shift 10 | # The output container image 11 | output_image=$1 12 | shift 13 | # Optional directory with secure boot keys; if none are provided, then we'll 14 | # generate some under target/ 15 | secureboot=${1:-} 16 | 17 | runv() { 18 | set -x 19 | "$@" 20 | } 21 | 22 | case $variant in 23 | ostree) 24 | # Nothing to do 25 | echo "Not building a sealed image; forwarding tag" 26 | runv podman tag $input_image $output_image 27 | exit 0 28 | ;; 29 | composefs-sealeduki*) 30 | ;; 31 | *) 32 | echo "Unknown variant=$variant" 1>&2; exit 1 33 | ;; 34 | esac 35 | 36 | 37 | graphroot=$(podman system info -f '{{.Store.GraphRoot}}') 38 | echo "Computing composefs digest..." 39 | cfs_digest=$(podman run --rm --privileged --read-only --security-opt=label=disable -v /sys:/sys:ro --net=none \ 40 | -v ${graphroot}:/run/host-container-storage:ro --tmpfs /var "$input_image" bootc container compute-composefs-digest) 41 | 42 | if test -z "${secureboot}"; then 43 | secureboot=$(pwd)/target/test-secureboot 44 | mkdir -p ${secureboot} 45 | cd $secureboot 46 | if test '!' -f db.cer; then 47 | echo "Generating test Secure Boot keys" 48 | systemd-id128 new -u > GUID.txt 49 | openssl req -quiet -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Platform Key/' -out PK.crt 50 | openssl x509 -outform DER -in PK.crt -out PK.cer 51 | openssl req -quiet -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Key Exchange Key/' -out KEK.crt 52 | openssl x509 -outform DER -in KEK.crt -out KEK.cer 53 | openssl req -quiet -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Signature Database key/' -out db.crt 54 | openssl x509 -outform DER -in db.crt -out db.cer 55 | else 56 | echo "Reusing Secure Boot keys in ${secureboot}" 57 | fi 58 | cd - 59 | fi 60 | 61 | runv podman build -t $output_image --build-arg=COMPOSEFS_FSVERITY=${cfs_digest} --build-arg=base=${input_image} \ 62 | --secret=id=key,src=${secureboot}/db.key \ 63 | --secret=id=cert,src=${secureboot}/db.crt -f Dockerfile.cfsuki . 64 | -------------------------------------------------------------------------------- /tmt/tests/tests.fmf: -------------------------------------------------------------------------------- 1 | # THIS IS GENERATED CODE - DO NOT EDIT 2 | # Generated by: cargo xtask tmt 3 | 4 | /test-01-readonly: 5 | summary: Execute booted readonly/nondestructive tests 6 | duration: 30m 7 | test: nu booted/test-01-readonly.nu 8 | 9 | /test-20-image-pushpull-upgrade: 10 | summary: Execute local upgrade tests 11 | duration: 30m 12 | test: nu booted/test-image-pushpull-upgrade.nu 13 | 14 | /test-21-logically-bound-switch: 15 | summary: Execute logically bound images tests for switching images 16 | duration: 30m 17 | test: nu booted/test-logically-bound-switch.nu 18 | 19 | /test-22-logically-bound-install: 20 | summary: Execute logically bound images tests for installing image 21 | duration: 30m 22 | test: nu booted/test-logically-bound-install.nu 23 | 24 | /test-23-install-outside-container: 25 | summary: Execute tests for installing outside of a container 26 | duration: 30m 27 | test: nu booted/test-install-outside-container.nu 28 | 29 | /test-23-usroverlay: 30 | summary: Execute tests for bootc usrover 31 | duration: 30m 32 | test: nu booted/test-usroverlay.nu 33 | 34 | /test-24-image-upgrade-reboot: 35 | summary: Execute local upgrade tests 36 | duration: 30m 37 | test: nu booted/test-image-upgrade-reboot.nu 38 | 39 | /test-25-soft-reboot: 40 | summary: Execute soft reboot test 41 | duration: 30m 42 | test: nu booted/test-soft-reboot.nu 43 | 44 | /test-26-examples-build: 45 | summary: Test bootc examples build scripts 46 | duration: 45m 47 | adjust: 48 | - when: running_env != image_mode 49 | enabled: false 50 | because: packit tests use RPM bootc and does not install /usr/lib/bootc/initramfs-setup 51 | test: bash booted/test-26-examples-build.sh 52 | 53 | /test-27-custom-selinux-policy: 54 | summary: Execute custom selinux policy test 55 | duration: 30m 56 | adjust: 57 | - when: running_env != image_mode 58 | enabled: false 59 | because: these tests require features only available in image mode 60 | test: nu booted/test-custom-selinux-policy.nu 61 | 62 | /test-28-factory-reset: 63 | summary: Execute factory reset tests 64 | duration: 30m 65 | test: nu booted/test-factory-reset.nu 66 | 67 | /test-29-soft-reboot-selinux-policy: 68 | summary: Test soft reboot with SELinux policy changes 69 | duration: 30m 70 | test: nu booted/test-soft-reboot-selinux-policy.nu 71 | -------------------------------------------------------------------------------- /ADOPTERS.md: -------------------------------------------------------------------------------- 1 | 2 | > **Note** 3 | > Do you want to add yourself to this list? Simply fork the repository and open a PR with the required change. 4 | > We have a short description of the adopter types at the bottom of this page. Each type is in alphabetical order. 5 | 6 | # bootc Adopters (direct) 7 | 8 | | Type | Name | Since | Website | Use-Case | 9 | |:-|:-|:-|:-|:-| 10 | Vendor | Red Hat | 2024 | https://redhat.com | Image Based Linux 11 | Vendor | HeliumOS | 2024 | https://www.heliumos.org/ | An atomic desktop operating system for your devices 12 | 13 | # bootc Adopters (indirect, via ostree) 14 | 15 | Bootc is a relatively new project, but much of the underlying technology and goals is *not* new. 16 | The underlying ostree project is over 13 years old (as of 2024). This project also relates 17 | to [rpm-ostree](https://github.com/coreos/rpm-ostree/) which has existed a really long time. 18 | 19 | Not every one of these projects uses bootc directly today, but a toplevel goal of bootc 20 | is to be the successor to ostree, and it is our aim to seamlessly carry forward these users. 21 | 22 | | Type | Name | Since | Website | Use-Case | 23 | |:-|:-|:-|:-|:-| 24 | | Vendor | Endless | 2014 | [link](https://www.endlessos.org/os) | A Completely Free, User-Friendly Operating System Packed with Educational Tools, Games, and More 25 | | Vendor | Red Hat | 2015 | [link](https://redhat.com) | Image Based Linux 26 | | Vendor | Apertis | 2020 | [link](https://apertis.org) | Collaborative OS platform for products 27 | | Vendor | Fedora Project | 2021 | [link](https://fedoraproject.org/atomic-desktops/) | An atomic desktop operating system aimed at good support for container-focused workflows 28 | | Vendor | Playtron GameOS | 2022 | [link](https://www.playtron.one/) | A video game console OS that has integration with the top PC game stores | 29 | | Vendor | Universal Blue | 2022 | [link](https://universal-blue.org/) | The reliability of a Chromebook, but with the flexibility and power of a traditional Linux desktop 30 | | Vendor | Fyra Labs | 2024 | [link](https://fyralabs.com) | Bootc powers an experimental variant of Ultramarine Linux 31 | 32 | ### Adopter Types 33 | 34 | **End-user**: The organization runs bootc in production in some way. 35 | 36 | **Integration**: The organization has a product that integrates with bootc, but does not contain bootc. 37 | 38 | **Vendor**: The organization packages bootc in their product and sells it as part of their product. 39 | 40 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/tar/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Losslessly export and import ostree commits as tar archives 2 | //! 3 | //! Convert an ostree commit into a tarball stream, and import it again, including 4 | //! support for OSTree signature verification. 5 | //! 6 | //! In the current libostree C library, while it supports export to tar, this 7 | //! process is lossy - commit metadata is discarded. Further, re-importing 8 | //! requires recalculating all of the object checksums, and tying these 9 | //! together, it does not support verifying ostree level cryptographic signatures 10 | //! such as GPG/ed25519. 11 | //! 12 | //! # Tar stream layout 13 | //! 14 | //! In order to solve these problems, this new tar serialization format effectively 15 | //! combines *both* a `/sysroot/ostree/repo/objects` directory and a checkout in `/usr`, 16 | //! where the latter are hardlinks to the former. 17 | //! 18 | //! The exported stream will have the ostree metadata first; in particular the commit object. 19 | //! Following the commit object is the `.commitmeta` object, which contains any cryptographic 20 | //! signatures. 21 | //! 22 | //! This library then supports verifying the pair of (commit, commitmeta) using an ostree 23 | //! remote, in the same way that `ostree pull` will do. 24 | //! 25 | //! The remainder of the stream is a breadth-first traversal of dirtree/dirmeta objects and the 26 | //! content objects they reference. 27 | //! 28 | //! # `bare-split-xattrs` repository mode 29 | //! 30 | //! In format version 1, the tar stream embeds a proper ostree repository using a tailored 31 | //! `bare-split-xattrs` mode. 32 | //! 33 | //! This is because extended attributes (xattrs) are a complex subject for tar, which has 34 | //! many variants. 35 | //! Further, when exporting bootable ostree commits to container images, it is not actually 36 | //! desired to have the container runtime try to unpack and apply those. 37 | //! 38 | //! For these reasons, extended attributes (xattrs) get serialized into detached objects 39 | //! which are associated with the relevant content objects. 40 | //! 41 | //! At a low level, two dedicated object types are used: 42 | //! * `file-xattrs` as regular files storing (and de-duplicating) xattrs content. 43 | //! * `file-xattrs-link` as hardlinks which associate a `file` object to its corresponding 44 | //! `file-xattrs` object. 45 | 46 | mod import; 47 | pub use import::*; 48 | mod export; 49 | pub use export::*; 50 | mod write; 51 | pub use write::*; 52 | --------------------------------------------------------------------------------