mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
Host,
|
||||
ExportServiceInterfaceParams,
|
||||
ServiceInterface,
|
||||
ActionRequest,
|
||||
RequestActionParams,
|
||||
MainStatus,
|
||||
} from "./osBindings"
|
||||
|
||||
@@ -14,7 +14,7 @@ export type Run<
|
||||
> = (options: {
|
||||
effects: T.Effects
|
||||
input: ExtractInputSpecType<A> & Record<string, any>
|
||||
}) => Promise<T.ActionResult | null | void | undefined>
|
||||
}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined>
|
||||
export type GetInput<
|
||||
A extends
|
||||
| Record<string, any>
|
||||
|
||||
@@ -3,11 +3,35 @@ import type { ActionVisibility } from "./ActionVisibility"
|
||||
import type { AllowedStatuses } from "./AllowedStatuses"
|
||||
|
||||
export type ActionMetadata = {
|
||||
/**
|
||||
* A human-readable name
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* A detailed description of what the action will do
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* Presents as an alert prior to executing the action. Should be used sparingly but important if the action could have harmful, unintended consequences
|
||||
*/
|
||||
warning: string | null
|
||||
/**
|
||||
* One of: "enabled", "hidden", or { disabled: "" }
|
||||
* - "enabled" - the action is available be run
|
||||
* - "hidden" - the action cannot be seen or run
|
||||
* - { disabled: "example explanation" } means the action is visible but cannot be run. Replace "example explanation" with a reason why the action is disable to prevent user confusion.
|
||||
*/
|
||||
visibility: ActionVisibility
|
||||
/**
|
||||
* One of: "only-stopped", "only-running", "all"
|
||||
* - "only-stopped" - the action can only be run when the service is stopped
|
||||
* - "only-running" - the action can only be run when the service is running
|
||||
* - "any" - the action can only be run regardless of the service's status
|
||||
*/
|
||||
allowedStatuses: AllowedStatuses
|
||||
hasInput: boolean
|
||||
/**
|
||||
* If provided, this action will be nested under a header of this value, along with other actions of the same group
|
||||
*/
|
||||
group: string | null
|
||||
}
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ActionResultMember = {
|
||||
/**
|
||||
* A human-readable name or title of the value, such as "Last Active" or "Login Password"
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* (optional) A description of the value, such as an explaining why it exists or how to use it
|
||||
*/
|
||||
description: string | null
|
||||
} & (
|
||||
| {
|
||||
type: "single"
|
||||
/**
|
||||
* The actual string value to display
|
||||
*/
|
||||
value: string
|
||||
/**
|
||||
* Whether or not to include a copy to clipboard icon to copy the value
|
||||
*/
|
||||
copyable: boolean
|
||||
/**
|
||||
* Whether or not to also display the value as a QR code
|
||||
*/
|
||||
qr: boolean
|
||||
/**
|
||||
* Whether or not to mask the value using ●●●●●●●, which is useful for password or other sensitive information
|
||||
*/
|
||||
masked: boolean
|
||||
}
|
||||
| { type: "group"; value: Array<ActionResultMember> }
|
||||
| {
|
||||
type: "group"
|
||||
/**
|
||||
* An new group of nested values, experienced by the user as an accordion dropdown
|
||||
*/
|
||||
value: Array<ActionResultMember>
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
import type { ActionResultValue } from "./ActionResultValue"
|
||||
|
||||
export type ActionResultV1 = {
|
||||
/**
|
||||
* Primary text to display as the header of the response modal. e.g. "Success!", "Name Updated", or "Service Information", whatever makes sense
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* (optional) A general message for the user, just under the title
|
||||
*/
|
||||
message: string | null
|
||||
/**
|
||||
* (optional) Structured data to present inside the modal
|
||||
*/
|
||||
result: ActionResultValue | null
|
||||
}
|
||||
|
||||
@@ -4,9 +4,27 @@ import type { ActionResultMember } from "./ActionResultMember"
|
||||
export type ActionResultValue =
|
||||
| {
|
||||
type: "single"
|
||||
/**
|
||||
* The actual string value to display
|
||||
*/
|
||||
value: string
|
||||
/**
|
||||
* Whether or not to include a copy to clipboard icon to copy the value
|
||||
*/
|
||||
copyable: boolean
|
||||
/**
|
||||
* Whether or not to also display the value as a QR code
|
||||
*/
|
||||
qr: boolean
|
||||
/**
|
||||
* Whether or not to mask the value using ●●●●●●●, which is useful for password or other sensitive information
|
||||
*/
|
||||
masked: boolean
|
||||
}
|
||||
| { type: "group"; value: Array<ActionResultMember> }
|
||||
| {
|
||||
type: "group"
|
||||
/**
|
||||
* An new group of nested values, experienced by the user as an accordion dropdown
|
||||
*/
|
||||
value: Array<ActionResultMember>
|
||||
}
|
||||
|
||||
6
sdk/node_modules/.package-lock.json
generated
vendored
6
sdk/node_modules/.package-lock.json
generated
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -33,12 +33,11 @@ import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
||||
import { List } from "../../base/lib/actions/input/builder/list"
|
||||
import { Install, InstallFn } from "./inits/setupInstall"
|
||||
import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
|
||||
import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall"
|
||||
import { UninstallFn, setupUninstall } from "./inits/setupUninstall"
|
||||
import { setupMain } from "./mainFn"
|
||||
import { defaultTrigger } from "./trigger/defaultTrigger"
|
||||
import { changeOnFirstSuccess, cooldownTrigger } from "./trigger"
|
||||
import {
|
||||
ServiceInterfacesReceipt,
|
||||
UpdateServiceInterfaces,
|
||||
setupServiceInterfaces,
|
||||
} from "../../base/lib/interfaces/setupInterfaces"
|
||||
@@ -240,68 +239,67 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
return runCommand<Manifest>(effects, image, command, options, name)
|
||||
},
|
||||
/**
|
||||
* TODO: rewrite this
|
||||
* @description Use this function to create a static Action, including optional form input.
|
||||
* @description Use this class to create an Action. By convention, each Action should receive its own file.
|
||||
*
|
||||
* By convention, each Action should receive its own file.
|
||||
*
|
||||
* @param id
|
||||
* @param metaData
|
||||
* @param fn
|
||||
* @returns
|
||||
* @example
|
||||
* In this example, we create an Action that prints a name to the console. We present a user
|
||||
* with a form for optionally entering a temp name. If no temp name is provided, we use the name
|
||||
* from the underlying `inputSpec.yaml` file. If no name is there, we use "Unknown". Then, we return
|
||||
* a message to the user informing them what happened.
|
||||
*
|
||||
* ```
|
||||
import { sdk } from '../sdk'
|
||||
const { InputSpec, Value } = sdk
|
||||
import { yamlFile } from '../file-models/inputSpec.yml'
|
||||
|
||||
const input = InputSpec.of({
|
||||
nameToPrint: Value.text({
|
||||
name: 'Temp Name',
|
||||
description: 'If no name is provided, the name from inputSpec will be used',
|
||||
required: false,
|
||||
}),
|
||||
})
|
||||
|
||||
export const nameToLog = sdk.createAction(
|
||||
// id
|
||||
'nameToLogs',
|
||||
|
||||
// metadata
|
||||
{
|
||||
name: 'Name to Logs',
|
||||
description: 'Prints "Hello [Name]" to the service logs.',
|
||||
warning: null,
|
||||
disabled: false,
|
||||
input,
|
||||
allowedStatuses: 'onlyRunning',
|
||||
group: null,
|
||||
},
|
||||
|
||||
// the execution function
|
||||
async ({ effects, input }) => {
|
||||
const name =
|
||||
input.nameToPrint || (await yamlFile.read(effects))?.name || 'Unknown'
|
||||
|
||||
console.info(`Hello ${name}`)
|
||||
|
||||
return {
|
||||
version: '0',
|
||||
message: `"Hello ${name}" has been written to the service logs. Open your logs to view it.`,
|
||||
value: name,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
* ```
|
||||
*/
|
||||
Action: {
|
||||
/**
|
||||
* @description Use this function to create an action that accepts form input
|
||||
* @param id - a unique ID for this action
|
||||
* @param metadata - information describing the action and its availability
|
||||
* @param inputSpec - define the form input using the InputSpec and Value classes
|
||||
* @param prefillFn - optionally fetch data from the file system to pre-fill the input form. Must returns a deep partial of the input spec
|
||||
* @param executionFn - execute the action. Optionally return data for the user to view. Must be in the structure of an ActionResult, version "1"
|
||||
* @example
|
||||
* In this example, we create an action for a user to provide their name.
|
||||
* We prefill the input form with their existing name from the service's yaml file.
|
||||
* The new name is saved to the yaml file, and we return nothing to the user, which
|
||||
* means they will receive a generic success message.
|
||||
*
|
||||
* ```
|
||||
import { sdk } from '../sdk'
|
||||
import { yamlFile } from '../file-models/config.yml'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
export const inputSpec = InputSpec.of({
|
||||
name: Value.text({
|
||||
name: 'Name',
|
||||
description:
|
||||
'When you launch the Hello World UI, it will display "Hello [Name]"',
|
||||
required: true,
|
||||
default: 'World',
|
||||
}),
|
||||
})
|
||||
|
||||
export const setName = sdk.Action.withInput(
|
||||
// id
|
||||
'set-name',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: 'Set Name',
|
||||
description: 'Set your name so Hello World can say hello to you',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// form input specification
|
||||
inputSpec,
|
||||
|
||||
// optionally pre-fill the input form
|
||||
async ({ effects }) => {
|
||||
const name = await yamlFile.read.const(effects)?.name
|
||||
return { name }
|
||||
},
|
||||
|
||||
// the execution function
|
||||
async ({ effects, input }) => yamlFile.merge(input)
|
||||
)
|
||||
* ```
|
||||
*/
|
||||
withInput: <
|
||||
Id extends T.ActionId,
|
||||
InputSpecType extends
|
||||
@@ -317,6 +315,50 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
getInput: GetInput<Type>,
|
||||
run: Run<Type>,
|
||||
) => Action.withInput(id, metadata, inputSpec, getInput, run),
|
||||
/**
|
||||
* @description Use this function to create an action that does not accept form input
|
||||
* @param id - a unique ID for this action
|
||||
* @param metadata - information describing the action and its availability
|
||||
* @param executionFn - execute the action. Optionally return data for the user to view. Must be in the structure of an ActionResult, version "1"
|
||||
* @example
|
||||
* In this example, we create an action that returns a secret phrase for the user to see.
|
||||
*
|
||||
* ```
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const showSecretPhrase = sdk.Action.withoutInput(
|
||||
// id
|
||||
'show-secret-phrase',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: 'Show Secret Phrase',
|
||||
description: 'Reveal the secret phrase for Hello World',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// the execution function
|
||||
async ({ effects }) => ({
|
||||
version: '1',
|
||||
title: 'Secret Phrase',
|
||||
message:
|
||||
'Below is your secret phrase. Use it to gain access to extraordinary places',
|
||||
result: {
|
||||
type: 'single',
|
||||
value: await sdk.store
|
||||
.getOwn(effects, sdk.StorePath.secretPhrase)
|
||||
.const(),
|
||||
copyable: true,
|
||||
qr: true,
|
||||
masked: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
* ```
|
||||
*/
|
||||
withoutInput: <Id extends T.ActionId>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
@@ -355,9 +397,9 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
id: string
|
||||
/** The human readable description. */
|
||||
description: string
|
||||
/** Not available until StartOS v0.4.0. If true, forces the user to select one URL (i.e. .onion, .local, or IP address) as the primary URL. This is needed by some services to function properly. */
|
||||
/** No effect until StartOS v0.4.0. If true, forces the user to select one URL (i.e. .onion, .local, or IP address) as the primary URL. This is needed by some services to function properly. */
|
||||
hasPrimary: boolean
|
||||
/** Affects how the interface appears to the user. One of: 'ui', 'api', 'p2p'. */
|
||||
/** Affects how the interface appears to the user. One of: 'ui', 'api', 'p2p'. If 'ui', the user will see a "Launch UI" button */
|
||||
type: ServiceInterfaceType
|
||||
/** (optional) prepends the provided username to all URLs. */
|
||||
username: null | string
|
||||
@@ -413,15 +455,22 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
* In this example, we back up the entire "main" volume and nothing else.
|
||||
*
|
||||
* ```
|
||||
export const { createBackup, restoreBackup } = sdk.setupBackups(sdk.Backups.addVolume('main'))
|
||||
import { sdk } from './sdk'
|
||||
|
||||
export const { createBackup, restoreBackup } = sdk.setupBackups(
|
||||
async ({ effects }) => sdk.Backups.volumes('main'),
|
||||
)
|
||||
* ```
|
||||
* @example
|
||||
* In this example, we back up the "main" and the "other" volume, but exclude hypothetical directory "excludedDir" from the "other".
|
||||
* In this example, we back up the "main" volume, but exclude hypothetical directory "excludedDir".
|
||||
*
|
||||
* ```
|
||||
export const { createBackup, restoreBackup } = sdk.setupBackups(sdk.Backups
|
||||
.addVolume('main')
|
||||
.addVolume('other', { exclude: ['path/to/excludedDir'] })
|
||||
import { sdk } from './sdk'
|
||||
|
||||
export const { createBackup, restoreBackup } = sdk.setupBackups(async () =>
|
||||
sdk.Backups.volumes('main').setOptions({
|
||||
exclude: ['excludedDir'],
|
||||
}),
|
||||
)
|
||||
* ```
|
||||
*/
|
||||
@@ -429,37 +478,36 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
setupBackups<Manifest>(options),
|
||||
/**
|
||||
* @description Use this function to set dependency information.
|
||||
*
|
||||
* The function executes on service install, update, and inputSpec save. "input" will be of type `Input` for inputSpec save. It will be `null` for install and update.
|
||||
* @example
|
||||
* In this example, we create a static dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "webui" health check.
|
||||
* In this example, we create a perpetual dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "primary" health check.
|
||||
*
|
||||
* ```
|
||||
export const setDependencies = sdk.setupDependencies(
|
||||
async ({ effects, input }) => {
|
||||
return {
|
||||
'hello-world': sdk.Dependency.of({
|
||||
type: 'running',
|
||||
versionRange: VersionRange.parse('>=1.0.0:0'),
|
||||
healthChecks: ['webui'],
|
||||
}),
|
||||
'hello-world': {
|
||||
kind: 'running',
|
||||
versionRange: '>=1.0.0',
|
||||
healthChecks: ['primary'],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
* ```
|
||||
* @example
|
||||
* In this example, we create a conditional dependency on Hello World based on a hypothetical "needsWorld" boolean in the store.
|
||||
* In this example, we create a conditional dependency on Hello World based on a hypothetical "needsWorld" boolean in our Store.
|
||||
* Using .const() ensures that if the "needsWorld" boolean changes, setupDependencies will re-run.
|
||||
*
|
||||
* ```
|
||||
export const setDependencies = sdk.setupDependencies(
|
||||
async ({ effects }) => {
|
||||
if (sdk.store.getOwn(sdk.StorePath.needsWorld).const()) {
|
||||
return {
|
||||
'hello-world': sdk.Dependency.of({
|
||||
type: 'running',
|
||||
versionRange: VersionRange.parse('>=1.0.0:0'),
|
||||
healthChecks: ['webui'],
|
||||
}),
|
||||
'hello-world': {
|
||||
kind: 'running',
|
||||
versionRange: '>=1.0.0',
|
||||
healthChecks: ['primary'],
|
||||
},
|
||||
}
|
||||
}
|
||||
return {}
|
||||
@@ -614,7 +662,8 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
name: 'Name',
|
||||
description:
|
||||
'When you launch the Hello World UI, it will display "Hello [Name]"',
|
||||
required: { default: 'World' },
|
||||
required: true,
|
||||
default: 'World'
|
||||
}),
|
||||
makePublic: Value.toggle({
|
||||
name: 'Make Public',
|
||||
@@ -673,6 +722,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
label: Value.text({
|
||||
name: 'Label',
|
||||
required: false,
|
||||
default: null,
|
||||
})
|
||||
})
|
||||
displayAs: 'label',
|
||||
@@ -690,11 +740,13 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
spec: InputSpec.of({
|
||||
label: Value.text({
|
||||
name: 'Label',
|
||||
required: { default: null },
|
||||
required: true,
|
||||
default: null,
|
||||
})
|
||||
pubkey: Value.text({
|
||||
name: 'Pubkey',
|
||||
required: { default: null },
|
||||
required: true,
|
||||
default: null,
|
||||
})
|
||||
})
|
||||
displayAs: 'label',
|
||||
@@ -707,11 +759,13 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
spec: InputSpec.of({
|
||||
label: Value.text({
|
||||
name: 'Label',
|
||||
required: { default: null },
|
||||
required: true,
|
||||
default: null,
|
||||
})
|
||||
pubkey: Value.text({
|
||||
name: 'Pubkey',
|
||||
required: { default: null },
|
||||
required: true,
|
||||
default: null,
|
||||
})
|
||||
})
|
||||
displayAs: 'label',
|
||||
@@ -777,6 +831,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
// required
|
||||
name: 'Text Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
@@ -801,6 +856,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
// required
|
||||
name: 'Textarea Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
@@ -821,6 +877,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
// required
|
||||
name: 'Number Example',
|
||||
required: false,
|
||||
default: null,
|
||||
integer: true,
|
||||
|
||||
// optional
|
||||
@@ -844,6 +901,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
// required
|
||||
name: 'Color Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
@@ -861,6 +919,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
// required
|
||||
name: 'Datetime Example',
|
||||
required: false,
|
||||
default: null,
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
@@ -880,7 +939,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
selectExample: Value.select({
|
||||
// required
|
||||
name: 'Select Example',
|
||||
required: false,
|
||||
default: 'radio1',
|
||||
values: {
|
||||
radio1: 'Radio 1',
|
||||
radio2: 'Radio 2',
|
||||
@@ -945,7 +1004,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
{
|
||||
// required
|
||||
name: 'Union Example',
|
||||
required: false,
|
||||
default: 'option1',
|
||||
|
||||
// optional
|
||||
description: null,
|
||||
|
||||
@@ -13,28 +13,7 @@ export type BackupSync<Volumes extends string> = {
|
||||
backupOptions?: Partial<T.SyncOptions>
|
||||
restoreOptions?: Partial<T.SyncOptions>
|
||||
}
|
||||
/**
|
||||
* This utility simplifies the volume backup process.
|
||||
* ```ts
|
||||
* export const { createBackup, restoreBackup } = Backups.volumes("main").build();
|
||||
* ```
|
||||
*
|
||||
* Changing the options of the rsync, (ie excludes) use either
|
||||
* ```ts
|
||||
* Backups.volumes("main").set_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
|
||||
* // or
|
||||
* Backups.with_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
|
||||
* ```
|
||||
*
|
||||
* Using the more fine control, using the addSets for more control
|
||||
* ```ts
|
||||
* Backups.addSets({
|
||||
* srcVolume: 'main', srcPath:'smallData/', dstPath: 'main/smallData/', dstVolume: : Backups.BACKUP
|
||||
* }, {
|
||||
* srcVolume: 'main', srcPath:'bigData/', dstPath: 'main/bigData/', dstVolume: : Backups.BACKUP, options: {exclude:['bigData/excludeThis']}}
|
||||
* ).build()q
|
||||
* ```
|
||||
*/
|
||||
|
||||
export class Backups<M extends T.SDKManifest> {
|
||||
private constructor(
|
||||
private options = DEFAULT_OPTIONS,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Effects } from "../../../../base/lib/types"
|
||||
import { stringFromStdErrOut } from "../../util"
|
||||
import { HealthCheckResult } from "./HealthCheckResult"
|
||||
|
||||
import { promisify } from "node:util"
|
||||
import * as CP from "node:child_process"
|
||||
|
||||
const cpExec = promisify(CP.exec)
|
||||
const cpExecFile = promisify(CP.execFile)
|
||||
|
||||
export function containsAddress(x: string, port: number) {
|
||||
const readPorts = x
|
||||
.split("\n")
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||
import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../../../base/lib/types"
|
||||
import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types"
|
||||
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { asError } from "../../../base/lib/util/asError"
|
||||
import {
|
||||
ExecSpawnable,
|
||||
MountOptions,
|
||||
SubContainerHandle,
|
||||
SubContainer,
|
||||
|
||||
@@ -19,7 +19,22 @@ import { CommandController } from "./CommandController"
|
||||
export const cpExec = promisify(CP.exec)
|
||||
export const cpExecFile = promisify(CP.execFile)
|
||||
export type Ready = {
|
||||
/** A human-readable display name for the health check. If null, the health check itself will be from the UI */
|
||||
display: string | null
|
||||
/**
|
||||
* @description The function to determine the health status of the daemon
|
||||
*
|
||||
* The SDK provides some built-in health checks. To see them, type sdk.healthCheck.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
fn: () =>
|
||||
sdk.healthCheck.checkPortListening(effects, 80, {
|
||||
successMessage: 'service listening on port 80',
|
||||
errorMessage: 'service is unreachable',
|
||||
})
|
||||
* ```
|
||||
*/
|
||||
fn: (
|
||||
spawnable: ExecSpawnable,
|
||||
) => Promise<HealthCheckResult> | HealthCheckResult
|
||||
@@ -32,11 +47,23 @@ type DaemonsParams<
|
||||
Command extends string,
|
||||
Id extends string,
|
||||
> = {
|
||||
/** The command line command to start the daemon */
|
||||
command: T.CommandType
|
||||
image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }
|
||||
/** Information about the image in which the daemon runs */
|
||||
image: {
|
||||
/** The ID of the image. Must be one of the image IDs declared in the manifest */
|
||||
id: keyof Manifest["images"] & T.ImageId
|
||||
/**
|
||||
* Whether or not to share the `/run` directory with the parent container.
|
||||
* This is useful if you are trying to connect to a service that exposes a unix domain socket or auth cookie via the `/run` directory
|
||||
*/
|
||||
sharedRun?: boolean
|
||||
}
|
||||
/** For mounting the necessary volumes. Syntax: sdk.Mounts.of().addVolume() */
|
||||
mounts: Mounts<Manifest>
|
||||
env?: Record<string, string>
|
||||
ready: Ready
|
||||
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
|
||||
requires: Exclude<Ids, Id>[]
|
||||
sigtermTimeout?: number
|
||||
onStdout?: (chunk: Buffer | string | any) => void
|
||||
|
||||
@@ -30,9 +30,13 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
||||
}
|
||||
|
||||
addVolume(
|
||||
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */
|
||||
id: Manifest["volumes"][number],
|
||||
/** The path within the volume to mount. Use `null` to mount the entire volume */
|
||||
subpath: string | null,
|
||||
/** Where to mount the volume. e.g. /data */
|
||||
mountpoint: string,
|
||||
/** Whether or not the volume should be readonly for this daemon */
|
||||
readonly: boolean,
|
||||
) {
|
||||
this.volumes.push({
|
||||
@@ -45,8 +49,11 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
||||
}
|
||||
|
||||
addAssets(
|
||||
/** The ID of the asset directory to mount. This is typically the same as the folder name in your assets directory */
|
||||
id: Manifest["assets"][number],
|
||||
/** The path within the asset directory to mount. Use `null` to mount the entire volume */
|
||||
subpath: string | null,
|
||||
/** Where to mount the asset. e.g. /asset */
|
||||
mountpoint: string,
|
||||
) {
|
||||
this.assets.push({
|
||||
@@ -58,10 +65,15 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
||||
}
|
||||
|
||||
addDependency<DependencyManifest extends T.SDKManifest>(
|
||||
/** The ID of the dependency service */
|
||||
dependencyId: keyof Manifest["dependencies"] & string,
|
||||
/** The ID of the volume belonging to the dependency service to mount */
|
||||
volumeId: DependencyManifest["volumes"][number],
|
||||
/** The path within the dependency's volume to mount. Use `null` to mount the entire volume */
|
||||
subpath: string | null,
|
||||
/** Where to mount the dependency's volume. e.g. /service-id */
|
||||
mountpoint: string,
|
||||
/** Whether or not the volume should be readonly for this daemon */
|
||||
readonly: boolean,
|
||||
) {
|
||||
this.dependencies.push({
|
||||
|
||||
@@ -11,7 +11,6 @@ import { execSync } from "child_process"
|
||||
/**
|
||||
* @description Use this function to define critical information about your package
|
||||
*
|
||||
* @param versions Every version of the package, imported from ./versions
|
||||
* @param manifest Static properties of the package
|
||||
*/
|
||||
export function setupManifest<
|
||||
@@ -23,7 +22,7 @@ export function setupManifest<
|
||||
assets: AssetTypes[]
|
||||
volumes: VolumesTypes[]
|
||||
} & SDKManifest,
|
||||
>(manifest: Manifest): Manifest {
|
||||
>(manifest: Manifest & SDKManifest): Manifest {
|
||||
return manifest
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ExposedStorePaths } from "../../../base/lib/types"
|
||||
import { Affine, _ } from "../util"
|
||||
import {
|
||||
PathBuilder,
|
||||
extractJsonPath,
|
||||
|
||||
@@ -6,8 +6,6 @@ import { Variants } from "../../../base/lib/actions/input/builder/variants"
|
||||
import { ValueSpec } from "../../../base/lib/actions/input/inputSpecTypes"
|
||||
import { setupManifest } from "../manifest/setupManifest"
|
||||
import { StartSdk } from "../StartSdk"
|
||||
import { VersionGraph } from "../version/VersionGraph"
|
||||
import { VersionInfo } from "../version/VersionInfo"
|
||||
|
||||
describe("builder tests", () => {
|
||||
test("text", async () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cooldownTrigger } from "./cooldownTrigger"
|
||||
import { changeOnFirstSuccess } from "./changeOnFirstSuccess"
|
||||
import { successFailure } from "./successFailure"
|
||||
|
||||
export const defaultTrigger = changeOnFirstSuccess({
|
||||
beforeFirstSuccess: cooldownTrigger(1000),
|
||||
|
||||
@@ -4,9 +4,10 @@ import * as cp from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { Buffer } from "node:buffer"
|
||||
import { once } from "../../../base/lib/util/once"
|
||||
|
||||
export const execFile = promisify(cp.execFile)
|
||||
const WORKDIR = (imageId: string) => `/media/startos/images/${imageId}/`
|
||||
const False = () => false
|
||||
|
||||
type ExecResults = {
|
||||
exitCode: number | null
|
||||
exitSignal: NodeJS.Signals | null
|
||||
|
||||
@@ -46,27 +46,34 @@ async function onCreated(path: string) {
|
||||
/**
|
||||
* @description Use this class to read/write an underlying configuration file belonging to the upstream service.
|
||||
*
|
||||
* Using the static functions, choose between officially supported file formats (json, yaml, toml), or a custom format (raw).
|
||||
* These type definitions should reflect the underlying file as closely as possible. For example, if the service does not require a particular value, it should be marked as optional(), even if your package requires it.
|
||||
*
|
||||
* It is recommended to use onMismatch() whenever possible. This provides an escape hatch in case the user edits the file manually and accidentally sets a value to an unsupported type.
|
||||
*
|
||||
* Officially supported file types are json, yaml, and toml. Other files types can use "raw"
|
||||
*
|
||||
* Choose between officially supported file formats (), or a custom format (raw).
|
||||
*
|
||||
* @example
|
||||
* Below are a few examples
|
||||
*
|
||||
* ```
|
||||
* import { matches, FileHelper } from '@start9labs/start-sdk'
|
||||
* const { arrayOf, boolean, literal, literals, object, oneOf, natural, string } = matches
|
||||
* const { arrayOf, boolean, literal, literals, object, natural, string } = matches
|
||||
*
|
||||
* export const jsonFile = FileHelper.json('./inputSpec.json', object({
|
||||
* passwords: arrayOf(string)
|
||||
* type: oneOf(literals('private', 'public'))
|
||||
* passwords: arrayOf(string).onMismatch([])
|
||||
* type: literals('private', 'public').optional().onMismatch(undefined)
|
||||
* }))
|
||||
*
|
||||
* export const tomlFile = FileHelper.toml('./inputSpec.toml', object({
|
||||
* url: literal('https://start9.com')
|
||||
* public: boolean
|
||||
* url: literal('https://start9.com').onMismatch('https://start9.com')
|
||||
* public: boolean.onMismatch(true)
|
||||
* }))
|
||||
*
|
||||
* export const yamlFile = FileHelper.yaml('./inputSpec.yml', object({
|
||||
* name: string
|
||||
* age: natural
|
||||
* name: string.optional().onMismatch(undefined)
|
||||
* age: natural.optional().onMismatch(undefined)
|
||||
* }))
|
||||
*
|
||||
* export const bitcoinConfFile = FileHelper.raw(
|
||||
@@ -183,7 +190,7 @@ export class FileHelper<A> {
|
||||
|
||||
/**
|
||||
* We wanted to be able to have a fileHelper, and just modify the path later in time.
|
||||
* Like one behaviour of another dependency or something similar.
|
||||
* Like one behavior of another dependency or something similar.
|
||||
*/
|
||||
withPath(path: string) {
|
||||
return new FileHelper<A>(path, this.writeData, this.readData, this.validate)
|
||||
|
||||
Reference in New Issue
Block a user