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:
40
docker/configurator/index.js
Executable file
40
docker/configurator/index.js
Executable 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")
|
||||
}
|
||||
93
docker/configurator/translator.js
Normal file
93
docker/configurator/translator.js
Normal 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
|
||||
105
docker/configurator/variables.js
Normal file
105
docker/configurator/variables.js
Normal 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
50
docker/nginx.conf
Normal 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
51
docker/run.sh
Normal 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;'
|
||||
Reference in New Issue
Block a user