Skip to content

Commit

Permalink
feat: Add JSON editor for schema-validated editing of configs (#340)
Browse files Browse the repository at this point in the history
This adds a browser-based editor to view, edit and validate YAML and
JSON config files in the configeditor/ subdirectory.
  • Loading branch information
nielm authored Sep 16, 2024
1 parent 062992a commit 6a167a3
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 3 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
yarn.lock
package-lock.json
public
configeditor/build
1 change: 1 addition & 0 deletions .gcloudignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ test/
.prettier*
*release-please*
*.md
configeditor/

#!include:.gitignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ kubernetes/**/resourcegroup.yaml

# Terratest
.test-data

configeditor/build
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions configeditor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<br />
<p align="center">
<h2 align="center">Autoscaler tool for Cloud Spanner</h2>
<img alt="Autoscaler" src="../resources/BlogHeader_Database_3.max-2200x2200.jpg">

<p align="center">
Validating editor for Autoscaler configuration.
<br />
<a href="../README.md">Home</a>
·
<a href="../src/scaler/README.md">Scaler component</a>
·
<a href="../src/poller/README.md">Poller component</a>
·
<a href="../src/forwarder/README.md">Forwarder component</a>
·
<a href="../terraform/README.md">Terraform configuration</a>
·
<a href="../terraform/README.md#Monitoring">Monitoring</a>
</p>

## 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
```
44 changes: 44 additions & 0 deletions configeditor/build-configeditor.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
--------------------
Config editor built.
--------------------
Open the following link in a browser to use the config editor
file://$(pwd)/index.html
or run:
cd $(pwd)
npx -y http-server -p 8080 -a 127.0.0.1
and then open http://127.0.0.1:8080
EOF
47 changes: 47 additions & 0 deletions configeditor/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<!--
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.
-->
<head>
<title>Autoscaler config file editor</title>
<meta charset="utf-8" />

<script src="index.mjs" lang="javascript" type="module"></script>
</head>

<body>
<h2>JSON Autoscaler config file editor</h2>
<p>Copy/Paste your YAML or JSON autoscaler config in the editor below.</p>
<p>The configuration will automatically be validated and any errors shown</p>
<div id="loading">
Loading...
<br />
If this Loading message does not disappear, check that you have run
<tt>./build-configeditor.sh</tt>
</div>
<div id="jsoneditor" style="width: 95%; height: 600px"></div>
<hr />
<h3>Equivalent GKE configmap YAML</h3>
<textarea
id="yamlequivalent"
readonly="true"
style="width: 90%; height: 400px; border: solid 2px slategray; margin-left: 53px"
></textarea>
<p>
Powered by <a href="https://github.com/josdejong/svelte-jsoneditor">jsoneditoronline.org</a>
</p>
</body>
</html>
175 changes: 175 additions & 0 deletions configeditor/index.mjs
Original file line number Diff line number Diff line change
@@ -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);
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand All @@ -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",
Expand Down

0 comments on commit 6a167a3

Please sign in to comment.