|
|
@@ -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 => {
|