Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions .github/scripts/microvm-ci-release_test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/bin/sh
set -eu
# ponytail: macOS bsdtar embeds AppleDouble "._*" sidecar entries for any
# xattr it finds (e.g. the com.apple.provenance macOS stamps on most files)
# unless this is set; harmless no-op on GNU tar/Linux.
export COPYFILE_DISABLE=1

repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
taskfile="$repo_root/Taskfile.yml"
Expand Down Expand Up @@ -31,6 +35,24 @@ forbid() {
fi
}

# ponytail: mirrors package-microvm-release.sh's reproducible_tar — bsdtar
# (macOS) rejects GNU-only --sort/--mtime/--owner/--group/--numeric-owner, so
# use a portable sorted -T file list + a portable epoch->touch stamp instead.
reproducible_bootstrap_tar() {
dir=$1
exclude=$2
archive=$3
epoch=0
if stamp=$(date -u -d "@$epoch" +%Y%m%d%H%M.%S 2>/dev/null); then :; else
stamp=$(date -u -r "$epoch" +%Y%m%d%H%M.%S)
fi
members=$(mktemp)
(cd "$dir" && find . -mindepth 1 -exec touch -h -t "$stamp" {} +)
(cd "$dir" && find . -mindepth 1 \( -type f -o -type l \) ! -name "$exclude") | sed 's#^\./##' | LC_ALL=C sort >"$members"
(cd "$dir" && tar -T "$members" -cf - | gzip -n >"$archive")
rm -f "$members"
}

# Ordinary PR CI must cross the nested-module boundary at every relevant gate.
require 'environment/microvm/go.sum' "$ci"
require 'cd environment/microvm && go build ./...' "$ci"
Expand Down Expand Up @@ -515,10 +537,7 @@ for name in mecatl-microvmd-linux-amd64 mecatl-guest-agent-linux-amd64 mecatl-ar
printf '{"name":"%s"}\n' "$name" >"$scratch/one/$name.spdx.json"
done
bootstrap=mecatl-microvm-v0.0.0-test-linux-amd64.tar.gz
(
cd "$scratch/one"
tar --sort=name --mtime='@0' --owner=0 --group=0 --numeric-owner --exclude="$bootstrap" -cf - . | gzip -n >"$bootstrap"
)
reproducible_bootstrap_tar "$scratch/one" "$bootstrap" "$scratch/one/$bootstrap"
bootstrap_sha=$(sha256sum "$scratch/one/$bootstrap" | cut -d' ' -f1)
jq -n --arg version v0.0.0-test --arg platform linux-amd64 \
--arg url "https://github.com/stacklok/mecatl/releases/download/v0.0.0-test/$bootstrap" \
Expand All @@ -536,11 +555,8 @@ cp -R "$scratch/one" "$scratch/generated-rerun"
for bundle in "$scratch/generated-rerun"/*.sigstore.json; do
printf '{"different_keyless_bundle":true}\n' >"$bundle"
done
(
cd "$scratch/generated-rerun"
rm -f "$bootstrap" microvm-default-linux-amd64.json
tar --sort=name --mtime='@0' --owner=0 --group=0 --numeric-owner --exclude="$bootstrap" -cf - . | gzip -n >"$bootstrap"
)
rm -f "$scratch/generated-rerun/$bootstrap" "$scratch/generated-rerun/microvm-default-linux-amd64.json"
reproducible_bootstrap_tar "$scratch/generated-rerun" "$bootstrap" "$scratch/generated-rerun/$bootstrap"
rerun_sha=$(sha256sum "$scratch/generated-rerun/$bootstrap" | cut -d' ' -f1)
jq -n --arg version v0.0.0-test --arg platform linux-amd64 \
--arg url "https://github.com/stacklok/mecatl/releases/download/v0.0.0-test/$bootstrap" \
Expand Down
33 changes: 32 additions & 1 deletion .github/scripts/package-microvm-release.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/bin/sh
set -eu
# ponytail: macOS bsdtar embeds AppleDouble "._*" sidecar entries for any
# xattr it finds (e.g. the com.apple.provenance macOS stamps on most files)
# unless this is set; harmless no-op on GNU tar/Linux.
export COPYFILE_DISABLE=1

if [ "$#" -ne 3 ]; then
echo "usage: $0 OUTPUT_DIR PLATFORM VERSION" >&2
Expand Down Expand Up @@ -55,6 +59,33 @@ sha256_file() {
fi
}

# ponytail: GNU tar's --sort/--mtime/--owner/--group/--numeric-owner have no
# bsdtar (macOS) equivalent, and bsdtar prints usage + exits nonzero rather
# than erroring loudly through the `tar ... | gzip` pipe (no pipefail here),
# so the archive silently came out empty on macOS. Reproducibility across
# runs is still required (microvm-ci-release_test.sh diffs two invocations),
# so pin every entry's mtime with a portable `touch -h -t` (GNU `date -d @N`
# vs BSD `date -r N` for the epoch->stamp conversion) and feed tar a
# pre-sorted `-T` file list instead of --sort; uid/gid are left as the
# current user's, which is identical across both compared runs. This does
# NOT need to match GNU tar's own output byte-for-byte: nothing downstream
# hashes/signs these tar.gz bytes across platforms — the signed provenance
# binds artifact_tree_digest, computed independently by mecatl-artifact-digest
# over the raw tree, not the archive bytes.
reproducible_tar() {
tree=$1
archive=$2
epoch=${SOURCE_DATE_EPOCH:-0}
if stamp=$(date -u -d "@$epoch" +%Y%m%d%H%M.%S 2>/dev/null); then :; else
stamp=$(date -u -r "$epoch" +%Y%m%d%H%M.%S)
fi
members=$(mktemp)
(cd "$tree" && find . -mindepth 1 -exec touch -h -t "$stamp" {} +)
(cd "$tree" && find . -mindepth 1 \( -type f -o -type l \)) | sed 's#^\./##' | LC_ALL=C sort >"$members"
tar -C "$tree" -T "$members" -cf - | gzip -n >"$archive"
rm -f "$members"
}

download_verified() {
url=$1
destination=$2
Expand Down Expand Up @@ -121,7 +152,7 @@ write_artifact() {
artifact_name="mecatl-$artifact_kind-$platform"
fi
artifact_tree_digest=$(artifact_digest "$artifact_tree")
tar --sort=name --mtime="@${SOURCE_DATE_EPOCH:-0}" --owner=0 --group=0 --numeric-owner -C "$artifact_tree" -cf - . | gzip -n >"$output/$artifact_name.tar.gz"
reproducible_tar "$artifact_tree" "$output/$artifact_name.tar.gz"
write_metadata "$artifact_name" "${artifact_tree_digest#sha256:}"
printf '%s\t%s\t%s\n' "$artifact_kind" "$artifact_name" "$artifact_tree_digest"
}
Expand Down
20 changes: 14 additions & 6 deletions .github/scripts/prepare-microvm-development-release.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#!/bin/sh
set -eu
umask 022
# ponytail: macOS bsdtar embeds AppleDouble "._*" sidecar entries for any
# xattr it finds (e.g. the com.apple.provenance macOS stamps on most files)
# unless this is set; harmless no-op on GNU tar/Linux.
export COPYFILE_DISABLE=1

if [ "$#" -ne 1 ] || [ -z "$1" ]; then
echo "usage: $0 SOURCE_BUILD_IDENTITY" >&2
Expand All @@ -10,7 +14,9 @@ source_build_identity=$1
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) platform=linux-amd64 ;;
*) echo "microVM development releases require Linux amd64" >&2; exit 1 ;;
Linux-aarch64|Linux-arm64) platform=linux-arm64 ;;
Darwin-arm64) platform=darwin-arm64 ;;
*) echo "microVM development releases require Linux amd64/arm64 or macOS arm64" >&2; exit 1 ;;
esac

"$repo_root/environment/microvm/e2e/prepare.sh"
Expand All @@ -20,8 +26,10 @@ rm -rf "$output"
mkdir -p "$output"
bundle="$output/mecatl-microvm-development-$platform.tar.gz"
members="$output/bundle-members"
find "$prepared/package" -mindepth 1 -printf '%P\0' | LC_ALL=C sort -z >"$members"
tar --no-recursion -C "$prepared/package" --null --verbatim-files-from --files-from="$members" -cf - | gzip -n >"$bundle"
# ponytail: -printf is GNU-find-only (missing on macOS/BSD find); cd + relative
# find + sed strip works on both and the package dir is flat (no subdirs/spaces).
(cd "$prepared/package" && find . -mindepth 1 -type f) | sed 's#^\./##' | LC_ALL=C sort >"$members"
tar -C "$prepared/package" -T "$members" -cf - | gzip -n >"$bundle"
rm -f "$members"
chmod 0600 "$bundle"
key="$output/publisher.pub"
Expand All @@ -34,12 +42,12 @@ sha256_file() {
bundle_sha=$(sha256_file "$bundle")
key_sha=$(sha256_file "$key")
descriptor="$output/release.json"
python3 - "$descriptor" "$source_build_identity" "$bundle" "$bundle_sha" "$key" "$key_sha" <<'PY'
python3 - "$descriptor" "$platform" "$source_build_identity" "$bundle" "$bundle_sha" "$key" "$key_sha" <<'PY'
import json, sys
path, source, bundle, bundle_sha, key, key_sha = sys.argv[1:]
path, platform, source, bundle, bundle_sha, key, key_sha = sys.argv[1:]
value = {
"schema": "mecatl-microvm-development-release/v1",
"platform": "linux-amd64",
"platform": platform,
"source_build_identity": source,
"bundle_path": bundle,
"bundle_sha256": bundle_sha,
Expand Down
12 changes: 10 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,18 @@ tasks:
- tar -C bin/microvm/guest-agent-artifact -cf bin/microvm/mecatl-guest-agent-artifact.tar mecatl-guest-agent

microvm:dev:prepare:
desc: Prepare an unsupported signed local development release under .scratch (Linux amd64; network/tooling required).
desc: Prepare an unsupported signed local development release under .scratch (Linux/macOS amd64/arm64; network/tooling required).
cmds:
- .github/scripts/prepare-microvm-development-release.sh "{{.BUILD_ID}}"
- MECATL_MICROVM_DEV_RELEASE_DESCRIPTOR="$(pwd)/.scratch/microvm-dev/linux-amd64/release.json" go test -tags=microvm_dev -run '^TestPreparedDevelopmentReleaseBundleIsImportable$' ./internal/adapter/microvmmanager
- cmd: |
set -eu
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) platform=linux-amd64 ;;
Linux-aarch64|Linux-arm64) platform=linux-arm64 ;;
Darwin-arm64) platform=darwin-arm64 ;;
*) echo "microVM development releases require Linux amd64/arm64 or macOS arm64" >&2; exit 1 ;;
esac
MECATL_MICROVM_DEV_RELEASE_DESCRIPTOR="$(pwd)/.scratch/microvm-dev/${platform}/release.json" go test -tags=microvm_dev -run '^TestPreparedDevelopmentReleaseBundleIsImportable$' ./internal/adapter/microvmmanager

microvm:dev:build:
desc: Build microvm_dev-tagged mecated and mecatui binaries under .scratch; ordinary task build is unchanged.
Expand Down
38 changes: 21 additions & 17 deletions internal/adapter/microvmmanager/default_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (o *DefaultOperations) platform() (string, string) {
func (o *DefaultOperations) Preflight(ctx context.Context, _ Paths) error {
goos, goarch := o.platform()
if !supportedPlatform(goos, goarch) {
return fmt.Errorf("%w: microvm-local supports Linux amd64 with KVM only; use host-local on this host", ErrUnsupportedPlatform)
return fmt.Errorf("%w: microvm-local supports Linux amd64/arm64 with KVM, or macOS 15+ Apple Silicon with Hypervisor.framework; use host-local on this host", ErrUnsupportedPlatform)
}
if _, err := exec.LookPath("git"); err != nil {
return errors.New("git is required for microVM worktrees")
Expand Down Expand Up @@ -110,7 +110,7 @@ func (o *DefaultOperations) Preflight(ctx context.Context, _ Paths) error {
}

func supportedPlatform(goos, goarch string) bool {
return goos == "linux" && goarch == "amd64"
return (goos == "linux" && (goarch == "amd64" || goarch == "arm64")) || (goos == "darwin" && goarch == "arm64")
}

func darwinMajor(version string) int {
Expand All @@ -119,14 +119,16 @@ func darwinMajor(version string) int {
}

// Download obtains, digest-verifies, and safely extracts one release bundle.
func (o *DefaultOperations) Download(ctx context.Context, release Release, destination string) (string, error) { //nolint:gocyclo // explicit fail-closed download transaction
// root is the manager's already-secured boundary (paths.DataDir) that
// destination is created under.
func (o *DefaultOperations) Download(ctx context.Context, release Release, root, destination string) (string, error) { //nolint:gocyclo // explicit fail-closed download transaction
if release.bundlePath == "" {
u, err := url.Parse(release.URL)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.Fragment != "" {
return "", errors.New("release URL must be an absolute HTTPS URL without userinfo or fragment")
}
}
if err := secureMkdirAll(destination); err != nil {
if err := secureMkdirAll(root, destination); err != nil {
return "", err
}
archivePath := filepath.Join(destination, "release.tar.gz")
Expand Down Expand Up @@ -213,10 +215,10 @@ func (o *DefaultOperations) Download(ctx context.Context, release Release, desti
if err := os.RemoveAll(unpacked); err != nil {
return "", err
}
if err := secureMkdirAll(unpacked); err != nil {
if err := secureMkdirAll(root, unpacked); err != nil {
return "", err
}
if err := extractReleaseBundle(archivePath, unpacked); err != nil {
if err := extractReleaseBundle(root, archivePath, unpacked); err != nil {
_ = os.RemoveAll(unpacked)
return "", err
}
Expand All @@ -229,7 +231,7 @@ func (o *DefaultOperations) Download(ctx context.Context, release Release, desti
return manifest, nil
}

func extractReleaseBundle(archivePath, destination string) error { //nolint:gocyclo // archive safety checks remain explicit
func extractReleaseBundle(root, archivePath, destination string) error { //nolint:gocyclo // archive safety checks remain explicit
file, err := os.Open(archivePath)
if err != nil {
return err
Expand Down Expand Up @@ -264,14 +266,14 @@ func extractReleaseBundle(archivePath, destination string) error { //nolint:gocy
}
switch header.Typeflag {
case tar.TypeDir:
if err := secureMkdirAll(target); err != nil {
if err := secureMkdirAll(root, target); err != nil {
return err
}
case tar.TypeReg:
if header.Mode < 0 || header.Mode > 0o777 {
return fmt.Errorf("invalid release bundle mode for %s", header.Name)
}
if err := secureMkdirAll(filepath.Dir(target)); err != nil {
if err := secureMkdirAll(root, filepath.Dir(target)); err != nil {
return err
}
out, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.FileMode(header.Mode)&0o700) // #nosec G115 -- bounded to Unix permission bits above.
Expand Down Expand Up @@ -312,14 +314,16 @@ func (*DefaultOperations) Verify(_ context.Context, release Release, manifest st
return nil
}

// Install executes the installer from the verified bundle.
func (o *DefaultOperations) Install(ctx context.Context, manifest, installRoot string) (InstalledArtifacts, error) {
// Install executes the installer from the verified bundle. root is the
// manager's already-secured boundary (paths.DataDir) that manifest and
// installRoot are nested under.
func (o *DefaultOperations) Install(ctx context.Context, manifest, root, installRoot string) (InstalledArtifacts, error) {
installerData, err := readBundleInstaller(manifest)
if err != nil {
return InstalledArtifacts{}, err
}
installer := filepath.Join(filepath.Dir(manifest), ".bundle-installer")
if err := atomicWriteMode(installer, installerData, 0o700); err != nil {
if err := atomicWriteMode(root, installer, installerData, 0o700); err != nil {
return InstalledArtifacts{}, fmt.Errorf("materialize verified installer: %w", err)
}
cmd := scrubbedCommand(exec.CommandContext(ctx, installer, manifest, installRoot)) // #nosec G204 -- executable and each argument are distinct verified paths.
Expand All @@ -340,7 +344,7 @@ func (o *DefaultOperations) Install(ctx context.Context, manifest, installRoot s
goos, goarch := o.platform()
source := filepath.Join(filepath.Dir(manifest), "mecatl-microvmd-"+goos+"-"+goarch)
target := filepath.Join(filepath.Dir(installRoot), "bin", "mecatl-microvmd")
if err := copyVerifiedExecutable(source, target); err != nil {
if err := copyVerifiedExecutable(root, source, target); err != nil {
return InstalledArtifacts{}, fmt.Errorf("install verified microvmd binary: %w", err)
}
return installed, nil
Expand Down Expand Up @@ -385,15 +389,15 @@ func readBundleInstaller(manifest string) ([]byte, error) {
return nil, errors.New("verified release bundle omitted its installer")
}

func copyVerifiedExecutable(source, target string) error {
func copyVerifiedExecutable(root, source, target string) error {
info, err := os.Lstat(source)
if err != nil {
return err
}
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() > 256<<20 {
return errors.New("verified microvmd binary is not a bounded regular file")
}
if err := secureMkdirAll(filepath.Dir(target)); err != nil {
if err := secureMkdirAll(root, filepath.Dir(target)); err != nil {
return err
}
in, err := os.Open(source)
Expand Down Expand Up @@ -467,7 +471,7 @@ func (*DefaultOperations) Start(_ context.Context, paths Paths) error {
return err
}
pid := []byte(strconv.Itoa(cmd.Process.Pid) + "\n")
if err := atomicWrite(filepath.Join(paths.StateDir, "microvmd.pid"), pid); err != nil {
if err := atomicWrite(paths.StateDir, filepath.Join(paths.StateDir, "microvmd.pid"), pid); err != nil {
_ = cmd.Process.Kill()
_ = log.Close()
return err
Expand All @@ -490,7 +494,7 @@ func (*DefaultOperations) Start(_ context.Context, paths Paths) error {
_ = log.Close()
return err
}
if err := atomicWrite(filepath.Join(paths.StateDir, "microvmd.process.json"), append(record, '\n')); err != nil {
if err := atomicWrite(paths.StateDir, filepath.Join(paths.StateDir, "microvmd.process.json"), append(record, '\n')); err != nil {
_ = cmd.Process.Kill()
_ = log.Close()
return err
Expand Down
25 changes: 15 additions & 10 deletions internal/adapter/microvmmanager/default_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ func TestDefaultOperationsPreflightInvokesAndPropagatesUserNamespaceProbe(t *tes
}

func TestMicroVMLivePlatformBoundary(t *testing.T) {
if !supportedPlatform("linux", "amd64") {
t.Fatal("Linux amd64 live platform was rejected")
for _, platform := range [][2]string{{"linux", "amd64"}, {"linux", "arm64"}, {"darwin", "arm64"}} {
if !supportedPlatform(platform[0], platform[1]) {
t.Fatalf("supported live platform rejected: %s/%s", platform[0], platform[1])
}
}
for _, platform := range [][2]string{{"linux", "arm64"}, {"darwin", "arm64"}, {"windows", "amd64"}} {
for _, platform := range [][2]string{{"darwin", "amd64"}, {"windows", "amd64"}} {
if supportedPlatform(platform[0], platform[1]) {
t.Fatalf("unsupported live platform accepted: %s/%s", platform[0], platform[1])
}
Expand Down Expand Up @@ -241,8 +243,9 @@ func TestMicroVMUserBootstrap_Scenario7_DownloadVerifiesBeforeExtraction(t *test
defer srv.Close()
digest := fmt.Sprintf("%x", sha256.Sum256(bundle))
ops := &DefaultOperations{HTTPClient: srv.Client(), GOOS: "linux", GOARCH: "amd64"}
dest := filepath.Join(t.TempDir(), "download")
manifest, err := ops.Download(context.Background(), Release{URL: srv.URL, SHA256: digest}, dest)
destRoot := t.TempDir()
dest := filepath.Join(destRoot, "download")
manifest, err := ops.Download(context.Background(), Release{URL: srv.URL, SHA256: digest}, destRoot, dest)
if err != nil {
t.Fatal(err)
}
Expand All @@ -253,8 +256,9 @@ func TestMicroVMUserBootstrap_Scenario7_DownloadVerifiesBeforeExtraction(t *test
t.Fatalf("manifest was not safely extracted: %s", manifest)
}

badDest := filepath.Join(t.TempDir(), "bad")
if _, err := ops.Download(context.Background(), Release{URL: srv.URL, SHA256: strings.Repeat("0", 64)}, badDest); err == nil {
badRoot := t.TempDir()
badDest := filepath.Join(badRoot, "bad")
if _, err := ops.Download(context.Background(), Release{URL: srv.URL, SHA256: strings.Repeat("0", 64)}, badRoot, badDest); err == nil {
t.Fatal("digest mismatch was accepted")
}
if _, err := os.Stat(filepath.Join(badDest, "unpacked")); !os.IsNotExist(err) {
Expand Down Expand Up @@ -285,7 +289,7 @@ func TestMicroVMFirstRunRepair_InstallerComesFromVerifiedBundle(t *testing.T) {
if err := os.MkdirAll(installRoot, 0o700); err != nil {
t.Fatal(err)
}
installed, err := (&DefaultOperations{}).Install(context.Background(), manifest, installRoot)
installed, err := (&DefaultOperations{}).Install(context.Background(), manifest, root, installRoot)
if err != nil {
t.Fatalf("Install with verified bundled installer: %v", err)
}
Expand All @@ -311,7 +315,8 @@ func TestMicroVMUserBootstrap_Scenario8_BundleSymlinkIsRejected(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(bundle) }))
defer srv.Close()
ops := &DefaultOperations{HTTPClient: srv.Client(), GOOS: "linux", GOARCH: "amd64"}
_, err := ops.Download(context.Background(), Release{URL: srv.URL, SHA256: fmt.Sprintf("%x", sha256.Sum256(bundle))}, filepath.Join(t.TempDir(), "download"))
downloadRoot := t.TempDir()
_, err := ops.Download(context.Background(), Release{URL: srv.URL, SHA256: fmt.Sprintf("%x", sha256.Sum256(bundle))}, downloadRoot, filepath.Join(downloadRoot, "download"))
if err == nil || !strings.Contains(err.Error(), "symlink") {
t.Fatalf("error = %v, want symlink refusal", err)
}
Expand All @@ -337,7 +342,7 @@ func TestMicroVMUsabilityRepair_Scenario2_InstallerComesFromVerifiedBundle(t *te

installRoot := filepath.Join(root, "install", "artifacts")
ops := &DefaultOperations{GOOS: "linux", GOARCH: "amd64"}
if _, err := ops.Install(context.Background(), manifest, installRoot); err != nil {
if _, err := ops.Install(context.Background(), manifest, root, installRoot); err != nil {
t.Fatalf("install with packaged bundle member: %v", err)
}
if _, err := os.Stat(packagedMarker); err != nil {
Expand Down
Loading
Loading