diff --git a/lib/nile/src/EventsApi.ts b/lib/nile/src/EventsApi.ts index f3e7e521..9ff23ce2 100644 --- a/lib/nile/src/EventsApi.ts +++ b/lib/nile/src/EventsApi.ts @@ -1,18 +1,55 @@ import { EntitiesApi, InstanceEvent } from './generated/openapi/src'; -export class EventsApi { +export type TimersType = { [key: number]: ReturnType }; +export type EventListenerOptions = { type: string; seq: number }; +export type ListenerCallback = (event: InstanceEvent) => Promise; + +export interface EventsApiInterface { + entities: EntitiesApi; + timers: TimersType; + on( + options: EventListenerOptions, + listener: ListenerCallback, + refresh?: number + ): number; + cancel(timerId: number): void; +} + +/** + * EventsApi - Interface + * @export + * @interface EventsApiInterface + */ +export default class EventsApi implements EventsApiInterface { static onCounter = 0; entities: EntitiesApi; - timers: { [key: number]: ReturnType }; + timers: TimersType; constructor(entities: EntitiesApi) { this.entities = entities; this.timers = {}; } + /** + * Listen for Nile events + * @example + * ```typescript + * import Nile from '@theniledev/js'; + * const nile = new Nile({ apiUrl: 'http://localhost:8080', workspace: 'myWorkspace' }); + * + * const listenerOptions = { + * type: 'myEntityType', + * seq: 0 // from the beginning of time + * }; + * + * nile.events.on(listenerOptions, (instanceEvent) => { + * console.log(JSON.stringify(instanceEvent, null, 2)); + * }); + * ``` + */ on( - options: { type: string; seq: number }, - listener: (event: InstanceEvent) => Promise, + options: EventListenerOptions, + listener: ListenerCallback, refresh = 5000 ): number { const id = EventsApi.onCounter++; @@ -41,11 +78,29 @@ export class EventsApi { return id; } - cancel(id: number): void { - const timer = this.timers[id]; + /** + * Remove and cancel a running timer + * @example + * ```typescript + * import Nile from '@theniledev/js'; + * const nile = new Nile({ apiUrl: 'http://localhost:8080', workspace: 'myWorkspace' }); + * + * const listenerOptions = { + * type: 'myEntityType', + * seq: 0 // from the beginning of time + * }; + * + * const timerId = nile.events.on(listenerOptions, (instanceEvent) => { + * console.log(JSON.stringify(instanceEvent, null, 2)); + * }); + * nile.events.cancel(timerId); + * ``` + */ + cancel(timerId: number): void { + const timer = this.timers[timerId]; if (timer) { clearTimeout(timer); - delete this.timers[id]; + delete this.timers[timerId]; } } } diff --git a/lib/nile/src/Nile.ts b/lib/nile/src/Nile.ts index fceb0a05..25ae26c0 100644 --- a/lib/nile/src/Nile.ts +++ b/lib/nile/src/Nile.ts @@ -4,7 +4,6 @@ import { DevelopersApi, EntitiesApi, - InstanceEvent, OrganizationsApi, UsersApi, WorkspacesApi, @@ -13,56 +12,7 @@ import { Configuration, ConfigurationParameters, } from './generated/openapi/src/runtime'; - -export class EventsApi { - static onCounter = 0; - entities: EntitiesApi; - timers: { [key: number]: ReturnType }; - - constructor(entities: EntitiesApi) { - this.entities = entities; - this.timers = {}; - } - - on( - options: { type: string; seq: number }, - listener: (event: InstanceEvent) => Promise, - refresh = 5000 - ): number { - const id = EventsApi.onCounter++; - let seq = options.seq; - const getEvents = async () => { - const events = await this.entities.instanceEvents({ - type: options.type, - seq, - }); - if (events) { - for (let i = 0; i < events.length; i++) { - const event = events[i]; - if (event) { - await listener(event); - if (seq == null || (event?.after?.seq || seq) > seq) { - seq = event?.after?.seq || seq; - } - } - } - } - const timer = setTimeout(getEvents, refresh); - this.timers[id] = timer; - }; - const timer = setTimeout(getEvents, 0); - this.timers[id] = timer; - return id; - } - - cancel(id: number): void { - const timer = this.timers[id]; - if (timer) { - clearTimeout(timer); - delete this.timers[id]; - } - } -} +import EventsApi from './EventsApi'; export class NileApi { users: UsersApi; diff --git a/lib/nile/templates/apis.mustache b/lib/nile/templates/apis.mustache index c5ddb765..5e661363 100644 --- a/lib/nile/templates/apis.mustache +++ b/lib/nile/templates/apis.mustache @@ -88,7 +88,7 @@ export class {{classname}} extends runtime.BaseAPI { *```typescript * import {{moduleName}} from '{{{projectName}}}'; * - * const nile = new {{{moduleName}}}({ apiUrl: 'http://localhost:8080', workspace: "myWorkspace" }); + * const nile = new {{{moduleName}}}({ apiUrl: 'http://localhost:8080', workspace: 'myWorkspace' }); * {{#hasParams}} * const body = { diff --git a/packages/events-example/src/commands/reconcile/index.ts b/packages/events-example/src/commands/reconcile/index.ts index 86544fc3..2a3af839 100644 --- a/packages/events-example/src/commands/reconcile/index.ts +++ b/packages/events-example/src/commands/reconcile/index.ts @@ -1,8 +1,9 @@ import { Command } from '@oclif/core'; import Nile, { Instance, NileApi } from '@theniledev/js'; +import { ReconciliationPlan } from '../../model/ReconciliationPlan'; + import { pulumiS3, PulumiAwsDeployment } from './lib/pulumi'; -import { ReconciliationPlan } from './ReconciliationPlan'; import { flagDefaults } from './flagDefaults'; // configuration for interacting with nile @@ -18,21 +19,16 @@ type DeveloperCreds = { export default class Reconcile extends Command { static enableJsonFlag = true; static description = 'reconcile nile/pulumi deploys'; + static flags = flagDefaults; deployment!: PulumiAwsDeployment; nile!: NileApi; - /** - * Runner for oclif - * @returns void - */ async run(): Promise { const { flags } = await this.parse(Reconcile); - const { status, - region, organization, entity, basePath, @@ -41,15 +37,14 @@ export default class Reconcile extends Command { password, } = flags; - // Nile setup + // nile setup await this.connectNile({ basePath, workspace, email, password }); const instances = await this.loadNileInstances(organization, entity); - // Pulumi setup + // pulumi setup this.deployment = await PulumiAwsDeployment.create( 'nile-examples', - pulumiS3, - { region } + pulumiS3 ); const stacks = await this.deployment.loadPulumiStacks(); @@ -79,7 +74,7 @@ export default class Reconcile extends Command { /** * sets up Nile instance, and set auth token to the logged in developer - * @param config Configuration for instantiating Nile and logging ing + * @param config Configuration for instantiating Nile and logging in */ async connectNile({ basePath, @@ -87,18 +82,17 @@ export default class Reconcile extends Command { email, password, }: NileConfig & DeveloperCreds) { - const developerPayload = { - loginInfo: { - email, - password, - }, - }; this.nile = Nile({ basePath, workspace, }); const token = await this.nile.developers - .loginDeveloper(developerPayload) + .loginDeveloper({ + loginInfo: { + email, + password, + }, + }) .catch((error: unknown) => { // eslint-disable-next-line no-console console.error('Nile authentication failed', error); diff --git a/packages/events-example/src/commands/reconcile/lib/pulumi/PulumiAwsDeployment.ts b/packages/events-example/src/commands/reconcile/lib/pulumi/PulumiAwsDeployment.ts index fa1eb7d8..ccdffde9 100644 --- a/packages/events-example/src/commands/reconcile/lib/pulumi/PulumiAwsDeployment.ts +++ b/packages/events-example/src/commands/reconcile/lib/pulumi/PulumiAwsDeployment.ts @@ -1,53 +1,45 @@ import { CliUx } from '@oclif/core'; import { + ConfigMap, DestroyResult, InlineProgramArgs, LocalWorkspace, PulumiFn, Stack, StackSummary, + UpdateSummary, UpResult, } from '@pulumi/pulumi/automation'; import { Instance } from '@theniledev/js'; -type AWSConfig = { - region: string; -}; -type PulumiFnGen = { - (staticContent: unknown): PulumiFn; -}; - -// eslint-disable-next-line no-console -const stackConfig = { onOutput: console.log }; +export interface PulumiFnGen { + (instance?: Instance): PulumiFn; +} export default class PulumiAwsDeployment { projectName!: string; private localWorkspace!: LocalWorkspace; private pulumiProgram: PulumiFnGen; - private awsConfig: AWSConfig; static async create( projectName: string, - pulumiProgram: PulumiFnGen, - awsConfig: AWSConfig + pulumiProgram: PulumiFnGen ): Promise { const ws = await LocalWorkspace.create({ projectSettings: { name: projectName, runtime: 'nodejs' }, }); ws.installPlugin('aws', 'v4.0.0'); - return new PulumiAwsDeployment(projectName, ws, pulumiProgram, awsConfig); + return new PulumiAwsDeployment(projectName, ws, pulumiProgram); } constructor( projectName: string, localWorkspace: LocalWorkspace, - pulumiProgram: PulumiFnGen, - awsConfig: AWSConfig + pulumiProgram: PulumiFnGen ) { this.projectName = projectName; this.localWorkspace = localWorkspace; this.pulumiProgram = pulumiProgram; - this.awsConfig = awsConfig; } async loadPulumiStacks(): Promise<{ [key: string]: StackSummary }> { @@ -55,7 +47,7 @@ export default class PulumiAwsDeployment { await this.localWorkspace.listStacks() ).reduce(async (accP, stack) => { const acc = await accP; - const fullStack = await this.getStack(stack.name, this.pulumiProgram({})); + const fullStack = await this.getStack(stack.name, this.pulumiProgram()); const info = await fullStack.info(); if (info?.kind != 'destroy') { acc[stack.name] = stack; @@ -65,21 +57,13 @@ export default class PulumiAwsDeployment { return stacks; } - async waitOnStack(stack: Stack): Promise { - let stackInfo; - do { - stackInfo = await stack.info(); - } while (stackInfo != undefined && stackInfo?.result !== 'succeeded'); - } - async getStack(stackName: string, program: PulumiFn): Promise { const args: InlineProgramArgs = { stackName, - projectName: 'tryhard', + projectName: this.projectName, program, }; const stack = await LocalWorkspace.createOrSelectStack(args); - await stack.setConfig('aws:region', { value: this.awsConfig.region }); return stack; } @@ -88,21 +72,48 @@ export default class PulumiAwsDeployment { instance.id, this.pulumiProgram(instance) ); + await this.configureStack(stack, instance); await this.waitOnStack(stack); + try { CliUx.ux.action.start(`Creating a stack id=${instance.id}`); - return await stack.up(stackConfig); + // eslint-disable-next-line no-console + return await stack.up({ onOutput: console.log }); } finally { CliUx.ux.action.stop(); } } + private async configureStack(stack: Stack, instance: Instance) { + const instanceProps = instance.properties as { config: ConfigMap }; + const stackConfig = instanceProps?.config ?? { 'aws:region': 'us-east-2' }; + + for (const key of Object.keys(stackConfig)) { + await stack.setConfig(key, { value: `${stackConfig[key]}` }); + } + } + + private async waitOnStack(stack: Stack): Promise { + let stackInfo; + do { + stackInfo = await stack.info(); + } while (this.isUnresolved(stackInfo)); + } + + private isUnresolved(stackInfo: UpdateSummary | undefined): boolean { + return ( + stackInfo != undefined && + !(stackInfo?.result == 'succeeded' || stackInfo?.result == 'failed') + ); + } + async destroyStack(id: string): Promise { - const stack = await this.getStack(id, this.pulumiProgram({})); + const stack = await this.getStack(id, this.pulumiProgram()); await this.waitOnStack(stack); try { CliUx.ux.action.start(`Destroying a stack id=${id}`); - return await stack.destroy(stackConfig); + // eslint-disable-next-line no-console + return await stack.destroy({ onOutput: console.log }); } finally { CliUx.ux.action.stop(); } diff --git a/packages/events-example/src/commands/reconcile/lib/pulumi/pulumiS3.ts b/packages/events-example/src/commands/reconcile/lib/pulumi/pulumiS3.ts index 7ead1ea6..08e5f7fa 100644 --- a/packages/events-example/src/commands/reconcile/lib/pulumi/pulumiS3.ts +++ b/packages/events-example/src/commands/reconcile/lib/pulumi/pulumiS3.ts @@ -2,7 +2,7 @@ import * as aws from '@pulumi/aws'; import { PolicyDocument } from '@pulumi/aws/iam'; import { Instance } from '@theniledev/js'; -export const pulumiProgram = (something: any) => { +export const pulumiS3 = (instance?: Instance) => { return async () => { // Create a bucket and expose a website index document. const siteBucket = new aws.s3.Bucket('s3-website-bucket', { @@ -11,14 +11,17 @@ export const pulumiProgram = (something: any) => { }, }); - const instanceProps = instance?.properties as { [key: string]: any }; + const instanceProps = instance?.properties as { [key: string]: unknown }; const greeting = instanceProps?.greeting || 'Hello, world!'; const indexContent = ` Hello S3 -

Hello, world!

Made with ❤️ with Pulumi

-

deployed with nile

-

${JSON.stringify(jsonBlob)}

+ +

${greeting}

+

Made with ❤️ with Pulumi

+

Deployed with nile

+

Instance Details

+

${JSON.stringify(instance, null, 2)}

`;