| /* |
| * 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 { spawn, execSync } = require('child_process'); |
| const fetch = require('fetch-retry'); |
| const kinds = require('./kinds/kinds'); |
| const path = require('path'); |
| |
| const RUNTIME_PORT = 8080; |
| const INIT_RETRY_DELAY_MS = 100; |
| |
| // https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md#system-limits |
| const OPENWHISK_DEFAULTS = { |
| timeout: 60*1000, |
| memory: 256 |
| }; |
| |
| function execute(cmd, options, verbose) { |
| cmd = cmd.replace(/\s+/g, ' '); |
| if (verbose) { |
| console.log(cmd); |
| } |
| const result = execSync(cmd, options); |
| 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) { |
| if (typeof value === "function") { |
| return value(...args); |
| } else { |
| return value; |
| } |
| } |
| |
| class OpenWhiskInvoker { |
| constructor(actionName, action, options, wskProps, wsk) { |
| this.actionName = actionName; |
| this.action = action; |
| |
| this.kind = options.kind; |
| this.image = options.image; |
| this.port = options.port; |
| this.internalPort = options.internalPort; |
| this.command = options.command; |
| this.dockerArgs = options.dockerArgs; |
| this.verbose = options.verbose; |
| |
| // the build path can be separate, if not, same as the source/watch path |
| this.sourcePath = options.buildPath || options.sourcePath; |
| if (this.sourcePath) { |
| this.sourceDir = process.cwd(); |
| // ensure sourcePath is relative to sourceDir |
| this.sourceFile = path.relative(this.sourceDir, this.sourcePath); |
| } |
| |
| this.main = options.main; |
| |
| this.wskProps = wskProps; |
| this.wsk = wsk; |
| |
| this.containerName = this.asContainerName(`wskdebug-${this.action.name}-${Date.now()}`); |
| } |
| |
| static async checkIfAvailable() { |
| try { |
| execute("docker info", {stdio: 'ignore'}); |
| } catch (e) { |
| throw new Error("Docker not running on local system. A local docker environment is required for the debugger.") |
| } |
| } |
| |
| async getImageForKind(kind) { |
| try { |
| const owSystemInfo = await this.wsk.actions.client.request("GET", "/"); |
| if (owSystemInfo.runtimes) { |
| // transform result into a nice dictionary kind => image |
| const runtimes = {}; |
| for (const set of Object.values(owSystemInfo.runtimes)) { |
| for (const entry of set) { |
| let image = entry.image; |
| // fix for Adobe I/O Runtime reporting incorrect image prefixes |
| image = image.replace("bladerunner/", "adobeapiplatform/"); |
| runtimes[entry.kind] = image; |
| } |
| } |
| return runtimes[kind]; |
| |
| } else if (this.verbose) { |
| console.warn("Could not retrieve runtime images from OpenWhisk, using default image list."); |
| } |
| |
| } catch (e) { |
| if (this.verbose) { |
| console.warn("Could not retrieve runtime images from OpenWhisk, using default image list.", e.message); |
| } |
| } |
| return kinds.images[kind]; |
| } |
| |
| async startContainer() { |
| const action = this.action; |
| |
| // this must run after initial build was kicked off in Debugger.startSourceWatching() |
| // so that built files are present |
| |
| // kind and image |
| |
| // precendence: |
| // 1. arguments (this.image) |
| // 2. action (action.exec.image) |
| // 3. defaults (kinds.images[kind]) |
| |
| const kind = this.kind || action.exec.kind; |
| |
| if (kind === "blackbox") { |
| throw new Error("Action is of kind 'blackbox', must specify kind using `--kind` argument."); |
| } |
| |
| // const runtime = kinds[kind] || {}; |
| this.image = this.image || action.exec.image || await this.getImageForKind(kind); |
| |
| if (!this.image) { |
| throw new Error(`Unknown kind: ${kind}. You might want to specify --image.`); |
| } |
| |
| // debugging instructions |
| this.debugKind = kinds.debugKinds[kind] || kind.split(":")[0]; |
| try { |
| this.debug = require(`${__dirname}/kinds/${this.debugKind}/${this.debugKind}`); |
| } catch (e) { |
| if (this.verbose) { |
| console.error(`Cannot find debug info for kind ${this.debugKind}:`, e.message); |
| } |
| this.debug = {}; |
| } |
| |
| this.debug.internalPort = this.internalPort || resolveValue(this.debug.port, this); |
| this.debug.port = this.port || this.internalPort || resolveValue(this.debug.port, this); |
| |
| // ------------------------ |
| |
| this.debug.command = this.command || resolveValue(this.debug.command, this); |
| |
| if (!this.debug.port) { |
| throw new Error(`No debug port known for kind: ${kind}. Please specify --port.`); |
| } |
| if (!this.debug.internalPort) { |
| throw new Error(`No debug port known for kind: ${kind}. Please specify --internal-port.`); |
| } |
| if (!this.debug.command) { |
| throw new Error(`No debug command known for kind: ${kind}. Please specify --command.`); |
| } |
| |
| // limits |
| const memory = (action.limits.memory || OPENWHISK_DEFAULTS.memory) * 1024 * 1024; |
| |
| // source mounting |
| if (this.sourcePath) { |
| if (!this.debug.mountAction) { |
| console.warn(`Warning: Sorry, mounting sources not yet supported for: ${kind}.`); |
| this.sourcePath = undefined; |
| } |
| } |
| |
| const dockerArgsFromKind = resolveValue(this.debug.dockerArgs, this) || ""; |
| const dockerArgsFromUser = this.dockerArgs || ""; |
| |
| let showDockerRunOutput = this.verbose; |
| |
| try { |
| execute(`docker inspect --type=image ${this.image} 2> /dev/null`); |
| } catch (e) { |
| // make sure the user can see the image download process as part of docker run |
| showDockerRunOutput = true; |
| console.log(` |
| +------------------------------------------------------------------------------------------+ |
| | Docker image must be 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: | |
| | | |
| | docker pull ${this.image} |
| | | |
| | Alternatively set a higher 'timeout' in the launch configuration, such as 60000 (1 min). | |
| +------------------------------------------------------------------------------------------+ |
| `); |
| } |
| |
| if (this.verbose) { |
| console.log(`Starting local debug container ${this.name()}`); |
| } |
| |
| execute( |
| `docker run |
| -d |
| --name ${this.name()} |
| --rm |
| -m ${memory} |
| -p ${RUNTIME_PORT} |
| -p ${this.debug.port}:${this.debug.internalPort} |
| ${dockerArgsFromKind} |
| ${dockerArgsFromUser} |
| ${this.image} |
| ${this.debug.command} |
| `, |
| // live stream view for docker image download output |
| { stdio: showDockerRunOutput ? "inherit" : null }, |
| this.verbose |
| ); |
| |
| this.containerRunning = true; |
| |
| spawn("docker", ["logs", "-t", "-f", this.name()], { |
| stdio: [ |
| "inherit", // stdin |
| global.mochaLogFile || "inherit", // stdout |
| global.mochaLogFile || "inherit" // stderr |
| ] |
| }); |
| } |
| |
| async logInfo() { |
| if (this.sourcePath) { |
| console.info(`Sources : ${this.sourcePath}`); |
| } |
| console.info(`Image : ${this.image}`); |
| console.info(`Debug type : ${this.debugKind}`); |
| console.info(`Debug port : localhost:${this.debug.port}`) |
| } |
| |
| async init(actionWithCode) { |
| let action; |
| if (this.sourcePath && this.debug.mountAction) { |
| action = resolveValue(this.debug.mountAction, this); |
| |
| if (this.verbose) { |
| console.log(`Mounting sources onto local debug container: ${this.sourcePath}`); |
| } |
| } else { |
| if (this.verbose) { |
| console.log(`Pushing action code to local debug container: ${this.action.name}`); |
| } |
| action = { |
| binary: actionWithCode.exec.binary, |
| main: actionWithCode.exec.main || "main", |
| code: actionWithCode.exec.code, |
| }; |
| } |
| |
| const response = await fetch(`${this.url()}/init`, { |
| method: "POST", |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| value: action |
| }), |
| retries: this.timeout() / INIT_RETRY_DELAY_MS, |
| retryDelay: INIT_RETRY_DELAY_MS |
| }); |
| |
| if (response.status === 502) { |
| const body = await response.json(); |
| throw new Error("Could not initialize action code on local debug container:\n\n" + body.error); |
| } |
| } |
| |
| async run(args, activationId) { |
| const response = await fetch(`${this.url()}/run`, { |
| method: "POST", |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| value: args, |
| |
| api_host : this.wskProps.apihost, |
| api_key : this.wskProps.api_key, |
| namespace : this.wskProps.namespace, |
| action_name : `/${this.wskProps.namespace}/${this.actionName}`, |
| activation_id : activationId, |
| deadline : `${Date.now() + this.timeout()}`, |
| allow_concurrent: "true" |
| }) |
| }); |
| |
| return response.json(); |
| } |
| |
| async stop() { |
| if (this.containerRunning) { |
| if (this.verbose) { |
| console.log("Stopping local debug container"); |
| } |
| execute(`docker kill ${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; |