-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
638f1a5
commit 01ab098
Showing
4 changed files
with
228 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
#!/usr/bin/env node | ||
|
||
// We use cjs because mjs requires to import the full .mjs path (and breaks) | ||
require('../cjs/lambda-plugin') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { Readable } from 'stream' | ||
|
||
export interface TargetImpl { | ||
default: (args: string[], opts: Opts, stream: Readable) => Promise<number> | ||
} | ||
|
||
export interface Opts { | ||
force: boolean | ||
json: boolean | ||
quiet: boolean | ||
error: (message: string) => void | ||
out: <T extends Object>(obj: T, short: (input: T) => string, long: (input: T) => string) => void | ||
} | ||
|
||
export function isEmptyString (input: string | null | undefined): input is null | undefined { | ||
return input === null || input === undefined || input === '' | ||
} | ||
|
||
async function getTargetImpl (target: string): Promise<TargetImpl | undefined> { | ||
if (target === 's3') { | ||
return await import('./lambda-plugins-s3') | ||
} | ||
} | ||
|
||
function help (): void { | ||
console.log(`lambda-plugins s3 - s3://bucket/path | ||
CLI tool based on the "aws" cli provided by amazon. | ||
Common usage: | ||
$ npm pack --loglevel silent | lambda-plugins - s3://bucket/path | ||
to deploy packages. It will warn you if a given package already | ||
exists and return the s3 paths to be used for looking up the | ||
plugin. It also works in combination with lerna: | ||
$ npx lerna exec "npm pack --loglevel silent | lambda-plugins s3 - s3://bucket" | ||
to deploy several plugins in a plugin directory at once. | ||
It is also possible to publish only simple file using: | ||
$ lambda-plugins s3 myplugin.tgz s3://bucket/path | ||
Note: Currently only deploying to s3 is supported. | ||
`) | ||
} | ||
|
||
;(async function main () { | ||
let args = process.argv.slice(2) | ||
const target = args.shift() | ||
if (isEmptyString(target)) { | ||
help() | ||
return 1 | ||
} | ||
const impl = await getTargetImpl(target) | ||
if (impl === undefined) { | ||
console.log('The first cli argument needs to be "s3", the currently only supported target.') | ||
return 1 | ||
} | ||
const opts: Opts = { | ||
force: false, | ||
quiet: false, | ||
json: false, | ||
out: () => {}, | ||
error: () => {} | ||
} | ||
args = args.map(arg => arg.trim()).filter(arg => { | ||
if (arg === '-f' || arg === '--force') { | ||
opts.force = true | ||
return false | ||
} | ||
if (arg === '--json') { | ||
opts.json = true | ||
return false | ||
} | ||
if (arg === '-q' || arg === '--quiet') { | ||
opts.quiet = true | ||
return false | ||
} | ||
return true | ||
}) | ||
opts.error = opts.json | ||
? error => console.log(JSON.stringify({ error })) | ||
: error => console.log(error.toString()) | ||
opts.out = opts.quiet | ||
? (obj, short, long) => console.log(short(obj)) | ||
: opts.json | ||
? (obj, short, long) => console.log(JSON.stringify(obj)) | ||
: (obj, short, long) => console.log(long(obj)) | ||
return await impl.default(args, opts, process.stdin) | ||
})().then( | ||
code => process.exit(code), | ||
err => { | ||
console.error(err) | ||
process.exit(1) | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
|
||
import { spawn } from 'child_process' | ||
import { access } from 'fs/promises' | ||
import { Readable } from 'stream' | ||
import { isEmptyString, Opts } from './lambda-plugin' | ||
|
||
async function listFiles (file: string, stdin: Readable): Promise<string[]> { | ||
if (file !== '-') { | ||
return [file] // already trimmed | ||
} | ||
stdin.resume() | ||
let buffer: string = '' | ||
let files: string[] = [] | ||
for await (const chunk of stdin) { | ||
buffer += (chunk as string | Buffer).toString() | ||
const lines = buffer.split('\n') | ||
buffer = lines.pop() ?? '' | ||
files = files.concat(lines) | ||
} | ||
files.push(buffer) | ||
return files | ||
.map(file => file.trim()) | ||
.filter(file => !(file === '' || file.startsWith('#'))) | ||
} | ||
|
||
/* eslint-disable-next-line @typescript-eslint/promise-function-async */ | ||
function simpleSpawn (cmd: string, args: string[]): Promise<{ code: number, out: string }> { | ||
return new Promise((resolve) => { | ||
const p = spawn(cmd, args) | ||
const chunks: Buffer[] = [] | ||
p.stderr.on('data', out => chunks.push(out)) | ||
p.stdout.on('data', out => chunks.push(out)) | ||
p.on('error', error => resolve({ code: 1, out: error.stack ?? String(error) })) | ||
p.on('exit', code => { | ||
resolve({ code: code ?? 0, out: Buffer.concat(chunks).toString() }) | ||
}) | ||
}) | ||
} | ||
|
||
interface FoundFile { | ||
date: string | ||
time: string | ||
size: string | ||
file: string | ||
} | ||
|
||
async function findFile (file: string, bucket: string): Promise<FoundFile | undefined> { | ||
const location = `${bucket}/${file}` | ||
const { out, code } = await simpleSpawn('aws', ['s3', 'ls', location]) | ||
if (code === 0) { | ||
for (const s3Line of out.split('\n')) { | ||
const [date, time, size, foundFile] = s3Line.split(/\s+/) | ||
if (foundFile === file) { | ||
return { date, time, size, file } | ||
} | ||
} | ||
} | ||
} | ||
|
||
async function upload (file: string, bucket: string): Promise<string> { | ||
const { out, code } = await simpleSpawn('aws', ['s3', 'cp', file, bucket]) | ||
if (code !== 0) { | ||
throw new Error(`Error while uploading ${file} to ${bucket}: ${out}`) | ||
} | ||
return `${bucket}/${file}` | ||
} | ||
|
||
export default async function s3 (args: string[], opts: Opts, stdin: Readable): Promise<number> { | ||
const { force, error, out } = opts | ||
const [input, bucket] = args | ||
if (isEmptyString(input)) { | ||
error('Argument Error: first command line argument - input - needs to be defined.') | ||
return 2 | ||
} | ||
if (isEmptyString(bucket)) { | ||
error('Argument Error: second command line argument - bucket - needs to be defined.') | ||
return 3 | ||
} | ||
const files = await listFiles(input, stdin) | ||
if (files.length === 0) { | ||
error('No files to deploy') | ||
return 4 | ||
} | ||
if (!force) { | ||
const foundFiles = (await Promise.all(files.map(async file => await findFile(file, bucket)))).filter(Boolean) as FoundFile[] | ||
if (foundFiles.length === files.length) { | ||
out( | ||
{ files: foundFiles }, | ||
({ files }) => 'pre-existing: ' + files.map(file => `${bucket}/${file.file}`).join('\n'), | ||
({ files }) => `Already deployed, no redeploy with -f flag: | ||
- ${files.map(({ date, time, file }) => `${date} ${time} ${file}`).join('\n - ')} | ||
` | ||
) | ||
return 0 | ||
} | ||
if (foundFiles.length > 0) { | ||
out( | ||
{ files: foundFiles }, | ||
({ files }) => 'unchanged: ' + files.map(file => `${bucket}/${file.file}`).join('\n'), | ||
({ files }) => `Deploy to s3 cancelled. Already deployed file found! | ||
Every deploy should have an unique file name (maybe update the package version?) | ||
If you wish to deploy anyways: pass in the -f option. | ||
Following files already deployed: | ||
- ${files.map(({ date, time, size, file }) => `${date} ${time} ${size} ${bucket}/${file}`).join('\n - ')} | ||
` | ||
) | ||
return 5 | ||
} | ||
} | ||
await Promise.all(files.map(async file => await access(file))) | ||
const uploaded = await Promise.all(files.map(async file => await upload(file, bucket))) | ||
out( | ||
{ files: uploaded }, | ||
({ files }) => 'uploaded: ' + files.join('\n'), | ||
({ files }) => `Uploaded following plugins: | ||
- ${files.join('\n - ')} | ||
` | ||
) | ||
return 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters