fix: re-init repo on setup

This commit is contained in:
Nicolas Meienberger 2026-05-17 12:44:11 +02:00
parent 2cf95685f7
commit 7476fcb261
No known key found for this signature in database
9 changed files with 239 additions and 440 deletions

View file

@ -97,7 +97,6 @@ COPY . .
RUN bun run build
RUN bun build apps/agent/src/index.ts --outfile .output/agent/index.mjs --target bun
RUN bun run build:backend-integration
FROM base AS production
@ -124,7 +123,3 @@ COPY ./LICENSE ./LICENSE.md
EXPOSE 4096
CMD ["bun", ".output/server/index.mjs"]
FROM production AS backend-integration
CMD ["bun", ".output/backend-integration/index.js"]

View file

@ -0,0 +1,47 @@
FROM oven/bun:1.3.13-alpine@sha256:4de475389889577f346c636f956b42a5c31501b654664e9ae5726f94d7bb5349
ARG RESTIC_VERSION="0.18.1"
ARG RCLONE_VERSION="1.74.0"
ARG SHOUTRRR_VERSION="0.14.3"
ARG TARGETARCH
WORKDIR /app
RUN apk update --no-cache && \
apk upgrade --no-cache && \
apk add --no-cache acl attr cifs-utils curl bzip2 unzip tar davfs2=1.6.1-r2 openssh-client fuse3 sshfs tini tzdata
COPY ./package.json ./bun.lock ./
COPY ./packages/core/package.json ./packages/core/package.json
COPY ./packages/contracts/package.json ./packages/contracts/package.json
COPY ./apps/agent/package.json ./apps/agent/package.json
COPY ./apps/docs/package.json ./apps/docs/package.json
RUN VITE_GIT_HOOKS=0 bun install --frozen-lockfile
COPY . .
RUN bun run build:backend-integration
RUN echo "Building for ${TARGETARCH}" && if [ "${TARGETARCH}" = "arm64" ]; then \
curl -fL -o restic.bz2 "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_arm64.bz2"; \
curl -fL -o rclone.zip "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-arm64.zip"; \
unzip rclone.zip; \
curl -fL -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v${SHOUTRRR_VERSION}/shoutrrr_linux_arm64v8_${SHOUTRRR_VERSION}.tar.gz"; \
elif [ "${TARGETARCH}" = "amd64" ]; then \
curl -fL -o restic.bz2 "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2"; \
curl -fL -o rclone.zip "https://github.com/rclone/rclone/releases/download/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-amd64.zip"; \
unzip rclone.zip; \
curl -fL -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v${SHOUTRRR_VERSION}/shoutrrr_linux_amd64_${SHOUTRRR_VERSION}.tar.gz"; \
else \
echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; \
exit 1; \
fi
RUN bzip2 -d restic.bz2 && install -m 0755 restic /usr/local/bin/restic
RUN mv rclone-v*-linux-*/rclone /usr/local/bin/rclone && chmod +x /usr/local/bin/rclone
RUN tar -xzf shoutrrr.tar.gz && install -m 0755 shoutrrr /usr/local/bin/shoutrrr
ENTRYPOINT ["/sbin/tini", "-s", "--"]
CMD ["bun", ".output/backend-integration/index.js"]

View file

@ -11,7 +11,7 @@ if [[ ! -f "$config_path" ]]; then
if [[ "$config_path" == "$default_config_path" ]]; then
echo "Generated config not found: $config_path" >&2
echo "Run the target bootstrap first:" >&2
echo " bash app/test/backend-integration/setup-debian-target.sh" >&2
echo " bash app/test/backend-integration/setup-target.sh" >&2
else
echo "Config file not found: $config_path" >&2
fi
@ -25,7 +25,7 @@ else
config_path="$config_dir/$(basename "$config_path")"
fi
docker build --target backend-integration -t "$image_tag" "$repo_root"
docker build -f "$script_dir/Dockerfile" -t "$image_tag" "$repo_root"
docker run --rm \
--cap-add SYS_ADMIN \
--device /dev/fuse:/dev/fuse \

View file

@ -172,9 +172,27 @@ async function restoreSnapshot(
}
async function cleanupScenario(backend: VolumeBackend, restoreTarget: string, scenarioWorkspace: string) {
await backend.unmount();
await fs.rm(restoreTarget, { recursive: true, force: true });
await fs.rm(scenarioWorkspace, { recursive: true, force: true });
const cleanupErrors: string[] = [];
const unmountResult = await backend.unmount();
const didUnmount = unmountResult.status !== "error";
if (!didUnmount) {
cleanupErrors.push(unmountResult.error ?? `Unmount returned ${unmountResult.status}`);
}
await fs.rm(restoreTarget, { recursive: true, force: true }).catch((error) => {
cleanupErrors.push(`Failed to remove restore target: ${formatError(error)}`);
});
if (didUnmount) {
await fs.rm(scenarioWorkspace, { recursive: true, force: true }).catch((error) => {
cleanupErrors.push(`Failed to remove scenario workspace: ${formatError(error)}`);
});
}
if (cleanupErrors.length > 0) {
throw new Error(cleanupErrors.join("; "));
}
}
async function runScenario(scenario: IntegrationScenario, runId: string): Promise<ScenarioReport> {
@ -256,16 +274,30 @@ async function runScenario(scenario: IntegrationScenario, runId: string): Promis
await runStage(stages, "verify-restore", async () => {
await verifyFilesystemEntries(restoreTarget, scenario.expectedEntries, "restored output");
});
logScenario(scenario.id, "passed");
} catch (error) {
status = "failed";
errorMessage = formatError(error);
logScenario(scenario.id, `failed: ${errorMessage}`);
} finally {
await runStage(stages, "cleanup", async () => {
await cleanupScenario(backend, restoreTarget, scenarioWorkspace);
});
try {
await runStage(stages, "cleanup", async () => {
await cleanupScenario(backend, restoreTarget, scenarioWorkspace);
});
} catch (error) {
status = "failed";
const cleanupError = formatError(error);
if (!errorMessage) {
errorMessage = cleanupError;
logScenario(scenario.id, `failed: ${cleanupError}`);
} else {
logScenario(scenario.id, `cleanup failed: ${cleanupError}`);
}
}
}
if (status === "passed") {
logScenario(scenario.id, "passed");
}
return {

View file

@ -42,19 +42,19 @@ function assertMetadata(
expected: ExpectedEntry,
actual: { uid?: number; gid?: number; mode?: number },
) {
if (expected.uid && actual.uid !== expected.uid) {
if (expected.uid !== undefined && actual.uid !== expected.uid) {
throw new Error(`${entryLabel} uid mismatch: expected ${expected.uid}, got ${String(actual.uid)}`);
}
if (expected.gid && actual.gid !== expected.gid) {
if (expected.gid !== undefined && actual.gid !== expected.gid) {
throw new Error(`${entryLabel} gid mismatch: expected ${expected.gid}, got ${String(actual.gid)}`);
}
if (!expected.mode) {
if (expected.mode === undefined) {
return;
}
if (!actual.mode) {
if (actual.mode === undefined) {
throw new Error(`${entryLabel} mode is missing`);
}

View file

@ -54,6 +54,9 @@ sftp_password="$4"
webdav_password="$5"
restic_password="$6"
public_key="$(printf '%s' "$7" | base64 -d)"
repo_path="/srv/zerobyte-backend-integration/restic-repo"
repo_password_fingerprint_path="$repo_path/.zerobyte-password-sha256"
repo_password_fingerprint="$(printf '%s' "$restic_password" | sha256sum | cut -d' ' -f1)"
export DEBIAN_FRONTEND=noninteractive
@ -62,6 +65,23 @@ write_file() {
cat >"$file_path"
}
initialize_restic_repo() {
local password_file
rm -rf "$repo_path"
install -d -o zerobyte-sftp -g zerobyte-sftp -m 0700 "$repo_path"
password_file="$(mktemp)"
printf '%s\n' "$restic_password" >"$password_file"
chown zerobyte-sftp:zerobyte-sftp "$password_file"
chmod 0600 "$password_file"
su -s /bin/sh -c "restic init --repo '$repo_path' --password-file '$password_file'" zerobyte-sftp
rm -f "$password_file"
printf '%s\n' "$repo_password_fingerprint" >"$repo_password_fingerprint_path"
chmod 0600 "$repo_password_fingerprint_path"
}
apt-get update
apt-get install -y apache2 apache2-utils nfs-kernel-server openssh-server restic rpcbind samba
@ -75,7 +95,6 @@ chown -R "$fixture_uid:$fixture_gid" /srv/zerobyte-backend-integration/fixtures
find /srv/zerobyte-backend-integration/fixtures -type d -exec chmod 0755 {} +
find /srv/zerobyte-backend-integration/fixtures -type f -exec chmod 0644 {} +
install -d -o zerobyte-sftp -g zerobyte-sftp -m 0700 /srv/zerobyte-backend-integration/restic-repo
install -d -o zerobyte-sftp -g zerobyte-sftp -m 0700 /home/zerobyte-sftp
install -d -o zerobyte-sftp -g zerobyte-sftp -m 0700 /home/zerobyte-sftp/.ssh
printf '%s\n' "$public_key" >/home/zerobyte-sftp/.ssh/authorized_keys
@ -88,13 +107,10 @@ printf 'zerobyte-sftp:%s\n' "$sftp_password" | chpasswd
passwd -u zerobyte-sftp >/dev/null 2>&1 || true
htpasswd -bc /etc/apache2/zerobyte-backend-integration.htpasswd zerobyte-webdav "$webdav_password" >/dev/null
if [[ ! -f /srv/zerobyte-backend-integration/restic-repo/config ]]; then
password_file="$(mktemp)"
printf '%s\n' "$restic_password" >"$password_file"
chown zerobyte-sftp:zerobyte-sftp "$password_file"
chmod 0600 "$password_file"
su -s /bin/sh -c "restic init --repo /srv/zerobyte-backend-integration/restic-repo --password-file '$password_file'" zerobyte-sftp
rm -f "$password_file"
if [[ ! -f "$repo_path/config" ]]; then
initialize_restic_repo
elif [[ ! -f "$repo_password_fingerprint_path" ]] || [[ "$(cat "$repo_password_fingerprint_path")" != "$repo_password_fingerprint" ]]; then
initialize_restic_repo
fi
write_file /etc/exports <<'EOF'

View file

@ -27,6 +27,12 @@ const nfsEntries = [
{ path: "docs/readme.md", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: readmeText },
];
const smbEntries = [
{ path: "hello.txt", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: fileText },
{ path: "docs", type: "directory", uid: fixtureUid, gid: fixtureGid, mode: "0755" },
{ path: "docs/readme.md", type: "file", uid: fixtureUid, gid: fixtureGid, mode: "0644", text: readmeText },
];
const contentOnlyEntries = [
{ path: "hello.txt", type: "file", text: fileText },
{ path: "docs", type: "directory" },
@ -58,13 +64,14 @@ const config = {
share: "zerobyte-backend-integration",
username: "zerobyte-smb",
password: smbPassword,
mapToContainerUidGid: false,
vers: "3.0",
port: 445,
readOnly: true,
},
repository: { backend: "local", path: "repo-smb" },
fixtureRoot: "case-a",
expectedEntries: nfsEntries,
expectedEntries: smbEntries,
},
{
id: "sftp-local-repo",

524
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,8 @@
"check": "vp check",
"dev": "NODE_ENV=development bunx --bun vite",
"build": "bunx --bun vite build",
"build:backend-integration": "bun build app/test/backend-integration/index.ts --outdir .output/backend-integration --target bun",
"start": "bun run .output/server/index.mjs",
"build:backend-integration": "bun build app/test/backend-integration/index.ts --outdir .output/backend-integration --target bun",
"test:integration:backends": "bash app/test/backend-integration/run.sh",
"preview": "bunx --bun vite preview",
"cli:dev": "bun run app/server/cli/main.ts",