Alan Colon 7 năm trước cách đây
commit
ab2fab4975
4 tập tin đã thay đổi với 202 bổ sung0 xóa
  1. 2 0
      .npmignore
  2. 76 0
      bin/portainer.js
  3. 105 0
      lib/client.js
  4. 19 0
      package.json

+ 2 - 0
.npmignore

@@ -0,0 +1,2 @@
+docker-compose.yml
+deploy.js

+ 76 - 0
bin/portainer.js

@@ -0,0 +1,76 @@
+#!/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 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)
+    }
+
+    switch (config._[0]) {
+        case 'deploy': await client.upsertStack(opts); break
+        case 'delete': await client.deleteStack(opts); break
+        default: help()
+    }
+
+}
+
+main().catch(err => {
+    console.error(err)
+    process.exit(1)
+})

+ 105 - 0
lib/client.js

@@ -0,0 +1,105 @@
+const PortainerClient = require('portainer-api-client')
+const fs = require('fs')
+const figlet = require('figlet')
+
+const TYPES = {
+    swarm: 1,
+    compose: 2
+}
+
+const createClient = (config) => {
+    config = Object.assign({
+        url: null,
+        username: null,
+        password: null
+    }, config)
+    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) : '')
+        const result = await client.callApiWithKey(method, path, data)
+        if (config.verbose) console.info(JSON.stringify(result, null, 4))
+        return result
+    }
+
+    const getEndpointSwarm = async (endpointId) => await call('get', `/api/endpoints/${encodeURIComponent(endpointId)}/docker/swarm`)
+    const getEndpoints = async () => await call('get', '/api/endpoints')
+    const getSwarms = async () => await call('get', '/api/swarms')
+    const getStacks = async () => await call('get', '/api/stacks')
+    const deleteStack = async (opts) => {
+        opts = Object.assign({
+            name: null,
+            id: null,
+            endpointId: null
+        }, opts)
+        if (!opts.id) {
+            if (!opts.name) {
+                throw new Error('Either name or id required')
+            }
+            const stacks = await getStacks()
+            const stack = stacks.find(x => x.Name === opts.name)
+            if (!stack) throw new Error(`Stack ${opts.name} not found.`)
+            opts.id = stack.Id
+        }
+        if (!opts.endpointId) {
+            const endpoints = await getEndpoints()
+            if (!endpoints || !endpoints.length) throw new Error('Could not find any endpoints')
+            opts.endpointId = endpoints[0].Id
+        }
+        console.log(`Deleting stack ${opts.id}`)
+        return call('delete', `/api/stacks/${encodeURIComponent(opts.id)}`)
+    }
+    const upsertStack = async (opts) => {
+        opts = Object.assign({
+            name: null,
+            stackFile: null,
+            stackFileContent: null,
+            swarmId: null,
+            endpointId: null,
+            type: 'swarm',
+            method: 'string',
+            prune: true,
+            warning: true
+        }, opts)
+        if (!opts.name) throw new Error('name required')
+        if (!opts.stackFileContent) {
+            if (!opts.stackFile) {
+                throw new Error('stackFile or stackFileContent required')
+            }
+            opts.stackFileContent = fs.readFileSync(opts.stackFile, 'utf-8')
+        }
+        const type = TYPES[opts.type] || opts.type
+        if (!opts.endpointId) {
+            const endpoints = await getEndpoints()
+            if (!endpoints || !endpoints.length) throw new Error('Could not find any endpoints')
+            opts.endpointId = endpoints[0].Id
+        }
+        if (!opts.swarmId) {
+            const swarm = await getEndpointSwarm(opts.endpointId)
+            opts.swarmId = swarm.ID
+        }
+        const stacks = await getStacks()
+        const stack = stacks.find(x => x.Name === opts.name)
+        if (stack) {
+            console.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,
+                SwarmID: opts.swarmId,
+                Prune: opts.prune
+            })
+        } else {
+            console.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,
+                SwarmID: opts.swarmId
+            })
+        }
+    }
+    return { getEndpoints, getSwarms, getStacks, upsertStack, deleteStack }
+}
+
+module.exports = { createClient }

+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+  "name": "portainer",
+  "version": "0.0.1",
+  "description": "Portainer command line deployment",
+  "main": "lib/client.js",
+  "bin": {
+    "portainer": "./bin/portainer.js"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "figlet": "^1.2.1",
+    "microservice-config": "^0.0.1",
+    "portainer-api-client": "^0.0.2"
+  }
+}