feat: Dependencies

This commit is contained in:
BluJ
2023-05-10 15:47:28 -06:00
parent 5536dfb55f
commit 33da2322b0
9 changed files with 329 additions and 36 deletions

View File

@@ -47,6 +47,7 @@ import { setupMain } from "./mainFn"
import { defaultTrigger } from "./trigger/defaultTrigger"
import { changeOnFirstSuccess, cooldownTrigger } from "./trigger"
import setupConfig, { Read, Save } from "./config/setupConfig"
import { setupDependencyMounts } from "./dependency/setupDependencyMounts"
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =
@@ -254,6 +255,7 @@ export class StartSdk<Manifest extends SDKManifest, Store, Vault> {
_configSpec: ConfigSpec,
fn: Save<Store, Vault, ConfigSpec, Manifest>,
) => fn,
setupDependencyMounts,
setupInit: (
migrations: Migrations<Store, Vault>,
install: Install<Store, Vault>,

View File

@@ -0,0 +1,43 @@
import { Effects } from "../types"
import {
Path,
ManifestId,
VolumeName,
NamedPath,
matchPath,
} from "./setupDependencyMounts"
export type MountDependenciesOut<A> =
// prettier-ignore
A extends Path ? string : A extends Record<string, unknown> ? {
[P in keyof A]: MountDependenciesOut<A[P]>;
} : never
export async function mountDependencies<
In extends
| Record<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(effects: Effects, value: In): Promise<MountDependenciesOut<In>> {
if (matchPath.test(value)) {
const mountPath = `${value.manifest.id}/${value.volume}/${value.name}`
return (await effects.mount({
location: {
path: mountPath,
},
target: {
packageId: value.manifest.id,
path: value.path,
readonly: value.readonly,
volumeId: value.volume,
},
})) as MountDependenciesOut<In>
}
return Object.fromEntries(
Object.entries(value).map(([key, value]) => [
key,
mountDependencies(effects, value),
]),
) as Record<string, unknown> as MountDependenciesOut<In>
}

View File

@@ -0,0 +1,70 @@
import { boolean, object, string } from "ts-matches"
import { SDKManifest } from "../manifest/ManifestTypes"
import { deepMerge } from "../util/deepMerge"
export type VolumeName = string
export type NamedPath = string
export type ManifestId = string
export const matchPath = object({
name: string,
volume: string,
path: string,
manifest: object({
id: string,
}),
readonly: boolean,
})
export type Path = typeof matchPath._TYPE
export type BuildPath<M extends Path> = {
[PId in M["manifest"]["id"]]: {
[V in M["volume"]]: {
[N in M["name"]]: M
}
}
}
type ValidIfNotInNested<
Building,
M extends Path,
> = Building extends BuildPath<M> ? never : M
class SetupDependencyMounts<Building> {
private constructor(readonly building: Building) {}
static of() {
return new SetupDependencyMounts({})
}
addPath<
NamedPath extends string,
VolumeName extends string,
PathNamed extends string,
M extends SDKManifest,
>(
newPath: ValidIfNotInNested<
Building,
{
name: NamedPath
volume: VolumeName
path: PathNamed
manifest: M
readonly: boolean
}
>,
) {
const building = deepMerge(this.building, {
[newPath.manifest.id]: {
[newPath.volume]: {
[newPath.name]: newPath,
},
},
}) as Building & BuildPath<typeof newPath>
return new SetupDependencyMounts(building)
}
build() {
return this.building
}
}
export function setupDependencyMounts() {
return SetupDependencyMounts.of()
}

View File

@@ -14,64 +14,68 @@ export interface Container {
export type ManifestVersion = ValidEmVer
export interface SDKManifest {
export type SDKManifest = {
/** The package identifier used by the OS. This must be unique amongst all other known packages */
id: string
readonly id: string
/** A human readable service title */
title: string
readonly title: string
/** Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOs
* - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of
* the service
*/
version: ManifestVersion
readonly version: ManifestVersion
/** Release notes for the update - can be a string, paragraph or URL */
releaseNotes: string
readonly releaseNotes: string
/** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/
license: string // name of license
readonly license: string // name of license
/** A list of normie (hosted, SaaS, custodial, etc) services this services intends to replace */
replaces: string[]
readonly replaces: Readonly<string[]>
/** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this),
* any scripts necessary for configuration, backups, actions, or health checks (more below). This key
* must exist. But could be embedded into the source repository
*/
wrapperRepo: string
readonly wrapperRepo: string
/** The original project repository URL. There is no upstream repo in this example */
upstreamRepo: string
readonly upstreamRepo: string
/** URL to the support site / channel for the project. This key can be omitted if none exists, or it can link to the original project repository issues */
supportSite: string
readonly supportSite: string
/** URL to the marketing site for the project. If there is no marketing site, it can link to the original project repository */
marketingSite: string
readonly marketingSite: string
/** URL where users can donate to the upstream project */
donationUrl: string | null
readonly donationUrl: string | null
/**Human readable descriptions for the service. These are used throughout the StartOS user interface, primarily in the marketplace. */
description: {
readonly description: {
/**This is the first description visible to the user in the marketplace */
short: string
readonly short: string
/** This description will display with additional details in the service's individual marketplace page */
long: string
readonly long: string
}
/** These assets are static files necessary for packaging the service for Start9 (into an s9pk).
* Each value is a path to the specified asset. If an asset is missing from this list, or otherwise
* denoted, it will be defaulted to the values denoted below.
*/
assets: {
icon: string // file path
instructions: string // file path
license: string // file path
readonly assets: {
/** This is the file path for the icon that will be this packages icon on the ui */
readonly icon: string
/** Instructions path to be seen in the ui section of the package */
readonly instructions: string
/** license path */
readonly license: string
}
/** Defines the containers needed to run the main and mounted volumes */
containers: Record<string, Container>
readonly containers: Record<string, Container>
/** This denotes any data, asset, or pointer volumes that should be connected when the "docker run" command is invoked */
volumes: Record<string, "data" | "assets">
alerts: {
install: string | null
update: string | null
uninstall: string | null
restore: string | null
start: string | null
stop: string | null
readonly volumes: Record<string, "data" | "assets">
readonly alerts: {
readonly install: string | null
readonly update: string | null
readonly uninstall: string | null
readonly restore: string | null
readonly start: string | null
readonly stop: string | null
}
dependencies: Record<string, ManifestDependency>
readonly dependencies: Readonly<Record<string, ManifestDependency>>
}
export interface ManifestDependency {

View File

@@ -0,0 +1,139 @@
import { setupManifest } from "../manifest/setupManifest"
import { mountDependencies } from "../dependency/mountDependencies"
import {
BuildPath,
setupDependencyMounts,
} from "../dependency/setupDependencyMounts"
describe("mountDependencies", () => {
const clnManifest = setupManifest({
id: "cln",
title: "",
version: "1",
releaseNotes: "",
license: "",
replaces: [],
wrapperRepo: "",
upstreamRepo: "",
supportSite: "",
marketingSite: "",
donationUrl: null,
description: {
short: "",
long: "",
},
assets: {
icon: "",
instructions: "",
license: "",
},
containers: {},
volumes: { main: "data" },
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {},
})
const lndManifest = setupManifest({
id: "lnd",
title: "",
version: "1",
releaseNotes: "",
license: "",
replaces: [],
wrapperRepo: "",
upstreamRepo: "",
supportSite: "",
marketingSite: "",
donationUrl: null,
description: {
short: "",
long: "",
},
assets: {
icon: "",
instructions: "",
license: "",
},
containers: {},
volumes: {},
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {},
})
clnManifest.id
type test = BuildPath<{
name: "root"
manifest: typeof clnManifest
volume: "main"
path: "/"
readonly: true
}> extends BuildPath<{
name: "root"
manifest: typeof clnManifest
volume: "main2"
path: "/"
readonly: true
}>
? true
: false
test("Types work", () => {
const dependencyMounts = setupDependencyMounts()
.addPath({
name: "root",
volume: "main",
path: "/",
manifest: clnManifest,
readonly: true,
})
.addPath({
name: "root",
manifest: lndManifest,
volume: "main",
path: "/",
readonly: true,
})
.build()
;() => {
const test = mountDependencies(
null as any,
dependencyMounts,
) satisfies Promise<{
cln: {
main: {
root: string
}
}
lnd: {
main: {
root: string
}
}
}>
const test2 = mountDependencies(
null as any,
dependencyMounts.cln,
) satisfies Promise<{
main: { root: string }
}>
const test3 = mountDependencies(
null as any,
dependencyMounts.cln.main,
) satisfies Promise<{
root: string
}>
}
})
})

View File

@@ -383,7 +383,8 @@ export type Effects = {
mount(options: {
location: {
volumeId: string
/** If there is no volumeId then we mount to runMedia a special mounting location */
volumeId?: string
path: string
}
target: {
@@ -392,7 +393,7 @@ export type Effects = {
path: string
readonly: boolean
}
}): Promise<void>
}): Promise<string>
stopped(packageId?: string): Promise<boolean>

View File

@@ -16,7 +16,22 @@ import { DefaultString } from "../config/configTypes"
import { getDefaultString } from "./getDefaultString"
import { GetStore, getStore } from "../store/getStore"
import { GetVault, getVault } from "./getVault"
import {
MountDependenciesOut,
mountDependencies,
} from "../dependency/mountDependencies"
import {
ManifestId,
VolumeName,
NamedPath,
Path,
} from "../dependency/setupDependencyMounts"
// prettier-ignore
type skipFirstParam<A> =
A extends [any, ...infer B] ? B :
A extends [] ? [] :
never
export type Utils<Store, Vault, WrapperOverWrite = { const: never }> = {
createOrUpdateVault: (opts: {
key: string
@@ -67,6 +82,15 @@ export type Utils<Store, Vault, WrapperOverWrite = { const: never }> = {
networkBuilder: () => NetworkBuilder
torHostName: (id: string) => TorHostname
nullIfEmpty: typeof nullIfEmpty
mountDependencies: <
In extends
| Record<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(
value: In,
) => Promise<MountDependenciesOut<In>>
}
export const utils = <
Store = never,
@@ -128,5 +152,14 @@ export const utils = <
set: (key: keyof Vault & string, value: string) =>
effects.vault.set({ key, value }),
},
mountDependencies: <
In extends
| Record<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(
value: In,
) => mountDependencies(effects, value),
})
function noop(): void {}

10
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"ts-node": "^10.9.1",
"tsc-multi": "^0.6.1",
"tsconfig-paths": "^3.14.2",
"typescript": "^5.0.0",
"vitest": "^0.29.2"
}
},
@@ -4878,17 +4879,16 @@
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=12.20"
}
},
"node_modules/ufo": {

View File

@@ -37,6 +37,7 @@
"ts-node": "^10.9.1",
"tsc-multi": "^0.6.1",
"tsconfig-paths": "^3.14.2",
"typescript": "^5.0.4",
"vitest": "^0.29.2"
},
"declaration": true