test: automate NFS integration

This commit is contained in:
Nicolas Meienberger 2026-06-02 20:36:33 +02:00
parent dfd787c8ae
commit 1526e3d441
No known key found for this signature in database
8 changed files with 89 additions and 56 deletions

View file

@ -4,8 +4,6 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET_HOST="192.168.2.41"
TARGET="root@$TARGET_HOST"
FIXTURE_UID="1000"
FIXTURE_GID="1000"
ARTIFACTS_DIR="$SCRIPT_DIR/artifacts/$TARGET_HOST"
KNOWN_HOSTS_PATH="$ARTIFACTS_DIR/known_hosts"
@ -30,12 +28,10 @@ chmod 700 "$ARTIFACTS_DIR"
SFTP_PASSWORD="$(read_or_create_secret "$SFTP_PASSWORD_FILE")"
ssh "$TARGET" bash -s -- "$FIXTURE_UID" "$FIXTURE_GID" "$SFTP_PASSWORD" <<'REMOTE'
ssh "$TARGET" bash -s -- "$SFTP_PASSWORD" <<'REMOTE'
set -euo pipefail
fixture_uid="$1"
fixture_gid="$2"
sftp_password="$3"
sftp_password="$1"
legacy_sshd_dir="/etc/ssh/zerobyte-backend-integration-legacy"
export DEBIAN_FRONTEND=noninteractive
@ -46,30 +42,19 @@ write_file() {
}
apt-get update
apt-get install -y nfs-kernel-server openssh-server rpcbind
apt-get install -y openssh-server
id -u zerobyte-sftp >/dev/null 2>&1 || useradd --create-home --home-dir /home/zerobyte-sftp --shell /bin/bash zerobyte-sftp
install -d -m 0755 /srv/zerobyte-backend-integration/fixtures/case-a/docs
printf 'hello from zerobyte integration\n' >/srv/zerobyte-backend-integration/fixtures/case-a/hello.txt
printf 'fixture documentation\n' >/srv/zerobyte-backend-integration/fixtures/case-a/docs/readme.md
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 {} +
printf 'zerobyte-sftp:%s\n' "$sftp_password" | chpasswd
passwd -u zerobyte-sftp >/dev/null 2>&1 || true
write_file /etc/exports <<'EOF'
/srv/zerobyte-backend-integration/fixtures *(ro,sync,no_subtree_check,insecure)
EOF
exportfs -ra
systemctl unmask rpcbind rpcbind.socket >/dev/null 2>&1
systemctl start rpcbind.socket
systemctl start rpcbind
systemctl start proc-fs-nfsd.mount
systemctl restart nfs-kernel-server
install -d -m 0700 "$legacy_sshd_dir"
if [[ ! -f "$legacy_sshd_dir/ssh_host_rsa_key" ]]; then
ssh-keygen -q -t rsa -b 2048 -N "" -f "$legacy_sshd_dir/ssh_host_rsa_key"
@ -143,8 +128,6 @@ if ! ssh-keyscan -T 5 -p 2222 "$TARGET_HOST" >>"$KNOWN_HOSTS_PATH" 2>/dev/null;
fi
INTEGRATION_HOST="$TARGET_HOST" \
FIXTURE_UID="$FIXTURE_UID" \
FIXTURE_GID="$FIXTURE_GID" \
SFTP_PASSWORD="$SFTP_PASSWORD" \
KNOWN_HOSTS_PATH="$KNOWN_HOSTS_PATH" \
CONFIG_PATH="$CONFIG_PATH" \

View file

@ -4,13 +4,7 @@ function getRequiredEnv(name: string) {
return process.env[name]!;
}
function getRequiredNumberEnv(name: string) {
return Number.parseInt(getRequiredEnv(name), 10);
}
const host = getRequiredEnv("INTEGRATION_HOST");
const fixtureUid = getRequiredNumberEnv("FIXTURE_UID");
const fixtureGid = getRequiredNumberEnv("FIXTURE_GID");
const sftpPassword = getRequiredEnv("SFTP_PASSWORD");
const knownHosts = fs.readFileSync(getRequiredEnv("KNOWN_HOSTS_PATH"), "utf8");
const configPath = getRequiredEnv("CONFIG_PATH");
@ -18,12 +12,6 @@ const configPath = getRequiredEnv("CONFIG_PATH");
const fileText = "hello from zerobyte integration\n";
const readmeText = "fixture documentation\n";
const nfsEntries = [
{ 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" },
@ -33,20 +21,6 @@ const contentOnlyEntries = [
const config = {
version: 1,
scenarios: [
{
id: "nfs-local-repo",
volume: {
backend: "nfs",
server: host,
exportPath: "/srv/zerobyte-backend-integration/fixtures",
port: 2049,
version: "4.1",
readOnly: true,
},
repository: { backend: "local", path: "repo-nfs" },
fixtureRoot: "case-a",
expectedEntries: nfsEntries,
},
{
id: "sftp-legacy-rsa-hostkey-local-repo",
volume: {

View file

@ -81,6 +81,18 @@ services:
timeout: 5s
retries: 30
nfs:
build:
context: ./nfs
privileged: true
volumes:
- nfs-data:/srv/zerobyte-integration
healthcheck:
test: ["CMD", "sh", "-c", "exportfs -v | grep -q /srv/zerobyte-integration/fixtures"]
interval: 1s
timeout: 5s
retries: 30
integration:
build:
context: ../../../..
@ -96,6 +108,8 @@ services:
condition: service_healthy
smb:
condition: service_healthy
nfs:
condition: service_healthy
cap_add:
- SYS_ADMIN
devices:
@ -111,3 +125,4 @@ services:
volumes:
rustfs-data:
sftp-data:
nfs-data:

View file

@ -0,0 +1,10 @@
FROM alpine:3.21
RUN apk add --no-cache nfs-utils
COPY entrypoint.sh /usr/local/bin/zerobyte-nfs-entrypoint
RUN chmod +x /usr/local/bin/zerobyte-nfs-entrypoint
EXPOSE 2049
ENTRYPOINT ["/usr/local/bin/zerobyte-nfs-entrypoint"]

View file

@ -0,0 +1,30 @@
#!/bin/sh
set -eu
FIXTURE_ROOT="/srv/zerobyte-integration/fixtures"
install -d -m 0755 "$FIXTURE_ROOT/case-a/docs"
printf 'hello from zerobyte integration\n' >"$FIXTURE_ROOT/case-a/hello.txt"
printf 'fixture documentation\n' >"$FIXTURE_ROOT/case-a/docs/readme.md"
find "$FIXTURE_ROOT" -type d -exec chmod 0755 {} +
find "$FIXTURE_ROOT" -type f -exec chmod 0644 {} +
mkdir -p /run/rpc_pipefs /proc/fs/nfsd
mountpoint -q /proc/fs/nfsd || mount -t nfsd nfsd /proc/fs/nfsd
mountpoint -q /run/rpc_pipefs || mount -t rpc_pipefs rpc_pipefs /run/rpc_pipefs
printf '1\n' >/proc/fs/nfsd/nfsv4gracetime
printf '1\n' >/proc/fs/nfsd/nfsv4leasetime
cat >/etc/exports <<EOF
$FIXTURE_ROOT *(ro,sync,no_subtree_check,insecure,fsid=0)
EOF
rpcbind -w
rpc.mountd --no-udp --foreground &
exportfs -ra
rpc.nfsd -V 4 -V 4.1 8
trap 'exportfs -ua; rpc.nfsd 0' INT TERM
while kill -0 "$(pidof rpc.mountd)" 2>/dev/null; do
sleep 1
done

View file

@ -36,7 +36,7 @@ if [[ "$exit_code" -eq 0 ]]; then
fi
if [[ "$exit_code" -eq 0 ]]; then
"${compose[@]}" up --build --no-color --detach --wait sftp webdav smb >>"$docker_output_log" 2>&1 || exit_code=$?
"${compose[@]}" up --build --no-color --detach --wait sftp webdav smb nfs >>"$docker_output_log" 2>&1 || exit_code=$?
fi
if [[ "$exit_code" -eq 0 ]]; then

View file

@ -0,0 +1,14 @@
import type { BackendConfig } from "@zerobyte/contracts/volumes";
export const NFS_HOST = "nfs";
export const NFS_PORT = 2049;
export const NFS_EXPORT_PATH = "/";
export const buildNfsVolumeConfig = (): BackendConfig => ({
backend: "nfs",
server: NFS_HOST,
exportPath: NFS_EXPORT_PATH,
port: NFS_PORT,
version: "4.1",
readOnly: true,
});

View file

@ -4,6 +4,7 @@ import path from "node:path";
import type { RepositoryConfig } from "@zerobyte/core/restic";
import { Effect } from "effect";
import { expect, test } from "vitest";
import { makeNfsBackend } from "../../../../apps/agent/src/volume-host/backends/nfs";
import { makeSmbBackend } from "../../../../apps/agent/src/volume-host/backends/smb";
import { makeSftpBackend } from "../../../../apps/agent/src/volume-host/backends/sftp";
import { makeWebdavBackend } from "../../../../apps/agent/src/volume-host/backends/webdav";
@ -11,6 +12,7 @@ import type { VolumeBackend } from "../../../../apps/agent/src/volume-host/types
import { INTEGRATION_ORGANIZATION_ID, INTEGRATION_RUNS_DIR } from "./constants";
import { assertFixtureSourceExists, assertRestoredFixture, assertSnapshotContainsFixture } from "./helpers/assertions";
import { createStaticVolumeFixture } from "./helpers/fixture";
import { buildNfsVolumeConfig } from "./helpers/nfs";
import { createIntegrationRestic } from "./helpers/restic";
import {
buildSftpPasswordVolumeConfig,
@ -57,6 +59,11 @@ const scenarios: VolumeScenario[] = [
name: "SMB volume with local repository",
createBackend: async (mountPath) => makeSmbBackend(buildSmbVolumeConfig(), mountPath),
},
{
id: "nfs-local-repo",
name: "NFS volume with local repository",
createBackend: async (mountPath) => makeNfsBackend(buildNfsVolumeConfig(), mountPath),
},
];
const volumeMountTest = process.env.SKIP_VOLUME_MOUNT_INTEGRATION_TESTS === "true" ? test.skip : test;
@ -97,6 +104,15 @@ volumeMountTest.concurrent.each(scenarios)("$name can backup and restore static
try {
await fs.mkdir(workspace, { recursive: true });
const initResult = await Effect.runPromise(
restic.init(repositoryConfig, {
organizationId: INTEGRATION_ORGANIZATION_ID,
timeoutMs: 120_000,
}),
);
expect(initResult.success).toBe(true);
expect(initResult.error).toBeNull();
backend = await scenario.createBackend(mountPath);
const mountResult = await backend.mount();
@ -108,15 +124,6 @@ volumeMountTest.concurrent.each(scenarios)("$name can backup and restore static
const fixture = createStaticVolumeFixture(path.join(mountPath, "case-a"));
await assertFixtureSourceExists(fixture);
const initResult = await Effect.runPromise(
restic.init(repositoryConfig, {
organizationId: INTEGRATION_ORGANIZATION_ID,
timeoutMs: 120_000,
}),
);
expect(initResult.success).toBe(true);
expect(initResult.error).toBeNull();
const backupResult = await Effect.runPromise(
restic.backup(repositoryConfig, fixture.sourceRoot, {
organizationId: INTEGRATION_ORGANIZATION_ID,