deploy: configure NC01 HWLAB monitor exposure
This commit is contained in:
@@ -33,6 +33,7 @@ interface GiteaTarget {
|
||||
enabled: boolean;
|
||||
createNamespace: boolean;
|
||||
storageClassName: string;
|
||||
publicExposureEnabled: boolean;
|
||||
}
|
||||
|
||||
interface GiteaConfig {
|
||||
@@ -150,6 +151,7 @@ interface GiteaGithubCredential {
|
||||
transport: "https-token";
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
format: "env" | "raw-token";
|
||||
requiredFor: string[];
|
||||
}
|
||||
|
||||
@@ -446,6 +448,7 @@ function parseGithubCredential(record: Record<string, unknown>, path: string): G
|
||||
transport: y.enumField(record, "transport", path, ["https-token"] as const),
|
||||
sourceRef: secretSourceRefField(record, "sourceRef", path),
|
||||
sourceKey: y.envKeyField(record, "sourceKey", path),
|
||||
format: optionalLiteralField(record, "format", path, ["env", "raw-token"] as const) ?? "env",
|
||||
requiredFor: y.stringArrayField(record, "requiredFor", path),
|
||||
};
|
||||
}
|
||||
@@ -569,6 +572,11 @@ function parseMirrorRepository(record: Record<string, unknown>, index: number):
|
||||
|
||||
function parseTarget(record: Record<string, unknown>, index: number): GiteaTarget {
|
||||
const path = `targets[${index}]`;
|
||||
const publicExposureRaw = record.publicExposure;
|
||||
if (publicExposureRaw !== undefined && (typeof publicExposureRaw !== "object" || publicExposureRaw === null || Array.isArray(publicExposureRaw))) {
|
||||
throw new Error(`${configLabel}.${path}.publicExposure must be an object`);
|
||||
}
|
||||
const publicExposure = publicExposureRaw as Record<string, unknown> | undefined;
|
||||
return {
|
||||
id: y.stringField(record, "id", path),
|
||||
route: y.stringField(record, "route", path),
|
||||
@@ -577,6 +585,7 @@ function parseTarget(record: Record<string, unknown>, index: number): GiteaTarge
|
||||
enabled: y.booleanField(record, "enabled", path),
|
||||
createNamespace: y.booleanField(record, "createNamespace", path),
|
||||
storageClassName: y.stringField(record, "storageClassName", path),
|
||||
publicExposureEnabled: publicExposure === undefined ? true : y.booleanField(publicExposure, "enabled", `${path}.publicExposure`),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -644,7 +653,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
const secrets = gitea.sourceAuthority.webhookSync.enabled && !options.dryRun ? ensureMirrorSecrets(gitea, true, true) : undefined;
|
||||
const result = await capture(config, target.route, ["sh"], remoteScript("apply", gitea, target, manifest, options, { frpcSecret, secrets }));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const caddy = !options.dryRun && result.exitCode === 0 && parsed?.ok === true && gitea.app.publicExposure.enabled
|
||||
const caddy = !options.dryRun && result.exitCode === 0 && parsed?.ok === true && publicExposureEnabled(gitea, target)
|
||||
? await applyGiteaCaddyBlock(config, gitea)
|
||||
: { ok: true, skipped: true, reason: options.dryRun ? "dry-run" : "apply-not-ready" };
|
||||
return {
|
||||
@@ -1265,7 +1274,7 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-bootstra
|
||||
UNIDESK_GITEA_GITHUB_PROXY_ENABLED: gitea.sourceAuthority.githubProxy.enabled ? "1" : "0",
|
||||
UNIDESK_GITEA_GITHUB_PROXY_URL: gitea.sourceAuthority.githubProxy.url,
|
||||
UNIDESK_GITEA_NO_PROXY: gitea.sourceAuthority.githubProxy.noProxy.join(","),
|
||||
UNIDESK_GITEA_FRPC_ENABLED: gitea.app.publicExposure.enabled ? "1" : "0",
|
||||
UNIDESK_GITEA_FRPC_ENABLED: publicExposureEnabled(gitea, target) ? "1" : "0",
|
||||
UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME: gitea.app.publicExposure.frpc.deploymentName,
|
||||
UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED: gitea.sourceAuthority.webhookSync.enabled ? "1" : "0",
|
||||
UNIDESK_GITEA_WEBHOOK_PUBLIC_URL: githubWebhookPublicUrl(gitea),
|
||||
@@ -1337,6 +1346,7 @@ function targetSummary(target: GiteaTarget): Record<string, unknown> {
|
||||
role: target.role,
|
||||
createNamespace: target.createNamespace,
|
||||
storageClassName: target.storageClassName,
|
||||
publicExposureEnabled: target.publicExposureEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1363,11 +1373,20 @@ function publicServiceTarget(gitea: GiteaConfig, target: GiteaTarget): PublicSer
|
||||
route: target.route,
|
||||
namespace: target.namespace,
|
||||
replicas: 1,
|
||||
publicExposure: gitea.app.publicExposure,
|
||||
publicExposure: targetPublicExposure(gitea, target),
|
||||
};
|
||||
}
|
||||
|
||||
function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSecretMaterial {
|
||||
function publicExposureEnabled(gitea: GiteaConfig, target: GiteaTarget): boolean {
|
||||
return gitea.app.publicExposure.enabled && target.publicExposureEnabled;
|
||||
}
|
||||
|
||||
function targetPublicExposure(gitea: GiteaConfig, target: GiteaTarget): GiteaConfig["app"]["publicExposure"] {
|
||||
return publicExposureEnabled(gitea, target) ? gitea.app.publicExposure : { ...gitea.app.publicExposure, enabled: false };
|
||||
}
|
||||
|
||||
function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSecretMaterial | undefined {
|
||||
if (!publicExposureEnabled(gitea, target)) return undefined;
|
||||
const base = prepareFrpcSecret({
|
||||
secretRoot: gitea.app.publicExposure.secretRoot,
|
||||
exposure: gitea.app.publicExposure,
|
||||
@@ -1438,7 +1457,8 @@ function publicExposureSummary(gitea: GiteaConfig): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function frpcSecretSummary(secret: FrpcSecretMaterial): Record<string, unknown> {
|
||||
function frpcSecretSummary(secret: FrpcSecretMaterial | undefined): Record<string, unknown> {
|
||||
if (secret === undefined) return { enabled: false, skipped: true, reason: "target-public-exposure-disabled", valuesPrinted: false };
|
||||
return {
|
||||
sourceRef: secret.sourceRef,
|
||||
sourcePath: secret.sourcePath,
|
||||
@@ -1568,9 +1588,10 @@ function webhookSyncSummary(gitea: GiteaConfig): Record<string, unknown> {
|
||||
|
||||
function credentialSummaries(gitea: GiteaConfig): Array<Record<string, unknown>> {
|
||||
const adminPath = credentialPath(gitea, gitea.sourceAuthority.credentials.admin.sourceRef);
|
||||
const githubPath = credentialPath(gitea, gitea.sourceAuthority.credentials.github.sourceRef);
|
||||
const github = gitea.sourceAuthority.credentials.github;
|
||||
const githubPath = credentialPath(gitea, github.sourceRef);
|
||||
const adminLinePair = existsSync(adminPath) ? parseLinePairCredential(adminPath, gitea.sourceAuthority.credentials.admin) : { username: null, password: null };
|
||||
const githubEnv = existsSync(githubPath) ? parseEnvFileSafe(githubPath) : {};
|
||||
const githubEnv = existsSync(githubPath) ? parseGithubCredentialFile(githubPath, github) : {};
|
||||
return [
|
||||
{
|
||||
id: "gitea-admin",
|
||||
@@ -1584,12 +1605,12 @@ function credentialSummaries(gitea: GiteaConfig): Array<Record<string, unknown>>
|
||||
},
|
||||
{
|
||||
id: "github-upstream",
|
||||
transport: gitea.sourceAuthority.credentials.github.transport,
|
||||
sourceRef: gitea.sourceAuthority.credentials.github.sourceRef,
|
||||
transport: github.transport,
|
||||
sourceRef: github.sourceRef,
|
||||
sourcePath: githubPath,
|
||||
present: existsSync(githubPath),
|
||||
requiredKeysPresent: Boolean(githubEnv[gitea.sourceAuthority.credentials.github.sourceKey]),
|
||||
fingerprint: fingerprintKeys(githubEnv, [gitea.sourceAuthority.credentials.github.sourceKey]),
|
||||
requiredKeysPresent: Boolean(githubEnv[github.sourceKey]),
|
||||
fingerprint: fingerprintKeys(githubEnv, [github.sourceKey]),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
];
|
||||
@@ -1614,7 +1635,7 @@ function ensureMirrorSecrets(gitea: GiteaConfig, createAdmin: boolean, createWeb
|
||||
const github = gitea.sourceAuthority.credentials.github;
|
||||
const githubPath = credentialPath(gitea, github.sourceRef);
|
||||
if (!existsSync(githubPath)) throw new Error(`${github.sourceRef} is missing; cannot sync GitHub upstream`);
|
||||
const githubEnv = parseEnvFileSafe(githubPath);
|
||||
const githubEnv = parseGithubCredentialFile(githubPath, github);
|
||||
const webhookSecret = ensureWebhookSecret(gitea, createWebhookSecret);
|
||||
const adminUsername = adminLinePair.username;
|
||||
const adminPassword = adminLinePair.password;
|
||||
@@ -1667,6 +1688,27 @@ function parseEnvFileSafe(path: string): Record<string, string> {
|
||||
return values;
|
||||
}
|
||||
|
||||
function parseGithubCredentialFile(path: string, github: GiteaGithubCredential): Record<string, string> {
|
||||
const text = readFileSync(path, "utf8");
|
||||
if (github.format === "raw-token") return { [github.sourceKey]: text.trim() };
|
||||
return parseEnvTextSafe(text);
|
||||
}
|
||||
|
||||
function parseEnvTextSafe(text: string): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
for (const rawLine of text.split(/\r?\n/u)) {
|
||||
let line = rawLine.trim();
|
||||
if (line.length === 0 || line.startsWith("#")) continue;
|
||||
if (line.startsWith("export ")) line = line.slice("export ".length).trim();
|
||||
const eq = line.indexOf("=");
|
||||
if (eq <= 0) continue;
|
||||
const key = line.slice(0, eq).trim();
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue;
|
||||
values[key] = unquote(line.slice(eq + 1).trim());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function unquote(value: string): string {
|
||||
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
|
||||
return value;
|
||||
@@ -2055,6 +2097,13 @@ function literalField<T extends string>(obj: Record<string, unknown>, key: strin
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function optionalLiteralField<T extends string>(obj: Record<string, unknown>, key: string, path: string, allowed: readonly T[]): T | null {
|
||||
if (obj[key] === undefined || obj[key] === null) return null;
|
||||
const value = y.stringField(obj, key, path);
|
||||
if (!allowed.includes(value as T)) throw new Error(`${configLabel}.${path}.${key} must be one of ${allowed.join(", ")}`);
|
||||
return value as T;
|
||||
}
|
||||
|
||||
function quantity(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = y.stringField(obj, key, path);
|
||||
if (!/^[0-9]+(?:m|Ki|Mi|Gi|Ti)?$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a Kubernetes quantity`);
|
||||
|
||||
Reference in New Issue
Block a user