diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml index 6a2bb37f..2af8108a 100644 --- a/deploy/deploy.yaml +++ b/deploy/deploy.yaml @@ -79,9 +79,15 @@ profiles: enabled: false namespace: hwlab-prod notes: Reserved placeholder only. PROD deployment and acceptance are explicitly out of scope. +nodes: + G14: + gitopsRoot: deploy/gitops/g14 + sourceRepo: git@github.com:pikasTech/HWLAB.git + publicHost: 74.48.78.17 lanes: v02: name: v0.2 + node: G14 sourceBranch: v0.2 gitopsBranch: v0.2-gitops namespace: hwlab-v02 @@ -92,7 +98,7 @@ lanes: legacyFrontend: http://74.48.78.17:19666 legacyApi: http://74.48.78.17:19667 artifactCatalog: deploy/artifact-catalog.v02.json - runtimePath: deploy/gitops/g14/runtime-v02 + runtimePath: runtime-v02 imageTagMode: full envReuseServices: - hwlab-cloud-api @@ -232,6 +238,7 @@ lanes: HWLAB_CODE_AGENT_AGENTRUN_REPO_URL: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git v03: name: v0.3 + node: G14 sourceBranch: v0.3 gitopsBranch: v0.3-gitops namespace: hwlab-v03 @@ -242,7 +249,7 @@ lanes: legacyFrontend: http://74.48.78.17:19766 legacyApi: http://74.48.78.17:19767 artifactCatalog: deploy/artifact-catalog.v03.json - runtimePath: deploy/gitops/g14/runtime-v03 + runtimePath: runtime-v03 imageTagMode: full envReuseServices: - hwlab-cloud-api diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index a1a3e94e..27349a86 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -15,7 +15,7 @@ import { runtimeLaneMirrorSpecs, runtimeLaneConfig, versionNameForRuntimeLane -} from "./src/g14-gitops-lane.ts"; +} from "./src/runtime-lane.ts"; import { readStructuredFile } from "./src/structured-config.mjs"; const execFileAsync = promisify(execFile); @@ -136,7 +136,7 @@ const forbiddenCiFragments = [ ]; const scanTargets = [ "scripts/g14-gitops-render.mjs", - "scripts/src/g14-gitops-lane.ts", + "scripts/src/runtime-lane.ts", "scripts/artifact-publish.mjs", "scripts/g14-artifact-publish.mjs", "scripts/src/g14-ci-plan-lib.mjs", @@ -203,6 +203,7 @@ function proxyEnv() { function parseArgs(argv) { const args = { lane: process.env.HWLAB_GITOPS_LANE || "g14", + nodeId: process.env.HWLAB_NODE_ID || "G14", outDir: defaultOutDir, catalogPath: defaultCatalogPath, imageTagMode: process.env.HWLAB_ARTIFACT_IMAGE_TAG_MODE || "short", @@ -225,6 +226,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--lane") args.lane = readOption(argv, ++index, arg); + else if (arg === "--node") args.nodeId = readOption(argv, ++index, arg); else if (arg === "--out") args.outDir = readOption(argv, ++index, arg); else if (arg === "--catalog-path") args.catalogPath = readOption(argv, ++index, arg); else if (arg === "--image-tag-mode") args.imageTagMode = readOption(argv, ++index, arg); @@ -285,6 +287,7 @@ function usage() { "", "options:", " --lane LANE g14 or runtime lane such as v02/v03/v04; default: g14", + ` --node NODE deployment node id from deploy.nodes; default: ${process.env.HWLAB_NODE_ID || "G14"}`, ` --out DIR default: ${defaultOutDir}`, ` --catalog-path PATH default: ${defaultCatalogPath}`, " --image-tag-mode MODE short or full; v02 uses full source commit IDs", @@ -735,6 +738,18 @@ function gitopsTargetForProfile(profile) { return isRuntimeLane(profile) ? profile : "g14"; } +function gitopsRootForArgs(args) { + return String(args.outDir || defaultOutDir).replace(/\/+$/u, ""); +} + +function gitopsPathForProfile(args, profile) { + return `${gitopsRootForArgs(args)}/${runtimePathForProfile(profile)}`; +} + +function gitopsPathFor(args, relativePath) { + return `${gitopsRootForArgs(args)}/${String(relativePath).replace(/^\/+/, "")}`; +} + function profileEndpoint(args, profile) { if (isRuntimeLane(profile)) return { runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint }; return profile === "prod" @@ -757,7 +772,7 @@ function laneNames(args) { serviceAccount: `${namespace}-tekton-runner`, pipelineRunPrefix: `${namespace}-ci-poll-`, runtimeNamespace: namespace, - runtimePath: `deploy/gitops/g14/runtime-${args.lane}`, + runtimePath: gitopsPathForProfile(args, args.lane), argoApplications: [`argocd/hwlab-g14-${args.lane}`] }; } @@ -769,7 +784,7 @@ function laneNames(args) { serviceAccount: "hwlab-tekton-runner", pipelineRunPrefix: "hwlab-g14-ci-poll-", runtimeNamespace: "hwlab-dev", - runtimePath: "deploy/gitops/g14/runtime-dev", + runtimePath: gitopsPathForProfile(args, "dev"), argoApplications: ["argocd/hwlab-g14-dev", "argocd/hwlab-g14-prod"] }; } @@ -3071,7 +3086,7 @@ function imagePublishTaskSet(args = { lane: "g14" }, deploy = null) { function tektonPipeline(args = { lane: "g14" }, deploy = null) { const settings = ciLaneSettings(args); - const runtimePath = `deploy/gitops/g14/${runtimePathForProfile(settings.profile)}`; + const runtimePath = gitopsPathForProfile(args, settings.profile); return { apiVersion: "tekton.dev/v1", kind: "Pipeline", @@ -3196,7 +3211,7 @@ function tektonPipeline(args = { lane: "g14" }, deploy = null) { function tektonPipelineRunTemplate({ source, args }) { const settings = ciLaneSettings(args); - const runtimePath = `deploy/gitops/g14/${runtimePathForProfile(settings.profile)}`; + const runtimePath = gitopsPathForProfile(args, settings.profile); return { apiVersion: "tekton.dev/v1", kind: "PipelineRun", @@ -3293,7 +3308,7 @@ function tektonPollerCronJob(args, deploy) { { name: "LANE", value: settings.lane }, { name: "CATALOG_PATH", value: args.catalogPath || settings.catalogPath }, { name: "IMAGE_TAG_MODE", value: args.imageTagMode || settings.imageTagMode }, - { name: "RUNTIME_PATH", value: `deploy/gitops/g14/${runtimePathForProfile(settings.profile)}` }, + { name: "RUNTIME_PATH", value: gitopsPathForProfile(args, settings.profile) }, { name: "GIT_URL", value: args.sourceRepo }, { name: "GIT_READ_URL", value: args.gitReadUrl }, { name: "SOURCE_BRANCH", value: args.sourceBranch }, @@ -3365,7 +3380,7 @@ function tektonControlPlaneReconcilerCronJob(args) { { name: "CATALOG_PATH", value: args.catalogPath || settings.catalogPath }, { name: "IMAGE_TAG_MODE", value: args.imageTagMode || settings.imageTagMode }, { name: "TEKTON_DIR", value: settings.tektonDir }, - { name: "ARGO_FILES", value: settings.lane === "v02" ? "deploy/gitops/g14/argocd/project.yaml,deploy/gitops/g14/argocd/application-v02.yaml" : "" }, + { name: "ARGO_FILES", value: settings.lane === "v02" ? `${gitopsPathFor(args, "argocd/project.yaml")},${gitopsPathFor(args, "argocd/application-v02.yaml")}` : "" }, { name: "FIELD_MANAGER", value: settings.controlPlaneReconcilerName }, { name: "RECONCILE_EVENT", value: `${settings.gitopsTarget}-control-plane-reconciled` }, { name: "GIT_URL", value: args.sourceRepo }, @@ -3434,8 +3449,8 @@ function uniqueStrings(values) { return Array.from(new Set(values.filter((value) => typeof value === "string" && value.length > 0))); } -function devopsInfraGitMirrorManifest(_args, deploy) { - const mirrorSpecs = runtimeLaneMirrorSpecs(deploy); +function devopsInfraGitMirrorManifest(args, deploy) { + const mirrorSpecs = runtimeLaneMirrorSpecs(deploy, { defaultNodeId: args.nodeId, defaultGitopsRoot: defaultOutDir }); assert.ok(mirrorSpecs.length > 0, "runtime lane git mirror requires at least one deploy.lanes entry"); const sourceBranches = uniqueStrings(mirrorSpecs.map((spec) => spec.sourceBranch)); const gitopsBranches = uniqueStrings(mirrorSpecs.map((spec) => spec.gitopsBranch)); @@ -4146,7 +4161,7 @@ function argoApplication(args, profile = "dev") { }, spec: { project, - source: { repoURL, targetRevision: args.gitopsBranch, path: `deploy/gitops/g14/${runtimePath}` }, + source: { repoURL, targetRevision: args.gitopsBranch, path: gitopsPathForProfile(args, profile) }, destination: { server: "https://kubernetes.default.svc", namespace }, syncPolicy: { automated: { prune: isRuntimeLane(profile), selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] } } @@ -4789,6 +4804,7 @@ function v02OpenFgaManifest({ profile = "v02", source }) { { name: "OPENFGA_GRPC_ADDR", value: "0.0.0.0:8081" } ]; const templateLabels = { ...labels, ...selector }; + const runtimeAnnotations = { ...annotations, "argocd.argoproj.io/sync-wave": "2" }; return { apiVersion: "v1", kind: "List", @@ -4802,7 +4818,8 @@ function v02OpenFgaManifest({ profile = "v02", source }) { labels, annotations: { ...annotations, - "argocd.argoproj.io/hook": "PreSync", + "argocd.argoproj.io/hook": "Sync", + "argocd.argoproj.io/sync-wave": "1", "argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded" } }, @@ -4820,7 +4837,7 @@ function v02OpenFgaManifest({ profile = "v02", source }) { { apiVersion: "apps/v1", kind: "Deployment", - metadata: { name, namespace, labels, annotations }, + metadata: { name, namespace, labels, annotations: runtimeAnnotations }, spec: { replicas: 1, selector: { matchLabels: selector }, @@ -4845,7 +4862,7 @@ function v02OpenFgaManifest({ profile = "v02", source }) { { apiVersion: "v1", kind: "Service", - metadata: { name, namespace, labels, annotations }, + metadata: { name, namespace, labels, annotations: runtimeAnnotations }, spec: { type: "ClusterIP", selector, ports: [{ name: "http", port: 8080, targetPort: "http" }, { name: "grpc", port: 8081, targetPort: "grpc" }] } } ] @@ -4915,6 +4932,7 @@ async function plannedFiles(args) { defaultBranch, defaultGitopsBranch, defaultCatalogPath, + defaultGitopsRoot: defaultOutDir, defaultRuntimeEndpoint, defaultWebEndpoint, defaultV02RuntimeEndpoint, @@ -4956,7 +4974,7 @@ async function plannedFiles(args) { gitReadUrl: args.gitReadUrl, gitWriteUrl: args.gitWriteUrl, registryPrefix: args.registryPrefix, - runtimePaths: profiles.map((profile) => `deploy/gitops/g14/${runtimePathForProfile(profile)}`), + runtimePaths: profiles.map((profile) => gitopsPathForProfile(args, profile)), publicEndpoints, frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile)}/${isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc"}`])), tektonPipeline: `hwlab-ci/${settings.pipelineName}`, diff --git a/scripts/hwlab-lane-expand.mjs b/scripts/hwlab-lane-expand.mjs index 7af9047c..2406feed 100644 --- a/scripts/hwlab-lane-expand.mjs +++ b/scripts/hwlab-lane-expand.mjs @@ -136,6 +136,7 @@ async function configureLane(deploy, options) { const target = deepTransform(clone(source), replacements); target.name = targetInfo.versionName; + target.node = target.node ?? source.node ?? "G14"; target.sourceBranch = targetInfo.sourceBranch; target.gitopsBranch = targetInfo.gitopsBranch; target.namespace = targetInfo.namespace; @@ -148,7 +149,7 @@ async function configureLane(deploy, options) { legacyApi: `http://${options.host}:${targetInfo.apiPort}` }; target.artifactCatalog = `deploy/artifact-catalog.${options.lane}.json`; - target.runtimePath = `deploy/gitops/g14/runtime-${options.lane}`; + target.runtimePath = `runtime-${options.lane}`; target.imageTagMode = target.imageTagMode ?? "full"; deploy.lanes[options.lane] = target; diff --git a/scripts/src/g14-ci-plan-lib.mjs b/scripts/src/g14-ci-plan-lib.mjs index b609a66a..36a5472e 100644 --- a/scripts/src/g14-ci-plan-lib.mjs +++ b/scripts/src/g14-ci-plan-lib.mjs @@ -42,7 +42,7 @@ export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([ "deploy/gitops/", "deploy/frp/", "scripts/g14-gitops-render.mjs", - "scripts/src/g14-gitops-lane.ts", + "scripts/src/runtime-lane.ts", "tsconfig.gitops.json" ]); diff --git a/scripts/src/g14-gitops-lane.ts b/scripts/src/runtime-lane.ts similarity index 67% rename from scripts/src/g14-gitops-lane.ts rename to scripts/src/runtime-lane.ts index b4098089..2d87b9a0 100644 --- a/scripts/src/g14-gitops-lane.ts +++ b/scripts/src/runtime-lane.ts @@ -1,4 +1,6 @@ type RuntimeLaneDefaults = { + defaultNodeId?: string; + defaultGitopsRoot?: string; defaultBranch?: string; defaultGitopsBranch?: string; defaultCatalogPath?: string; @@ -10,6 +12,8 @@ type RuntimeLaneDefaults = { export type GitOpsRenderArgs = { lane: string; + nodeId?: string; + outDir: string; sourceBranch: string; gitopsBranch: string; catalogPath: string; @@ -21,9 +25,11 @@ export type GitOpsRenderArgs = { }; export type RuntimeLaneConfig = { + node?: string; sourceBranch?: string; gitopsBranch?: string; artifactCatalog?: string; + runtimePath?: string; imageTagMode?: string; endpoint?: string; publicEndpoints?: { @@ -34,6 +40,12 @@ export type RuntimeLaneConfig = { sourceRepo?: string; }; +export type NodeConfig = { + gitopsRoot?: string; + sourceRepo?: string; + publicHost?: string; +}; + export type RuntimeLaneMirrorSpec = { lane: string; sourceBranch: string; @@ -43,9 +55,12 @@ export type RuntimeLaneMirrorSpec = { }; export type DeployConfig = { + nodes?: Record; lanes?: Record; }; +const defaultNodeId = "G14"; +const defaultGitopsRoot = "deploy/gitops/g14"; const defaultPublicHost = "74.48.78.17"; export function isRuntimeLane(lane: unknown): lane is string { @@ -71,6 +86,28 @@ export function runtimePathForRuntimeLane(lane: string): string { return `runtime-${lane}`; } +export function nodeConfig(deploy: DeployConfig, nodeId: string): NodeConfig { + const config = deploy.nodes?.[nodeId]; + return isObject(config) ? config : {}; +} + +export function nodeIdForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string { + const laneConfig = runtimeLaneConfig(deploy, lane); + return laneConfig.node ?? defaults.defaultNodeId ?? defaultNodeId; +} + +export function gitopsRootForNode(deploy: DeployConfig, nodeId: string, defaults: RuntimeLaneDefaults = {}): string { + const config = nodeConfig(deploy, nodeId); + return normalizeRepoPath(config.gitopsRoot ?? defaults.defaultGitopsRoot ?? defaultGitopsRoot); +} + +export function runtimeGitopsPathForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string { + const laneConfig = runtimeLaneConfig(deploy, lane); + const runtimePath = normalizeRepoPath(laneConfig.runtimePath ?? runtimePathForRuntimeLane(lane)); + if (runtimePath.startsWith("deploy/")) return runtimePath; + return joinRepoPath(gitopsRootForNode(deploy, nodeIdForRuntimeLane(deploy, lane, defaults), defaults), runtimePath); +} + export function defaultPortForRuntimeLane(lane: string): number { return 19466 + runtimeLaneMinor(lane) * 100; } @@ -95,8 +132,12 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep if (!isRuntimeLane(args.lane)) return args; const result: GitOpsRenderArgs = { ...args }; const laneConfig = runtimeLaneConfig(deploy, args.lane); + const nodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults); + const node = nodeConfig(deploy, nodeId); const versionName = versionNameForRuntimeLane(args.lane); - const defaultEndpointOptions = defaults.defaultV02RuntimeEndpoint ? { v02Endpoint: defaults.defaultV02RuntimeEndpoint } : {}; + const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {}; + if (node.publicHost) defaultEndpointOptions.host = node.publicHost; + if (defaults.defaultV02RuntimeEndpoint) defaultEndpointOptions.v02Endpoint = defaults.defaultV02RuntimeEndpoint; const defaultEndpoint = defaultEndpointForRuntimeLane(args.lane, defaultEndpointOptions); const configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint; const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint; @@ -107,19 +148,22 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep const defaultRuntimeEndpoint = defaults.defaultRuntimeEndpoint ?? "http://74.48.78.17:17667"; const defaultWebEndpoint = defaults.defaultWebEndpoint ?? "http://74.48.78.17:17666"; const defaultSourceRepo = defaults.defaultSourceRepo ?? "git@github.com:pikasTech/HWLAB.git"; + const defaultOutDir = defaults.defaultGitopsRoot ?? defaultGitopsRoot; const defaultCatalog = `deploy/artifact-catalog.${args.lane}.json`; + if (!result.nodeId || result.nodeId === defaultNodeId) result.nodeId = nodeId; + if (result.outDir === defaultOutDir) result.outDir = gitopsRootForNode(deploy, nodeId, defaults); if (result.sourceBranch === defaultBranch || result.sourceBranch === versionName) result.sourceBranch = laneConfig.sourceBranch ?? versionName; if (result.gitopsBranch === defaultGitopsBranch || result.gitopsBranch === `${versionName}-gitops`) result.gitopsBranch = laneConfig.gitopsBranch ?? `${versionName}-gitops`; if (result.catalogPath === defaultCatalogPath || result.catalogPath === defaultCatalog) result.catalogPath = laneConfig.artifactCatalog ?? defaultCatalog; if (result.imageTagMode === "full") result.imageTagMode = laneConfig.imageTagMode ?? result.imageTagMode; if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint; if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint; - if (result.sourceRepo === defaultSourceRepo && laneConfig.sourceRepo) result.sourceRepo = laneConfig.sourceRepo; + if (result.sourceRepo === defaultSourceRepo) result.sourceRepo = laneConfig.sourceRepo ?? node.sourceRepo ?? result.sourceRepo; return result; } -export function runtimeLaneMirrorSpecs(deploy: DeployConfig): RuntimeLaneMirrorSpec[] { +export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] { return Object.entries(deploy.lanes ?? {}) .filter(([lane, config]) => isRuntimeLane(lane) && isObject(config)) .sort(([left], [right]) => runtimeLaneMinor(left) - runtimeLaneMinor(right)) @@ -131,11 +175,19 @@ export function runtimeLaneMirrorSpecs(deploy: DeployConfig): RuntimeLaneMirrorS sourceBranch: laneConfig.sourceBranch ?? versionName, gitopsBranch: laneConfig.gitopsBranch ?? `${versionName}-gitops`, artifactCatalog: laneConfig.artifactCatalog ?? `deploy/artifact-catalog.${lane}.json`, - runtimePath: `deploy/gitops/g14/${runtimePathForRuntimeLane(lane)}`, + runtimePath: runtimeGitopsPathForRuntimeLane(deploy, lane, defaults), }; }); } +function normalizeRepoPath(value: string): string { + return value.replaceAll("\\\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, ""); +} + +function joinRepoPath(left: string, right: string): string { + return `${normalizeRepoPath(left)}/${normalizeRepoPath(right).replace(/^\/+/, "")}`; +} + function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/tsconfig.gitops.json b/tsconfig.gitops.json index 6510f0f7..ff17b691 100644 --- a/tsconfig.gitops.json +++ b/tsconfig.gitops.json @@ -19,6 +19,6 @@ "useUnknownInCatchVariables": true }, "include": [ - "scripts/src/g14-gitops-lane.ts" + "scripts/src/runtime-lane.ts" ] }