Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
81aff48
test: retrieveing remote container digest
pujitm Aug 12, 2025
a9e62b4
don't skip ci
pujitm Aug 12, 2025
9655c00
use php-loader to load containers (untested)
pujitm Aug 12, 2025
21466f1
add container status query to gql
pujitm Aug 13, 2025
17d151a
fix: copy wrapper.php into api build
pujitm Aug 13, 2025
95560c9
add docker container resolver
pujitm Aug 13, 2025
19fa436
feat: check cached update status
pujitm Aug 13, 2025
bb6dac2
chore: add AsyncMutex to @unraid/shared/util/processing.ts
pujitm Aug 20, 2025
261d6c5
productionize & simplify docker digest computation
pujitm Aug 20, 2025
f704143
rm redundant executeOperation function
pujitm Aug 20, 2025
f9ebcb6
add ContainerStatusJob to docker.module
pujitm Aug 20, 2025
aa04064
rm fast-xml-parser
pujitm Aug 20, 2025
cf96f14
rm docker-auth.service
pujitm Aug 20, 2025
5f728c0
refactor: improve code organization
pujitm Aug 20, 2025
88b0875
fix AsynxMutex import
pujitm Aug 20, 2025
90aecc3
use enum for update status
pujitm Sep 2, 2025
240e104
refactor: docker config service -> docker organizer config service
pujitm Sep 2, 2025
99a2103
revert to cron 4.3.0 for compat with @nest/schedule@6.0.0
pujitm Sep 3, 2025
05bbe84
feat: make cron schedule configurable for container manifest refresh
pujitm Sep 3, 2025
fc5fb1a
watch php files
pujitm Sep 3, 2025
d1c9849
code cleanup
pujitm Sep 3, 2025
7a68068
add docs
pujitm Sep 3, 2025
49189d9
fix details in AsyncMutex util docs
pujitm Sep 3, 2025
473608e
feat: feature flag system
pujitm Sep 8, 2025
c128d8e
feat: guard new docker features behind `ENABLE_NEXT_DOCKER_RELEASE` env
pujitm Sep 8, 2025
34d542f
export schedule module from job module
pujitm Sep 8, 2025
28a1ec5
test: docker config validation
pujitm Sep 8, 2025
e76dd65
enable `ENABLE_NEXT_DOCKER_RELEASE` during development
pujitm Sep 8, 2025
74835d1
add validation for docker digest cache file
pujitm Sep 9, 2025
20986b2
use zod for validation instead
pujitm Sep 9, 2025
4fa89b9
refactor: explicit status item type for gql
pujitm Sep 9, 2025
5b33e90
use FeatureFlags const instead of direct env var
pujitm Sep 9, 2025
e1bbe0c
fix: uncaught synchronous exception in AsyncMutex
pujitm Sep 9, 2025
88cc616
replace `target: any` with `object` in omit-if decorator
pujitm Sep 9, 2025
bcd3bba
unit test docker-push regex in docker-php.service.ts
pujitm Sep 9, 2025
783e818
update generated schema
pujitm Sep 9, 2025
2d0135e
fix processing.ts tests
pujitm Sep 9, 2025
3c4b007
simplify asyncmutex definition -- don't support generic operations
pujitm Sep 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ BYPASS_CORS_CHECKS=true
CHOKIDAR_USEPOLLING=true
LOG_TRANSPORT=console
LOG_LEVEL=trace
ENABLE_NEXT_DOCKER_RELEASE=true
3 changes: 3 additions & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ dev/local-session

# local OIDC config for testing - contains secrets
dev/configs/oidc.local.json

# local api keys
dev/keys/*
247 changes: 247 additions & 0 deletions api/docs/developer/feature-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Feature Flags

Feature flags allow you to conditionally enable or disable functionality in the Unraid API. This is useful for gradually rolling out new features, A/B testing, or keeping experimental code behind flags during development.

## Setting Up Feature Flags

### 1. Define the Feature Flag

Feature flags are defined as environment variables and collected in `src/consts.ts`:

```typescript
// src/environment.ts
export const ENABLE_MY_NEW_FEATURE = process.env.ENABLE_MY_NEW_FEATURE === 'true';

// src/consts.ts
export const FeatureFlags = Object.freeze({
ENABLE_NEXT_DOCKER_RELEASE,
ENABLE_MY_NEW_FEATURE, // Add your new flag here
});
```

### 2. Set the Environment Variable

Set the environment variable when running the API:

```bash
ENABLE_MY_NEW_FEATURE=true unraid-api start
```

Or add it to your `.env` file:

```env
ENABLE_MY_NEW_FEATURE=true
```

## Using Feature Flags in GraphQL

### Method 1: @UseFeatureFlag Decorator (Schema-Level)

The `@UseFeatureFlag` decorator conditionally includes or excludes GraphQL fields, queries, and mutations from the schema based on feature flags. When a feature flag is disabled, the field won't appear in the GraphQL schema at all.

```typescript
import { UseFeatureFlag } from '@app/unraid-api/decorators/use-feature-flag.decorator.js';
import { Query, Mutation, ResolveField } from '@nestjs/graphql';

@Resolver()
export class MyResolver {

// Conditionally include a query
@UseFeatureFlag('ENABLE_MY_NEW_FEATURE')
@Query(() => String)
async experimentalQuery() {
return 'This query only exists when ENABLE_MY_NEW_FEATURE is true';
}

// Conditionally include a mutation
@UseFeatureFlag('ENABLE_MY_NEW_FEATURE')
@Mutation(() => Boolean)
async experimentalMutation() {
return true;
}

// Conditionally include a field resolver
@UseFeatureFlag('ENABLE_MY_NEW_FEATURE')
@ResolveField(() => String)
async experimentalField() {
return 'This field only exists when the flag is enabled';
}
}
```

**Benefits:**
- Clean schema - disabled features don't appear in GraphQL introspection
- No runtime overhead for disabled features
- Clear feature boundaries

**Use when:**
- You want to completely hide features from the GraphQL schema
- The feature is experimental or in beta
- You're doing a gradual rollout

### Method 2: checkFeatureFlag Function (Runtime)

The `checkFeatureFlag` function provides runtime feature flag checking within resolver methods. It throws a `ForbiddenException` if the feature is disabled.

```typescript
import { checkFeatureFlag } from '@app/unraid-api/utils/feature-flag.helper.js';
import { FeatureFlags } from '@app/consts.js';
import { Query, ResolveField } from '@nestjs/graphql';

@Resolver()
export class MyResolver {

@Query(() => String)
async myQuery(
@Args('useNewAlgorithm', { nullable: true }) useNewAlgorithm?: boolean
) {
// Conditionally use new logic based on feature flag
if (useNewAlgorithm) {
checkFeatureFlag(FeatureFlags, 'ENABLE_MY_NEW_FEATURE');
return this.newAlgorithm();
}

return this.oldAlgorithm();
}

@ResolveField(() => String)
async dataField() {
// Check flag at the start of the method
checkFeatureFlag(FeatureFlags, 'ENABLE_MY_NEW_FEATURE');

// Feature-specific logic here
return this.computeExperimentalData();
}
}
```

**Benefits:**
- More granular control within methods
- Can conditionally execute parts of a method
- Useful for A/B testing scenarios
- Good for gradual migration strategies

**Use when:**
- You need conditional logic within a method
- The field should exist but behavior changes based on the flag
- You're migrating from old to new implementation gradually

## Feature Flag Patterns

### Pattern 1: Complete Feature Toggle

Hide an entire feature behind a flag:

```typescript
@UseFeatureFlag('ENABLE_DOCKER_TEMPLATES')
@Resolver(() => DockerTemplate)
export class DockerTemplateResolver {
// All resolvers in this class are toggled by the flag
}
```

### Pattern 2: Gradual Migration

Migrate from old to new implementation:

```typescript
@Query(() => [Container])
async getContainers(@Args('version') version?: string) {
if (version === 'v2') {
checkFeatureFlag(FeatureFlags, 'ENABLE_CONTAINERS_V2');
return this.getContainersV2();
}

return this.getContainersV1();
}
```

### Pattern 3: Beta Features

Mark features as beta:

```typescript
@UseFeatureFlag('ENABLE_BETA_FEATURES')
@ResolveField(() => BetaMetrics, {
description: 'BETA: Advanced metrics (requires ENABLE_BETA_FEATURES flag)'
})
async betaMetrics() {
return this.computeBetaMetrics();
}
```

### Pattern 4: Performance Optimizations

Toggle expensive operations:

```typescript
@ResolveField(() => Statistics)
async statistics() {
const basicStats = await this.getBasicStats();

try {
checkFeatureFlag(FeatureFlags, 'ENABLE_ADVANCED_ANALYTICS');
const advancedStats = await this.getAdvancedStats();
return { ...basicStats, ...advancedStats };
} catch {
// Feature disabled, return only basic stats
return basicStats;
}
}
```

## Testing with Feature Flags

When writing tests for feature-flagged code, create a mock to control feature flag values:

```typescript
import { vi } from 'vitest';

// Mock the entire consts module
vi.mock('@app/consts.js', async () => {
const actual = await vi.importActual('@app/consts.js');
return {
...actual,
FeatureFlags: {
ENABLE_MY_NEW_FEATURE: true, // Set your test value
ENABLE_NEXT_DOCKER_RELEASE: false,
}
};
});

describe('MyResolver', () => {
it('should execute new logic when feature is enabled', async () => {
// Test new behavior with mocked flag
});
});
```

## Best Practices

1. **Naming Convention**: Use `ENABLE_` prefix for boolean feature flags
2. **Environment Variables**: Always use uppercase with underscores
3. **Documentation**: Document what each feature flag controls
4. **Cleanup**: Remove feature flags once features are stable and fully rolled out
5. **Default State**: New features should default to `false` (disabled)
6. **Granularity**: Keep feature flags focused on a single feature or capability
7. **Testing**: Always test both enabled and disabled states

## Common Use Cases

- **Experimental Features**: Hide unstable features in production
- **Gradual Rollouts**: Enable features for specific environments first
- **A/B Testing**: Toggle between different implementations
- **Performance**: Disable expensive operations when not needed
- **Breaking Changes**: Provide migration path with both old and new behavior
- **Debug Features**: Enable additional logging or debugging tools

## Checking Active Feature Flags

To see which feature flags are currently active:

```typescript
// Log all feature flags on startup
console.log('Active Feature Flags:', FeatureFlags);
```

Or check via GraphQL introspection to see which fields are available based on current flags.
23 changes: 23 additions & 0 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ type ArrayDisk implements Node {
"""ata | nvme | usb | (others)"""
transport: String
color: ArrayDiskFsColor

"""Whether the disk is currently spinning"""
isSpinning: Boolean
}

interface Node {
Expand Down Expand Up @@ -346,6 +349,9 @@ type Disk implements Node {

"""The partitions on the disk"""
partitions: [DiskPartition!]!

"""Whether the disk is spinning or not"""
isSpinning: Boolean!
}

"""The type of interface the disk uses to connect to the system"""
Expand Down Expand Up @@ -1044,6 +1050,19 @@ enum ThemeName {
white
}

type ExplicitStatusItem {
name: String!
updateStatus: UpdateStatus!
}

"""Update status of a container."""
enum UpdateStatus {
UP_TO_DATE
UPDATE_AVAILABLE
REBUILD_READY
UNKNOWN
}

type ContainerPort {
ip: String
privatePort: Port
Expand Down Expand Up @@ -1083,6 +1102,8 @@ type DockerContainer implements Node {
networkSettings: JSON
mounts: [JSON!]
autoStart: Boolean!
isUpdateAvailable: Boolean
isRebuildReady: Boolean
}

enum ContainerState {
Expand Down Expand Up @@ -1113,6 +1134,7 @@ type Docker implements Node {
containers(skipCache: Boolean! = false): [DockerContainer!]!
networks(skipCache: Boolean! = false): [DockerNetwork!]!
organizer: ResolvedOrganizerV1!
containerUpdateStatuses: [ExplicitStatusItem!]!
}

type ResolvedOrganizerView {
Expand Down Expand Up @@ -2413,6 +2435,7 @@ type Mutation {
setDockerFolderChildren(folderId: String, childrenIds: [String!]!): ResolvedOrganizerV1!
deleteDockerEntries(entryIds: [String!]!): ResolvedOrganizerV1!
moveDockerEntriesToFolder(sourceEntryIds: [String!]!, destinationFolderId: String!): ResolvedOrganizerV1!
refreshDockerDigests: Boolean!

"""Initiates a flash drive backup using a configured remote."""
initiateFlashBackup(input: InitiateFlashBackupInput!): FlashBackupStatus!
Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"command-exists": "1.2.9",
"convert": "5.12.0",
"cookie": "1.0.2",
"cron": "4.3.3",
"cron": "4.3.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downgraded?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ye, there was a type conflict between 4.3.3 and 4.3.0 (which is what nest scheduler currently depends on)

"cross-fetch": "4.1.0",
"diff": "8.0.2",
"dockerode": "4.0.7",
Expand Down
13 changes: 12 additions & 1 deletion api/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { join } from 'path';

import type { JSONWebKeySet } from 'jose';

import { PORT } from '@app/environment.js';
import { ENABLE_NEXT_DOCKER_RELEASE, PORT } from '@app/environment.js';

export const getInternalApiAddress = (isHttp = true, nginxPort = 80) => {
const envPort = PORT;
Expand Down Expand Up @@ -79,3 +79,14 @@ export const KEYSERVER_VALIDATION_ENDPOINT = 'https://keys.lime-technology.com/v

/** Set the max retries for the GraphQL Client */
export const MAX_RETRIES_FOR_LINEAR_BACKOFF = 100;

/**
* Feature flags are used to conditionally enable or disable functionality in the Unraid API.
*
* Keys are human readable feature flag names -- will be used to construct error messages.
*
* Values are boolean/truthy values.
*/
export const FeatureFlags = Object.freeze({
ENABLE_NEXT_DOCKER_RELEASE,
});
3 changes: 3 additions & 0 deletions api/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,6 @@ export const PATHS_CONFIG_MODULES =

export const PATHS_LOCAL_SESSION_FILE =
process.env.PATHS_LOCAL_SESSION_FILE ?? '/var/run/unraid-api/local-session';

/** feature flag for the upcoming docker release */
export const ENABLE_NEXT_DOCKER_RELEASE = process.env.ENABLE_NEXT_DOCKER_RELEASE === 'true';
3 changes: 2 additions & 1 deletion api/src/unraid-api/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Avoid double initialization of the scheduler (JobModule + CronModule).

If CronModule already imports JobModule (which calls ScheduleModule.forRoot()), importing JobModule again here can register the scheduler twice. Keep only one path.

Option A — keep CronModule, remove JobModule:

-import { JobModule } from '@app/unraid-api/cron/job.module.js';
...
-        JobModule,

Option B — keep JobModule, remove CronModule (if CronModule is now a thin wrapper depending on JobModule and otherwise redundant):

 import { CronModule } from '@app/unraid-api/cron/cron.module.js';
-import { JobModule } from '@app/unraid-api/cron/job.module.js';
+import { JobModule } from '@app/unraid-api/cron/job.module.js';
...
-        JobModule,
+        JobModule,
...
-        CronModule,
+        // CronModule,

Run to verify single ScheduleModule.forRoot() usage and JobModule import graph:

Also applies to: 28-28


🏁 Script executed:

#!/bin/bash
# Ensure ScheduleModule.forRoot() appears exactly once and confirm import graph.
rg -nP "ScheduleModule\\s*\\.forRoot\\s*\\(" -C2
rg -nP "from ['\"][^'\"]*job\\.module\\.js['\"]" -C2

Length of output: 2556


Remove duplicate JobModule import in AppModule
In api/src/unraid-api/app/app.module.ts, drop the import { JobModule } … and its entry in the @Module({ imports: […] }); CronModule already imports JobModule (and calls ScheduleModule.forRoot()).

🤖 Prompt for AI Agents
In api/src/unraid-api/app/app.module.ts around line 17, there is a duplicate
import and inclusion of JobModule in the AppModule; remove the line importing
JobModule and delete JobModule from the @Module({ imports: [...] }) array so
AppModule no longer directly imports it (CronModule already imports JobModule
and calls ScheduleModule.forRoot()), leaving only the necessary modules in
AppModule's imports.

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';
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions api/src/unraid-api/cron/cron.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

import { JobModule } from '@app/unraid-api/cron/job.module.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Scheduler init centralized via JobModule — LGTM. Verify no stray ScheduleModule.forRoot() left elsewhere.

Looks correct and uses .js extensions. To be safe, scan for any remaining direct ScheduleModule.forRoot() calls.

Also applies to: 8-9


🏁 Script executed:

#!/bin/bash
# Expect: only api/src/unraid-api/cron/job.module.ts contains ScheduleModule.forRoot()
rg -n "ScheduleModule\\.forRoot\\(" -g "api/**.ts" -S -C1

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search entire repository for ScheduleModule.forRoot occurrences in TypeScript files
rg -n "ScheduleModule\\.forRoot" -t ts -C1

Length of output: 551


Centralize ScheduleModule initialization in tests via JobModule
In api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts (around line 21), replace the direct ScheduleModule.forRoot() call with importing and using JobModule so that all schedule setups remain centralized.

🤖 Prompt for AI Agents
In api/src/unraid-api/cron/cron.module.ts around line 3, tests are directly
calling ScheduleModule.forRoot() elsewhere instead of reusing the centralized
JobModule; update the test at
api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
to remove the direct ScheduleModule.forRoot() import and instead import and
include JobModule (from '@app/unraid-api/cron/job.module.js') in the testing
module setup so all scheduling initialization is centralized via JobModule.

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 {}
Loading
Loading