@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.
Files changed (91) hide show
  1. package/dist/{chunk-WFWXDYUX.js → chunk-X5BK6JSN.js} +877 -194
  2. package/dist/chunk-X5BK6JSN.js.map +1 -0
  3. package/dist/highstate.manifest.json +12 -2
  4. package/dist/index.js +1 -1
  5. package/dist/units/databases/etcd-patch/index.js +20 -0
  6. package/dist/units/databases/etcd-patch/index.js.map +1 -0
  7. package/dist/units/databases/existing-etcd/index.js +14 -0
  8. package/dist/units/databases/existing-etcd/index.js.map +1 -0
  9. package/dist/units/databases/existing-mariadb/index.js +2 -2
  10. package/dist/units/databases/existing-mariadb/index.js.map +1 -1
  11. package/dist/units/databases/existing-mongodb/index.js +2 -2
  12. package/dist/units/databases/existing-mongodb/index.js.map +1 -1
  13. package/dist/units/databases/existing-postgresql/index.js +2 -2
  14. package/dist/units/databases/existing-postgresql/index.js.map +1 -1
  15. package/dist/units/databases/existing-redis/index.js +2 -2
  16. package/dist/units/databases/existing-redis/index.js.map +1 -1
  17. package/dist/units/databases/existing-s3/index.js +18 -0
  18. package/dist/units/databases/existing-s3/index.js.map +1 -0
  19. package/dist/units/databases/mariadb-patch/index.js +24 -0
  20. package/dist/units/databases/mariadb-patch/index.js.map +1 -0
  21. package/dist/units/databases/mongodb-patch/index.js +24 -0
  22. package/dist/units/databases/mongodb-patch/index.js.map +1 -0
  23. package/dist/units/databases/postgresql-patch/index.js +24 -0
  24. package/dist/units/databases/postgresql-patch/index.js.map +1 -0
  25. package/dist/units/databases/redis-patch/index.js +27 -0
  26. package/dist/units/databases/redis-patch/index.js.map +1 -0
  27. package/dist/units/databases/s3-patch/index.js +25 -0
  28. package/dist/units/databases/s3-patch/index.js.map +1 -0
  29. package/dist/units/dns/record-set/index.js +14 -20
  30. package/dist/units/dns/record-set/index.js.map +1 -1
  31. package/dist/units/existing-server/index.js +3 -4
  32. package/dist/units/existing-server/index.js.map +1 -1
  33. package/dist/units/network/address-space/index.js +20 -0
  34. package/dist/units/network/address-space/index.js.map +1 -0
  35. package/dist/units/network/endpoint-filter/index.js +15 -0
  36. package/dist/units/network/endpoint-filter/index.js.map +1 -0
  37. package/dist/units/network/l3-endpoint/index.js +2 -2
  38. package/dist/units/network/l3-endpoint/index.js.map +1 -1
  39. package/dist/units/network/l4-endpoint/index.js +2 -2
  40. package/dist/units/network/l4-endpoint/index.js.map +1 -1
  41. package/dist/units/network/l7-endpoint/index.js +12 -0
  42. package/dist/units/network/l7-endpoint/index.js.map +1 -0
  43. package/dist/units/script/index.js +1 -1
  44. package/dist/units/server-patch/index.js +9 -12
  45. package/dist/units/server-patch/index.js.map +1 -1
  46. package/dist/units/ssh/key-pair/index.js +1 -1
  47. package/package.json +64 -10
  48. package/src/shared/command.ts +1 -1
  49. package/src/shared/dns.ts +11 -93
  50. package/src/shared/files.ts +3 -3
  51. package/src/shared/impl-ref.ts +4 -0
  52. package/src/shared/index.ts +2 -0
  53. package/src/shared/network/address-space.spec.ts +114 -0
  54. package/src/shared/network/address-space.ts +364 -0
  55. package/src/shared/network/address.spec.ts +109 -0
  56. package/src/shared/network/address.ts +119 -0
  57. package/src/shared/network/endpoints.spec.ts +249 -0
  58. package/src/shared/network/endpoints.ts +608 -0
  59. package/src/shared/network/index.ts +4 -0
  60. package/src/shared/network/ip.ts +236 -0
  61. package/src/shared/network/subnet.spec.ts +62 -0
  62. package/src/shared/network/subnet.ts +137 -0
  63. package/src/shared/ssh.ts +1 -1
  64. package/src/shared/tls.ts +21 -5
  65. package/src/shared/utils.ts +93 -0
  66. package/src/units/databases/etcd-patch/index.ts +23 -0
  67. package/src/units/databases/existing-etcd/index.ts +11 -0
  68. package/src/units/databases/existing-mariadb/index.ts +1 -1
  69. package/src/units/databases/existing-mongodb/index.ts +1 -1
  70. package/src/units/databases/existing-postgresql/index.ts +1 -1
  71. package/src/units/databases/existing-redis/index.ts +1 -1
  72. package/src/units/databases/existing-s3/index.ts +1 -1
  73. package/src/units/databases/mariadb-patch/index.ts +27 -0
  74. package/src/units/databases/mongodb-patch/index.ts +27 -0
  75. package/src/units/databases/postgresql-patch/index.ts +27 -0
  76. package/src/units/databases/redis-patch/index.ts +32 -0
  77. package/src/units/databases/s3-patch/index.ts +28 -0
  78. package/src/units/dns/record-set/index.ts +15 -20
  79. package/src/units/existing-server/index.ts +3 -4
  80. package/src/units/network/address-space/index.ts +20 -0
  81. package/src/units/network/endpoint-filter/index.ts +5 -5
  82. package/src/units/network/l3-endpoint/index.ts +2 -2
  83. package/src/units/network/l4-endpoint/index.ts +2 -2
  84. package/src/units/network/l7-endpoint/index.ts +2 -2
  85. package/src/units/remote-file/index.ts +12 -5
  86. package/src/units/server-patch/index.ts +10 -13
  87. package/dist/chunk-WFWXDYUX.js.map +0 -1
  88. package/dist/units/server-dns/index.js +0 -26
  89. package/dist/units/server-dns/index.js.map +0 -1
  90. package/src/shared/network.ts +0 -413
  91. 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(issuer.domain)) {
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(issuer.domain))) {
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
- throw new Error(
90
- `No TLS issuer matched the common name "${commonName}" and DNS names "${dnsNames?.join(", ") ?? ""}"`,
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
+ })