diff --git a/compat/getConfig.ts b/compat/getConfig.ts new file mode 100644 index 0000000..a26dd5e --- /dev/null +++ b/compat/getConfig.ts @@ -0,0 +1,29 @@ + +import { YAML } from "../dependencies.ts"; +import { matches } from "../dependencies.ts"; +import { ExpectedExports } from "../types.ts"; +import { ConfigSpec } from "../types.ts"; + +const { any, string, dictionary } = matches; + +const matchConfig = dictionary([string, any]); + +export const getConfig = (spec: ConfigSpec): ExpectedExports.getConfig => async (effects) => { + const config = await effects + .readFile({ + path: "start9/config.yaml", + volumeId: "main", + }) + .then((x) => YAML.parse(x)) + .then((x) => matchConfig.unsafeCast(x)) + .catch((e) => { + effects.info(`Got error ${e} while trying to read the config`); + return undefined; + }); + return { + result: { + config, + spec, + }, + }; +}; diff --git a/compat/mod.ts b/compat/mod.ts new file mode 100644 index 0000000..7c66a2a --- /dev/null +++ b/compat/mod.ts @@ -0,0 +1,3 @@ +export { properties } from "./properties.ts"; +export { setConfig } from "./setConfig.ts"; +export { getConfig } from "./getConfig.ts"; \ No newline at end of file diff --git a/compat/properties.ts b/compat/properties.ts new file mode 100644 index 0000000..5de57d1 --- /dev/null +++ b/compat/properties.ts @@ -0,0 +1,38 @@ +import { YAML } from "../dependencies.ts"; +import { exists } from "../exists.ts"; +import { ResultType, Properties, ExpectedExports, Effects } from "../types.ts"; + + +// deno-lint-ignore no-explicit-any +const asResult = (result: any) => ({ result: result as Properties }) +const noPropertiesFound: ResultType = { + result: { + version: 2, + data: { + "Not Ready": { + type: "string", + value: "Could not find properties. The service might still be starting", + qr: false, + copyable: false, + masked: false, + description: "Fallback Message When Properties could not be found" + } + } + } +} as const +/** + * Default will pull from a file (start9/stats.yaml) expected to be made on the main volume + * @param effects + * @returns + */ +export const properties: ExpectedExports.properties = async ( + effects: Effects, +) => { + if (await exists(effects, { path: "start9/stats.yaml", volumeId: "main" }) === false) { + return noPropertiesFound; + } + return await effects.readFile({ + path: "start9/stats.yaml", + volumeId: "main", + }).then(YAML.parse).then(asResult) +}; diff --git a/compat/setConfig.ts b/compat/setConfig.ts new file mode 100644 index 0000000..aa34437 --- /dev/null +++ b/compat/setConfig.ts @@ -0,0 +1,31 @@ +import { YAML } from "../dependencies.ts"; +import { ExpectedExports, Effects, Config, SetResult, DependsOn } from "../types.ts"; + +/** + * Will set the config to the default start9/config.yaml + * @param effects + * @param newConfig Config to be written to start9/config.yaml + * @param depends_on This would be the depends on for condition depends_on + * @returns + */ +export const setConfig: ExpectedExports.setConfig = async ( + effects: Effects, + newConfig: Config, + depends_on: DependsOn = {} +) => { + await effects.createDir({ + path: "start9", + volumeId: "main", + }); + await effects.writeFile({ + path: "start9/config.yaml", + toWrite: YAML.stringify(newConfig), + volumeId: "main", + }); + + const result: SetResult = { + signal: "SIGTERM", + "depends-on": {}, + }; + return { result, depends_on }; +}; diff --git a/dependencies.ts b/dependencies.ts new file mode 100644 index 0000000..75ea292 --- /dev/null +++ b/dependencies.ts @@ -0,0 +1,2 @@ +export * as matches from "https://deno.land/x/ts_matches@v5.1.8/mod.ts"; +export * as YAML from "https://deno.land/std@0.140.0/encoding/yaml.ts"; \ No newline at end of file diff --git a/exists.ts b/exists.ts new file mode 100644 index 0000000..69443aa --- /dev/null +++ b/exists.ts @@ -0,0 +1,4 @@ +import { Effects } from "./types.ts"; + +/** Used to check if the file exists before hand */ +export const exists = (effects: Effects, props: { path: string, volumeId: string }) => effects.metadata(props).then(_ => true, _ => false); \ No newline at end of file diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..654e9de --- /dev/null +++ b/mod.ts @@ -0,0 +1,5 @@ +export { matches, YAML } from './dependencies.ts' +export * as types from './types.ts' + +export { exists } from './exists.ts' +export * as compat from './compat/mod.ts' \ No newline at end of file diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..0e8546f --- /dev/null +++ b/types.ts @@ -0,0 +1,385 @@ +// deno-lint-ignore no-namespace +export namespace ExpectedExports { + /** Set configuration is called after we have modified and saved the configuration in the embassy ui. Use this to make a file for the docker to read from for configuration. */ + export type setConfig = ( + effects: Effects, + input: Config, + ) => Promise>; + /** Get configuration returns a shape that describes the format that the embassy ui will generate, and later send to the set config */ + export type getConfig = (effects: Effects) => Promise>; + /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */ + export type dependencies = Dependencies; + /** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */ + export type properties = ( + effects: Effects, + ) => Promise>; + + export type health = { + /** Should be the health check id */ + [id: string]: ( + effects: Effects, + dateMs: number, + ) => Promise>; + }; + export type migration = ( + effects: Effects, + version: string, + ) => Promise>; +} + + +/** Used to reach out from the pure js runtime */ +export type Effects = { + /** Usable when not sandboxed */ + writeFile( + input: { path: string; volumeId: string; toWrite: string }, + ): Promise; + readFile(input: { volumeId: string; path: string }): Promise; + metadata(input: { volumeId: string; path: string }): Promise; + /** Create a directory. Usable when not sandboxed */ + createDir(input: { volumeId: string; path: string }): Promise; + /** Remove a directory. Usable when not sandboxed */ + removeDir(input: { volumeId: string; path: string }): Promise; + removeFile(input: { volumeId: string; path: string }): Promise; + + /** Write a json file into an object. Usable when not sandboxed */ + writeJsonFile( + input: { volumeId: string; path: string; toWrite: Record }, + ): Promise; + + /** Read a json file into an object */ + readJsonFile(input: { volumeId: string; path: string }): Promise>; + + /** Log at the trace level */ + trace(whatToPrint: string): void; + /** Log at the warn level */ + warn(whatToPrint: string): void; + /** Log at the error level */ + error(whatToPrint: string): void; + /** Log at the debug level */ + debug(whatToPrint: string): void; + /** Log at the info level */ + info(whatToPrint: string): void; + + /** Sandbox mode lets us read but not write */ + is_sandboxed(): boolean; + + exists(input: { volumeId: string; path: string }): Promise; + +}; +export type Metadata = { + fileType: string, + isDir: boolean, + isFile: boolean, + isSymlink: boolean, + len: number, + modified?: Date, + accessed?: Date, + created?: Date, + readonly: boolean, + uid: number, + gid: number, + mode: number +} + +export type MigrationRes = { + configured: boolean; +}; + +export type ActionResult = { + version: "0"; + message: string; + value?: string; + copyable: boolean; + qr: boolean; +}; + +export type ConfigRes = { + /** This should be the previous config, that way during set config we start with the previous */ + config?: Config; + /** Shape that is describing the form in the ui */ + spec: ConfigSpec; +}; +export type Config = { + [propertyName: string]: unknown; +}; + +export type ConfigSpec = { + /** Given a config value, define what it should render with the following spec */ + [configValue: string]: ValueSpecAny; +}; +export type WithDefault = T & { + default?: Default; +}; + +export type WithDescription = T & { + description?: string; + name: string; + warning?: string; +}; + +export type ListSpec = { + spec: T; + range: string; +}; + +export type Tag = V & { + type: T; +}; + +export type Subtype = V & { + subtype: T; +}; + +export type Target = V & { + target: T; +}; + +export type UniqueBy = + | { + any: UniqueBy[]; + } + | string + | null; + +export type WithNullable = T & { + nullable: boolean; +}; +export type DefaultString = + | string + | { + /** The chars available for the randome generation */ + charset?: string; + /** Length that we generate to */ + len: number; + }; + +export type ValueSpecString = + & ( + // deno-lint-ignore ban-types + | {} + | { + pattern: string; + "pattern-description": string; + } + ) + & { + copyable?: boolean; + masked?: boolean; + placeholder?: string; + }; +export type ValueSpecNumber = { + /** Something like [3,6] or [0, *) */ + range?: string; + integral?: boolean; + /** Used a description of the units */ + units?: string; + placeholder?: number; +}; +export type ValueSpecBoolean = Record; +export type ValueSpecAny = + | Tag<"boolean", WithDescription>> + | Tag< + "string", + WithDescription, DefaultString>> + > + | Tag< + "number", + WithDescription, number>> + > + | Tag< + "enum", + WithDescription< + WithDefault< + { + values: string[]; + "value-names": { + [key: string]: string; + }; + }, + string + > + > + > + | Tag<"list", ValueSpecList> + | Tag<"object", WithDescription>> + | Tag<"union", WithDescription>> + | Tag< + "pointer", + WithDescription< + | Subtype< + "package", + | Target< + "tor-key", + { + "package-id": string; + interface: string; + } + > + | Target< + "tor-address", + { + "package-id": string; + interface: string; + } + > + | Target< + "lan-address", + { + "package-id": string; + interface: string; + } + > + | Target< + "config", + { + "package-id": string; + selector: string; + multi: boolean; + } + > + > + | Subtype<"system", Record> + > + >; +export type ValueSpecUnion = { + /** What tag for the specification, for tag unions */ + tag: { + id: string; + name: string; + description?: string; + "variant-names": { + [key: string]: string; + }; + }; + /** The possible enum values */ + variants: { + [key: string]: ConfigSpec; + }; + "display-as"?: string; + "unique-by"?: UniqueBy; +}; +export type ValueSpecObject = { + spec: ConfigSpec; + "display-as"?: string; + "unique-by"?: UniqueBy; +}; +export type ValueSpecList = + | Subtype< + "boolean", + WithDescription, boolean[]>> + > + | Subtype< + "string", + WithDescription, string[]>> + > + | Subtype< + "number", + WithDescription, number[]>> + > + | Subtype< + "enum", + WithDescription< + WithDefault< + ListSpec< + ValueSpecEnum + >, + string[] + > + > + > + | Subtype< + "object", + WithDescription, Record[]>> + > + | Subtype< + "union", + WithDescription, string[]>> + >; +export type ValueSpecEnum = { + values: string[]; + "value-names": { [key: string]: string }; +}; + +export type SetResult = { + /** These are the unix process signals */ + signal: + | "SIGTERM" + | "SIGHUP" + | "SIGINT" + | "SIGQUIT" + | "SIGILL" + | "SIGTRAP" + | "SIGABRT" + | "SIGBUS" + | "SIGFPE" + | "SIGKILL" + | "SIGUSR1" + | "SIGSEGV" + | "SIGUSR2" + | "SIGPIPE" + | "SIGALRM" + | "SIGSTKFLT" + | "SIGCHLD" + | "SIGCONT" + | "SIGSTOP" + | "SIGTSTP" + | "SIGTTIN" + | "SIGTTOU" + | "SIGURG" + | "SIGXCPU" + | "SIGXFSZ" + | "SIGVTALRM" + | "SIGPROF" + | "SIGWINCH" + | "SIGIO" + | "SIGPWR" + | "SIGSYS" + | "SIGEMT" + | "SIGINFO"; + "depends-on": DependsOn +}; + +export type DependsOn = { + [packageId: string]: string[]; +}; + +export type KnownError = { error: string } | { + "error-code": [number, string] | readonly [number, string]; +}; +export type ResultType = KnownError | { result: T }; + +export type PackagePropertiesV2 = { + [name: string]: PackagePropertyObject | PackagePropertyString; +}; +export type PackagePropertyString = { + type: "string"; + description?: string; + value: string; + /** Let's the ui make this copyable button */ + copyable?: boolean; + /** Let the ui create a qr for this field */ + qr?: boolean; + /** Hiding the value unless toggled off for field */ + masked?: boolean; +}; +export type PackagePropertyObject = { + value: PackagePropertiesV2; + type: "object"; + description: string; +}; + +export type Properties = { + version: 2; + data: PackagePropertiesV2; +}; + +export type Dependencies = { + /** Id is the id of the package, should be the same as the manifest */ + [id: string]: { + /** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */ + check(effects: Effects, input: Config): Promise>; + /** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */ + autoConfigure(effects: Effects, input: Config): Promise>; + }; +}; \ No newline at end of file