feature: full-spectrum runtime Docker configuration (via #4965)

* reorganize docker things

* Configurator WIP

* implement Docker runtime config generator

* add tests

* update documentation

* fix Markdown tables

* Move Docker section

* add note to README

* move up `nodejs` install for more aggressive caching

* drop exclusive test

* fix missing `DISPLAY_OPERATION_ID`
This commit is contained in:
kyle
2018-11-01 14:53:29 -04:00
committed by GitHub
parent b9300211bb
commit 31a8b13777
12 changed files with 596 additions and 84 deletions

40
docker/configurator/index.js Executable file
View File

@@ -0,0 +1,40 @@
const fs = require("fs")
const path = require("path")
const translator = require("./translator")
const configSchema = require("./variables")
const START_MARKER = "// Begin Swagger UI call region"
const END_MARKER = "// End Swagger UI call region"
const targetPath = path.normalize(process.cwd() + "/" + process.argv[2])
const originalHtmlContent = fs.readFileSync(targetPath, "utf8")
const startMarkerIndex = originalHtmlContent.indexOf(START_MARKER)
const endMarkerIndex = originalHtmlContent.indexOf(END_MARKER)
const beforeStartMarkerContent = originalHtmlContent.slice(0, startMarkerIndex)
const afterEndMarkerContent = originalHtmlContent.slice(endMarkerIndex + END_MARKER.length)
fs.writeFileSync(targetPath, `${beforeStartMarkerContent}
${START_MARKER}
const ui = SwaggerUIBundle({
${indent(translator(process.env, { injectBaseConfig: true }), 8, 2)}
})
${END_MARKER}
${afterEndMarkerContent}`)
function indent(str, len, fromLine) {
return str
.split("\n")
.map((line, i) => {
if(i + 1 >= fromLine) {
return `${Array(len + 1).join(" ")}${line}`
} else {
return line
}
})
.join("\n")
}

View File

@@ -0,0 +1,93 @@
// Converts an object of environment variables into a Swagger UI config object
const configSchema = require("./variables")
const baseConfig = {
url: {
value: "https://petstore.swagger.io/v2/swagger.json",
schema: {
type: "string"
}
},
dom_id: {
value: "#swagger-ui",
schema: {
type: "string"
}
},
deepLinking: {
value: "true",
schema: {
type: "boolean"
}
},
presets: {
value: `[\n SwaggerUIBundle.presets.apis,\n SwaggerUIStandalonePreset\n]`,
schema: {
type: "array"
}
},
plugins: {
value: `[\n SwaggerUIBundle.plugins.DownloadUrl\n]`,
schema: {
type: "array"
}
},
layout: {
value: "StandaloneLayout",
schema: {
type: "string"
}
}
}
function objectToKeyValueString(env, { injectBaseConfig = false, schema = configSchema } = {}) {
let valueStorage = injectBaseConfig ? Object.assign({}, baseConfig) : {}
const keys = Object.keys(env)
// Compute an intermediate representation that holds candidate values and schemas.
//
// This is useful for deduping between multiple env keys that set the same
// config variable.
keys.forEach(key => {
const varSchema = schema[key]
const value = env[key]
if(!varSchema) return
const storageContents = valueStorage[varSchema.name]
if(storageContents) {
if(varSchema.legacy === true) {
// If we're looking at a legacy var, it should lose out to any already-set value
return
}
delete valueStorage[varSchema.name]
}
valueStorage[varSchema.name] = {
value,
schema: varSchema
}
})
// Compute a key:value string based on valueStorage's contents.
let result = ""
Object.keys(valueStorage).forEach(key => {
const value = valueStorage[key]
const escapedName = /[^a-zA-Z0-9]/.test(key) ? `"${key}"` : key
if (value.schema.type === "string") {
result += `${escapedName}: "${value.value}",\n`
} else {
result += `${escapedName}: ${value.value === "" ? `undefined` : value.value},\n`
}
})
return result.trim()
}
module.exports = objectToKeyValueString

View File

@@ -0,0 +1,105 @@
const standardVariables = {
CONFIG_URL: {
type: "string",
name: "configUrl"
},
DOM_ID: {
type: "string",
name: "dom_id"
},
SPEC: {
type: "object",
name: "spec"
},
URL: {
type: "string",
name: "url"
},
URLS: {
type: "array",
name: "urls"
},
URLS_PRIMARY_NAME: {
type: "string",
name: "urls.primaryName"
},
LAYOUT: {
type: "string",
name: "layout"
},
DEEP_LINKING: {
type: "boolean",
name: "deepLinking"
},
DISPLAY_OPERATION_ID: {
type: "boolean",
name: "displayOperationId"
},
DEFAULT_MODELS_EXPAND_DEPTH: {
type: "number",
name: "defaultModelsExpandDepth"
},
DEFAULT_MODEL_EXPAND_DEPTH: {
type: "number",
name: "defaultModelExpandDepth"
},
DEFAULT_MODEL_RENDERING: {
type: "string",
name: "defaultModelRendering"
},
DISPLAY_REQUEST_DURATION: {
type: "boolean",
name: "displayRequestDuration"
},
DOC_EXPANSION: {
type: "string",
name: "docExpansion"
},
FILTER: {
type: "string",
name: "filter"
},
MAX_DISPLAYED_TAGS: {
type: "number",
name: "maxDisplayedTags"
},
SHOW_EXTENSIONS: {
type: "boolean",
name: "showExtensions"
},
SHOW_COMMON_EXTENSIONS: {
type: "boolean",
name: "showCommonExtensions"
},
OAUTH2_REDIRECT_URL: {
type: "string",
name: "oauth2RedirectUrl"
},
SHOW_MUTATED_REQUEST: {
type: "boolean",
name: "showMutatedRequest"
},
SUPPORTED_SUBMIT_METHODS: {
type: "array",
name: "supportedSubmitMethods"
},
VALIDATOR_URL: {
type: "string",
name: "validatorUrl"
}
}
const legacyVariables = {
API_URL: {
type: "string",
name: "url",
legacy: true
},
API_URLS: {
type: "array",
name: "urls",
legacy: true
}
}
module.exports = Object.assign({}, standardVariables, legacyVariables)

50
docker/nginx.conf Normal file
View File

@@ -0,0 +1,50 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
index index.html index.htm;
location / {
alias /usr/share/nginx/html/;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
}
}
}

51
docker/run.sh Normal file
View File

@@ -0,0 +1,51 @@
#! /bin/sh
set -e
BASE_URL=${BASE_URL:-/}
NGINX_ROOT=/usr/share/nginx/html
INDEX_FILE=$NGINX_ROOT/index.html
node /usr/share/nginx/configurator $INDEX_FILE
replace_in_index () {
if [ "$1" != "**None**" ]; then
sed -i "s|/\*||g" $INDEX_FILE
sed -i "s|\*/||g" $INDEX_FILE
sed -i "s|$1|$2|g" $INDEX_FILE
fi
}
replace_or_delete_in_index () {
if [ -z "$2" ]; then
sed -i "/$1/d" $INDEX_FILE
else
replace_in_index $1 $2
fi
}
if [ "${BASE_URL}" ]; then
sed -i "s|location .* {|location $BASE_URL {|g" /etc/nginx/nginx.conf
fi
replace_in_index myApiKeyXXXX123456789 $API_KEY
replace_or_delete_in_index your-client-id $OAUTH_CLIENT_ID
replace_or_delete_in_index your-client-secret-if-required $OAUTH_CLIENT_SECRET
replace_or_delete_in_index your-realms $OAUTH_REALM
replace_or_delete_in_index your-app-name $OAUTH_APP_NAME
if [ "$OAUTH_ADDITIONAL_PARAMS" != "**None**" ]; then
replace_in_index "additionalQueryStringParams: {}" "additionalQueryStringParams: {$OAUTH_ADDITIONAL_PARAMS}"
fi
if [[ -f $SWAGGER_JSON ]]; then
cp -s $SWAGGER_JSON $NGINX_ROOT
REL_PATH="./$(basename $SWAGGER_JSON)"
sed -i "s|https://petstore.swagger.io/v2/swagger.json|$REL_PATH|g" $INDEX_FILE
sed -i "s|http://example.com/api|$REL_PATH|g" $INDEX_FILE
fi
# replace the PORT that nginx listens on if PORT is supplied
if [[ -n "${PORT}" ]]; then
sed -i "s|8080|${PORT}|g" /etc/nginx/nginx.conf
fi
exec nginx -g 'daemon off;'