feat: 接入 ApiState 自动交付

This commit is contained in:
Codex
2026-07-16 09:56:05 +02:00
parent c84501fb16
commit 8edeefa4f4
9 changed files with 115 additions and 38 deletions
+6 -6
View File
@@ -313,23 +313,23 @@ sourceAuthority:
- key: sub2rank-nc01
targetId: NC01
upstream:
repository: pikasTech/sub2rank
cloneUrl: https://github.com/pikasTech/sub2rank.git
repository: pikasTech/apistate
cloneUrl: https://github.com/pikasTech/apistate.git
branch: master
gitea:
owner: mirrors
name: pikasTech-sub2rank
name: pikasTech-apistate
mirrorMode: controlled-push
publicRead: true
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-apistate.git
gitops:
branch: master
flushDisposition: not-a-gitops-branch
snapshot:
naming: application-source-snapshot
prefix: refs/unidesk/snapshots/gitea-actions/sub2rank-master-nc01
prefix: refs/unidesk/snapshots/gitea-actions/apistate-master-nc01
legacyGitMirror:
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-apistate.git
configRef: config/platform-infra/sub2rank.yaml#application.remote
disposition: replaced-by-gitea
- key: hwlab-jd01-v03
+5 -5
View File
@@ -176,18 +176,18 @@ repositories:
name: sub2rank-nc01
namespace: devops-infra
providerType: gitea
url: https://gitea.pikapython.com/mirrors/pikasTech-sub2rank
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
url: https://gitea.pikapython.com/mirrors/pikasTech-apistate
cloneUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-apistate.git
owner: mirrors
repo: pikasTech-sub2rank
repo: pikasTech-apistate
secretName: pac-gitea-sub2rank-nc01
tokenKey: token
webhookSecretKey: webhook.secret
concurrencyLimit: 1
params:
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-sub2rank.git
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-apistate.git
source_branch: master
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/sub2rank-master-nc01
source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/apistate-master-nc01
node: NC01
image_repository: 127.0.0.1:5000/sub2rank/sub2rank
registry_probe_base: http://127.0.0.1:5000
+3
View File
@@ -57,6 +57,9 @@ targets:
- id: webterm
configRef: config/platform-infra/webterm.yaml
path: targets[id=local-7683].publicExposure
- id: apistate
configRef: config/platform-infra/sub2rank.yaml
path: targets[id=NC01].publicEdge
- id: selfmedia-development
configRef: config/selfmedia.yaml
path: delivery.targets.NC01.deployment.publicExposure.edge
+26 -12
View File
@@ -2,7 +2,7 @@ version: 1
kind: platform-infra-sub2rank
metadata:
id: sub2rank-public
id: apistate-public
owner: unidesk
relatedIssues: []
@@ -10,11 +10,11 @@ defaults:
targetId: NC01
application:
repository: pikasTech/sub2rank
remote: https://github.com/pikasTech/sub2rank.git
repository: pikasTech/apistate
remote: https://github.com/pikasTech/apistate.git
branch: master
sourceRoot: /root/sub2rank
configRef: /root/sub2rank/config/sub2rank.yaml
sourceRoot: /root/apistate
configRef: /root/apistate/config/sub2rank.yaml
sourceConfigPath: config/sub2rank.yaml
dockerfile: Dockerfile
runtimeTarget: k8s
@@ -34,7 +34,7 @@ delivery:
workspaceSize: 4Gi
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
buildkitImage: 127.0.0.1:5000/hwlab/buildkit:rootless
sourceSnapshotPrefix: refs/unidesk/snapshots/gitea-actions/sub2rank-master-nc01
sourceSnapshotPrefix: refs/unidesk/snapshots/gitea-actions/apistate-master-nc01
build:
networkMode: host
proxy:
@@ -77,11 +77,11 @@ targets:
enabled: true
replicas: 1
publicExposure:
enabled: true
publicBaseUrl: https://rank.pikapython.com
enabled: false
publicBaseUrl: https://apistate.hwpod.com
dns:
hostname: rank.pikapython.com
expectedA: 82.156.23.220
hostname: apistate.hwpod.com
expectedA: 152.53.229.148
resolvers:
- 1.1.1.1
- 8.8.8.8
@@ -105,6 +105,12 @@ targets:
caddyConfigPath: /etc/caddy/Caddyfile
caddyServiceName: caddy
responseHeaderTimeoutSeconds: 60
publicEdge:
enabled: true
hostname: apistate.hwpod.com
upstream: sub2rank.platform-infra.svc.cluster.local:8080
healthPath: /health
expectedA: 152.53.229.148
runtime:
sharedNetworkPolicyRef:
@@ -128,16 +134,24 @@ runtime:
claimName: sub2rank-data
size: 1Gi
mountPath: /var/lib/sub2rank
hostMounts:
- source: /root/unidesk
target: /opt/unidesk
readOnly: true
secret:
configRef: config/secrets-distribution.yaml
declarationId: sub2rank-runtime
requiredTargetKeys:
- SUB2RANK_ADMIN_TOKEN
- APISTATE_WEB_PASSWORD
- APISTATE_API_KEY
- APISTATE_SESSION_SECRET
- SUB2API_ADMIN_EMAIL
- SUB2API_ADMIN_PASSWORD
- FRP_TOKEN
appEnvTargetKeys:
- SUB2RANK_ADMIN_TOKEN
- APISTATE_WEB_PASSWORD
- APISTATE_API_KEY
- APISTATE_SESSION_SECRET
- SUB2API_ADMIN_EMAIL
- SUB2API_ADMIN_PASSWORD
probes:
+21 -7
View File
@@ -82,16 +82,24 @@ sources:
- FRP_TOKEN
createIfMissing:
enabled: false
- sourceRef: platform-infra/sub2rank.env
- sourceRef: platform-infra/apistate.env
type: env
requiredKeys:
- SUB2RANK_ADMIN_TOKEN
- APISTATE_WEB_PASSWORD
- APISTATE_API_KEY
- APISTATE_SESSION_SECRET
createIfMissing:
enabled: true
randomBase64Url:
SUB2RANK_ADMIN_TOKEN:
APISTATE_WEB_PASSWORD:
bytes: 32
prefix: srk_
prefix: asp_
APISTATE_API_KEY:
bytes: 32
prefix: ask_
APISTATE_SESSION_SECRET:
bytes: 32
prefix: ass_
- sourceRef: platform-infra/pikaoa.env
type: env
requiredKeys:
@@ -399,9 +407,15 @@ kubernetesSecrets:
secretName: sub2rank-secrets
type: Opaque
data:
- sourceRef: platform-infra/sub2rank.env
sourceKey: SUB2RANK_ADMIN_TOKEN
targetKey: SUB2RANK_ADMIN_TOKEN
- sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_WEB_PASSWORD
targetKey: APISTATE_WEB_PASSWORD
- sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_API_KEY
targetKey: APISTATE_API_KEY
- sourceRef: platform-infra/apistate.env
sourceKey: APISTATE_SESSION_SECRET
targetKey: APISTATE_SESSION_SECRET
- sourceRef: platform-infra/sub2api.env
sourceKey: ADMIN_EMAIL
targetKey: SUB2API_ADMIN_EMAIL
@@ -279,3 +279,7 @@
## R4 [completed]
关联 [UniDesk #2281](https://github.com/pikasTech/unidesk/issues/2281),增强 Sub2API runtime 模板批量 CRUD:支持通过显式参数按原生 runtime 已配置同类策略和账号类型自动选集,并支持精确排除 selector;dry-run 直接输出最终账号名称、ID、选中来源与排除摘要,confirm 复用既有写后自动回读对账,避免先 list、人工抄账号 ID、再 apply 的重复调用;显式 JSON 使用紧凑 mutation 投影保留 provenance 且不触发输出保护,不修改 Sub2API 源码、版本、runtime、YAML 或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R4_Task_Report.md)。
## R5 [in_progress]
将现有 Sub2Rank 迁移为 ApiState 多页面运维站:源码 authority 切换到 pikasTech/apistate,本地根目录迁到 /root/apistate,沿用 NC01 PaC/Tekton、GitOps/Argo 自动链;默认每 10 分钟通过既有 UniDesk Sub2API CLI 刷新所有分组账号评分,提供账号评分表、用户排名与抽奖多页面、用户名密码登录和 API key 全功能调用;Secret 仅由 YAML sourceRef/targetKey 分发,公网发布到 apistate.hwpod.com,并完成 API 与 web-probe 原入口验收,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R5_Task_Report.md)。
+5 -3
View File
@@ -237,7 +237,7 @@ export function publicServicePolicyChecks(
options: { requireAllowAllManifest?: boolean } = {},
): Array<Record<string, unknown>> {
const checks: Array<Record<string, unknown>> = [
{ name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: `${serviceName} public exposure must use PK01 Caddy+FRP, not Kubernetes Ingress.` },
{ name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: `${serviceName} does not own Kubernetes Ingress; public exposure follows its owning YAML.` },
{ name: "no-nodeport-or-loadbalancer", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Services must stay ClusterIP." },
{ name: "no-host-network", ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml), detail: "Pods must not use host network." },
{ name: "no-host-port", ok: !/^\s*hostPort:\s*[0-9]+\s*$/mu.test(yaml), detail: "Pods must not expose host ports." },
@@ -435,8 +435,10 @@ export function publicServiceStatusScript(params: {
resources: PublicServiceRuntimeResources;
healthPath: string;
servicePort: number;
includeFrpc?: boolean;
}): string {
const requiredSecretKeys = JSON.stringify(params.resources.requiredSecretKeys);
const includeFrpc = params.includeFrpc !== false;
return `
set -u
tmp="$(mktemp -d)"
@@ -448,7 +450,7 @@ capture_json() {
printf '%s' "$?" >"$tmp/$name.rc"
}
capture_json app kubectl -n ${params.target.namespace} get deployment ${params.resources.appDeploymentName}
capture_json frpc kubectl -n ${params.target.namespace} get deployment ${params.target.publicExposure.frpc.deploymentName}
${includeFrpc ? `capture_json frpc kubectl -n ${params.target.namespace} get deployment ${params.target.publicExposure.frpc.deploymentName}` : ""}
capture_json service kubectl -n ${params.target.namespace} get service ${params.resources.serviceName}
capture_json pvc kubectl -n ${params.target.namespace} get pvc ${params.resources.persistentVolumeClaimName}
capture_json configmap kubectl -n ${params.target.namespace} get configmap ${params.resources.configMapName}
@@ -483,7 +485,7 @@ pvc = load("pvc") or {}
endpoints = load("endpoints") or {}
addresses = sum(len(subset.get("addresses", []) or []) for subset in endpoints.get("subsets", []) or [])
app = deployment("app")
frpc = deployment("frpc")
frpc = deployment("frpc") if ${includeFrpc ? "True" : "False"} else {"present": False, "desired": 0, "ready": 0, "available": 0, "ok": True, "skipped": True}
payload = {
"ok": app["ok"] and frpc["ok"] and rc("service") == 0 and rc("pvc") == 0 and pvc.get("status", {}).get("phase") == "Bound" and rc("configmap") == 0 and rc("secret") == 0 and not missing_keys and addresses > 0${params.resources.sharedNetworkPolicyName === undefined ? "" : ' and rc("networkpolicy") == 0'},
"target": "${params.target.id}",
+21 -1
View File
@@ -80,6 +80,7 @@ export interface Sub2RankConfig {
configMap: { name: string; key: string; mountPath: string };
command: string[];
storage: { claimName: string; size: string; mountPath: string };
hostMounts: Array<{ source: string; target: string; readOnly: true }>;
secret: ResolvedSecretDeclaration & { configRef: string; declarationId: string; requiredTargetKeys: string[]; appEnvTargetKeys: string[] };
probes: { healthPath: string; initialDelaySeconds: number; periodSeconds: number; timeoutSeconds: number; failureThreshold: number };
rollout: { fieldManager: string; waitTimeoutSeconds: number };
@@ -218,7 +219,14 @@ function parseApplication(record: Record<string, unknown>): Sub2RankConfig["appl
server: {
listenPort: integerValue(server.listenPort, `${configRef}.runtime.serverTargets.${runtimeTarget}.listenPort`),
databasePath: absolutePathValue(server.databasePath, `${configRef}.runtime.serverTargets.${runtimeTarget}.databasePath`),
envKeys: ["adminTokenEnv", "sub2apiAdminEmailEnv", "sub2apiAdminPasswordEnv"].map((key) => envKeyValue(server[key], `${configRef}.runtime.serverTargets.${runtimeTarget}.${key}`)),
envKeys: [...new Set([
"adminTokenEnv",
"sub2apiAdminEmailEnv",
"sub2apiAdminPasswordEnv",
"webPasswordEnv",
"apiKeyEnv",
"sessionSecretEnv",
].map((key) => envKeyValue(server[key], `${configRef}.runtime.serverTargets.${runtimeTarget}.${key}`)))],
},
};
}
@@ -293,6 +301,11 @@ function parseRuntime(record: Record<string, unknown>): Omit<Sub2RankConfig["run
const sharedNetworkPolicyRef = y.objectField(record, "sharedNetworkPolicyRef", "runtime");
const configMap = y.objectField(record, "configMap", "runtime");
const storage = y.objectField(record, "storage", "runtime");
const hostMounts = y.arrayOfRecords(record.hostMounts, "runtime.hostMounts").map((mount, index) => ({
source: y.absolutePathField(mount, "source", `runtime.hostMounts[${index}]`),
target: y.absolutePathField(mount, "target", `runtime.hostMounts[${index}]`),
readOnly: requiredTrueField(mount, "readOnly", `runtime.hostMounts[${index}]`),
}));
const secret = y.objectField(record, "secret", "runtime");
const probes = y.objectField(record, "probes", "runtime");
const rollout = y.objectField(record, "rollout", "runtime");
@@ -314,6 +327,7 @@ function parseRuntime(record: Record<string, unknown>): Omit<Sub2RankConfig["run
size: storageSize(y.stringField(storage, "size", "runtime.storage")),
mountPath: y.absolutePathField(storage, "mountPath", "runtime.storage"),
},
hostMounts,
secret: {
configRef: configRefValue,
declarationId: simpleId(y.stringField(secret, "declarationId", "runtime.secret"), "runtime.secret.declarationId"),
@@ -486,6 +500,12 @@ function disabledField(record: Record<string, unknown>, key: string, path: strin
return false;
}
function requiredTrueField(record: Record<string, unknown>, key: string, path: string): true {
const value = y.booleanField(record, key, path);
if (!value) throw new Error(`${sub2RankConfigLabel}.${path}.${key} must be true`);
return true;
}
function storageSize(value: string): string {
if (!/^[1-9][0-9]*(?:Mi|Gi|Ti)$/u.test(value)) throw new Error(`${sub2RankConfigLabel}.runtime.storage.size must use Mi, Gi, or Ti`);
return value;
+24 -4
View File
@@ -34,7 +34,7 @@ export function sub2RankHelp(): Record<string, unknown> {
"bun scripts/cli.ts platform-infra sub2rank validate [--target NC01] [--full|--raw]",
],
configTruth: sub2RankConfigLabel,
applicationConfigRef: "/root/sub2rank/config/sub2rank.yaml",
applicationConfigRef: "application.configRef in config/platform-infra/sub2rank.yaml",
artifactPolicy: "Application PR merge is the sole delivery trigger; PaC/Tekton builds the image and publishes a digest-pinned GitOps manifest.",
secretPolicy: "Resolves the existing Secret declaration from config/secrets-distribution.yaml and never reads or prints runtime Secret values.",
};
@@ -76,11 +76,14 @@ function plan(options: OpsCommonOptions): Record<string, unknown> {
pipeline: sub2rank.delivery.pipeline.name,
},
},
topology: `client -> PK01 Caddy -> PK01 frps:${target.publicExposure.frpc.remotePort} -> ${target.id} frpc -> ${sub2rank.runtime.service.name}.${target.namespace}.svc.cluster.local:${sub2rank.runtime.service.port}`,
topology: target.publicExposure.enabled
? `client -> PK01 Caddy -> PK01 frps:${target.publicExposure.frpc.remotePort} -> ${target.id} frpc -> ${sub2rank.runtime.service.name}.${target.namespace}.svc.cluster.local:${sub2rank.runtime.service.port}`
: `client -> ${target.id} shared public-edge -> ${sub2rank.runtime.service.name}.${target.namespace}.svc.cluster.local:${sub2rank.runtime.service.port}`,
next: {
secrets: "bun scripts/cli.ts secrets sync --config config/secrets-distribution.yaml --scope sub2rank --confirm",
publicExposureDryRun: `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --dry-run`,
publicExposureApply: `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --confirm`,
publicExposureDryRun: target.publicExposure.enabled ? `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --dry-run` : null,
publicExposureApply: target.publicExposure.enabled ? `bun scripts/cli.ts platform-infra sub2rank public-exposure --target ${target.id} --confirm` : null,
sharedPublicEdge: target.publicExposure.enabled ? null : `bun scripts/cli.ts platform-infra public-edge plan --target ${target.id}`,
status: `bun scripts/cli.ts platform-infra sub2rank status --target ${target.id}`,
validate: `bun scripts/cli.ts platform-infra sub2rank validate --target ${target.id}`,
},
@@ -90,6 +93,17 @@ function plan(options: OpsCommonOptions): Record<string, unknown> {
async function publicExposure(config: UniDeskConfig, options: OpsApplyOptions): Promise<Record<string, unknown>> {
const sub2rank = readSub2RankConfig();
const target = resolveSub2RankTarget(sub2rank, options.targetId);
if (!target.publicExposure.enabled) {
return {
ok: true,
action: "platform-infra-sub2rank-public-exposure",
mode: "disabled",
mutation: false,
target: targetSummary(target),
reason: "legacy-public-exposure-disabled",
next: `bun scripts/cli.ts platform-infra public-edge plan --target ${target.id}`,
};
}
if (options.dryRun) {
return {
ok: true,
@@ -126,6 +140,7 @@ async function status(config: UniDeskConfig, options: OpsCommonOptions): Promise
resources,
healthPath: sub2rank.runtime.probes.healthPath,
servicePort: sub2rank.runtime.service.port,
includeFrpc: target.publicExposure.enabled,
}));
const parsed = parseJsonOutput(remote.stdout);
return {
@@ -173,6 +188,8 @@ export function renderSub2RankManifest(config: Sub2RankConfig, target: Sub2RankT
const sourceCommit = options.sourceCommit ?? config.application.sourceCommit;
const configHash = createHash("sha256").update(JSON.stringify({ deployment: deploymentHashInput(config), target })).digest("hex").slice(0, 16);
const appEnv = runtime.secret.appEnvTargetKeys.map((key) => ` - name: ${key}\n valueFrom:\n secretKeyRef:\n name: ${runtime.secret.secretName}\n key: ${key}`).join("\n");
const hostVolumeMounts = runtime.hostMounts.map((mount, index) => ` - name: host-source-${index}\n mountPath: ${mount.target}\n readOnly: true`).join("\n");
const hostVolumes = runtime.hostMounts.map((mount, index) => ` - name: host-source-${index}\n hostPath:\n path: ${mount.source}\n type: Directory`).join("\n");
const command = runtime.command.map((value) => ` - ${yamlQuoted(value)}`).join("\n");
return `apiVersion: v1
kind: ConfigMap
@@ -283,6 +300,7 @@ ${appEnv}
readOnly: true
- name: data
mountPath: ${runtime.storage.mountPath}
${hostVolumeMounts.length > 0 ? hostVolumeMounts : ""}
volumes:
- name: app-config
configMap:
@@ -290,6 +308,7 @@ ${appEnv}
- name: data
persistentVolumeClaim:
claimName: ${runtime.storage.claimName}
${hostVolumes.length > 0 ? hostVolumes : ""}
${renderEnvSecretFrpcManifest(target, {
configMapName: target.publicExposure.frpc.configMapName,
configKey: target.publicExposure.frpc.configKey,
@@ -332,6 +351,7 @@ function configSummary(config: Sub2RankConfig, target: Sub2RankTarget): Record<s
secret: secretSummary(config),
},
publicExposure: {
enabled: target.publicExposure.enabled,
publicBaseUrl: target.publicExposure.publicBaseUrl,
expectedA: target.publicExposure.dns.expectedA,
remotePort: target.publicExposure.frpc.remotePort,