fix: 支持 AgentRun render-only REST 入口 (#170)
Co-authored-by: AgentRun Codex <agentrun-codex@users.noreply.github.com>
This commit is contained in:
@@ -44,6 +44,27 @@ interface EnvVarValue {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SecretEnvVarValue {
|
||||
name: string;
|
||||
secretRef: {
|
||||
name: string;
|
||||
key: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface FrpTcpExposure {
|
||||
enabled: true;
|
||||
kind: "frp-tcp";
|
||||
name: string;
|
||||
serverAddr: string;
|
||||
serverPort: number;
|
||||
localIP: string;
|
||||
localPort: number;
|
||||
remotePort: number;
|
||||
publicBaseUrl: string;
|
||||
masterBaseUrl: string;
|
||||
}
|
||||
|
||||
const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git";
|
||||
const unideskSshEndpointEnvNames = new Set(["UNIDESK_MAIN_SERVER_IP", "UNIDESK_MAIN_SERVER_HOST", "UNIDESK_FRONTEND_URL"]);
|
||||
|
||||
@@ -64,6 +85,8 @@ export async function renderGitops(options: RenderOptions): Promise<JsonRecord>
|
||||
const gitopsBranch = stringField(deploy, "gitopsBranch", "v0.1-gitops");
|
||||
const runtimePath = stringField(deploy, "runtimePath", "deploy/gitops/g14/runtime-v01");
|
||||
const unideskSshEndpointEnv = optionalUnideskSshEndpointEnv(deploy);
|
||||
const managerApiKeyEnv = managerApiKeySecretEnv(deploy);
|
||||
const publicExposure = optionalFrpTcpExposure(deploy);
|
||||
const catalog = await loadCatalog(options, gitopsBranch);
|
||||
const image = imageForService(catalog, "agentrun-mgr", options);
|
||||
|
||||
@@ -77,9 +100,9 @@ export async function renderGitops(options: RenderOptions): Promise<JsonRecord>
|
||||
await writeFile(path.join(options.outDir, "runtime-v01", "kustomization.yaml"), kustomizationYaml());
|
||||
await writeFile(path.join(options.outDir, "runtime-v01", "namespace.yaml"), namespaceYaml(runtimeNamespace));
|
||||
await writeFile(path.join(options.outDir, "runtime-v01", "postgres.yaml"), postgresYaml(runtimeNamespace));
|
||||
await writeFile(path.join(options.outDir, "runtime-v01", "mgr.yaml"), managerYaml(runtimeNamespace, image, options.sourceCommit, unideskSshEndpointEnv));
|
||||
await writeFile(path.join(options.outDir, "runtime-v01", "mgr.yaml"), managerYaml(runtimeNamespace, image, options.sourceCommit, unideskSshEndpointEnv, managerApiKeyEnv, publicExposure));
|
||||
await writeFile(path.join(options.outDir, "runtime-v01", "runner-rbac.yaml"), runnerRbacYaml(runtimeNamespace));
|
||||
return { outDir: options.outDir, runtimeNamespace, gitopsBranch, runtimePath, image: repositoryDigestForService(image), sourceCommit: options.sourceCommit, envIdentity: image.envIdentity ?? null, artifactStatus: image.status ?? null, unideskSshEndpointEnv: unideskSshEndpointEnv ? { name: unideskSshEndpointEnv.name, valuesPrinted: false } : null };
|
||||
return { outDir: options.outDir, runtimeNamespace, gitopsBranch, runtimePath, image: repositoryDigestForService(image), sourceCommit: options.sourceCommit, envIdentity: image.envIdentity ?? null, artifactStatus: image.status ?? null, unideskSshEndpointEnv: unideskSshEndpointEnv ? { name: unideskSshEndpointEnv.name, valuesPrinted: false } : null, managerAuth: { env: managerApiKeyEnv.name, secretRef: { name: managerApiKeyEnv.secretRef.name, key: managerApiKeyEnv.secretRef.key }, valuesPrinted: false }, publicExposure: publicExposure ? { kind: publicExposure.kind, name: publicExposure.name, remotePort: publicExposure.remotePort, masterBaseUrl: publicExposure.masterBaseUrl, publicBaseUrl: publicExposure.publicBaseUrl, valuesPrinted: false } : null };
|
||||
}
|
||||
|
||||
async function loadCatalog(options: RenderOptions, gitopsBranch: string): Promise<ArtifactCatalog> {
|
||||
@@ -246,10 +269,12 @@ spec:
|
||||
`;
|
||||
}
|
||||
|
||||
function managerYaml(namespace: string, image: CatalogService, sourceCommit: string, unideskSshEndpointEnv: EnvVarValue | null): string {
|
||||
function managerYaml(namespace: string, image: CatalogService, sourceCommit: string, unideskSshEndpointEnv: EnvVarValue | null, apiKeyEnv: SecretEnvVarValue, publicExposure: FrpTcpExposure | null): string {
|
||||
const imageRef = repositoryDigestForService(image);
|
||||
const envIdentity = image.envIdentity ?? image.imageTag ?? "unknown";
|
||||
const unideskSshEndpointEnvYaml = unideskSshEndpointEnv ? ` - name: ${unideskSshEndpointEnv.name}\n value: ${JSON.stringify(unideskSshEndpointEnv.value)}\n` : "";
|
||||
const apiKeyEnvYaml = ` - name: ${apiKeyEnv.name}\n valueFrom:\n secretKeyRef:\n name: ${apiKeyEnv.secretRef.name}\n key: ${apiKeyEnv.secretRef.key}\n`;
|
||||
const frpcYaml = publicExposure ? `---\n${frpTcpExposureYaml(namespace, publicExposure)}` : "";
|
||||
return `apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
@@ -322,6 +347,7 @@ spec:
|
||||
value: ${JSON.stringify(imageRef)}
|
||||
- name: AGENTRUN_RUNNER_SERVICE_ACCOUNT
|
||||
value: "agentrun-v01-runner"
|
||||
${apiKeyEnvYaml}
|
||||
${unideskSshEndpointEnvYaml}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
@@ -390,6 +416,70 @@ roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: agentrun-v01-mgr-provider-secret-manager
|
||||
${frpcYaml}
|
||||
`;
|
||||
}
|
||||
|
||||
function frpTcpExposureYaml(namespace: string, exposure: FrpTcpExposure): string {
|
||||
const configMapName = `${exposure.name}-config`;
|
||||
return `apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ${configMapName}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: agentrun
|
||||
app.kubernetes.io/name: ${exposure.name}
|
||||
app.kubernetes.io/part-of: agentrun
|
||||
data:
|
||||
frpc.toml: |
|
||||
serverAddr = ${JSON.stringify(exposure.serverAddr)}
|
||||
serverPort = ${exposure.serverPort}
|
||||
loginFailExit = true
|
||||
|
||||
[[proxies]]
|
||||
name = ${JSON.stringify(exposure.name)}
|
||||
type = "tcp"
|
||||
localIP = ${JSON.stringify(exposure.localIP)}
|
||||
localPort = ${exposure.localPort}
|
||||
remotePort = ${exposure.remotePort}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ${exposure.name}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: agentrun
|
||||
app.kubernetes.io/name: ${exposure.name}
|
||||
app.kubernetes.io/part-of: agentrun
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: ${exposure.name}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: agentrun
|
||||
app.kubernetes.io/name: ${exposure.name}
|
||||
app.kubernetes.io/part-of: agentrun
|
||||
spec:
|
||||
containers:
|
||||
- name: frpc
|
||||
image: fatedier/frpc:v0.68.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- -c
|
||||
- /etc/frp/frpc.toml
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/frp
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: ${configMapName}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -477,3 +567,69 @@ function optionalUnideskSshEndpointEnv(deploy: JsonRecord): EnvVarValue | null {
|
||||
if (typeof envValue !== "string" || envValue.length === 0) throw new AgentRunError("schema-invalid", "unideskSshEndpointEnv.value must be a non-empty string", { httpStatus: 2 });
|
||||
return { name, value: envValue };
|
||||
}
|
||||
|
||||
function managerApiKeySecretEnv(deploy: JsonRecord): SecretEnvVarValue {
|
||||
const value = deploy.managerApiKeyEnv;
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new AgentRunError("schema-invalid", "managerApiKeyEnv must be an object", { httpStatus: 2 });
|
||||
const record = value as JsonRecord;
|
||||
const name = record.name;
|
||||
const secretRef = record.secretRef;
|
||||
if (name !== "AGENTRUN_API_KEY") throw new AgentRunError("schema-invalid", "managerApiKeyEnv.name must be AGENTRUN_API_KEY", { httpStatus: 2 });
|
||||
if (typeof secretRef !== "object" || secretRef === null || Array.isArray(secretRef)) throw new AgentRunError("schema-invalid", "managerApiKeyEnv.secretRef must be an object", { httpStatus: 2 });
|
||||
const ref = secretRef as JsonRecord;
|
||||
const secretName = ref.name;
|
||||
const key = ref.key;
|
||||
if (typeof secretName !== "string" || !/^agentrun-v01-[a-z0-9-]+$/u.test(secretName)) throw new AgentRunError("schema-invalid", "managerApiKeyEnv.secretRef.name must be an agentrun-v01 Secret name", { httpStatus: 2 });
|
||||
if (typeof key !== "string" || key !== "HWLAB_API_KEY") throw new AgentRunError("schema-invalid", "managerApiKeyEnv.secretRef.key must be HWLAB_API_KEY", { httpStatus: 2 });
|
||||
return { name, secretRef: { name: secretName, key } };
|
||||
}
|
||||
|
||||
function optionalFrpTcpExposure(deploy: JsonRecord): FrpTcpExposure | null {
|
||||
const value = deploy.managerPublicExposure;
|
||||
if (value === undefined) return null;
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new AgentRunError("schema-invalid", "managerPublicExposure must be an object", { httpStatus: 2 });
|
||||
const record = value as JsonRecord;
|
||||
if (record.enabled !== true) return null;
|
||||
if (record.kind !== "frp-tcp") throw new AgentRunError("schema-invalid", "managerPublicExposure.kind must be frp-tcp", { httpStatus: 2 });
|
||||
const exposure: FrpTcpExposure = {
|
||||
enabled: true,
|
||||
kind: "frp-tcp",
|
||||
name: requiredDnsLabel(record, "name"),
|
||||
serverAddr: requiredNonEmptyString(record, "serverAddr"),
|
||||
serverPort: requiredPort(record, "serverPort"),
|
||||
localIP: requiredNonEmptyString(record, "localIP"),
|
||||
localPort: requiredPort(record, "localPort"),
|
||||
remotePort: requiredPort(record, "remotePort"),
|
||||
publicBaseUrl: requiredHttpUrl(record, "publicBaseUrl"),
|
||||
masterBaseUrl: requiredHttpUrl(record, "masterBaseUrl"),
|
||||
};
|
||||
if (!exposure.localIP.endsWith(".svc.cluster.local")) throw new AgentRunError("schema-invalid", "managerPublicExposure.localIP must be a cluster service DNS name", { httpStatus: 2 });
|
||||
if (!exposure.publicBaseUrl.startsWith("https://")) throw new AgentRunError("schema-invalid", "managerPublicExposure.publicBaseUrl must use https://", { httpStatus: 2 });
|
||||
if (!exposure.masterBaseUrl.startsWith("http://127.0.0.1:")) throw new AgentRunError("schema-invalid", "managerPublicExposure.masterBaseUrl must point to the master-local FRP port", { httpStatus: 2 });
|
||||
return exposure;
|
||||
}
|
||||
|
||||
function requiredDnsLabel(record: JsonRecord, key: string): string {
|
||||
const value = requiredNonEmptyString(record, key);
|
||||
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/u.test(value)) throw new AgentRunError("schema-invalid", `managerPublicExposure.${key} must be a DNS label`, { httpStatus: 2 });
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredNonEmptyString(record: JsonRecord, key: string): string {
|
||||
const value = record[key];
|
||||
if (typeof value !== "string" || value.trim().length === 0) throw new AgentRunError("schema-invalid", `managerPublicExposure.${key} must be a non-empty string`, { httpStatus: 2 });
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function requiredPort(record: JsonRecord, key: string): number {
|
||||
const value = record[key];
|
||||
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 65535) throw new AgentRunError("schema-invalid", `managerPublicExposure.${key} must be a TCP port`, { httpStatus: 2 });
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredHttpUrl(record: JsonRecord, key: string): string {
|
||||
const value = requiredNonEmptyString(record, key);
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") throw new AgentRunError("schema-invalid", `managerPublicExposure.${key} must be http(s)`, { httpStatus: 2 });
|
||||
return value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user