feat: make cron schedule configurable for container manifest refresh

This commit is contained in:
Pujit Mehrotra
2025-09-03 10:35:12 -04:00
parent 99a2103d16
commit 05bbe84175
9 changed files with 100 additions and 18 deletions
+2 -1
View File
@@ -14,6 +14,7 @@ import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { AuthenticationGuard } from '@app/unraid-api/auth/authentication.guard.js';
import { LegacyConfigModule } from '@app/unraid-api/config/legacy-config.module.js';
import { CronModule } from '@app/unraid-api/cron/cron.module.js';
import { JobModule } from '@app/unraid-api/cron/job.module.js';
import { GraphModule } from '@app/unraid-api/graph/graph.module.js';
import { GlobalDepsModule } from '@app/unraid-api/plugin/global-deps.module.js';
import { RestModule } from '@app/unraid-api/rest/rest.module.js';
@@ -24,7 +25,7 @@ import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/u
GlobalDepsModule,
LegacyConfigModule,
PubSubModule,
ScheduleModule.forRoot(),
JobModule,
LoggerModule.forRoot({
pinoHttp: {
logger: apiLogger,
+2 -2
View File
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { JobModule } from '@app/unraid-api/cron/job.module.js';
import { LogRotateService } from '@app/unraid-api/cron/log-rotate.service.js';
import { WriteFlashFileService } from '@app/unraid-api/cron/write-flash-file.service.js';
@Module({
imports: [],
imports: [JobModule],
providers: [WriteFlashFileService, LogRotateService],
})
export class CronModule {}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
/**
* Sets up common dependencies for initializing jobs (e.g. scheduler registry, cron jobs).
*
* Simplifies testing setup & application dependency tree by ensuring `forRoot` is called only once.
*/
@Module({
imports: [ScheduleModule.forRoot()],
})
export class JobModule {}
@@ -1,15 +1,33 @@
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression, Timeout } from '@nestjs/schedule';
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { SchedulerRegistry, Timeout } from '@nestjs/schedule';
import { CronJob } from 'cron';
import { DockerConfigService } from '@app/unraid-api/graph/resolvers/docker/docker-config.service.js';
import { DockerManifestService } from '@app/unraid-api/graph/resolvers/docker/docker-manifest.service.js';
@Injectable()
export class ContainerStatusJob {
constructor(private readonly dockerManifestService: DockerManifestService) {}
export class ContainerStatusJob implements OnApplicationBootstrap {
private readonly logger = new Logger(ContainerStatusJob.name);
constructor(
private readonly dockerManifestService: DockerManifestService,
private readonly schedulerRegistry: SchedulerRegistry,
private readonly dockerConfigService: DockerConfigService
) {}
@Cron(CronExpression.EVERY_DAY_AT_6AM)
async refreshContainerDigests() {
await this.dockerManifestService.refreshDigests();
onApplicationBootstrap() {
const cronExpression = this.dockerConfigService.getConfig().updateCheckCronSchedule;
const cronJob = CronJob.from({
cronTime: cronExpression,
onTick: () => {
this.dockerManifestService.refreshDigests();
},
start: true,
});
this.schedulerRegistry.addCronJob(ContainerStatusJob.name, cronJob);
this.logger.verbose(
`Initialized cron job for refreshing container update status: ${ContainerStatusJob.name}`
);
}
/**
@@ -0,0 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class DockerConfig {
@Field(() => String)
updateCheckCronSchedule!: string;
}
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CronExpression } from '@nestjs/schedule';
import { ConfigFilePersister } from '@unraid/shared/services/config-file.js';
import { validateCronExpression } from 'cron';
import { AppError } from '@app/core/errors/app-error.js';
import { DockerConfig } from '@app/unraid-api/graph/resolvers/docker/docker-config.model.js';
import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js';
@Injectable()
export class DockerConfigService extends ConfigFilePersister<DockerConfig> {
constructor(configService: ConfigService) {
super(configService);
}
configKey(): string {
return 'docker';
}
fileName(): string {
return 'docker.config.json';
}
defaultConfig(): DockerConfig {
return {
updateCheckCronSchedule: CronExpression.EVERY_DAY_AT_6AM,
};
}
async validate(config: object): Promise<DockerConfig> {
const dockerConfig = await validateObject(DockerConfig, config);
const cronExpression = validateCronExpression(dockerConfig.updateCheckCronSchedule);
if (!cronExpression.valid) {
throw new AppError(`Cron expression not supported: ${dockerConfig.updateCheckCronSchedule}`);
}
return dockerConfig;
}
}
@@ -3,8 +3,7 @@ import { Mutation, Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Resource } from '@unraid/shared/graphql.model.js';
import {
AuthActionVerb,
AuthPossession,
AuthAction,
UsePermissions,
} from '@unraid/shared/use-permissions.directive.js';
@@ -18,9 +17,8 @@ export class DockerContainerResolver {
constructor(private readonly dockerManifestService: DockerManifestService) {}
@UsePermissions({
action: AuthActionVerb.READ,
action: AuthAction.READ_ANY,
resource: Resource.DOCKER,
possession: AuthPossession.ANY,
})
@ResolveField(() => Boolean, { nullable: true })
public async isUpdateAvailable(@Parent() container: DockerContainer) {
@@ -33,9 +31,8 @@ export class DockerContainerResolver {
}
@UsePermissions({
action: AuthActionVerb.READ,
action: AuthAction.READ_ANY,
resource: Resource.DOCKER,
possession: AuthPossession.ANY,
})
@ResolveField(() => Boolean, { nullable: true })
public async isRebuildReady(@Parent() container: DockerContainer) {
@@ -43,9 +40,8 @@ export class DockerContainerResolver {
}
@UsePermissions({
action: AuthActionVerb.UPDATE,
action: AuthAction.UPDATE_ANY,
resource: Resource.DOCKER,
possession: AuthPossession.ANY,
})
@Mutation(() => Boolean)
public async refreshDockerDigests() {
@@ -1,7 +1,9 @@
import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { describe, expect, it, vi } from 'vitest';
import { DockerConfigService } from '@app/unraid-api/graph/resolvers/docker/docker-config.service.js';
import { DockerEventService } from '@app/unraid-api/graph/resolvers/docker/docker-event.service.js';
import { DockerPhpService } from '@app/unraid-api/graph/resolvers/docker/docker-php.service.js';
import { DockerModule } from '@app/unraid-api/graph/resolvers/docker/docker.module.js';
@@ -20,6 +22,8 @@ describe('DockerModule', () => {
.useValue({ getDockerClient: vi.fn() })
.overrideProvider(DockerOrganizerConfigService)
.useValue({ getConfig: vi.fn() })
.overrideProvider(DockerConfigService)
.useValue({ getConfig: vi.fn() })
.compile();
expect(module).toBeDefined();
@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common';
import { JobModule } from '@app/unraid-api/cron/job.module.js';
import { ContainerStatusJob } from '@app/unraid-api/graph/resolvers/docker/container-status.job.js';
import { DockerConfigService } from '@app/unraid-api/graph/resolvers/docker/docker-config.service.js';
import { DockerContainerResolver } from '@app/unraid-api/graph/resolvers/docker/docker-container.resolver.js';
import { DockerManifestService } from '@app/unraid-api/graph/resolvers/docker/docker-manifest.service.js';
import { DockerPhpService } from '@app/unraid-api/graph/resolvers/docker/docker-php.service.js';
@@ -11,6 +13,7 @@ import { DockerOrganizerConfigService } from '@app/unraid-api/graph/resolvers/do
import { DockerOrganizerService } from '@app/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.js';
@Module({
imports: [JobModule],
providers: [
// Services
DockerService,
@@ -18,6 +21,7 @@ import { DockerOrganizerService } from '@app/unraid-api/graph/resolvers/docker/o
DockerOrganizerService,
DockerManifestService,
DockerPhpService,
DockerConfigService,
// DockerEventService,
// Jobs