From 6a167a31fe7e122afc49d1401d41efcbf275ae71 Mon Sep 17 00:00:00 2001 From: Niel Markwick Date: Mon, 16 Sep 2024 19:33:09 +0200 Subject: [PATCH] feat: Add JSON editor for schema-validated editing of configs (#340) This adds a browser-based editor to view, edit and validate YAML and JSON config files in the configeditor/ subdirectory. --- .eslintignore | 1 + .gcloudignore | 1 + .gitignore | 2 + README.md | 4 + configeditor/README.md | 51 +++++++++ configeditor/build-configeditor.sh | 44 ++++++++ configeditor/index.html | 47 ++++++++ configeditor/index.mjs | 175 +++++++++++++++++++++++++++++ package.json | 7 +- 9 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 configeditor/README.md create mode 100755 configeditor/build-configeditor.sh create mode 100644 configeditor/index.html create mode 100644 configeditor/index.mjs diff --git a/.eslintignore b/.eslintignore index 1fb48e00..0d92de43 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ node_modules yarn.lock package-lock.json public +configeditor/build diff --git a/.gcloudignore b/.gcloudignore index aac997e5..8317d3e5 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -24,5 +24,6 @@ test/ .prettier* *release-please* *.md +configeditor/ #!include:.gitignore diff --git a/.gitignore b/.gitignore index b8bdde10..ad36998b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ kubernetes/**/resourcegroup.yaml # Terratest .test-data + +configeditor/build diff --git a/README.md b/README.md index 2b7f8579..5e460e1d 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,9 @@ deployment type, but the mechanism for configuration differs slightly: * [Cloud Functions](terraform/cloud-functions/README.md#configuration) * [Google Kubernetes Engine (GKE)](terraform/gke/README.md#building-and-deploying-the-autoscaler-services) +There is also a [browser-based configuration file editor and a command line +configuration file validator][configeditor]. + ## Licensing ```lang-none @@ -208,6 +211,7 @@ support channels. [cloud-logging]: https://cloud.google.com/logging [compute-capacity]: https://cloud.google.com/spanner/docs/compute-capacity#compute_capacity [code-of-conduct]: code-of-conduct.md +[configeditor]: configeditor/README.md [contributing-guidelines]: contributing.md [gke]: https://cloud.google.com/kubernetes-engine [new-issue]: https://github.com/cloudspannerecosystem/autoscaler/issues/new diff --git a/configeditor/README.md b/configeditor/README.md new file mode 100644 index 00000000..e01b3c77 --- /dev/null +++ b/configeditor/README.md @@ -0,0 +1,51 @@ +
+

+

Autoscaler tool for Cloud Spanner

+ Autoscaler + +

+ Validating editor for Autoscaler configuration. +
+ Home + · + Scaler component + · + Poller component + · + Forwarder component + · + Terraform configuration + · + Monitoring +

+ +## Overview + +This directory contains a simple web-based autoscaler config file editor that +validates that the JSON config is correct - both for JSON syntax errors and that +the config has the correct set of parameters and values. + +For GKE configurations, a YAML ConfigMap equivalent is displayed below. + +While directly editing the YAML configMap for GKE is not supported, you +can paste the configmap into the editor, and it will be converted to JSON for +editing and validation. + +## Usage + +Build the editor and start the HTTP server on port `8080`: + +```sh +npm run start-configeditor-server -- --port 8080 +``` + +Then browse to `http://127.0.0.1:8080/` + +## Command line config validation + +The JSON and YAML configurations can also be validated using the command line: + +```sh +npm install +npm run validateConfigFile -- path/to/config_file +``` diff --git a/configeditor/build-configeditor.sh b/configeditor/build-configeditor.sh new file mode 100755 index 00000000..c51151bb --- /dev/null +++ b/configeditor/build-configeditor.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# +# Copyright 2024 Google 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. +# +# +set -e + +SCRIPTDIR=$(dirname "$0") +cd "$SCRIPTDIR" + +npm install --quiet +mkdir -p build/vanilla-jsoneditor +[[ ! -e build/vanilla-jsoneditor/standalone.js ]] && \ + curl -o build/vanilla-jsoneditor/standalone.js \ + https://cdn.jsdelivr.net/npm/vanilla-jsoneditor@0.23.8/standalone.js + +cp -r ../node_modules/js-yaml ../autoscaler-config.schema.json build/ + +[[ "$1" == "--quiet" ]] || cat < + + + + Autoscaler config file editor + + + + + + +

JSON Autoscaler config file editor

+

Copy/Paste your YAML or JSON autoscaler config in the editor below.

+

The configuration will automatically be validated and any errors shown

+
+ Loading... +
+ If this Loading message does not disappear, check that you have run + ./build-configeditor.sh +
+
+
+

Equivalent GKE configmap YAML

+ +

+ Powered by jsoneditoronline.org +

+ + diff --git a/configeditor/index.mjs b/configeditor/index.mjs new file mode 100644 index 00000000..37168ec5 --- /dev/null +++ b/configeditor/index.mjs @@ -0,0 +1,175 @@ +/** + * Copyright 2024 Google 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. + */ + +/** + * @fileoverview Support scripts for browser-based config editor. + */ + +// eslint-disable-next-line +import { + JSONEditor, + createAjvValidator, + renderJSONSchemaEnum, + renderValue, +} from "./build/vanilla-jsoneditor/standalone.js"; +import schema from "./build/autoscaler-config.schema.json" with { type: "json" }; +import * as JsYaml from "./build/js-yaml/dist/js-yaml.mjs"; + +/** @typedef {import("vanilla-jsoneditor").Content} Content */ + +/** @type {JSONEditor} */ +let editor; + +/** + * Checks if the JSON is valid, and if so, copy it to the YAML textarea. + * + * If it is not, but _is_ valid YAML, convert it to JSON and update both the + * JSON editor and the YAML textarea. + * + * @param {Content} content + * @param {Content} previousContent + * @param {{ + * contentErrors: import("vanilla-jsoneditor").ContentErrors | null, + * patchResult: import("vanilla-jsoneditor").JSONPatchResult | null + * }} changeStatus + */ +function jsonEditorChangeHandler(newcontent, previousContent, changeStatus) { + const yamlTextarea = document.getElementById("yamlequivalent"); + + if (changeStatus?.contentErrors?.parseError) { + console.log( + "jsonEditorChangeHandler - got JSON parsing errors %o", + changeStatus.contentErrors, + ); + if (newcontent.text?.search("\nkind: ConfigMap\n") >= 0) { + // Check if it is valid YAML text to see if we need to convert it back + // to JSON. + try { + const configMap = JsYaml.load(newcontent.text); + if ( + configMap && + configMap.kind === "ConfigMap" && + configMap.data && + Object.values(configMap.data)[0] + ) { + // The autoscaler ConfigMap data is YAML stored as text in a YAML, + // so we need to re-parse the data object. + const configMapData = JsYaml.load(Object.values(configMap.data)[0]); + console.log("got yaml configMap data object: %o", configMapData); + + // Asynchronously update the content with the parsed configmap data. + // This is because JSON editor likes to finish the onchange before + // anything else happens! + setTimeout(() => { + /** @type {Content} */ + const content = { json: configMapData }; + editor.updateProps({ + content, + mode: "text", + selection: null, + }); + editor.refresh(); + // Trigger refresh of YAML textarea. + updateYamlWithJsonContent(content); + }, 100); + return; + } + } catch (e) { + console.log("not valid yaml " + e); + } + } + // Some other unparsable JSON. + yamlTextarea.setAttribute("disabled", "true"); + } else { + // Got valid JSON, even if it might not be valid Autoscaler config, we + // update the YAML version. + updateYamlWithJsonContent(newcontent); + } +} + +/** + * Converts the content from JSONEditor to YAML and stores it in the YAML + * textarea. + * + * @param {Content} content + */ +function updateYamlWithJsonContent(content) { + const yamlTextarea = document.getElementById("yamlequivalent"); + yamlTextarea.removeAttribute("disabled"); + const json = content.text ? JSON.parse(content.text) : content.json; + + const configMap = { + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + name: "autoscaler-config", + namespace: "spanner-autoscaler", + }, + data: { + // Autoscaler configmap.data is YAML as text. + "autoscaler-config.yaml": JsYaml.dump(json, { lineWidth: -1 }), + }, + }; + yamlTextarea.value = JsYaml.dump(configMap, { lineWidth: -1 }); +} + +/** + * Handles addling rendering of Schema enums in JSONEditor. + * + * @param {import("vanilla-jsoneditor").RenderValueProps} props + * @return {import("vanilla-jsoneditor").RenderValueComponentDescription[]} + */ +function onRenderValue(props) { + return renderJSONSchemaEnum(props, schema) || renderValue(props); +} + +/** @type {Content} */ +const EXAMPLE_CONFIG = { + json: [ + { + $comment: "Sample autoscaler config", + projectId: "my-project", + instanceId: "my-instance", + units: "NODES", + minSize: 1, + maxSize: 3, + stateDatabase: { + name: "firestore", + }, + scalerPubSubTopic: "projects/my-project/topics/scaler-topic", + }, + ], +}; + +/** Handles DOMContentLoaded event. */ +function onDOMContentLoaded() { + editor = new JSONEditor({ + target: document.getElementById("jsoneditor"), + props: { + content: EXAMPLE_CONFIG, + mode: "text", + schema, + indentation: 2, + validator: createAjvValidator({ schema }), + onChange: jsonEditorChangeHandler, + onRenderValue, + }, + }); + updateYamlWithJsonContent(EXAMPLE_CONFIG); + document.getElementById("loading").style.display = "none"; +} + +document.addEventListener("DOMContentLoaded", onDOMContentLoaded, false); diff --git a/package.json b/package.json index 5642e35b..0fabd404 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "eslint-fix": "eslint --fix .", "format": "prettier --write .", "install-all": "npm install --save", - "markdown-link-check": "find . -name '*.md' -not \\( -path './node_modules/*' -o -path '*/.terraform/*' -o -path './CHANGELOG.md' -prune \\) -print0 | xargs --null markdown-link-check --config markdown-link-checker.json --quiet", - "mdlint": "markdownlint '**/*.md' --config .mdl.json --ignore '**/node_modules/**' --ignore 'code-of-conduct.md' --ignore 'CHANGELOG.md'", + "markdown-link-check": "find . -name '*.md' -not \\( -path './node_modules/*' -o -path './configeditor/build/*' -o -path '*/.terraform/*' -o -path './CHANGELOG.md' -prune \\) -print0 | xargs --null markdown-link-check --config markdown-link-checker.json --quiet", + "mdlint": "markdownlint '**/*.md' --config .mdl.json --ignore '**/node_modules/**' --ignore '**/build/**' --ignore 'code-of-conduct.md' --ignore 'CHANGELOG.md'", "poller-job": "node -e \"require('./src/poller/index').main()\"", "prepare": "{ git rev-parse --is-inside-work-tree >/dev/null 2>/dev/null && test \"$NODE_ENV\" != production -a \"$CI\" != true && husky ; } || true", "prettier": "prettier --write .", @@ -29,7 +29,8 @@ "typecheck": "tsc --project jsconfig.json --maxNodeModuleJsDepth 0 --noEmit", "unified-job": "node -e \"require('./src/unifiedScaler').main()\"", "update-all": "npm update -S", - "validate-config-file": "node -e \"require('./src/poller/poller-core/config-validator').main()\" -- " + "validate-config-file": "node -e \"require('./src/poller/poller-core/config-validator').main()\" -- ", + "start-configeditor-server": "cd configeditor && ./build-configeditor.sh --quiet && npm exec http-server -- -a 127.0.0.1 " }, "dependencies": { "@google-cloud/firestore": "^7.10.0",