perf: streamline repository and deployment awareness
ForgeFlow quality gate / quality (push) Canceled after 0s

This commit is contained in:
NuklearRabbit
2026-08-12 15:26:53 +02:00
parent 38e221cbd1
commit 32ed4fcb5e
19 changed files with 351 additions and 73 deletions
+97 -30
View File
@@ -34,18 +34,23 @@ function createUnraidInventoryMethods({
fi
if [ -n "$ids" ]; then
disappeared=0
while IFS= read -r container_id; do
[ -n "$container_id" ] || continue
# Use Docker's own JSON document instead of a Go template. Accessing an
# absent .State.Health map key makes the formatted Docker inspect fail for
# every container without a healthcheck, which previously made those
# containers look as if they disappeared during the scan.
if inspect=$(docker inspect "$container_id" 2>/dev/null); then
printf 'C\\t%s\\n' "$(printf '%s' "$inspect" | base64 | tr -d '\\r\\n')"
else
disappeared=$((disappeared + 1))
fi
done <<< "$ids"
mapfile -t container_ids <<< "$ids"
# Docker accepts multiple IDs and returns one JSON array. This avoids one
# daemon round-trip per container on larger Unraid installations.
if inspect=$(docker inspect "\${container_ids[@]}" 2>/dev/null); then
printf 'C\\t%s\\n' "$(printf '%s' "$inspect" | base64 | tr -d '\\r\\n')"
else
# A container can disappear between docker ps and inspect. Fall back to
# individual reads so the remaining inventory stays complete.
for container_id in "\${container_ids[@]}"; do
[ -n "$container_id" ] || continue
if inspect=$(docker inspect "$container_id" 2>/dev/null); then
printf 'C\\t%s\\n' "$(printf '%s' "$inspect" | base64 | tr -d '\\r\\n')"
else
disappeared=$((disappeared + 1))
fi
done
fi
if [ "$disappeared" -gt 0 ]; then
printf 'W\\t%s\\n' "$(printf '%s' "$disappeared stale container reference(s) disappeared during inventory; current containers were still processed." | base64 | tr -d '\\r\\n')"
fi
@@ -99,26 +104,35 @@ function createUnraidInventoryMethods({
(
set -- -f "$primary"
files_text=$primary
has_override=false
for extra in "$dir/compose.override.yml" "$dir/compose.override.yaml" "$dir/docker-compose.override.yml" "$dir/docker-compose.override.yaml"; do
[ -f "$extra" ] || continue
has_override=true
set -- "$@" -f "$extra"
files_text="$files_text
$extra"
done
project_name=$(sed -n 's/^name:[[:space:]]*//p' "$primary" 2>/dev/null | head -n1 | cut -d'#' -f1 | tr -d '"' | tr -d "'" | xargs 2>/dev/null || true)
[ -n "$project_name" ] || project_name=$(basename "$dir")
valid=false; services=''; images=''; compose_error=''
valid=false; services=''; compose_error=''
images=$(awk '
/^[[:space:]]*services:[[:space:]]*($|#)/ { in_services=1; next }
in_services && /^[^[:space:]]/ { exit }
in_services && /^[[:space:]]+image:[[:space:]]*/ {
line=$0; sub(/^[[:space:]]*image:[[:space:]]*/, "", line); sub(/[[:space:]]+#.*/, "", line); gsub(/"/, "", line); print line
}
' "$primary" 2>/dev/null || true)
if [ "$compose_ok" != true ]; then
compose_error='Docker Compose is unavailable; file metadata was still detected.'
elif [ "$compose_v2" = true ]; then
if services=$(cd "$dir" && docker compose "$@" config --services 2>&1); then
valid=true
images=$(cd "$dir" && docker compose "$@" config --images 2>/dev/null || true)
if [ "$has_override" = true ] || [ -z "$images" ] || printf '%s' "$images" | grep -q '\$'; then images=$(cd "$dir" && docker compose "$@" config --images 2>/dev/null || true); fi
else compose_error=$services; services=''; fi
else
if services=$(cd "$dir" && docker-compose "$@" config --services 2>&1); then
valid=true
images=$(cd "$dir" && docker-compose "$@" config --images 2>/dev/null || true)
if [ "$has_override" = true ] || [ -z "$images" ] || printf '%s' "$images" | grep -q '\$'; then images=$(cd "$dir" && docker-compose "$@" config --images 2>/dev/null || true); fi
else compose_error=$services; services=''; fi
fi
if [ -z "$services" ]; then
@@ -130,15 +144,6 @@ function createUnraidInventoryMethods({
}
' "$primary" 2>/dev/null || true)
fi
if [ -z "$images" ]; then
images=$(awk '
/^[[:space:]]*services:[[:space:]]*($|#)/ { in_services=1; next }
in_services && /^[^[:space:]]/ { exit }
in_services && /^[[:space:]]+image:[[:space:]]*/ {
line=$0; sub(/^[[:space:]]*image:[[:space:]]*/, "", line); sub(/[[:space:]]+#.*/, "", line); gsub(/"/, "", line); print line
}
' "$primary" 2>/dev/null || true)
fi
printf 'Y\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n' \\
"$(printf '%s' "$dir" | base64 | tr -d '\\r\\n')" \\
"$(printf '%s' "$files_text" | base64 | tr -d '\\r\\n')" \\
@@ -310,7 +315,7 @@ function createUnraidInventoryMethods({
};
}
async saveWorkloadState(profile, workload, server) {
async saveWorkloadState(profile, workload, server, { expectedGiteaSha = null, health = null } = {}) {
const candidateSha = String(workload.metadata?.liveRevision || "");
const previousState = this.store.getDeploymentState?.(profile.id) || {};
const observedLiveSha = /^[0-9a-f]{40,64}$/i.test(candidateSha) ? candidateSha.toLowerCase() : null;
@@ -318,11 +323,21 @@ function createUnraidInventoryMethods({
const profileRemote = inventoryRemoteIdentity(profile.cloneUrl);
const workloadRemote = inventoryRemoteIdentity(workload.metadata?.sourceRepository);
const repositoryMatches = Boolean(observedLiveSha && profileRemote && workloadRemote && profileRemote === workloadRemote);
const verifiedGiteaSha = /^[0-9a-f]{40}$/i.test(String(expectedGiteaSha || ""))
? String(expectedGiteaSha).toLowerCase()
: null;
const matchesGitea = Boolean(repositoryMatches && verifiedGiteaSha && observedLiveSha === verifiedGiteaSha);
const primary = workload.containers.find((container) => container.running) || workload.containers[0] || {};
const dockerHealthy = workload.runtime.health === "healthy" ? true : workload.runtime.health === "unhealthy" ? false : null;
const effectiveHealthy = workload.runtime.running === false
? false
: health?.configured ? health.healthy : dockerHealthy;
return this.store.saveDeploymentState(profile.id, {
liveSha,
healthy: workload.runtime.health === "healthy" ? true : workload.runtime.health === "unhealthy" ? false : null,
runtimeVerification: workload.runtime.health === "unverified" ? "running-unverified" : workload.runtime.health,
healthy: effectiveHealthy,
healthStatus: health?.status ?? null,
healthLatencyMs: health?.latencyMs ?? null,
runtimeVerification: workload.runtime.running === false ? "stopped" : health?.configured ? "desktop-healthcheck" : workload.runtime.health === "unverified" ? "running-unverified" : workload.runtime.health,
containerRunning: workload.runtime.running,
dockerHealth: primary.health || null,
containerName: primary.name || profile.containerName,
@@ -332,8 +347,8 @@ function createUnraidInventoryMethods({
composeProject: workload.compose?.project || null,
observedAt: workload.observedAt,
evidence: liveSha ? "container-provenance-label" : "runtime-only",
giteaSha: repositoryMatches ? observedLiveSha : previousState.giteaSha || null,
matchesGitea: repositoryMatches ? true : previousState.matchesGitea === true && previousState.liveSha === liveSha,
giteaSha: verifiedGiteaSha,
matchesGitea,
previousSha: previousState.previousSha || null,
});
}
@@ -408,6 +423,7 @@ function createUnraidInventoryMethods({
}
async scanServerInventory(serverId, repositories, { autoLink = false } = {}) {
const started = Date.now();
const { server, inventory, workloads } = await this.collectServerInventory(serverId, repositories);
let adopted = 0;
const adoptedLinks = [];
@@ -442,6 +458,7 @@ function createUnraidInventoryMethods({
adopted,
adoptedLinks,
readOnly: !autoLink,
durationMs: Date.now() - started,
});
return response;
}
@@ -609,7 +626,57 @@ function createUnraidInventoryMethods({
}
async discoverServerWorkloads(serverId, repositories) {
return this.scanServerInventory(serverId, repositories, { autoLink: true });
const started = Date.now();
const inventory = await this.scanServerInventory(serverId, repositories, { autoLink: true });
const server = this.store.getServer(serverId);
const queue = inventory.workloads.filter((workload) => workload.link?.profileId && workload.link?.repositoryFullName);
const refreshedProfileIds = [];
let giteaUnavailable = false;
let giteaFailureReported = false;
const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
while (queue.length) {
const workload = queue.shift();
const repository = repositories.find((item) => String(item.fullName).toLowerCase() === String(workload.link.repositoryFullName).toLowerCase());
const profile = this.store.getDeploymentProfile?.(workload.link.repositoryFullName, workload.link.profileId)
|| this.store.getDeploymentProfiles?.(workload.link.repositoryFullName)?.find((item) => item.id === workload.link.profileId)
|| this.allSshProfiles().find((item) => item.id === workload.link.profileId);
if (!repository || !profile) continue;
let expectedGiteaSha = null;
const status = repository.localStatus;
if (status?.head && status.branch?.head === profile.branch && status.branch?.upstream && status.branch.ahead === 0 && status.branch.behind === 0) {
expectedGiteaSha = status.head;
} else if (!giteaUnavailable) {
try {
const [owner, repo] = String(repository.fullName).split("/");
const branch = await this.gitea.getBranch(owner, repo, profile.branch);
expectedGiteaSha = branch?.commit?.id || branch?.commit?.sha || null;
} catch (error) {
if (!error?.status || Number(error.status) >= 500) {
giteaUnavailable = true;
if (!giteaFailureReported) {
giteaFailureReported = true;
await this.diagnostics?.warning("unraid.workloads.gitea-verification-degraded", {
serverId,
message: error.message,
});
}
}
}
}
const health = workload.runtime.running
? await this.checkHealth(profile.healthcheckUrl)
: { configured: false, healthy: false, skipped: "container-stopped" };
await this.saveWorkloadState(profile, workload, server, { expectedGiteaSha, health });
refreshedProfileIds.push(profile.id);
}
});
await Promise.all(workers);
await this.diagnostics?.debug("unraid.workloads.states-refreshed", {
serverId,
profiles: refreshedProfileIds.length,
durationMs: Date.now() - started,
});
return { ...inventory, refreshedProfiles: refreshedProfileIds.length, refreshedProfileIds };
}
async linkServerWorkload({ repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "" }) {