diff --git a/app/test/backend-integration/setup-target.sh b/app/test/backend-integration/setup-target.sh index 0253b77d..2fb151c2 100755 --- a/app/test/backend-integration/setup-target.sh +++ b/app/test/backend-integration/setup-target.sh @@ -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" \ diff --git a/app/test/backend-integration/write-generated-config.ts b/app/test/backend-integration/write-generated-config.ts index 087a33c0..3d67554b 100644 --- a/app/test/backend-integration/write-generated-config.ts +++ b/app/test/backend-integration/write-generated-config.ts @@ -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: { diff --git a/app/test/integration/infra/docker-compose.yml b/app/test/integration/infra/docker-compose.yml index 46729ffd..a603a2ad 100644 --- a/app/test/integration/infra/docker-compose.yml +++ b/app/test/integration/infra/docker-compose.yml @@ -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: diff --git a/app/test/integration/infra/nfs/Dockerfile b/app/test/integration/infra/nfs/Dockerfile new file mode 100644 index 00000000..6eecad1e --- /dev/null +++ b/app/test/integration/infra/nfs/Dockerfile @@ -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"] diff --git a/app/test/integration/infra/nfs/entrypoint.sh b/app/test/integration/infra/nfs/entrypoint.sh new file mode 100644 index 00000000..de61944e --- /dev/null +++ b/app/test/integration/infra/nfs/entrypoint.sh @@ -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 </dev/null; do + sleep 1 +done diff --git a/app/test/integration/run.sh b/app/test/integration/run.sh index 40c213f7..636a7ff8 100644 --- a/app/test/integration/run.sh +++ b/app/test/integration/run.sh @@ -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 diff --git a/app/test/integration/src/helpers/nfs.ts b/app/test/integration/src/helpers/nfs.ts new file mode 100644 index 00000000..f47fbd7f --- /dev/null +++ b/app/test/integration/src/helpers/nfs.ts @@ -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, +}); diff --git a/app/test/integration/src/volume-backends.test.ts b/app/test/integration/src/volume-backends.test.ts index 83dba5f3..7d8f6a37 100644 --- a/app/test/integration/src/volume-backends.test.ts +++ b/app/test/integration/src/volume-backends.test.ts @@ -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,