Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into rebase/feat/domains

This commit is contained in:
Matt Hill
2024-03-27 10:35:42 -06:00
77 changed files with 297 additions and 856 deletions

View File

@@ -147,11 +147,6 @@ export class HostSystemStartOs implements Effects {
T.Effects["exposeForDependents"] T.Effects["exposeForDependents"]
> >
} }
exposeUi(...[options]: Parameters<T.Effects["exposeUi"]>) {
return this.rpcRound("exposeUi", options) as ReturnType<
T.Effects["exposeUi"]
>
}
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) { getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
return this.rpcRound("getConfigured", null) as ReturnType< return this.rpcRound("getConfigured", null) as ReturnType<
T.Effects["getConfigured"] T.Effects["getConfigured"]

View File

@@ -25,15 +25,12 @@ export class MainLoop {
wait: Promise<unknown> wait: Promise<unknown>
}> }>
| undefined | undefined
private propertiesEvent: NodeJS.Timeout | undefined
constructor( constructor(
readonly system: SystemForEmbassy, readonly system: SystemForEmbassy,
readonly effects: HostSystemStartOs, readonly effects: HostSystemStartOs,
readonly runProperties: () => Promise<void>,
) { ) {
this.healthLoops = this.constructHealthLoops() this.healthLoops = this.constructHealthLoops()
this.mainEvent = this.constructMainEvent() this.mainEvent = this.constructMainEvent()
this.propertiesEvent = this.constructPropertiesEvent()
} }
private async constructMainEvent() { private async constructMainEvent() {
@@ -85,23 +82,14 @@ export class MainLoop {
} }
public async clean(options?: { timeout?: number }) { public async clean(options?: { timeout?: number }) {
const { mainEvent, healthLoops, propertiesEvent } = this const { mainEvent, healthLoops } = this
const main = await mainEvent const main = await mainEvent
delete this.mainEvent delete this.mainEvent
delete this.healthLoops delete this.healthLoops
delete this.propertiesEvent
if (mainEvent) await main?.daemon.term() if (mainEvent) await main?.daemon.term()
clearInterval(propertiesEvent)
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval)) if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
} }
private constructPropertiesEvent() {
const { runProperties } = this
return setInterval(() => {
runProperties()
}, EMBASSY_PROPERTIES_LOOP)
}
private constructHealthLoops() { private constructHealthLoops() {
const { manifest } = this.system const { manifest } = this.system
const effects = this.effects const effects = this.effects

View File

@@ -59,6 +59,33 @@ export type PackagePropertyObject = {
type: "object" type: "object"
description: string description: string
} }
const asProperty_ = (
x: PackagePropertyString | PackagePropertyObject,
): T.PropertiesValue => {
if (x.type === "object") {
return {
...x,
value: Object.fromEntries(
Object.entries(x.value).map(([key, value]) => [
key,
asProperty_(value),
]),
),
}
}
return {
masked: false,
description: null,
qr: null,
copyable: null,
...x,
}
}
const asProperty = (x: PackagePropertiesV2): T.PropertiesReturn =>
Object.fromEntries(
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
)
const [matchPackageProperties, setMatchPackageProperties] = const [matchPackageProperties, setMatchPackageProperties] =
deferred<PackagePropertiesV2>() deferred<PackagePropertiesV2>()
const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> = const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> =
@@ -92,44 +119,6 @@ const matchProperties = object({
data: matchPackageProperties, data: matchPackageProperties,
}) })
type ExportUi = {
values: { [key: string]: any }
expose: { [key: string]: T.ExposeUiPathsAll }
}
function propertiesToExportUi(
properties: PackagePropertiesV2,
previousPath = "",
): ExportUi {
const exportUi: ExportUi = {
values: {},
expose: {},
}
for (const [key, value] of Object.entries(properties)) {
const path = `${previousPath}/${key}`
if (value.type === "object") {
const { values, expose } = propertiesToExportUi(value.value, path)
exportUi.values[key] = values
exportUi.expose[key] = {
type: "object",
value: expose,
description: value.description,
}
continue
}
exportUi.values[key] = value.value
exportUi.expose[key] = {
type: "string",
path,
description: value.description ?? null,
masked: value.masked ?? false,
copyable: value.copyable ?? null,
qr: value.qr ?? null,
}
}
return exportUi
}
export class SystemForEmbassy implements System { export class SystemForEmbassy implements System {
currentRunning: MainLoop | undefined currentRunning: MainLoop | undefined
static async of(manifestLocation: string = MANIFEST_LOCATION) { static async of(manifestLocation: string = MANIFEST_LOCATION) {
@@ -236,6 +225,8 @@ export class SystemForEmbassy implements System {
return this.getConfig(effects) return this.getConfig(effects)
case "/config/set": case "/config/set":
return this.setConfig(effects, input) return this.setConfig(effects, input)
case "/properties":
return this.properties(effects)
case "/actions/metadata": case "/actions/metadata":
return todo() return todo()
case "/init": case "/init":
@@ -279,9 +270,7 @@ export class SystemForEmbassy implements System {
private async mainStart(effects: HostSystemStartOs): Promise<void> { private async mainStart(effects: HostSystemStartOs): Promise<void> {
if (!!this.currentRunning) return if (!!this.currentRunning) return
this.currentRunning = new MainLoop(this, effects, () => this.currentRunning = new MainLoop(this, effects)
this.properties(effects),
)
} }
private async mainStop( private async mainStop(
effects: HostSystemStartOs, effects: HostSystemStartOs,
@@ -472,51 +461,44 @@ export class SystemForEmbassy implements System {
} }
return { configured: true } return { configured: true }
} }
private async properties(effects: HostSystemStartOs): Promise<undefined> { private async properties(
effects: HostSystemStartOs,
): Promise<ReturnType<T.ExpectedExports.Properties>> {
// TODO BLU-J set the properties ever so often // TODO BLU-J set the properties ever so often
const setConfigValue = this.manifest.properties const setConfigValue = this.manifest.properties
if (!setConfigValue) return if (!setConfigValue) throw new Error("There is no properties")
if (setConfigValue.type === "docker") { if (setConfigValue.type === "docker") {
const container = await DockerProcedureContainer.of( const container = await DockerProcedureContainer.of(
effects, effects,
setConfigValue, setConfigValue,
this.manifest.volumes, this.manifest.volumes,
) )
const properties = JSON.parse( const properties = matchProperties.unsafeCast(
( JSON.parse(
await container.exec([ (
setConfigValue.entrypoint, await container.exec([
...setConfigValue.args, setConfigValue.entrypoint,
]) ...setConfigValue.args,
).stdout.toString(), ])
).stdout.toString(),
),
) )
if (!matchProperties.test(properties)) return return asProperty(properties.data)
const exposeUis = propertiesToExportUi(properties.data)
await effects.store.set<any, any>({
path: "/properties",
value: exposeUis.values,
})
await effects.exposeUi(exposeUis.expose)
} else if (setConfigValue.type === "script") { } else if (setConfigValue.type === "script") {
const moduleCode = this.moduleCode const moduleCode = this.moduleCode
const method = moduleCode.properties const method = moduleCode.properties
if (!method) if (!method)
throw new Error("Expecting that the method properties exists") throw new Error("Expecting that the method properties exists")
const properties = await method( const properties = matchProperties.unsafeCast(
new PolyfillEffects(effects, this.manifest), await method(new PolyfillEffects(effects, this.manifest)).then((x) => {
).then((x) => { if ("result" in x) return x.result
if ("result" in x) return x.result if ("error" in x) throw new Error("Error getting config: " + x.error)
if ("error" in x) throw new Error("Error getting config: " + x.error) throw new Error("Error getting config: " + x["error-code"][1])
throw new Error("Error getting config: " + x["error-code"][1]) }),
}) )
if (!matchProperties.test(properties)) return return asProperty(properties.data)
const exposeUis = propertiesToExportUi(properties.data)
await effects.store.set<any, any>({
path: "/properties",
value: exposeUis.values,
})
await effects.exposeUi(exposeUis.expose)
} }
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
} }
private async health( private async health(
effects: HostSystemStartOs, effects: HostSystemStartOs,
@@ -651,279 +633,6 @@ export class SystemForEmbassy implements System {
throw new Error("Error getting config: " + x["error-code"][1]) throw new Error("Error getting config: " + x["error-code"][1])
})) as any })) as any
} }
// private async sandbox(
// effects: HostSystemStartOs,
// options: {
// procedure:
// | "/createBackup"
// | "/restoreBackup"
// | "/getConfig"
// | "/setConfig"
// | "migration"
// | "/properties"
// | `/action/${string}`
// | `/dependencies/${string}/check`
// | `/dependencies/${string}/autoConfigure`
// input: unknown
// timeout?: number | undefined
// },
// ): Promise<unknown> {
// const input = options.input
// switch (options.procedure) {
// case "/createBackup":
// return this.roCreateBackup(effects)
// case "/restoreBackup":
// return this.roRestoreBackup(effects)
// case "/getConfig":
// return this.roGetConfig(effects)
// case "/setConfig":
// return this.roSetConfig(effects, input)
// case "migration":
// return this.roMigration(effects, input)
// case "/properties":
// return this.roProperties(effects)
// default:
// const procedure = options.procedure.split("/")
// switch (true) {
// case options.procedure.startsWith("/action/"):
// return this.roAction(effects, procedure[2], input)
// case options.procedure.startsWith("/dependencies/") &&
// procedure[3] === "check":
// return this.roDependenciesCheck(effects, procedure[2], input)
// case options.procedure.startsWith("/dependencies/") &&
// procedure[3] === "autoConfigure":
// return this.roDependenciesAutoconfig(effects, procedure[2], input)
// }
// }
// }
// private async roCreateBackup(effects: HostSystemStartOs): Promise<void> {
// const backup = this.manifest.backup.create
// if (backup.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(backup)
// await container.exec([backup.entrypoint, ...backup.args])
// } else {
// const moduleCode = await this.moduleCode
// await moduleCode.createBackup?.(new PolyfillEffects(effects))
// }
// }
// private async roRestoreBackup(effects: HostSystemStartOs): Promise<void> {
// const restoreBackup = this.manifest.backup.restore
// if (restoreBackup.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(restoreBackup)
// await container.exec([restoreBackup.entrypoint, ...restoreBackup.args])
// } else {
// const moduleCode = await this.moduleCode
// await moduleCode.restoreBackup?.(new PolyfillEffects(effects))
// }
// }
// private async roGetConfig(effects: HostSystemStartOs): Promise<T.ConfigRes> {
// const config = this.manifest.config?.get
// if (!config) return { spec: {} }
// if (config.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(config)
// return JSON.parse(
// (await container.exec([config.entrypoint, ...config.args])).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.getConfig
// if (!method) throw new Error("Expecting that the method getConfig exists")
// return (await method(new PolyfillEffects(effects)).then((x) => {
// if ("result" in x) return x.result
// if ("error" in x) throw new Error("Error getting config: " + x.error)
// throw new Error("Error getting config: " + x["error-code"][1])
// })) as any
// }
// }
// private async roSetConfig(
// effects: HostSystemStartOs,
// newConfig: unknown,
// ): Promise<T.SetResult> {
// const setConfigValue = this.manifest.config?.set
// if (!setConfigValue) return { signal: "SIGTERM", "depends-on": {} }
// if (setConfigValue.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// setConfigValue,
// )
// return JSON.parse(
// (
// await container.exec([
// setConfigValue.entrypoint,
// ...setConfigValue.args,
// JSON.stringify(newConfig),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.setConfig
// if (!method) throw new Error("Expecting that the method setConfig exists")
// return await method(
// new PolyfillEffects(effects),
// newConfig as U.Config,
// ).then((x) => {
// if ("result" in x) return x.result
// if ("error" in x) throw new Error("Error getting config: " + x.error)
// throw new Error("Error getting config: " + x["error-code"][1])
// })
// }
// }
// private async roMigration(
// effects: HostSystemStartOs,
// fromVersion: unknown,
// ): Promise<T.MigrationRes> {
// throw new Error("Migrations should never be ran in the sandbox mode")
// }
// private async roProperties(effects: HostSystemStartOs): Promise<unknown> {
// const setConfigValue = this.manifest.properties
// if (!setConfigValue) return {}
// if (setConfigValue.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// setConfigValue,
// )
// return JSON.parse(
// (
// await container.exec([
// setConfigValue.entrypoint,
// ...setConfigValue.args,
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.properties
// if (!method)
// throw new Error("Expecting that the method properties exists")
// return await method(new PolyfillEffects(effects)).then((x) => {
// if ("result" in x) return x.result
// if ("error" in x) throw new Error("Error getting config: " + x.error)
// throw new Error("Error getting config: " + x["error-code"][1])
// })
// }
// }
// private async roHealth(
// effects: HostSystemStartOs,
// healthId: string,
// timeSinceStarted: unknown,
// ): Promise<void> {
// const healthProcedure = this.manifest["health-checks"][healthId]
// if (!healthProcedure) return
// if (healthProcedure.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// healthProcedure,
// )
// return JSON.parse(
// (
// await container.exec([
// healthProcedure.entrypoint,
// ...healthProcedure.args,
// JSON.stringify(timeSinceStarted),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.health?.[healthId]
// if (!method) throw new Error("Expecting that the method health exists")
// await method(new PolyfillEffects(effects), Number(timeSinceStarted)).then(
// (x) => {
// if ("result" in x) return x.result
// if ("error" in x) throw new Error("Error getting config: " + x.error)
// throw new Error("Error getting config: " + x["error-code"][1])
// },
// )
// }
// }
// private async roAction(
// effects: HostSystemStartOs,
// actionId: string,
// formData: unknown,
// ): Promise<T.ActionResult> {
// const actionProcedure = this.manifest.actions?.[actionId]?.implementation
// if (!actionProcedure) return { message: "Action not found", value: null }
// if (actionProcedure.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// actionProcedure,
// )
// return JSON.parse(
// (
// await container.exec([
// actionProcedure.entrypoint,
// ...actionProcedure.args,
// JSON.stringify(formData),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.action?.[actionId]
// if (!method) throw new Error("Expecting that the method action exists")
// return (await method(new PolyfillEffects(effects), formData as any).then(
// (x) => {
// if ("result" in x) return x.result
// if ("error" in x) throw new Error("Error getting config: " + x.error)
// throw new Error("Error getting config: " + x["error-code"][1])
// },
// )) as any
// }
// }
// private async roDependenciesCheck(
// effects: HostSystemStartOs,
// id: string,
// oldConfig: unknown,
// ): Promise<object> {
// const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
// if (!actionProcedure) return { message: "Action not found", value: null }
// if (actionProcedure.type === "docker") {
// const container = await DockerProcedureContainer.readonlyOf(
// actionProcedure,
// )
// return JSON.parse(
// (
// await container.exec([
// actionProcedure.entrypoint,
// ...actionProcedure.args,
// JSON.stringify(oldConfig),
// ])
// ).stdout,
// )
// } else {
// const moduleCode = await this.moduleCode
// const method = moduleCode.dependencies?.[id]?.check
// if (!method)
// throw new Error(
// `Expecting that the method dependency check ${id} exists`,
// )
// return (await method(new PolyfillEffects(effects), oldConfig as any).then(
// (x) => {
// if ("result" in x) return x.result
// if ("error" in x) throw new Error("Error getting config: " + x.error)
// throw new Error("Error getting config: " + x["error-code"][1])
// },
// )) as any
// }
// }
// private async roDependenciesAutoconfig(
// effects: HostSystemStartOs,
// id: string,
// oldConfig: unknown,
// ): Promise<void> {
// const moduleCode = await this.moduleCode
// const method = moduleCode.dependencies?.[id]?.autoConfigure
// if (!method)
// throw new Error(
// `Expecting that the method dependency autoConfigure ${id} exists`,
// )
// return (await method(new PolyfillEffects(effects), oldConfig as any).then(
// (x) => {
// if ("result" in x) return x.result
// if ("error" in x) throw new Error("Error getting config: " + x.error)
// throw new Error("Error getting config: " + x["error-code"][1])
// },
// )) as any
// }
} }
async function removePointers(value: T.ConfigRes): Promise<T.ConfigRes> { async function removePointers(value: T.ConfigRes): Promise<T.ConfigRes> {
const startingSpec = structuredClone(value.spec) const startingSpec = structuredClone(value.spec)

View File

@@ -35,6 +35,7 @@ export const jsonPath = some(
"/backup/create", "/backup/create",
"/backup/restore", "/backup/restore",
"/actions/metadata", "/actions/metadata",
"/properties",
), ),
string.refine(isNestedPath, "isNestedPath"), string.refine(isNestedPath, "isNestedPath"),
) )

29
core/Cargo.lock generated
View File

@@ -2685,7 +2685,7 @@ dependencies = [
"tokio", "tokio",
"torut", "torut",
"tracing", "tracing",
"ts-rs", "ts-rs 7.1.1",
"yasi", "yasi",
] ]
@@ -4730,7 +4730,7 @@ dependencies = [
"tracing-journald", "tracing-journald",
"tracing-subscriber", "tracing-subscriber",
"trust-dns-server", "trust-dns-server",
"ts-rs", "ts-rs 8.1.0",
"typed-builder", "typed-builder",
"url", "url",
"urlencoding", "urlencoding",
@@ -5441,7 +5441,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6" checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6"
dependencies = [ dependencies = [
"thiserror", "thiserror",
"ts-rs-macros", "ts-rs-macros 7.1.1",
]
[[package]]
name = "ts-rs"
version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d3fa4606cdab1e9b668cc65ce2545941d01f52bc27536a195c66c55b91cb84"
dependencies = [
"thiserror",
"ts-rs-macros 8.1.0",
] ]
[[package]] [[package]]
@@ -5457,6 +5467,19 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "ts-rs-macros"
version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86ae36cbb2d58b86677ad413054feeb0712e382e822131cf9a4a1e580c419b5"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.52",
"termcolor",
]
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.21.0" version = "0.21.0"

View File

@@ -9,6 +9,7 @@ pub enum ProcedureName {
GetConfig, GetConfig,
SetConfig, SetConfig,
CreateBackup, CreateBackup,
Properties,
RestoreBackup, RestoreBackup,
ActionMetadata, ActionMetadata,
RunAction(ActionId), RunAction(ActionId),
@@ -29,6 +30,7 @@ impl ProcedureName {
ProcedureName::SetConfig => "/config/set".to_string(), ProcedureName::SetConfig => "/config/set".to_string(),
ProcedureName::GetConfig => "/config/get".to_string(), ProcedureName::GetConfig => "/config/get".to_string(),
ProcedureName::CreateBackup => "/backup/create".to_string(), ProcedureName::CreateBackup => "/backup/create".to_string(),
ProcedureName::Properties => "/properties".to_string(),
ProcedureName::RestoreBackup => "/backup/restore".to_string(), ProcedureName::RestoreBackup => "/backup/restore".to_string(),
ProcedureName::ActionMetadata => "/actions/metadata".to_string(), ProcedureName::ActionMetadata => "/actions/metadata".to_string(),
ProcedureName::RunAction(id) => format!("/actions/{}/run", id), ProcedureName::RunAction(id) => format!("/actions/{}/run", id),

View File

@@ -174,7 +174,7 @@ tracing-futures = "0.2.5"
tracing-journald = "0.3.0" tracing-journald = "0.3.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
trust-dns-server = "0.23.1" trust-dns-server = "0.23.1"
ts-rs = "7.1.1" ts-rs = "8.1.0"
typed-builder = "0.18.0" typed-builder = "0.18.0"
url = { version = "2.4.1", features = ["serde"] } url = { version = "2.4.1", features = ["serde"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AllowedStatuses } from "./AllowedStatuses"; import type { AllowedStatuses } from "./AllowedStatuses";
export interface ActionMetadata { name: string, description: string, warning: string | null, disabled: boolean, input: {[key: string]: any}, allowedStatuses: AllowedStatuses, group: string | null, } export type ActionMetadata = { name: string, description: string, warning: string | null, disabled: boolean, input: {[key: string]: any}, allowedStatuses: AllowedStatuses, group: string | null, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface AddSslOptions { scheme: string | null, preferredExternalPort: number, addXForwardedHeaders: boolean | null, } export type AddSslOptions = { scheme: string | null, preferredExternalPort: number, addXForwardedHeaders: boolean | null, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BindOptions } from "./BindOptions"; import type { BindOptions } from "./BindOptions";
export interface AddressInfo { username: string | null, hostId: string, bindOptions: BindOptions, suffix: string, } export type AddressInfo = { username: string | null, hostId: string, bindOptions: BindOptions, suffix: string, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AllowedStatuses = "only-running" | "only-stopped" | "any"; export type AllowedStatuses = "onlyRunning" | "onlyStopped" | "any";

View File

@@ -2,4 +2,4 @@
import type { AddSslOptions } from "./AddSslOptions"; import type { AddSslOptions } from "./AddSslOptions";
import type { BindOptionsSecure } from "./BindOptionsSecure"; import type { BindOptionsSecure } from "./BindOptionsSecure";
export interface BindOptions { scheme: string | null, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, } export type BindOptions = { scheme: string | null, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface BindOptionsSecure { ssl: boolean, } export type BindOptionsSecure = { ssl: boolean, };

View File

@@ -3,4 +3,4 @@ import type { AddSslOptions } from "./AddSslOptions";
import type { BindKind } from "./BindKind"; import type { BindKind } from "./BindKind";
import type { BindOptionsSecure } from "./BindOptionsSecure"; import type { BindOptionsSecure } from "./BindOptionsSecure";
export interface BindParams { kind: BindKind, id: string, internalPort: number, scheme: string, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, } export type BindParams = { kind: BindKind, id: string, internalPort: number, scheme: string, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ChrootParams { env: string | null, workdir: string | null, user: string | null, path: string, command: string, args: string[], } export type ChrootParams = { env: string | null, workdir: string | null, user: string | null, path: string, command: string, args: string[], };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface CreateOverlayedImageParams { imageId: string, } export type CreateOverlayedImageParams = { imageId: string, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface DestroyOverlayedImageParams { guid: string, } export type DestroyOverlayedImageParams = { guid: string, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ExecuteAction { serviceId: string | null, actionId: string, input: any, } export type ExecuteAction = { serviceId: string | null, actionId: string, input: any, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionMetadata } from "./ActionMetadata"; import type { ActionMetadata } from "./ActionMetadata";
export interface ExportActionParams { id: string, metadata: ActionMetadata, } export type ExportActionParams = { id: string; metadata: ActionMetadata };

View File

@@ -2,4 +2,4 @@
import type { AddressInfo } from "./AddressInfo"; import type { AddressInfo } from "./AddressInfo";
import type { ServiceInterfaceType } from "./ServiceInterfaceType"; import type { ServiceInterfaceType } from "./ServiceInterfaceType";
export interface ExportServiceInterfaceParams { id: string, name: string, description: string, hasPrimary: boolean, disabled: boolean, masked: boolean, addressInfo: AddressInfo, type: ServiceInterfaceType, } export type ExportServiceInterfaceParams = { id: string, name: string, description: string, hasPrimary: boolean, disabled: boolean, masked: boolean, addressInfo: AddressInfo, type: ServiceInterfaceType, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ExposeForDependentsParams { paths: string[], } export type ExposeForDependentsParams = { paths: string[], };

View File

@@ -2,4 +2,4 @@
import type { Callback } from "./Callback"; import type { Callback } from "./Callback";
import type { GetHostInfoParamsKind } from "./GetHostInfoParamsKind"; import type { GetHostInfoParamsKind } from "./GetHostInfoParamsKind";
export interface GetHostInfoParams { kind: GetHostInfoParamsKind | null, serviceInterfaceId: string, packageId: string | null, callback: Callback, } export type GetHostInfoParams = { kind: GetHostInfoParamsKind | null, serviceInterfaceId: string, packageId: string | null, callback: Callback, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Callback } from "./Callback"; import type { Callback } from "./Callback";
export interface GetPrimaryUrlParams { packageId: string | null, serviceInterfaceId: string, callback: Callback, } export type GetPrimaryUrlParams = { packageId: string | null, serviceInterfaceId: string, callback: Callback, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Callback } from "./Callback"; import type { Callback } from "./Callback";
export interface GetServiceInterfaceParams { packageId: string | null, serviceInterfaceId: string, callback: Callback, } export type GetServiceInterfaceParams = { packageId: string | null, serviceInterfaceId: string, callback: Callback, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface GetServicePortForwardParams { packageId: string | null, internalPort: number, } export type GetServicePortForwardParams = { packageId: string | null, internalPort: number, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Algorithm } from "./Algorithm"; import type { Algorithm } from "./Algorithm";
export interface GetSslCertificateParams { packageId: string | null, hostId: string, algorithm: Algorithm | null, } export type GetSslCertificateParams = { packageId: string | null, hostId: string, algorithm: Algorithm | null, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Algorithm } from "./Algorithm"; import type { Algorithm } from "./Algorithm";
export interface GetSslKeyParams { packageId: string | null, hostId: string, algorithm: Algorithm | null, } export type GetSslKeyParams = { packageId: string | null, hostId: string, algorithm: Algorithm | null, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface GetStoreParams { packageId: string | null, path: string, } export type GetStoreParams = { packageId: string | null, path: string, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Callback } from "./Callback"; import type { Callback } from "./Callback";
export interface GetSystemSmtpParams { callback: Callback, } export type GetSystemSmtpParams = { callback: Callback, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Callback } from "./Callback"; import type { Callback } from "./Callback";
export interface ListServiceInterfacesParams { packageId: string | null, callback: Callback, } export type ListServiceInterfacesParams = { packageId: string | null, callback: Callback, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MountTarget } from "./MountTarget"; import type { MountTarget } from "./MountTarget";
export interface MountParams { location: string, target: MountTarget, } export type MountParams = { location: string, target: MountTarget, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface MountTarget { packageId: string, volumeId: string, subpath: string | null, readonly: boolean, } export type MountTarget = { packageId: string, volumeId: string, subpath: string | null, readonly: boolean, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ParamsMaybePackageId { packageId: string | null, } export type ParamsMaybePackageId = { packageId: string | null, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ParamsPackageId { packageId: string, } export type ParamsPackageId = { packageId: string, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface RemoveActionParams { id: string, } export type RemoveActionParams = { id: string, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface RemoveAddressParams { id: string, } export type RemoveAddressParams = { id: string, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReverseProxyBind { ip: string | null, port: number, ssl: boolean, } export type ReverseProxyBind = { ip: string | null, port: number, ssl: boolean, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReverseProxyDestination { ip: string | null, port: number, ssl: boolean, } export type ReverseProxyDestination = { ip: string | null, port: number, ssl: boolean, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReverseProxyHttp { headers: null | {[key: string]: string}, } export type ReverseProxyHttp = { headers: null | {[key: string]: string}, };

View File

@@ -3,4 +3,4 @@ import type { ReverseProxyBind } from "./ReverseProxyBind";
import type { ReverseProxyDestination } from "./ReverseProxyDestination"; import type { ReverseProxyDestination } from "./ReverseProxyDestination";
import type { ReverseProxyHttp } from "./ReverseProxyHttp"; import type { ReverseProxyHttp } from "./ReverseProxyHttp";
export interface ReverseProxyParams { bind: ReverseProxyBind, dst: ReverseProxyDestination, http: ReverseProxyHttp, } export type ReverseProxyParams = { bind: ReverseProxyBind, dst: ReverseProxyDestination, http: ReverseProxyHttp, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface SetConfigured { configured: boolean, } export type SetConfigured = { configured: boolean, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DependencyRequirement } from "./DependencyRequirement"; import type { DependencyRequirement } from "./DependencyRequirement";
export interface SetDependenciesParams { dependencies: Array<DependencyRequirement>, } export type SetDependenciesParams = { dependencies: Array<DependencyRequirement>, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HealthCheckString } from "./HealthCheckString"; import type { HealthCheckString } from "./HealthCheckString";
export interface SetHealth { name: string, status: HealthCheckString, message: string | null, } export type SetHealth = { name: string, status: HealthCheckString, message: string | null, };

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Status } from "./Status"; import type { Status } from "./Status";
export interface SetMainStatus { status: Status, } export type SetMainStatus = { status: Status, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface SetStoreParams { value: any, path: string, } export type SetStoreParams = { value: any, path: string, };

View File

@@ -93,7 +93,6 @@ pub fn auth() -> ParentHandler {
from_fn_async(logout) from_fn_async(logout)
.with_metadata("get-session", Value::Bool(true)) .with_metadata("get-session", Value::Bool(true))
.with_remote_cli::<CliContext>() .with_remote_cli::<CliContext>()
// TODO @dr-bonez
.no_display(), .no_display(),
) )
.subcommand("session", session()) .subcommand("session", session())

View File

@@ -45,6 +45,15 @@ pub fn backup() -> ParentHandler {
.subcommand("target", target::target()) .subcommand("target", target::target())
} }
pub fn package_backup() -> ParentHandler {
ParentHandler::new().subcommand(
"restore",
from_fn_async(restore::restore_packages_rpc)
.no_display()
.with_remote_cli::<CliContext>(),
)
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct BackupMetadata { struct BackupMetadata {
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,

View File

@@ -33,7 +33,6 @@ pub struct RestorePackageParams {
pub password: String, pub password: String,
} }
// TODO dr Why doesn't anything use this
// #[command(rename = "restore", display(display_none))] // #[command(rename = "restore", display(display_none))]
#[instrument(skip(ctx, password))] #[instrument(skip(ctx, password))]
pub async fn restore_packages_rpc( pub async fn restore_packages_rpc(
@@ -71,7 +70,7 @@ pub async fn restore_packages_rpc(
pub async fn recover_full_embassy( pub async fn recover_full_embassy(
ctx: SetupContext, ctx: SetupContext,
disk_guid: Arc<String>, disk_guid: Arc<String>,
embassy_password: String, start_os_password: String,
recovery_source: TmpMountGuard, recovery_source: TmpMountGuard,
recovery_password: Option<String>, recovery_password: Option<String>,
) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> { ) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
@@ -89,7 +88,7 @@ pub async fn recover_full_embassy(
)?; )?;
os_backup.account.password = argon2::hash_encoded( os_backup.account.password = argon2::hash_encoded(
embassy_password.as_bytes(), start_os_password.as_bytes(),
&rand::random::<[u8; 16]>()[..], &rand::random::<[u8; 16]>()[..],
&argon2::Config::rfc9106_low_mem(), &argon2::Config::rfc9106_low_mem(),
) )

View File

@@ -42,7 +42,7 @@ pub struct CifsBackupTarget {
path: PathBuf, path: PathBuf,
username: String, username: String,
mountable: bool, mountable: bool,
embassy_os: Option<EmbassyOsRecoveryInfo>, start_os: Option<EmbassyOsRecoveryInfo>,
} }
pub fn cifs() -> ParentHandler { pub fn cifs() -> ParentHandler {
@@ -93,7 +93,7 @@ pub async fn add(
password, password,
}; };
let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?; let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?;
let embassy_os = recovery_info(guard.path()).await?; let start_os = recovery_info(guard.path()).await?;
guard.unmount().await?; guard.unmount().await?;
let id = ctx let id = ctx
.db .db
@@ -116,7 +116,7 @@ pub async fn add(
path: cifs.path, path: cifs.path,
username: cifs.username, username: cifs.username,
mountable: true, mountable: true,
embassy_os, start_os,
}), }),
}) })
} }
@@ -157,7 +157,7 @@ pub async fn update(
password, password,
}; };
let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?; let guard = TmpMountGuard::mount(&cifs, ReadOnly).await?;
let embassy_os = recovery_info(guard.path()).await?; let start_os = recovery_info(guard.path()).await?;
guard.unmount().await?; guard.unmount().await?;
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
@@ -180,7 +180,7 @@ pub async fn update(
path: cifs.path, path: cifs.path,
username: cifs.username, username: cifs.username,
mountable: true, mountable: true,
embassy_os, start_os,
}), }),
}) })
} }
@@ -224,11 +224,11 @@ pub async fn list(db: &DatabaseModel) -> Result<Vec<(u32, CifsBackupTarget)>, Er
let mut cifs = Vec::new(); let mut cifs = Vec::new();
for (id, model) in db.as_private().as_cifs().as_entries()? { for (id, model) in db.as_private().as_cifs().as_entries()? {
let mount_info = model.de()?; let mount_info = model.de()?;
let embassy_os = async { let start_os = async {
let guard = TmpMountGuard::mount(&mount_info, ReadOnly).await?; let guard = TmpMountGuard::mount(&mount_info, ReadOnly).await?;
let embassy_os = recovery_info(guard.path()).await?; let start_os = recovery_info(guard.path()).await?;
guard.unmount().await?; guard.unmount().await?;
Ok::<_, Error>(embassy_os) Ok::<_, Error>(start_os)
} }
.await; .await;
cifs.push(( cifs.push((
@@ -237,8 +237,8 @@ pub async fn list(db: &DatabaseModel) -> Result<Vec<(u32, CifsBackupTarget)>, Er
hostname: mount_info.hostname, hostname: mount_info.hostname,
path: mount_info.path, path: mount_info.path,
username: mount_info.username, username: mount_info.username,
mountable: embassy_os.is_ok(), mountable: start_os.is_ok(),
embassy_os: embassy_os.ok().and_then(|a| a), start_os: start_os.ok().and_then(|a| a),
}, },
)); ));
} }

View File

@@ -165,7 +165,6 @@ pub struct SetParams {
pub config: StdinDeserializable<Option<Config>>, pub config: StdinDeserializable<Option<Config>>,
} }
// TODO Dr Why isn't this used?
// #[command( // #[command(
// subcommands(self(set_impl(async, context(RpcContext))), set_dry), // subcommands(self(set_impl(async, context(RpcContext))), set_dry),
// display(display_none), // display(display_none),

View File

@@ -19,7 +19,6 @@ use super::setup::CURRENT_SECRET;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler}; use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler};
use crate::db::model::package::CurrentDependents;
use crate::db::prelude::PatchDbExt; use crate::db::prelude::PatchDbExt;
use crate::dependencies::compute_dependency_config_errs; use crate::dependencies::compute_dependency_config_errs;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
@@ -207,39 +206,6 @@ impl RpcContext {
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn cleanup_and_initialize(&self) -> Result<(), Error> { pub async fn cleanup_and_initialize(&self) -> Result<(), Error> {
self.db
.mutate(|f| {
let mut current_dependents = f
.as_public_mut()
.as_package_data()
.keys()?
.into_iter()
.map(|k| (k.clone(), BTreeMap::new()))
.collect::<BTreeMap<_, _>>();
for (package_id, package) in
f.as_public_mut().as_package_data_mut().as_entries_mut()?
{
for (k, v) in package.clone().into_current_dependencies().into_entries()? {
let mut entry: BTreeMap<_, _> =
current_dependents.remove(&k).unwrap_or_default();
entry.insert(package_id.clone(), v.de()?);
current_dependents.insert(k, entry);
}
}
for (package_id, current_dependents) in current_dependents {
if let Some(deps) = f
.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package_id)
.map(|i| i.as_current_dependents_mut())
{
deps.ser(&CurrentDependents(current_dependents))?;
}
}
Ok(())
})
.await?;
self.services.init(&self).await?; self.services.init(&self).await?;
tracing::info!("Initialized Package Managers"); tracing::info!("Initialized Package Managers");

View File

@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use emver::VersionRange;
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{DataUrl, HealthCheckId, HostId, PackageId}; use models::{DataUrl, HealthCheckId, HostId, PackageId};
use patch_db::json_ptr::JsonPointer; use patch_db::json_ptr::JsonPointer;
@@ -299,7 +300,6 @@ pub struct PackageDataEntry {
pub icon: DataUrl<'static>, pub icon: DataUrl<'static>,
pub last_backup: Option<DateTime<Utc>>, pub last_backup: Option<DateTime<Utc>>,
pub dependency_info: BTreeMap<PackageId, StaticDependencyInfo>, pub dependency_info: BTreeMap<PackageId, StaticDependencyInfo>,
pub current_dependents: CurrentDependents,
pub current_dependencies: CurrentDependencies, pub current_dependencies: CurrentDependencies,
pub interface_addresses: InterfaceAddressMap, pub interface_addresses: InterfaceAddressMap,
pub hosts: HostInfo, pub hosts: HostInfo,
@@ -357,29 +357,6 @@ impl Default for ExposedUI {
} }
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct CurrentDependents(pub BTreeMap<PackageId, CurrentDependencyInfo>);
impl CurrentDependents {
pub fn map(
mut self,
transform: impl Fn(
BTreeMap<PackageId, CurrentDependencyInfo>,
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
) -> Self {
self.0 = transform(self.0);
self
}
}
impl Map for CurrentDependents {
type Key = PackageId;
type Value = CurrentDependencyInfo;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct CurrentDependencies(pub BTreeMap<PackageId, CurrentDependencyInfo>); pub struct CurrentDependencies(pub BTreeMap<PackageId, CurrentDependencyInfo>);
impl CurrentDependencies { impl CurrentDependencies {
@@ -416,10 +393,21 @@ pub struct StaticDependencyInfo {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(tag = "kind")] #[serde(tag = "kind")]
pub enum CurrentDependencyInfo { pub enum CurrentDependencyInfo {
Exists, #[serde(rename_all = "camelCase")]
Exists {
#[ts(type = "string")]
url: Url,
#[ts(type = "string")]
version_spec: VersionRange,
},
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
Running { Running {
#[ts(type = "string")]
url: Url,
#[ts(type = "string")]
version_spec: VersionRange,
#[serde(default)] #[serde(default)]
#[ts(type = "string[]")]
health_checks: BTreeSet<HealthCheckId>, health_checks: BTreeSet<HealthCheckId>,
}, },
} }

View File

@@ -56,8 +56,6 @@ pub struct DepInfo {
pub version: VersionRange, pub version: VersionRange,
pub requirement: DependencyRequirement, pub requirement: DependencyRequirement,
pub description: Option<String>, pub description: Option<String>,
#[serde(default)]
pub config: Option<Value>, // TODO: remove
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser)]

View File

@@ -101,7 +101,7 @@ fn display_disk_info(params: WithIoFormat<Empty>, args: Vec<DiskInfo>) {
} else { } else {
"N/A" "N/A"
}, },
if let Some(eos) = part.embassy_os.as_ref() { if let Some(eos) = part.start_os.as_ref() {
eos.version.as_str() eos.version.as_str()
} else { } else {
"N/A" "N/A"

View File

@@ -49,7 +49,7 @@ pub struct PartitionInfo {
pub label: Option<String>, pub label: Option<String>,
pub capacity: u64, pub capacity: u64,
pub used: Option<u64>, pub used: Option<u64>,
pub embassy_os: Option<EmbassyOsRecoveryInfo>, pub start_os: Option<EmbassyOsRecoveryInfo>,
pub guid: Option<String>, pub guid: Option<String>,
} }
@@ -390,7 +390,7 @@ async fn disk_info(disk: PathBuf) -> DiskInfo {
} }
async fn part_info(part: PathBuf) -> PartitionInfo { async fn part_info(part: PathBuf) -> PartitionInfo {
let mut embassy_os = None; let mut start_os = None;
let label = get_label(&part) let label = get_label(&part)
.await .await
.map_err(|e| tracing::warn!("Could not get label of {}: {}", part.display(), e.source)) .map_err(|e| tracing::warn!("Could not get label of {}: {}", part.display(), e.source))
@@ -417,7 +417,7 @@ async fn part_info(part: PathBuf) -> PartitionInfo {
None None
} }
} { } {
embassy_os = Some(recovery_info) start_os = Some(recovery_info)
} }
if let Err(e) = mount_guard.unmount().await { if let Err(e) = mount_guard.unmount().await {
tracing::error!("Error unmounting partition {}: {}", part.display(), e); tracing::error!("Error unmounting partition {}: {}", part.display(), e);
@@ -430,7 +430,7 @@ async fn part_info(part: PathBuf) -> PartitionInfo {
label, label,
capacity, capacity,
used, used,
embassy_os, start_os,
guid: None, guid: None,
} }
} }

View File

@@ -236,7 +236,7 @@ pub fn package() -> ParentHandler {
.with_remote_cli::<CliContext>(), .with_remote_cli::<CliContext>(),
) )
.subcommand("dependency", dependencies::dependency()) .subcommand("dependency", dependencies::dependency())
.subcommand("package-backup", backup::backup()) .subcommand("backup", backup::package_backup())
.subcommand("connect", from_fn_async(service::connect_rpc).no_cli()) .subcommand("connect", from_fn_async(service::connect_rpc).no_cli())
.subcommand( .subcommand(
"connect", "connect",

View File

@@ -183,7 +183,6 @@ pub async fn root_ca_start_time() -> Result<SystemTime, Error> {
const EC_CURVE_NAME: nid::Nid = nid::Nid::X9_62_PRIME256V1; const EC_CURVE_NAME: nid::Nid = nid::Nid::X9_62_PRIME256V1;
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref EC_GROUP: EcGroup = EcGroup::from_curve_name(EC_CURVE_NAME).unwrap(); static ref EC_GROUP: EcGroup = EcGroup::from_curve_name(EC_CURVE_NAME).unwrap();
static ref SSL_MUTEX: Mutex<()> = Mutex::new(()); // TODO: make thread safe
} }
pub async fn export_key(key: &PKey<Private>, target: &Path) -> Result<(), Error> { pub async fn export_key(key: &PKey<Private>, target: &Path) -> Result<(), Error> {

View File

@@ -168,7 +168,7 @@ pub fn main_ui_server_router(ctx: RpcContext) -> Router {
}), }),
) )
.fallback(any(move |request: Request| async move { .fallback(any(move |request: Request| async move {
main_embassy_ui(request, ctx) main_start_os_ui(request, ctx)
.await .await
.unwrap_or_else(server_error) .unwrap_or_else(server_error)
})) }))
@@ -218,7 +218,7 @@ async fn if_authorized<
} }
} }
async fn main_embassy_ui(req: Request, ctx: RpcContext) -> Result<Response, Error> { async fn main_start_os_ui(req: Request, ctx: RpcContext) -> Result<Response, Error> {
let (request_parts, _body) = req.into_parts(); let (request_parts, _body) = req.into_parts();
match ( match (
&request_parts.method, &request_parts.method,

View File

@@ -768,7 +768,7 @@ async fn test() {
let mut conn = torut::control::UnauthenticatedConn::new( let mut conn = torut::control::UnauthenticatedConn::new(
TcpStream::connect(SocketAddr::from(([127, 0, 0, 1], 9051))) TcpStream::connect(SocketAddr::from(([127, 0, 0, 1], 9051)))
.await .await
.unwrap(), // TODO .unwrap(),
); );
let auth = conn let auth = conn
.load_protocol_info() .load_protocol_info()

View File

@@ -7,6 +7,7 @@ use rpc_toolkit::{command, from_fn_async, AnyContext, HandlerExt, ParentHandler}
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use crate::context::config::ServerConfig;
use crate::context::{CliContext, InstallContext}; use crate::context::{CliContext, InstallContext};
use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::block_dev::BlockDev;
@@ -23,14 +24,6 @@ use crate::ARCH;
mod gpt; mod gpt;
mod mbr; mod mbr;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PostInstallConfig {
os_partitions: OsPartitionInfo,
ethernet_interface: String,
wifi_interface: Option<String>,
}
pub fn install() -> ParentHandler { pub fn install() -> ParentHandler {
ParentHandler::new() ParentHandler::new()
.subcommand("disk", disk()) .subcommand("disk", disk())
@@ -263,10 +256,11 @@ pub async fn execute(
tokio::fs::write( tokio::fs::write(
rootfs.path().join("config/config.yaml"), rootfs.path().join("config/config.yaml"),
IoFormat::Yaml.to_vec(&PostInstallConfig { IoFormat::Yaml.to_vec(&ServerConfig {
os_partitions: part_info.clone(), os_partitions: Some(part_info.clone()),
ethernet_interface: eth_iface, ethernet_interface: Some(eth_iface),
wifi_interface: wifi_iface, wifi_interface: wifi_iface,
..Default::default()
})?, })?,
) )
.await?; .await?;
@@ -299,7 +293,7 @@ pub async fn execute(
.invoke(crate::ErrorKind::OpenSsh) .invoke(crate::ErrorKind::OpenSsh)
.await?; .await?;
let embassy_fs = MountGuard::mount( let start_os_fs = MountGuard::mount(
&Bind::new(rootfs.path()), &Bind::new(rootfs.path()),
current.join("media/embassy/embassyfs"), current.join("media/embassy/embassyfs"),
MountType::ReadOnly, MountType::ReadOnly,
@@ -348,7 +342,7 @@ pub async fn execute(
} }
sys.unmount(false).await?; sys.unmount(false).await?;
proc.unmount(false).await?; proc.unmount(false).await?;
embassy_fs.unmount(false).await?; start_os_fs.unmount(false).await?;
if let Some(efi) = efi { if let Some(efi) = efi {
efi.unmount(false).await?; efi.unmount(false).await?;
} }

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::{borrow::Borrow, collections::BTreeMap};
use clap::Parser; use clap::Parser;
use imbl_value::{json, InOMap, InternedString, Value}; use imbl_value::{json, InOMap, InternedString, Value};
@@ -15,48 +15,6 @@ pub fn display_properties(response: Value) {
println!("{}", response); println!("{}", response);
} }
trait IntoProperties {
fn into_properties(self, store: &Value) -> Value;
}
impl IntoProperties for ExposedUI {
fn into_properties(self, store: &Value) -> Value {
match self {
ExposedUI::Object { value, description } => {
json!({
"type": "object",
"description": description,
"value": value.into_iter().map(|(k, v)| (k, v.into_properties(store))).collect::<BTreeMap<String,_>>()
})
}
ExposedUI::String {
path,
description,
masked,
copyable,
qr,
} => json!({
"type": "string",
"description": description,
"value": path.get(store).cloned().unwrap_or_default(),
"copyable": copyable,
"qr": qr,
"masked": masked
}),
}
}
}
impl IntoProperties for StoreExposedUI {
fn into_properties(self, store: &Value) -> Value {
Value::Object(
self.0
.into_iter()
.map(|(k, v)| (k, v.into_properties(store)))
.collect::<InOMap<InternedString, Value>>(),
)
}
}
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
@@ -68,19 +26,11 @@ pub async fn properties(
ctx: RpcContext, ctx: RpcContext,
PropertiesParam { id }: PropertiesParam, PropertiesParam { id }: PropertiesParam,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let peeked = ctx.db.peek().await; match &*ctx.services.get(&id).await {
let data = peeked Some(service) => service.properties().await,
.as_private() None => Err(Error::new(
.as_package_stores() eyre!("Could not find a service with id {id}"),
.as_idx(&id) ErrorKind::NotFound,
.map(|x| x.de()) )),
.unwrap_or_else(|| Ok(json!({})))?; }
Ok(peeked
.as_public()
.as_package_data()
.as_idx(&id)
.or_not_found(&id)?
.as_store_exposed_ui()
.de()?
.into_properties(&data))
} }

View File

@@ -132,7 +132,7 @@ async fn add_image(
Command::new("bash") Command::new("bash")
.arg("-c") .arg("-c")
.arg(format!( .arg(format!(
"{CONTAINER_TOOL} export {container_id} | mksquashfs - {sqfs} -tar -force-uid 100000 -force-gid 100000", // TODO: real uid mapping "{CONTAINER_TOOL} export {container_id} | mksquashfs - {sqfs} -tar",
container_id = container_id.trim(), container_id = container_id.trim(),
sqfs = sqfs_path.display() sqfs = sqfs_path.display()
)) ))

View File

@@ -1,7 +1,9 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use imbl_value::InOMap; use imbl_value::InOMap;
pub use models::PackageId; pub use models::PackageId;
use models::VolumeId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@@ -11,7 +13,6 @@ use crate::prelude::*;
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements};
use crate::util::Version; use crate::util::Version;
use crate::version::{Current, VersionT}; use crate::version::{Current, VersionT};
use crate::volume::Volumes;
fn current_version() -> Version { fn current_version() -> Version {
Current::new().semver().into() Current::new().semver().into()
@@ -40,7 +41,7 @@ pub struct Manifest {
pub donation_url: Option<Url>, pub donation_url: Option<Url>,
#[serde(default)] #[serde(default)]
pub alerts: Alerts, pub alerts: Alerts,
pub volumes: Volumes, pub volumes: BTreeMap<VolumeId, Value>,
#[serde(default)] #[serde(default)]
pub dependencies: Dependencies, pub dependencies: Dependencies,
pub config: Option<InOMap<String, Value>>, pub config: Option<InOMap<String, Value>>,

View File

@@ -19,7 +19,6 @@ use crate::s9pk::v1::reader::S9pkReader;
use crate::s9pk::v2::S9pk; use crate::s9pk::v2::S9pk;
use crate::util::io::TmpDir; use crate::util::io::TmpDir;
use crate::util::Invoke; use crate::util::Invoke;
use crate::volume::Volume;
use crate::ARCH; use crate::ARCH;
pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01]; pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01];
@@ -254,7 +253,7 @@ impl S9pk<Section<MultiCursorFile>> {
for (asset_id, _) in manifest for (asset_id, _) in manifest
.volumes .volumes
.iter() .iter()
.filter(|(_, v)| matches!(v, Volume::Assets { .. })) .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("assets"))
{ {
let assets_path = asset_dir.join(&asset_id); let assets_path = asset_dir.join(&asset_id);
let sqfs_path = assets_path.with_extension("squashfs"); let sqfs_path = assets_path.with_extension("squashfs");
@@ -338,13 +337,13 @@ impl From<ManifestV1> for Manifest {
assets: value assets: value
.volumes .volumes
.iter() .iter()
.filter(|(_, v)| matches!(v, &&Volume::Assets { .. })) .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("assets"))
.map(|(id, _)| id.clone()) .map(|(id, _)| id.clone())
.collect(), .collect(),
volumes: value volumes: value
.volumes .volumes
.iter() .iter()
.filter(|(_, v)| matches!(v, &&Volume::Data { .. })) .filter(|(_, v)| v.get("type").and_then(|v| v.as_str()) == Some("data"))
.map(|(id, _)| id.clone()) .map(|(id, _)| id.clone())
.collect(), .collect(),
alerts: value.alerts, alerts: value.alerts,

View File

@@ -325,6 +325,17 @@ impl Service {
.await .await
.with_kind(ErrorKind::Action) .with_kind(ErrorKind::Action)
} }
pub async fn properties(&self) -> Result<Value, Error> {
let container = &self.seed.persistent_container;
container
.execute::<Value>(
ProcedureName::Properties,
Value::Null,
Some(Duration::from_secs(30)),
)
.await
.with_kind(ErrorKind::Unknown)
}
pub async fn shutdown(self) -> Result<(), Error> { pub async fn shutdown(self) -> Result<(), Error> {
self.actor self.actor

View File

@@ -8,6 +8,7 @@ use std::sync::{Arc, Weak};
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use clap::Parser; use clap::Parser;
use emver::VersionRange;
use imbl::OrdMap; use imbl::OrdMap;
use imbl_value::{json, InternedString}; use imbl_value::{json, InternedString};
use models::{ActionId, HealthCheckId, ImageId, PackageId, VolumeId}; use models::{ActionId, HealthCheckId, ImageId, PackageId, VolumeId};
@@ -16,6 +17,7 @@ use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use ts_rs::TS; use ts_rs::TS;
use url::Url;
use crate::db::model::package::{ use crate::db::model::package::{
CurrentDependencies, CurrentDependencyInfo, ExposedUI, StoreExposedUI, CurrentDependencies, CurrentDependencyInfo, ExposedUI, StoreExposedUI,
@@ -255,7 +257,7 @@ struct RemoveAddressParams {
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")]
enum AllowedStatuses { enum AllowedStatuses {
OnlyRunning, // onlyRunning OnlyRunning, // onlyRunning
OnlyStopped, OnlyStopped,
@@ -1096,15 +1098,19 @@ enum DependencyRequirement {
id: PackageId, id: PackageId,
#[ts(type = "string[]")] #[ts(type = "string[]")]
health_checks: BTreeSet<HealthCheckId>, health_checks: BTreeSet<HealthCheckId>,
version_spec: String, #[ts(type = "string")]
url: String, version_spec: VersionRange,
#[ts(type = "string")]
url: Url,
}, },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
Exists { Exists {
#[ts(type = "string")] #[ts(type = "string")]
id: PackageId, id: PackageId,
version_spec: String, #[ts(type = "string")]
url: String, version_spec: VersionRange,
#[ts(type = "string")]
url: Url,
}, },
} }
// filebrowser:exists,bitcoind:running:foo+bar+baz // filebrowser:exists,bitcoind:running:foo+bar+baz
@@ -1114,8 +1120,8 @@ impl FromStr for DependencyRequirement {
match s.split_once(':') { match s.split_once(':') {
Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists { Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists {
id: id.parse()?, id: id.parse()?,
url: "".to_string(), url: "".parse()?, // TODO
version_spec: "*".to_string(), version_spec: "*".parse()?, // TODO
}), }),
Some((id, rest)) => { Some((id, rest)) => {
let health_checks = match rest.split_once(':') { let health_checks = match rest.split_once(':') {
@@ -1138,15 +1144,15 @@ impl FromStr for DependencyRequirement {
Ok(Self::Running { Ok(Self::Running {
id: id.parse()?, id: id.parse()?,
health_checks, health_checks,
url: "".to_string(), url: "".parse()?, // TODO
version_spec: "*".to_string(), version_spec: "*".parse()?, // TODO
}) })
} }
None => Ok(Self::Running { None => Ok(Self::Running {
id: s.parse()?, id: s.parse()?,
health_checks: BTreeSet::new(), health_checks: BTreeSet::new(),
url: "".to_string(), url: "".parse()?, // TODO
version_spec: "*".to_string(), version_spec: "*".parse()?, // TODO
}), }),
} }
} }
@@ -1183,23 +1189,23 @@ async fn set_dependencies(
id, id,
url, url,
version_spec, version_spec,
} => (id, CurrentDependencyInfo::Exists), } => (id, CurrentDependencyInfo::Exists { url, version_spec }),
DependencyRequirement::Running { DependencyRequirement::Running {
id, id,
health_checks, health_checks,
url, url,
version_spec, version_spec,
} => (id, CurrentDependencyInfo::Running { health_checks }), } => (
id,
CurrentDependencyInfo::Running {
url,
version_spec,
health_checks,
},
),
}) })
.collect(), .collect(),
); );
for (dep, entry) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
if let Some(info) = dependencies.0.get(&dep) {
entry.as_current_dependents_mut().insert(id, info)?;
} else {
entry.as_current_dependents_mut().remove(id)?;
}
}
db.as_public_mut() db.as_public_mut()
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(id) .as_idx_mut(id)

View File

@@ -171,7 +171,6 @@ impl ServiceMap {
icon, icon,
last_backup: None, last_backup: None,
dependency_info: Default::default(), dependency_info: Default::default(),
current_dependents: Default::default(), // TODO: initialize
current_dependencies: Default::default(), current_dependencies: Default::default(),
interface_addresses: Default::default(), interface_addresses: Default::default(),
hosts: Default::default(), hosts: Default::default(),

View File

@@ -240,9 +240,9 @@ pub async fn verify_cifs(
ReadWrite, ReadWrite,
) )
.await?; .await?;
let embassy_os = recovery_info(guard.path()).await?; let start_os = recovery_info(guard.path()).await?;
guard.unmount().await?; guard.unmount().await?;
embassy_os.ok_or_else(|| Error::new(eyre!("No Backup Found"), crate::ErrorKind::NotFound)) start_os.ok_or_else(|| Error::new(eyre!("No Backup Found"), crate::ErrorKind::NotFound))
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@@ -256,8 +256,8 @@ pub enum RecoverySource {
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ExecuteParams { pub struct ExecuteParams {
embassy_logicalname: PathBuf, start_os_logicalname: PathBuf,
embassy_password: EncryptedWire, start_os_password: EncryptedWire,
recovery_source: Option<RecoverySource>, recovery_source: Option<RecoverySource>,
recovery_password: Option<EncryptedWire>, recovery_password: Option<EncryptedWire>,
} }
@@ -266,13 +266,13 @@ pub struct ExecuteParams {
pub async fn execute( pub async fn execute(
ctx: SetupContext, ctx: SetupContext,
ExecuteParams { ExecuteParams {
embassy_logicalname, start_os_logicalname,
embassy_password, start_os_password,
recovery_source, recovery_source,
recovery_password, recovery_password,
}: ExecuteParams, }: ExecuteParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
let embassy_password = match embassy_password.decrypt(&*ctx) { let start_os_password = match start_os_password.decrypt(&*ctx) {
Some(a) => a, Some(a) => a,
None => { None => {
return Err(Error::new( return Err(Error::new(
@@ -311,8 +311,8 @@ pub async fn execute(
let ctx = ctx.clone(); let ctx = ctx.clone();
match execute_inner( match execute_inner(
ctx.clone(), ctx.clone(),
embassy_logicalname, start_os_logicalname,
embassy_password, start_os_password,
recovery_source, recovery_source,
recovery_password, recovery_password,
) )
@@ -375,8 +375,8 @@ pub async fn exit(ctx: SetupContext) -> Result<(), Error> {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn execute_inner( pub async fn execute_inner(
ctx: SetupContext, ctx: SetupContext,
embassy_logicalname: PathBuf, start_os_logicalname: PathBuf,
embassy_password: String, start_os_password: String,
recovery_source: Option<RecoverySource>, recovery_source: Option<RecoverySource>,
recovery_password: Option<String>, recovery_password: Option<String>,
) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> { ) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
@@ -387,7 +387,7 @@ pub async fn execute_inner(
}; };
let guid = Arc::new( let guid = Arc::new(
crate::disk::main::create( crate::disk::main::create(
&[embassy_logicalname], &[start_os_logicalname],
&pvscan().await?, &pvscan().await?,
&ctx.datadir, &ctx.datadir,
encryption_password, encryption_password,
@@ -403,20 +403,20 @@ pub async fn execute_inner(
.await?; .await?;
if let Some(RecoverySource::Backup { target }) = recovery_source { if let Some(RecoverySource::Backup { target }) = recovery_source {
recover(ctx, guid, embassy_password, target, recovery_password).await recover(ctx, guid, start_os_password, target, recovery_password).await
} else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source { } else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source {
migrate(ctx, guid, &old_guid, embassy_password).await migrate(ctx, guid, &old_guid, start_os_password).await
} else { } else {
let (hostname, tor_addr, root_ca) = fresh_setup(&ctx, &embassy_password).await?; let (hostname, tor_addr, root_ca) = fresh_setup(&ctx, &start_os_password).await?;
Ok((guid, hostname, tor_addr, root_ca)) Ok((guid, hostname, tor_addr, root_ca))
} }
} }
async fn fresh_setup( async fn fresh_setup(
ctx: &SetupContext, ctx: &SetupContext,
embassy_password: &str, start_os_password: &str,
) -> Result<(Hostname, OnionAddressV3, X509), Error> { ) -> Result<(Hostname, OnionAddressV3, X509), Error> {
let account = AccountInfo::new(embassy_password, root_ca_start_time().await?)?; let account = AccountInfo::new(start_os_password, root_ca_start_time().await?)?;
let db = ctx.db().await?; let db = ctx.db().await?;
db.put(&ROOT, &Database::init(&account)?).await?; db.put(&ROOT, &Database::init(&account)?).await?;
drop(db); drop(db);
@@ -432,7 +432,7 @@ async fn fresh_setup(
async fn recover( async fn recover(
ctx: SetupContext, ctx: SetupContext,
guid: Arc<String>, guid: Arc<String>,
embassy_password: String, start_os_password: String,
recovery_source: BackupTargetFS, recovery_source: BackupTargetFS,
recovery_password: Option<String>, recovery_password: Option<String>,
) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> { ) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
@@ -440,7 +440,7 @@ async fn recover(
recover_full_embassy( recover_full_embassy(
ctx, ctx,
guid.clone(), guid.clone(),
embassy_password, start_os_password,
recovery_source, recovery_source,
recovery_password, recovery_password,
) )
@@ -452,7 +452,7 @@ async fn migrate(
ctx: SetupContext, ctx: SetupContext,
guid: Arc<String>, guid: Arc<String>,
old_guid: &str, old_guid: &str,
embassy_password: String, start_os_password: String,
) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> { ) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
*ctx.setup_status.write().await = Some(Ok(SetupStatus { *ctx.setup_status.write().await = Some(Ok(SetupStatus {
bytes_transferred: 0, bytes_transferred: 0,
@@ -537,7 +537,7 @@ async fn migrate(
} => res, } => res,
} }
let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(embassy_password)).await?; let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(start_os_password)).await?;
crate::disk::main::export(&old_guid, "/media/embassy/migrate").await?; crate::disk::main::export(&old_guid, "/media/embassy/migrate").await?;

View File

@@ -1,15 +1,9 @@
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub use helpers::script_dir; pub use helpers::script_dir;
use imbl_value::InternedString;
pub use models::VolumeId; pub use models::VolumeId;
use models::{HostId, PackageId}; use models::{HostId, PackageId};
use serde::{Deserialize, Serialize};
use tracing::instrument;
use crate::context::RpcContext;
use crate::net::PACKAGE_CERT_PATH; use crate::net::PACKAGE_CERT_PATH;
use crate::prelude::*; use crate::prelude::*;
use crate::util::Version; use crate::util::Version;
@@ -17,72 +11,6 @@ use crate::util::Version;
pub const PKG_VOLUME_DIR: &str = "package-data/volumes"; pub const PKG_VOLUME_DIR: &str = "package-data/volumes";
pub const BACKUP_DIR: &str = "/media/embassy/backups"; pub const BACKUP_DIR: &str = "/media/embassy/backups";
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Volumes(BTreeMap<VolumeId, Volume>);
impl Volumes {
#[instrument(skip_all)]
pub async fn install(
&self,
ctx: &RpcContext,
pkg_id: &PackageId,
version: &Version,
) -> Result<(), Error> {
for (volume_id, volume) in &self.0 {
volume
.install(&ctx.datadir, pkg_id, version, volume_id)
.await?; // TODO: concurrent?
}
Ok(())
}
pub fn get_path_for(
&self,
path: &PathBuf,
pkg_id: &PackageId,
version: &Version,
volume_id: &VolumeId,
) -> Option<PathBuf> {
self.0
.get(volume_id)
.map(|volume| volume.path_for(path, pkg_id, version, volume_id))
}
pub fn to_readonly(&self) -> Self {
Volumes(
self.0
.iter()
.map(|(id, volume)| {
let mut volume = volume.clone();
volume.set_readonly();
(id.clone(), volume)
})
.collect(),
)
}
}
impl Deref for Volumes {
type Target = BTreeMap<VolumeId, Volume>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Volumes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Map for Volumes {
type Key = VolumeId;
type Value = Volume;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
match key {
VolumeId::Custom(id) => Ok(id.clone().into()),
_ => Self::key_str(key).map(|s| InternedString::intern(s.as_ref())),
}
}
}
pub fn data_dir<P: AsRef<Path>>(datadir: P, pkg_id: &PackageId, volume_id: &VolumeId) -> PathBuf { pub fn data_dir<P: AsRef<Path>>(datadir: P, pkg_id: &PackageId, volume_id: &VolumeId) -> PathBuf {
datadir datadir
.as_ref() .as_ref()
@@ -108,114 +36,3 @@ pub fn backup_dir(pkg_id: &PackageId) -> PathBuf {
pub fn cert_dir(pkg_id: &PackageId, host_id: &HostId) -> PathBuf { pub fn cert_dir(pkg_id: &PackageId, host_id: &HostId) -> PathBuf {
Path::new(PACKAGE_CERT_PATH).join(pkg_id).join(host_id) Path::new(PACKAGE_CERT_PATH).join(pkg_id).join(host_id)
} }
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum Volume {
#[serde(rename_all = "camelCase")]
Data {
#[serde(skip)]
readonly: bool,
},
#[serde(rename_all = "camelCase")]
Assets {},
#[serde(rename_all = "camelCase")]
Pointer {
package_id: PackageId,
volume_id: VolumeId,
path: PathBuf,
readonly: bool,
},
#[serde(rename_all = "camelCase")]
Certificate { interface_id: HostId },
#[serde(rename_all = "camelCase")]
Backup { readonly: bool },
}
impl Volume {
pub async fn install(
&self,
path: &PathBuf,
pkg_id: &PackageId,
version: &Version,
volume_id: &VolumeId,
) -> Result<(), Error> {
match self {
Volume::Data { .. } => {
tokio::fs::create_dir_all(self.path_for(path, pkg_id, version, volume_id)).await?;
}
_ => (),
}
Ok(())
}
pub fn path_for(
&self,
data_dir_path: impl AsRef<Path>,
pkg_id: &PackageId,
version: &Version,
volume_id: &VolumeId,
) -> PathBuf {
match self {
Volume::Data { .. } => data_dir(&data_dir_path, pkg_id, volume_id),
Volume::Assets {} => asset_dir(&data_dir_path, pkg_id, version).join(volume_id),
Volume::Pointer {
package_id,
volume_id,
path,
..
} => data_dir(&data_dir_path, package_id, volume_id).join(if path.is_absolute() {
path.strip_prefix("/").unwrap()
} else {
path.as_ref()
}),
Volume::Certificate { interface_id } => cert_dir(pkg_id, &interface_id),
Volume::Backup { .. } => backup_dir(pkg_id),
}
}
pub fn pointer_path(&self, data_dir_path: impl AsRef<Path>) -> Option<PathBuf> {
if let Volume::Pointer {
path,
package_id,
volume_id,
..
} = self
{
Some(
data_dir(data_dir_path.as_ref(), package_id, volume_id).join(
if path.is_absolute() {
path.strip_prefix("/").unwrap()
} else {
path.as_ref()
},
),
)
} else {
None
}
}
pub fn set_readonly(&mut self) {
match self {
Volume::Data { readonly } => {
*readonly = true;
}
Volume::Pointer { readonly, .. } => {
*readonly = true;
}
Volume::Backup { readonly } => {
*readonly = true;
}
_ => (),
}
}
pub fn readonly(&self) -> bool {
match self {
Volume::Data { readonly } => *readonly,
Volume::Assets {} => true,
Volume::Pointer { readonly, .. } => *readonly,
Volume::Certificate { .. } => true,
Volume::Backup { readonly } => *readonly,
}
}
}

View File

@@ -172,7 +172,7 @@ export type ActionMetadata = {
warning: string | null warning: string | null
input: InputSpec input: InputSpec
disabled: boolean disabled: boolean
allowedStatuses: "only-running" | "only-stopped" | "any" allowedStatuses: "onlyRunning" | "onlyStopped" | "any"
/** /**
* So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions * So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions
*/ */

30
web/package-lock.json generated
View File

@@ -8,17 +8,17 @@
"name": "startos-ui", "name": "startos-ui",
"version": "0.3.5.1", "version": "0.3.5.1",
"dependencies": { "dependencies": {
"@angular/animations": "^17.0.6", "@angular/animations": "^17.3.1",
"@angular/cdk": "^17.0.3", "@angular/cdk": "^17.3.1",
"@angular/common": "^17.0.6", "@angular/common": "^17.3.1",
"@angular/compiler": "^17.0.6", "@angular/compiler": "^17.3.1",
"@angular/core": "^17.0.6", "@angular/core": "^17.3.1",
"@angular/forms": "^17.0.6", "@angular/forms": "^17.3.1",
"@angular/platform-browser": "^17.0.6", "@angular/platform-browser": "^17.3.1",
"@angular/platform-browser-dynamic": "^17.0.6", "@angular/platform-browser-dynamic": "^17.3.1",
"@angular/pwa": "^17.0.6", "@angular/pwa": "^17.3.1",
"@angular/router": "^17.0.6", "@angular/router": "^17.3.1",
"@angular/service-worker": "^17.0.6", "@angular/service-worker": "^17.3.1",
"@ionic/angular": "^6.1.15", "@ionic/angular": "^6.1.15",
"@materia-ui/ngx-monaco-editor": "^6.0.0", "@materia-ui/ngx-monaco-editor": "^6.0.0",
"@start9labs/argon2": "^0.1.0", "@start9labs/argon2": "^0.1.0",
@@ -62,10 +62,10 @@
"zone.js": "^0.14.2" "zone.js": "^0.14.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.0.6", "@angular-devkit/build-angular": "^17.3.1",
"@angular/cli": "^17.0.6", "@angular/cli": "^17.3.1",
"@angular/compiler-cli": "^17.0.6", "@angular/compiler-cli": "^17.3.1",
"@angular/language-service": "^17.0.6", "@angular/language-service": "^17.3.1",
"@ionic/cli": "^6.19.0", "@ionic/cli": "^6.19.0",
"@types/dompurify": "^2.3.3", "@types/dompurify": "^2.3.3",
"@types/estree": "^0.0.51", "@types/estree": "^0.0.51",

View File

@@ -179,13 +179,13 @@ export class ServiceRoute {
depErrors, depErrors,
) )
const depInfo = pkg.dependencyInfo[depId] const { title, icon, versionSpec } = pkg.currentDependencies[depId]
return { return {
id: depId, id: depId,
version: pkg.currentDependencies[depId].versionRange, version: versionSpec,
title: depInfo?.title || depId, title,
icon: depInfo?.icon || '', icon,
errorText: errorText errorText: errorText
? `${errorText}. ${pkgManifest.title} will not work as expected.` ? `${errorText}. ${pkgManifest.title} will not work as expected.`
: '', : '',
@@ -252,7 +252,7 @@ export class ServiceRoute {
return this.installDep(pkg, pkgManifest, depId) return this.installDep(pkg, pkgManifest, depId)
case 'configure': case 'configure':
return this.formDialog.open<PackageConfigData>(ServiceConfigModal, { return this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
label: `${pkg.dependencyInfo[depId].title} config`, label: `${pkg.currentDependencies[depId].title} config`,
data: { data: {
pkgId: depId, pkgId: depId,
dependentInfo: pkgManifest, dependentInfo: pkgManifest,
@@ -269,7 +269,7 @@ export class ServiceRoute {
const dependentInfo: DependentInfo = { const dependentInfo: DependentInfo = {
id: manifest.id, id: manifest.id,
title: manifest.title, title: manifest.title,
version: pkg.currentDependencies[depId].versionRange, version: pkg.currentDependencies[depId].versionSpec,
} }
const navigationExtras: NavigationExtras = { const navigationExtras: NavigationExtras = {
state: { dependentInfo }, state: { dependentInfo },

View File

@@ -1493,7 +1493,6 @@ export module Mock {
}, },
}, },
currentDependencies: {}, currentDependencies: {},
dependencyInfo: {},
marketplaceUrl: 'https://registry.start9.com/', marketplaceUrl: 'https://registry.start9.com/',
developerKey: 'developer-key', developerKey: 'developer-key',
outboundProxy: null, outboundProxy: null,
@@ -1624,15 +1623,13 @@ export module Mock {
}, },
}, },
currentDependencies: { currentDependencies: {
bitcoind: {
versionRange: '>=26.0.0',
healthChecks: [],
},
},
dependencyInfo: {
bitcoind: { bitcoind: {
title: Mock.MockManifestBitcoind.title, title: Mock.MockManifestBitcoind.title,
icon: 'assets/img/service-icons/bitcoind.svg', icon: 'assets/img/service-icons/bitcoind.svg',
kind: 'running',
registryUrl: '',
versionSpec: '>=26.0.0',
healthChecks: [],
}, },
}, },
marketplaceUrl: 'https://registry.start9.com/', marketplaceUrl: 'https://registry.start9.com/',
@@ -1871,23 +1868,21 @@ export module Mock {
}, },
}, },
currentDependencies: { currentDependencies: {
bitcoind: {
versionRange: '>=26.0.0',
healthChecks: [],
},
'btc-rpc-proxy': {
versionRange: '>2.0.0', // @TODO
healthChecks: [],
},
},
dependencyInfo: {
bitcoind: { bitcoind: {
title: Mock.MockManifestBitcoind.title, title: Mock.MockManifestBitcoind.title,
icon: 'assets/img/service-icons/bitcoind.svg', icon: 'assets/img/service-icons/bitcoind.svg',
kind: 'running',
registryUrl: 'https://registry.start9.com',
versionSpec: '>=26.0.0',
healthChecks: [],
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
title: Mock.MockManifestBitcoinProxy.title, title: Mock.MockManifestBitcoinProxy.title,
icon: 'assets/img/service-icons/btc-rpc-proxy.png', icon: 'assets/img/service-icons/btc-rpc-proxy.png',
kind: 'exists',
registryUrl: 'https://community-registry.start9.com',
versionSpec: '>2.0.0', // @TODO
healthChecks: [],
}, },
}, },
marketplaceUrl: 'https://registry.start9.com/', marketplaceUrl: 'https://registry.start9.com/',

View File

@@ -402,7 +402,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
currentDependencies: {}, currentDependencies: {},
dependencyInfo: {},
marketplaceUrl: 'https://registry.start9.com/', marketplaceUrl: 'https://registry.start9.com/',
developerKey: 'developer-key', developerKey: 'developer-key',
outboundProxy: null, outboundProxy: null,
@@ -639,23 +638,21 @@ export const mockPatchData: DataModel = {
}, },
}, },
currentDependencies: { currentDependencies: {
bitcoind: {
versionRange: '>=26.0.0',
healthChecks: [],
},
'btc-rpc-proxy': {
versionRange: '>2.0.0',
healthChecks: [],
},
},
dependencyInfo: {
bitcoind: { bitcoind: {
title: 'Bitcoin Core', title: 'Bitcoin Core',
icon: 'assets/img/service-icons/bitcoind.svg', icon: 'assets/img/service-icons/bitcoind.svg',
kind: 'running',
registryUrl: 'https://registry.start9.com',
versionSpec: '>=26.0.0',
healthChecks: [],
}, },
'btc-rpc-proxy': { 'btc-rpc-proxy': {
title: 'Bitcoin Proxy', title: 'Bitcoin Proxy',
icon: 'assets/img/service-icons/btc-rpc-proxy.png', icon: 'assets/img/service-icons/btc-rpc-proxy.png',
kind: 'running',
registryUrl: 'https://community-registry.start9.com',
versionSpec: '>2.0.0',
healthChecks: [],
}, },
}, },
marketplaceUrl: 'https://registry.start9.com/', marketplaceUrl: 'https://registry.start9.com/',

View File

@@ -88,14 +88,14 @@ export class DepErrorService {
} }
} }
const versionRange = pkg.currentDependencies[depId].versionRange const versionSpec = pkg.currentDependencies[depId].versionSpec
const depManifest = dep.stateInfo.manifest const depManifest = dep.stateInfo.manifest
// incorrect version // incorrect version
if (!this.emver.satisfies(depManifest.version, versionRange)) { if (!this.emver.satisfies(depManifest.version, versionSpec)) {
return { return {
type: DependencyErrorType.IncorrectVersion, type: DependencyErrorType.IncorrectVersion,
expected: versionRange, expected: versionSpec,
received: depManifest.version, received: depManifest.version,
} }
} }

View File

@@ -154,13 +154,7 @@ export type PackageDataEntry<T extends StateInfo = StateInfo> = {
status: Status status: Status
actions: Record<string, T.ActionMetadata> actions: Record<string, T.ActionMetadata>
lastBackup: string | null lastBackup: string | null
currentDependencies: { [id: string]: CurrentDependencyInfo } currentDependencies: Record<string, CurrentDependencyInfo>
dependencyInfo: {
[id: string]: {
title: string
icon: Url
}
}
serviceInterfaces: Record<string, T.ServiceInterfaceWithHostInfo> serviceInterfaces: Record<string, T.ServiceInterfaceWithHostInfo>
marketplaceUrl: string | null marketplaceUrl: string | null
developerKey: string developerKey: string
@@ -197,7 +191,11 @@ export enum PackageState {
} }
export interface CurrentDependencyInfo { export interface CurrentDependencyInfo {
versionRange: string title: string
icon: string
kind: 'exists' | 'running'
registryUrl: string
versionSpec: string
healthChecks: string[] // array of health check IDs healthChecks: string[] // array of health check IDs
} }

View File

@@ -12,8 +12,7 @@ export function dryUpdate(
pkg => pkg =>
Object.keys(pkg.currentDependencies || {}).some( Object.keys(pkg.currentDependencies || {}).some(
pkgId => pkgId === id, pkgId => pkgId === id,
) && ) && !emver.satisfies(version, pkg.currentDependencies[id].versionSpec),
!emver.satisfies(version, pkg.currentDependencies[id].versionRange),
) )
.map(pkg => getManifest(pkg).title) .map(pkg => getManifest(pkg).title)
} }