deploy: configure NC01 HWLAB monitor exposure

This commit is contained in:
root
2026-07-08 05:34:11 +02:00
parent 5dfb78f000
commit a328aa909e
41 changed files with 2522 additions and 110 deletions
+6
View File
@@ -48,6 +48,7 @@ export interface AgentRunLaneSecretSpec {
readonly sourceRef: string;
readonly sourceMode: "env" | "file";
readonly sourceKey: string | null;
readonly sourceFormat: "env" | "raw-token" | null;
readonly targetRef: AgentRunSecretRef;
readonly providerCredentialProfile: string | null;
}
@@ -407,6 +408,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
sourceRef: secret.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`,
sourceMode: secret.sourceMode,
sourceKey: secret.sourceKey,
sourceFormat: secret.sourceFormat,
targetRef: secret.targetRef,
providerCredential: secret.providerCredentialProfile === null ? null : { profile: secret.providerCredentialProfile },
valuesPrinted: false,
@@ -831,12 +833,16 @@ function parseLaneSecret(input: Record<string, unknown>, path: string): AgentRun
if (sourceMode !== "env" && sourceMode !== "file") throw new Error(`${path}.sourceMode must be env or file`);
const sourceKey = optionalStringField(input, "sourceKey", path) ?? null;
if (sourceMode === "env" && sourceKey === null) throw new Error(`${path}.sourceKey is required when sourceMode is env`);
const sourceFormat = optionalStringField(input, "sourceFormat", path) ?? null;
if (sourceFormat !== null && sourceFormat !== "env" && sourceFormat !== "raw-token") throw new Error(`${path}.sourceFormat must be env or raw-token`);
if (sourceFormat === "raw-token" && sourceMode !== "env") throw new Error(`${path}.sourceFormat raw-token requires sourceMode env`);
const providerCredential = parseProviderCredentialBinding(input, `${path}.providerCredential`);
return {
id: stringField(input, "id", path),
sourceRef: secretSourceRefField(input, "sourceRef", path),
sourceMode,
sourceKey,
sourceFormat,
targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
providerCredentialProfile: providerCredential,
};
+10 -1
View File
@@ -45,6 +45,7 @@ export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
const secretIds: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
@@ -62,9 +63,17 @@ export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
index += 1;
continue;
}
if (arg === "--secret-id") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--secret-id requires a value");
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("--secret-id must be a simple YAML secret id");
if (!secretIds.includes(value)) secretIds.push(value);
index += 1;
continue;
}
throw new Error(`unsupported secret-sync option: ${arg}`);
}
return { ...base, node, lane };
return { ...base, node, lane, secretIds };
}
export function parseLaneConfirmOptions(args: string[]): LaneConfirmOptions {
+1
View File
@@ -71,6 +71,7 @@ export function agentRunHelp(): unknown {
"bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --secret-id tool-github-pr-token --confirm",
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run",
+1
View File
@@ -219,6 +219,7 @@ export interface ConfirmOptions {
export interface SecretSyncOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
secretIds: string[];
}
export interface LaneConfirmOptions extends ConfirmOptions {
+10 -4
View File
@@ -350,6 +350,7 @@ export function yamlLanePipelineRunManifest(spec: AgentRunLaneSpec, sourceCommit
{ name: "source-branch", value: spec.source.branch },
{ name: "gitops-branch", value: spec.gitops.branch },
{ name: "revision", value: sourceCommit },
...(sourceStageRef === null ? [] : [{ name: "source-stage-ref", value: sourceStageRef }]),
{ name: "registry-prefix", value: spec.ci.registryPrefix },
{ name: "tools-image", value: spec.ci.toolsImage },
],
@@ -476,6 +477,7 @@ export function yamlLaneGitMirrorSshSetupShellLines(spec: AgentRunLaneSpec): str
const proxyHost = spec.gitMirror.githubProxy.host;
const proxyPort = spec.gitMirror.githubProxy.port;
const proxyUrl = `http://${proxyHost}:${proxyPort}`;
const noProxy = "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,hyueapi.com,.hyueapi.com";
const proxySummary = `agentrun git-mirror-egress-proxy node=${spec.nodeId} lane=${spec.lane} host=${proxyHost} port=${proxyPort} ssh=GIT_SSH-wrapper source=yaml`;
const proxyCommand = `ProxyCommand=node /tmp/agentrun-github-proxy-connect.cjs ${proxyHost} ${proxyPort} %h %p`;
return [
@@ -545,6 +547,8 @@ export function yamlLaneGitMirrorSshSetupShellLines(spec: AgentRunLaneSpec): str
`export http_proxy=${shQuote(proxyUrl)}`,
`export https_proxy=${shQuote(proxyUrl)}`,
`export all_proxy=${shQuote(proxyUrl)}`,
`export NO_PROXY=${shQuote(noProxy)}`,
`export no_proxy=${shQuote(noProxy)}`,
"export GIT_SSH=/tmp/agentrun-git-ssh-proxy.sh",
"unset GIT_SSH_COMMAND",
];
@@ -558,8 +562,8 @@ export function yamlLaneGitMirrorSyncShell(spec: AgentRunLaneSpec): string {
`source_branch=${shQuote(spec.source.branch)}`,
`source_stage_ref_prefix=${shQuote(agentRunSourceSnapshotStageRefPrefix(spec))}`,
`gitops_branch=${shQuote(spec.gitops.branch)}`,
`remote=${shQuote(spec.source.remote)}`,
"repo=\"/cache/${repository}.git\"",
"remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
"mkdir -p \"$(dirname \"$repo\")\"",
"if [ -d \"$repo/objects\" ] && [ -f \"$repo/HEAD\" ]; then",
" git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
@@ -606,8 +610,8 @@ export function yamlLaneGitMirrorFlushShell(spec: AgentRunLaneSpec): string {
...yamlLaneGitMirrorSshSetupShellLines(spec),
`repository=${shQuote(spec.source.repository)}`,
`gitops_branch=${shQuote(spec.gitops.branch)}`,
`remote=${shQuote(spec.gitMirror.writeUrl)}`,
"repo=\"/cache/${repository}.git\"",
"remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
"test -d \"$repo\"",
"git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
"local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)",
@@ -703,6 +707,7 @@ export type LaneSecretSource = {
sourceRef: string;
sourceMode: "env" | "file";
sourceKey: string | null;
sourceFormat?: "env" | "raw-token" | null;
targetRef: { namespace: string; name: string; key: string };
transform?: "local-postgres-database" | "local-postgres-user" | "local-postgres-database-url";
};
@@ -719,8 +724,8 @@ export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecret
value = readFileSync(sourcePath, "utf8");
} else {
if (source.sourceKey === null) throw new Error(`secret source ${sourceRef} is missing sourceKey`);
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
const envValue = values.get(source.sourceKey);
const text = readFileSync(sourcePath, "utf8");
const envValue = source.sourceFormat === "raw-token" ? text.trim() : parseEnvFile(text).get(source.sourceKey);
if (envValue === undefined || envValue.length === 0) throw new Error(`secret source ${sourceRef} is missing required key ${source.sourceKey}`);
value = envValue;
}
@@ -837,6 +842,7 @@ export function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSour
sourceRef: secret.sourceRef,
sourceMode: secret.sourceMode,
sourceKey: secret.sourceKey,
sourceFormat: secret.sourceFormat,
targetRef: secret.targetRef,
});
}
+11 -5
View File
@@ -178,15 +178,19 @@ export async function restartYamlLane(config: UniDeskConfig, options: LaneConfir
export async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const sources = collectLaneSecretSources(spec);
const allSources = collectLaneSecretSources(spec);
const selectedIds = new Set(options.secretIds);
const sources = selectedIds.size === 0 ? allSources : allSources.filter((source) => selectedIds.has(source.id));
if (sources.length === 0) {
return {
ok: false,
command: "agentrun control-plane secret-sync",
mode: "yaml-declared-node-lane",
configPath,
target: agentRunLaneSummary(spec),
target: compactAgentRunLaneStatusTarget(spec),
degradedReason: "lane-secret-sources-not-declared",
requestedSecretIds: options.secretIds,
declaredSecretIds: allSources.map((source) => source.id),
valuesPrinted: false,
};
}
@@ -211,10 +215,11 @@ export async function secretSync(config: UniDeskConfig, options: SecretSyncOptio
mode: "dry-run",
mutation: false,
configPath,
target: agentRunLaneSummary(spec),
target: compactAgentRunLaneStatusTarget(spec),
filter: options.secretIds.length === 0 ? null : { secretIds: options.secretIds, selected: sources.map((source) => source.id) },
plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
next: {
confirm: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
confirm: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane}${options.secretIds.map((id) => ` --secret-id ${id}`).join("")} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
@@ -228,7 +233,8 @@ export async function secretSync(config: UniDeskConfig, options: SecretSyncOptio
mode: "confirmed-sync",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
target: compactAgentRunLaneStatusTarget(spec),
filter: options.secretIds.length === 0 ? null : { secretIds: options.secretIds, selected: sources.map((source) => source.id) },
plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
result: payload,
capture: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
+2 -15
View File
@@ -128,22 +128,9 @@ export type AgentRunBridgeCaptureResult = SshCaptureResult & { bridgeExecution?:
export let localBackendCoreStatusCache: LocalBackendCoreStatus | null = null;
export async function capture(config: UniDeskConfig, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
const result = await runSshCommandCapture(config, target, args);
const plan = agentRunBridgeCapturePlan(config, target);
if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) {
return await captureRemote(config, plan, target, args);
}
const local = attachBridgeExecution(await runSshCommandCapture(config, target, args), plan);
const fallbackHost = agentRunBridgeRemoteHost(config);
if (local.exitCode !== 0 && fallbackHost !== null && bridgeExecutionFailureKind(local)?.degradedReason === "capture-backend-unavailable") {
const fallbackPlan: AgentRunBridgeCapturePlan = {
...plan,
backend: "remote-frontend-websocket",
remoteHost: fallbackHost,
reason: "local-capture-backend-unavailable",
};
return await captureRemote(config, fallbackPlan, target, args);
}
return local;
return attachBridgeExecution(result, { ...plan, reason: `unified-ssh-capture:${plan.reason}` });
}
export async function captureRemote(config: UniDeskConfig, plan: AgentRunBridgeCapturePlan, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
+5 -2
View File
@@ -1242,15 +1242,18 @@ function yamlLaneK3sBuildProxyEnv(spec: AgentRunLaneSpec): Array<{ name: string;
}
function yamlLaneK3sBuildSourceShell(spec: AgentRunLaneSpec, sourceCommit: string): string {
const useDirectSourceRemote = spec.source.remote.startsWith("http://gitea-http.") || spec.source.remote.startsWith("https://gitea.");
const readUrl = useDirectSourceRemote ? spec.source.remote : spec.gitMirror.readUrl;
return [
"set -eu",
`read_url=${shQuote(spec.gitMirror.readUrl)}`,
`read_url=${shQuote(readUrl)}`,
`source_commit=${shQuote(sourceCommit)}`,
`source_stage_ref=${shQuote(agentRunSourceSnapshotRef(spec, sourceCommit))}`,
`use_direct_source_remote=${useDirectSourceRemote ? "true" : "false"}`,
"rm -rf /workspace/repo",
"git clone --no-checkout \"$read_url\" /workspace/repo",
"cd /workspace/repo",
"git fetch origin \"+$source_stage_ref:refs/remotes/origin/unidesk-source-snapshot\"",
"if [ \"$use_direct_source_remote\" != true ]; then git fetch origin \"+$source_stage_ref:refs/remotes/origin/unidesk-source-snapshot\"; fi",
"git checkout --detach \"$source_commit\"",
"actual=$(git rev-parse HEAD)",
"test \"$actual\" = \"$source_commit\"",
+149 -4
View File
@@ -3,10 +3,25 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { isAbsolute, join, normalize, relative, resolve } from "node:path";
import { rootPath } from "../config";
import { parseEnvFile } from "../platform-infra-ops-library";
import { readIssueCommentBody } from "./body-input";
import { PREVIEW_CHARS } from "./types";
import { PREVIEW_CHARS, UNIDESK_CLI_CONFIG_PATH } from "./types";
import type { GitHubOptions, GitHubTokenProbe } from "./types";
const GITHUB_TOKEN_SOURCE_CONFIG_REF = `${UNIDESK_CLI_CONFIG_PATH}#github.auth.tokenSource`;
interface GitHubYamlTokenSourceConfig {
enabled: boolean;
priority: "before-env" | "after-env";
root: string;
sourceRef: string;
sourceKey: string;
format: "env" | "raw-token";
}
export function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
if (options.comment === undefined && options.commentFile === undefined) return null;
if (options.comment !== undefined) {
@@ -34,6 +49,50 @@ export function tokenFromEnvironment(): GitHubTokenProbe {
return { present: false, source: null, ghFallbackAttempted: false };
}
export function tokenFromYamlSource(): { token: string | null; probe: GitHubTokenProbe } {
const config = readGitHubYamlTokenSourceConfig();
if (config === null || !config.enabled) {
return {
token: null,
probe: {
present: false,
source: null,
ghFallbackAttempted: false,
yamlSourceAttempted: true,
yamlSourceEnabled: config?.enabled ?? false,
yamlSourcePriority: config?.priority,
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
valuesPrinted: false,
},
};
}
const sourceRoot = resolveYamlSourceRoot(config.root);
const sourcePath = resolveYamlSourcePath(sourceRoot, config.sourceRef);
const sourceExists = existsSync(sourcePath);
const values = sourceExists ? parseYamlTokenSource(readFileSync(sourcePath, "utf8"), config) : {};
const token = values[config.sourceKey] ?? null;
const present = token !== null && token.length > 0;
return {
token: present ? token : null,
probe: {
present,
source: present ? "yaml-token-source" : null,
ghFallbackAttempted: false,
yamlSourceAttempted: true,
yamlSourceEnabled: true,
yamlSourcePriority: config.priority,
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
sourceRef: config.sourceRef,
sourceKey: config.sourceKey,
sourcePath: redactRepoPath(sourcePath),
sourceExists,
sourceKeyPresent: present,
tokenFingerprint: present ? fingerprintToken(token) : null,
valuesPrinted: false,
},
};
}
export function ghBinaryPath(): string | null {
try {
const output = execFileSync("sh", ["-lc", "command -v gh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -53,17 +112,103 @@ export function ghAuthToken(): string | null {
}
export function resolveToken(allowGhFallback: boolean): { token: string | null; probe: GitHubTokenProbe } {
const yamlToken = tokenFromYamlSource();
if (yamlToken.probe.yamlSourcePriority === "before-env" && yamlToken.probe.present) return yamlToken;
const envProbe = tokenFromEnvironment();
if (envProbe.present) {
const token = envProbe.source === "GH_TOKEN" ? process.env.GH_TOKEN ?? null : process.env.GITHUB_TOKEN ?? null;
return { token, probe: envProbe };
}
if (!allowGhFallback) return { token: null, probe: envProbe };
if (yamlToken.probe.present) return yamlToken;
if (!allowGhFallback) return { token: null, probe: yamlToken.probe };
const ghPath = ghBinaryPath();
if (ghPath === null) return { token: null, probe: { present: false, source: null, ghFallbackAttempted: true, ghBinaryFound: false, ghAuthTokenAvailable: null } };
if (ghPath === null) return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: false, ghAuthTokenAvailable: null } };
const token = ghAuthToken();
if (token !== null) return { token, probe: { present: true, source: "gh-auth-token", ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: true } };
return { token: null, probe: { present: false, source: null, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
}
function readGitHubYamlTokenSourceConfig(): GitHubYamlTokenSourceConfig | null {
const configPath = rootPath(UNIDESK_CLI_CONFIG_PATH);
if (!existsSync(configPath)) return null;
const parsed = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, UNIDESK_CLI_CONFIG_PATH);
const github = optionalRecord(parsed.github, `${UNIDESK_CLI_CONFIG_PATH}.github`);
const auth = optionalRecord(github?.auth, `${UNIDESK_CLI_CONFIG_PATH}.github.auth`);
const tokenSource = optionalRecord(auth?.tokenSource, `${GITHUB_TOKEN_SOURCE_CONFIG_REF}`);
if (tokenSource === null) return null;
return {
enabled: booleanField(tokenSource, "enabled", GITHUB_TOKEN_SOURCE_CONFIG_REF),
priority: tokenSourcePriorityField(tokenSource, "priority", GITHUB_TOKEN_SOURCE_CONFIG_REF),
root: stringField(tokenSource, "root", GITHUB_TOKEN_SOURCE_CONFIG_REF),
sourceRef: stringField(tokenSource, "sourceRef", GITHUB_TOKEN_SOURCE_CONFIG_REF),
sourceKey: envKeyField(tokenSource, "sourceKey", GITHUB_TOKEN_SOURCE_CONFIG_REF),
format: tokenSourceFormatField(tokenSource, "format", GITHUB_TOKEN_SOURCE_CONFIG_REF),
};
}
function parseYamlTokenSource(text: string, config: GitHubYamlTokenSourceConfig): Record<string, string> {
if (config.format === "raw-token") return { [config.sourceKey]: text.trim() };
return parseEnvFile(text);
}
function resolveYamlSourceRoot(root: string): string {
return isAbsolute(root) ? resolve(root) : rootPath(root);
}
function resolveYamlSourcePath(root: string, sourceRef: string): string {
if (isAbsolute(sourceRef) || sourceRef.includes("..")) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef must be a relative path without ..`);
const resolved = normalize(join(root, sourceRef));
if (resolved !== root && !resolved.startsWith(`${root}/`)) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef escapes configured root`);
return resolved;
}
function redactRepoPath(value: string): string {
const root = rootPath();
return value === root ? "." : value.startsWith(`${root}/`) ? relative(root, value) : value;
}
function fingerprintToken(value: string): string {
return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
return value as Record<string, unknown>;
}
function optionalRecord(value: unknown, path: string): Record<string, unknown> | null {
if (value === undefined || value === null) return null;
return asRecord(value, path);
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return value.trim();
}
function envKeyField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${path}.${key} must be an env key`);
return value;
}
function tokenSourcePriorityField(obj: Record<string, unknown>, key: string, path: string): "before-env" | "after-env" {
const value = stringField(obj, key, path);
if (value !== "before-env" && value !== "after-env") throw new Error(`${path}.${key} must be before-env or after-env`);
return value;
}
function tokenSourceFormatField(obj: Record<string, unknown>, key: string, path: string): "env" | "raw-token" {
const value = obj[key] === undefined ? "env" : stringField(obj, key, path);
if (value !== "env" && value !== "raw-token") throw new Error(`${path}.${key} must be env or raw-token`);
return value;
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
const value = obj[key];
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
return value;
}
export function repoParts(repo: string): { owner: string; name: string } {
+1 -1
View File
@@ -29,7 +29,7 @@ export async function authStatus(repo: string): Promise<GitHubCommandResult> {
};
}
degraded.push("missing-token");
return commandError("auth status", repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required", { details: probe }), {
return commandError("auth status", repo, errorPayload("missing-token", "GH_TOKEN, GITHUB_TOKEN, or the YAML-declared GitHub token source is required", { details: probe }), {
degraded,
gh: { binaryFound: ghPath !== null, path: ghPath },
token: probe,
+3 -3
View File
@@ -335,12 +335,12 @@ export async function githubGraphqlRequest<T>(
export function authRequired(repo: string, command: string, tokenProbe: GitHubTokenProbe): GitHubCommandResult | null {
if (!tokenProbe.present) {
if (tokenProbe.ghFallbackAttempted && tokenProbe.ghBinaryFound === false) {
return commandError(command, repo, errorPayload("missing-binary", "gh binary is missing and no GH_TOKEN/GITHUB_TOKEN is available", { details: tokenProbe }));
return commandError(command, repo, errorPayload("missing-binary", "gh binary is missing and no env or YAML GitHub token source is available", { details: tokenProbe }));
}
if (tokenProbe.ghFallbackAttempted && tokenProbe.ghBinaryFound === true && tokenProbe.ghAuthTokenAvailable === false) {
return commandError(command, repo, errorPayload("auth-failed", "gh auth token failed and no GH_TOKEN/GITHUB_TOKEN is available", { details: tokenProbe }));
return commandError(command, repo, errorPayload("auth-failed", "gh auth token failed and no env or YAML GitHub token source is available", { details: tokenProbe }));
}
return commandError(command, repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required", { details: tokenProbe }), {
return commandError(command, repo, errorPayload("missing-token", "GH_TOKEN, GITHUB_TOKEN, or the YAML-declared GitHub token source is required", { details: tokenProbe }), {
degraded: ["missing-token"],
token: tokenProbe,
});
+10 -1
View File
@@ -377,8 +377,17 @@ function renderAuthStatus(result: GitHubCommandResult): string {
const token = record(result.token);
const gh = record(result.gh);
const probes = record(result.probes);
const tokenDetail = [
`source=${ghText(token.source)}`,
`yaml=${ghText(token.yamlSourceEnabled)}`,
`priority=${ghText(token.yamlSourcePriority)}`,
`sourceRef=${ghText(token.sourceRef)}`,
`key=${ghText(token.sourceKey)}`,
`fingerprint=${ghText(token.tokenFingerprint)}`,
`ghFallback=${ghText(token.ghFallbackAttempted)}`,
].filter((item) => !item.endsWith("=-") && !item.endsWith("=undefined")).join(" ");
const rows = [
["token", token.present === true ? "ok" : "missing", `source=${ghText(token.source)} ghFallback=${ghText(token.ghFallbackAttempted)}`],
["token", token.present === true ? "ok" : "missing", tokenDetail],
["gh-binary", gh.binaryFound === true ? "ok" : "missing", `path=${ghText(gh.path)}`],
["rest-api", probeStatus(probes.restApi), ghText(probes.restApi)],
["repo", probeStatus(probes.repo), probeDetail(probes.repo)],
+12 -1
View File
@@ -410,10 +410,21 @@ export type GitHubCommandFailure = GitHubCommandResult & { ok: false };
export interface GitHubTokenProbe {
present: boolean;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "gh-auth-token" | null;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "yaml-token-source" | "gh-auth-token" | null;
ghFallbackAttempted: boolean;
ghBinaryFound?: boolean;
ghAuthTokenAvailable?: boolean | null;
yamlSourceAttempted?: boolean;
yamlSourceEnabled?: boolean;
yamlSourcePriority?: "before-env" | "after-env";
configRef?: string;
sourceRef?: string;
sourceKey?: string;
sourcePath?: string;
sourceExists?: boolean;
sourceKeyPresent?: boolean;
tokenFingerprint?: string | null;
valuesPrinted?: false;
}
export interface GitHubOptions {
+57
View File
@@ -0,0 +1,57 @@
import { existsSync, readFileSync } from "node:fs";
import { isAbsolute, join } from "node:path";
import { rootPath } from "./config";
const hostK8sConfigPath = "config/unidesk-host-k8s.yaml";
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return value;
}
function readEnvFile(path: string): Record<string, string> {
const values: Record<string, string> = {};
const text = readFileSync(path, "utf8");
for (const [index, rawLine] of text.split(/\r?\n/u).entries()) {
const line = rawLine.trim();
if (line.length === 0 || line.startsWith("#")) continue;
const eq = line.indexOf("=");
if (eq <= 0) throw new Error(`${path}:${index + 1} must be KEY=value`);
values[line.slice(0, eq)] = line.slice(eq + 1);
}
return values;
}
function readHostK8sConfig(): Record<string, unknown> | null {
const path = rootPath(hostK8sConfigPath);
if (!existsSync(path)) return null;
return asRecord(Bun.YAML.parse(readFileSync(path, "utf8")) as unknown, hostK8sConfigPath);
}
export function readHostK8sPublicHost(): string | null {
const config = readHostK8sConfig();
if (config === null) return null;
const runtime = asRecord(config.runtime, `${hostK8sConfigPath}.runtime`);
return stringField(runtime, "publicHost", `${hostK8sConfigPath}.runtime`);
}
export function readHostK8sSshClientToken(): string | null {
const config = readHostK8sConfig();
if (config === null) return null;
const runtimeSecrets = asRecord(config.runtimeSecrets, `${hostK8sConfigPath}.runtimeSecrets`);
const sourceRef = asRecord(runtimeSecrets.sourceRef, `${hostK8sConfigPath}.runtimeSecrets.sourceRef`);
const target = asRecord(runtimeSecrets.target, `${hostK8sConfigPath}.runtimeSecrets.target`);
const keys = asRecord(target.keys, `${hostK8sConfigPath}.runtimeSecrets.target.keys`);
const sourcePathRaw = stringField(sourceRef, "path", `${hostK8sConfigPath}.runtimeSecrets.sourceRef`);
const tokenKey = stringField(keys, "sshClientToken", `${hostK8sConfigPath}.runtimeSecrets.target.keys`);
const sourcePath = isAbsolute(sourcePathRaw) ? sourcePathRaw : join(rootPath(), sourcePathRaw);
if (!existsSync(sourcePath)) return null;
const token = readEnvFile(sourcePath)[tokenKey]?.trim() ?? "";
return token.length > 0 ? token : null;
}
+2 -4
View File
@@ -1817,10 +1817,7 @@ function tektonGitWorkspaceSecretSpec(
if (sourceRefFrom !== "gitMirror.githubTransport") {
throw new Error(`${path}.sourceRefFrom must be gitMirror.githubTransport`);
}
if (githubTransport.mode !== "ssh") {
throw new Error(`${path}.sourceRefFrom=gitMirror.githubTransport requires gitMirror.githubTransport.mode=ssh`);
}
if (githubTransport.knownHostsSecretKey === null) {
if (githubTransport.mode === "ssh" && githubTransport.knownHostsSecretKey === null) {
throw new Error(`${path}.sourceRefFrom=gitMirror.githubTransport requires gitMirror.githubTransport.knownHostsSecretKey`);
}
const name = stringField(raw, "name", path);
@@ -2301,6 +2298,7 @@ function gitMirrorGithubHttpsToken(transport: Extract<ControlPlaneGitMirrorGithu
const source = readEnvSourceFile({
root: absolute ? "/" : rootPath("."),
sourceRef: absolute ? transport.tokenSourceRef.slice(1) : transport.tokenSourceRef,
rawKey: transport.tokenSourceKey,
missingMessage: () => `gitMirror.githubTransport token source ${transport.tokenSourceRef} is missing; create the YAML-declared sourceRef with ${transport.tokenSourceKey} before applying the control plane`,
});
const value = requiredEnvValue(source.values, transport.tokenSourceKey, transport.tokenSourceRef);
+37 -11
View File
@@ -476,6 +476,23 @@ export function nodeRuntimeStatusDegradedReason(input: {
}
export function nodeRuntimePublicProbeStatus(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
if (spec.publicExposure?.enabled !== true) {
return {
ready: true,
skipped: true,
reason: "public-exposure-not-configured",
web: { kind: "web", url: spec.publicWebUrl, ok: true, skipped: true, httpStatus: null },
apiHealth: { kind: "apiHealth", url: joinUrlPath(spec.publicApiUrl, "/health/live"), ok: true, skipped: true, httpStatus: null },
targetHost: { ready: true, skipped: true, probeAvailable: false },
diagnostic: {
kind: "public-entry-probe-skipped",
affectsUserEntry: false,
targetHostReady: true,
message: "publicExposure is not configured for this node/lane; public endpoint readiness is not a deployment gate.",
nextAction: null,
},
};
}
const web = publicHttpProbe("web", spec.publicWebUrl);
const apiHealth = publicHttpProbe("apiHealth", joinUrlPath(spec.publicApiUrl, "/health/live"));
const ready = web.ok === true && apiHealth.ok === true;
@@ -1896,10 +1913,11 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" if (!fs.existsSync(file)) return false;",
" const doc = readYaml(file) || {};",
" const resources = Array.isArray(doc.resources) ? doc.resources : [];",
" const publicExposureEnabled = overlay.publicExposure && overlay.publicExposure.enabled;",
" const next = resources.filter((item) => {",
" if (!(overlay.observability && overlay.observability.prometheusOperator === false)) return true;",
" const resource = String(item);",
" if (resource === 'observability.yaml') return false;",
" if (!publicExposureEnabled && resource === 'node-frpc.yaml') return false;",
" if (overlay.observability && overlay.observability.prometheusOperator === false && resource === 'observability.yaml') return false;",
" return !(/\\.ya?ml$/u.test(resource) && !fs.existsSync(path.join(runtimePath, resource)));",
" });",
" let changed = false;",
@@ -1918,7 +1936,6 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" const items = listItems(doc).filter(Boolean);",
" let changed = false;",
" const endpointIsIpv4 = /^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$/.test(String(access.endpointAddress));",
" const endpointSliceName = String(pg.serviceName) + '-host';",
" for (const item of items) {",
" if (!item || typeof item !== 'object') continue;",
" item.metadata = item.metadata || {};",
@@ -1946,11 +1963,14 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" }",
" if (item.kind === 'EndpointSlice') {",
" if (!endpointIsIpv4) { item.__delete = true; changed = true; continue; }",
" item.metadata.name = endpointSliceName;",
" item.apiVersion = 'v1';",
" item.kind = 'Endpoints';",
" item.metadata.name = pg.serviceName;",
" item.metadata.labels['kubernetes.io/service-name'] = pg.serviceName;",
" item.addressType = 'IPv4';",
" item.ports = [{ name: 'postgres', port: access.port, protocol: 'TCP' }];",
" item.endpoints = [{ addresses: [access.endpointAddress], conditions: { ready: true } }];",
" delete item.addressType;",
" delete item.ports;",
" delete item.endpoints;",
" item.subsets = [{ addresses: [{ ip: access.endpointAddress }], ports: [{ name: 'postgres', port: access.port, protocol: 'TCP' }] }];",
" changed = true;",
" }",
" }",
@@ -2015,8 +2035,12 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"}",
"function patchPublicExposure() {",
" const exposure = overlay.publicExposure;",
" if (!exposure || !exposure.enabled) return { configured: false, changed: false };",
" const file = path.join(runtimePath, 'node-frpc.yaml');",
" if (!exposure || !exposure.enabled) {",
" const changed = fs.existsSync(file);",
" if (changed) fs.rmSync(file, { force: true });",
" return { configured: false, changed };",
" }",
" if (!fs.existsSync(file)) {",
" console.error(JSON.stringify({ event: 'unidesk-public-exposure-postprocess', ok: false, reason: 'node-frpc-yaml-missing', filePath: file, hostname: exposure.hostname }));",
" process.exit(49);",
@@ -2253,6 +2277,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" if (!fs.existsSync(file)) fail('external-postgres-missing');",
" const items = listItems(readYaml(file)).filter(Boolean);",
" const service = items.find((item) => item && item.kind === 'Service' && item.metadata && item.metadata.name === pg.serviceName);",
" const endpoints = items.find((item) => item && item.kind === 'Endpoints' && item.metadata && item.metadata.name === String(pg.serviceName));",
" const endpointSlice = items.find((item) => item && item.kind === 'EndpointSlice' && item.metadata && item.metadata.name === String(pg.serviceName) + '-host');",
" if (!service) fail('external-postgres-service-missing', { expected: pg.serviceName });",
" const endpointIsIpv4 = /^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$/.test(String(access.endpointAddress));",
@@ -2262,11 +2287,12 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" if (!service.spec || String(service.spec.externalName) !== String(access.endpointAddress)) fail('external-postgres-external-name-mismatch', { value: service.spec && service.spec.externalName, expected: access.endpointAddress });",
" if (Number(servicePort) !== Number(access.port)) fail('external-postgres-port-mismatch', { servicePort, expectedPort: access.port });",
" if (endpointSlice) fail('external-postgres-endpointslice-unexpected', { name: String(pg.serviceName) + '-host' });",
" if (endpoints) fail('external-postgres-endpoints-unexpected', { name: String(pg.serviceName) });",
" checks.push('external-postgres-bridge');",
" } else {",
" if (!endpointSlice) fail('external-postgres-endpointslice-missing', { expected: String(pg.serviceName) + '-host' });",
" const endpointPort = Array.isArray(endpointSlice.ports) && endpointSlice.ports[0] ? endpointSlice.ports[0].port : null;",
" const endpointAddress = Array.isArray(endpointSlice.endpoints) && endpointSlice.endpoints[0] && Array.isArray(endpointSlice.endpoints[0].addresses) ? endpointSlice.endpoints[0].addresses[0] : null;",
" if (!endpoints && !endpointSlice) fail('external-postgres-endpoints-missing', { expected: String(pg.serviceName) });",
" const endpointPort = endpoints && Array.isArray(endpoints.subsets) && endpoints.subsets[0] && Array.isArray(endpoints.subsets[0].ports) && endpoints.subsets[0].ports[0] ? endpoints.subsets[0].ports[0].port : endpointSlice && Array.isArray(endpointSlice.ports) && endpointSlice.ports[0] ? endpointSlice.ports[0].port : null;",
" const endpointAddress = endpoints && Array.isArray(endpoints.subsets) && endpoints.subsets[0] && Array.isArray(endpoints.subsets[0].addresses) && endpoints.subsets[0].addresses[0] ? endpoints.subsets[0].addresses[0].ip : endpointSlice && Array.isArray(endpointSlice.endpoints) && endpointSlice.endpoints[0] && Array.isArray(endpointSlice.endpoints[0].addresses) ? endpointSlice.endpoints[0].addresses[0] : null;",
" if (Number(servicePort) !== Number(access.port) || Number(endpointPort) !== Number(access.port)) fail('external-postgres-port-mismatch', { servicePort, endpointPort, expectedPort: access.port });",
" if (String(endpointAddress) !== String(access.endpointAddress)) fail('external-postgres-address-mismatch', { endpointAddress, expectedEndpointAddress: access.endpointAddress });",
" checks.push('external-postgres-bridge');",
+19 -13
View File
@@ -62,7 +62,13 @@ function shellUrlEncodeFunction(): string[] {
export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
if (options.dryRun && options.confirm) throw new Error("control-plane allow-endpoint-bridge accepts only one of --dry-run or --confirm");
const dryRun = options.dryRun || !options.confirm;
const result = runTransScript(options.node, endpointBridgeScript({ lane: options.lane, dryRun }), "", options.timeoutSeconds);
const secretSpec = runtimeSecretSpec({ node: options.node, lane: options.lane });
const result = runTransScript(options.node, endpointBridgeScript({
lane: options.lane,
dryRun,
platformService: secretSpec.platformPostgresService,
hostEndpointSlice: secretSpec.platformPostgresEndpointSlice,
}), "", options.timeoutSeconds);
const fields = keyValueLinesFromText(statusText(result));
const beforeExcluded = fields.beforeEndpointResourcesExcluded === "yes";
const beforeIgnored = fields.beforeEndpointsIgnoreUpdates === "yes" || fields.beforeEndpointSliceIgnoreUpdates === "yes";
@@ -74,8 +80,8 @@ export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScoped
const afterLegacyEndpoints = fields.afterLegacyEndpointsExists === "yes";
const beforeHostEndpointSlice = fields.beforeHostEndpointSliceExists === "yes";
const afterHostEndpointSlice = fields.afterHostEndpointSliceExists === "yes";
const bridgeReady = !afterLegacyEndpoints && afterHostEndpointSlice && afterExtraEndpointSlices.length === 0;
const ok = result.exitCode === 0 && !afterExcluded && !afterIgnored && bridgeReady;
const bridgeReady = (afterLegacyEndpoints || afterHostEndpointSlice) && afterExtraEndpointSlices.length === 0;
const ok = result.exitCode === 0 && !afterExcluded && !afterIgnored;
return {
ok: dryRun ? result.exitCode === 0 : ok,
command: "hwlab nodes control-plane allow-endpoint-bridge",
@@ -105,8 +111,8 @@ export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScoped
extraEndpointSlices: afterExtraEndpointSlices,
},
runtimeNamespace: fields.runtimeNamespace || `hwlab-${options.lane}`,
platformService: fields.platformService || "g14-platform-postgres",
hostEndpointSlice: fields.hostEndpointSlice || "g14-platform-postgres-host",
platformService: fields.platformService || secretSpec.platformPostgresService,
hostEndpointSlice: fields.hostEndpointSlice || secretSpec.platformPostgresEndpointSlice,
patchExitCode: numericField(fields.patchExitCode),
rolloutRestartExitCode: numericField(fields.rolloutRestartExitCode),
rolloutStatusExitCode: numericField(fields.rolloutStatusExitCode),
@@ -115,15 +121,16 @@ export function runNodeEndpointBridge(options: ReturnType<typeof parseNodeScoped
refreshExitCode: numericField(fields.refreshExitCode),
exitCode: result.exitCode,
stderr: result.exitCode === 0 ? "" : result.stderr.trim().slice(0, 2000),
summary: !afterExcluded && !afterIgnored && bridgeReady
? "Argo tracks HWLAB external Postgres EndpointSlice and no legacy Endpoints remain"
: "Argo endpoint bridge is not in final Service plus EndpointSlice shape",
bridgeReady,
summary: !afterExcluded && !afterIgnored
? "Argo tracks HWLAB external Postgres bridge resource"
: "Argo endpoint bridge resources are still excluded or ignored",
},
result: compactCommandResult(result),
};
}
export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun: boolean }): string {
export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun: boolean; platformService: string; hostEndpointSlice: string }): string {
const application = `hwlab-node-${options.lane}`;
const runtimeNamespace = `hwlab-${options.lane}`;
return [
@@ -133,8 +140,8 @@ export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun:
"configmap=argocd-cm",
`application=${shellQuote(application)}`,
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
"platform_service=g14-platform-postgres",
"host_endpointslice=g14-platform-postgres-host",
`platform_service=${shellQuote(options.platformService)}`,
`host_endpointslice=${shellQuote(options.hostEndpointSlice)}`,
"preset=endpoint-bridge-resource-tracking",
"cm_data() { kubectl -n \"$namespace\" get configmap \"$configmap\" -o \"go-template={{ index .data \\\"$1\\\" }}\" 2>/dev/null || true; }",
"cm_has_key() { value=$(cm_data \"$1\"); [ -n \"$value\" ] && [ \"$value\" != \"<no value>\" ] && printf yes || printf no; }",
@@ -146,7 +153,7 @@ export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun:
" current_legacy=$(resource_exists endpoints \"$platform_service\")",
" current_extra=$(extra_endpoint_slices)",
" current_host=$(resource_exists endpointslice \"$host_endpointslice\")",
" if [ \"$current_legacy\" != yes ] && [ -z \"$current_extra\" ] && [ \"$current_host\" = yes ]; then return 0; fi",
" if { [ \"$current_legacy\" = yes ] || [ \"$current_host\" = yes ]; } && [ -z \"$current_extra\" ]; then return 0; fi",
" sleep 2",
" done",
" return 1",
@@ -310,7 +317,6 @@ export function endpointBridgeScript(options: { lane: HwlabRuntimeLane; dryRun:
"printf 'deleteExtraEndpointSlicesExitCode\\t%s\\n' \"$delete_extra_endpointslices_exit\"",
"printf 'refreshExitCode\\t%s\\n' \"$refresh_exit\"",
"if [ \"$dry_run\" != true ] && { [ \"$after_endpoint_resources_excluded\" = yes ] || [ \"$after_endpoints_ignore_updates\" = yes ] || [ \"$after_endpoint_slice_ignore_updates\" = yes ]; }; then exit 46; fi",
"if [ \"$dry_run\" != true ] && { [ \"$after_legacy_endpoints_exists\" = yes ] || [ -n \"$after_extra_endpoint_slice_names\" ] || [ \"$after_host_endpointslice_exists\" != yes ]; }; then exit 47; fi",
"if [ -n \"$patch_exit\" ] && [ \"$patch_exit\" != 0 ]; then exit \"$patch_exit\"; fi",
"if [ -n \"$rollout_restart_exit\" ] && [ \"$rollout_restart_exit\" != 0 ]; then exit \"$rollout_restart_exit\"; fi",
"if [ -n \"$rollout_status_exit\" ] && [ \"$rollout_status_exit\" != 0 ]; then exit \"$rollout_status_exit\"; fi",
+11 -4
View File
@@ -1706,14 +1706,20 @@ export function externalPostgresBridgeStatus(spec: HwlabRuntimeLaneSpec, namespa
service: compactRuntimeCommand(service),
};
}
const endpointSlice = runNodeK3sArgs(spec, ["kubectl", "-n", spec.runtimeNamespace, "get", "endpointslice", `${pg.serviceName}-host`, "-o", "jsonpath={.addressType}{\"\\n\"}{.endpoints[0].addresses[0]}{\"\\n\"}{.ports[0].port}{\"\\n\"}"], 60);
const [addressType = "", address = "", port = ""] = endpointSlice.stdout.split(/\r?\n/u);
const endpoints = runNodeK3sArgs(spec, ["kubectl", "-n", spec.runtimeNamespace, "get", "endpoints", pg.serviceName, "-o", "jsonpath={.subsets[0].addresses[0].ip}{\"\\n\"}{.subsets[0].ports[0].port}{\"\\n\"}"], 60);
const [endpointsAddress = "", endpointsPort = ""] = endpoints.stdout.split(/\r?\n/u);
const endpointSlice = endpoints.exitCode === 0 && endpointsAddress.length > 0
? { exitCode: 1, stdout: "", stderr: "", timedOut: false }
: runNodeK3sArgs(spec, ["kubectl", "-n", spec.runtimeNamespace, "get", "endpointslice", `${pg.serviceName}-host`, "-o", "jsonpath={.addressType}{\"\\n\"}{.endpoints[0].addresses[0]}{\"\\n\"}{.ports[0].port}{\"\\n\"}"], 60);
const [addressType = "", endpointSliceAddress = "", endpointSlicePort = ""] = endpointSlice.stdout.split(/\r?\n/u);
const address = endpointsAddress || endpointSliceAddress;
const port = endpointsPort || endpointSlicePort;
return {
required: true,
ready: service.exitCode === 0 && endpointSlice.exitCode === 0 && serviceType !== "ExternalName" && address === runtimeAccess.endpointAddress && Number(port) === runtimeAccess.port,
ready: service.exitCode === 0 && (endpoints.exitCode === 0 || endpointSlice.exitCode === 0) && serviceType !== "ExternalName" && address === runtimeAccess.endpointAddress && Number(port) === runtimeAccess.port,
serviceName: pg.serviceName,
serviceType: serviceType || null,
addressType: addressType || null,
addressType: endpoints.exitCode === 0 ? "IPv4" : addressType || null,
endpointAddress: address || null,
expectedEndpointAddress: runtimeAccess.endpointAddress,
port: numericField(port),
@@ -1722,6 +1728,7 @@ export function externalPostgresBridgeStatus(spec: HwlabRuntimeLaneSpec, namespa
providerPort: pg.port,
runtimeAccess: pg.runtimeAccess ?? null,
service: compactRuntimeCommand(service),
endpoints: compactRuntimeCommand(endpoints),
endpointSlice: compactRuntimeCommand(endpointSlice),
};
}
+12 -4
View File
@@ -49,8 +49,9 @@ process.stderr.on("error", (error) => {
});
export function emitJson<T>(command: string, data: T, ok = true): void {
const envelope: JsonEnvelope<T> = { ok, command, data };
safeStdoutWrite(renderEnvelope(command, envelope));
const safeCommand = redactSensitiveCommandArgs(command);
const envelope: JsonEnvelope<T> = { ok, command: safeCommand, data };
safeStdoutWrite(renderEnvelope(safeCommand, envelope));
}
export function isRenderedCliResult(value: unknown): value is RenderedCliResult {
@@ -67,8 +68,15 @@ export function emitText(text: string, command = "text"): void {
export function emitError(command: string, error: unknown): void {
const payload = normalizeErrorPayload(command, error);
const envelope: JsonEnvelope<never> = { ok: false, command, error: payload };
safeStdoutWrite(renderEnvelope(command, envelope));
const safeCommand = redactSensitiveCommandArgs(command);
const envelope: JsonEnvelope<never> = { ok: false, command: safeCommand, error: payload };
safeStdoutWrite(renderEnvelope(safeCommand, envelope));
}
function redactSensitiveCommandArgs(command: string): string {
return command
.replace(/(--(?:provider-token|token|password|auth-password|session-secret|ssh-client-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/giu, "$1<redacted>")
.replace(/\b((?:PROVIDER_TOKEN|UNIDESK_PROVIDER_TOKEN|UNIDESK_SSH_CLIENT_TOKEN|AUTH_PASSWORD|SESSION_SECRET)=)("[^"]*"|'[^']*'|\S+)/gu, "$1<redacted>");
}
export function readCliOutputPolicy(): CliOutputPolicy {
+7 -2
View File
@@ -30,6 +30,9 @@ capture_json() {
run_apply() {
manifest="$tmp/gitea.k8s.yaml"
printf '%s' "$UNIDESK_GITEA_MANIFEST_B64" | base64 -d >"$manifest"
if [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
kubectl create namespace "$UNIDESK_GITEA_NAMESPACE" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f - >"$tmp/namespace.out" 2>"$tmp/namespace.err"
fi
if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
printf '%s' "$UNIDESK_GITEA_FRPC_TOML_B64" | base64 -d >"$tmp/frpc.toml"
kubectl -n "$UNIDESK_GITEA_NAMESPACE" create secret generic "$UNIDESK_GITEA_FRPC_SECRET_NAME" \
@@ -63,11 +66,13 @@ run_apply() {
fi
fi
dry_arg=""
apply_args="--server-side --force-conflicts"
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
dry_arg="--dry-run=server"
dry_arg="--dry-run=client"
apply_args=""
fi
if [ "$frpc_rc" -eq 0 ] && [ "$webhook_secret_rc" -eq 0 ]; then
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply --server-side $dry_arg --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply $apply_args $dry_arg --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
apply_rc=$?
else
apply_rc=1
+61 -12
View File
@@ -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`);
+41 -7
View File
@@ -42,7 +42,7 @@ interface HostProxyServer {
interface HostProxySource {
id: string;
sourceType: "benchmark-validated-master-shadowsocks";
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks";
serverRef: string;
benchmarkRef: string;
implementationRef: string;
@@ -167,7 +167,7 @@ function readHostProxyConfig(): HostProxyConfig {
const server = parseServer(record(root.server, `${configPath}.server`), `${configPath}.server`);
const sources = Object.fromEntries(Object.entries(record(root.sources, `${configPath}.sources`)).map(([id, value]) => {
const source = parseSource(id, record(value, `${configPath}.sources.${id}`));
if (source.serverRef !== `server.${server.id}`) throw new Error(`${configPath}.sources.${id}.serverRef must be server.${server.id}`);
if (sourceUsesManagedServer(source) && source.serverRef !== `server.${server.id}`) throw new Error(`${configPath}.sources.${id}.serverRef must be server.${server.id}`);
return [id, source];
}));
const targets: Record<string, HostProxyTarget> = {};
@@ -232,7 +232,7 @@ function parseServer(raw: Record<string, unknown>, label: string): HostProxyServ
}
function parseSource(id: string, raw: Record<string, unknown>): HostProxySource {
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks"] as const);
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks"] as const);
const sourceConfigRef = stringField(raw, "sourceConfigRef", `${configPath}.sources.${id}`);
const resolved = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.sources.${id}.sourceConfigRef`);
if (resolved.sourceType !== "master-shadowsocks" || resolved.masterShadowsocks === null) {
@@ -389,7 +389,7 @@ function plan(server: HostProxyServer, target: HostProxyTarget): Record<string,
function apply(server: HostProxyServer, target: HostProxyTarget): Record<string, unknown> {
const material = proxySecretMaterial(server, target.source);
const serverApply = applyMasterProxyServer(server, material);
const serverApply = sourceUsesManagedServer(target.source) ? applyMasterProxyServer(server, material) : skippedMasterProxyServer(target.source, "apply");
const artifact = prepareClientArtifact(target.source.client);
const remotePrepare = runTrans(target.route, remotePrepareScript(target.source.client), 30);
const remoteArchive = remotePrepare.exitCode === 0
@@ -429,7 +429,7 @@ function apply(server: HostProxyServer, target: HostProxyTarget): Record<string,
}
function status(server: HostProxyServer, target: HostProxyTarget): Record<string, unknown> {
const serverStatus = masterProxyServerStatus(server);
const serverStatus = sourceUsesManagedServer(target.source) ? masterProxyServerStatus(server) : skippedMasterProxyServer(target.source, "status");
const result = runTrans(target.route, remoteStatusScript(target), 45);
const parsed = parseJson(result.stdout);
return {
@@ -446,6 +446,34 @@ function status(server: HostProxyServer, target: HostProxyTarget): Record<string
};
}
function sourceUsesManagedServer(source: HostProxySource): boolean {
return source.sourceType === "benchmark-validated-master-shadowsocks";
}
function skippedMasterProxyServer(source: HostProxySource, mode: "apply" | "status"): Record<string, unknown> {
return {
ok: true,
id: source.id,
sourceType: source.sourceType,
enabled: false,
mode,
skipped: true,
reason: `source ${source.id} uses externally managed ${source.sourceType} server`,
benchmarkRef: source.benchmarkRef,
implementationRef: source.implementationRef,
sourceConfigRef: source.sourceConfigRef,
sourceFingerprint: source.sourceFingerprint,
materialFingerprint: null,
compose: null,
existingHealthy: null,
inspect: null,
container: "external",
listen: `${source.masterShadowsocks.serverHost}:${source.masterShadowsocks.serverPort}`,
health: { ok: true, mode: "external", host: "", port: 0 },
valuesPrinted: false,
};
}
function proxySecretMaterial(server: HostProxyServer, source: HostProxySource): HostProxySecretMaterial {
const envSource = readEnvSourceFile({
root: rootPath(".state", "secrets"),
@@ -951,9 +979,12 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
const client = record(remote.client);
const podProbe = record(remote.podAccessProbe);
const probe = record(remote.externalProbe);
const serverRows = serverApply.enabled === false
? table(["SOURCE", "OK", "UPSTREAM", "TYPE"], [[text(serverApply.id), text(serverApply.ok), text(serverApply.listen), text(serverApply.sourceType)]])
: table(["SERVER", "OK", "LISTEN", "BENCHMARK"], [[text(server.id), text(serverApply.ok), text(server.listen), text(server.benchmarkRef)]]);
const lines = [
"PLATFORM-INFRA HOST PROXY APPLY",
...table(["SERVER", "OK", "LISTEN", "BENCHMARK"], [[text(server.id), text(serverApply.ok), text(server.listen), text(server.benchmarkRef)]]),
...serverRows,
"",
...table(["SERVER_HEALTH", "CLIENT_BINARY", "DISTRIBUTION"], [[text(serverHealth.ok), text(artifact.binaryAction), text(distribution.mode)]]),
"",
@@ -979,9 +1010,12 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
const health = record(remote.health);
const probe = record(remote.externalProbe);
const podProbe = record(remote.podAccessProbe);
const serverRows = serverStatus.enabled === false
? table(["SOURCE", "OK", "UPSTREAM", "TYPE"], [[text(serverStatus.id), text(serverStatus.ok), text(serverStatus.listen), text(serverStatus.sourceType)]])
: table(["SERVER", "OK", "LISTEN", "CONTAINER"], [[text(server.id), text(serverStatus.ok), text(server.listen), text(serverStatus.container)]]);
const lines = [
"PLATFORM-INFRA HOST PROXY STATUS",
...table(["SERVER", "OK", "LISTEN", "CONTAINER"], [[text(server.id), text(serverStatus.ok), text(server.listen), text(serverStatus.container)]]),
...serverRows,
"",
...table(["TARGET", "ROUTE", "OK", "CLIENT"], [[text(target.id), text(target.route), result.ok === false ? "false" : "true", text(client.service)]]),
"",
@@ -70,6 +70,23 @@ prepare_gitea_api_base() {
export UNIDESK_PAC_GITEA_API_BASE_URL
}
resolve_service_url_to_cluster_ip() {
value="$1"
hostport=$(printf '%s' "$value" | sed -n 's#^http://\([^/]*\).*$#\1#p')
if ! printf '%s' "$hostport" | grep -q '\.svc\.cluster\.local'; then
printf '%s' "$value"
return
fi
host=${hostport%%:*}
port=${hostport##*:}
if [ "$port" = "$hostport" ]; then port=80; fi
service=${host%%.*}
rest=${host#*.}
namespace=${rest%%.*}
cluster_ip=$(kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}')
printf '%s' "$value" | sed "s#^http://$hostport#http://$cluster_ip:$port#"
}
gitea_api() {
method="$1"
path="$2"
@@ -118,6 +135,7 @@ NODE
}
repository_manifest() {
repository_url=$(resolve_service_url_to_cluster_ip "$UNIDESK_PAC_REPOSITORY_URL")
cat <<EOF
apiVersion: pipelinesascode.tekton.dev/v1alpha1
kind: Repository
@@ -129,7 +147,7 @@ metadata:
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
unidesk.ai/spec: $UNIDESK_PAC_SPEC
spec:
url: $UNIDESK_PAC_REPOSITORY_URL
url: $repository_url
git_provider:
type: gitea
url: $UNIDESK_PAC_GITEA_BASE_URL
+3 -1
View File
@@ -32,6 +32,7 @@ import {
type SshRemoteCommandExecutor,
type SshRemoteCommandStreamHandlers,
} from "./ssh-file-transfer";
import { readHostK8sSshClientToken } from "./host-k8s-config";
interface FrontendSession {
baseUrl: string;
@@ -282,7 +283,8 @@ async function loginFrontend(host: string, config: UniDeskConfig): Promise<Front
function sshClientTokenFromEnv(env: NodeJS.ProcessEnv = process.env): string | null {
const token = env.UNIDESK_SSH_CLIENT_TOKEN?.trim() ?? "";
return token.length > 0 ? token : null;
if (token.length > 0) return token;
return readHostK8sSshClientToken();
}
function scopedSshFrontendSession(host: string, config: UniDeskConfig, token: string): FrontendSession {
+3 -2
View File
@@ -716,16 +716,17 @@ export function readTextFile(path: string): string {
return readFileSync(path, "utf8");
}
export function readEnvSourceFile(params: { root: string; sourceRef: string; missingMessage?: (sourcePath: string) => string }): EnvSourceFileMaterial {
export function readEnvSourceFile(params: { root: string; sourceRef: string; rawKey?: string; missingMessage?: (sourcePath: string) => string }): EnvSourceFileMaterial {
const sourcePath = join(params.root, params.sourceRef);
if (!existsSync(sourcePath)) {
throw new Error(params.missingMessage?.(sourcePath) ?? `required secret source ${redactRepoPath(sourcePath)} is missing`);
}
const text = readFileSync(sourcePath, "utf8");
return {
sourceRef: params.sourceRef,
sourcePath,
sourcePathRedacted: redactRepoPath(sourcePath),
values: parseEnvFile(readFileSync(sourcePath, "utf8")),
values: params.rawKey === undefined ? parseEnvFile(text) : { [params.rawKey]: text.trim() },
valuesPrinted: false,
};
}
+2
View File
@@ -24,6 +24,7 @@ import {
import { readTransHostProxyEnvRule, type TransHostProxyEnvRule } from "./trans-host-proxy";
import { readTransSshBackendConfig, type TransSshBackendConfig } from "./trans-config";
import { readCliOutputPolicy } from "./output";
import { readHostK8sPublicHost } from "./host-k8s-config";
export interface ParsedSshArgs {
remoteCommand: string | null;
@@ -3743,6 +3744,7 @@ function sshCaptureRemoteHost(config: UniDeskConfig, env: NodeJS.ProcessEnv): st
return normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_IP)
?? normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_HOST)
?? normalizeRemoteHost(env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST)
?? normalizeRemoteHost(readHostK8sPublicHost() ?? undefined)
?? normalizeRemoteHost(config.network.publicHost);
}