@highstate/common 0.14.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WFWXDYUX.js → chunk-X5BK6JSN.js} +877 -194
- package/dist/chunk-X5BK6JSN.js.map +1 -0
- package/dist/highstate.manifest.json +12 -2
- package/dist/index.js +1 -1
- package/dist/units/databases/etcd-patch/index.js +20 -0
- package/dist/units/databases/etcd-patch/index.js.map +1 -0
- package/dist/units/databases/existing-etcd/index.js +14 -0
- package/dist/units/databases/existing-etcd/index.js.map +1 -0
- package/dist/units/databases/existing-mariadb/index.js +2 -2
- package/dist/units/databases/existing-mariadb/index.js.map +1 -1
- package/dist/units/databases/existing-mongodb/index.js +2 -2
- package/dist/units/databases/existing-mongodb/index.js.map +1 -1
- package/dist/units/databases/existing-postgresql/index.js +2 -2
- package/dist/units/databases/existing-postgresql/index.js.map +1 -1
- package/dist/units/databases/existing-redis/index.js +2 -2
- package/dist/units/databases/existing-redis/index.js.map +1 -1
- package/dist/units/databases/existing-s3/index.js +18 -0
- package/dist/units/databases/existing-s3/index.js.map +1 -0
- package/dist/units/databases/mariadb-patch/index.js +24 -0
- package/dist/units/databases/mariadb-patch/index.js.map +1 -0
- package/dist/units/databases/mongodb-patch/index.js +24 -0
- package/dist/units/databases/mongodb-patch/index.js.map +1 -0
- package/dist/units/databases/postgresql-patch/index.js +24 -0
- package/dist/units/databases/postgresql-patch/index.js.map +1 -0
- package/dist/units/databases/redis-patch/index.js +27 -0
- package/dist/units/databases/redis-patch/index.js.map +1 -0
- package/dist/units/databases/s3-patch/index.js +25 -0
- package/dist/units/databases/s3-patch/index.js.map +1 -0
- package/dist/units/dns/record-set/index.js +14 -20
- package/dist/units/dns/record-set/index.js.map +1 -1
- package/dist/units/existing-server/index.js +3 -4
- package/dist/units/existing-server/index.js.map +1 -1
- package/dist/units/network/address-space/index.js +20 -0
- package/dist/units/network/address-space/index.js.map +1 -0
- package/dist/units/network/endpoint-filter/index.js +15 -0
- package/dist/units/network/endpoint-filter/index.js.map +1 -0
- package/dist/units/network/l3-endpoint/index.js +2 -2
- package/dist/units/network/l3-endpoint/index.js.map +1 -1
- package/dist/units/network/l4-endpoint/index.js +2 -2
- package/dist/units/network/l4-endpoint/index.js.map +1 -1
- package/dist/units/network/l7-endpoint/index.js +12 -0
- package/dist/units/network/l7-endpoint/index.js.map +1 -0
- package/dist/units/script/index.js +1 -1
- package/dist/units/server-patch/index.js +9 -12
- package/dist/units/server-patch/index.js.map +1 -1
- package/dist/units/ssh/key-pair/index.js +1 -1
- package/package.json +64 -10
- package/src/shared/command.ts +1 -1
- package/src/shared/dns.ts +11 -93
- package/src/shared/files.ts +3 -3
- package/src/shared/impl-ref.ts +4 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network/address-space.spec.ts +114 -0
- package/src/shared/network/address-space.ts +364 -0
- package/src/shared/network/address.spec.ts +109 -0
- package/src/shared/network/address.ts +119 -0
- package/src/shared/network/endpoints.spec.ts +249 -0
- package/src/shared/network/endpoints.ts +608 -0
- package/src/shared/network/index.ts +4 -0
- package/src/shared/network/ip.ts +236 -0
- package/src/shared/network/subnet.spec.ts +62 -0
- package/src/shared/network/subnet.ts +137 -0
- package/src/shared/ssh.ts +1 -1
- package/src/shared/tls.ts +21 -5
- package/src/shared/utils.ts +93 -0
- package/src/units/databases/etcd-patch/index.ts +23 -0
- package/src/units/databases/existing-etcd/index.ts +11 -0
- package/src/units/databases/existing-mariadb/index.ts +1 -1
- package/src/units/databases/existing-mongodb/index.ts +1 -1
- package/src/units/databases/existing-postgresql/index.ts +1 -1
- package/src/units/databases/existing-redis/index.ts +1 -1
- package/src/units/databases/existing-s3/index.ts +1 -1
- package/src/units/databases/mariadb-patch/index.ts +27 -0
- package/src/units/databases/mongodb-patch/index.ts +27 -0
- package/src/units/databases/postgresql-patch/index.ts +27 -0
- package/src/units/databases/redis-patch/index.ts +32 -0
- package/src/units/databases/s3-patch/index.ts +28 -0
- package/src/units/dns/record-set/index.ts +15 -20
- package/src/units/existing-server/index.ts +3 -4
- package/src/units/network/address-space/index.ts +20 -0
- package/src/units/network/endpoint-filter/index.ts +5 -5
- package/src/units/network/l3-endpoint/index.ts +2 -2
- package/src/units/network/l4-endpoint/index.ts +2 -2
- package/src/units/network/l7-endpoint/index.ts +2 -2
- package/src/units/remote-file/index.ts +12 -5
- package/src/units/server-patch/index.ts +10 -13
- package/dist/chunk-WFWXDYUX.js.map +0 -1
- package/dist/units/server-dns/index.js +0 -26
- package/dist/units/server-dns/index.js.map +0 -1
- package/src/shared/network.ts +0 -413
- package/src/units/server-dns/index.ts +0 -26
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { network } from "@highstate/library"
|
|
2
|
+
|
|
3
|
+
export type AddressType = network.AddressType
|
|
4
|
+
|
|
5
|
+
export type ParsedIp = {
|
|
6
|
+
type: AddressType
|
|
7
|
+
value: bigint
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ParsedCidr = {
|
|
11
|
+
type: AddressType
|
|
12
|
+
ip: bigint
|
|
13
|
+
prefixLength: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseIp(value: string): ParsedIp {
|
|
17
|
+
if (value.includes(":")) {
|
|
18
|
+
return { type: "ipv6", value: parseIpv6(value) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { type: "ipv4", value: parseIpv4(value) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseCidr(value: string): ParsedCidr {
|
|
25
|
+
const [ipPart, prefixPart, ...rest] = value.split("/")
|
|
26
|
+
if (!ipPart || !prefixPart || rest.length > 0) {
|
|
27
|
+
throw new Error(`Invalid CIDR: "${value}"`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parsedIp = parseIp(ipPart.trim())
|
|
31
|
+
const prefix = parseInt(prefixPart.trim(), 10)
|
|
32
|
+
if (!Number.isFinite(prefix)) {
|
|
33
|
+
throw new Error(`Invalid CIDR prefix: "${value}"`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const bits = parsedIp.type === "ipv4" ? 32 : 128
|
|
37
|
+
if (prefix < 0 || prefix > bits) {
|
|
38
|
+
throw new Error(`Invalid CIDR prefix length: "${value}"`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { type: parsedIp.type, ip: parsedIp.value, prefixLength: prefix }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function subnetBaseFromCidr(parsed: ParsedCidr): bigint {
|
|
45
|
+
const bits = parsed.type === "ipv4" ? 32 : 128
|
|
46
|
+
|
|
47
|
+
if (parsed.prefixLength === 0) {
|
|
48
|
+
return 0n
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const mask = ((1n << BigInt(parsed.prefixLength)) - 1n) << BigInt(bits - parsed.prefixLength)
|
|
52
|
+
return parsed.ip & mask
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function cidrBlockSize(type: AddressType, prefixLength: number): bigint {
|
|
56
|
+
const bits = type === "ipv4" ? 32 : 128
|
|
57
|
+
return 1n << BigInt(bits - prefixLength)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ipToString(type: AddressType, value: bigint): string {
|
|
61
|
+
return type === "ipv4" ? ipv4ToString(value) : ipv6ToString(value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseIpv4(value: string): bigint {
|
|
65
|
+
const parts = value.trim().split(".")
|
|
66
|
+
if (parts.length !== 4) {
|
|
67
|
+
throw new Error(`Invalid IPv4 address: "${value}"`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let result = 0n
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
if (!part) {
|
|
73
|
+
throw new Error(`Invalid IPv4 address: "${value}"`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const octet = parseInt(part, 10)
|
|
77
|
+
if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
|
|
78
|
+
throw new Error(`Invalid IPv4 address: "${value}"`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result = (result << 8n) + BigInt(octet)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseIpv6(value: string): bigint {
|
|
88
|
+
const input = value.trim().toLowerCase()
|
|
89
|
+
if (!input) {
|
|
90
|
+
throw new Error(`Invalid IPv6 address: "${value}"`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let leftParts: string[] = []
|
|
94
|
+
let rightParts: string[] = []
|
|
95
|
+
|
|
96
|
+
const doubleColonIndex = input.indexOf("::")
|
|
97
|
+
if (doubleColonIndex >= 0) {
|
|
98
|
+
const [left, right] = input.split("::")
|
|
99
|
+
leftParts = left ? left.split(":") : []
|
|
100
|
+
rightParts = right ? right.split(":") : []
|
|
101
|
+
} else {
|
|
102
|
+
leftParts = input.split(":")
|
|
103
|
+
rightParts = []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const expandIpv4Tail = (parts: string[]): string[] => {
|
|
107
|
+
if (parts.length === 0) return parts
|
|
108
|
+
const last = parts.at(-1)!
|
|
109
|
+
if (!last.includes(".")) {
|
|
110
|
+
return parts
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ipv4Value = parseIpv4(last)
|
|
114
|
+
const high = Number((ipv4Value >> 16n) & 0xffffn)
|
|
115
|
+
const low = Number(ipv4Value & 0xffffn)
|
|
116
|
+
|
|
117
|
+
return [...parts.slice(0, -1), high.toString(16), low.toString(16)]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
leftParts = leftParts.filter(p => p.length > 0)
|
|
121
|
+
rightParts = rightParts.filter(p => p.length > 0)
|
|
122
|
+
|
|
123
|
+
leftParts = expandIpv4Tail(leftParts)
|
|
124
|
+
rightParts = expandIpv4Tail(rightParts)
|
|
125
|
+
|
|
126
|
+
const totalParts = leftParts.length + rightParts.length
|
|
127
|
+
if (doubleColonIndex < 0 && totalParts !== 8) {
|
|
128
|
+
throw new Error(`Invalid IPv6 address: "${value}"`)
|
|
129
|
+
}
|
|
130
|
+
if (totalParts > 8) {
|
|
131
|
+
throw new Error(`Invalid IPv6 address: "${value}"`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const missing = 8 - totalParts
|
|
135
|
+
const parts =
|
|
136
|
+
doubleColonIndex >= 0
|
|
137
|
+
? [...leftParts, ...Array.from({ length: missing }, () => "0"), ...rightParts]
|
|
138
|
+
: leftParts
|
|
139
|
+
|
|
140
|
+
if (parts.length !== 8) {
|
|
141
|
+
throw new Error(`Invalid IPv6 address: "${value}"`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let result = 0n
|
|
145
|
+
for (const part of parts) {
|
|
146
|
+
if (!part) {
|
|
147
|
+
throw new Error(`Invalid IPv6 address: "${value}"`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const hextet = parseInt(part, 16)
|
|
151
|
+
if (!Number.isInteger(hextet) || hextet < 0 || hextet > 0xffff) {
|
|
152
|
+
throw new Error(`Invalid IPv6 address: "${value}"`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
result = (result << 16n) + BigInt(hextet)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ipv4ToString(value: bigint): string {
|
|
162
|
+
const parts = [
|
|
163
|
+
Number((value >> 24n) & 0xffn),
|
|
164
|
+
Number((value >> 16n) & 0xffn),
|
|
165
|
+
Number((value >> 8n) & 0xffn),
|
|
166
|
+
Number(value & 0xffn),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
return parts.join(".")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ipv6ToString(value: bigint): string {
|
|
173
|
+
const hextets: number[] = []
|
|
174
|
+
for (let i = 0; i < 8; i++) {
|
|
175
|
+
const shift = BigInt((7 - i) * 16)
|
|
176
|
+
hextets.push(Number((value >> shift) & 0xffffn))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Find the longest run of zeros to compress.
|
|
180
|
+
let bestStart = -1
|
|
181
|
+
let bestLength = 0
|
|
182
|
+
let currentStart = -1
|
|
183
|
+
let currentLength = 0
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < hextets.length; i++) {
|
|
186
|
+
if (hextets[i] === 0) {
|
|
187
|
+
if (currentStart === -1) {
|
|
188
|
+
currentStart = i
|
|
189
|
+
currentLength = 1
|
|
190
|
+
} else {
|
|
191
|
+
currentLength++
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (currentLength > bestLength) {
|
|
195
|
+
bestStart = currentStart
|
|
196
|
+
bestLength = currentLength
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
currentStart = -1
|
|
200
|
+
currentLength = 0
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// RFC 5952: only compress runs of 2+ hextets.
|
|
205
|
+
if (bestLength < 2) {
|
|
206
|
+
bestStart = -1
|
|
207
|
+
bestLength = 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const parts: string[] = []
|
|
211
|
+
for (let i = 0; i < hextets.length; i++) {
|
|
212
|
+
if (bestStart >= 0 && i >= bestStart && i < bestStart + bestLength) {
|
|
213
|
+
if (i === bestStart) {
|
|
214
|
+
parts.push("")
|
|
215
|
+
}
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
parts.push(hextets[i]!.toString(16))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let result = parts.join(":")
|
|
223
|
+
if (bestStart === 0) {
|
|
224
|
+
result = `:${result}`
|
|
225
|
+
}
|
|
226
|
+
if (bestStart >= 0 && bestStart + bestLength === 8) {
|
|
227
|
+
result = `${result}:`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (result === "") {
|
|
231
|
+
return "::"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Normalize possible ":::" artifacts into "::".
|
|
235
|
+
return result.replace(/:{3,}/g, "::")
|
|
236
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { network } from "@highstate/library"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { parseAddress } from "./address"
|
|
4
|
+
import { parseSubnet, subnetToString } from "./subnet"
|
|
5
|
+
|
|
6
|
+
describe("parseSubnet", () => {
|
|
7
|
+
it("parses an IPv4 CIDR string and canonicalizes the base address", () => {
|
|
8
|
+
const result = parseSubnet("10.0.0.7/24")
|
|
9
|
+
|
|
10
|
+
expect(result.type).toBe("ipv4")
|
|
11
|
+
expect(result.baseAddress).toBe("10.0.0.0")
|
|
12
|
+
expect(subnetToString(result)).toBe("10.0.0.0/24")
|
|
13
|
+
expect(result.prefixLength).toBe(24)
|
|
14
|
+
|
|
15
|
+
expect(network.subnetEntity.schema.safeParse(result).success).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("treats an IPv4 address string as a /32 subnet", () => {
|
|
19
|
+
const result = parseSubnet("10.0.0.7")
|
|
20
|
+
|
|
21
|
+
expect(result.type).toBe("ipv4")
|
|
22
|
+
expect(result.baseAddress).toBe("10.0.0.7")
|
|
23
|
+
expect(subnetToString(result)).toBe("10.0.0.7/32")
|
|
24
|
+
expect(result.prefixLength).toBe(32)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("canonicalizes IPv6 subnet strings", () => {
|
|
28
|
+
const result = parseSubnet("2001:db8:0:0:0:0:0:1/64")
|
|
29
|
+
|
|
30
|
+
expect(result.type).toBe("ipv6")
|
|
31
|
+
expect(result.baseAddress).toBe("2001:db8::")
|
|
32
|
+
expect(subnetToString(result)).toBe("2001:db8::/64")
|
|
33
|
+
expect(result.prefixLength).toBe(64)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("treats an IPv6 address string as a /128 subnet", () => {
|
|
37
|
+
const result = parseSubnet("2001:db8:0:0:0:0:0:1")
|
|
38
|
+
|
|
39
|
+
expect(result.type).toBe("ipv6")
|
|
40
|
+
expect(result.baseAddress).toBe("2001:db8::1")
|
|
41
|
+
expect(subnetToString(result)).toBe("2001:db8::1/128")
|
|
42
|
+
expect(result.prefixLength).toBe(128)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("treats an Address entity as a host subnet", () => {
|
|
46
|
+
const address = parseAddress("10.0.0.7")
|
|
47
|
+
const result = parseSubnet(address)
|
|
48
|
+
|
|
49
|
+
expect(result.type).toBe("ipv4")
|
|
50
|
+
expect(result.baseAddress).toBe("10.0.0.7")
|
|
51
|
+
expect(subnetToString(result)).toBe("10.0.0.7/32")
|
|
52
|
+
expect(result.prefixLength).toBe(32)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("throws on invalid subnet", () => {
|
|
56
|
+
expect(() => parseSubnet("not-a-cidr")).toThrow(/Invalid/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("throws on invalid prefix", () => {
|
|
60
|
+
expect(() => parseSubnet("10.0.0.0/33")).toThrow(/Invalid CIDR prefix length/)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { check } from "@highstate/contract"
|
|
2
|
+
import { network } from "@highstate/library"
|
|
3
|
+
import { type InputArray, toPromise } from "@highstate/pulumi"
|
|
4
|
+
import { filter, isNonNullish, map, pipe, uniqueBy } from "remeda"
|
|
5
|
+
import { doesAddressBelongToSubnet } from "./address"
|
|
6
|
+
import { ipToString, type ParsedCidr, parseCidr, parseIp, subnetBaseFromCidr } from "./ip"
|
|
7
|
+
|
|
8
|
+
export type InputSubnet = network.Subnet | network.Address | string
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parses and normalizes the given subnet string.
|
|
12
|
+
*
|
|
13
|
+
* If a Subnet entity is given, it is returned as-is.
|
|
14
|
+
*
|
|
15
|
+
* @param subnet The subnet to parse.
|
|
16
|
+
* @returns The normalized Subnet entity.
|
|
17
|
+
*/
|
|
18
|
+
export function parseSubnet(subnet: InputSubnet): network.Subnet {
|
|
19
|
+
if (check(network.subnetEntity.schema, subnet)) {
|
|
20
|
+
return subnet
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (check(network.addressEntity.schema, subnet)) {
|
|
24
|
+
const prefixLength = subnet.type === "ipv4" ? 32 : 128
|
|
25
|
+
|
|
26
|
+
const result: network.Subnet = {
|
|
27
|
+
type: subnet.type,
|
|
28
|
+
baseAddress: subnet.value,
|
|
29
|
+
prefixLength,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const validated = network.subnetEntity.schema.safeParse(result)
|
|
33
|
+
if (!validated.success) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Invalid subnet "${subnet.value}/${prefixLength}": ${validated.error.message}`,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return validated.data
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const input = subnet.trim()
|
|
43
|
+
if (!input) {
|
|
44
|
+
throw new Error("Empty subnet string")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let parsed: ParsedCidr
|
|
48
|
+
if (input.includes("/")) {
|
|
49
|
+
parsed = parseCidr(input)
|
|
50
|
+
} else {
|
|
51
|
+
const ip = parseIp(input)
|
|
52
|
+
const prefixLength = ip.type === "ipv4" ? 32 : 128
|
|
53
|
+
parsed = { type: ip.type, ip: ip.value, prefixLength }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const subnetBase = subnetBaseFromCidr(parsed)
|
|
57
|
+
const baseAddress = ipToString(parsed.type, subnetBase)
|
|
58
|
+
|
|
59
|
+
const result: network.Subnet = {
|
|
60
|
+
type: parsed.type,
|
|
61
|
+
baseAddress,
|
|
62
|
+
prefixLength: parsed.prefixLength,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const validated = network.subnetEntity.schema.safeParse(result)
|
|
66
|
+
if (!validated.success) {
|
|
67
|
+
throw new Error(`Invalid subnet "${input}": ${validated.error.message}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return validated.data
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const privateIpV4Subnets = [
|
|
74
|
+
parseSubnet("10.0.0.0/8"),
|
|
75
|
+
parseSubnet("127.0.0.0/8"),
|
|
76
|
+
parseSubnet("172.16.0.0/12"),
|
|
77
|
+
parseSubnet("192.168.0.0/16"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
export const privateIpV6Subnets = [
|
|
81
|
+
parseSubnet("fc00::/7"),
|
|
82
|
+
|
|
83
|
+
// IPv4-mapped private ranges.
|
|
84
|
+
parseSubnet("::ffff:10.0.0.0/104"),
|
|
85
|
+
parseSubnet("::ffff:127.0.0.0/104"),
|
|
86
|
+
parseSubnet("::ffff:172.16.0.0/108"),
|
|
87
|
+
parseSubnet("::ffff:192.168.0.0/112"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
export const privateSubnets = [...privateIpV4Subnets, ...privateIpV6Subnets]
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks whether the given address is a private address.
|
|
94
|
+
*
|
|
95
|
+
* @param address The address to check.
|
|
96
|
+
* @returns True if the address is private, false otherwise.
|
|
97
|
+
*/
|
|
98
|
+
export function isPrivateAddress(address: network.Address): boolean {
|
|
99
|
+
for (const subnet of privateSubnets) {
|
|
100
|
+
if (doesAddressBelongToSubnet(address, subnet)) {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parses multiple subnets from strings and input objects.
|
|
110
|
+
*
|
|
111
|
+
* @param subnets The subnet strings to parse.
|
|
112
|
+
* @param inputSubnets The input subnet objects to use.
|
|
113
|
+
* @returns The parsed list of subnets objects with duplicates removed.
|
|
114
|
+
*/
|
|
115
|
+
export async function parseSubnets(
|
|
116
|
+
subnets: (string | undefined | null)[] | null | undefined,
|
|
117
|
+
inputSubnets: InputArray<network.Subnet | undefined | null> | null | undefined,
|
|
118
|
+
): Promise<network.Subnet[]> {
|
|
119
|
+
const resolvedInputSubnets = await toPromise(inputSubnets ?? [])
|
|
120
|
+
|
|
121
|
+
return pipe(
|
|
122
|
+
[...(subnets ?? []), ...resolvedInputSubnets],
|
|
123
|
+
filter(isNonNullish),
|
|
124
|
+
map(subnet => parseSubnet(subnet)),
|
|
125
|
+
uniqueBy(subnet => subnetToString(subnet)),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Converts a subnet to its canonical string representation (CIDR).
|
|
131
|
+
*
|
|
132
|
+
* @param subnet The subnet to convert.
|
|
133
|
+
* @returns The string representation of the subnet.
|
|
134
|
+
*/
|
|
135
|
+
export function subnetToString(subnet: network.Subnet): string {
|
|
136
|
+
return `${subnet.baseAddress}/${subnet.prefixLength}`
|
|
137
|
+
}
|
package/src/shared/ssh.ts
CHANGED
|
@@ -13,7 +13,7 @@ import getKeys, { PrivateExport } from "micro-key-producer/ssh.js"
|
|
|
13
13
|
import { randomBytes } from "micro-key-producer/utils.js"
|
|
14
14
|
import * as images from "../../assets/images.json"
|
|
15
15
|
import { Command } from "./command"
|
|
16
|
-
import { l3EndpointToL4, l3EndpointToString } from "./network"
|
|
16
|
+
import { l3EndpointToL4, l3EndpointToString } from "./network/endpoints"
|
|
17
17
|
|
|
18
18
|
export async function createSshTerminal(
|
|
19
19
|
credentials: Input<ssh.Connection>,
|
package/src/shared/tls.ts
CHANGED
|
@@ -74,11 +74,11 @@ export class TlsCertificate extends ComponentResource {
|
|
|
74
74
|
}).apply(async ({ issuers, commonName, dnsNames }) => {
|
|
75
75
|
// for now, we require single issuer to match all requested names
|
|
76
76
|
const matchedIssuer = issuers.find(issuer => {
|
|
77
|
-
if (commonName && !commonName.endsWith(
|
|
77
|
+
if (commonName && !issuer.zones.some(zone => commonName.endsWith(zone))) {
|
|
78
78
|
return false
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
if (dnsNames && !dnsNames.every(name => name.endsWith(
|
|
81
|
+
if (dnsNames && !dnsNames.every(name => issuer.zones.some(zone => name.endsWith(zone)))) {
|
|
82
82
|
return false
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -86,9 +86,25 @@ export class TlsCertificate extends ComponentResource {
|
|
|
86
86
|
})
|
|
87
87
|
|
|
88
88
|
if (!matchedIssuer) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
if (commonName && dnsNames && dnsNames.length > 0) {
|
|
90
|
+
const dnsNameList = dnsNames.join(", ")
|
|
91
|
+
|
|
92
|
+
throw new Error(
|
|
93
|
+
`No TLS issuer matched the common name "${commonName}" and DNS names "${dnsNameList}"`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (commonName) {
|
|
98
|
+
throw new Error(`No TLS issuer matched the common name "${commonName}"`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (dnsNames && dnsNames.length > 0) {
|
|
102
|
+
const dnsNameList = dnsNames.join(", ")
|
|
103
|
+
|
|
104
|
+
throw new Error(`No TLS issuer matched the DNS names "${dnsNameList}"`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error("No TLS issuer provided")
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
return await tlsCertificateMediator.call(matchedIssuer.implRef, {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Metadata, MetadataContainer } from "@highstate/library"
|
|
2
|
+
import { compile } from "filter-expression"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Filter a list of items using a filter expression.
|
|
6
|
+
*
|
|
7
|
+
* See [filter-expression](https://github.com/tronghieu/filter-expression?tab=readme-ov-file#language) for more details on the expression syntax.
|
|
8
|
+
*
|
|
9
|
+
* @param items The items to filter.
|
|
10
|
+
* @param expression The filter expression.
|
|
11
|
+
* @param getContext The function to get the context for each item. Defaults to the item itself.
|
|
12
|
+
* @returns The filtered items.
|
|
13
|
+
*/
|
|
14
|
+
export function filterByExpression<T>(
|
|
15
|
+
items: T[],
|
|
16
|
+
expression: string,
|
|
17
|
+
getContext = (item: T) => item as Record<string, unknown>,
|
|
18
|
+
): T[] {
|
|
19
|
+
const { evaluate } = compile(expression)
|
|
20
|
+
|
|
21
|
+
return items.filter(item => evaluate(getContext(item)))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Filter a list of items with metadata using a filter expression.
|
|
26
|
+
*
|
|
27
|
+
* See `filterByExpression` for more details.
|
|
28
|
+
*
|
|
29
|
+
* @param items The items to filter.
|
|
30
|
+
* @param expression The filter expression.
|
|
31
|
+
* @param getContext The function to get the context for each item. Defaults to the item itself.
|
|
32
|
+
* @returns The filtered items.
|
|
33
|
+
*/
|
|
34
|
+
export function filterWithMetadataByExpression<T extends MetadataContainer>(
|
|
35
|
+
items: T[],
|
|
36
|
+
expression: string,
|
|
37
|
+
getContext = (item: T) => item as Record<string, unknown>,
|
|
38
|
+
): T[] {
|
|
39
|
+
return filterByExpression(items, expression, item =>
|
|
40
|
+
getContext({ ...item, metadata: item.metadata ? flattenMetadata(item.metadata) : undefined }),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Transforms each dot-separated key in the metadata into a nested object structure.
|
|
46
|
+
*
|
|
47
|
+
* Should be used to create valid context for `filterByExpression`.
|
|
48
|
+
*
|
|
49
|
+
* Example:
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* const metadata = {
|
|
53
|
+
* "k8s.service": {}
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* becomes
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* const flattened = {
|
|
61
|
+
* k8s: {
|
|
62
|
+
* service: {}
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @param metadata The metadata to flatten.
|
|
68
|
+
* @returns The flattened metadata.
|
|
69
|
+
*/
|
|
70
|
+
export function flattenMetadata(metadata: Metadata): Record<string, unknown> {
|
|
71
|
+
const result = {}
|
|
72
|
+
|
|
73
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
74
|
+
const path = key.split(".")
|
|
75
|
+
// biome-ignore lint/suspicious/noExplicitAny: to simplify implementation
|
|
76
|
+
let current: any = result
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < path.length; i++) {
|
|
79
|
+
const segment = path[i]
|
|
80
|
+
|
|
81
|
+
if (i === path.length - 1) {
|
|
82
|
+
current[segment] = value
|
|
83
|
+
} else {
|
|
84
|
+
if (!(segment in current)) {
|
|
85
|
+
current[segment] = {}
|
|
86
|
+
}
|
|
87
|
+
current = current[segment] as Record<string, unknown>
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { databases } from "@highstate/library"
|
|
2
|
+
import { forUnit, toPromise } from "@highstate/pulumi"
|
|
3
|
+
import { l4EndpointToString, parseEndpoints } from "../../../shared"
|
|
4
|
+
|
|
5
|
+
const { args, inputs, outputs } = forUnit(databases.etcdPatch)
|
|
6
|
+
|
|
7
|
+
const resolvedInputEndpoints = await toPromise(inputs.endpoints ?? [])
|
|
8
|
+
|
|
9
|
+
const shouldOverrideEndpoints =
|
|
10
|
+
args.endpoints.length > 0 || resolvedInputEndpoints.some(endpoint => endpoint != null)
|
|
11
|
+
const endpoints = shouldOverrideEndpoints
|
|
12
|
+
? await parseEndpoints(args.endpoints, inputs.endpoints, 4)
|
|
13
|
+
: await parseEndpoints([], inputs.endpoints, 4)
|
|
14
|
+
|
|
15
|
+
export default outputs({
|
|
16
|
+
etcd: {
|
|
17
|
+
endpoints,
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
$statusFields: {
|
|
21
|
+
endpoints: endpoints.map(l4EndpointToString),
|
|
22
|
+
},
|
|
23
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { databases } from "@highstate/library"
|
|
2
|
+
import { forUnit } from "@highstate/pulumi"
|
|
3
|
+
import { parseEndpoints } from "../../../shared"
|
|
4
|
+
|
|
5
|
+
const { args, inputs, outputs } = forUnit(databases.existingEtcd)
|
|
6
|
+
|
|
7
|
+
export default outputs({
|
|
8
|
+
etcd: {
|
|
9
|
+
endpoints: parseEndpoints(args.endpoints, inputs.endpoints, 4),
|
|
10
|
+
},
|
|
11
|
+
})
|
|
@@ -6,7 +6,7 @@ const { args, secrets, inputs, outputs } = forUnit(databases.existingMariadb)
|
|
|
6
6
|
|
|
7
7
|
export default outputs({
|
|
8
8
|
mariadb: {
|
|
9
|
-
endpoints: parseEndpoints(args.endpoints, inputs.endpoints),
|
|
9
|
+
endpoints: parseEndpoints(args.endpoints, inputs.endpoints, 4),
|
|
10
10
|
username: args.username,
|
|
11
11
|
password: secrets.password,
|
|
12
12
|
database: args.database,
|
|
@@ -6,7 +6,7 @@ const { args, secrets, inputs, outputs } = forUnit(databases.existingMongodb)
|
|
|
6
6
|
|
|
7
7
|
export default outputs({
|
|
8
8
|
mongodb: {
|
|
9
|
-
endpoints: parseEndpoints(args.endpoints, inputs.endpoints),
|
|
9
|
+
endpoints: parseEndpoints(args.endpoints, inputs.endpoints, 4),
|
|
10
10
|
username: args.username,
|
|
11
11
|
password: secrets.password,
|
|
12
12
|
database: args.database,
|
|
@@ -6,7 +6,7 @@ const { args, secrets, inputs, outputs } = forUnit(databases.existingPostgresql)
|
|
|
6
6
|
|
|
7
7
|
export default outputs({
|
|
8
8
|
postgresql: {
|
|
9
|
-
endpoints: parseEndpoints(args.endpoints, inputs.endpoints),
|
|
9
|
+
endpoints: parseEndpoints(args.endpoints, inputs.endpoints, 4),
|
|
10
10
|
username: args.username,
|
|
11
11
|
password: secrets.password,
|
|
12
12
|
database: args.database,
|
|
@@ -12,7 +12,7 @@ if (redisDatabase !== undefined && Number.isNaN(redisDatabase)) {
|
|
|
12
12
|
|
|
13
13
|
export default outputs({
|
|
14
14
|
redis: {
|
|
15
|
-
endpoints: parseEndpoints(args.endpoints, inputs.endpoints),
|
|
15
|
+
endpoints: parseEndpoints(args.endpoints, inputs.endpoints, 4),
|
|
16
16
|
database: redisDatabase ?? 0,
|
|
17
17
|
},
|
|
18
18
|
})
|
|
@@ -6,7 +6,7 @@ const { args, secrets, inputs, outputs } = forUnit(databases.existingS3)
|
|
|
6
6
|
|
|
7
7
|
export default outputs({
|
|
8
8
|
s3: {
|
|
9
|
-
endpoints: parseEndpoints(args.endpoints, inputs.endpoints),
|
|
9
|
+
endpoints: parseEndpoints(args.endpoints, inputs.endpoints, 4),
|
|
10
10
|
region: args.region,
|
|
11
11
|
accessKey: args.accessKey,
|
|
12
12
|
secretKey: secrets.secretKey,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { databases } from "@highstate/library"
|
|
2
|
+
import { forUnit, toPromise } from "@highstate/pulumi"
|
|
3
|
+
import { l4EndpointToString, parseEndpoints } from "../../../shared"
|
|
4
|
+
|
|
5
|
+
const { args, inputs, outputs } = forUnit(databases.mariadbPatch)
|
|
6
|
+
|
|
7
|
+
const mariadb = await toPromise(inputs.mariadb)
|
|
8
|
+
const resolvedInputEndpoints = await toPromise(inputs.endpoints ?? [])
|
|
9
|
+
|
|
10
|
+
const shouldOverrideEndpoints =
|
|
11
|
+
args.endpoints.length > 0 || resolvedInputEndpoints.some(endpoint => endpoint != null)
|
|
12
|
+
const endpoints = shouldOverrideEndpoints
|
|
13
|
+
? await parseEndpoints(args.endpoints, inputs.endpoints, 4)
|
|
14
|
+
: mariadb.endpoints
|
|
15
|
+
|
|
16
|
+
export default outputs({
|
|
17
|
+
mariadb: {
|
|
18
|
+
...mariadb,
|
|
19
|
+
endpoints,
|
|
20
|
+
username: args.username ?? mariadb.username,
|
|
21
|
+
database: args.database ?? mariadb.database,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
$statusFields: {
|
|
25
|
+
endpoints: endpoints.map(l4EndpointToString),
|
|
26
|
+
},
|
|
27
|
+
})
|