Skip to content

Commit 4d613e5

Browse files
authored
Merge pull request #125 from raydak-labs/feat/root-path
2 parents afd9de4 + f218b56 commit 4d613e5

18 files changed

+175
-53
lines changed

.dockerignore

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ repos
77
dockerrepos
88
playwright-report
99
test-results
10+
config.yml
11+
secrets.yml

.env.template

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
#ROOT_PATH=/app
2+
#CUSTOM_REPO_ROOT=/app/repos
13
#CONFIG_LOCATION=/app/config/config.yml
24
#SECRETS_LOCATION=/app/config/secrets.yml
3-
DRY_RUN=true # not fully supported yet
4-
LOAD_LOCAL_SAMPLES=false
5-
DEBUG_CREATE_FILES=false
6-
LOG_LEVEL=info
5+
#DRY_RUN=true # not fully supported yet
6+
#LOAD_LOCAL_SAMPLES=false
7+
#DEBUG_CREATE_FILES=false
8+
#LOG_LEVEL=info

Dockerfile

-5
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ COPY esbuild.ts ./
2929
RUN pnpm run build
3030

3131
FROM base AS dev
32-
ENV CONFIG_LOCATION=/app/config/config.yml
33-
ENV SECRETS_LOCATION=/app/config/secrets.yml
3432
# manually mount src etc
3533

3634
CMD [ "pnpm", "start" ]
@@ -46,8 +44,5 @@ RUN apk add --no-cache libstdc++ dumb-init git
4644

4745
COPY --from=builder /app/bundle.cjs /app/index.js
4846

49-
ENV CONFIG_LOCATION=/app/config/config.yml
50-
ENV SECRETS_LOCATION=/app/config/secrets.yml
51-
5247
# Run with dumb-init to not start node with PID=1, since Node.js was not designed to run as PID 1
5348
CMD ["dumb-init", "node", "index.js"]

Dockerfile-deno.Dockerfile

-4
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ COPY index.ts esbuild.ts ./
1616
RUN deno --allow-env --allow-read --allow-run esbuild.ts
1717

1818
FROM base AS dev
19-
ENV CONFIG_LOCATION=/app/config/config.yml
20-
ENV SECRETS_LOCATION=/app/config/secrets.yml
2119
ENV DENO_DIR=/app/.deno_cache
2220
# manually mount src etc
2321

@@ -34,8 +32,6 @@ RUN apk add --no-cache libstdc++ dumb-init git
3432

3533
COPY --from=builder /app/bundle.cjs /app/index.cjs
3634

37-
ENV CONFIG_LOCATION=/app/config/config.yml
38-
ENV SECRETS_LOCATION=/app/config/secrets.yml
3935
ENV DENO_DIR=/app/.deno_cache
4036

4137
# Compile cache / modify for multi-user
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
sidebar_position: 2
3+
title: Environment Variables
4+
description: "Learn about the environment variables used in our application configuration."
5+
keywords: [environment variables, configuration, setup]
6+
---
7+
8+
# Environment Variables
9+
10+
This document outlines the available environment variables for configuring Configarr besides the config files.
11+
Each variable can be set to customize the behavior of the application.
12+
13+
## Available Environment Variables
14+
15+
| Variable Name | Default Value | Required | Description |
16+
| -------------------- | ------------------------- | -------- | ------------------------------------------------------------------------------------------- |
17+
| `LOG_LEVEL` | `"info"` | No | Sets the logging level. Options are `trace`, `debug`, `info`, `warn`, `error`, and `fatal`. |
18+
| `CONFIG_LOCATION` | `"./config/config.yml"` | No | Specifies the path to the configuration file. |
19+
| `SECRETS_LOCATION` | `"./config/secrets.yml"` | No | Specifies the path to the secrets file. |
20+
| `CUSTOM_REPO_ROOT` | `"./repos"` | No | Defines the root directory for custom repositories. |
21+
| `ROOT_PATH` | Current working directory | No | Sets the root path for the application. Defaults to the current working directory. |
22+
| `DRY_RUN` | `"false"` | No | When set to `"true"`, runs the application in dry run mode without making changes. |
23+
| `LOAD_LOCAL_SAMPLES` | `"false"` | No | If `"true"`, loads local sample data for testing purposes. |
24+
| `DEBUG_CREATE_FILES` | `"false"` | No | Enables debugging for file creation processes when set to `"true"`. |
25+
26+
## Usage
27+
28+
To use these environment variables, set them in your shell or include them in your deployment configuration via docker or kubernetes.
29+
30+
## Examples
31+
32+
- For example you change the default path for all configs, repos with the `ROOT_PATH` variables.
33+
As default it would store them inside the application directory (in the container this is `/app`)
34+
35+
## References
36+
37+
Check the `.env.template` file in the repository [Github](https://github.com/raydak-labs/configarr/blob/main/.env.template)

docs/docs/configuration/experimental-support.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 2
2+
sidebar_position: 3
33
title: Experimental Support
44
description: "Experimental and testing support for other *Arr tools"
55
keywords: [configarr configuration, yaml config, custom formats, expermintal, whisparr, readarr]

docs/docs/configuration/scheduled.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 3
2+
sidebar_position: 4
33
title: Scheduling
44
description: "How to run configarr regulary/schedueld"
55
keywords: [configarr configuration, schedule, scheduler, regular, cron]

examples/full/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
!config/*.yml
22
dockerrepos/
3+
data/

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"pino-pretty": "13.0.0",
2424
"simple-git": "3.27.0",
2525
"tsx": "4.19.2",
26-
"yaml": "2.6.1"
26+
"yaml": "2.6.1",
27+
"zod": "^3.24.1"
2728
},
2829
"devDependencies": {
2930
"@hyrious/esbuild-plugin-commonjs": "0.2.4",

pnpm-lock.yaml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config.ts

+16-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { existsSync, readFileSync } from "node:fs";
22
import yaml from "yaml";
3+
import { getHelpers } from "./env";
34
import { logger } from "./logger";
45
import {
56
ConfigArrInstance,
@@ -12,10 +13,6 @@ import {
1213
InputConfigSchema,
1314
MergedConfigInstance,
1415
} from "./types/config.types";
15-
import { ROOT_PATH } from "./util";
16-
17-
const CONFIG_LOCATION = process.env.CONFIG_LOCATION ?? `${ROOT_PATH}/config.yml`;
18-
const SECRETS_LOCATION = process.env.SECRETS_LOCATION ?? `${ROOT_PATH}/secrets.yml`;
1916

2017
let config: ConfigSchema;
2118
let secrets: any;
@@ -50,12 +47,14 @@ export const getConfig = (): ConfigSchema => {
5047
return config;
5148
}
5249

53-
if (!existsSync(CONFIG_LOCATION)) {
54-
logger.error(`Config file in location "${CONFIG_LOCATION}" does not exists.`);
50+
const configLocation = getHelpers().configLocation;
51+
52+
if (!existsSync(configLocation)) {
53+
logger.error(`Config file in location "${configLocation}" does not exists.`);
5554
throw new Error("Config file not found.");
5655
}
5756

58-
const file = readFileSync(CONFIG_LOCATION, "utf8");
57+
const file = readFileSync(configLocation, "utf8");
5958

6059
const inputConfig = yaml.parse(file, { customTags: [secretsTag, envTag] }) as InputConfigSchema;
6160

@@ -65,12 +64,14 @@ export const getConfig = (): ConfigSchema => {
6564
};
6665

6766
export const readConfigRaw = (): object => {
68-
if (!existsSync(CONFIG_LOCATION)) {
69-
logger.error(`Config file in location "${CONFIG_LOCATION}" does not exists.`);
67+
const configLocation = getHelpers().configLocation;
68+
69+
if (!existsSync(configLocation)) {
70+
logger.error(`Config file in location "${configLocation}" does not exists.`);
7071
throw new Error("Config file not found.");
7172
}
7273

73-
const file = readFileSync(CONFIG_LOCATION, "utf8");
74+
const file = readFileSync(configLocation, "utf8");
7475

7576
const inputConfig = yaml.parse(file, { customTags: [secretsTag, envTag] });
7677

@@ -82,12 +83,14 @@ export const getSecrets = () => {
8283
return secrets;
8384
}
8485

85-
if (!existsSync(SECRETS_LOCATION)) {
86-
logger.error(`Secret file in location "${SECRETS_LOCATION}" does not exists.`);
86+
const secretLocation = getHelpers().secretLocation;
87+
88+
if (!existsSync(secretLocation)) {
89+
logger.error(`Secret file in location "${secretLocation}" does not exists.`);
8790
throw new Error("Secret file not found.");
8891
}
8992

90-
const file = readFileSync(SECRETS_LOCATION, "utf8");
93+
const file = readFileSync(secretLocation, "utf8");
9194
config = yaml.parse(file);
9295
return config;
9396
};

src/custom-formats.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import path from "node:path";
33
import { MergedCustomFormatResource } from "./__generated__/mergedTypes";
44
import { getUnifiedClient } from "./clients/unified-client";
55
import { getConfig } from "./config";
6+
import { getEnvs } from "./env";
67
import { logger } from "./logger";
78
import { loadTrashCFs } from "./trash-guide";
89
import { ArrType, CFProcessing, ConfigarrCF } from "./types/common.types";
910
import { ConfigCustomFormatList, CustomFormatDefinitions } from "./types/config.types";
1011
import { TrashCF } from "./types/trashguide.types";
11-
import { IS_DRY_RUN, IS_LOCAL_SAMPLE_MODE, compareCustomFormats, loadJsonFile, mapImportCfToRequestCf, toCarrCF } from "./util";
12+
import { compareCustomFormats, loadJsonFile, mapImportCfToRequestCf, toCarrCF } from "./util";
1213

1314
export const deleteAllCustomFormats = async () => {
1415
const api = getUnifiedClient();
@@ -21,7 +22,7 @@ export const deleteAllCustomFormats = async () => {
2122
};
2223

2324
export const loadServerCustomFormats = async (): Promise<MergedCustomFormatResource[]> => {
24-
if (IS_LOCAL_SAMPLE_MODE) {
25+
if (getEnvs().LOAD_LOCAL_SAMPLES) {
2526
return loadJsonFile<MergedCustomFormatResource[]>(path.resolve(__dirname, "../tests/samples/cfs.json"));
2627
}
2728
const api = getUnifiedClient();
@@ -61,7 +62,7 @@ export const manageCf = async (
6162
logger.info(`Found mismatch for ${tr.requestConfig.name}: ${comparison.changes}`);
6263

6364
try {
64-
if (IS_DRY_RUN) {
65+
if (getEnvs().DRY_RUN) {
6566
logger.info(`DryRun: Would update CF: ${existingCf.id} - ${existingCf.name}`);
6667
updatedCFs.push(existingCf);
6768
} else {
@@ -83,7 +84,7 @@ export const manageCf = async (
8384
} else {
8485
// Create
8586
try {
86-
if (IS_DRY_RUN) {
87+
if (getEnvs().DRY_RUN) {
8788
logger.info(`Would create CF: ${tr.requestConfig.name}`);
8889
} else {
8990
const createResult = await api.createCustomFormat(tr.requestConfig);
@@ -114,6 +115,7 @@ export const manageCf = async (
114115

115116
return { createCFs, updatedCFs, validCFs, errorCFs };
116117
};
118+
117119
export const loadLocalCfs = async (): Promise<CFProcessing | null> => {
118120
const config = getConfig();
119121

src/env.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import path from "node:path";
2+
import { z } from "zod";
3+
4+
const DEFAULT_ROOT_PATH = path.resolve(process.cwd());
5+
6+
const schema = z.object({
7+
// NODE_ENV: z.enum(["production", "development", "test"] as const),
8+
LOG_LEVEL: z
9+
.enum(["trace", "debug", "info", "warn", "error", "fatal"] as const)
10+
.optional()
11+
.default("info"),
12+
CONFIG_LOCATION: z.string().optional(),
13+
SECRETS_LOCATION: z.string().optional(),
14+
// TODO: deprecate?
15+
CUSTOM_REPO_ROOT: z.string().optional(),
16+
ROOT_PATH: z.string().optional().default(DEFAULT_ROOT_PATH),
17+
DRY_RUN: z
18+
.string()
19+
.toLowerCase()
20+
.transform((x) => x === "true")
21+
.pipe(z.boolean())
22+
.default("false"),
23+
LOAD_LOCAL_SAMPLES: z
24+
.string()
25+
.toLowerCase()
26+
.transform((x) => x === "true")
27+
.pipe(z.boolean())
28+
.default("false"),
29+
DEBUG_CREATE_FILES: z
30+
.string()
31+
.toLowerCase()
32+
.transform((x) => x === "true")
33+
.pipe(z.boolean())
34+
.default("false"),
35+
});
36+
37+
// declare global {
38+
// // eslint-disable-next-line @typescript-eslint/no-namespace
39+
// namespace NodeJS {
40+
// // eslint-disable-next-line @typescript-eslint/no-empty-object-type
41+
// interface ProcessEnv extends z.infer<typeof schema> {}
42+
// }
43+
// }
44+
45+
let envs: z.infer<typeof schema>;
46+
47+
export function initEnvs() {
48+
const parsed = schema.safeParse(process.env);
49+
50+
if (parsed.success === false) {
51+
console.error("Invalid environment variables:", parsed.error.flatten().fieldErrors);
52+
throw new Error("Invalid environment variables.");
53+
}
54+
55+
envs = parsed.data;
56+
}
57+
58+
export const getEnvs = () => {
59+
if (envs) return envs;
60+
61+
envs = schema.parse(process.env);
62+
63+
return envs;
64+
};
65+
66+
export const getHelpers = () => ({
67+
configLocation: getEnvs().CONFIG_LOCATION ?? `${getEnvs().ROOT_PATH}/config/config.yml`,
68+
secretLocation: getEnvs().SECRETS_LOCATION ?? `${getEnvs().ROOT_PATH}/config/secrets.yml`,
69+
// TODO: check for different env name
70+
repoPath: getEnvs().CUSTOM_REPO_ROOT ?? `${getEnvs().ROOT_PATH}/repos`,
71+
// TODO: add stuff like isDryRun,...?
72+
});

src/index.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
// those must be run first!
12
import "dotenv/config";
3+
import { getEnvs, initEnvs } from "./env";
4+
initEnvs();
25

36
import fs from "node:fs";
47
import { MergedCustomFormatResource } from "./__generated__/mergedTypes";
@@ -26,7 +29,6 @@ import {
2629
MergedConfigInstance,
2730
} from "./types/config.types";
2831
import { TrashQualityDefintion } from "./types/trashguide.types";
29-
import { DEBUG_CREATE_FILES, IS_DRY_RUN } from "./util";
3032

3133
/**
3234
* Load data from trash, recyclarr, custom configs and merge.
@@ -250,7 +252,7 @@ const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => {
250252
const { changeMap, create, restData } = calculateQualityDefinitionDiff(serverQD, qdTrash, config.quality_definition?.preferred_ratio);
251253

252254
if (changeMap.size > 0) {
253-
if (IS_DRY_RUN) {
255+
if (getEnvs().DRY_RUN) {
254256
logger.info("DryRun: Would update QualityDefinitions.");
255257
} else {
256258
logger.info(`Diffs in quality definitions found`, changeMap.values());
@@ -272,15 +274,15 @@ const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => {
272274
const serverQP = await loadQualityProfilesFromServer();
273275
const { changedQPs, create, noChanges } = await calculateQualityProfilesDiff(mergedCFs, config, serverQP, serverQD, serverCFs);
274276

275-
if (DEBUG_CREATE_FILES) {
277+
if (getEnvs().DEBUG_CREATE_FILES) {
276278
create.concat(changedQPs).forEach((e, i) => {
277279
fs.writeFileSync(`debug/test${i}.json`, JSON.stringify(e, null, 2), "utf-8");
278280
});
279281
}
280282

281283
logger.info(`QualityProfiles: Create: ${create.length}, Update: ${changedQPs.length}, Unchanged: ${noChanges.length}`);
282284

283-
if (!IS_DRY_RUN) {
285+
if (!getEnvs().DRY_RUN) {
284286
for (const element of create) {
285287
try {
286288
const newProfile = await api.createQualityProfile(element);
@@ -306,7 +308,7 @@ const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => {
306308
};
307309

308310
const run = async () => {
309-
if (IS_DRY_RUN) {
311+
if (getEnvs().DRY_RUN) {
310312
logger.info("DryRun: Running in dry-run mode!");
311313
}
312314

0 commit comments

Comments
 (0)