Răsfoiți Sursa

Reactor to vorpal

Alan Colon 7 ani în urmă
părinte
comite
426f79cd8e
3 a modificat fișierele cu 93 adăugiri și 66 ștergeri
  1. 83 61
      bin/portainer.js
  2. 6 4
      lib/client.js
  3. 4 1
      package.json

+ 83 - 61
bin/portainer.js

@@ -1,73 +1,95 @@
 #!/usr/bin/env node
 const { createClient } = require('../lib/client')
 const getConfig = require('microservice-config')
-
-const help = () => {
-    console.log(`
-        portainer command line interface
-
-        Usage: npx portainer <command> [options]
-
-        Global settings:
-
-        --username <username>
-        or PORTAINER_USERNAME=<username>
-        
-        --password <password>
-        or PORTAINER_PASSWORD=<password>
-
-        --url <url>
-        or PORTAINER_URL=<url>
-        example: --url http://server:9000
-
-        --verbose
-
-        Commands:
-        
-        deploy - Creates or updates a stack
-        
-        --name <name> - Name of the stack.
-        --source-file <path to yml> - Path to docker-compose.yml.
-
-        delete - Deletes a stack. Requires either name or id.
-
-        --name <name> - Name of the stack.
-        --id <id> - ID of the stack
-    `.trim().split('\n').map(x => x.trim()).join('\n'))
-}
+const passwordPrompt = require('password-prompt')
+const asTable = require('as-table')
+const Vorpal = require('vorpal')
 
 const main = async() => {
-    const config = getConfig()
-
-    if (config.help) return help()
-    if (config._.length !== 1) return help()
-
-    const tf = x => x !== 'false' && x !== 'False' && x !== 0 && !!x
 
-    const client = createClient({
-        username: config.username || config.PORTAINER_USERNAME,
-        password: config.password || config.PORTAINER_PASSWORD,
-        url: config.url || config.PORTAINER_URL,
-        verbose: tf(config.verbose || config.v)
-    })
-
-    const opts = {
-        name: config.name,
-        id: config.id,
-        endpointId: config['endpoint-id'],
-        stackFile: config['stack-file'],
-        stackFileContent: config['stack-file-content'],
-        swarmId: config['swarm-id'],
-        prune: tf(config.prune),
-        warning: tf(config.warning)
+    const vorpal = new Vorpal()
+    function log(...args) { vorpal.activeCommand.log(...args) }
+
+    const fillModel = async model => {
+        if (!model.options) model.options = {}
+        model.options.username = model.options.username || process.env.PORTAINER_USERNAME
+        if (!model.options.username) throw new Error('Username is required.')
+        model.options.password = model.options.password || process.env.PORTAINER_PASSWORD || await passwordPrompt('Password: ', {method: 'hide'})
+        if (!model.options.password) throw new Error('Password is required.')
+        model.options.url = model.options.url || process.env.PORTAINER_URL
+        if (!model.options.url) throw new Error('URL is required.')
+        if (model.options.verbose) {
+            log('options', JSON.stringify({
+                ...model,
+                options: {
+                    ...model.options,
+                    password: '******'
+                }
+            }, null, 4))
+        }
     }
-
-    switch (config._[0]) {
-        case 'deploy': await client.upsertStack(opts); break
-        case 'delete': await client.deleteStack(opts); break
-        default: help()
+    
+    const command = commandSpec => vorpal.command(commandSpec)
+        .option('-v, --verbose', 'Enable verbose logging')
+        .option('-u, --username <username>', 'Portainer username. Defaults to environment variable PORTAINER_USERNAME.')
+        .option('-p, --password <password>', 'Portainer password. Defaults to environment variable PORTAINER_PASSWORD.')
+        .option('-r, --url <url>', 'Portainer URL, such as http://server:9000. Defaults to environment variable PORTAINER_URL.')
+        .option('-j, --json', 'Format JSON.')
+    const action = handler => async function(model, cb) {
+        try {
+            await fillModel(model)
+            const client = createClient({
+                log,
+                ...model.options
+            })
+            const result = await handler(client, model)
+            if (result) {
+                if (!model.options.json) {
+                    if (Array.isArray(result)) {
+                        log(asTable(result))
+                    } else {
+                        log(asTable([result]))
+                    }
+                } else {
+                    log(JSON.stringify(result))
+                }
+            }
+            cb()
+        } catch (err) {
+            console.warn(err.message || err)
+            process.exit(1)
+        }
     }
 
+    command('stack deploy <name>', 'Creates or updates a stack.')
+    .option('-f, --file <yaml_file>', 'Path to docker-compose.yml file. Defaults to ./docker-compose.yml.')
+    .option('-P, --prune', 'Prune obsolete or orphaned containers. Defaults to true.')
+    .option('-W, --warning', 'Add warning message to head of yaml. Defaults to true.')
+    .action(action(async (client, model) => {
+        if (model.options.prune === undefined) model.options.prune = true
+        if (model.options.warning === undefined) model.options.warning = true
+        if (model.options.file === undefined) model.options.file = 'docker-compose.yml'
+        log('Upserting stack')
+        return await client.upsertStack({
+                stackFile: model.options.file,
+                ...model,
+                ...model.options
+            })
+    }))
+
+    command('stack ls', 'List stacks')
+    .action(action(async (client, model) => await client.getStacks()))
+
+    command('stack rm <name>', 'Removes a stack')
+    .action(action(async (client, model) => await client.deleteStack(model)))
+
+    command('endpoint ls', 'List endpoints')
+    .action(action(async (client, model) => await client.getEndpoints()))
+
+    await vorpal.parse(process.argv).catch(err => {
+        console.error(err)
+        process.exit(1)
+    })
 }
 
 main().catch(err => {

+ 6 - 4
lib/client.js

@@ -13,14 +13,16 @@ const createClient = (config) => {
         username: null,
         password: null
     }, config)
+    const log = config.log || console.log
+
     const warning = `# ${figlet.textSync('WARNING!', 'colossal').split('\n').join('\n# ')}\n# This stack was deployed automatically. Any changes made here may be overwritten.\n#\n\n`
 
     const client = new PortainerClient(config.url, config.username, config.password);
 
     const call = async (method, path, data) => {
-        if (config.verbose) console.info(method, path, data ? JSON.stringify(data, null, 4) : '')
+        if (config.verbose) log(method, path, data ? JSON.stringify(data, null, 4) : '')
         const result = await client.callApiWithKey(method, path, data)
-        if (config.verbose) console.info(JSON.stringify(result, null, 4))
+        if (config.verbose) log(JSON.stringify(result, null, 4))
         return result
     }
 
@@ -83,7 +85,7 @@ const createClient = (config) => {
         const stacks = await getStacks()
         const stack = stacks.find(x => x.Name === opts.name)
         if (stack) {
-            console.log(`Found existing stack ${opts.name}. Updating...`)
+            log(`Found existing stack ${opts.name}. Updating...`)
             return await call('PUT', `/api/stacks/${encodeURIComponent(stack.Id)}?method=${encodeURIComponent(opts.method)}&type=${encodeURIComponent(type)}&endpointId=${encodeURIComponent(opts.endpointId)}`, {
                 Name: opts.name,
                 StackFileContent: (opts.warning ? warning : '') + opts.stackFileContent,
@@ -91,7 +93,7 @@ const createClient = (config) => {
                 Prune: opts.prune
             })
         } else {
-            console.log(`New stack ${opts.name}`)
+            log(`New stack ${opts.name}`)
             return await call('POST', `/api/stacks?method=${encodeURIComponent(opts.method)}&type=${encodeURIComponent(type)}&endpointId=${encodeURIComponent(opts.endpointId)}`, {
                 Name: opts.name,
                 StackFileContent: (opts.warning ? warning : '') + opts.stackFileContent,

+ 4 - 1
package.json

@@ -12,8 +12,11 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
+    "as-table": "^1.0.36",
     "figlet": "^1.2.1",
     "microservice-config": "^0.0.1",
-    "portainer-api-client": "^0.0.2"
+    "password-prompt": "^1.0.7",
+    "portainer-api-client": "^0.0.2",
+    "vorpal": "^1.12.0"
   }
 }