diff --git a/internal/dis/component/server/admin/socket/actions.go b/internal/dis/component/server/admin/socket/actions.go index b210497..a33e662 100644 --- a/internal/dis/component/server/admin/socket/actions.go +++ b/internal/dis/component/server/admin/socket/actions.go @@ -2,91 +2,43 @@ package socket import ( "context" - "encoding/json" - "fmt" "io" - - "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" - "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" - "github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision" - "github.com/FAU-CDI/wisski-distillery/internal/models" - "github.com/FAU-CDI/wisski-distillery/internal/wisski" ) func (sockets *Sockets) Actions() ActionMap { - return map[string]Action{ - // generic actions - "backup": sockets.Generic(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error { - return sockets.dependencies.Exporter.MakeExport( - ctx, - out, - exporter.ExportTask{ - Dest: "", - Instance: nil, + actions := make(ActionMap, len(sockets.dependencies.Actions)+len(sockets.dependencies.IActions)) - StagingOnly: false, - }, - ) - }), - "provision": sockets.Generic(scopes.ScopeUserAdmin, "", 1, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error { - // read the flags of the instance to be provisioned - var flags provision.Flags - if err := json.Unmarshal([]byte(params[0]), &flags); err != nil { - return err - } + // setup basic actions + for _, a := range sockets.dependencies.Actions { + a := a + meta := a.Action() + actions[meta.Name] = Action{ + NumParams: meta.NumParams, + Scope: meta.Scope, + ScopeParam: meta.ScopeParam, - instance, err := sockets.dependencies.Provision.Provision( - out, - ctx, - flags, - ) - if err != nil { - return err - } - - fmt.Fprintf(out, "URL: %s\n", instance.URL().String()) - fmt.Fprintf(out, "Username: %s\n", instance.DrupalUsername) - fmt.Fprintf(out, "Password: %s\n", instance.DrupalPassword) - - return nil - }), - - // instance-specific actions! - - "snapshot": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, socket *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { - return socket.dependencies.Exporter.MakeExport( - ctx, - out, - exporter.ExportTask{ - Dest: "", - Instance: instance, - - StagingOnly: false, - }, - ) - }), - "rebuild": sockets.Instance(scopes.ScopeUserAdmin, "", 1, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { - // read the flags of the instance to be provisioned - var system models.System - if err := json.Unmarshal([]byte(params[0]), &system); err != nil { - return err - } - return instance.SystemManager().Apply(ctx, out, system, true) - }), - "update": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { - return instance.Composer().Update(ctx, out) - }), - "cron": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, str io.Writer, params ...string) error { - return instance.Drush().Cron(ctx, str) - }), - "start": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { - return instance.Barrel().Stack().Up(ctx, out) - }), - "stop": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { - return instance.Barrel().Stack().Down(ctx, out) - }), - "purge": sockets.Instance(scopes.ScopeUserAdmin, "", 0, func(ctx context.Context, sockets *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { - return sockets.dependencies.Purger.Purge(ctx, out, instance.Slug) - }), + Handle: a.Act, + } } + + // setup instance actions + for _, a := range sockets.dependencies.IActions { + a := a + meta := a.Action() + actions[meta.Name] = Action{ + NumParams: meta.NumParams + 1, + Scope: meta.Scope, + ScopeParam: meta.ScopeParam, + + Handle: func(ctx context.Context, in io.Reader, out io.Writer, params ...string) error { + instance, err := sockets.dependencies.Instances.WissKI(ctx, params[0]) + if err != nil { + return err + } + return a.Act(ctx, instance, in, out, params[1:]...) + }, + } + } + + return actions } diff --git a/internal/dis/component/server/admin/socket/actions/actions.go b/internal/dis/component/server/admin/socket/actions/actions.go new file mode 100644 index 0000000..6108f1c --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/actions.go @@ -0,0 +1,38 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +// Routeable is a component that is servable +type WebsocketAction interface { + component.Component + + Action() Action + Act(ctx context.Context, in io.Reader, out io.Writer, params ...string) error +} + +type WebsocketInstanceAction interface { + component.Component + + Action() InstanceAction + Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error +} + +// Action represents information about an action +type Action struct { + Name string + + Scope scopes.Scope + ScopeParam string + NumParams int +} + +type InstanceAction struct { + Action +} diff --git a/internal/dis/component/server/admin/socket/actions/backup.go b/internal/dis/component/server/admin/socket/actions/backup.go new file mode 100644 index 0000000..f0d7b95 --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/backup.go @@ -0,0 +1,42 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" +) + +type Backup struct { + component.Base + dependencies struct { + Exporter *exporter.Exporter + } +} + +var ( + _ WebsocketAction = (*Backup)(nil) +) + +func (*Backup) Action() Action { + return Action{ + Name: "backup", + Scope: scopes.ScopeUserAdmin, + NumParams: 0, + } +} + +func (b *Backup) Act(ctx context.Context, in io.Reader, out io.Writer, params ...string) error { + return b.dependencies.Exporter.MakeExport( + ctx, + out, + exporter.ExportTask{ + Dest: "", + Instance: nil, + + StagingOnly: false, + }, + ) +} diff --git a/internal/dis/component/server/admin/socket/actions/cron.go b/internal/dis/component/server/admin/socket/actions/cron.go new file mode 100644 index 0000000..e5af5ca --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/cron.go @@ -0,0 +1,32 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +type Cron struct { + component.Base +} + +var ( + _ WebsocketInstanceAction = (*Cron)(nil) +) + +func (*Cron) Action() InstanceAction { + return InstanceAction{ + Action: Action{ + Name: "cron", + Scope: scopes.ScopeUserAdmin, + NumParams: 0, + }, + } +} + +func (c *Cron) Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { + return instance.Drush().Cron(ctx, out) +} diff --git a/internal/dis/component/server/admin/socket/actions/provision.go b/internal/dis/component/server/admin/socket/actions/provision.go new file mode 100644 index 0000000..b0762e1 --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/provision.go @@ -0,0 +1,54 @@ +package actions + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision" +) + +type Provision struct { + component.Base + dependencies struct { + Provision *provision.Provision + } +} + +var ( + _ WebsocketAction = (*Provision)(nil) +) + +func (*Provision) Action() Action { + return Action{ + Name: "provision", + Scope: scopes.ScopeUserAdmin, + NumParams: 1, + } +} + +func (p *Provision) Act(ctx context.Context, in io.Reader, out io.Writer, params ...string) error { + // read the flags of the instance to be provisioned + var flags provision.Flags + if err := json.Unmarshal([]byte(params[0]), &flags); err != nil { + return err + } + + instance, err := p.dependencies.Provision.Provision( + out, + ctx, + flags, + ) + if err != nil { + return err + } + + fmt.Fprintf(out, "URL: %s\n", instance.URL().String()) + fmt.Fprintf(out, "Username: %s\n", instance.DrupalUsername) + fmt.Fprintf(out, "Password: %s\n", instance.DrupalPassword) + + return nil +} diff --git a/internal/dis/component/server/admin/socket/actions/purge.go b/internal/dis/component/server/admin/socket/actions/purge.go new file mode 100644 index 0000000..db15c7e --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/purge.go @@ -0,0 +1,36 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +type Purge struct { + component.Base + dependencies struct { + Purger *purger.Purger + } +} + +var ( + _ WebsocketInstanceAction = (*Stop)(nil) +) + +func (*Purge) Action() InstanceAction { + return InstanceAction{ + Action: Action{ + Name: "purge", + Scope: scopes.ScopeUserAdmin, + NumParams: 0, + }, + } +} + +func (p *Purge) Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { + return p.dependencies.Purger.Purge(ctx, out, instance.Slug) +} diff --git a/internal/dis/component/server/admin/socket/actions/rebuild.go b/internal/dis/component/server/admin/socket/actions/rebuild.go new file mode 100644 index 0000000..21f63f4 --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/rebuild.go @@ -0,0 +1,39 @@ +package actions + +import ( + "context" + "encoding/json" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/models" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +type Rebuild struct { + component.Base +} + +var ( + _ WebsocketInstanceAction = (*Rebuild)(nil) +) + +func (*Rebuild) Action() InstanceAction { + return InstanceAction{ + Action: Action{ + Name: "rebuild", + Scope: scopes.ScopeUserAdmin, + NumParams: 1, + }, + } +} + +func (r *Rebuild) Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { + // read the flags of the instance to be provisioned + var system models.System + if err := json.Unmarshal([]byte(params[0]), &system); err != nil { + return err + } + return instance.SystemManager().Apply(ctx, out, system, true) +} diff --git a/internal/dis/component/server/admin/socket/actions/snapshot.go b/internal/dis/component/server/admin/socket/actions/snapshot.go new file mode 100644 index 0000000..3f21fa9 --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/snapshot.go @@ -0,0 +1,45 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +type Snapshot struct { + component.Base + dependencies struct { + Exporter *exporter.Exporter + } +} + +var ( + _ WebsocketInstanceAction = (*Snapshot)(nil) +) + +func (*Snapshot) Action() InstanceAction { + return InstanceAction{ + Action: Action{ + Name: "snapshot", + Scope: scopes.ScopeUserAdmin, + NumParams: 0, + }, + } +} + +func (s *Snapshot) Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { + return s.dependencies.Exporter.MakeExport( + ctx, + out, + exporter.ExportTask{ + Dest: "", + Instance: instance, + + StagingOnly: false, + }, + ) +} diff --git a/internal/dis/component/server/admin/socket/actions/start.go b/internal/dis/component/server/admin/socket/actions/start.go new file mode 100644 index 0000000..31d4a97 --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/start.go @@ -0,0 +1,32 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +type Start struct { + component.Base +} + +var ( + _ WebsocketInstanceAction = (*Start)(nil) +) + +func (*Start) Action() InstanceAction { + return InstanceAction{ + Action: Action{ + Name: "start", + Scope: scopes.ScopeUserAdmin, + NumParams: 0, + }, + } +} + +func (*Start) Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { + return instance.Barrel().Stack().Up(ctx, out) +} diff --git a/internal/dis/component/server/admin/socket/actions/stop.go b/internal/dis/component/server/admin/socket/actions/stop.go new file mode 100644 index 0000000..3a7eb77 --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/stop.go @@ -0,0 +1,32 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +type Stop struct { + component.Base +} + +var ( + _ WebsocketInstanceAction = (*Stop)(nil) +) + +func (*Stop) Action() InstanceAction { + return InstanceAction{ + Action: Action{ + Name: "stop", + Scope: scopes.ScopeUserAdmin, + NumParams: 0, + }, + } +} + +func (*Stop) Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { + return instance.Barrel().Stack().Down(ctx, out) +} diff --git a/internal/dis/component/server/admin/socket/actions/update.go b/internal/dis/component/server/admin/socket/actions/update.go new file mode 100644 index 0000000..c4c5410 --- /dev/null +++ b/internal/dis/component/server/admin/socket/actions/update.go @@ -0,0 +1,32 @@ +package actions + +import ( + "context" + "io" + + "github.com/FAU-CDI/wisski-distillery/internal/dis/component" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes" + "github.com/FAU-CDI/wisski-distillery/internal/wisski" +) + +type Update struct { + component.Base +} + +var ( + _ WebsocketInstanceAction = (*Update)(nil) +) + +func (*Update) Action() InstanceAction { + return InstanceAction{ + Action: Action{ + Name: "update", + Scope: scopes.ScopeUserAdmin, + NumParams: 0, + }, + } +} + +func (u *Update) Act(ctx context.Context, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error { + return instance.Composer().Update(ctx, out) +} diff --git a/internal/dis/component/server/admin/socket/socket.go b/internal/dis/component/server/admin/socket/socket.go index 2d49c99..89ddab7 100644 --- a/internal/dis/component/server/admin/socket/socket.go +++ b/internal/dis/component/server/admin/socket/socket.go @@ -12,6 +12,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket/actions" "github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/rs/zerolog" "github.com/tkw1536/pkglib/httpx" @@ -24,6 +25,9 @@ type Sockets struct { actions lazy.Lazy[ActionMap] dependencies struct { + Actions []actions.WebsocketAction + IActions []actions.WebsocketInstanceAction + Provision *provision.Provision Instances *instances.Instances Exporter *exporter.Exporter diff --git a/internal/dis/component/ws.go b/internal/dis/component/ws.go new file mode 100644 index 0000000..cc264c9 --- /dev/null +++ b/internal/dis/component/ws.go @@ -0,0 +1 @@ +package component diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index 0e7fced..d4e4b2c 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -27,6 +27,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket" + "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/admin/socket/actions" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/cron" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/home" @@ -209,7 +210,6 @@ func (dis *Distillery) allComponents(context *lifetime.RegisterContext[component resolver.RefreshInterval = time.Minute }) lifetime.Place[*admin.Admin](context) // TODO: Remove analytics - lifetime.Place[*socket.Sockets](context) lifetime.Place[*legal.Legal](context) lifetime.Place[*news.News](context) @@ -217,6 +217,18 @@ func (dis *Distillery) allComponents(context *lifetime.RegisterContext[component lifetime.Place[*logo.Logo](context) lifetime.Place[*templating.Templating](context) + // Websockets + lifetime.Place[*socket.Sockets](context) + lifetime.Place[*actions.Backup](context) + lifetime.Place[*actions.Provision](context) + lifetime.Place[*actions.Snapshot](context) + lifetime.Place[*actions.Rebuild](context) + lifetime.Place[*actions.Update](context) + lifetime.Place[*actions.Cron](context) + lifetime.Place[*actions.Start](context) + lifetime.Place[*actions.Stop](context) + lifetime.Place[*actions.Purge](context) + // Cron lifetime.Place[*cron.Cron](context) diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 0000000..0ba8eac --- /dev/null +++ b/utils/README.md @@ -0,0 +1,5 @@ +This folder contains utility code used for testing and maintaing the distillery. +None of this is used by the distillery itself. +Currently these are: + +- `wsclient`: A client for the websocket API, written in typescript. \ No newline at end of file diff --git a/utils/wsclient/client/calls.ts b/utils/wsclient/client/calls.ts new file mode 100644 index 0000000..923af71 --- /dev/null +++ b/utils/wsclient/client/calls.ts @@ -0,0 +1,83 @@ +import type { WebSocketCall } from "."; + +/** Backup backups everything */ +export function Backup(): WebSocketCall { + return { + 'call': 'backup', + 'params': [], + } +} + +type ProvisionParams = { + Slug: string; + Flavor?: "Drupal 10" | "Drupal 9", + System: SystemParams +} + +type SystemParams = { + PHP: "Default (8.1)" | "8.0" | "8.1" | "8.2", + OpCacheDevelopment: boolean, + ContentSecurityPolicy: string, +} + +/** Provision provisions a new instance */ +export function Provision(params: ProvisionParams): WebSocketCall { + return { + 'call': 'provision', + 'params': [ + JSON.stringify(params) + ], + } +} + +/** Snapshot makes a snapshot of an instance */ +export function Snapshot(Slug: string): WebSocketCall { + return { + 'call': 'snapshot', + 'params': [Slug], + } +} + +/** Rebuild rebuilds an instance */ +export function Rebuild(Slug: string, params: SystemParams): WebSocketCall { + return { + 'call': 'rebuild', + 'params': [ + Slug, + JSON.stringify(params) + ], + } +} + +/** Update updates a specific instance */ +export function Update(Slug: string): WebSocketCall { + return { + 'call': 'update', + 'params': [Slug], + } +} + + +/** Start starts a specific instance */ +export function Start(Slug: string): WebSocketCall { + return { + 'call': 'start', + 'params': [Slug], + } +} + +/** Stop stops a specific instance */ +export function Stop(Slug: string): WebSocketCall { + return { + 'call': 'stop', + 'params': [Slug], + } +} + +/** Purge purges a specific instance */ +export function Purge(Slug: string): WebSocketCall { + return { + 'call': 'purge', + 'params': [Slug], + } +} diff --git a/utils/wsclient/client/index.ts b/utils/wsclient/client/index.ts new file mode 100644 index 0000000..91f7309 --- /dev/null +++ b/utils/wsclient/client/index.ts @@ -0,0 +1,71 @@ +/** @file implements the websocket protocol used by the distillery */ + +import WebSocket from "ws"; + +/** A call to the websocket endpoint */ +export interface WebSocketCall { + call: string; + params: string[]; +} + +/** the result of a websocket call */ +export interface WebSocketResult { + success: boolean, + message: string, +} + +/** optional hooks to call when something happens */ +export interface Hooks { + beforeCall: (call: WebSocketCall) => void; // called right before sending the request + afterCall: (call: WebSocketCall, result: WebSocketResult) => void; // called when the socket is closed + onError: (call: WebSocketCall, error: any) => void; // called when an error occurs before rejecting the promise + onLogLine: (call: WebSocketCall, line: string) => void; // called when a log line is received +} + +/** specifies a remote endpoint */ +export interface Remote { + url: string; // the remote websocket url to talk to + token?: string; // optional token +} + +/** run a websocket remote call */ +export default async function Call(remote: Remote, call: WebSocketCall, hooks?: Partial): Promise { + return new Promise((resolve, reject) => { + let options = { headers: {} }; + if (remote.token) { + options.headers = { 'Authorization': 'Bearer ' + remote.token }; + } + const ws = new WebSocket(remote.url, options); + + let result = {'success': false, 'message': 'Unknown error'}; + ws.on('error', (err) => { + if (hooks && hooks.onError) { + hooks.onError(call, err); + } + reject(err) + }); + ws.on('open', () => { + if (hooks && hooks.beforeCall) { + hooks.beforeCall(call); + } + ws.send(Buffer.from(JSON.stringify(call), 'utf8')); + }); + + ws.on('message', async (msg, isBinary) => { + if (!isBinary) { + if (hooks && hooks.onLogLine) { + hooks.onLogLine(call, msg.toString()); + } + return; + } + result = JSON.parse(msg.toString()); + }); + + ws.on('close', () => { + if (hooks && hooks.afterCall) { + hooks.afterCall(call, result); + } + resolve(result); + }); + }); +} diff --git a/utils/wsclient/example_provision.ts b/utils/wsclient/example_provision.ts new file mode 100644 index 0000000..a849758 --- /dev/null +++ b/utils/wsclient/example_provision.ts @@ -0,0 +1,43 @@ +import Call, { Hooks } from './client' +import { Provision } from './client/calls'; + +const eConsole = new console.Console(process.stderr, process.stderr); + +// read API KEY +const API_KEY = process.env.API_KEY; +if (!API_KEY) { + eConsole.error('API_KEY not speciied') +} + +// READ ARGUMENTS +if (process.argv.length < 4) { + eConsole.error('Usage: API_KEY=$API_KEY