diff --git a/config/platform-infra/gitea.yaml b/config/platform-infra/gitea.yaml index e1be1292..7c2465ba 100644 --- a/config/platform-infra/gitea.yaml +++ b/config/platform-infra/gitea.yaml @@ -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 diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index 32d53457..c602806a 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -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 diff --git a/config/platform-infra/public-edge.yaml b/config/platform-infra/public-edge.yaml index 66d43f17..fa82f1d6 100644 --- a/config/platform-infra/public-edge.yaml +++ b/config/platform-infra/public-edge.yaml @@ -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 diff --git a/config/platform-infra/sub2rank.yaml b/config/platform-infra/sub2rank.yaml index 2c6b02e4..3dd8195e 100644 --- a/config/platform-infra/sub2rank.yaml +++ b/config/platform-infra/sub2rank.yaml @@ -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: diff --git a/config/secrets-distribution.yaml b/config/secrets-distribution.yaml index 839a0700..1534838f 100644 --- a/config/secrets-distribution.yaml +++ b/config/secrets-distribution.yaml @@ -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 diff --git a/docs/MDTODO/sub2api-upstream-reliability.md b/docs/MDTODO/sub2api-upstream-reliability.md index 172c4dcd..3a4a520f 100644 --- a/docs/MDTODO/sub2api-upstream-reliability.md +++ b/docs/MDTODO/sub2api-upstream-reliability.md @@ -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)。 diff --git a/scripts/src/platform-infra-public-service.ts b/scripts/src/platform-infra-public-service.ts index 3137f1e0..16c53390 100644 --- a/scripts/src/platform-infra-public-service.ts +++ b/scripts/src/platform-infra-public-service.ts @@ -237,7 +237,7 @@ export function publicServicePolicyChecks( options: { requireAllowAllManifest?: boolean } = {}, ): Array> { const checks: Array> = [ - { 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}", diff --git a/scripts/src/platform-infra-sub2rank-config.ts b/scripts/src/platform-infra-sub2rank-config.ts index 2897fc29..54b26207 100644 --- a/scripts/src/platform-infra-sub2rank-config.ts +++ b/scripts/src/platform-infra-sub2rank-config.ts @@ -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): 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): Omit ({ + 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): Omit, key: string, path: strin return false; } +function requiredTrueField(record: Record, 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; diff --git a/scripts/src/platform-infra-sub2rank.ts b/scripts/src/platform-infra-sub2rank.ts index a94a8196..ea88e31d 100644 --- a/scripts/src/platform-infra-sub2rank.ts +++ b/scripts/src/platform-infra-sub2rank.ts @@ -34,7 +34,7 @@ export function sub2RankHelp(): Record { "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 { 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 { async function publicExposure(config: UniDeskConfig, options: OpsApplyOptions): Promise> { 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