Bugfix/ssl proxy to ssl (#2956)

* fix registry rm command

* fix bind with addSsl on ssl proto

* fix bind with addSsl on ssl proto

* Add pre-release version migrations

* fix os build

* add mime to package deps

* update lockfile

* more ssl fixes

* add waitFor

* improve restart lockup

* beta.26

* fix dependency health check logic

* handle missing health check

* fix port forwards

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Dominion5254
2025-06-04 19:41:21 -06:00
committed by GitHub
parent 02413a4fac
commit ab6ca8e16a
40 changed files with 1240 additions and 816 deletions

View File

@@ -81,9 +81,13 @@ export async function checkDependencies<
) {
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
}
const errors = Object.entries(dep.result.healthChecks)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res.result !== "success")
const errors =
dep.requirement.kind === "running"
? dep.requirement.healthChecks
.map((id) => [id, dep.result.healthChecks[id] ?? null] as const)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res?.result !== "success")
: []
return errors.length === 0
}
const pkgSatisfied = (packageId: DependencyId) =>
@@ -153,15 +157,20 @@ export async function checkDependencies<
) {
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
}
const errors = Object.entries(dep.result.healthChecks)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res.result !== "success")
const errors =
dep.requirement.kind === "running"
? dep.requirement.healthChecks
.map((id) => [id, dep.result.healthChecks[id] ?? null] as const)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res?.result !== "success")
: []
if (errors.length) {
throw new Error(
errors
.map(
([_, e]) =>
`Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`,
.map(([id, e]) =>
e
? `Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`
: `Health Check ${id} of ${dep.result.title} does not exist`,
)
.join("; "),
)

View File

@@ -134,19 +134,26 @@ export class MultiHost {
const preferredExternalPort =
options.preferredExternalPort ||
knownProtocols[options.protocol].defaultPort
const sslProto = this.getSslProto(options, protoInfo)
const addSsl =
sslProto && "alpn" in protoInfo
const sslProto = this.getSslProto(options)
const addSsl = sslProto
? {
// addXForwardedHeaders: null,
preferredExternalPort: knownProtocols[sslProto].defaultPort,
scheme: sslProto,
alpn: "alpn" in protoInfo ? protoInfo.alpn : null,
...("addSsl" in options ? options.addSsl : null),
}
: options.addSsl
? {
// addXForwardedHeaders: null,
preferredExternalPort: knownProtocols[sslProto].defaultPort,
preferredExternalPort: 443,
scheme: sslProto,
alpn: protoInfo.alpn,
alpn: null,
...("addSsl" in options ? options.addSsl : null),
}
: null
const secure: Security | null = !protoInfo.secure ? null : { ssl: false }
const secure: Security | null = protoInfo.secure ?? null
await this.options.effects.bind({
id: this.options.id,
@@ -159,12 +166,12 @@ export class MultiHost {
return new Origin(this, internalPort, options.protocol, sslProto)
}
private getSslProto(
options: BindOptionsByKnownProtocol,
protoInfo: KnownProtocols[keyof KnownProtocols],
) {
private getSslProto(options: BindOptionsByKnownProtocol) {
const proto = options.protocol
const protoInfo = knownProtocols[proto]
if (inObject("noAddSsl", options) && options.noAddSsl) return null
if ("withSsl" in protoInfo && protoInfo.withSsl) return protoInfo.withSsl
if (protoInfo.secure?.ssl) return proto
return null
}
}

View File

@@ -71,4 +71,30 @@ export class GetSystemSmtp {
),
)
}
/**
* Watches the system SMTP credentials. Returns when the predicate is true
*/
async waitFor(pred: (value: T.SmtpValue | null) => boolean) {
const resolveCell = { resolve: () => {} }
this.effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (this.effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
const res = await this.effects.getSystemSmtp({
callback: () => callback(),
})
if (pred(res)) {
resolveCell.resolve()
return res
}
await waitForNext
}
return null
}
}

View File

@@ -366,6 +366,36 @@ export class GetServiceInterface {
),
)
}
/**
* Watches the requested service interface. Returns when the predicate is true
*/
async waitFor(pred: (value: ServiceInterfaceFilled | null) => boolean) {
const { id, packageId } = this.opts
const resolveCell = { resolve: () => {} }
this.effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (this.effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
const res = await makeInterfaceFilled({
effects: this.effects,
id,
packageId,
callback,
})
if (pred(res)) {
resolveCell.resolve()
return res
}
await waitForNext
}
return null
}
}
export function getServiceInterface(
effects: Effects,

View File

@@ -130,6 +130,35 @@ export class GetServiceInterfaces {
),
)
}
/**
* Watches the service interfaces for the package. Returns when the predicate is true
*/
async waitFor(pred: (value: ServiceInterfaceFilled[] | null) => boolean) {
const { packageId } = this.opts
const resolveCell = { resolve: () => {} }
this.effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (this.effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
const res = await makeManyInterfaceFilled({
effects: this.effects,
packageId,
callback,
})
if (pred(res)) {
resolveCell.resolve()
return res
}
await waitForNext
}
return null
}
}
export function getServiceInterfaces(
effects: Effects,

View File

@@ -249,6 +249,26 @@ export class StartSdk<Manifest extends T.SDKManifest> {
),
)
},
waitFor: async (pred: (value: string | null) => boolean) => {
const resolveCell = { resolve: () => {} }
effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
const res = await effects.getContainerIp({ ...options, callback })
if (pred(res)) {
resolveCell.resolve()
return res
}
await waitForNext
}
return null
},
}
},

View File

@@ -189,6 +189,8 @@ async function runRsync(rsyncOptions: {
}> {
const { srcPath, dstPath, options } = rsyncOptions
await fs.mkdir(dstPath, { recursive: true })
const command = "rsync"
const args: string[] = []
if (options.delete) {

View File

@@ -82,4 +82,32 @@ export class GetSslCertificate {
),
)
}
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns when the predicate is true
*/
async waitFor(pred: (value: [string, string, string] | null) => boolean) {
const resolveCell = { resolve: () => {} }
this.effects.onLeaveContext(() => {
resolveCell.resolve()
})
while (this.effects.isInContext) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
resolveCell.resolve = resolve
})
const res = await this.effects.getSslCertificate({
hostnames: this.hostnames,
algorithm: this.algorithm,
callback: () => callback(),
})
if (pred(res)) {
resolveCell.resolve()
return res
}
await waitForNext
}
return null
}
}

View File

@@ -26,13 +26,17 @@ async function onCreated(path: string) {
await onCreated(parent)
const ctrl = new AbortController()
const watch = fs.watch(parent, { persistent: false, signal: ctrl.signal })
if (await exists(path)) {
ctrl.abort()
return
}
if (
await fs.access(path).then(
() => true,
() => false,
)
) {
ctrl.abort("finished")
ctrl.abort()
return
}
for await (let event of watch) {
@@ -100,6 +104,10 @@ type ReadType<A> = {
effects: T.Effects,
callback: (value: A | null, error?: Error) => void | Promise<void>,
) => void
waitFor: (
effects: T.Effects,
pred: (value: A | null) => boolean,
) => Promise<A | null>
}
/**
@@ -228,7 +236,7 @@ export class FileHelper<A> {
const listen = Promise.resolve()
.then(async () => {
for await (const _ of watch) {
ctrl.abort("finished")
ctrl.abort()
return null
}
})
@@ -271,6 +279,40 @@ export class FileHelper<A> {
)
}
private async readWaitFor<B>(
effects: T.Effects,
pred: (value: B | null, error?: Error) => boolean,
map: (value: A) => B,
): Promise<B | null> {
while (effects.isInContext) {
if (await exists(this.path)) {
const ctrl = new AbortController()
const watch = fs.watch(this.path, {
persistent: false,
signal: ctrl.signal,
})
const newRes = await this.readOnce(map)
const listen = Promise.resolve()
.then(async () => {
for await (const _ of watch) {
ctrl.abort()
return null
}
})
.catch((e) => console.error(asError(e)))
if (pred(newRes)) {
ctrl.abort()
return newRes
}
await listen
} else {
if (pred(null)) return null
await onCreated(this.path).catch((e) => console.error(asError(e)))
}
}
return null
}
read(): ReadType<A>
read<B>(
map: (value: A) => B,
@@ -290,6 +332,8 @@ export class FileHelper<A> {
effects: T.Effects,
callback: (value: A | null, error?: Error) => void | Promise<void>,
) => this.readOnChange(effects, callback, map, eq),
waitFor: (effects: T.Effects, pred: (value: A | null) => boolean) =>
this.readWaitFor(effects, pred, map),
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.25",
"version": "0.4.0-beta.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.25",
"version": "0.4.0-beta.26",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",
@@ -16,7 +16,7 @@
"deep-equality-data-structures": "^2.0.0",
"ini": "^5.0.0",
"isomorphic-fetch": "^3.0.0",
"mime-types": "^3.0.1",
"mime": "^4.0.7",
"ts-matches": "^6.3.2",
"yaml": "^2.7.1"
},
@@ -3865,25 +3865,19 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"node_modules/mime": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz",
"integrity": "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==",
"funding": [
"https://github.com/sponsors/broofa"
],
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
"bin": {
"mime": "bin/cli.js"
},
"engines": {
"node": ">= 0.6"
"node": ">=16"
}
},
"node_modules/mimic-fn": {

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.25",
"version": "0.4.0-beta.26",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",
@@ -32,7 +32,7 @@
"homepage": "https://github.com/Start9Labs/start-sdk#readme",
"dependencies": {
"isomorphic-fetch": "^3.0.0",
"mime-types": "^3.0.1",
"mime": "^4.0.7",
"ts-matches": "^6.3.2",
"yaml": "^2.7.1",
"deep-equality-data-structures": "^2.0.0",