engine: resources: Add an http ui resource

Many years ago I built and demoed a prototype of a simple web ui with a
slider, and as you moved it left and right, it started up or shutdown
some number of virtual machines.

The webui was standalone code, but the rough idea of having events from
a high-level overview flow into mgmt, was what I wanted to test out. At
this stage, I didn't even have the language built yet. This prototype
helped convince me of the way a web ui would fit into everything.

Years later, I build an autogrouping prototype which looks quite similar
to what we have today. I recently picked it back up to polish it a bit
more. It's certainly not perfect, and might even be buggy, but it's
useful enough that it's worth sharing.

If I had more cycles, I'd probably consider removing the "store" mode,
and replace it with the normal "value" system, but we would need the
resource "mutate" API if we wanted this. This would allow us to directly
change the "value" field, without triggering a graph swap, which would
be a lot less clunky than the "store" situation.

Of course I'd love to see a GTK version of this concept, but I figured
it would be more practical to have a web ui over HTTP.

One notable missing feature, is that if the "web ui" changes (rather
than just a value changing) we need to offer to the user to reload it.
It currently doesn't get an event for that, and so don't confuse your
users. We also need to be better at validating "untrusted" input here.

There's also no major reason to use the "gin" framework, we should
probably redo this with the standard library alone, but it was easier
for me to push out something quick this way. We can optimize that later.

Lastly, this is all quite ugly since I'm not a very good web dev, so if
you want to make this polished, please do! The wasm code is also quite
terrible due to limitations in the compiler, and maybe one day when that
works better and doesn't constantly deadlock, we can improve it.
This commit is contained in:
James Shubin
2025-05-02 02:14:14 -04:00
parent 6b10477ebc
commit 807c4b3430
27 changed files with 3266 additions and 43 deletions

View File

@@ -38,6 +38,7 @@ SHELL = bash
# a large amount of output from this `find`, can cause `make` to be much slower!
GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
MCL_FILES := $(shell find lang/ -name '*.mcl' -not -path 'old/*' -not -path 'tmp/*')
MISC_FILES := $(shell find engine/resources/http_ui/)
SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always))
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
@@ -198,11 +199,15 @@ lang: ## generates the lexer/parser for the language frontend
@# recursively run make in child dir named lang
@$(MAKE) --quiet -C lang
resources: ## builds the resources dependencies required for the engine backend
@# recursively run make in child dir named engine/resources
@$(MAKE) --quiet -C engine/resources
# build a `mgmt` binary for current host os/arch
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
cp -a $< $@
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES) go.mod go.sum
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES) $(MISC_FILES) go.mod go.sum
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
go generate
go build $(TRIMPATH) -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS);
@@ -221,7 +226,7 @@ baddev: $(PROGRAM)
# extract os and arch from target pattern
GOOS=$(firstword $(subst -, ,$*))
GOARCH=$(lastword $(subst -, ,$*))
build/mgmt-%: $(GO_FILES) $(MCL_FILES) go.mod go.sum | lang funcgen
build/mgmt-%: $(GO_FILES) $(MCL_FILES) $(MISC_FILES) go.mod go.sum | lang resources funcgen
@# If you need to run `go mod tidy` then this can trigger.
@if [ "$(PKGNAME)" = "" ]; then echo "\$$(PKGNAME) is empty, test with: go list ."; exit 42; fi
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."

View File

@@ -165,8 +165,68 @@ func (obj *Engine) Process(ctx context.Context, vertex pgraph.Vertex) error {
}
}
}
// XXX: maybe we want to receive from someone that was grouped?
// XXX: can obj.SendRecv() do that or do we have to modify it?
// XXX: it might already *just work* -- test it to double check!
}
// If we contain grouped resources, maybe someone inside wants to recv?
// This code is similar to the above and was added for http:ui stuff.
// XXX: Maybe this block isn't needed, as mentioned we need to check!
if res, ok := vertex.(engine.GroupableRes); ok {
process := res.GetGroup() // look through these
grouped := []engine.GroupableRes{} // found resources
for len(process) > 0 { // recurse through any nesting
var x engine.GroupableRes
x, process = process[0], process[1:] // pop from front!
g := x.GetGroup()
grouped = append(grouped, g...) // add to the end
}
//for _, g := res.GetGroup() // non-recursive, one-layer method
for _, g := range grouped { // recursive method!
r, ok := g.(engine.RecvableRes)
if !ok {
continue
}
// This section looks almost identical to the above one!
if updated, err := SendRecv(r, nil); err != nil {
return errwrap.Wrapf(err, "could not grouped SendRecv")
} else if len(updated) > 0 {
for r, m := range updated { // map[engine.RecvableRes]map[string]*engine.Send
v, ok := r.(pgraph.Vertex)
if !ok {
continue
}
_, stateExists := obj.state[v] // autogrouped children probably don't have a state
if !stateExists {
continue
}
for s, send := range m {
if !send.Changed {
continue
}
obj.Logf("Send/Recv: %v.%s -> %v.%s", send.Res, send.Key, r, s)
// if send.Changed == true, at least one was updated
// invalidate cache, mark as dirty
obj.state[v].setDirty()
//break // we might have more vertices now
}
// re-validate after we change any values
if err := engine.Validate(r); err != nil {
return errwrap.Wrapf(err, "failed grouped Validate after SendRecv")
}
}
}
}
}
// XXX: this might not work with two merged "CompatibleRes" resources...
// XXX: fix that so we can have the mappings to do it in lang/interpret.go ?
var ok = true
var applied = false // did we run an apply?
var noop = res.MetaParams().Noop // lookup the noop value

View File

@@ -95,12 +95,20 @@ func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
return fmt.Errorf("one of the autogroup flags is false")
}
// We don't want to bail on these two conditions if the kinds are the
// same. This prevents us from having a linear chain of pkg->pkg->pkg,
// instead of flattening all of them into one arbitrary choice. But if
// we are doing hierarchical grouping, then we want to allow this type
// of grouping, or we won't end up building any hierarchies! This change
// was added for http:ui stuff. Check this condition is really required.
if r1.Kind() == r2.Kind() { // XXX: needed or do we unwrap the contents?
if r1.IsGrouped() { // already grouped!
return fmt.Errorf("already grouped")
}
if len(r2.GetGroup()) > 0 { // already has children grouped!
return fmt.Errorf("already has groups")
}
}
if err := r1.GroupCmp(r2); err != nil { // resource groupcmp failed!
return errwrap.Wrapf(err, "the GroupCmp failed")
}

View File

@@ -49,6 +49,13 @@ import (
func init() {
engine.RegisterResource("nooptest", func() engine.Res { return &NoopResTest{} })
engine.RegisterResource("nooptestkind:foo", func() engine.Res { return &NoopResTest{} })
engine.RegisterResource("nooptestkind:foo:hello", func() engine.Res { return &NoopResTest{} })
engine.RegisterResource("nooptestkind:foo:world", func() engine.Res { return &NoopResTest{} })
engine.RegisterResource("nooptestkind:foo:world:big", func() engine.Res { return &NoopResTest{} })
engine.RegisterResource("nooptestkind:foo:world:bad", func() engine.Res { return &NoopResTest{} })
engine.RegisterResource("nooptestkind:foo:world:bazzz", func() engine.Res { return &NoopResTest{} })
engine.RegisterResource("nooptestkind:this:is:very:long", func() engine.Res { return &NoopResTest{} })
}
// NoopResTest is a no-op resource that groups strangely.
@@ -108,19 +115,35 @@ func (obj *NoopResTest) GroupCmp(r engine.GroupableRes) error {
}
// TODO: implement this in vertexCmp for *testGrouper instead?
k1 := strings.HasPrefix(obj.Kind(), "nooptestkind:")
k2 := strings.HasPrefix(res.Kind(), "nooptestkind:")
if !k1 && !k2 { // XXX: compat mode, to skip during "kind" tests
if strings.Contains(res.Name(), ",") { // HACK
return fmt.Errorf("already grouped") // element to be grouped is already grouped!
}
}
// XXX: make a better grouping algorithm for test expression
// XXX: this prevents us from re-using the same kind twice in a test...
// group different kinds if they're hierarchical (helpful hack for testing)
if obj.Kind() != res.Kind() {
s1 := strings.Split(obj.Kind(), ":")
s2 := strings.Split(res.Kind(), ":")
if len(s1) > len(s2) { // let longer get grouped INTO shorter
return fmt.Errorf("chunk inversion")
}
}
// group if they start with the same letter! (helpful hack for testing)
if obj.Name()[0] != res.Name()[0] {
return fmt.Errorf("different starting letter")
}
//fmt.Printf("group of: %+v into: %+v\n", res.Kind(), obj.Kind())
return nil
}
func NewNoopResTest(name string) *NoopResTest {
n, err := engine.NewNamedResource("nooptest", name)
func NewKindNoopResTest(kind, name string) *NoopResTest {
n, err := engine.NewNamedResource(kind, name)
if err != nil {
panic(fmt.Sprintf("unexpected error: %+v", err))
}
@@ -138,6 +161,10 @@ func NewNoopResTest(name string) *NoopResTest {
return x
}
func NewNoopResTest(name string) *NoopResTest {
return NewKindNoopResTest("nooptest", name)
}
func NewNoopResTestSema(name string, semas []string) *NoopResTest {
n := NewNoopResTest(name)
n.MetaParams().Sema = semas
@@ -174,21 +201,29 @@ func (obj *testGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
return fmt.Errorf("v2 is not a GroupableRes")
}
if r1.Kind() != r2.Kind() { // we must group similar kinds
// TODO: maybe future resources won't need this limitation?
return fmt.Errorf("the two resources aren't the same kind")
}
//if r1.Kind() != r2.Kind() { // we must group similar kinds
// // TODO: maybe future resources won't need this limitation?
// return fmt.Errorf("the two resources aren't the same kind")
//}
// someone doesn't want to group!
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
return fmt.Errorf("one of the autogroup flags is false")
}
// We don't want to bail on these two conditions if the kinds are the
// same. This prevents us from having a linear chain of pkg->pkg->pkg,
// instead of flattening all of them into one arbitrary choice. But if
// we are doing hierarchical grouping, then we want to allow this type
// of grouping, or we won't end up building any hierarchies!
if r1.Kind() == r2.Kind() {
if r1.IsGrouped() { // already grouped!
return fmt.Errorf("already grouped")
}
if len(r2.GetGroup()) > 0 { // already has children grouped!
return fmt.Errorf("already has groups")
}
}
if err := r1.GroupCmp(r2); err != nil { // resource groupcmp failed!
return errwrap.Wrapf(err, "the GroupCmp failed")
}
@@ -197,6 +232,8 @@ func (obj *testGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
}
func (obj *testGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
//fmt.Printf("merge of: %s into: %s\n", v2, v1)
// NOTE: this doesn't look at kind!
r1 := v1.(engine.GroupableRes)
r2 := v2.(engine.GroupableRes)
if err := r1.GroupRes(r2); err != nil { // group them first
@@ -273,9 +310,13 @@ Loop:
for v1 := range g1.Adjacency() { // for each vertex in g1
r1 := v1.(engine.GroupableRes)
l1 := strings.Split(r1.Name(), ",") // make list of everyone's names...
// XXX: this should be recursive for hierarchical grouping...
// XXX: instead, hack it for now:
if !strings.HasPrefix(r1.Kind(), "nooptestkind:") {
for _, x1 := range r1.GetGroup() {
l1 = append(l1, x1.Name()) // add my contents
}
}
l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates
sort.Strings(l1)
@@ -283,9 +324,13 @@ Loop:
for v2 := range g2.Adjacency() { // does it match in g2 ?
r2 := v2.(engine.GroupableRes)
l2 := strings.Split(r2.Name(), ",")
// XXX: this should be recursive for hierarchical grouping...
// XXX: instead, hack it for now:
if !strings.HasPrefix(r2.Kind(), "nooptestkind:") {
for _, x2 := range r2.GetGroup() {
l2 = append(l2, x2.Name())
}
}
l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates
sort.Strings(l2)
@@ -771,9 +816,9 @@ func TestPgraphGrouping16(t *testing.T) {
a := NewNoopResTest("a1,a2")
b1 := NewNoopResTest("b1")
c1 := NewNoopResTest("c1")
e1 := NE("e1")
e2 := NE("e2")
e3 := NE("e3")
e1 := NE("e1") // +e3 a bit?
e2 := NE("e2") // ok!
e3 := NE("e3") // +e1 a bit?
g3.AddEdge(a, b1, e1)
g3.AddEdge(b1, c1, e2)
g3.AddEdge(a, c1, e3)
@@ -859,9 +904,9 @@ func TestPgraphGrouping18(t *testing.T) {
a := NewNoopResTest("a1,a2")
b := NewNoopResTest("b1,b2")
c1 := NewNoopResTest("c1")
e1 := NE("e1")
e2 := NE("e2,e4")
e3 := NE("e3")
e1 := NE("e1") // +e3 a bit?
e2 := NE("e2,e4") // ok!
e3 := NE("e3") // +e1 a bit?
g3.AddEdge(a, b, e1)
g3.AddEdge(b, c1, e2)
g3.AddEdge(a, c1, e3)
@@ -978,3 +1023,110 @@ func TestPgraphSemaphoreGrouping3(t *testing.T) {
}
runGraphCmp(t, g1, g2)
}
func TestPgraphGroupingKinds0(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewKindNoopResTest("nooptestkind:foo", "a1")
a2 := NewKindNoopResTest("nooptestkind:foo:hello", "a2")
g1.AddVertex(a1, a2)
}
g2, _ := pgraph.NewGraph("g2") // expected result ?
{
a := NewNoopResTest("a1,a2")
g2.AddVertex(a)
}
runGraphCmp(t, g1, g2)
}
func TestPgraphGroupingKinds1(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewKindNoopResTest("nooptestkind:foo", "a1")
a2 := NewKindNoopResTest("nooptestkind:foo:world", "a2")
a3 := NewKindNoopResTest("nooptestkind:foo:world:big", "a3")
g1.AddVertex(a1, a2, a3)
}
g2, _ := pgraph.NewGraph("g2") // expected result ?
{
a := NewNoopResTest("a1,a2,a3")
g2.AddVertex(a)
}
runGraphCmp(t, g1, g2)
}
func TestPgraphGroupingKinds2(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewKindNoopResTest("nooptestkind:foo", "a1")
a2 := NewKindNoopResTest("nooptestkind:foo:world", "a2")
a3 := NewKindNoopResTest("nooptestkind:foo:world:big", "a3")
a4 := NewKindNoopResTest("nooptestkind:foo:world:bad", "a4")
g1.AddVertex(a1, a2, a3, a4)
}
g2, _ := pgraph.NewGraph("g2") // expected result ?
{
a := NewNoopResTest("a1,a2,a3,a4")
g2.AddVertex(a)
}
runGraphCmp(t, g1, g2)
}
func TestPgraphGroupingKinds3(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewKindNoopResTest("nooptestkind:foo", "a1")
a2 := NewKindNoopResTest("nooptestkind:foo:world", "a2")
a3 := NewKindNoopResTest("nooptestkind:foo:world:big", "a3")
a4 := NewKindNoopResTest("nooptestkind:foo:world:bad", "a4")
a5 := NewKindNoopResTest("nooptestkind:foo:world:bazzz", "a5")
g1.AddVertex(a1, a2, a3, a4, a5)
}
g2, _ := pgraph.NewGraph("g2") // expected result ?
{
a := NewNoopResTest("a1,a2,a3,a4,a5")
g2.AddVertex(a)
}
runGraphCmp(t, g1, g2)
}
// This test is valid, but our test system doesn't support duplicate kinds atm.
//func TestPgraphGroupingKinds4(t *testing.T) {
// g1, _ := pgraph.NewGraph("g1") // original graph
// {
// a1 := NewKindNoopResTest("nooptestkind:foo", "a1")
// a2 := NewKindNoopResTest("nooptestkind:foo:world", "a2")
// a3 := NewKindNoopResTest("nooptestkind:foo:world:big", "a3")
// a4 := NewKindNoopResTest("nooptestkind:foo:world:big", "a4")
// g1.AddVertex(a1, a2, a3, a4)
// }
// g2, _ := pgraph.NewGraph("g2") // expected result ?
// {
// a := NewNoopResTest("a1,a2,a3,a4")
// g2.AddVertex(a)
// }
// runGraphCmp(t, g1, g2)
//}
func TestPgraphGroupingKinds5(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewKindNoopResTest("nooptestkind:foo", "a1")
a2 := NewKindNoopResTest("nooptestkind:foo:world", "a2")
a3 := NewKindNoopResTest("nooptestkind:foo:world:big", "a3")
a4 := NewKindNoopResTest("nooptestkind:foo:world:bad", "a4")
a5 := NewKindNoopResTest("nooptestkind:foo:world:bazzz", "a5")
b1 := NewKindNoopResTest("nooptestkind:foo", "b1")
// NOTE: the very long one shouldn't group, but our test doesn't
// support detecting this pattern at the moment...
b2 := NewKindNoopResTest("nooptestkind:this:is:very:long", "b2")
g1.AddVertex(a1, a2, a3, a4, a5, b1, b2)
}
g2, _ := pgraph.NewGraph("g2") // expected result ?
{
a := NewNoopResTest("a1,a2,a3,a4,a5")
b := NewNoopResTest("b1,b2")
g2.AddVertex(a, b)
}
runGraphCmp(t, g1, g2)
}

43
engine/resources/Makefile Normal file
View File

@@ -0,0 +1,43 @@
# Mgmt
# Copyright (C) James Shubin and the project contributors
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Additional permission under GNU GPL version 3 section 7
#
# If you modify this program, or any covered work, by linking or combining it
# with embedded mcl code and modules (and that the embedded mcl code and
# modules which link with this program, contain a copy of their source code in
# the authoritative form) containing parts covered by the terms of any other
# license, the licensors of this program grant you additional permission to
# convey the resulting work. Furthermore, the licensors of this program grant
# the original author, James Shubin, additional permission to update this
# additional permission if he deems it necessary to achieve the goals of this
# additional permission.
SHELL = /usr/bin/env bash
.PHONY: build clean
default: build
WASM_FILE = http_ui/main.wasm
build: $(WASM_FILE)
$(WASM_FILE): http_ui/main.go
@echo "Generating: wasm..."
cd http_ui/ && env GOOS=js GOARCH=wasm go build -o `basename $(WASM_FILE)`
clean:
@rm -f $(WASM_FILE) || true

791
engine/resources/http_ui.go Normal file
View File

@@ -0,0 +1,791 @@
// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package resources
import (
"context"
_ "embed" // embed data with go:embed
"fmt"
"html/template"
"net/http"
"sort"
"strings"
"sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources/http_ui/common"
"github.com/purpleidea/mgmt/engine/resources/http_ui/static"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/gin-gonic/gin"
)
const (
httpUIKind = httpKind + ":ui"
httpUIIndexHTMLTmpl = "index.html.tmpl"
)
var (
//go:embed http_ui/index.html.tmpl
httpUIIndexHTMLTmplData string
//go:embed http_ui/wasm_exec.js
httpUIWasmExecData []byte
//go:embed http_ui/main.wasm
httpUIMainWasmData []byte
)
func init() {
engine.RegisterResource(httpUIKind, func() engine.Res { return &HTTPUIRes{} })
}
// HTTPServerUIGroupableRes is the interface that you must implement if you want
// to allow a resource the ability to be grouped into the http server ui
// resource. As an added safety, the Kind must also begin with "http:ui:", and
// not have more than one colon to avoid accidents of unwanted grouping.
type HTTPServerUIGroupableRes interface {
engine.Res
// ParentName is used to limit which resources autogroup into this one.
// If it's empty then it's ignored, otherwise it must match the Name of
// the parent to get grouped.
ParentName() string
// GetKind returns the "kind" of resource that this UI element is. This
// is technically different than the Kind() field, because it can be a
// unique kind that's specific to the HTTP form UI resources.
GetKind() string
// GetID returns the unique ID that this UI element responds to. Note
// that this is NOT replaceable by Name() because this ID is used in
// places that might be public, such as in webui form source code.
GetID() string
// SetValue sends the new value that was obtained from submitting the
// form. This is the raw, unsafe value that you must validate first.
SetValue(context.Context, []string) error
// GetValue gets a string representation for the form value, that we'll
// use in our html form.
GetValue(context.Context) (string, error)
// GetType returns a map that you can use to build the input field in
// the ui.
GetType() map[string]string
// GetSort returns a string that you can use to determine the global
// sorted display order of all the elements in a ui.
GetSort() string
}
// HTTPUIResData represents some additional data to attach to the resource.
type HTTPUIResData struct {
// Title is the generated page title that is displayed to the user.
Title string `lang:"title" yaml:"title"`
// Head is a list of strings to insert into the <head> and </head> tags
// of your page. This string allows HTML, so choose carefully!
// XXX: a *string should allow a partial struct here without having this
// field, but our type unification algorithm isn't this fancy yet...
Head string `lang:"head" yaml:"head"`
}
// HTTPUIRes is a web UI resource that exists within an http server. The name is
// used as the public path of the ui, unless the path field is specified, and in
// that case it is used instead. The way this works is that it autogroups at
// runtime with an existing http server resource, and in doing so makes the form
// associated with this resource available for serving from that http server.
type HTTPUIRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can be grouped into HTTPServerRes
init *engine.Init
// Server is the name of the http server resource to group this into. If
// it is omitted, and there is only a single http resource, then it will
// be grouped into it automatically. If there is more than one main http
// resource being used, then the grouping behaviour is *undefined* when
// this is not specified, and it is not recommended to leave this blank!
Server string `lang:"server" yaml:"server"`
// Path is the name of the path that this should be exposed under. For
// example, you might want to name this "/ui/" to expose it as "ui"
// under the server root. This overrides the name variable that is set.
Path string `lang:"path" yaml:"path"`
// Data represents some additional data to attach to the resource.
Data *HTTPUIResData `lang:"data" yaml:"data"`
//eventStream chan error
eventsChanMap map[engine.Res]chan error
// notifications contains a channel for every long poller waiting for a
// reply.
notifications map[engine.Res]map[chan struct{}]struct{}
// rwmutex guards the notifications map.
rwmutex *sync.RWMutex
ctx context.Context // set by Watch
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPUIRes) Default() engine.Res {
return &HTTPUIRes{}
}
// getPath returns the actual path we respond to. When Path is not specified, we
// use the Name. Note that this is the handler path that will be seen on the
// root http server, and this ui application might use a querystring and/or POST
// data as well.
func (obj *HTTPUIRes) getPath() string {
if obj.Path != "" {
return obj.Path
}
return obj.Name()
}
// routerPath returns an appropriate path for our router based on what we want
// to achieve using our parent prefix.
func (obj *HTTPUIRes) routerPath(p string) string {
if strings.HasPrefix(p, "/") {
return obj.getPath() + p[1:]
}
return obj.getPath() + p
}
// ParentName is used to limit which resources autogroup into this one. If it's
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPUIRes) ParentName() string {
return obj.Server
}
// AcceptHTTP determines whether we will respond to this request. Return nil to
// accept, or any error to pass.
func (obj *HTTPUIRes) AcceptHTTP(req *http.Request) error {
requestPath := req.URL.Path // TODO: is this what we want here?
//if requestPath != obj.getPath() {
// return fmt.Errorf("unhandled path")
//}
if !strings.HasPrefix(requestPath, obj.getPath()) {
return fmt.Errorf("unhandled path")
}
return nil
}
// getResByID returns the grouped resource with the id we're searching for if it
// exists, otherwise nil and false.
func (obj *HTTPUIRes) getResByID(id string) (HTTPServerUIGroupableRes, bool) {
for _, x := range obj.GetGroup() { // grouped elements
res, ok := x.(HTTPServerUIGroupableRes) // convert from Res
if !ok {
continue
}
if obj.init.Debug {
obj.init.Logf("Got grouped resource: %s", res.String())
}
if id != res.GetID() {
continue
}
return res, true
}
return nil, false
}
// ginLogger is a helper to get structured logs out of gin.
func (obj *HTTPUIRes) ginLogger() gin.HandlerFunc {
return func(c *gin.Context) {
//start := time.Now()
c.Next()
//duration := time.Since(start)
//timestamp := time.Now().Format(time.RFC3339)
method := c.Request.Method
path := c.Request.URL.Path
status := c.Writer.Status()
//latency := duration
clientIP := c.ClientIP()
if obj.init.Debug {
return
}
obj.init.Logf("%v %s %s (%d)", clientIP, method, path, status)
}
}
// getTemplate builds the super template that contains the map of each file name
// so that it can be used easily to send out named, templated documents.
func (obj *HTTPUIRes) getTemplate() (*template.Template, error) {
// XXX: get this from somewhere
m := make(map[string]string)
//m["foo.tmpl"] = "hello from file1" // TODO: add more content?
m[httpUIIndexHTMLTmpl] = httpUIIndexHTMLTmplData // index.html.tmpl
filenames := []string{}
for filename := range m {
filenames = append(filenames, filename)
}
sort.Strings(filenames) // deterministic order
var t *template.Template
// This logic from golang/src/html/template/template.go:parseFiles(...)
for _, filename := range filenames {
data := m[filename]
var tmpl *template.Template
if t == nil {
t = template.New(filename)
}
if filename == t.Name() {
tmpl = t
} else {
tmpl = t.New(filename)
}
if _, err := tmpl.Parse(data); err != nil {
return nil, err
}
}
t = t.Option("missingkey=error") // be thorough
return t, nil
}
// ServeHTTP is the standard HTTP handler that will be used here.
func (obj *HTTPUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// XXX: do all the router bits in Init() if we can...
gin.SetMode(gin.ReleaseMode) // for production
router := gin.New()
router.Use(obj.ginLogger(), gin.Recovery())
templ, err := obj.getTemplate() // do in init?
if err != nil {
obj.init.Logf("template error: %+v", err)
return
}
router.SetHTMLTemplate(templ)
router.GET(obj.routerPath("/index.html"), func(c *gin.Context) {
h := gin.H{}
h["program"] = obj.init.Program
h["version"] = obj.init.Version
h["hostname"] = obj.init.Hostname
h["embedded"] = static.HTTPUIStaticEmbedded // true or false
h["title"] = "" // key must be specified
h["path"] = obj.getPath()
if obj.Data != nil {
h["title"] = obj.Data.Title // template var
h["head"] = template.HTML(obj.Data.Head)
}
c.HTML(http.StatusOK, httpUIIndexHTMLTmpl, h)
})
router.GET(obj.routerPath("/main.wasm"), func(c *gin.Context) {
c.Data(http.StatusOK, "application/wasm", httpUIMainWasmData)
})
router.GET(obj.routerPath("/wasm_exec.js"), func(c *gin.Context) {
// the version of this file has to match compiler version
// the original came from: ~golang/lib/wasm/wasm_exec.js
// XXX: add a test to ensure this matches the compiler version
// the content-type matters or this won't work in the browser
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", httpUIWasmExecData)
})
if static.HTTPUIStaticEmbedded {
router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapCSS), func(c *gin.Context) {
c.Data(http.StatusOK, "text/css;charset=UTF-8", static.HTTPUIIndexStaticBootstrapCSS)
})
router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapJS), func(c *gin.Context) {
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", static.HTTPUIIndexStaticBootstrapJS)
})
}
router.POST(obj.routerPath("/save/"), func(c *gin.Context) {
id, ok := c.GetPostForm("id")
if !ok || id == "" {
msg := "missing id"
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}
values, ok := c.GetPostFormArray("value")
if !ok {
msg := "missing value"
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}
res, ok := obj.getResByID(id)
if !ok {
msg := fmt.Sprintf("id `%s` not found", id)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}
// we're storing data...
if err := res.SetValue(obj.ctx, values); err != nil {
msg := fmt.Sprintf("bad data: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}
// XXX: instead of an event to everything, instead if SetValue
// is an active sub resource (instead of something that noop's)
// that should send an event and eventually propagate to here,
// so skip sending this global one...
// Trigger a Watch() event so that CheckApply() calls Send/Recv,
// so our newly received POST value gets sent through the graph.
//select {
//case obj.eventStream <- nil: // send an event
//case <-obj.ctx.Done(): // in case Watch dies
// c.JSON(http.StatusInternalServerError, gin.H{
// "error": "Internal Server Error",
// "code": 500,
// })
//}
c.JSON(http.StatusOK, nil)
})
router.GET(obj.routerPath("/list/"), func(c *gin.Context) {
elements := []*common.FormElement{}
for _, x := range obj.GetGroup() { // grouped elements
res, ok := x.(HTTPServerUIGroupableRes) // convert from Res
if !ok {
continue
}
element := &common.FormElement{
Kind: res.GetKind(),
ID: res.GetID(),
Type: res.GetType(),
Sort: res.GetSort(),
}
elements = append(elements, element)
}
form := &common.Form{
Elements: elements,
}
// XXX: c.JSON or c.PureJSON ?
c.JSON(http.StatusOK, form) // send the struct as json
})
router.GET(obj.routerPath("/list/:id"), func(c *gin.Context) {
id := c.Param("id")
res, ok := obj.getResByID(id)
if !ok {
msg := fmt.Sprintf("id `%s` not found", id)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}
val, err := res.GetValue(obj.ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"code": 500,
})
return
}
el := &common.FormElementGeneric{ // XXX: text or string?
Value: val,
}
c.JSON(http.StatusOK, el) // send the struct as json
})
router.GET(obj.routerPath("/watch/:id"), func(c *gin.Context) {
id := c.Param("id")
res, ok := obj.getResByID(id)
if !ok {
msg := fmt.Sprintf("id `%s` not found", id)
c.JSON(http.StatusBadRequest, gin.H{"error": msg})
return
}
ch := make(chan struct{})
//defer close(ch) // don't close, let it gc instead
obj.rwmutex.Lock()
obj.notifications[res][ch] = struct{}{} // add to notification "list"
obj.rwmutex.Unlock()
defer func() {
obj.rwmutex.Lock()
delete(obj.notifications[res], ch)
obj.rwmutex.Unlock()
}()
select {
case <-ch: // http long poll
// pass
//case <-obj.???[res].Done(): // in case Watch dies
// c.JSON(http.StatusInternalServerError, gin.H{
// "error": "Internal Server Error",
// "code": 500,
// })
case <-obj.ctx.Done(): // in case Watch dies
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"code": 500,
})
return
}
val, err := res.GetValue(obj.ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"code": 500,
})
return
}
el := &common.FormElementGeneric{ // XXX: text or string?
Value: val,
}
c.JSON(http.StatusOK, el) // send the struct as json
})
router.GET(obj.routerPath("/ping"), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
router.ServeHTTP(w, req)
return
}
// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPUIRes) Validate() error {
if obj.getPath() == "" {
return fmt.Errorf("empty path")
}
// FIXME: does getPath need to start with a slash or end with one?
if !strings.HasPrefix(obj.getPath(), "/") {
return fmt.Errorf("the Path must be absolute")
}
if !strings.HasSuffix(obj.getPath(), "/") {
return fmt.Errorf("the Path must end with a slash")
}
return nil
}
// Init runs some startup code for this resource.
func (obj *HTTPUIRes) Init(init *engine.Init) error {
obj.init = init // save for later
//obj.eventStream = make(chan error)
obj.eventsChanMap = make(map[engine.Res]chan error)
obj.notifications = make(map[engine.Res]map[chan struct{}]struct{})
obj.rwmutex = &sync.RWMutex{}
// NOTE: If we don't Init anything that's autogrouped, then it won't
// even get an Init call on it.
// TODO: should we do this in the engine? Do we want to decide it here?
for _, res := range obj.GetGroup() { // grouped elements
// NOTE: We build a new init, but it's not complete. We only add
// what we're planning to use, and we ignore the rest for now...
r := res // bind the variable!
obj.eventsChanMap[r] = make(chan error)
obj.notifications[r] = make(map[chan struct{}]struct{})
event := func() {
select {
case obj.eventsChanMap[r] <- nil:
// send!
}
obj.rwmutex.RLock()
for ch := range obj.notifications[r] {
select {
case ch <- struct{}{}:
// send!
default:
// skip immediately if nobody is listening
}
}
obj.rwmutex.RUnlock()
// We don't do this here (why?) we instead read from the
// above channel and then send on multiplexedChan to the
// main loop, where it runs the obj.init.Event function.
//obj.init.Event() // notify engine of an event (this can block)
}
newInit := &engine.Init{
Program: obj.init.Program,
Version: obj.init.Version,
Hostname: obj.init.Hostname,
// Watch:
Running: event,
Event: event,
// CheckApply:
//Refresh: func() bool { // TODO: do we need this?
// innerRes, ok := r.(engine.RefreshableRes)
// if !ok {
// panic("res does not support the Refreshable trait")
// }
// return innerRes.Refresh()
//},
Send: engine.GenerateSendFunc(r),
Recv: engine.GenerateRecvFunc(r), // unused
FilteredGraph: func() (*pgraph.Graph, error) {
panic("FilteredGraph for HTTP:UI not implemented")
},
Local: obj.init.Local,
World: obj.init.World,
//VarDir: obj.init.VarDir, // TODO: wrap this
Debug: obj.init.Debug,
Logf: func(format string, v ...interface{}) {
obj.init.Logf(res.Kind()+": "+format, v...)
},
}
if err := res.Init(newInit); err != nil {
return errwrap.Wrapf(err, "autogrouped Init failed")
}
}
return nil
}
// Cleanup is run by the engine to clean up after the resource is done.
func (obj *HTTPUIRes) Cleanup() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events. This
// particular one does absolutely nothing but block until we've received a done
// signal.
func (obj *HTTPUIRes) Watch(ctx context.Context) error {
multiplexedChan := make(chan error)
defer close(multiplexedChan) // closes after everyone below us is finished
wg := &sync.WaitGroup{}
defer wg.Wait()
innerCtx, cancel := context.WithCancel(ctx) // store for ServeHTTP
defer cancel()
obj.ctx = innerCtx
for _, r := range obj.GetGroup() { // grouped elements
res := r // optional in newer golang
wg.Add(1)
go func() {
defer wg.Done()
defer close(obj.eventsChanMap[res]) // where Watch sends events
if err := res.Watch(ctx); err != nil {
select {
case multiplexedChan <- err:
case <-ctx.Done():
}
}
}()
// wait for Watch first Running() call or immediate error...
select {
case <-obj.eventsChanMap[res]: // triggers on start or on err...
}
wg.Add(1)
go func() {
defer wg.Done()
for {
var ok bool
var err error
select {
// receive
case err, ok = <-obj.eventsChanMap[res]:
if !ok {
return
}
}
// send (multiplex)
select {
case multiplexedChan <- err:
case <-ctx.Done():
return
}
}
}()
}
// we block until all the children are started first...
obj.init.Running() // when started, notify engine that we're running
startupChan := make(chan struct{})
close(startupChan) // send one initial signal
var send = false // send event?
for {
if obj.init.Debug {
obj.init.Logf("Looping...")
}
select {
case <-startupChan:
startupChan = nil
send = true
//case err, ok := <-obj.eventStream:
// if !ok { // shouldn't happen
// obj.eventStream = nil
// continue
// }
// if err != nil {
// return err
// }
// send = true
case err, ok := <-multiplexedChan:
if !ok { // shouldn't happen
multiplexedChan = nil
continue
}
if err != nil {
return err
}
send = true
case <-ctx.Done(): // closed by the engine to signal shutdown
return nil
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
obj.init.Event() // notify engine of an event (this can block)
}
}
//return nil // unreachable
}
// CheckApply is responsible for the Send/Recv aspects of the autogrouped
// resources. It recursively calls any autogrouped children.
func (obj *HTTPUIRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
if obj.init.Debug {
obj.init.Logf("CheckApply")
}
checkOK := true
for _, res := range obj.GetGroup() { // grouped elements
if c, err := res.CheckApply(ctx, apply); err != nil {
return false, errwrap.Wrapf(err, "autogrouped CheckApply failed")
} else if !c {
checkOK = false
}
}
return checkOK, nil // w00t
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *HTTPUIRes) Cmp(r engine.Res) error {
// we can only compare HTTPUIRes to others of the same resource kind
res, ok := r.(*HTTPUIRes)
if !ok {
return fmt.Errorf("res is not the same kind")
}
if obj.Server != res.Server {
return fmt.Errorf("the Server field differs")
}
if obj.Path != res.Path {
return fmt.Errorf("the Path differs")
}
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *HTTPUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPUIRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*HTTPUIRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to HTTPUIRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = HTTPUIRes(raw) // restore from indirection with type conversion!
return nil
}
// GroupCmp returns whether two resources can be grouped together or not. Can
// these two resources be merged, aka, does this resource support doing so? Will
// resource allow itself to be grouped _into_ this obj?
func (obj *HTTPUIRes) GroupCmp(r engine.GroupableRes) error {
res, ok := r.(HTTPServerUIGroupableRes) // different from what we usually do!
if !ok {
return fmt.Errorf("resource is not the right kind")
}
// If the http resource has the parent name field specified, then it
// must match against our name field if we want it to group with us.
if pn := res.ParentName(); pn != "" && pn != obj.Name() {
return fmt.Errorf("resource groups with a different parent name")
}
p := httpUIKind + ":"
// http:ui:foo is okay, but http:file is not
if !strings.HasPrefix(r.Kind(), p) {
return fmt.Errorf("not one of our children")
}
// http:ui:foo is okay, but http:ui:foo:bar is not
s := strings.TrimPrefix(r.Kind(), p)
if len(s) != len(r.Kind()) && strings.Count(s, ":") > 0 { // has prefix
return fmt.Errorf("maximum one resource after `%s` prefix", httpUIKind)
}
return nil
}

1
engine/resources/http_ui/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
main.wasm

View File

@@ -0,0 +1,8 @@
This directory contains the golang wasm source for the `http_ui` resource. It
gets built automatically when you run `make` from the main project root
directory.
After it gets built, the compiled artifact gets bundled into the main project
binary via go embed.
It is not a normal package that should get built with everything else.

View File

@@ -0,0 +1,82 @@
// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
// Package common contains some code that is shared between the wasm and the
// http:ui packages.
package common
const (
// HTTPUIInputType represents the field in the "Type" map that specifies
// which input type we're using.
HTTPUIInputType = "type"
// HTTPUIInputTypeText is the representation of the html "text" type.
HTTPUIInputTypeText = "text"
// HTTPUIInputTypeRange is the representation of the html "range" type.
HTTPUIInputTypeRange = "range"
// HTTPUIInputTypeRangeMin is the html input "range" min field.
HTTPUIInputTypeRangeMin = "min"
// HTTPUIInputTypeRangeMax is the html input "range" max field.
HTTPUIInputTypeRangeMax = "max"
// HTTPUIInputTypeRangeStep is the html input "range" step field.
HTTPUIInputTypeRangeStep = "step"
)
// Form represents the entire form containing all the desired elements.
type Form struct {
// Elements is a list of form elements in this form.
// TODO: Maybe this should be an interface?
Elements []*FormElement `json:"elements"`
}
// FormElement represents each form element.
type FormElement struct {
// Kind is the kind of form element that this is.
Kind string `json:"kind"`
// ID is the unique public id for this form element.
ID string `json:"id"`
// Type is a map that you can use to build the input field in the ui.
Type map[string]string `json:"type"`
// Sort is a string that you can use to determine the global sorted
// display order of all the elements in a ui.
Sort string `json:"sort"`
}
// FormElementGeneric is a value store.
type FormElementGeneric struct {
// Value holds the string value we're interested in.
Value string `json:"value"`
}

View File

@@ -0,0 +1,163 @@
{{- /*
Mgmt
Copyright (C) James Shubin and the project contributors
Written by James Shubin <james@shubin.ca> and the project contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Additional permission under GNU GPL version 3 section 7
If you modify this program, or any covered work, by linking or combining it
with embedded mcl code and modules (and that the embedded mcl code and
modules which link with this program, contain a copy of their source code in
the authoritative form) containing parts covered by the terms of any other
license, the licensors of this program grant you additional permission to
convey the resulting work. Furthermore, the licensors of this program grant
the original author, James Shubin, additional permission to update this
additional permission if he deems it necessary to achieve the goals of this
additional permission.
This was modified from the boiler-plate in the ~golang/misc/wasm/* directory.
*/ -}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
{{ if .title }}
<title>{{ .title }}</title>
{{ end }}
{{ if .head }}
{{ .head }}
{{ end }}
{{ if .embedded }}
<link href="static/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<script src="static/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
{{ else }}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-SgOJa3DmI69IUzQ2PVdRZhwQ+dy64/BUtbMJw1MZ8t5HZApcHrRKUc4W0kG879m7" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js" integrity="sha384-k6d4wzSIapyDyv1kpU366/PK5hCdSbCRGRCMv+eplOQJWyd1fbcAu9OCUj5zNLiq" crossorigin="anonymous"></script>
{{ end }}
<style>
/* Auto-apply Bootstrap-like blue (primary) styling based on element type. */
body {
--bs-primary: #0d6efd; /* Bootstrap 5 default primary color */
}
h1, h2, h3, h4, h5, h6, strong, b {
color: var(--bs-primary);
}
a {
color: var(--bs-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
color: #0b5ed7; /* slightly darker blue */
}
button, input[type="submit"], input[type="button"] {
background-color: var(--bs-primary);
color: #fff;
border: none;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
}
button:hover, input[type="submit"]:hover, input[type="button"]:hover {
background-color: #0b5ed7;
}
p, span, li {
color: #212529; /* standard text color */
}
code, pre {
background-color: #e7f1ff;
color: #084298;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
fieldset {
background-color: #e7f1ff;
border: 1px solid blue;
padding: 10px; /* optional: adds spacing inside the border */
margin-bottom: 20px; /* optional: adds spacing below the fieldset */
margin: 0 20px; /* adds 20px space on left and right */
}
label {
display: inline-block;
width: 100px; /* arbitrary */
text-align: right; /* aligns label text to the right */
margin-right: 10px; /* spacing between label and input */
margin-bottom: 8px; /* small vertical space below each label */
}
input[type="text"] {
width: 30ch; /* the number of characters you want to fit */
box-sizing: border-box; /* ensures padding and border are included in the width */
}
input[type="range"] {
vertical-align: middle; /* aligns the range input vertically with other elements */
width: 30ch; /* the number of characters you want to fit (to match text) */
box-sizing: border-box; /* ensures padding and border are included in the width */
}
</style>
</head>
<body>
<!--
Add the following polyfill for Microsoft Edge 17/18 support:
<script src="https://cdn.jsdelivr.net/npm/text-encoding@0.7.0/lib/encoding.min.js"></script>
(see https://caniuse.com/#feat=textencoder)
-->
<script src="wasm_exec.js"></script>
<script>
// These values can be read from inside the wasm program.
window._mgmt_program = "{{ .program }}";
window._mgmt_version = "{{ .version }}";
window._mgmt_hostname = "{{ .hostname }}";
window._mgmt_title = "{{ .title }}";
window._mgmt_path = "{{ .path }}";
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
//let mod, inst;
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
//mod = result.module;
//inst = result.instance;
go.run(result.instance);
}).catch((err) => {
console.error(err);
});
//async function run() {
// console.clear();
// await go.run(inst);
// inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
//}
</script>
</body>
</html>

View File

@@ -0,0 +1,314 @@
// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"syscall/js"
"time"
"github.com/purpleidea/mgmt/engine/resources/http_ui/common"
"github.com/purpleidea/mgmt/util/errwrap"
)
// Main is the main implementation of this process. It holds our shared data.
type Main struct {
// some values we pull in
program string
version string
hostname string
title string
path string
document js.Value
body js.Value
// window.location.origin (the base url with port for XHR)
wlo string
// base is the wlo + the specific path suffix
base string
response chan *Response
}
// Init must be called before the Main struct is used.
func (obj *Main) Init() error {
fmt.Println("Hello from mgmt wasm!")
obj.program = js.Global().Get("_mgmt_program").String()
obj.version = js.Global().Get("_mgmt_version").String()
obj.hostname = js.Global().Get("_mgmt_hostname").String()
obj.title = js.Global().Get("_mgmt_title").String()
obj.path = js.Global().Get("_mgmt_path").String()
obj.document = js.Global().Get("document")
obj.body = obj.document.Get("body")
obj.wlo = js.Global().Get("window").Get("location").Get("origin").String()
obj.base = obj.wlo + obj.path
obj.response = make(chan *Response)
return nil
}
// Run is the main execution of this program.
func (obj *Main) Run() error {
h1 := obj.document.Call("createElement", "h1")
h1.Set("innerHTML", obj.title)
obj.body.Call("appendChild", h1)
h6 := obj.document.Call("createElement", "h6")
pre := obj.document.Call("createElement", "pre")
pre.Set("textContent", fmt.Sprintf("This is: %s, version: %s, on %s", obj.program, obj.version, obj.hostname))
//pre.Set("innerHTML", fmt.Sprintf("This is: %s, version: %s, on %s", obj.program, obj.version, obj.hostname))
h6.Call("appendChild", pre)
obj.body.Call("appendChild", h6)
obj.body.Call("appendChild", obj.document.Call("createElement", "hr"))
//document.baseURI
// XXX: how to get the base so we can add our own querystring???
fmt.Println("URI: ", obj.document.Get("baseURI").String())
fmt.Println("window.location.origin: ", obj.wlo)
fmt.Println("BASE: ", obj.base)
fieldset := obj.document.Call("createElement", "fieldset")
legend := obj.document.Call("createElement", "legend")
legend.Set("textContent", "live!") // XXX: pick some message here
fieldset.Call("appendChild", legend)
// XXX: consider using this instead: https://github.com/hashicorp/go-retryablehttp
//client := retryablehttp.NewClient()
//client.RetryMax = 10
client := &http.Client{
//Timeout: time.Duration(timeout) * time.Second,
//CheckRedirect: checkRedirectFunc,
}
// Startup form building...
// XXX: Add long polling to know if the form shape changes, and offer a
// refresh to the end-user to see the new form.
listURL := obj.base + "list/"
watchURL := obj.base + "watch/"
resp, err := client.Get(listURL) // works
if err != nil {
return errwrap.Wrapf(err, "could not list ui")
}
s, err := io.ReadAll(resp.Body) // TODO: apparently we can stream
resp.Body.Close()
if err != nil {
return errwrap.Wrapf(err, "could read from listed ui")
}
fmt.Printf("Response: %+v\n", string(s))
var form *common.Form
if err := json.Unmarshal(s, &form); err != nil {
return errwrap.Wrapf(err, "could not unmarshal form")
}
//fmt.Printf("%+v\n", form) // debug
// Sort according to the "sort" field so elements are in expected order.
sort.Slice(form.Elements, func(i, j int) bool {
return form.Elements[i].Sort < form.Elements[j].Sort
})
for _, x := range form.Elements {
id := x.ID
resp, err := client.Get(listURL + id)
if err != nil {
return errwrap.Wrapf(err, "could not get id %s", id)
}
s, err := io.ReadAll(resp.Body) // TODO: apparently we can stream
resp.Body.Close()
if err != nil {
return errwrap.Wrapf(err, "could not read from id %s", id)
}
fmt.Printf("Response: %+v\n", string(s))
var element *common.FormElementGeneric // XXX: switch based on x.Kind
if err := json.Unmarshal(s, &element); err != nil {
return errwrap.Wrapf(err, "could not unmarshal id %s", id)
}
//fmt.Printf("%+v\n", element) // debug
inputType, exists := x.Type[common.HTTPUIInputType] // "text" or "range" ...
if !exists {
fmt.Printf("Element has no input type: %+v\n", element)
continue
}
label := obj.document.Call("createElement", "label")
label.Call("setAttribute", "for", id)
label.Set("innerHTML", fmt.Sprintf("%s: ", id))
fieldset.Call("appendChild", label)
el := obj.document.Call("createElement", "input")
el.Set("id", id)
//el.Call("setAttribute", "id", id)
//el.Call("setAttribute", "name", id)
el.Set("type", inputType)
if inputType == common.HTTPUIInputTypeRange {
if val, exists := x.Type[common.HTTPUIInputTypeRangeMin]; exists {
el.Set("min", val)
}
if val, exists := x.Type[common.HTTPUIInputTypeRangeMax]; exists {
el.Set("max", val)
}
if val, exists := x.Type[common.HTTPUIInputTypeRangeStep]; exists {
el.Set("step", val)
}
}
el.Set("value", element.Value) // XXX: here or after change handler?
// event handler
changeEvent := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
event := args[0]
value := event.Get("target").Get("value").String()
//obj.wg.Add(1)
go func() {
//defer obj.wg.Done()
fmt.Println("Action!")
u := obj.base + "save/"
values := url.Values{
"id": {id},
"value": {value},
}
resp, err := http.PostForm(u, values)
//fmt.Println(resp, err) // debug
s, err := io.ReadAll(resp.Body) // TODO: apparently we can stream
resp.Body.Close()
fmt.Printf("Response: %+v\n", string(s))
fmt.Printf("Error: %+v\n", err)
obj.response <- &Response{
Str: string(s),
Err: err,
}
}()
return nil
})
defer changeEvent.Release()
el.Call("addEventListener", "change", changeEvent)
// http long poll
go func() {
for {
fmt.Printf("About to long poll for: %s\n", id)
//resp, err := client.Get(watchURL + id) // XXX: which?
resp, err := http.Get(watchURL + id)
if err != nil {
fmt.Println("Error fetching:", watchURL+id, err) // XXX: test error paths
time.Sleep(2 * time.Second)
continue
}
s, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Println("Error reading response:", err)
time.Sleep(2 * time.Second)
continue
}
var element *common.FormElementGeneric // XXX: switch based on x.Kind
if err := json.Unmarshal(s, &element); err != nil {
fmt.Println("could not unmarshal id %s: %v", id, err)
time.Sleep(2 * time.Second)
continue
}
//fmt.Printf("%+v\n", element) // debug
fmt.Printf("Long poll for %s got: %s\n", id, element.Value)
obj.document.Call("getElementById", id).Set("value", element.Value)
//time.Sleep(1 * time.Second)
}
}()
fieldset.Call("appendChild", el)
br := obj.document.Call("createElement", "br")
fieldset.Call("appendChild", br)
}
obj.body.Call("appendChild", fieldset)
// We need this mainloop for receiving the results of our async stuff...
for {
select {
case resp, ok := <-obj.response:
if !ok {
break
}
if err := resp.Err; err != nil {
fmt.Printf("Err: %+v\n", err)
continue
}
fmt.Printf("Str: %+v\n", resp.Str)
}
}
return nil
}
// Response is a standard response struct which we pass through.
type Response struct {
Str string
Err error
}
func main() {
m := &Main{}
if err := m.Init(); err != nil {
fmt.Printf("Error: %+v\n", err)
return
}
if err := m.Run(); err != nil {
fmt.Printf("Error: %+v\n", err)
return
}
select {} // don't shutdown wasm
}

View File

@@ -0,0 +1,2 @@
*.css
*.js

View File

@@ -0,0 +1,51 @@
// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
//go:build httpuistatic
package static
import (
_ "embed" // embed data with go:embed
)
const (
// HTTPUIStaticEmbedded specifies whether files have been embedded.
HTTPUIStaticEmbedded = true
)
var (
// HTTPUIIndexStaticBootstrapCSS is the embedded data. It is embedded.
//go:embed http_ui/static/bootstrap.min.css
HTTPUIIndexStaticBootstrapCSS []byte
// HTTPUIIndexStaticBootstrapJS is the embedded data. It is embedded.
//go:embed http_ui/static/bootstrap.bundle.min.js
HTTPUIIndexStaticBootstrapJS []byte
)

View File

@@ -0,0 +1,45 @@
// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
//go:build !httpuistatic
package static
const (
// HTTPUIStaticEmbedded specifies whether files have been embedded.
HTTPUIStaticEmbedded = false
)
var (
// HTTPUIIndexStaticBootstrapCSS is the embedded data. It is empty here.
HTTPUIIndexStaticBootstrapCSS []byte
// HTTPUIIndexStaticBootstrapJS is the embedded data. It is empty here.
HTTPUIIndexStaticBootstrapJS []byte
)

View File

@@ -0,0 +1,42 @@
// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
// Package static contains some optional embedded data which can be useful if we
// are running from an entirely offline, internet-absent scenario.
package static
const (
// HTTPUIIndexBootstrapCSS is the path to the bootstrap css file when
// embedded, relative to the parent directory.
HTTPUIIndexBootstrapCSS = "static/bootstrap.min.css"
// HTTPUIIndexBootstrapJS is the path to the bootstrap js file when
// embedded, relative to the parent directory.
HTTPUIIndexBootstrapJS = "static/bootstrap.bundle.min.js"
)

View File

@@ -0,0 +1,577 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This was copied from the original in the ~golang/lib/wasm/* directory.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.path) {
globalThis.path = {
resolve(...pathSegments) {
return pathSegments.join("/");
}
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
callExport: testCallExport,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

View File

@@ -0,0 +1,653 @@
// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package resources
import (
"context"
"fmt"
"net/url"
"strconv"
"sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources/http_ui/common"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util/errwrap"
)
const (
httpUIInputKind = httpUIKind + ":input"
httpUIInputStoreKey = "key"
httpUIInputStoreSchemeLocal = "local"
httpUIInputStoreSchemeWorld = "world"
httpUIInputTypeText = common.HTTPUIInputTypeText // "text"
httpUIInputTypeRange = common.HTTPUIInputTypeRange // "range"
)
func init() {
engine.RegisterResource(httpUIInputKind, func() engine.Res { return &HTTPUIInputRes{} })
}
// HTTPUIInputRes is a form element that exists within a http:ui resource, which
// exists within an http server. The name is used as the unique id of the field,
// unless the id field is specified, and in that case it is used instead. The
// way this works is that it autogroups at runtime with an existing http:ui
// resource, and in doing so makes the form field associated with this resource
// available as part of that ui which is itself grouped and served from the http
// server resource.
type HTTPUIInputRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can be grouped into HTTPUIRes
traits.Sendable
init *engine.Init
// Path is the name of the http ui resource to group this into. If it is
// omitted, and there is only a single http ui resource, then it will
// be grouped into it automatically. If there is more than one main http
// ui resource being used, then the grouping behaviour is *undefined*
// when this is not specified, and it is not recommended to leave this
// blank!
Path string `lang:"path" yaml:"path"`
// ID is the unique id for this element. It is used in form fields and
// should not be a private identifier. It must be unique within a given
// http ui.
ID string `lang:"id" yaml:"id"`
// Value is the default value to use for the form field. If you change
// it, then the resource graph will change and we'll rebuild and have
// the new value visible. You can use either this or the Store field.
// XXX: If we ever add our resource mutate API, we might not need to
// swap to a new resource graph, and maybe Store is not needed?
Value string `lang:"value" yaml:"value"`
// Store the data in this source. It will also read in a default value
// from there if one is present. It will watch it for changes as well,
// and update the displayed value if it's changed from another source.
// This cannot be used at the same time as the Value field.
Store string `lang:"store" yaml:"store"`
// Type specifies the type of input field this is, and some information
// about it.
// XXX: come up with a format such as "multiline://?max=60&style=foo"
Type string `lang:"type" yaml:"type"`
// Sort is a string that you can use to determine the global sorted
// display order of all the elements in a ui.
Sort string `lang:"sort" yaml:"sort"`
scheme string // the scheme we're using with Store, cached for later
key string // the key we're using with Store, cached for later
typeURL *url.URL // the type data, cached for later
typeURLValues url.Values // the type data, cached for later
last *string // the last value we sent
value string // what we've last received from SetValue
storeEvent bool // did a store event happen?
mutex *sync.Mutex // guards storeEvent and value
event chan struct{} // local event that the setValue sends
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPUIInputRes) Default() engine.Res {
return &HTTPUIInputRes{
Type: "text://",
}
}
// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPUIInputRes) Validate() error {
if obj.GetID() == "" {
return fmt.Errorf("empty id")
}
if obj.Value != "" && obj.Store != "" {
return fmt.Errorf("may only use either Value or Store")
}
if obj.Value != "" {
if err := obj.checkValue(obj.Value); err != nil {
return errwrap.Wrapf(err, "the Value field is invalid")
}
}
if obj.Store != "" {
// XXX: check the URI format
}
return nil
}
// Init runs some startup code for this resource.
func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
obj.init = init // save for later
u, err := url.Parse(obj.Type)
if err != nil {
return err
}
if u == nil {
return fmt.Errorf("can't parse Type")
}
if u.Scheme != httpUIInputTypeText && u.Scheme != httpUIInputTypeRange {
return fmt.Errorf("unknown scheme: %s", u.Scheme)
}
values, err := url.ParseQuery(u.RawQuery)
if err != nil {
return err
}
obj.typeURL = u
obj.typeURLValues = values
if obj.Store != "" {
u, err := url.Parse(obj.Store)
if err != nil {
return err
}
if u == nil {
return fmt.Errorf("can't parse Store")
}
if u.Scheme != httpUIInputStoreSchemeLocal && u.Scheme != httpUIInputStoreSchemeWorld {
return fmt.Errorf("unknown scheme: %s", u.Scheme)
}
values, err := url.ParseQuery(u.RawQuery)
if err != nil {
return err
}
obj.scheme = u.Scheme // cache for later
obj.key = obj.Name() // default
x, exists := values[httpUIInputStoreKey]
if exists && len(x) > 0 && x[0] != "" { // ignore absent or broken keys
obj.key = x[0]
}
}
// populate our obj.value cache somehow, so we don't mutate obj.Value
obj.value = obj.Value // copy
obj.mutex = &sync.Mutex{}
obj.event = make(chan struct{}, 1) // buffer to avoid blocks or deadlock
return nil
}
// Cleanup is run by the engine to clean up after the resource is done.
func (obj *HTTPUIInputRes) Cleanup() error {
return nil
}
// getKey returns the key to be used for this resource. If the Store field is
// specified, it will use that parsed part, otherwise it uses the Name.
func (obj *HTTPUIInputRes) getKey() string {
if obj.Store != "" {
return obj.key
}
return obj.Name()
}
// ParentName is used to limit which resources autogroup into this one. If it's
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPUIInputRes) ParentName() string {
return obj.Path
}
// GetKind returns the kind of this resource.
func (obj *HTTPUIInputRes) GetKind() string {
// NOTE: We don't *need* to return such a specific string, and "input"
// would be enough, but we might as well use this because we have it.
return httpUIInputKind
}
// GetID returns the actual ID we respond to. When ID is not specified, we use
// the Name.
func (obj *HTTPUIInputRes) GetID() string {
if obj.ID != "" {
return obj.ID
}
return obj.Name()
}
// SetValue stores the new value field that was obtained from submitting the
// form. This receives the raw, unsafe value that you must validate first.
func (obj *HTTPUIInputRes) SetValue(ctx context.Context, vs []string) error {
if len(vs) != 1 {
return fmt.Errorf("unexpected length of %d", len(vs))
}
value := vs[0]
if err := obj.checkValue(value); err != nil {
return err
}
obj.mutex.Lock()
obj.setValue(ctx, value) // also sends an event
obj.mutex.Unlock()
return nil
}
// setValue is the helper version where the caller must provide the mutex.
func (obj *HTTPUIInputRes) setValue(ctx context.Context, val string) error {
obj.value = val
select {
case obj.event <- struct{}{}:
default:
}
return nil
}
func (obj *HTTPUIInputRes) checkValue(value string) error {
// XXX: validate based on obj.Type
// XXX: validate what kind of values are allowed, probably no \n, etc...
return nil
}
// GetValue gets a string representation for the form value, that we'll use in
// our html form.
func (obj *HTTPUIInputRes) GetValue(ctx context.Context) (string, error) {
obj.mutex.Lock()
defer obj.mutex.Unlock()
if obj.storeEvent {
val, exists, err := obj.storeGet(ctx, obj.getKey())
if err != nil {
return "", errwrap.Wrapf(err, "error during get")
}
if !exists {
return "", nil // default
}
return val, nil
}
return obj.value, nil
}
// GetType returns a map that you can use to build the input field in the ui.
func (obj *HTTPUIInputRes) GetType() map[string]string {
m := make(map[string]string)
if obj.typeURL.Scheme == httpUIInputTypeRange {
m = obj.rangeGetType()
}
m[common.HTTPUIInputType] = obj.typeURL.Scheme
return m
}
func (obj *HTTPUIInputRes) rangeGetType() map[string]string {
m := make(map[string]string)
base := 10
bits := 64
if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeMin]; exists && len(sa) > 0 {
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
m[common.HTTPUIInputTypeRangeMin] = strconv.FormatInt(x, base)
}
}
if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeMax]; exists && len(sa) > 0 {
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
m[common.HTTPUIInputTypeRangeMax] = strconv.FormatInt(x, base)
}
}
if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeStep]; exists && len(sa) > 0 {
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
m[common.HTTPUIInputTypeRangeStep] = strconv.FormatInt(x, base)
}
}
return m
}
// GetSort returns a string that you can use to determine the global sorted
// display order of all the elements in a ui.
func (obj *HTTPUIInputRes) GetSort() string {
return obj.Sort
}
// Watch is the primary listener for this resource and it outputs events. This
// particular one does absolutely nothing but block until we've received a done
// signal.
func (obj *HTTPUIInputRes) Watch(ctx context.Context) error {
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
return obj.localWatch(ctx)
}
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
return obj.worldWatch(ctx)
}
obj.init.Running() // when started, notify engine that we're running
// XXX: do we need to watch on obj.event for normal .Value stuff?
select {
case <-ctx.Done(): // closed by the engine to signal shutdown
}
//obj.init.Event() // notify engine of an event (this can block)
return nil
}
func (obj *HTTPUIInputRes) localWatch(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch, err := obj.init.Local.ValueWatch(ctx, obj.getKey()) // get possible events!
if err != nil {
return errwrap.Wrapf(err, "error during watch")
}
obj.init.Running() // when started, notify engine that we're running
for {
select {
case _, ok := <-ch:
if !ok { // channel shutdown
return nil
}
obj.mutex.Lock()
obj.storeEvent = true
obj.mutex.Unlock()
case <-obj.event:
case <-ctx.Done(): // closed by the engine to signal shutdown
return nil
}
if obj.init.Debug {
obj.init.Logf("event!")
}
obj.init.Event() // notify engine of an event (this can block)
}
}
func (obj *HTTPUIInputRes) worldWatch(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch, err := obj.init.World.StrWatch(ctx, obj.getKey()) // get possible events!
if err != nil {
return errwrap.Wrapf(err, "error during watch")
}
obj.init.Running() // when started, notify engine that we're running
for {
select {
case err, ok := <-ch:
if !ok { // channel shutdown
return nil
}
if err != nil {
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
}
obj.mutex.Lock()
obj.storeEvent = true
obj.mutex.Unlock()
case <-obj.event:
case <-ctx.Done(): // closed by the engine to signal shutdown
return nil
}
if obj.init.Debug {
obj.init.Logf("event!")
}
obj.init.Event() // notify engine of an event (this can block)
}
}
// CheckApply performs the send/recv portion of this autogrouped resources. That
// can fail, but only if the send portion fails for some reason. If we're using
// the Store feature, then it also reads and writes to and from that store.
func (obj *HTTPUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
if obj.init.Debug {
obj.init.Logf("CheckApply")
}
// If we're in ".Value" mode, we want to look at the incoming value, and
// send it onwards. This function mostly exists as a stub in this case.
// The private value gets set by obj.SetValue from the http:ui parent.
// If we're in ".Store" mode, then we're reconciling between the "World"
// and the http:ui "Web".
if obj.Store != "" {
return obj.storeCheckApply(ctx, apply)
}
return obj.valueCheckApply(ctx, apply)
}
func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (bool, error) {
obj.mutex.Lock()
value := obj.value // gets set by obj.SetValue
obj.mutex.Unlock()
if obj.last != nil && *obj.last == value {
return true, nil // expected value has already been sent
}
if !apply { // XXX: does this break send/recv if we end early?
return false, nil
}
s := value // copy
obj.last = &s // cache
// XXX: This is getting called twice, what's the bug?
obj.init.Logf("sending: %s", value)
// send
if err := obj.init.Send(&HTTPUIInputSends{
Value: &value,
}); err != nil {
return false, err
}
return false, nil
//return true, nil // always succeeds, with nothing to do!
}
// storeCheckApply is a tricky function where we attempt to reconcile the state
// between a third-party changing the value in the World database, and a recent
// "http:ui" change by and end user. Basically whoever runs last is the "right"
// value that we want to use. We know who sent the event from reading the
// storeEvent variable, and if it was the World, we want to cache it locally,
// and if it was the Web, then we want to push it up to the store.
func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) {
v1, exists, err := obj.storeGet(ctx, obj.getKey())
if err != nil {
return false, errwrap.Wrapf(err, "error during get")
}
obj.mutex.Lock()
v2 := obj.value // gets set by obj.SetValue
storeEvent := obj.storeEvent
obj.storeEvent = false // reset it
obj.mutex.Unlock()
if exists && v1 == v2 { // both sides are happy
return true, nil
}
if !apply { // XXX: does this break send/recv if we end early?
return false, nil
}
obj.mutex.Lock()
if storeEvent { // event from World, pull down the value
err = obj.setValue(ctx, v1) // also sends an event
}
value := obj.value
obj.mutex.Unlock()
if err != nil {
return false, err
}
if !exists || !storeEvent { // event from web, push up the value
if err := obj.storeSet(ctx, obj.getKey(), value); err != nil {
return false, errwrap.Wrapf(err, "error during set")
}
}
obj.init.Logf("sending: %s", value)
// send
if err := obj.init.Send(&HTTPUIInputSends{
Value: &value,
}); err != nil {
return false, err
}
return false, nil
}
func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) {
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
val, err := obj.init.Local.ValueGet(ctx, key)
if err != nil {
return "", false, err // real error
}
if val == nil { // if val is nil, and no error then it doesn't exist
return "", false, nil // val doesn't exist
}
s, ok := val.(string)
if !ok {
// TODO: support different types perhaps?
return "", false, fmt.Errorf("not a string") // real error
}
return s, true, nil
}
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
val, err := obj.init.World.StrGet(ctx, key)
if err != nil && obj.init.World.StrIsNotExist(err) {
return "", false, nil // val doesn't exist
}
if err != nil {
return "", false, err // real error
}
return val, true, nil
}
return "", false, nil // something else
}
func (obj *HTTPUIInputRes) storeSet(ctx context.Context, key, val string) error {
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
return obj.init.Local.ValueSet(ctx, key, val)
}
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
return obj.init.World.StrSet(ctx, key, val)
}
return nil // something else
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *HTTPUIInputRes) Cmp(r engine.Res) error {
// we can only compare HTTPUIInputRes to others of the same resource kind
res, ok := r.(*HTTPUIInputRes)
if !ok {
return fmt.Errorf("res is not the same kind")
}
if obj.Path != res.Path {
return fmt.Errorf("the Path differs")
}
if obj.ID != res.ID {
return fmt.Errorf("the ID differs")
}
if obj.Value != res.Value {
return fmt.Errorf("the Value differs")
}
if obj.Store != res.Store {
return fmt.Errorf("the Store differs")
}
if obj.Type != res.Type {
return fmt.Errorf("the Type differs")
}
if obj.Sort != res.Sort {
return fmt.Errorf("the Sort differs")
}
return nil
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *HTTPUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPUIInputRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*HTTPUIInputRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to HTTPUIInputRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = HTTPUIInputRes(raw) // restore from indirection with type conversion!
return nil
}
// HTTPUIInputSends is the struct of data which is sent after a successful
// Apply.
type HTTPUIInputSends struct {
// Value is the text element value being sent.
Value *string `lang:"value"`
}
// Sends represents the default struct of values we can send using Send/Recv.
func (obj *HTTPUIInputRes) Sends() interface{} {
return &HTTPUIInputSends{
Value: nil,
}
}

View File

@@ -287,7 +287,11 @@ func (obj *KVRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
if val, exists := obj.init.Recv()["value"]; exists && val.Changed {
// if we received on Value, and it changed, wooo, nothing to do.
obj.init.Logf("`value` was received!")
if obj.Value == nil {
obj.init.Logf("nil `value` was received!")
} else {
obj.init.Logf("`value` (%s) was received!", *obj.Value)
}
}
value, exists, err := obj.kvGet(ctx, obj.getKey())

View File

@@ -78,12 +78,25 @@ func (obj *Groupable) GroupCmp(res engine.GroupableRes) error {
// GroupRes groups resource argument (res) into self. Callers of this method
// should probably also run SetParent.
func (obj *Groupable) GroupRes(res engine.GroupableRes) error {
// We can keep this check with hierarchical grouping by adding in the
// Kind test which we seen inside... If they're all the same, then we
// can't do it. But if they're dissimilar, then it's okay to group!
if l := len(res.GetGroup()); l > 0 {
kind := res.Kind()
ok := true // assume okay for now
for _, r := range res.GetGroup() {
if r.Kind() == kind {
ok = false // non-hierarchical grouping, error!
}
}
if !ok {
return fmt.Errorf("the `%s` resource already contains %d grouped resources", res, l)
}
if res.IsGrouped() {
return fmt.Errorf("the `%s` resource is already grouped", res)
}
// XXX: Do we need to disable this to support hierarchical grouping?
//if res.IsGrouped() {
// return fmt.Errorf("the `%s` resource is already grouped", res)
//}
obj.grouped = append(obj.grouped, res)
res.SetGrouped(true) // i am contained _in_ a group

View File

@@ -72,7 +72,12 @@ type Recvable struct {
Bug5819 interface{} // XXX: workaround
}
// SetRecv is used to inject incoming values into the resource.
// SetRecv is used to inject incoming values into the resource. More
// specifically, it stores the mapping of what gets received from what, so that
// later on, we know which resources should ask which other resources for the
// values that they want to receive. Since this happens when we're building the
// graph, and before the autogrouping step, we'll have pointers to the original,
// ungrouped resources here, so that it will work even after they're grouped in!
func (obj *Recvable) SetRecv(recv map[string]*engine.Send) {
//if obj.recv == nil {
// obj.recv = make(map[string]*engine.Send)

View File

@@ -0,0 +1,43 @@
import "world"
http:server ":8080" { # by default http uses :80 but using :8080 avoids needing root!
#address => ":8080", # you can override the name like this
}
# you can add a raw file like this...
http:file "/file1" {
data => "hello, world, i'm file1 and i don't exist on disk!\n",
}
http:ui "/ui/" {
#path => "/ui/", # we can override the name like this if needed
data => struct{
title => "mgmt http ui",
head => "", # XXX: type unification requires specifying all fields for now
},
}
$text1_id = "text1"
$range1_id = "range1"
http:ui:input $text1_id {
store => "world://",
sort => "a",
}
http:ui:input $range1_id {
store => "world://",
type => "range://?min=0&max=5&step=1",
sort => "b",
}
#Http:Ui:Input[$text1_id].value -> Kv[$text1_id].value
#kv $text1_id { # store in world
#}
$ret1 = world.getval($text1_id) # name of kv resource
test "get1" {
anotherstr => $ret1->value,
onlyshow => ["AnotherStr",], # displays nicer
}

View File

@@ -0,0 +1,65 @@
import "value"
http:server ":8080" { # by default http uses :80 but using :8080 avoids needing root!
#address => ":8080", # you can override the name like this
}
# you can add a raw file like this...
http:file "/file1" {
data => "hello, world, i'm file1 and i don't exist on disk!\n",
}
http:ui "/ui/" {
#path => "/ui/", # we can override the name like this if needed
data => struct{
title => "mgmt http ui",
head => "", # XXX: type unification requires specifying all fields for now
},
}
$text1_id = "text1"
$range1_id = "range1"
$text1_val = if $ret1->ready {
$ret1->value
} else {
"default" # some default
}
$range1_val = if $ret2->ready {
$ret2->value
} else {
"2" # some default
}
http:ui:input $text1_id {
value => $text1_val, # it passes back into itself!
}
http:ui:input $range1_id {
value => $range1_val,
type => "range://?min=0&max=5&step=1",
sort => "b",
}
Http:Ui:Input[$text1_id].value -> Value[$text1_id].any
value $text1_id {
any => "whatever", # TODO: remove the temporary placeholder here
#any => "", # XXX: remove any placeholder to see the bug when absent
}
value $range1_id {
any => "whatever", # TODO: remove the temporary placeholder here
#any => "", # XXX: remove any placeholder to see the bug when absent
}
$ret1 = value.get_str($text1_id) # name of value resource
$ret2 = value.get_str($range1_id) # name of value resource
test "get1" {
anotherstr => $ret1->value,
onlyshow => ["AnotherStr",], # displays nicer
}
test "get2" {
anotherstr => $ret2->value,
onlyshow => ["AnotherStr",], # displays nicer
}

32
go.mod
View File

@@ -41,8 +41,8 @@ require (
go.etcd.io/etcd/client/v3 v3.5.18
go.etcd.io/etcd/server/v3 v3.5.18
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.32.0
golang.org/x/sys v0.29.0
golang.org/x/crypto v0.37.0
golang.org/x/sys v0.32.0
golang.org/x/time v0.9.0
golang.org/x/tools v0.29.0
gopkg.in/yaml.v2 v2.4.0
@@ -61,10 +61,14 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/bmc-toolbox/common v0.0.0-20250114061816-fab80349cae0 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
@@ -73,11 +77,18 @@ require (
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
@@ -107,6 +118,8 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect
@@ -127,6 +140,7 @@ require (
github.com/nxadm/tail v1.4.11 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
@@ -142,7 +156,9 @@ require (
github.com/spf13/pflag v1.0.6-0.20250109003754-5ca813443bd2 // indirect
github.com/stmcginnis/gofish v0.20.0 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect
@@ -163,18 +179,20 @@ require (
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.0.3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

57
go.sum
View File

@@ -50,6 +50,11 @@ github.com/bmc-toolbox/common v0.0.0-20250114061816-fab80349cae0 h1:tsKHYBkzElym
github.com/bmc-toolbox/common v0.0.0-20250114061816-fab80349cae0/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I=
github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4mvaOAM=
github.com/bombsimon/logrusr/v2 v2.0.1/go.mod h1:ByVAX+vHdLGAfdroiMg6q0zgq2FODY2lc5YJvzmOJio=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -63,6 +68,10 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
@@ -111,8 +120,14 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -136,7 +151,15 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -280,6 +303,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -292,6 +319,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/libvirt/libvirt-go v7.4.0+incompatible h1:crnSLkwPqCdXtg6jib/FxBG/hweAc/3Wxth1AehCXL4=
github.com/libvirt/libvirt-go v7.4.0+incompatible/go.mod h1:34zsnB4iGeOv7Byj6qotuW8Ya4v4Tr43ttjz/F0wjLE=
github.com/libvirt/libvirt-go-xml v7.4.0+incompatible h1:+BBo2XjlT8pAK4pm+aSX8mC/6nc/rdRac10ZukpW31U=
@@ -364,6 +393,8 @@ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2sz
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
@@ -445,6 +476,7 @@ github.com/stmcginnis/gofish v0.20.0 h1:hH2V2Qe898F2wWT1loApnkDUrXXiLKqbSlMaH3Y1
github.com/stmcginnis/gofish v0.20.0/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -453,7 +485,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
@@ -461,9 +496,13 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1
github.com/tredoe/osutil v1.5.0 h1:UGVxbbHRoZi8xXVmbNZ2vgG6XoJ15ndE4LniiQ3rJKg=
github.com/tredoe/osutil v1.5.0/go.mod h1:TEzphzUUunysbdDRfdOgqkg10POQbnfIPV50ynqOfIg=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
@@ -531,6 +570,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -538,6 +579,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
@@ -575,6 +618,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -588,6 +633,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -632,15 +679,21 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -689,6 +742,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -724,5 +779,7 @@ honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68w
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -0,0 +1,11 @@
-- main.mcl --
http:ui:input "text1" {}
Http:Ui:Input["text1"].value -> Kv["kv1"].value
kv "kv1" {}
-- OUTPUT --
Edge: http:ui:input[text1] -> kv[kv1] # http:ui:input[text1] -> kv[kv1]
Vertex: http:ui:input[text1]
Vertex: kv[kv1]

View File

@@ -49,6 +49,11 @@ if [[ "$@" = *"--integration"* ]]; then
else
for pkg in `go list -e ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old" | grep -v "^${base}/old/" | grep -v "^${base}/tmp" | grep -v "^${base}/tmp/" | grep -v "^${base}/integration"`; do
echo -e "\ttesting: $pkg"
if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_ui" ]; then
continue # skip this special main package
fi
if [[ "$@" = *"--race"* ]]; then
# split up long tests to avoid CI timeouts
if [ "$pkg" = "${base}/lang" ]; then # pkg lang is big!

View File

@@ -129,6 +129,11 @@ function reflowed-comments() {
# run go vet on a per-package basis
base=$(go list .)
for pkg in `go list -e ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old" | grep -v "^${base}/old/" | grep -v "^${base}/tmp" | grep -v "^${base}/tmp/"`; do
if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_ui" ]; then
continue # skip this special main package
fi
echo -e "\tgo vet: $pkg"
run-test go vet -source "$pkg" || fail_test "go vet -source did not pass pkg"