UNPKG

@adpt/cloud

Version:
193 lines (189 loc) 7.42 kB
"use strict"; /* * Copyright 2019 Unbounded Systems, LLC * * Licensed 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. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const core_1 = tslib_1.__importStar(require("@adpt/core")); const utils_1 = require("@adpt/utils"); const child_process_1 = require("child_process"); const fs_extra_1 = require("fs-extra"); const path = tslib_1.__importStar(require("path")); const url_1 = require("url"); const util_1 = require("util"); const Container_1 = require("../Container"); const docker_1 = require("../docker"); const handles_1 = require("../handles"); const http_1 = require("../http"); const NetworkService_1 = require("../NetworkService"); const Service_1 = require("../Service"); const nginxImg = "nginx:latest"; function upstream(_r, i, _url) { return `upstream_url_${i}`; } function getPort(lurl) { if (lurl.port && lurl.port !== "") { return lurl.port; } switch (lurl.protocol) { case "http:": return 80; case "https:": return 443; default: return 80; //Throw here? } } function useMakeNginxConf(props) { const [resolvedRoutes, setResolvedRoutes] = core_1.useState([]); setResolvedRoutes(() => props.routes.map((r) => { let lurl; if (util_1.isString(r.endpoint)) { lurl = r.endpoint; } else { const hostname = core_1.callInstanceMethod(r.endpoint, undefined, "hostname"); const port = core_1.callInstanceMethod(r.endpoint, undefined, "port"); if (!port || !hostname) return undefined; lurl = `http://${hostname}:${port}/`; } const upstreamPath = r.upstreamPath || "/"; return { path: r.path, upstreamPath, url: lurl }; }).filter(utils_1.notNull)); const locationBlocks = resolvedRoutes.map((r, i) => { const lurl = new url_1.URL(r.url); const varName = upstream(r, i, lurl); const { hostname, protocol } = lurl; const port = getPort(lurl); const upPath = r.upstreamPath; // NOTE: the set $encoded line is required because the captured // portion of the regex ($1) has been URL-decoded. The set line // re-encodes it. But using $1 directly on the proxy_pass line does not. return ` set $${varName} ${protocol}//${hostname}:${port}; location ~ ^${r.path}(.*)$ { set $encoded $${varName + upPath}$1; proxy_pass $encoded; proxy_set_header Host $host; } `; }); let locationText = locationBlocks.join("\n"); if (locationBlocks.length === 0) { locationText = `location / { return ${(props.routes.length === 0) ? "404" : "503"}; } `; } const mainConfig = []; if (props.debug) mainConfig.push("error_log stderr debug;"); return ` ${mainConfig.join("\n")} events { worker_connections 1024; } http { resolver_timeout 1s; proxy_connect_timeout 11s; include conf.d/*; server { listen ${props.port}; ${locationText} } } `; } const execP = util_1.promisify(child_process_1.exec); async function checkNginxConf(conf) { return utils_1.withTmpDir(async (dir) => { const confPath = path.join(dir, "nginx.conf"); await fs_extra_1.writeFile(confPath, conf); try { // tslint:disable-next-line:max-line-length await execP(`cat ${confPath} | docker run -i --rm ${nginxImg} bash -c "cat - > /nginx.conf && nginx -t -c /nginx.conf"`, { timeout: 3600 }); } catch (e) { if (e.signal !== null) { throw new Error("Timeout trying to validate nginx configuration"); } if ((e.code != null) && (e.code !== 0)) { const errs = e.stdout.toString() + "\n" + e.stderr.toString(); throw new Error(`Internal Error: generated invalid nginx configuration: ${errs} **Configuration** ${conf} `); } throw e; } }); } const defaultProps = Object.assign({}, http_1.UrlRouter.defaultProps, { debug: false }); /** * {@link http.UrlRouter} implementation based on {@link https://nginx.org | nginx} * * @public */ function UrlRouter(propsIn) { const props = propsIn; const h = handles_1.handles(); http_1.checkUrlEndpoints(props.routes); const nginxConf = useMakeNginxConf(props); //FIXME(manishv) nginx config check will only pass if all hostnames can be resolved locally, how to fix? if (false) core_1.useAsync(async () => checkNginxConf(nginxConf), undefined); const nginxExec = props.debug ? "nginx-debug" : "nginx"; const img = core_1.handle(); const externalPort = props.externalPort || props.port; return core_1.default.createElement(core_1.Sequence, { key: props.key }, core_1.default.createElement(docker_1.LocalDockerImage, { handle: img, dockerfile: ` FROM ${nginxImg} RUN apt-get update && \ apt-get install --no-install-recommends --no-install-suggests -y inotify-tools && \ apt-get clean WORKDIR /router COPY --from=files / . RUN chmod a+x start_nginx.sh make_resolvers.sh CMD [ "/router/start_nginx.sh" ] `, files: [{ path: "start_nginx.sh", contents: `#!/bin/sh mkdir conf.d ./make_resolvers.sh exec ${nginxExec} -g "daemon off;" -c /router/nginx.conf ` }, { path: "make_resolvers.sh", contents: `#!/bin/sh conf="resolver $(/usr/bin/awk 'BEGIN{ORS = " "} $1=="nameserver" {print $2}' /etc/resolv.conf);" [ "$conf" = "resolver ;" ] && exit 0 confpath=conf.d/resolvers.conf echo "$conf" > $confpath ` }, { path: "nginx.conf", contents: nginxConf }], options: { imageName: "nginx-url-router", uniqueTag: true } }), core_1.default.createElement(Service_1.Service, { key: props.key }, core_1.default.createElement(NetworkService_1.NetworkService, { key: props.key + "-netsvc", endpoint: h.create.nginx, port: externalPort, targetPort: props.port, scope: "external" }), core_1.default.createElement(Container_1.Container, { key: props.key, handle: h.nginx, name: "nginx-url-router", image: img, ports: [props.port], imagePullPolicy: "Never" }))); } exports.UrlRouter = UrlRouter; exports.default = UrlRouter; //# sourceMappingURL=UrlRouter.js.map