* add support for idmapped mounts to start-sdk

* misc fixes

* misc fixes

* add default to textarea

* fix iptables masquerade rule

* fix textarea types

* more fixes

* better logging for rsync

* fix tty size

* fix wg conf generation for android

* disable file mounts on dependencies

* mostly there, some styling issues (#3069)

* mostly there, some styling issues

* fix: address comments (#3070)

* fix: address comments

* fix: fix

* show SSL for any address with secure protocol and ssl added

* better sorting and messaging

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>

* fixes for nextcloud

* allow sidebar navigation during service state traansitions

* wip: x-forwarded headers

* implement x-forwarded-for proxy

* lowercase domain names and fix warning popover bug

* fix http2 websockets

* fix websocket retry behavior

* add arch filters to s9pk pack

* use docker for start-cli install

* add version range to package signer on registry

* fix rcs < 0

* fix user information parsing

* refactor service interface getters

* disable idmaps

* build fixes

* update docker login action

* streamline build

* add start-cli workflow

* rename

* riscv64gc

* fix ui packing

* no default features on cli

* make cli depend on GIT_HASH

* more build fixes

* more build fixes

* interpolate arch within dockerfile

* fix tests

* add launch ui to service page plus other small improvements (#3075)

* add launch ui to service page plus other small improvements

* revert translation disable

* add spinner to service list if service is health and loading

* chore: some visual tune up

* chore: update Taiga UI

---------

Co-authored-by: waterplea <alexander@inkin.ru>

* fix backups

* feat: use arm hosted runners and don't fail when apt package does not exist (#3076)

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Shadowy Super Coder <musashidisciple@proton.me>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Remco Ros <remcoros@live.nl>
This commit is contained in:
Aiden McClelland
2025-12-15 13:30:50 -07:00
committed by GitHub
parent b945243d1a
commit 0430e0f930
148 changed files with 2572 additions and 1761 deletions

View File

@@ -56,7 +56,7 @@ check:
npm run check
fmt: package/node_modules base/node_modules
npx prettier . "**/*.ts" --write
npx --prefix base prettier "**/*.ts" --write
package/package-lock.json: package/package.json
cd package && npm i

View File

@@ -298,6 +298,20 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
required: Required
minLength?: number | null
maxLength?: number | null
/**
* @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails.
* @default []
* @example
* ```
[
{
regex: "[a-z]",
description: "May only contain lower case letters from the English alphabet."
}
]
* ```
*/
patterns?: Pattern[]
/** Defaults to 3 */
minRows?: number
/** Maximum number of rows before scroll appears. Defaults to 6 */
@@ -316,6 +330,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
warning: null,
minLength: null,
maxLength: null,
patterns: [],
minRows: 3,
maxRows: 6,
placeholder: null,
@@ -336,6 +351,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
required: Required
minLength?: number | null
maxLength?: number | null
patterns?: Pattern[]
minRows?: number
maxRows?: number
placeholder?: string | null
@@ -351,6 +367,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
warning: null,
minLength: null,
maxLength: null,
patterns: [],
minRows: 3,
maxRows: 6,
placeholder: null,

View File

@@ -58,12 +58,14 @@ export type ValueSpecTextarea = {
warning: string | null
type: "textarea"
patterns: Pattern[]
placeholder: string | null
minLength: number | null
maxLength: number | null
minRows: number
maxRows: number
required: boolean
default: string | null
disabled: false | string
immutable: boolean
}

View File

@@ -45,10 +45,9 @@ export interface ActionInfo<
readonly _INPUT: Type
}
export class Action<
Id extends T.ActionId,
Type extends Record<string, any>,
> implements ActionInfo<Id, Type> {
export class Action<Id extends T.ActionId, Type extends Record<string, any>>
implements ActionInfo<Id, Type>
{
readonly _INPUT: Type = null as any as Type
private prevInputSpec: Record<
string,
@@ -149,7 +148,8 @@ export class Action<
export class Actions<
AllActions extends Record<T.ActionId, Action<T.ActionId, any>>,
> implements InitScript {
> implements InitScript
{
private constructor(private readonly actions: AllActions) {}
static of(): Actions<{}> {
return new Actions({})

View File

@@ -137,7 +137,7 @@ export class MultiHost {
const sslProto = this.getSslProto(options)
const addSsl = sslProto
? {
// addXForwardedHeaders: null,
addXForwardedHeaders: false,
preferredExternalPort: knownProtocols[sslProto].defaultPort,
scheme: sslProto,
alpn: "alpn" in protoInfo ? protoInfo.alpn : null,
@@ -145,7 +145,7 @@ export class MultiHost {
}
: options.addSsl
? {
// addXForwardedHeaders: null,
addXForwardedHeaders: false,
preferredExternalPort: 443,
scheme: sslProto,
alpn: null,

View File

@@ -3,5 +3,6 @@ import type { AlpnInfo } from "./AlpnInfo"
export type AddSslOptions = {
preferredExternalPort: number
addXForwardedHeaders: boolean
alpn: AlpnInfo | null
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type IdMap = { fromId: number; toId: number; range: number }

View File

@@ -1,5 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FileType } from "./FileType"
import type { IdMap } from "./IdMap"
import type { PackageId } from "./PackageId"
import type { VolumeId } from "./VolumeId"
@@ -8,5 +8,5 @@ export type MountTarget = {
volumeId: VolumeId
subpath: string | null
readonly: boolean
filetype: FileType
idmap: Array<IdMap>
}

View File

@@ -105,6 +105,7 @@ export { HostId } from "./HostId"
export { HostnameInfo } from "./HostnameInfo"
export { Hosts } from "./Hosts"
export { Host } from "./Host"
export { IdMap } from "./IdMap"
export { ImageConfig } from "./ImageConfig"
export { ImageId } from "./ImageId"
export { ImageMetadata } from "./ImageMetadata"

View File

@@ -109,11 +109,9 @@ export class DropPromise<T> implements Promise<T> {
}
}
export class DropGenerator<
T = unknown,
TReturn = any,
TNext = unknown,
> implements AsyncGenerator<T, TReturn, TNext> {
export class DropGenerator<T = unknown, TReturn = any, TNext = unknown>
implements AsyncGenerator<T, TReturn, TNext>
{
private static dropFns: { [id: number]: () => void } = {}
private static registry = new FinalizationRegistry((id: number) => {
const drop = DropGenerator.dropFns[id]

View File

@@ -19,7 +19,7 @@ export const getHostname = (url: string): Hostname | null => {
type FilterKinds =
| "onion"
| "local"
| "mdns"
| "domain"
| "ip"
| "ipv4"
@@ -42,10 +42,10 @@ type VisibilityFilter<V extends "public" | "private"> = V extends "public"
: never
type KindFilter<K extends FilterKinds> = K extends "onion"
? (HostnameInfo & { kind: "onion" }) | KindFilter<Exclude<K, "onion">>
: K extends "local"
: K extends "mdns"
?
| (HostnameInfo & { kind: "ip"; hostname: { kind: "local" } })
| KindFilter<Exclude<K, "local">>
| KindFilter<Exclude<K, "mdns">>
: K extends "domain"
?
| (HostnameInfo & { kind: "ip"; hostname: { kind: "domain" } })
@@ -80,11 +80,17 @@ type FilterReturnTy<F extends Filter> = F extends {
: Exclude<HostnameInfo, FilterReturnTy<E>>
: HostnameInfo
const defaultFilter = {
const nonLocalFilter = {
exclude: {
kind: ["localhost", "link-local"] as ("localhost" | "link-local")[],
},
}
} as const
const publicFilter = {
visibility: "public",
} as const
const onionFilter = {
kind: "onion",
} as const
type Formats = "hostname-info" | "urlstring" | "url"
type FormatReturnTy<
@@ -98,7 +104,7 @@ type FormatReturnTy<
? UrlString | FormatReturnTy<F, Exclude<Format, "urlstring">>
: never
export type Filled = {
export type Filled<F extends Filter = {}> = {
hostnames: HostnameInfo[]
toUrls: (h: HostnameInfo) => {
@@ -106,30 +112,17 @@ export type Filled = {
sslUrl: UrlString | null
}
filter: <
F extends Filter = typeof defaultFilter,
Format extends Formats = "urlstring",
>(
filter?: F,
format: <Format extends Formats = "urlstring">(
format?: Format,
) => FormatReturnTy<F, Format>[]
) => FormatReturnTy<{}, Format>[]
publicHostnames: HostnameInfo[]
onionHostnames: HostnameInfo[]
localHostnames: HostnameInfo[]
ipHostnames: HostnameInfo[]
ipv4Hostnames: HostnameInfo[]
ipv6Hostnames: HostnameInfo[]
nonIpHostnames: HostnameInfo[]
filter: <NewFilter extends Filter>(
filter: NewFilter,
) => Filled<NewFilter & Filter>
urls: UrlString[]
publicUrls: UrlString[]
onionUrls: UrlString[]
localUrls: UrlString[]
ipUrls: UrlString[]
ipv4Urls: UrlString[]
ipv6Urls: UrlString[]
nonIpUrls: UrlString[]
nonLocal: Filled<typeof nonLocalFilter & Filter>
public: Filled<typeof publicFilter & Filter>
onion: Filled<typeof onionFilter & Filter>
}
export type FilledAddressInfo = AddressInfo & Filled
export type ServiceInterfaceFilled = {
@@ -225,7 +218,7 @@ function filterRec(
(h) =>
invert !==
((kind.has("onion") && h.kind === "onion") ||
(kind.has("local") &&
(kind.has("mdns") &&
h.kind === "ip" &&
h.hostname.kind === "local") ||
(kind.has("domain") &&
@@ -258,86 +251,45 @@ export const filledAddress = (
}
const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? []
return {
...addressInfo,
hostnames,
toUrls,
filter: <
F extends Filter = typeof defaultFilter,
Format extends Formats = "urlstring",
>(
filter?: F,
format?: Format,
) => {
const filtered = filterRec(hostnames, filter ?? defaultFilter, false)
let res: FormatReturnTy<F, Format>[] = filtered as any
if (format === "hostname-info") return res
const urls = filtered.flatMap(toUrlArray)
if (format === "url") res = urls.map((u) => new URL(u)) as any
else res = urls as any
return res
},
get publicHostnames() {
return hostnames.filter((h) => h.kind === "onion" || h.public)
},
get onionHostnames() {
return hostnames.filter((h) => h.kind === "onion")
},
get localHostnames() {
return hostnames.filter(
(h) => h.kind === "ip" && h.hostname.kind === "local",
)
},
get ipHostnames() {
return hostnames.filter(
(h) =>
h.kind === "ip" &&
(h.hostname.kind === "ipv4" || h.hostname.kind === "ipv6"),
)
},
get ipv4Hostnames() {
return hostnames.filter(
(h) => h.kind === "ip" && h.hostname.kind === "ipv4",
)
},
get ipv6Hostnames() {
return hostnames.filter(
(h) => h.kind === "ip" && h.hostname.kind === "ipv6",
)
},
get nonIpHostnames() {
return hostnames.filter(
(h) =>
h.kind === "ip" &&
h.hostname.kind !== "ipv4" &&
h.hostname.kind !== "ipv6",
)
},
get urls() {
return this.hostnames.flatMap(toUrlArray)
},
get publicUrls() {
return this.publicHostnames.flatMap(toUrlArray)
},
get onionUrls() {
return this.onionHostnames.flatMap(toUrlArray)
},
get localUrls() {
return this.localHostnames.flatMap(toUrlArray)
},
get ipUrls() {
return this.ipHostnames.flatMap(toUrlArray)
},
get ipv4Urls() {
return this.ipv4Hostnames.flatMap(toUrlArray)
},
get ipv6Urls() {
return this.ipv6Hostnames.flatMap(toUrlArray)
},
get nonIpUrls() {
return this.nonIpHostnames.flatMap(toUrlArray)
},
function filledAddressFromHostnames<F extends Filter>(
hostnames: HostnameInfo[],
): Filled<F> & AddressInfo {
return {
...addressInfo,
hostnames,
toUrls,
format: <Format extends Formats = "urlstring">(format?: Format) => {
let res: FormatReturnTy<{}, Format>[] = hostnames as any
if (format === "hostname-info") return res
const urls = hostnames.flatMap(toUrlArray)
if (format === "url") res = urls.map((u) => new URL(u)) as any
else res = urls as any
return res
},
filter: <NewFilter extends Filter>(filter: NewFilter) => {
return filledAddressFromHostnames<NewFilter & F>(
filterRec(hostnames, filter, false),
)
},
get nonLocal(): Filled<typeof nonLocalFilter & F> {
return filledAddressFromHostnames<typeof nonLocalFilter & F>(
filterRec(hostnames, nonLocalFilter, false),
)
},
get public(): Filled<typeof publicFilter & F> {
return filledAddressFromHostnames<typeof publicFilter & F>(
filterRec(hostnames, publicFilter, false),
)
},
get onion(): Filled<typeof onionFilter & F> {
return filledAddressFromHostnames<typeof onionFilter & F>(
filterRec(hostnames, onionFilter, false),
)
},
}
}
return filledAddressFromHostnames<{}>(hostnames)
}
const makeInterfaceFilled = async ({

View File

@@ -18,6 +18,9 @@ export class ComposableRegex {
}
}
export const escapeLiteral = (str: string) =>
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
// https://ihateregex.io/expr/ipv6/
export const ipv6 = new ComposableRegex(
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
@@ -69,3 +72,13 @@ export const emailWithName = new ComposableRegex(
export const base64 = new ComposableRegex(
/(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))/,
)
//https://rgxdb.com/r/1NUN74O6
export const base64Whitespace = new ComposableRegex(
/(?:([a-zA-Z0-9+\/]\s*){4})*(?:|(?:([a-zA-Z0-9+\/]\s*){3}=)|(?:([a-zA-Z0-9+\/]\s*){2}==)|(?:([a-zA-Z0-9+\/]\s*){1}===))/,
)
export const pem = (label: string) =>
new ComposableRegex(
`-----BEGIN ${escapeLiteral(label)}-----\r?\n[a-zA-Z0-9+/\n\r=]*?\r?\n-----END ${escapeLiteral(label)}-----`,
)

View File

@@ -74,6 +74,7 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0",
@@ -1614,6 +1615,7 @@
"integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.20.0"
}
@@ -1915,6 +1917,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
@@ -2984,6 +2987,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -4046,6 +4050,7 @@
"integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"commander": "^10.0.0",
"source-map-generator": "0.8.0"
@@ -4644,6 +4649,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -4764,6 +4770,7 @@
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -163,8 +163,8 @@ export class StartSdk<Manifest extends T.SDKManifest> {
effects.action.clearTasks({ only: replayIds }),
},
checkDependencies: checkDependencies as <
DependencyId extends keyof Manifest["dependencies"] & PackageId =
keyof Manifest["dependencies"] & PackageId,
DependencyId extends keyof Manifest["dependencies"] &
PackageId = keyof Manifest["dependencies"] & PackageId,
>(
effects: Effects,
packageIds?: DependencyId[],

View File

@@ -208,13 +208,20 @@ async function runRsync(rsyncOptions: {
const lines = String(data).replace("\r", "\n").split("\n")
for (const line of lines) {
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
if (!parsed) continue
if (!parsed) {
console.log(parsed)
continue
}
percentage = Number.parseFloat(parsed)
}
})
spawned.stderr.on("data", (data: unknown) => {
console.error(`Backups.runAsync`, asError(data))
let stderr = ""
spawned.stderr.on("data", (data: string | Buffer) => {
const errString = data.toString("utf-8")
stderr += errString
console.error(`Backups.runAsync`, asError(errString))
})
const id = async () => {
@@ -229,7 +236,7 @@ async function runRsync(rsyncOptions: {
if (code === 0) {
resolve(null)
} else {
reject(new Error(`rsync exited with code ${code}`))
reject(new Error(`rsync exited with code ${code}\n${stderr}`))
}
})
})

View File

@@ -51,7 +51,9 @@ export class Daemon<
)
const res = new Daemon(subc, startCommand)
effects.onLeaveContext(() => {
res.stop().catch((e) => console.error(asError(e)))
res
.term({ destroySubcontainer: true })
.catch((e) => console.error(asError(e)))
})
return res
}
@@ -72,7 +74,7 @@ export class Daemon<
this.commandController = await this.startCommand()
if (!this.shouldBeRunning) {
// handles race condition if stopped while starting
await this.stop()
await this.term()
break
}
const success = await this.commandController.wait().then(
@@ -107,12 +109,7 @@ export class Daemon<
async term(termOptions?: {
signal?: NodeJS.Signals | undefined
timeout?: number | undefined
}) {
return this.stop(termOptions)
}
async stop(termOptions?: {
signal?: NodeJS.Signals | undefined
timeout?: number | undefined
destroySubcontainer?: boolean
}) {
this.shouldBeRunning = false
this.exitedSuccess = false
@@ -122,7 +119,9 @@ export class Daemon<
.catch((e) => console.error(asError(e)))
this.commandController = null
this.onExitFns = []
await this.subcontainer?.destroy()
if (termOptions?.destroySubcontainer) {
await this.subcontainer?.destroy()
}
}
}
subcontainerRc(): SubContainerRc<Manifest> | null {
@@ -132,6 +131,6 @@ export class Daemon<
this.onExitFns.push(fn)
}
onDrop(): void {
this.stop().catch((e) => console.error(asError(e)))
this.term().catch((e) => console.error(asError(e)))
}
}

View File

@@ -55,7 +55,7 @@ export type ExecCommandOptions = {
runAsInit?: boolean
env?:
| {
[variable: string]: string
[variable in string]?: string
}
| undefined
cwd?: string | undefined
@@ -412,16 +412,12 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
}
async term() {
try {
for (let result of await Promise.allSettled(
this.healthDaemons.map((x) => x.term()),
)) {
if (result.status === "rejected") {
console.error(result.reason)
}
for (let result of await Promise.allSettled(
this.healthDaemons.map((x) => x.term({ destroySubcontainer: true })),
)) {
if (result.status === "rejected") {
console.error(result.reason)
}
} finally {
this.effects.setMainStatus({ status: "stopped" })
}
}

View File

@@ -54,6 +54,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
async term(termOptions?: {
signal?: NodeJS.Signals | undefined
timeout?: number | undefined
destroySubcontainer?: boolean
}) {
this.healthWatchers = []
this.running = false
@@ -87,7 +88,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
this.started = performance.now()
} else {
console.debug(`Stopping ${this.id}...`)
;(await this.daemon)?.stop()
;(await this.daemon)?.term()
this.turnOffHealthCheck()
this.setHealth({ result: "starting", message: null })
@@ -143,7 +144,6 @@ export class HealthDaemon<Manifest extends SDKManifest> {
const response: HealthCheckResult = await Promise.resolve(
this.ready.fn(),
).catch((err) => {
console.error(asError(err))
return {
result: "failure",
message: "message" in err ? err.message : String(err),
@@ -188,6 +188,9 @@ export class HealthDaemon<Manifest extends SDKManifest> {
performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000)
)
result = "starting"
if (result === "failure") {
console.error(`Health Check ${this.id} failed:`, health.message)
}
await this.effects.setHealth({
...health,
id: this.id,

View File

@@ -1,5 +1,5 @@
import * as T from "../../../base/lib/types"
import { MountOptions } from "../util/SubContainer"
import { IdMap, MountOptions } from "../util/SubContainer"
type MountArray = { mountpoint: string; options: MountOptions }[]
@@ -14,6 +14,23 @@ type SharedOptions = {
* defaults to "directory"
* */
type?: "file" | "directory" | "infer"
// /**
// * Whether to map uids/gids for the mount
// *
// * https://www.kernel.org/doc/html/latest/filesystems/idmappings.html
// */
// idmap?: {
// /** The (starting) id of the data on the filesystem (u) */
// fromId: number
// /** The (starting) id of the data in the mount point (k) */
// toId: number
// /**
// * Optional: the number of incremental ids to map (r)
// *
// * defaults to 1
// * */
// range?: number
// }[]
}
type VolumeOpts<Manifest extends T.SDKManifest> = {
@@ -114,6 +131,7 @@ export class Mounts<
subpath: v.subpath,
readonly: v.readonly,
filetype: v.type ?? "directory",
idmap: [],
},
})),
)
@@ -124,6 +142,7 @@ export class Mounts<
type: "assets",
subpath: a.subpath,
filetype: a.type ?? "directory",
idmap: [],
},
})),
)
@@ -137,6 +156,7 @@ export class Mounts<
subpath: d.subpath,
readonly: d.readonly,
filetype: d.type ?? "directory",
idmap: [],
},
})),
)

View File

@@ -3,7 +3,7 @@ import { Daemons } from "./Daemons"
import "../../../base/lib/interfaces/ServiceInterfaceBuilder"
import "../../../base/lib/interfaces/Origin"
export const DEFAULT_SIGTERM_TIMEOUT = 30_000
export const DEFAULT_SIGTERM_TIMEOUT = 60_000
/**
* Used to ensure that the main function is running with the valid proofs.
* We first do the folowing order of things

View File

@@ -53,10 +53,19 @@ async function bind(
from: string,
to: string,
type: "file" | "directory" | "infer",
idmap: IdMap[],
) {
await prepBind(from, to, type)
await execFile("mount", ["--bind", from, to])
const args = ["--bind"]
if (idmap.length) {
args.push(
`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(" ")}`,
)
}
await execFile("mount", [...args, from, to])
}
export interface SubContainer<
@@ -137,9 +146,9 @@ export interface SubContainer<
* Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts.
*/
export class SubContainerOwned<
Manifest extends T.SDKManifest,
Effects extends T.Effects = T.Effects,
>
Manifest extends T.SDKManifest,
Effects extends T.Effects = T.Effects,
>
extends Drop
implements SubContainer<Manifest, Effects>
{
@@ -306,7 +315,7 @@ export class SubContainerOwned<
: "/"
const from = `/media/startos/volumes/${options.volumeId}${subpath}`
await bind(from, path, mount.options.filetype)
await bind(from, path, options.filetype, options.idmap)
} else if (options.type === "assets") {
const subpath = options.subpath
? options.subpath.startsWith("/")
@@ -315,9 +324,9 @@ export class SubContainerOwned<
: "/"
const from = `/media/startos/assets/${subpath}`
await bind(from, path, mount.options.filetype)
await bind(from, path, options.filetype, options.idmap)
} else if (options.type === "pointer") {
await prepBind(null, path, options.filetype)
await prepBind(null, path, "directory")
await this.effects.mount({ location: path, target: options })
} else if (options.type === "backup") {
const subpath = options.subpath
@@ -327,7 +336,7 @@ export class SubContainerOwned<
: "/"
const from = `/media/startos/backup${subpath}`
await bind(from, path, mount.options.filetype)
await bind(from, path, options.filetype, options.idmap)
} else {
throw new Error(`unknown type ${(options as any).type}`)
}
@@ -536,7 +545,9 @@ export class SubContainerOwned<
delete options.cwd
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
for (let [k, v] of Object.entries(options.env).filter(
([_, v]) => v != undefined,
)) {
extra.push(`--env=${k}=${v}`)
}
}
@@ -585,7 +596,9 @@ export class SubContainerOwned<
delete options.cwd
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
for (let [k, v] of Object.entries(options.env).filter(
([_, v]) => v != undefined,
)) {
extra.push(`--env=${k}=${v}`)
}
}
@@ -615,9 +628,9 @@ export class SubContainerOwned<
}
export class SubContainerRc<
Manifest extends T.SDKManifest,
Effects extends T.Effects = T.Effects,
>
Manifest extends T.SDKManifest,
Effects extends T.Effects = T.Effects,
>
extends Drop
implements SubContainer<Manifest, Effects>
{
@@ -718,7 +731,9 @@ export class SubContainerRc<
if (rcs < 0) console.error(new Error("UNREACHABLE: rcs < 0").stack)
}
}
await this.destroying
if (this.destroying) {
await this.destroying
}
this.destroyed = true
this.destroying = null
return null
@@ -798,7 +813,7 @@ export type CommandOptions = {
/**
* Environment variables to set for this command
*/
env?: { [variable: string]: string }
env?: { [variable in string]?: string }
/**
* the working directory to run this command in
*/
@@ -813,6 +828,8 @@ export type StdioOptions = {
stdio?: cp.IOType
}
export type IdMap = { fromId: number; toId: number; range: number }
export type MountOptions =
| MountOptionsVolume
| MountOptionsAssets
@@ -825,12 +842,14 @@ export type MountOptionsVolume = {
subpath: string | null
readonly: boolean
filetype: "file" | "directory" | "infer"
idmap: IdMap[]
}
export type MountOptionsAssets = {
type: "assets"
subpath: string | null
filetype: "file" | "directory" | "infer"
idmap: { fromId: number; toId: number; range: number }[]
}
export type MountOptionsPointer = {
@@ -839,13 +858,14 @@ export type MountOptionsPointer = {
volumeId: string
subpath: string | null
readonly: boolean
filetype: "file" | "directory" | "infer"
idmap: { fromId: number; toId: number; range: number }[]
}
export type MountOptionsBackup = {
type: "backup"
subpath: string | null
filetype: "file" | "directory" | "infer"
idmap: { fromId: number; toId: number; range: number }[]
}
function wait(time: number) {
return new Promise((resolve) => setTimeout(resolve, time))