From 26984db5a6205156c4b9a9f3308d6eb95b68d67f Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Tue, 21 Jul 2026 18:13:42 -0300 Subject: [PATCH] fix(k8s): support control-plane tests on slim runner --- .../workflows/test-automated-nfs-backup.yaml | 2 + k8s/README.md | 2 +- k8s/backup-k3s-control-plane-config.sh | 4 ++ .../test-backup-k3s-control-plane-config.sh | 40 ++++++++++++++++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/test-automated-nfs-backup.yaml b/.gitea/workflows/test-automated-nfs-backup.yaml index 1f3b14a..7b556f9 100644 --- a/.gitea/workflows/test-automated-nfs-backup.yaml +++ b/.gitea/workflows/test-automated-nfs-backup.yaml @@ -20,5 +20,7 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@v4 + - name: Install GNU coreutils and findutils for control-plane tests + run: apk add --no-cache coreutils findutils - name: Run k3s control-plane configuration backup tests run: bash k8s/test/test-backup-k3s-control-plane-config.sh diff --git a/k8s/README.md b/k8s/README.md index 17a1c64..63d9865 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -33,7 +33,7 @@ Useful overrides: - Source paths: `K3S_CONFIG_DIR`, `K3S_SERVER_DIR`, `K3S_SERVICE_FILES` (colon-separated service configuration files). - Destination and identity: `REMOTE_USER`, `REMOTE_HOST`, `REMOTE_DIR`, `REMOTE_IDENTITY_FILE`, `SSH_PORT`. - Archive and locking: `ARCHIVE_PREFIX`, `ARCHIVE_TS_FORMAT`, `LOCAL_TMP_PARENT`, `LOCK_PATH`, `CLEANUP_KEEP_COUNT` (default `4`; `0` disables deletion). -- Binaries: `ZIP_BIN`, `UNZIP_BIN`, `SSH_BIN`, `SCP_BIN`, `CURL_BIN`, `FLOCK_BIN`, `MKTEMP_BIN`, `DATE_BIN`, `REMOTE_BASH_BIN`. +- Binaries: `ZIP_BIN`, `UNZIP_BIN`, `SSH_BIN`, `SCP_BIN`, `CURL_BIN`, `FLOCK_BIN`, `MKTEMP_BIN`, `DATE_BIN`, `REALPATH_BIN`, `REMOTE_BASH_BIN`. `REALPATH_BIN` must provide GNU `realpath` `-e` and `-m` options. - Notifications: `NOTIFY_SUCCESS_URL` (default `http://notify.haven/template/notify/backup`), `NOTIFY_FAILURE_URL` (default `http://notify.haven/template/notify/error`), `NOTIFY_TITLE`, `NOTIFY_ASSET`, and `NOTIFY_CALLER`. Set either URL to an empty value to disable it. Failed notifications only generate a warning; they do not change an otherwise successful backup result. ## Environment Variables diff --git a/k8s/backup-k3s-control-plane-config.sh b/k8s/backup-k3s-control-plane-config.sh index 0036de8..ce4be55 100755 --- a/k8s/backup-k3s-control-plane-config.sh +++ b/k8s/backup-k3s-control-plane-config.sh @@ -238,6 +238,10 @@ add_archive_entry_if_present() { local relative_path local protected_path + if [[ "$absolute_path" =~ (^|/)(\.|\.\.)(/|$) ]]; then + die "Configured source path is not safe to archive" + fi + if [[ ! -e "$absolute_path" && ! -L "$absolute_path" ]]; then return 0 fi diff --git a/k8s/test/test-backup-k3s-control-plane-config.sh b/k8s/test/test-backup-k3s-control-plane-config.sh index 2d29460..e7bf5aa 100755 --- a/k8s/test/test-backup-k3s-control-plane-config.sh +++ b/k8s/test/test-backup-k3s-control-plane-config.sh @@ -35,6 +35,7 @@ path_exists() { [[ -e "$1" ]]; } path_does_not_exist() { [[ ! -e "$1" ]]; } file_contains() { grep -Fq -- "$2" "$1"; } file_lacks() { [[ ! -e "$1" ]] || ! grep -Fq -- "$2" "$1"; } +file_is_empty() { [[ ! -s "$1" ]]; } file_line_count_is() { [[ "$(grep -Fc -- "$2" "$1")" -eq "$3" ]]; } matching_archive_count_is() { @@ -278,6 +279,9 @@ newline_archive="${success_remote}/k3s-control-plane-config_2026-06-01_12-00-00. touch -t "202606011200" "$newline_archive" : > "$newline_archive" touch -t "202606011200" "$newline_archive" +tab_archive="${success_remote}/k3s-control-plane-config_2026-05-01_12-00-00.zip"$'\tlegacy.zip' +: > "$tab_archive" +touch -t "202605011200" "$tab_archive" : > "${success_remote}/unrelated.zip" : > "${TMPD}/curl_trace.log" : > "${TMPD}/unzip_trace.log" @@ -303,6 +307,7 @@ assert "retention leaves exactly four newest matching zip archives" matching_arc assert "retention removes the two oldest matching zip archives" path_does_not_exist "${success_remote}/k3s-control-plane-config_2026-07-01_12-00-00.zip" assert "retention removes the next-oldest matching zip archive" path_does_not_exist "${success_remote}/k3s-control-plane-config_2026-07-02_12-00-00.zip" assert "retention removes matching old archives with newlines in their filenames" path_does_not_exist "$newline_archive" +assert "retention removes matching old archives with tabs in their filenames" path_does_not_exist "$tab_archive" assert "retention keeps the standalone unrelated.zip sentinel" path_exists "${success_remote}/unrelated.zip" assert "retention keeps unrelated legacy .7z backups" path_exists "${success_remote}/k8s-backup_2026-07-01.7z" assert "retention keeps unrelated zip backups" path_exists "${success_remote}/other-prefix_2026-07-01.zip" @@ -332,12 +337,45 @@ set -e assert "unsafe source override exits non-zero" is_nonzero "$source_scope_exit_code" assert "unsafe source override reports the source-scope error" file_contains "${source_scope_dir}/run.log" "Configured source path is not safe to archive" -assert "unsafe source override is rejected before zip runs" file_lacks "${TMPD}/zip_invocations.log" "server/db/.." +assert "unsafe source override is rejected before zip runs" file_is_empty "${TMPD}/zip_invocations.log" assert "unsafe source override archive excludes server database content" file_lacks "$source_scope_archive" "state.db" assert "unsafe source override archive excludes kine socket" file_lacks "$source_scope_archive" "kine.sock" assert "unsafe source override does not publish an archive" directory_is_empty "$source_scope_remote" assert "unsafe source override removes local temporary state" directory_is_empty "$source_scope_local_tmp" +# The source path and the server directory can be overridden independently; +# protected database paths must still be derived from the configured source. +source_scope_mismatch_dir="${TMPD}/source-scope-mismatch" +source_scope_mismatch_fixture="${source_scope_mismatch_dir}/fixture" +source_scope_mismatch_remote="${source_scope_mismatch_dir}/remote" +source_scope_mismatch_local_tmp="${source_scope_mismatch_dir}/local-tmp" +source_scope_mismatch_unrelated_server="${source_scope_mismatch_fixture}/unrelated-server" +source_scope_mismatch_archive="${source_scope_mismatch_remote}/k3s-control-plane-config_2026-07-21_12-00-00.zip" +mkdir -p "$source_scope_mismatch_remote" "$source_scope_mismatch_local_tmp" +create_source_fixture "$source_scope_mismatch_fixture" +mkdir -p "$source_scope_mismatch_unrelated_server" +: > "${TMPD}/zip_invocations.log" + +set +e +run_backup \ + "$source_scope_mismatch_fixture" \ + "$source_scope_mismatch_remote" \ + "$source_scope_mismatch_local_tmp" \ + "${source_scope_mismatch_dir}/run.log" \ + "K3S_CONFIG_DIR=${source_scope_mismatch_fixture}/var/lib/rancher/k3s/server/db/.." \ + "K3S_SERVER_DIR=${source_scope_mismatch_unrelated_server}" +source_scope_mismatch_exit_code=$? +set -e + +assert "mismatched source override exits non-zero" is_nonzero "$source_scope_mismatch_exit_code" +assert "mismatched source override reports the source-scope error" file_contains "${source_scope_mismatch_dir}/run.log" "Configured source path is not safe to archive" +assert "mismatched source override is rejected before zip runs" file_is_empty "${TMPD}/zip_invocations.log" +assert "mismatched source override archive excludes server database content" file_lacks "$source_scope_mismatch_archive" "state.db" +assert "mismatched source override archive excludes kine socket" file_lacks "$source_scope_mismatch_archive" "kine.sock" +assert "mismatched source override does not publish an archive" path_does_not_exist "$source_scope_mismatch_archive" +assert "mismatched source override leaves the remote directory empty" directory_is_empty "$source_scope_mismatch_remote" +assert "mismatched source override removes local temporary state" directory_is_empty "$source_scope_mismatch_local_tmp" + # Upload failure: the partial remote temporary file is removed, no final archive # is published, local temporary data is removed, and one safe error notification # is attempted.