use docker api client dockerode instead of `docker` child process (#61)
- improves performance slightly as no child process execution is required
- easier to use API than building cli arguments & parsing stdout
- red colored stderr output of container
- parse dockerArgs manually, support -e and -v for now
- use fast ping for docker availability check
- fix namespace display bug if action is inside a package
diff --git a/README.md b/README.md
index e9ad006..54399e8 100644
--- a/README.md
+++ b/README.md
@@ -685,12 +685,12 @@
<a name="default-debug-ports-and-commands"></a>
#### Default debug ports and commands
-To just add default debug ports and docker command for a kind, add a custom debug kind and export an object with `description`, `port` and `command` fields. Optionally `dockerArgs` for extra docker arguments (such as passing in environment variables using `-e` if necessary).
+To just add default debug ports and docker command for a kind, add a custom debug kind and export an object with `description`, `port` and `command` fields. Optionally implement `updateContainerConfig()` for extra container settings (such as passing in environment variables using `-e` or mounting volumes).
<a name="support-code-reloading"></a>
#### Support code reloading
-To support live code reloading/mounting, add a custom debug kind and export an object with a `mountAction` function. This has to return an action that dynamically loads the code at the start of each activation. A typical approach is to mount the `<source-path>` (folder) passed on the cli as `/code` inside the docker container, from where the mount action can reload it. The exact mechanism will depend on the language - in node.js for example, `eval()` is [used for plain actions](src/kinds/nodejs/mount-plain.js#L30). The docker mounting can be specified in `dockerArgs`.
+To support live code reloading/mounting, add a custom debug kind and export an object with a `mountAction` function. This has to return an action that dynamically loads the code at the start of each activation. A typical approach is to mount the `<source-path>` (folder) passed on the cli as `/code` inside the docker container, from where the mount action can reload it. The exact mechanism will depend on the language - in node.js for example, `eval()` is [used for plain actions](src/kinds/nodejs/mount-plain.js#L30). The docker mounting can be specified in `updateContainerConfig()`.
The `mountAction(invoker)` must return an object that is an openwhisk action `/init` definition, which consists of:
diff --git a/package-lock.json b/package-lock.json
index ae04f84..2c5ed8e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -462,6 +462,14 @@
"integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=",
"dev": true
},
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
"astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
@@ -479,11 +487,34 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
+ "base64-js": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
"binary-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow=="
},
+ "bl": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
+ "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
+ "requires": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -508,6 +539,20 @@
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
+ "buffer": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
+ "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
+ "requires": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+ },
"caching-transform": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
@@ -567,6 +612,11 @@
"readdirp": "~3.3.0"
}
},
+ "chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ },
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -645,6 +695,17 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
+ "concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
"convert-source-map": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
@@ -733,6 +794,27 @@
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
+ "docker-modem": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.2.tgz",
+ "integrity": "sha512-fwlfnsK9WV+m+qc/NZCiGt+oYAMjmCUeir0a/l3oHb0yc8FhRAucdwT4htKD3aLtV+1w2syQePH9pQFxsq1GFA==",
+ "requires": {
+ "debug": "^4.1.1",
+ "readable-stream": "^3.5.0",
+ "split-ca": "^1.0.1",
+ "ssh2": "^0.8.7"
+ }
+ },
+ "dockerode": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.2.0.tgz",
+ "integrity": "sha512-C+y/W4Kks7YLBsfUOTMkk1IVilb4cdj+rE+UZ5hnE+rpcn2frSs7kX+6H8GteTqHcv8sln+GyxuP1qdno3IzIw==",
+ "requires": {
+ "concat-stream": "~2.0.0",
+ "docker-modem": "^2.1.0",
+ "tar-fs": "~2.0.1"
+ }
+ },
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -755,6 +837,14 @@
"iconv-lite": "~0.4.13"
}
},
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
"es-abstract": {
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
@@ -1202,6 +1292,11 @@
"integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==",
"dev": true
},
+ "fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+ },
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -1250,8 +1345,7 @@
"get-port": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
- "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
- "dev": true
+ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="
},
"glob": {
"version": "7.1.6",
@@ -1402,6 +1496,11 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
+ "ieee754": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+ },
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -1452,8 +1551,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"inquirer": {
"version": "7.1.0",
@@ -1971,6 +2069,11 @@
"minimist": "^1.2.5"
}
},
+ "mkdirp-classic": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz",
+ "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g=="
+ },
"mocha": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz",
@@ -2472,7 +2575,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
"requires": {
"wrappy": "1"
}
@@ -2690,6 +2792,15 @@
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
"dev": true
},
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -2702,6 +2813,16 @@
"integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==",
"dev": true
},
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
"readdirp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
@@ -2922,12 +3043,35 @@
}
}
},
+ "split-ca": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
+ "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY="
+ },
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
+ "ssh2": {
+ "version": "0.8.9",
+ "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz",
+ "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==",
+ "requires": {
+ "ssh2-streams": "~0.4.10"
+ }
+ },
+ "ssh2-streams": {
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz",
+ "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==",
+ "requires": {
+ "asn1": "~0.2.0",
+ "bcrypt-pbkdf": "^1.0.2",
+ "streamsearch": "~0.1.2"
+ }
+ },
"stream-events": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
@@ -2937,6 +3081,11 @@
"stubs": "^3.0.0"
}
},
+ "streamsearch": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+ "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+ },
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
@@ -2989,6 +3138,21 @@
"es-abstract": "^1.17.5"
}
},
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+ "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+ }
+ }
+ },
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@@ -3082,6 +3246,29 @@
}
}
},
+ "tar-fs": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
+ "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
+ "requires": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.0.0"
+ }
+ },
+ "tar-stream": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz",
+ "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==",
+ "requires": {
+ "bl": "^4.0.1",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ }
+ },
"teeny-request": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz",
@@ -3155,6 +3342,11 @@
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==",
"dev": true
},
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+ },
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
@@ -3170,6 +3362,11 @@
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
},
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -3199,6 +3396,11 @@
"integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=",
"dev": true
},
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -3299,8 +3501,7 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write": {
"version": "1.0.3",
diff --git a/package.json b/package.json
index 3c90c97..61a4fa0 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,10 @@
"chalk": "^4.0.0",
"clone": "^2.1.2",
"debug": "^4.1.1",
+ "dockerode": "^3.2.0",
"fetch-retry": "^3.1.0",
"fs-extra": "^8.1.0",
+ "get-port": "^5.1.1",
"isomorphic-fetch": "^2.2.1",
"livereload": "^0.9.1",
"manakin": "^0.5.2",
@@ -65,7 +67,6 @@
"eslint": "^6.8.0",
"eslint-config-problems": "^4.0.0",
"eslint-plugin-mocha": "^6.3.0",
- "get-port": "^5.1.1",
"mocha": "^7.1.0",
"mocha-multi-reporters": "^1.1.7",
"mock-require": "^3.0.3",
diff --git a/src/debugger.js b/src/debugger.js
index e7d250b..e0db32a 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -36,6 +36,12 @@
}
}
+function getNamespaceFromActionMetadata(actionMetadata) {
+ // if the action is inside a package, this returns <namespace>/<package>
+ // but we only want the namespace
+ return actionMetadata.namespace.split("/")[0];
+}
+
/**
* Central component of wskdebug.
*/
@@ -74,7 +80,7 @@
// get the action metadata
this.actionMetadata = await this.agentMgr.peekAction();
log.debug("fetched action metadata from openwhisk");
- this.wskProps.namespace = this.actionMetadata.namespace;
+ this.wskProps.namespace = getNamespaceFromActionMetadata(this.actionMetadata);
const h = log.highlightColor;
log.step("Debugging " + h(`/${this.wskProps.namespace}/${this.actionName}`) + " on " + h(this.wskProps.apihost));
@@ -82,6 +88,9 @@
// local debug container
this.invoker = new OpenWhiskInvoker(this.actionName, this.actionMetadata, this.argv, this.wskProps, this.wsk);
+ // quick fail for missing requirements such as docker not running
+ await this.invoker.checkIfDockerAvailable();
+
try {
// run build initially (would be required by starting container)
if (this.argv.onBuild) {
@@ -95,7 +104,6 @@
// task 1 - start local container
const containerTask = (async () => {
const debug2 = log.newDebug();
- log.spinner('Starting local container');
// start container - get it up fast for VSCode to connect within its 10 seconds timeout
await this.invoker.startContainer(debug2);
@@ -277,21 +285,29 @@
log.log();
log.log();
log.debug("shutting down...");
- log.spinner("Shutting down");
} else {
log.debug("aborting start - shutting down ...");
}
+ log.spinner("Shutting down");
// need to shutdown everything even if some fail, hence tryCatch() for each
if (this.agentMgr) {
await this.tryCatch(this.agentMgr.shutdown());
}
+
+ // ------------< critical removal must happen above this line >---------------
+
+ // in VS Code, we will not run beyond this line upon debug stop.
+ // this is because invoker.stop() will kill the container & thus close the
+ // debug port, upon which VS Code kills the debug process (us)
if (this.invoker) {
await this.tryCatch(this.invoker.stop());
- log.debug(`stopped container: ${this.invoker.name()}`);
}
+
if (this.watcher) {
+ // this is not critical on a process exit, only if Debugger is used programmatically
+ // and might be reused for a new run()
await this.tryCatch(this.watcher.stop());
log.debug("stopped source file watching");
}
diff --git a/src/dockerutils.js b/src/dockerutils.js
new file mode 100644
index 0000000..0298c63
--- /dev/null
+++ b/src/dockerutils.js
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const yargsParser = require('yargs-parser');
+
+function safeContainerName(name) {
+ // docker container names are restricted to [a-zA-Z0-9][a-zA-Z0-9_.-]*
+
+ // 1. replace special characters with dash
+ name = name.replace(/[^a-zA-Z0-9_.-]+/g, '-');
+ // 2. leading character is more limited
+ name = name.replace(/^[^a-zA-Z0-9]+/g, '');
+ // 3. (nice to have) remove trailing special chars
+ name = name.replace(/[^a-zA-Z0-9]+$/g, '');
+
+ return name;
+}
+
+// convert docker run cli args to docker create container config
+// https://docs.docker.com/engine/reference/commandline/run/
+// https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
+function dockerRunArgs2CreateContainerConfig(args, containerConfig) {
+ if (!args) {
+ return containerConfig;
+ }
+
+ containerConfig = containerConfig || {};
+
+ const argv = yargsParser(args.split(" "));
+
+ for (const [key, value] of Object.entries(argv)) {
+ // treat all as array, makes it simpler below
+ const values = Array.isArray(value) ? value : [ value ];
+
+ switch (key) {
+ case "e": // environment variables
+ values.forEach(e => containerConfig.Env.push(e));
+ break;
+ case "v": // volume mounts (binds)
+ values.forEach(v => containerConfig.HostConfig.Binds.push(v));
+ break;
+ case "_": // ignore yargs specials
+ case "$0":
+ break;
+ default:
+ throw new Error(`Unsupported argument in --dockerArgs: '-${key}'. Please report at https://github.com/apache/openwhisk-wskdebug/issues`)
+ }
+ }
+
+ return containerConfig;
+}
+
+module.exports = {
+ safeContainerName,
+ dockerRunArgs2CreateContainerConfig
+};
diff --git a/src/invoker.js b/src/invoker.js
index 32b9d65..0fcd536 100644
--- a/src/invoker.js
+++ b/src/invoker.js
@@ -17,14 +17,18 @@
'use strict';
-const { spawn, execSync } = require('child_process');
const fetch = require('fetch-retry')(require('isomorphic-fetch'));
const kinds = require('./kinds/kinds');
const path = require('path');
const log = require("./log");
+const Docker = require('dockerode');
+const getPort = require('get-port');
+const dockerUtils = require('./dockerutils');
+const prettyBytes = require('pretty-bytes');
const RUNTIME_PORT = 8080;
-const INIT_RETRY_DELAY_MS = 100;
+const MAX_INIT_RETRY_MS = 20000; // 20 sec
+const INIT_RETRY_DELAY_MS = 200;
// https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md#system-limits
const OPENWHISK_DEFAULTS = {
@@ -32,18 +36,6 @@
memory: 256
};
-function execute(cmd, options, debug2) {
- cmd = cmd.replace(/\s+/g, ' ');
- const result = execSync(cmd, options);
-
- (debug2 || log.debug)(`executed: ${cmd}`);
- if (result) {
- return result.toString().trim();
- } else {
- return '';
- }
-}
-
// if value is a function, invoke it with args, otherwise return it as object
// if value is undefined, will return undefined
function resolveValue(value, ...args) {
@@ -79,12 +71,14 @@
this.wskProps = wskProps;
this.wsk = wsk;
- this.containerName = this.asContainerName(`wskdebug-${this.action.name}-${Date.now()}`);
+ this.containerName = dockerUtils.safeContainerName(`wskdebug-${this.action.name}-${Date.now()}`);
+ this.docker = new Docker();
}
async checkIfDockerAvailable() {
try {
- execute("docker info", {stdio: 'ignore'});
+ await this.docker.ping();
+ log.debug("docker - availability check")
} catch (e) {
throw new Error("Docker not running on local system. A local docker environment is required for the debugger.")
}
@@ -183,20 +177,72 @@
}
}
- async startContainer(debug2) {
- let showDockerRunOutput = log.isVerbose;
-
- // quick fail for missing requirements such as docker not running
- await this.checkIfDockerAvailable();
-
+ async isImagePresent(image, debug) {
try {
- execute(`docker inspect --type=image ${this.image} 2> /dev/null`);
+ await this.docker.getImage(image).inspect();
+ debug(`docker - image inspected, is present: ${image}`);
+ return true;
} catch (e) {
- // make sure the user can see the image download process as part of docker run
- showDockerRunOutput = true;
- log.warn(`
+ debug(`docker - image inspected, not found: ${image}`);
+ return false;
+ }
+ }
+
+ async pull(image) {
+ await new Promise((resolve, reject) => {
+ this.docker.pull(image, (err, stream) => {
+ // streaming output from pull...
+ if (err) {
+ return reject(err);
+ }
+
+ function onFinished(err, output) {
+ if (err) {
+ return reject(err);
+ }
+ return resolve(output);
+ }
+
+ const events = {};
+ function onProgress(event) {
+ if (!event.progress) {
+ return;
+ }
+
+ if (event.status) {
+ events[event.status] = events[event.status] || {};
+ if (event.id) {
+ events[event.status][event.id] = event;
+ }
+ }
+ const progressMsg = Object.entries(events).reduce((result, [status, events], idx) => {
+ const progress = Object.values(events).reduce((sum, e) => {
+ if (e.progressDetail && e.progressDetail.current && e.progressDetail.total) {
+ sum.current += e.progressDetail.current;
+ sum.total += e.progressDetail.total;
+ }
+ return sum;
+ }, { current: 0, total: 0 });
+
+ return result + `${idx > 0 ? ", " : ""}${status}: ${prettyBytes(progress.current)} of ${prettyBytes(progress.total)}`;
+ }, "");
+
+ log.spinner(`Pulling docker image ${image} (${progressMsg})`);
+ }
+
+ this.docker.modem.followProgress(stream, onFinished, onProgress);
+ });
+ });
+ }
+
+ async startContainer(debug) {
+ if (!await this.isImagePresent(this.image, debug)) {
+ // show after 8 seconds, as VS code will timeout after 10 secs by default,
+ // so that the user can see it after all the "docker pull" progress output
+ setTimeout(() => {
+ log.warn(`
+------------------------------------------------------------------------------------------+
-| Docker image must be downloaded: ${this.image}
+| Docker image being downloaded: ${this.image}
| |
| Note: If you debug in VS Code and it fails with "Cannot connect to runtime process" |
| due to a timeout, run this command once: |
@@ -206,36 +252,84 @@
| Alternatively set a higher 'timeout' in the launch configuration, such as 60000 (1 min). |
+------------------------------------------------------------------------------------------+
`);
+ }, 8000);
+
+ debug(`Pulling ${this.image}`)
+ log.spinner(`Pulling ${this.image}...`);
+
+ await this.pull(this.image);
+
+ debug("Pull complete");
}
- execute(
- `docker run
- -d
- --name ${this.name()}
- --rm
- -m ${this.memory}
- -p ${RUNTIME_PORT}
- -p ${this.debug.port}:${this.debug.internalPort}
- ${this.dockerArgsFromKind}
- ${this.dockerArgsFromUser}
- ${this.image}
- ${this.debug.command}
- `,
- // live stream view for docker image download output
- { stdio: showDockerRunOutput ? "inherit" : null },
- debug2
+ log.spinner('Starting container');
+
+ // links for docker create container config:
+ // docker api: https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
+ // docker run impl: https://github.com/docker/cli/blob/2c3797015f5e7ef4502235b638d161279c471a8d/cli/command/container/run.go#L33
+ // https://github.com/apocas/dockerode/issues/257
+ // https://github.com/apocas/dockerode/blob/master/lib/docker.js#L1442
+ // https://medium.com/@johnnyeric/how-to-reproduce-command-docker-run-via-docker-remote-api-with-node-js-5918d7b221ea
+
+ const containerRuntimePort = `${RUNTIME_PORT}/tcp`;
+ const hostRuntimePort = await getPort();
+ this.containerURL = `http://0.0.0.0:${hostRuntimePort}`;
+ const containerDebugPort = `${this.debug.internalPort}/tcp`;
+
+ const createContainerConfig = {
+ name: this.containerName,
+ Image: this.image,
+ Cmd: [ 'sh', '-c', this.debug.command ],
+ Env: [],
+ Volumes: {},
+ ExposedPorts: {
+ [containerRuntimePort]: {},
+ [containerDebugPort]: {}
+ },
+ HostConfig: {
+ AutoRemove: true,
+ PortBindings: {
+ [containerRuntimePort]: [{ HostPort: `${hostRuntimePort}` }],
+ [containerDebugPort]: [{ HostPort: `${this.debug.port}` }]
+ },
+ Memory: this.memory,
+ Binds: []
+ }
+ };
+
+ if (this.debug.updateContainerConfig) {
+ this.debug.updateContainerConfig(this, createContainerConfig);
+ }
+
+ dockerUtils.dockerRunArgs2CreateContainerConfig(this.dockerArgsFromUser, createContainerConfig);
+
+ debug("docker - creating container:", createContainerConfig);
+
+ this.container = await this.docker.createContainer(createContainerConfig);
+
+ const stream = await this.container.attach({
+ stream: true,
+ stdout: true,
+ stderr: true
+ });
+
+ const spinnerSafeStream = (stream) => ({
+ write: (data) => {
+ log.stopSpinner();
+ stream(data.toString().replace(/\n$/, ""));
+ log.resumeSpinner();
+ }
+ });
+
+ this.container.modem.demuxStream(
+ stream,
+ spinnerSafeStream(console.log),
+ spinnerSafeStream(console.error)
);
- this.containerRunning = true;
+ await this.container.start();
- log.stopSpinner();
- spawn("docker", ["logs", "-t", "-f", this.name()], {
- stdio: [
- "inherit", // stdin
- global.mochaLogFile || "inherit", // stdout
- global.mochaLogFile || "inherit" // stderr
- ]
- });
+ debug(`docker - started container ${this.container.id}`);
}
getSourcePath() {
@@ -254,6 +348,18 @@
return this.debug.port;
}
+ name() {
+ return this.containerName;
+ }
+
+ url() {
+ return this.containerURL || "";
+ }
+
+ timeout() {
+ return this.action.limits.timeout || OPENWHISK_DEFAULTS.timeout;
+ }
+
async init(actionWithCode) {
let action;
if (this.sourceMountAction) {
@@ -267,6 +373,8 @@
};
}
+ const RETRIES = MAX_INIT_RETRY_MS / INIT_RETRY_DELAY_MS;
+
const response = await fetch(`${this.url()}/init`, {
method: "POST",
headers: {
@@ -275,8 +383,14 @@
body: JSON.stringify({
value: action
}),
- retries: this.timeout() / INIT_RETRY_DELAY_MS,
- retryDelay: INIT_RETRY_DELAY_MS
+ retryDelay: INIT_RETRY_DELAY_MS,
+ retryOn: function(attempt, error) {
+ // after 1.5 seconds, show retry to user via spinner
+ if (attempt >= 1500 / INIT_RETRY_DELAY_MS) {
+ log.spinner(`Installing action (retry ${attempt}/${RETRIES})`)
+ }
+ return error !== null && attempt < RETRIES;
+ }
});
if (response.status === 502) {
@@ -308,40 +422,15 @@
}
async stop() {
- if (this.containerRunning) {
- execute(`docker kill ${this.name()}`);
+ if (this.container) {
+ // log this here for VS Code, will be the last visible log message since
+ // we will be killed by VS code after the container is gone after the kill()
+ log.log(`Stopping container ${this.name()}.`);
+ await this.container.kill();
+ delete this.container;
+ log.debug(`docker - stopped container ${this.name()}`);
}
}
-
- name() {
- return this.containerName;
- }
-
- url() {
- if (!this.containerURL) {
- // ask docker for the exposed IP and port of the RUNTIME_PORT on the container
- const host = execute(`docker port ${this.name()} ${RUNTIME_PORT}`);
- this.containerURL = `http://${host}`;
- }
- return this.containerURL;
- }
-
- timeout() {
- return this.action.limits.timeout || OPENWHISK_DEFAULTS.timeout;
- }
-
- asContainerName(name) {
- // docker container names are restricted to [a-zA-Z0-9][a-zA-Z0-9_.-]*
-
- // 1. replace special characters with dash
- name = name.replace(/[^a-zA-Z0-9_.-]+/g, '-');
- // 2. leading character is more limited
- name = name.replace(/^[^a-zA-Z0-9]+/g, '');
- // 3. (nice to have) remove trailing special chars
- name = name.replace(/[^a-zA-Z0-9]+$/g, '');
-
- return name;
- }
}
module.exports = OpenWhiskInvoker;
diff --git a/src/kinds/nodejs/nodejs.js b/src/kinds/nodejs/nodejs.js
index 1791def..5f1e32b 100644
--- a/src/kinds/nodejs/nodejs.js
+++ b/src/kinds/nodejs/nodejs.js
@@ -34,25 +34,22 @@
return `node --expose-gc --inspect=0.0.0.0:${invoker.debug.internalPort} app.js`
},
- // return extra docker arguments such as mounting the source path
- dockerArgs: function(invoker) {
- let args = "";
+ // set extra docker container settings such as mounting the source path
+ updateContainerConfig: function(invoker, containerConfig) {
if (invoker.sourceDir) {
if (!invoker.sourceFile) {
throw new Error(`[source-path] or --build-path must point to a source file, it cannot be a folder: '${invoker.sourcePath}'`);
}
- args += ` -v "${invoker.sourceDir}:${CODE_MOUNT}"`;
+ containerConfig.HostConfig.Binds.push(`${invoker.sourceDir}:${CODE_MOUNT}`);
}
if (process.env.WSK_NODE_DEBUG) {
- args += ` -e NODE_DEBUG='${process.env.WSK_NODE_DEBUG}'`;
+ containerConfig.Env.push(`NODE_DEBUG=${process.env.WSK_NODE_DEBUG}`);
}
if (process.env.DEBUG) {
- args += ` -e DEBUG='${process.env.DEBUG}'`;
+ containerConfig.Env.push(`DEBUG=${process.env.DEBUG}`);
}
-
- return args;
},
// return action for /init that mounts the sources specified by invoker.sourcePath
diff --git a/src/log.js b/src/log.js
index fc39e25..0b5f555 100644
--- a/src/log.js
+++ b/src/log.js
@@ -117,6 +117,12 @@
}
},
+ verboseStep: function(...args) {
+ if (this.isVerbose) {
+ this.step(...args);
+ }
+ },
+
verboseWrite: function(text) {
if (this.isVerbose) {
process.stdout.write(text);
@@ -161,6 +167,12 @@
spinner.stop();
},
+ resumeSpinner: function() {
+ if (spinner.text) {
+ this.spinner(spinner.text);
+ }
+ },
+
/** Finish any running spinner and show a log message with a success symbol in front. */
succeed: function(text) {
spinner.stopAndPersist({
diff --git a/test/wskdebug.test.js b/test/wskdebug.test.js
index e988ad3..e771336 100644
--- a/test/wskdebug.test.js
+++ b/test/wskdebug.test.js
@@ -23,6 +23,7 @@
// tests basic cli
let wskdebug = require('../index');
+const dockerUtils = require('../src/dockerutils');
const test = require('./test');
const assert = require('assert');
@@ -111,4 +112,26 @@
await wskdebug(`package/action`);
assert.strictEqual(argv.action, "package/action");
});
+
+ it("should parse docker args", function() {
+ const args = " -e foo=bar -v /some/path:/mount/path -v /another:/path";
+
+ const containerConfig = {
+ Cmd: [],
+ Env: [],
+ Volumes: {},
+ HostConfig: {
+ Binds: [],
+ ExposedPorts: {},
+ PortBindings: {}
+ }
+ };
+ dockerUtils.dockerRunArgs2CreateContainerConfig(args, containerConfig);
+
+ console.log(containerConfig);
+
+ assert.strictEqual(containerConfig.Env[0], "foo=bar");
+ assert.strictEqual(containerConfig.HostConfig.Binds[0], "/some/path:/mount/path");
+ assert.strictEqual(containerConfig.HostConfig.Binds[1], "/another:/path");
+ })
});