fix(ci OPS50): guards de banco aceitam o gateway da bridge do runner (RFC1918) — postgres do services: fica no IP do gateway, não em localhost
Some checks failed
Agent PR Ready + auto-merge / ready-automerge (pull_request) Has been skipped
CI (PR) / docs-guards (pull_request) Successful in 6s
CI (PR) / plans-only-closes (pull_request) Successful in 6s
CI (PR) / scope (pull_request) Successful in 31s
CI (PR) / lint (pull_request) Successful in 40s
CI (PR) / format (pull_request) Successful in 48s
CI (PR) / typecheck (pull_request) Successful in 31s
CI (PR) / knip (pull_request) Successful in 32s
CI (PR) / cycles (pull_request) Successful in 33s
CI (PR) / unit (pull_request) Successful in 46s
CI (PR) / build (pull_request) Has been skipped
CI (PR) / int (pull_request) Failing after 1m5s
CI (PR) / e2e (pull_request) Failing after 2m47s
CI (PR) / checks (pull_request) Failing after 1s
Some checks failed
Agent PR Ready + auto-merge / ready-automerge (pull_request) Has been skipped
CI (PR) / docs-guards (pull_request) Successful in 6s
CI (PR) / plans-only-closes (pull_request) Successful in 6s
CI (PR) / scope (pull_request) Successful in 31s
CI (PR) / lint (pull_request) Successful in 40s
CI (PR) / format (pull_request) Successful in 48s
CI (PR) / typecheck (pull_request) Successful in 31s
CI (PR) / knip (pull_request) Successful in 32s
CI (PR) / cycles (pull_request) Successful in 33s
CI (PR) / unit (pull_request) Successful in 46s
CI (PR) / build (pull_request) Has been skipped
CI (PR) / int (pull_request) Failing after 1m5s
CI (PR) / e2e (pull_request) Failing after 2m47s
CI (PR) / checks (pull_request) Failing after 1s
This commit is contained in:
parent
0de9e9f57d
commit
02f68894c3
3 changed files with 74 additions and 0 deletions
|
|
@ -7,6 +7,7 @@
|
|||
* any of these re-spelled outside this module.
|
||||
*/
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
|
@ -31,6 +32,44 @@ export const loadCliEnv = () => {
|
|||
/** Local database hosts — including the docker-compose service name. THE one list. */
|
||||
export const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres'])
|
||||
|
||||
/**
|
||||
* Gateway of the default route, read from /proc/net/route (Linux). The
|
||||
* self-hosted Forgejo runner publishes each job's `services:` Postgres on the
|
||||
* bridge gateway IP (192.168.x.1 — differs per job), so a job reaches it via
|
||||
* that IP instead of localhost. The same octet order the ci-pr.yml awk
|
||||
* produces (the /proc fields are little-endian). Returns null when there is
|
||||
* no default route or the file is unreadable (macOS/Windows dev boxes).
|
||||
*/
|
||||
export const defaultGatewayHost = () => {
|
||||
try {
|
||||
const routes = readFileSync('/proc/net/route', 'utf8').split('\n')
|
||||
for (const line of routes) {
|
||||
const fields = line.trim().split(/\s+/)
|
||||
if (fields[1] !== '00000000') continue
|
||||
const hex = fields[2] ?? ''
|
||||
if (hex === '00000000' || hex.length < 8) return null
|
||||
const octets = [
|
||||
parseInt(hex.slice(6, 8), 16),
|
||||
parseInt(hex.slice(4, 6), 16),
|
||||
parseInt(hex.slice(2, 4), 16),
|
||||
parseInt(hex.slice(0, 2), 16),
|
||||
]
|
||||
const isRfc1918 =
|
||||
octets[0] === 10 ||
|
||||
(octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) ||
|
||||
(octets[0] === 192 && octets[1] === 168)
|
||||
if (!isRfc1918) return null
|
||||
return octets.join('.')
|
||||
}
|
||||
} catch {
|
||||
// /proc/net/route is Linux-only; fall through to the static list.
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const gatewayHost = defaultGatewayHost()
|
||||
if (gatewayHost) LOCAL_HOSTS.add(gatewayHost)
|
||||
|
||||
export const ALLOW_REMOTE_DB_FLAG = 'ALLOW_REMOTE_DB'
|
||||
|
||||
export const isRemoteDbOverrideSet = () =>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@
|
|||
/** `teqo_test`, `teqo_wt15_test`, … — the _test suffix is the hard contract. */
|
||||
const TEST_DATABASE_NAME = /^teqo(_[a-z0-9]+)?_test$/
|
||||
|
||||
// Self-hosted Forgejo runner: the job's postgres service is published on the
|
||||
// bridge gateway IP (RFC1918), not localhost. Same source as the CLI guard so
|
||||
// both guards admit exactly the same hosts (OPS50).
|
||||
import { defaultGatewayHost } from '../../scripts/lib/cli.mjs'
|
||||
|
||||
export function assertTestDatabase(databaseUrl: string | undefined): void {
|
||||
if (!databaseUrl) {
|
||||
throw new Error(
|
||||
|
|
@ -29,6 +34,8 @@ export function assertTestDatabase(databaseUrl: string | undefined): void {
|
|||
|
||||
const databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, ''))
|
||||
const localHosts = new Set(['localhost', '127.0.0.1', '::1', '[::1]'])
|
||||
const gateway = defaultGatewayHost()
|
||||
if (gateway) localHosts.add(gateway)
|
||||
|
||||
if (
|
||||
parsedUrl.protocol !== 'postgresql:' ||
|
||||
|
|
|
|||
28
tests/unit/localHosts.unit.spec.ts
Normal file
28
tests/unit/localHosts.unit.spec.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { defaultGatewayHost, LOCAL_HOSTS } from '../../scripts/lib/cli.mjs'
|
||||
|
||||
describe('LOCAL_HOSTS (OPS50 — self-hosted runner reaches services via the bridge gateway)', () => {
|
||||
it('still contains the canonical static hosts', () => {
|
||||
for (const host of ['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) {
|
||||
expect(LOCAL_HOSTS.has(host)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('admits the machine default gateway when it is RFC1918 (Linux CI bridge)', () => {
|
||||
const gateway = defaultGatewayHost()
|
||||
if (gateway) {
|
||||
// The Forgejo runner publishes the job's postgres service on the bridge
|
||||
// gateway IP; assertLocalDatabase must accept it so seeds run in CI.
|
||||
expect(LOCAL_HOSTS.has(gateway)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('never admits public or loopback hosts', () => {
|
||||
expect(LOCAL_HOSTS.has('8.8.8.8')).toBe(false)
|
||||
expect(LOCAL_HOSTS.has('127.0.0.2')).toBe(false)
|
||||
expect(LOCAL_HOSTS.has('203.0.113.1')).toBe(false)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue