v0.30.1 configuration engine as wasm
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
This commit is contained in:
1 parent
883af0fcaa
commit
bfcc15613a
13 files changed
+320
-52
No files matched your search
@@ -0,0 +1,64 @@
|
||||
import { after, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
// Built by make wasm and started as the page starts it: the toolchain's loader
|
||||
// beside the module, main running until the page closes. The make run is its
|
||||
// own: a parent GNU make's flags would stop a BSD one.
|
||||
const dir = mkdtempSync(join(tmpdir(), "lwconf-"));
|
||||
after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
const made = execFileSync("make", ["-s", "wasm", `BIN_DIR=${dir}`, "VERSION=v0.0.0-node"], {
|
||||
cwd: join(import.meta.dirname, "../.."), encoding: "utf8",
|
||||
env: { ...process.env, MAKEFLAGS: "", MFLAGS: "", MAKELEVEL: "" },
|
||||
});
|
||||
await import(pathToFileURL(join(dir, "wasm_exec.js")));
|
||||
const go = new globalThis.Go();
|
||||
const { instance } = await WebAssembly.instantiate(readFileSync(join(dir, "lwconf.wasm")), go.importObject);
|
||||
go.run(instance);
|
||||
const call = (name, ...args) => JSON.parse(globalThis.lwconf[name](...args));
|
||||
|
||||
test("make wasm stamps the version and prints both checksums", () => {
|
||||
for (const file of ["lwconf.wasm", "wasm_exec.js"]) {
|
||||
const sum = createHash("sha256").update(readFileSync(join(dir, file))).digest("hex");
|
||||
assert.match(made, new RegExp(`^${sum} +${file.replace(".", "\\.")}$`, "m"));
|
||||
}
|
||||
assert.equal(call("schema").value.version, "v0.0.0-node");
|
||||
});
|
||||
|
||||
// Off the host a directory path ends in '/'
|
||||
test("a preset's pipelines, written as a command line, parse and validate back to themselves", () => {
|
||||
const { value: pipelines } = call("preset", "tail", JSON.stringify({ path: "/var/log/" }));
|
||||
assert.equal(pipelines[0].plugin_sources[0].config.directory, "/var/log/");
|
||||
const { value: line } = call("emit", JSON.stringify(pipelines), "command");
|
||||
assert.match(line, /^lw \\\n {2}--pipeline tail/);
|
||||
assert.deepEqual(call("parse", line).value, pipelines);
|
||||
assert.deepEqual(call("validate", JSON.stringify(pipelines)).value, pipelines);
|
||||
assert.match(call("emit", JSON.stringify(pipelines), "file").value, /^\[\[pipelines\]\]/m);
|
||||
});
|
||||
|
||||
test("a refused call answers why, and the engine keeps answering", () => {
|
||||
const refused = [
|
||||
[["parse", "lw --nope x"], /not a pipeline flag/],
|
||||
[["parse", "lw"], /no pipeline flags/],
|
||||
[["parse"], /^usage: lwconf\.parse\(line\)/],
|
||||
[["parse", 5], /^usage: /],
|
||||
[["preset", "tail", JSON.stringify({ path: "/var/log" })], /path/],
|
||||
[["validate", JSON.stringify([{ name: "p" }])], /no sources/],
|
||||
[["validate", JSON.stringify(["a", "b"].map((name, i) => ({
|
||||
name, plugin_sources: [{ type: "tcp_chain", config: { port: [9000, "x"][i] } }], plugin_sinks: [{ type: "null" }],
|
||||
})))], /^pipelines\[1\]\.plugin_sources\[tcp_chain\]\.config\.port: not an integer$/],
|
||||
[["emit", JSON.stringify([{ name: "p" }]), "command"], /no sources/],
|
||||
[["emit", "[]", "yaml"], /no form "yaml"/],
|
||||
];
|
||||
for (const [args, want] of refused) {
|
||||
const got = call(...args);
|
||||
assert.equal(got.value, undefined, `${args}`);
|
||||
assert.match(got.error, want, `${args}`);
|
||||
}
|
||||
assert.ok(call("schema").value.sources.length > 0);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
//go:build js && wasm
|
||||
|
||||
// Command lwconf is the configuration engine as WebAssembly, for the website.
|
||||
// It sets globalThis.lwconf, whose calls take strings and return a JSON object:
|
||||
// {"value": ...}, or {"error": "..."} for a call refused. Pipelines travel in
|
||||
// compose's JSON form; off the host a preset's directory path ends in '/'.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/lixenwraith/logwisp/internal/compose"
|
||||
)
|
||||
|
||||
// calls are lwconf's, by name, with the string arguments each takes
|
||||
var calls = map[string]struct {
|
||||
args []string
|
||||
call func(args []string) (any, error)
|
||||
}{
|
||||
"schema": {nil, func([]string) (any, error) { return compose.NewSchema(), nil }},
|
||||
"preset": {[]string{"name", "values"}, func(a []string) (any, error) {
|
||||
var values map[string]string
|
||||
if err := json.Unmarshal([]byte(a[1]), &values); err != nil {
|
||||
return nil, fmt.Errorf("values: %w", err)
|
||||
}
|
||||
return pipelines(compose.FromPreset(a[0], values, nil))
|
||||
}},
|
||||
"parse": {[]string{"line"}, func(a []string) (any, error) {
|
||||
return pipelines(compose.FromCommandLine(a[0], nil))
|
||||
}},
|
||||
"validate": {[]string{"pipelines"}, func(a []string) (any, error) {
|
||||
c, err := compose.FromJSON([]byte(a[0]))
|
||||
if err == nil {
|
||||
err = c.Validate()
|
||||
}
|
||||
return pipelines(c, err)
|
||||
}},
|
||||
"emit": {[]string{"pipelines", "form"}, func(a []string) (any, error) {
|
||||
c, err := compose.FromJSON([]byte(a[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
forms := compose.Forms()
|
||||
if i := slices.IndexFunc(forms, func(f compose.Form) bool { return f.Name == a[1] }); i >= 0 {
|
||||
return forms[i].Write(c)
|
||||
}
|
||||
return nil, fmt.Errorf("no form %q", a[1])
|
||||
}},
|
||||
}
|
||||
|
||||
// pipelines is a composition's value: its pipelines settled, ids given
|
||||
func pipelines(c *compose.Composition, err error) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := c.JSON()
|
||||
return json.RawMessage(data), err
|
||||
}
|
||||
|
||||
func main() {
|
||||
api := map[string]any{}
|
||||
for name := range calls {
|
||||
api[name] = js.FuncOf(func(_ js.Value, args []js.Value) any { return answer(name, args) })
|
||||
}
|
||||
js.Global().Set("lwconf", js.ValueOf(api))
|
||||
select {} // the page calls in until it closes
|
||||
}
|
||||
|
||||
// answer is a call's JSON for the page: its value, or why it was refused
|
||||
func answer(name string, args []js.Value) string {
|
||||
c := calls[name]
|
||||
value, err := any(nil), fmt.Errorf("usage: lwconf.%s(%s), each a string", name, strings.Join(c.args, ", "))
|
||||
if len(args) == len(c.args) && !slices.ContainsFunc(args, func(v js.Value) bool { return v.Type() != js.TypeString }) {
|
||||
strs := make([]string, len(args))
|
||||
for i, arg := range args {
|
||||
strs[i] = arg.String()
|
||||
}
|
||||
value, err = c.call(strs)
|
||||
}
|
||||
out := map[string]any{"value": value}
|
||||
if err != nil {
|
||||
out = map[string]any{"error": err.Error()}
|
||||
}
|
||||
data, _ := json.Marshal(out)
|
||||
return string(data)
|
||||
}
|
||||
Reference in new issue
Block a user