mirror of
https://github.com/unraid/api.git
synced 2026-01-04 23:50:37 -06:00
- Add a new utility class, `AsyncMutex` in `unraid-shared -> processing.ts`, for ergonomically de-duplicating async operations. - Add an `@OmitIf` decorator for omitting graphql queries, mutations, or field resolvers from the runtime graphql schema. - Add feature-flagging system - `FeatureFlags` export from `consts.ts` - `@UseFeatureFlag` decorator built upon `OmitIf` - `checkFeatureFlag` for constructing & throwing a `ForbiddenError` if the given feature flag evaluates to `false`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Expose disk spinning state, per-container "update available" and "rebuild ready" indicators, a structured per-container update-status list, and a mutation to refresh Docker digests. Periodic and post-startup digest refreshes added (feature-flag gated). * **Chores** * Cron scheduling refactor and scheduler centralization. * Build now bundles a PHP wrapper asset. * Added feature-flag env var and .gitignore entry for local keys. * **Documentation** * Added developer guide for feature flags. * **Tests** * New concurrency, parser, decorator, config, and mutex test suites. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { readFile } from 'fs/promises';
|
|
|
|
import { z } from 'zod';
|
|
|
|
import { phpLoader } from '@app/core/utils/plugins/php-loader.js';
|
|
import {
|
|
ExplicitStatusItem,
|
|
UpdateStatus,
|
|
} from '@app/unraid-api/graph/resolvers/docker/docker-update-status.model.js';
|
|
import { parseDockerPushCalls } from '@app/unraid-api/graph/resolvers/docker/utils/docker-push-parser.js';
|
|
|
|
type StatusItem = { name: string; updateStatus: 0 | 1 | 2 | 3 };
|
|
|
|
/**
|
|
* These types reflect the structure of the /var/lib/docker/unraid-update-status.json file,
|
|
* which is not controlled by the Unraid API.
|
|
*/
|
|
const CachedStatusEntrySchema = z.object({
|
|
/** sha256 digest - "sha256:..." */
|
|
local: z.string(),
|
|
/** sha256 digest - "sha256:..." */
|
|
remote: z.string(),
|
|
/** whether update is available (true), not available (false), or unknown (null) */
|
|
status: z.enum(['true', 'false']).nullable(),
|
|
});
|
|
const CachedStatusSchema = z.record(z.string(), CachedStatusEntrySchema);
|
|
export type CachedStatusEntry = z.infer<typeof CachedStatusEntrySchema>;
|
|
|
|
@Injectable()
|
|
export class DockerPhpService {
|
|
private readonly logger = new Logger(DockerPhpService.name);
|
|
constructor() {}
|
|
|
|
/**
|
|
* Reads JSON from a file containing cached update status.
|
|
* If the file does not exist, an empty object is returned.
|
|
* @param cacheFile
|
|
* @returns
|
|
*/
|
|
async readCachedUpdateStatus(
|
|
cacheFile = '/var/lib/docker/unraid-update-status.json'
|
|
): Promise<Record<string, CachedStatusEntry>> {
|
|
try {
|
|
const cache = await readFile(cacheFile, 'utf8');
|
|
const cacheData = JSON.parse(cache);
|
|
const { success, data } = CachedStatusSchema.safeParse(cacheData);
|
|
if (success) return data;
|
|
this.logger.warn(cacheData, 'Invalid cached update status');
|
|
return {};
|
|
} catch (error) {
|
|
this.logger.warn(error, 'Failed to read cached update status');
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**----------------------
|
|
* Refresh Container Digests
|
|
*------------------------**/
|
|
|
|
/**
|
|
* Recomputes local/remote digests by triggering `DockerTemplates->getAllInfo(true)` via DockerUpdate.php
|
|
* @param dockerUpdatePath - Path to the DockerUpdate.php file
|
|
* @returns True if the digests were refreshed, false if the file is not found or the operation failed
|
|
*/
|
|
async refreshDigestsViaPhp(
|
|
dockerUpdatePath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerUpdate.php'
|
|
) {
|
|
try {
|
|
await phpLoader({
|
|
file: dockerUpdatePath,
|
|
method: 'GET',
|
|
});
|
|
return true;
|
|
} catch {
|
|
// ignore; offline may keep remote as 'undef'
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**----------------------
|
|
* Parse Container Statuses
|
|
*------------------------**/
|
|
|
|
private parseStatusesFromDockerPush(js: string): ExplicitStatusItem[] {
|
|
const matches = parseDockerPushCalls(js);
|
|
return matches.map(({ name, updateStatus }) => ({
|
|
name,
|
|
updateStatus: this.updateStatusToString(updateStatus as StatusItem['updateStatus']),
|
|
}));
|
|
}
|
|
|
|
private updateStatusToString(updateStatus: 0): UpdateStatus.UP_TO_DATE;
|
|
private updateStatusToString(updateStatus: 1): UpdateStatus.UPDATE_AVAILABLE;
|
|
private updateStatusToString(updateStatus: 2): UpdateStatus.REBUILD_READY;
|
|
private updateStatusToString(updateStatus: 3): UpdateStatus.UNKNOWN;
|
|
// prettier-ignore
|
|
private updateStatusToString(updateStatus: StatusItem['updateStatus']): ExplicitStatusItem['updateStatus'];
|
|
private updateStatusToString(
|
|
updateStatus: StatusItem['updateStatus']
|
|
): ExplicitStatusItem['updateStatus'] {
|
|
switch (updateStatus) {
|
|
case 0:
|
|
return UpdateStatus.UP_TO_DATE;
|
|
case 1:
|
|
return UpdateStatus.UPDATE_AVAILABLE;
|
|
case 2:
|
|
return UpdateStatus.REBUILD_READY;
|
|
default:
|
|
return UpdateStatus.UNKNOWN;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the update statuses for all containers by triggering `DockerTemplates->getAllInfo(true)` via DockerContainers.php
|
|
* @param dockerContainersPath - Path to the DockerContainers.php file
|
|
* @returns The update statuses for all containers
|
|
*/
|
|
async getContainerUpdateStatuses(
|
|
dockerContainersPath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerContainers.php'
|
|
): Promise<ExplicitStatusItem[]> {
|
|
const stdout = await phpLoader({
|
|
file: dockerContainersPath,
|
|
method: 'GET',
|
|
});
|
|
const parts = stdout.split('\0'); // [html, "docker.push(...)", busyFlag]
|
|
const js = parts[1] || '';
|
|
return this.parseStatusesFromDockerPush(js);
|
|
}
|
|
}
|