├── .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-via-local-oci.yaml │ │ ├── spec-rfe-ostree-deployment.yaml │ │ ├── spec-v1a1.yaml │ │ ├── spec-only-booted.yaml │ │ ├── spec-ostree-remote.yaml │ │ ├── spec-ostree-to-bootc.yaml │ │ ├── spec-staged-download-only.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 │ │ └── utils.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 │ ├── 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 ├── tests-integration │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── selinux.rs │ │ ├── hostpriv.rs │ │ └── tests-integration.rs └── xtask │ └── Cargo.toml ├── 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-inspect.8.md │ │ ├── bootc-install-finalize.8.md │ │ ├── bootc-container.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-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 ├── _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 │ │ │ ├── 021-test-rhsm-facts.nu │ │ │ ├── 015-test-fsck.nu │ │ │ ├── 020-test-relabel.nu │ │ │ ├── 051-test-initramfs.nu │ │ │ ├── 012-test-unit-status.nu │ │ │ ├── 030-test-locking-read.nu │ │ │ ├── 017-test-bound-storage.nu │ │ │ ├── 011-test-ostree-ext-cli.nu │ │ │ ├── 010-test-bootc-container-store.nu │ │ │ ├── 011-test-resolvconf.nu │ │ │ ├── 030-test-composefs.nu │ │ │ └── 001-test-status.nu │ │ ├── README.md │ │ ├── tap.nu │ │ ├── test-01-readonly.nu │ │ ├── bootc_testlib.nu │ │ ├── test-switch-mutate-in-place.nu │ │ ├── test-usroverlay.nu │ │ ├── test-install-unified-flag.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 │ ├── test-30-install-unified-flag.fmf │ ├── test-31-switch-to-unified.fmf │ └── Dockerfile.upgrade ├── 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 ├── packit-reboot.yml ├── compute-composefs-digest ├── os-image-map.json ├── build-sealed ├── generate-secureboot-keys ├── system-reinstall-bootc.exp ├── Containerfile └── Containerfile.packit ├── .cargo └── config.toml ├── .bootc-dev-infra-commit.txt ├── renovate.json ├── .gitignore ├── contrib ├── packaging │ ├── fedora-systemd-boot.txt │ ├── usr-extras │ │ └── lib │ │ │ └── bootc │ │ │ └── kargs.d │ │ │ └── 21-console-hvc0.toml │ ├── README-usr-extras.md │ ├── fedora-extra.txt │ ├── install-buildroot │ ├── configure-systemdboot │ ├── install-rpm-and-setup │ ├── configure-variant │ ├── configure-rootfs │ └── build-rpm └── scripts │ └── fedora-bootc-destructive-cleanup ├── clippy.toml ├── .github ├── auto-review-config.yml ├── workflows │ ├── labeler.yml │ ├── autovendor.yml │ ├── crates-release.yml │ ├── docs.yml │ ├── auto-review.yml │ ├── rebase.yml │ ├── openssf-scorecard.yml │ └── build-and-publish.yml ├── labeler.yml └── actions │ └── setup-rust │ └── action.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 │ └── status-outside-container │ └── run ├── 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 └── Dockerfile.cfsuki /.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 | 2dd498656b9653c321e5d9a8600e6b506714acb3 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 | -------------------------------------------------------------------------------- /contrib/packaging/fedora-systemd-boot.txt: -------------------------------------------------------------------------------- 1 | # This file defines the package name for systemd-boot 2 | systemd-boot-unsigned 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 | -------------------------------------------------------------------------------- /crates/ostree-ext/tests/it/fixtures/hlinks.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/bootc/HEAD/crates/ostree-ext/tests/it/fixtures/hlinks.tar.gz -------------------------------------------------------------------------------- /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/src/fixtures/ostree-gpg-test-home.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/bootc/HEAD/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 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/fixtures/fedora-coreos-contentmeta.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/bootc/HEAD/crates/ostree-ext/src/fixtures/fedora-coreos-contentmeta.json.gz -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tmt/tests/test-30-install-unified-flag.fmf: -------------------------------------------------------------------------------- 1 | summary: Test bootc install with experimental unified storage flag 2 | test: nu booted/test-install-unified-flag.nu 3 | duration: 30m 4 | -------------------------------------------------------------------------------- /tmt/tests/test-31-switch-to-unified.fmf: -------------------------------------------------------------------------------- 1 | summary: Onboard to unified storage, build derived image, and switch to it 2 | test: nu booted/test-switch-to-unified.nu 3 | duration: 30m 4 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/container/tests/it/fixtures/exampleos.tar.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootc-dev/bootc/HEAD/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/booted/readonly/021-test-rhsm-facts.nu: -------------------------------------------------------------------------------- 1 | use std assert 2 | use tap.nu 3 | 4 | tap begin "rhsm facts" 5 | 6 | # Verify we have this feature 7 | if ("/etc/rhsm" | path exists) { 8 | bootc internals publish-rhsm-facts --help 9 | let status = systemctl show -P ActiveState bootc-publish-rhsm-facts.service 10 | assert equal $status "inactive" 11 | } 12 | 13 | tap ok 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/lib/src/bootc_composefs/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod boot; 2 | pub(crate) mod delete; 3 | pub(crate) mod digest; 4 | pub(crate) mod finalize; 5 | pub(crate) mod gc; 6 | pub(crate) mod repo; 7 | pub(crate) mod rollback; 8 | pub(crate) mod service; 9 | pub(crate) mod soft_reboot; 10 | pub(crate) mod state; 11 | pub(crate) mod status; 12 | pub(crate) mod switch; 13 | pub(crate) mod update; 14 | pub(crate) mod utils; 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-01-readonly.nu: -------------------------------------------------------------------------------- 1 | # number: 1 2 | # extra: 3 | # try_bind_storage: true 4 | # tmt: 5 | # summary: Execute booted readonly/nondestructive tests 6 | # duration: 30m 7 | # 8 | # Run all readonly tests in sequence 9 | use tap.nu 10 | 11 | tap begin "readonly tests" 12 | 13 | # Get all readonly test files and run them in order 14 | let tests = (ls booted/readonly/*-test-*.nu | get name | sort) 15 | 16 | for test_file in $tests { 17 | print $"Running ($test_file)..." 18 | nu $test_file 19 | } 20 | 21 | tap ok -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /hack/compute-composefs-digest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | # This just runs `bootc container compute-composefs-digest` in a provided container image 4 | image=$1 5 | shift 6 | # Find the container storage 7 | graphroot=$(podman system info -f '{{.Store.GraphRoot}}') 8 | # --pull=never because we don't want to pollute the output with progress and most use cases 9 | # for this really should be operating on pre-pulled images. 10 | exec podman run --pull=never --quiet --rm --privileged --read-only --security-opt=label=disable -v /sys:/sys:ro --net=none \ 11 | -v ${graphroot}:/run/host-container-storage:ro --tmpfs /var "$image" bootc container compute-composefs-digest-from-storage 12 | -------------------------------------------------------------------------------- /docs/src/man/bootc-container-inspect.8.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | bootc-container-inspect - Output JSON to stdout containing the container image metadata 4 | 5 | # SYNOPSIS 6 | 7 | bootc container inspect 8 | 9 | # DESCRIPTION 10 | 11 | Output JSON to stdout containing the container image metadata 12 | 13 | # OPTIONS 14 | 15 | 16 | **--rootfs**=*ROOTFS* 17 | 18 | Operate on the provided rootfs 19 | 20 | Default: / 21 | 22 | 23 | 24 | # EXAMPLES 25 | 26 | Inspect container image metadata: 27 | 28 | bootc container inspect 29 | 30 | # SEE ALSO 31 | 32 | **bootc**(8) 33 | 34 | # VERSION 35 | 36 | 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) 8 | dnf config-manager --set-enabled crb 9 | # Enable EPEL for sbsigntools 10 | dnf -y install epel-release 11 | ;; 12 | fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';; 13 | esac 14 | # Handle version skew, xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 15 | dnf -y distro-sync ostree{,-libs} systemd 16 | # Install base build requirements 17 | dnf -y builddep bootc.spec 18 | # And extra packages 19 | grep -Ev -e '^#' fedora-extra.txt | xargs dnf -y install 20 | -------------------------------------------------------------------------------- /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-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@v6 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.1.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 | -------------------------------------------------------------------------------- /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 | bootc-kernel-cmdline = { path = "../kernel_cmdline", version = "0.0.0" } 19 | 20 | [lints] 21 | workspace = true 22 | 23 | [features] 24 | default = ['pre-6.15'] 25 | rhel9 = ['composefs/rhel9'] 26 | 'pre-6.15' = ['composefs/pre-6.15'] 27 | -------------------------------------------------------------------------------- /.github/workflows/crates-release.yml: -------------------------------------------------------------------------------- 1 | # See https://crates.io/docs/trusted-publishing 2 | name: Publish to crates.io 3 | on: 4 | push: 5 | tags: ['v*'] # Triggers when pushing tags starting with 'v' 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-24.04 9 | permissions: 10 | id-token: write # Required for OIDC token exchange 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: rust-lang/crates-io-auth-action@v1 14 | id: auth 15 | - run: | 16 | for crate in bootc-internal-utils bootc-internal-blockdev; do 17 | echo "Publishing $crate..." 18 | cargo publish -p "$crate" 19 | echo "Successfully published $crate" 20 | done 21 | env: 22 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /hack/os-image-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": { 3 | "rhel-10.2": "images.paas.redhat.com/bootc/rhel-bootc:latest-10.2", 4 | "rhel-9.8": "images.paas.redhat.com/bootc/rhel-bootc:latest-9.8", 5 | "centos-9": "quay.io/centos-bootc/centos-bootc:stream9", 6 | "centos-10": "quay.io/centos-bootc/centos-bootc:stream10", 7 | "fedora-42": "quay.io/fedora/fedora-bootc:42", 8 | "fedora-43": "quay.io/fedora/fedora-bootc:43", 9 | "fedora-44": "quay.io/fedora/fedora-bootc:rawhide" 10 | }, 11 | "buildroot-base": { 12 | "centos-9": "quay.io/centos/centos:stream9", 13 | "centos-10": "quay.io/centos/centos:stream10", 14 | "fedora-42": "quay.io/fedora/fedora:42", 15 | "fedora-43": "quay.io/fedora/fedora:43", 16 | "fedora-44": "quay.io/fedora/fedora:rawhide" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | #[test] 23 | fn test_dirtree() { 24 | // Just a compilation test 25 | let data = b"".try_as_aligned().ok(); 26 | if let Some(data) = data { 27 | let _t = gv_dirtree!().cast(data); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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 | # Allow more delay for bootc to settle 8 | sleep 120sec 9 | 10 | tmt-reboot 11 | } 12 | 13 | # True if we're running in bcvk with `--bind-storage-ro` and 14 | # we can expect to be able to pull container images from the host. 15 | # See xtask.rs 16 | export def have_hostexports [] { 17 | $env.BCVK_EXPORT? == "1" 18 | } 19 | 20 | # Parse the kernel commandline into a list. 21 | # This is not a proper parser, but good enough 22 | # for what we need here. 23 | export def parse_cmdline [] { 24 | open /proc/cmdline | str trim | split row " " 25 | } 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/actions/setup-rust/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Rust' 2 | description: 'Install Rust toolchain with caching and nextest' 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - name: Install Rust toolchain 7 | uses: dtolnay/rust-toolchain@stable 8 | - name: Install nextest 9 | uses: taiki-e/install-action@v2 10 | with: 11 | tool: nextest 12 | - name: Setup Rust cache 13 | uses: Swatinem/rust-cache@v2 14 | with: 15 | cache-all-crates: true 16 | # Only generate caches on push to git main 17 | save-if: ${{ github.ref == 'refs/heads/main' }} 18 | # Suppress actually using the cache for builds running from 19 | # git main so that we avoid incremental compilation bugs 20 | lookup-only: ${{ github.ref == 'refs/heads/main' }} 21 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-switch-mutate-in-place.nu: -------------------------------------------------------------------------------- 1 | # number: 31 2 | # tmt: 3 | # summary: switch --mutate-in-place 4 | # duration: 30m 5 | # 6 | use std assert 7 | use tap.nu 8 | use bootc_testlib.nu 9 | 10 | # See https://github.com/bootc-dev/bootc/issues/1854 11 | 12 | let st = bootc status --json | from json 13 | let is_composefs = ($st.status.booted.composefs? != null) 14 | if not $is_composefs { 15 | # This is aiming to reproduce an environment closer to the Anaconda case 16 | # where we're chrooted into a non-booted system. TODO: What we really want 17 | # is to add `bootc switch --sysroot` too. 18 | mv /run/ostree-booted /run/ostree-booted.orig 19 | unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && bootc switch --mutate-in-place quay.io/nosuchimage/image-to-test-switch' 20 | tap ok 21 | } 22 | -------------------------------------------------------------------------------- /contrib/packaging/configure-systemdboot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Helper for signing and re-injecting systemd-boot 3 | set -euo pipefail 4 | op=$1 5 | shift 6 | 7 | sdboot="usr/lib/systemd/boot/efi/systemd-bootx64.efi" 8 | sdboot_bn=$(basename ${sdboot}) 9 | 10 | case $op in 11 | download) 12 | mkdir -p /out 13 | cd /out 14 | dnf -y download systemd-boot-unsigned 15 | ;; 16 | sign) 17 | mkdir -p /out 18 | rpm -Uvh /run/sdboot-package/out/*.rpm 19 | # Sign with sbsign using db certificate and key 20 | sbsign \ 21 | --key /run/secrets/secureboot_key \ 22 | --cert /run/secrets/secureboot_cert \ 23 | --output /out/${sdboot_bn} \ 24 | /${sdboot} 25 | ls -al /out/${sdboot_bn} 26 | ;; 27 | *) echo "Unknown operation $op" 1>&2; exit 1 28 | ;; 29 | esac 30 | -------------------------------------------------------------------------------- /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.1.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 | -------------------------------------------------------------------------------- /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 inspect** | Output JSON to stdout containing the container image metadata | 23 | | **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build | 24 | 25 | 26 | 27 | # VERSION 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | downloadOnly: false 25 | ostree: 26 | checksum: 439f6bd2e2361bee292c1f31840d798c5ac5ba76483b8021dc9f7b0164ac0f48 27 | deploySerial: 0 28 | stateroot: default 29 | rollback: null 30 | rollbackQueued: false 31 | type: bootcHost -------------------------------------------------------------------------------- /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 | downloadOnly: false 15 | store: null 16 | ostree: 17 | checksum: 1c24260fdd1be20f72a4a97a75c582834ee3431fbb0fa8e4f482bb219d633a45 18 | deploySerial: 0 19 | stateroot: default 20 | booted: 21 | image: null 22 | cachedUpdate: null 23 | incompatible: false 24 | pinned: false 25 | downloadOnly: false 26 | store: null 27 | ostree: 28 | checksum: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 29 | deploySerial: 0 30 | stateroot: default 31 | rollback: null 32 | rollbackQueued: false 33 | type: null 34 | -------------------------------------------------------------------------------- /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-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 | downloadOnly: false 23 | ostree: 24 | checksum: 41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3 25 | deploySerial: 0 26 | stateroot: default 27 | rollback: null 28 | isContainer: false 29 | -------------------------------------------------------------------------------- /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/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.1.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 | -------------------------------------------------------------------------------- /hack/build-sealed: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | # This should turn into https://github.com/bootc-dev/bootc/issues/1498 4 | 5 | dn=$(cd $(dirname $0) && pwd) 6 | 7 | variant=$1 8 | shift 9 | # The un-sealed container image we want to use 10 | input_image=$1 11 | shift 12 | # The output container image 13 | output_image=$1 14 | shift 15 | 16 | runv() { 17 | set -x 18 | "$@" 19 | } 20 | 21 | case $variant in 22 | ostree) 23 | # Nothing to do 24 | echo "Not building a sealed image; forwarding tag" 25 | runv podman tag $input_image $output_image 26 | exit 0 27 | ;; 28 | composefs-sealeduki*) 29 | ;; 30 | *) 31 | echo "Unknown variant=$variant" 1>&2; exit 1 32 | ;; 33 | esac 34 | 35 | cfs_digest=$(${dn}/compute-composefs-digest $input_image) 36 | runv podman build -t $output_image \ 37 | --build-arg=COMPOSEFS_FSVERITY=${cfs_digest} --build-arg=base=${input_image} "$@" -f Dockerfile.cfsuki . 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | downloadOnly: false 25 | ostree: 26 | checksum: 439f6bd2e2361bee292c1f31840d798c5ac5ba76483b8021dc9f7b0164ac0f48 27 | deploySerial: 0 28 | stateroot: default 29 | rollback: null 30 | rollbackQueued: false 31 | type: bootcHost -------------------------------------------------------------------------------- /hack/generate-secureboot-keys: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | # Generate Secure Boot keys, only intended to be used for our CI pipeline. 4 | d=target/test-secureboot 5 | # This file existing signals completion 6 | if test -f "${d}/.done"; then exit 0; fi 7 | mkdir -p "$d" 8 | cd "$d" 9 | systemd-id128 new -u > GUID.txt 10 | openssl req -quiet -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Platform Key/' -out PK.crt 11 | openssl x509 -outform DER -in PK.crt -out PK.cer 12 | openssl req -quiet -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Key Exchange Key/' -out KEK.crt 13 | openssl x509 -outform DER -in KEK.crt -out KEK.cer 14 | openssl req -quiet -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Signature Database key/' -out db.crt 15 | openssl x509 -outform DER -in db.crt -out db.cer 16 | touch .done 17 | echo "Generated Secure Boot keys in ${d}" 18 | -------------------------------------------------------------------------------- /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 | downloadOnly: false 24 | ostree: 25 | checksum: 41af286dc0b172ed2f1ca934fd2278de4a1192302ffa07087cea2682e7d372e3 26 | deploySerial: 0 27 | stateroot: default 28 | rollback: null 29 | isContainer: false 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # If we have --bind-storage-ro, then verify it 20 | if ($env.BOOTC_upgrade_image? != null) { 21 | let booted = $st.status.booted 22 | let imgref = $booted.image.image.image 23 | let digest = $booted.image.imageDigest 24 | let imgref_untagged = $imgref | split row ':' | first 25 | let digested_imgref = $"($imgref_untagged)@($digest)" 26 | systemd-run -dqP /bin/env 27 | podman inspect $digested_imgref 28 | } 29 | 30 | tap ok 31 | -------------------------------------------------------------------------------- /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.11", path = "../lib" } 19 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.1.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.1.0" 8 | 9 | [dependencies] 10 | # Internal crates 11 | bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.1.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" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bootc-internal-utils" 3 | description = "Internal implementation component of bootc; do not use" 4 | version = "0.1.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/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 | scopeguard = "1.2.0" 33 | 34 | [lints] 35 | workspace = true 36 | -------------------------------------------------------------------------------- /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 | itertools = "0.14.0" 35 | 36 | [lints] 37 | workspace = true 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | bootc image cmd pull busybox 20 | podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage image exists busybox 21 | 22 | 'corrupted JSON!@#%!@#' | save -f /run/ostree/auth.json 23 | let e = bootc image cmd pull busybox | complete | get exit_code 24 | assert not equal $e 0 25 | rm -v /run/ostree/auth.json 26 | } 27 | 28 | tap ok 29 | -------------------------------------------------------------------------------- /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 < 25 | **--all** 26 | 27 | Print all configuration 28 | 29 | 30 | 31 | # VERSION 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /contrib/packaging/configure-variant: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Configure system for a specific bootc variant 3 | set -xeuo pipefail 4 | 5 | dn=$(dirname $0) 6 | 7 | VARIANT="${1:-}" 8 | 9 | if [ -z "$VARIANT" ]; then 10 | # No variant specified, nothing to do 11 | exit 0 12 | fi 13 | 14 | # Handle variant-specific configuration 15 | case "${VARIANT}" in 16 | *-sdboot) 17 | # Install systemd-boot and remove bootupd; 18 | # We downloaded this in an earlier phase 19 | sdboot="usr/lib/systemd/boot/efi/systemd-bootx64.efi" 20 | sdboot_bn=$(basename ${sdboot}) 21 | rpm -Uvh /run/sdboot-content/out/*.rpm 22 | # And override with our signed binary 23 | install -m 0644 /run/sdboot-signed/out/${sdboot_bn} /${sdboot} 24 | 25 | # Uninstall bootupd 26 | rpm -e bootupd 27 | rm -rf /usr/lib/bootupd/updates 28 | # Clean up package manager caches 29 | dnf clean all 30 | rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/* 31 | ;; 32 | # Future variants can be added here 33 | # For Debian support, this could check package manager type and use apt instead 34 | esac 35 | -------------------------------------------------------------------------------- /crates/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The inevitable catchall "utils" crate. Generally only add 2 | //! things here that only depend on the standard library and 3 | //! "core" crates. 4 | //! 5 | mod command; 6 | pub use command::*; 7 | mod path; 8 | pub use path::*; 9 | mod iterators; 10 | pub use iterators::*; 11 | mod timestamp; 12 | pub use timestamp::*; 13 | mod tracing_util; 14 | pub use tracing_util::*; 15 | /// Re-execute the current process 16 | pub mod reexec; 17 | mod result_ext; 18 | pub use result_ext::*; 19 | 20 | /// The name of our binary 21 | pub const NAME: &str = "bootc"; 22 | 23 | /// Intended for use in `main`, calls an inner function and 24 | /// handles errors by printing them. 25 | pub fn run_main(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 | -------------------------------------------------------------------------------- /tests/container/status-outside-container/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Test that `bootc status` works on a non-bootc-deployed system. 3 | # 4 | # This simulates a "package mode" environment where bootc is installed 5 | # via RPM on a traditional system (not deployed via ostree/composefs). 6 | # The key is hiding /run/.containerenv so bootc doesn't think it's in a container. 7 | # 8 | # xref: https://issues.redhat.com/browse/RHEL-135687 9 | set -euo pipefail 10 | image=$1 11 | 12 | # Run the container with: 13 | # - A tmpfs over /run to hide .containerenv (simulates bare metal) 14 | # - Unset the 'container' environment variable 15 | # - No /sysroot (simulates package mode) 16 | # Use --format=humanreadable to get consistent output 17 | output=$(podman run --rm \ 18 | --tmpfs /run \ 19 | --env container= \ 20 | "$image" \ 21 | bootc status --format=humanreadable 2>&1) 22 | 23 | # Verify the output indicates this is not a bootc-deployed system 24 | if echo "$output" | grep -q "System is not deployed via bootc"; then 25 | echo "ok status-outside-container: correctly reports non-bootc system" 26 | else 27 | echo "FAIL: unexpected output from bootc status:" >&2 28 | echo "$output" >&2 29 | exit 1 30 | fi 31 | -------------------------------------------------------------------------------- /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/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/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 | downloadOnly: false 24 | store: ostreeContainer 25 | ostree: 26 | checksum: 05cbf6dcae32e7a1c5a0774a648a073a5834a305ca92204b53fb6c281fe49db1 27 | deploySerial: 0 28 | stateroot: default 29 | booted: 30 | image: null 31 | cachedUpdate: null 32 | incompatible: false 33 | pinned: false 34 | downloadOnly: false 35 | store: null 36 | ostree: 37 | checksum: f9fa3a553ceaaaf30cf85bfe7eed46a822f7b8fd7e14c1e3389cbc3f6d27f791 38 | deploySerial: 0 39 | stateroot: default 40 | rollback: null 41 | rollbackQueued: false 42 | type: null 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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.1.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/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/lib/src/fixtures/spec-booted-pinned.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: true 24 | downloadOnly: false 25 | ostree: 26 | checksum: 439f6bd2e2361bee292c1f31840d798c5ac5ba76483b8021dc9f7b0164ac0f48 27 | deploySerial: 0 28 | stateroot: default 29 | otherDeployments: 30 | - image: 31 | image: 32 | image: quay.io/centos-bootc/centos-bootc:stream9 33 | transport: registry 34 | version: stream9.20240807.0 35 | timestamp: null 36 | imageDigest: sha256:47e5ed613a970b6574bfa954ab25bb6e85656552899aa518b5961d9645102b37 37 | architecture: arm64 38 | cachedUpdate: null 39 | incompatible: false 40 | pinned: true 41 | downloadOnly: false 42 | ostree: 43 | checksum: 99b2cc3b6edce9ebaef6a6076effa5ee3e1dcff3523016ffc94a1b27c6c67e12 44 | deploySerial: 0 45 | stateroot: default 46 | rollback: null 47 | rollbackQueued: false 48 | type: bootcHost 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | downloadOnly: false 25 | ostree: 26 | checksum: 3c6dad657109522e0b2e49bf44b5420f16f0b438b5b9357e5132211cfbad135d 27 | deploySerial: 0 28 | stateroot: default 29 | booted: 30 | image: 31 | image: 32 | image: quay.io/example/someimage:latest 33 | transport: registry 34 | signature: insecure 35 | architecture: arm64 36 | version: nightly 37 | timestamp: 2023-09-30T19:22:16Z 38 | imageDigest: sha256:736b359467c9437c1ac915acaae952aad854e07eb4a16a94999a48af08c83c34 39 | incompatible: false 40 | pinned: false 41 | downloadOnly: false 42 | ostree: 43 | checksum: 26836632adf6228d64ef07a26fd3efaf177104efd1f341a2cf7909a3e4e2c72c 44 | deploySerial: 0 45 | stateroot: default 46 | rollback: null 47 | isContainer: false 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/openssf-scorecard.yml: -------------------------------------------------------------------------------- 1 | # Upstream https://github.com/ossf/scorecard/blob/main/.github/workflows/scorecard-analysis.yml 2 | # Tweaked to not pin actions by SHA digest as I think that's overkill noisy security theater. 3 | name: OpenSSF Scorecard analysis 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | analysis: 13 | name: Scorecard analysis 14 | runs-on: ubuntu-24.04 15 | permissions: 16 | # Needed for Code scanning upload 17 | security-events: write 18 | # Needed for GitHub OIDC token if publish_results is true 19 | id-token: write 20 | 21 | steps: 22 | - name: "Checkout code" 23 | uses: actions/checkout@v6 24 | with: 25 | persist-credentials: false 26 | 27 | - name: "Run analysis" 28 | uses: ossf/scorecard-action@v2.4.3 29 | with: 30 | results_file: results.sarif 31 | results_format: sarif 32 | # Scorecard team runs a weekly scan of public GitHub repos, 33 | # see https://github.com/ossf/scorecard#public-data. 34 | # Setting `publish_results: true` helps us scale by leveraging your workflow to 35 | # extract the results instead of relying on our own infrastructure to run scans. 36 | # And it's free for you! 37 | publish_results: true 38 | 39 | - name: "Upload artifact" 40 | uses: actions/upload-artifact@v6 41 | with: 42 | name: SARIF file 43 | path: results.sarif 44 | retention-days: 5 45 | 46 | - name: "Upload to code-scanning" 47 | uses: github/codeql-action/upload-sarif@v4 48 | with: 49 | sarif_file: results.sarif 50 | 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/lib/src/bootc_composefs/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | bootc_composefs::{ 3 | boot::{compute_boot_digest_uki, SYSTEMD_UKI_DIR}, 4 | state::update_boot_digest_in_origin, 5 | }, 6 | store::Storage, 7 | }; 8 | use anyhow::Result; 9 | use bootc_kernel_cmdline::utf8::Cmdline; 10 | use fn_error_context::context; 11 | 12 | fn get_uki(storage: &Storage, deployment_verity: &str) -> Result> { 13 | let uki_dir = storage 14 | .esp 15 | .as_ref() 16 | .ok_or_else(|| anyhow::anyhow!("ESP not mounted"))? 17 | .fd 18 | .open_dir(SYSTEMD_UKI_DIR)?; 19 | 20 | let req_fname = format!("{deployment_verity}.efi"); 21 | 22 | for entry in uki_dir.entries_utf8()? { 23 | let pe = entry?; 24 | 25 | let filename = pe.file_name()?; 26 | 27 | if filename != req_fname { 28 | continue; 29 | } 30 | 31 | return Ok(uki_dir.read(filename)?); 32 | } 33 | 34 | anyhow::bail!("UKI for deployment {deployment_verity} not found") 35 | } 36 | 37 | #[context("Computing and storing boot digest for UKI")] 38 | pub(crate) fn compute_store_boot_digest_for_uki( 39 | storage: &Storage, 40 | deployment_verity: &str, 41 | ) -> Result { 42 | let uki = get_uki(storage, deployment_verity)?; 43 | let digest = compute_boot_digest_uki(&uki)?; 44 | 45 | update_boot_digest_in_origin(storage, &deployment_verity, &digest)?; 46 | return Ok(digest); 47 | } 48 | 49 | #[context("Getting UKI cmdline")] 50 | pub(crate) fn get_uki_cmdline( 51 | storage: &Storage, 52 | deployment_verity: &str, 53 | ) -> Result> { 54 | let uki = get_uki(storage, deployment_verity)?; 55 | let cmdline = composefs_boot::uki::get_cmdline(&uki)?; 56 | 57 | return Ok(Cmdline::from(cmdline.to_owned())); 58 | } 59 | -------------------------------------------------------------------------------- /crates/ostree-ext/src/isolation.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | use std::sync::OnceLock; 3 | 4 | pub(crate) const DEFAULT_UNPRIVILEGED_USER: &str = "nobody"; 5 | 6 | /// Checks if the current process is (apparently at least) 7 | /// running under systemd. We use this in various places 8 | /// to e.g. log to the journal instead of printing to stdout. 9 | pub(crate) fn running_in_systemd() -> 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 | -------------------------------------------------------------------------------- /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 | # For reproducible builds: 33 | # - use_source_date_epoch_as_buildtime: RPM build timestamp uses SOURCE_DATE_EPOCH 34 | # - clamp_mtime_to_source_date_epoch: file mtimes clamped to SOURCE_DATE_EPOCH 35 | # - _buildhost: fixed hostname for consistent RPM metadata 36 | rpmbuild -bb \ 37 | --define "_topdir /tmp/rpmbuild" \ 38 | --define "_builddir ${SRC_DIR}" \ 39 | --define "container_build 1" \ 40 | --define "use_source_date_epoch_as_buildtime 1" \ 41 | --define "clamp_mtime_to_source_date_epoch 1" \ 42 | --define "_buildhost reproducible" \ 43 | --with tests \ 44 | --nocheck \ 45 | "${SPEC_FILE}" 46 | 47 | # Copy built RPMs to output directory 48 | ARCH=$(uname -m) 49 | mkdir -p "${OUTPUT_DIR}" 50 | cp /tmp/rpmbuild/RPMS/${ARCH}/*.rpm "${OUTPUT_DIR}/" 51 | rm -rf /tmp/rpmbuild 52 | -------------------------------------------------------------------------------- /tmt/tests/booted/test-install-unified-flag.nu: -------------------------------------------------------------------------------- 1 | # number: 30 2 | # extra: 3 | # tmt: 4 | # summary: Test bootc install with experimental unified storage flag 5 | # duration: 30m 6 | # 7 | # Test bootc install with --experimental-unified-storage flag 8 | # This test performs an actual install to a loopback device and verifies 9 | # the unified storage path is used. 10 | 11 | use std assert 12 | use tap.nu 13 | 14 | # Use a generic target image to test skew between the bootc binary doing 15 | # the install and the target image 16 | let target_image = "docker://quay.io/centos-bootc/centos-bootc:stream10" 17 | 18 | def main [] { 19 | tap begin "install with experimental unified storage flag" 20 | 21 | # Setup filesystem - create a loopback disk image 22 | mkdir /var/mnt 23 | truncate -s 10G disk.img 24 | 25 | # Disable SELinux enforcement for the install (same as test-install-outside-container) 26 | setenforce 0 27 | 28 | # Perform the install with unified storage flag 29 | # We use systemd-run to handle mount namespace issues 30 | systemd-run -p MountFlags=slave -qdPG -- /bin/sh -c $" 31 | set -xeuo pipefail 32 | if test -d /sysroot/ostree; then mount --bind /usr/share/empty /sysroot/ostree; fi 33 | mkdir -p /tmp/ovl/{upper,work} 34 | mount -t overlay -olowerdir=/usr,workdir=/tmp/ovl/work,upperdir=/tmp/ovl/upper overlay /usr 35 | # Note we do keep the other bootupd state 36 | rm -vrf /usr/lib/bootupd/updates 37 | # Another bootc install bug, we should not look at this in outside-of-container flows 38 | rm -vrf /usr/lib/bootc/bound-images.d 39 | # Install with unified storage flag to loopback disk 40 | bootc install to-disk --disable-selinux --via-loopback --filesystem xfs --experimental-unified-storage --source-imgref ($target_image) ./disk.img 41 | " 42 | 43 | # Cleanup 44 | rm -f disk.img 45 | 46 | tap ok 47 | } 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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= parameter 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-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 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Images 2 | 3 | permissions: 4 | packages: write 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | workflow_dispatch: {} 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | LIBVIRT_DEFAULT_URI: "qemu:///session" 14 | DEV_IMAGE: ghcr.io/bootc-dev/dev-bootc 15 | 16 | jobs: 17 | # Build and publish container images to ghcr.io 18 | publish-images: 19 | strategy: 20 | matrix: 21 | test_os: [fedora-42, fedora-43, fedora-44, centos-9, centos-10] 22 | variant: [ostree, composefs-sealeduki-sdboot] 23 | exclude: 24 | # centos-9 UKI is experimental/broken (https://github.com/bootc-dev/bootc/issues/1812) 25 | - test_os: centos-9 26 | variant: composefs-sealeduki-sdboot 27 | runs-on: ubuntu-24.04 28 | steps: 29 | - uses: actions/checkout@v6 30 | - name: Bootc Ubuntu Setup 31 | uses: ./.github/actions/bootc-ubuntu-setup 32 | 33 | - name: Setup env 34 | run: | 35 | BASE=$(just pullspec-for-os base ${{ matrix.test_os }}) 36 | echo "BOOTC_base=${BASE}" >> $GITHUB_ENV 37 | echo "BOOTC_variant=${{ matrix.variant }}" >> $GITHUB_ENV 38 | 39 | if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then 40 | BUILDROOTBASE=$(just pullspec-for-os buildroot-base ${{ matrix.test_os }}) 41 | echo "BOOTC_buildroot_base=${BUILDROOTBASE}" >> $GITHUB_ENV 42 | fi 43 | 44 | - name: Build container 45 | run: just build-integration-test-image 46 | 47 | - name: Login to ghcr.io 48 | uses: redhat-actions/podman-login@v1 49 | with: 50 | registry: ghcr.io 51 | username: ${{ github.actor }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Push container image 55 | run: | 56 | if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then 57 | TAG="${{ matrix.test_os }}-uki" 58 | else 59 | TAG="${{ matrix.test_os }}" 60 | fi 61 | podman tag localhost/bootc ${{ env.DEV_IMAGE }}:${TAG} 62 | podman push ${{ env.DEV_IMAGE }}:${TAG} 63 | -------------------------------------------------------------------------------- /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 | # extra: 3 | # try_bind_storage: true 4 | # tmt: 5 | # summary: Execute local upgrade tests 6 | # duration: 30m 7 | # 8 | # This test does: 9 | # bootc image copy-to-storage 10 | # podman build 11 | # bootc switch --apply 12 | # Verify we boot into the new image 13 | # 14 | use std assert 15 | use tap.nu 16 | 17 | # This code runs on *each* boot. 18 | # Here we just capture information. 19 | bootc status 20 | journalctl --list-boots 21 | 22 | let st = bootc status --json | from json 23 | let booted = $st.status.booted.image 24 | 25 | # Parse the kernel commandline into a list. 26 | # This is not a proper parser, but good enough 27 | # for what we need here. 28 | def parse_cmdline [] { 29 | open /proc/cmdline | str trim | split row " " 30 | } 31 | 32 | def imgsrc [] { 33 | $env.BOOTC_upgrade_image? | default "localhost/bootc-derived-local" 34 | } 35 | 36 | # Run on the first boot 37 | def initial_build [] { 38 | tap begin "local image push + pull + upgrade" 39 | 40 | let imgsrc = imgsrc 41 | # For the packit case, we build locally right now 42 | if ($imgsrc | str ends-with "-local") { 43 | bootc image copy-to-storage 44 | 45 | # A simple derived container that adds a file 46 | "FROM localhost/bootc 47 | RUN touch /usr/share/testing-bootc-upgrade-apply 48 | " | save Dockerfile 49 | # Build it 50 | podman build -t $imgsrc . 51 | } 52 | 53 | # Now, switch into the new image 54 | print $"Applying ($imgsrc)" 55 | bootc switch --transport containers-storage ($imgsrc) 56 | tmt-reboot 57 | } 58 | 59 | # Check we have the updated image 60 | def second_boot [] { 61 | print "verifying second boot" 62 | assert equal $booted.image.transport containers-storage 63 | assert equal $booted.image.image $"(imgsrc)" 64 | 65 | # Verify the new file exists 66 | "/usr/share/testing-bootc-upgrade-apply" | path exists 67 | 68 | tap ok 69 | } 70 | 71 | def main [] { 72 | # See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test 73 | match $env.TMT_REBOOT_COUNT? { 74 | null | "0" => initial_build, 75 | "1" => second_boot, 76 | $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /crates/tests-integration/src/tests-integration.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests. 2 | 3 | use camino::Utf8PathBuf; 4 | use cap_std_ext::cap_std::{self, fs::Dir}; 5 | use clap::Parser; 6 | 7 | mod container; 8 | mod hostpriv; 9 | mod install; 10 | mod runvm; 11 | mod selinux; 12 | mod system_reinstall; 13 | 14 | #[derive(Debug, Parser)] 15 | #[clap(name = "bootc-integration-tests", version, rename_all = "kebab-case")] 16 | pub(crate) enum Opt { 17 | SystemReinstall { 18 | /// Source container image reference 19 | image: String, 20 | #[clap(flatten)] 21 | testargs: libtest_mimic::Arguments, 22 | }, 23 | InstallAlongside { 24 | /// Source container image reference 25 | image: String, 26 | #[clap(flatten)] 27 | testargs: libtest_mimic::Arguments, 28 | }, 29 | HostPrivileged { 30 | image: String, 31 | #[clap(flatten)] 32 | testargs: libtest_mimic::Arguments, 33 | }, 34 | /// Tests which should be executed inside an existing bootc container image. 35 | /// These should be nondestructive. 36 | Container { 37 | #[clap(flatten)] 38 | testargs: libtest_mimic::Arguments, 39 | }, 40 | #[clap(subcommand)] 41 | RunVM(runvm::Opt), 42 | /// Extra helper utility to verify SELinux label presence 43 | #[clap(name = "verify-selinux")] 44 | VerifySELinux { 45 | /// Path to target root 46 | rootfs: Utf8PathBuf, 47 | #[clap(long)] 48 | warn: bool, 49 | }, 50 | } 51 | 52 | fn main() { 53 | let opt = Opt::parse(); 54 | let r = match opt { 55 | Opt::SystemReinstall { image, testargs } => system_reinstall::run(&image, testargs), 56 | Opt::InstallAlongside { image, testargs } => install::run_alongside(&image, testargs), 57 | Opt::HostPrivileged { image, testargs } => hostpriv::run_hostpriv(&image, testargs), 58 | Opt::Container { testargs } => container::run(testargs), 59 | Opt::RunVM(opts) => runvm::run(opts), 60 | Opt::VerifySELinux { rootfs, warn } => { 61 | let root = &Dir::open_ambient_dir(&rootfs, cap_std::ambient_authority()).unwrap(); 62 | selinux::verify_selinux_recurse(root, warn) 63 | } 64 | }; 65 | if let Err(e) = r { 66 | eprintln!("error: {e:?}"); 67 | std::process::exit(1); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Dockerfile.cfsuki: -------------------------------------------------------------------------------- 1 | # Override via --build-arg=base= to use a different base 2 | ARG base=localhost/bootc 3 | FROM $base AS base 4 | 5 | FROM base as kernel 6 | RUN <