Add automatic component update checks

This commit is contained in:
ushan0v
2026-07-11 02:08:39 +03:00
parent 3292766afe
commit 6bfdca6229
25 changed files with 2014 additions and 1076 deletions
@@ -72,6 +72,14 @@ component_out="$(PODKOP_LIB="$fake_lib" ucode "$PODKOP_BIN" component_action sin
[ "$component_out" = $'action\tcomponent-action\tsing_box\tupdate' ] ||
fail "component_action must dispatch through components/action.uc with arguments"
component_check_cache_out="$(PODKOP_LIB="$fake_lib" ucode "$PODKOP_BIN" component_update_check_cache)"
[ "$component_check_cache_out" = $'updates\tcomponent-update-check-cache\t\t' ] ||
fail "component_update_check_cache must dispatch through components/updates.uc"
component_updates_out="$(PODKOP_LIB="$fake_lib" ucode "$PODKOP_BIN" component_updates_if_due)"
[ "$component_updates_out" = $'updates\tcomponent-updates-if-due\t\t' ] ||
fail "component_updates_if_due must dispatch through components/updates.uc"
rm -f "$fake_lib/service/lifecycle.uc"
set +e
PODKOP_TEST_DNS_RESTORE_MARKER="$WORK_DIR/dns-restore.marker" PODKOP_LIB="$fake_lib" \
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PODKOP_LIB="$ROOT_DIR/podkop/files/usr/lib"
UPDATES_UC="$PODKOP_LIB/components/updates.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
fake_lib="$WORK_DIR/lib"
cache_dir="$WORK_DIR/cache"
job_dir="$WORK_DIR/jobs"
state_file="$job_dir/manual.json"
output_file="$job_dir/manual.out"
timestamp_file="$WORK_DIR/component-update-check.timestamp"
mkdir -p \
"$fake_lib/core" \
"$fake_lib/config" \
"$fake_lib/components" \
"$fake_lib/service" \
"$fake_lib/providers/zapret" \
"$fake_lib/providers/zapret2" \
"$fake_lib/providers/byedpi" \
"$job_dir"
cat >"$fake_lib/core/uci.uc" <<'UCODE'
function get_all(_config, section) {
if (section == "settings") {
return {
component_update_check_enabled: getenv("TEST_COMPONENT_UPDATE_CHECK_ENABLED") || "0",
component_update_check_interval: getenv("TEST_COMPONENT_UPDATE_CHECK_INTERVAL") || "1d"
};
}
return {};
}
function section_objects(_config, _type) {
return [];
}
return {
get_all,
section_objects
};
UCODE
cat >"$fake_lib/config/connections.uc" <<'UCODE'
return {};
UCODE
cat >"$fake_lib/components/action.uc" <<'UCODE'
#!/usr/bin/env ucode
let component = ARGV[1] || "podkop";
let action = ARGV[2] || "check_update";
print(sprintf("%J\n", {
success: true,
component,
action,
message: "Update is available",
current_version: "1.0.0",
latest_version: "1.1.0",
release_url: "https://example.com/release",
changed: 0,
status: "outdated"
}));
UCODE
cat >"$fake_lib/providers/zapret/runtime.uc" <<'UCODE'
#!/usr/bin/env ucode
exit(1);
UCODE
cp "$fake_lib/providers/zapret/runtime.uc" "$fake_lib/providers/zapret2/runtime.uc"
cp "$fake_lib/providers/zapret/runtime.uc" "$fake_lib/providers/byedpi/runtime.uc"
cat >"$fake_lib/service/state.uc" <<'UCODE'
#!/usr/bin/env ucode
exit(0);
UCODE
updates_ucode() {
TEST_COMPONENT_UPDATE_CHECK_ENABLED="${TEST_COMPONENT_UPDATE_CHECK_ENABLED:-1}" \
TEST_COMPONENT_UPDATE_CHECK_INTERVAL="${TEST_COMPONENT_UPDATE_CHECK_INTERVAL:-1d}" \
PODKOP_LIB="$fake_lib" \
PODKOP_COMPONENT_UPDATE_CHECK_CACHE_DIR="$cache_dir" \
PODKOP_COMPONENT_UPDATE_CHECK_STATE_FILE="$timestamp_file" \
UPDATES_JOB_DIR="$job_dir" \
ucode -L "$fake_lib" -L "$PODKOP_LIB" "$UPDATES_UC" "$@"
}
updates_ucode component-action-worker "$state_file" "$output_file" zapret check_update
[ -s "$cache_dir/zapret.json" ] ||
fail "manual checks must be cached while automatic checks are enabled"
manual_cache="$(updates_ucode component-update-check-cache)"
node -e '
const value = JSON.parse(process.argv[1]);
if (!value.enabled || value.results.length !== 1 ||
value.results[0].component !== "zapret" ||
value.results[0].status !== "outdated" ||
value.results[0].latest_version !== "1.1.0") process.exit(1);
' "$manual_cache" || fail "cached manual check response is invalid"
rm -rf "$cache_dir"
TEST_COMPONENT_UPDATE_CHECK_ENABLED=0 updates_ucode \
component-action-worker "$state_file" "$output_file" zapret check_update
[ ! -e "$cache_dir/zapret.json" ] ||
fail "manual checks must not be cached while automatic checks are disabled"
rm -f "$state_file" "$timestamp_file"
updates_ucode component-updates-if-due
[ -s "$cache_dir/podkop.json" ] ||
fail "automatic checks must cache the Podkop Plus result"
[ -s "$timestamp_file" ] ||
fail "automatic checks must record their last run"
printf 'component update check cache regression checks passed\n'
@@ -983,7 +983,9 @@ cat >"$WORK_DIR/reload-state-signatures.json" <<'JSON'
{
"settings": {
"list_update_enabled": "1",
"update_interval": "6h"
"update_interval": "6h",
"component_update_check_enabled": "1",
"component_update_check_interval": "2h"
},
"section": [
{
@@ -1096,6 +1098,8 @@ EOF_LIST_SIG
cat >"$WORK_DIR/cron-signature.expected" <<'EOF_CRON_SIG'
[settings.update_interval]
6h
[settings.component_update_check_interval]
2h
[lists.list_proxy.ports]
443,80,443-444
[lists.list_proxy.community_subnet_lists]
+13 -8
View File
@@ -112,7 +112,9 @@ cat >"$WORK_DIR/cron-plan.json" <<'JSON'
{
"settings": {
"list_update_enabled": "1",
"update_interval": "30m"
"update_interval": "30m",
"component_update_check_enabled": "1",
"component_update_check_interval": "2h"
},
"section": [
{
@@ -148,8 +150,8 @@ cat >"$WORK_DIR/cron-plan.json" <<'JSON'
}
JSON
assert_eq $'list\t*/30 * * * * /usr/bin/podkop list_update_if_due # list\nsubscription-error\tbad_sub\tbad\nsubscription\t*/45 * * * * /usr/bin/podkop subscription_update_if_due # subscription' \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan.json" /usr/bin/podkop '# list' '# subscription')" \
assert_eq $'list\t*/30 * * * * /usr/bin/podkop list_update_if_due # list\nsubscription-error\tbad_sub\tbad\nsubscription\t*/45 * * * * /usr/bin/podkop subscription_update_if_due # subscription\ncomponent\t0 */2 * * * /usr/bin/podkop component_updates_if_due # component' \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan.json" /usr/bin/podkop '# list' '# subscription' '# component')" \
"cron refresh plan"
grep -Fq 'fs.writefile(tmp, as_string(text)) == null' "$UPDATES_UC" ||
@@ -173,7 +175,7 @@ cat >"$WORK_DIR/cron-plan-list-disabled.json" <<'JSON'
JSON
assert_eq "list-disabled" \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-list-disabled.json" /usr/bin/podkop '# list' '# subscription')" \
"$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-list-disabled.json" /usr/bin/podkop '# list' '# subscription' '# component')" \
"disabled list cron plan"
cat >"$WORK_DIR/cron-plan-invalid-list.json" <<'JSON'
@@ -194,7 +196,7 @@ cat >"$WORK_DIR/cron-plan-invalid-list.json" <<'JSON'
JSON
status=0
invalid_plan="$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-invalid-list.json" /usr/bin/podkop '# list' '# subscription')" || status="$?"
invalid_plan="$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-invalid-list.json" /usr/bin/podkop '# list' '# subscription' '# component')" || status="$?"
assert_eq 1 "$status" "invalid list cron plan status"
assert_eq $'list-error\tbad' "$invalid_plan" "invalid list cron plan output"
@@ -202,9 +204,10 @@ cat >"$WORK_DIR/existing.cron" <<'CRON'
0 1 * * * /bin/true # keep
* * * * * /usr/bin/podkop list_update_if_due # list
* * * * * /usr/bin/podkop subscription_update_if_due # subscription
* * * * * /usr/bin/podkop component_updates_if_due # component
CRON
updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan.json" "$WORK_DIR/existing.cron" /usr/bin/podkop '# list' '# subscription' >"$WORK_DIR/cron-apply.json"
updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan.json" "$WORK_DIR/existing.cron" /usr/bin/podkop '# list' '# subscription' '# component' >"$WORK_DIR/cron-apply.json"
node - "$WORK_DIR/cron-apply.json" <<'JS'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
@@ -212,6 +215,7 @@ const expected = [
"0 1 * * * /bin/true # keep",
"*/30 * * * * /usr/bin/podkop list_update_if_due # list",
"*/45 * * * * /usr/bin/podkop subscription_update_if_due # subscription",
"0 */2 * * * /usr/bin/podkop component_updates_if_due # component",
""
].join("\n");
if (value.crontab !== expected) {
@@ -221,14 +225,15 @@ if (value.crontab !== expected) {
const messages = value.logs.map(item => item.message);
if (!messages.includes("The cron job removed") ||
!messages.includes("The cron job has been created: */30 * * * * /usr/bin/podkop list_update_if_due # list") ||
!messages.includes("The subscription cron job has been created: */45 * * * * /usr/bin/podkop subscription_update_if_due # subscription")) {
!messages.includes("The subscription cron job has been created: */45 * * * * /usr/bin/podkop subscription_update_if_due # subscription") ||
!messages.includes("The component update check cron job has been created: 0 */2 * * * /usr/bin/podkop component_updates_if_due # component")) {
console.error("unexpected cron apply logs", JSON.stringify(value.logs));
process.exit(1);
}
JS
status=0
updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan-invalid-list.json" "$WORK_DIR/existing.cron" /usr/bin/podkop '# list' '# subscription' >"$WORK_DIR/cron-apply-invalid.json" || status="$?"
updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan-invalid-list.json" "$WORK_DIR/existing.cron" /usr/bin/podkop '# list' '# subscription' '# component' >"$WORK_DIR/cron-apply-invalid.json" || status="$?"
assert_eq 1 "$status" "invalid cron apply status"
node - "$WORK_DIR/cron-apply-invalid.json" <<'JS'
const fs = require("fs");