engine: resources: Add the proper prefix to grouped http resources
Resources that can be grouped into the http:server resource must have that prefix. Grouping is basically hierarchical, and without that common prefix, it means we'd have to special-case our grouping algorithm.
This commit is contained in:
2
Makefile
2
Makefile
@@ -38,7 +38,7 @@ SHELL = bash
|
|||||||
# a large amount of output from this `find`, can cause `make` to be much slower!
|
# 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/*')
|
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/*')
|
MCL_FILES := $(shell find lang/ -name '*.mcl' -not -path 'old/*' -not -path 'tmp/*')
|
||||||
MISC_FILES := $(shell find engine/resources/http_ui/)
|
MISC_FILES := $(shell find engine/resources/http_server_ui/)
|
||||||
|
|
||||||
SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always))
|
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))
|
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func (obj *Engine) Process(ctx context.Context, vertex pgraph.Vertex) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we contain grouped resources, maybe someone inside wants to recv?
|
// 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.
|
// This code is similar to the above and was added for http:server:ui.
|
||||||
// XXX: Maybe this block isn't needed, as mentioned we need to check!
|
// XXX: Maybe this block isn't needed, as mentioned we need to check!
|
||||||
if res, ok := vertex.(engine.GroupableRes); ok {
|
if res, ok := vertex.(engine.GroupableRes); ok {
|
||||||
process := res.GetGroup() // look through these
|
process := res.GetGroup() // look through these
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
|
|||||||
// same. This prevents us from having a linear chain of pkg->pkg->pkg,
|
// 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
|
// instead of flattening all of them into one arbitrary choice. But if
|
||||||
// we are doing hierarchical grouping, then we want to allow this type
|
// 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
|
// of grouping, or we won't end up building any hierarchies! This was
|
||||||
// was added for http:ui stuff. Check this condition is really required.
|
// added for http:server:ui. Check this condition is really required.
|
||||||
if r1.Kind() == r2.Kind() { // XXX: needed or do we unwrap the contents?
|
if r1.Kind() == r2.Kind() { // XXX: needed or do we unwrap the contents?
|
||||||
if r1.IsGrouped() { // already grouped!
|
if r1.IsGrouped() { // already grouped!
|
||||||
return fmt.Errorf("already grouped")
|
return fmt.Errorf("already grouped")
|
||||||
|
|||||||
@@ -58,17 +58,18 @@ func (ag *baseGrouper) Init(g *pgraph.Graph) error {
|
|||||||
ag.graph = g // pointer
|
ag.graph = g // pointer
|
||||||
|
|
||||||
// We sort deterministically, first by kind, and then by name. In
|
// We sort deterministically, first by kind, and then by name. In
|
||||||
// particular, longer kind chunks sort first. So http:ui:text should
|
// particular, longer kind chunks sort first. So http:server:ui:input
|
||||||
// appear before http:server and http:ui. This is a hack so that if we
|
// should appear before http:server and http:server:ui. This is a
|
||||||
// are doing hierarchical automatic grouping, it gives the http:ui:text
|
// strategy so that if we are doing hierarchical automatic grouping, it
|
||||||
// a chance to get grouped into http:ui, before http:ui gets grouped
|
// gives the http:server:ui:input a chance to get grouped into
|
||||||
// into http:server, because once that happens, http:ui:text will never
|
// http:server:ui, before http:server:ui gets grouped into http:server,
|
||||||
// get grouped, and this won't work properly. This works, because when
|
// because once that happens, http:server:ui:input will never get
|
||||||
// we start comparing iteratively the list of resources, it does this
|
// grouped, and this won't work properly. This works, because when we
|
||||||
// with a O(n^2) loop that compares the X and Y zero indexes first, and
|
// start comparing iteratively the list of resources, it does this with
|
||||||
// and then continues along. If the "longer" resources appear first,
|
// a O(n^2) loop that compares the X and Y zero indexes first, and then
|
||||||
// then they'll group together first. We should probably put this into
|
// continues along. If the "longer" resources appear first, then they'll
|
||||||
// a new Grouper struct, but for now we might as well leave it here.
|
// group together first. We should probably put this into a new Grouper
|
||||||
|
// struct, but for now we might as well leave it here.
|
||||||
//vertices := ag.graph.VerticesSorted() // formerly
|
//vertices := ag.graph.VerticesSorted() // formerly
|
||||||
vertices := RHVSort(ag.graph.Vertices())
|
vertices := RHVSort(ag.graph.Vertices())
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ func (obj RHVSlice) Less(i, j int) bool {
|
|||||||
li := len(si)
|
li := len(si)
|
||||||
lj := len(sj)
|
lj := len(sj)
|
||||||
|
|
||||||
if li != lj { // eg: http:ui vs. http:ui:text
|
if li != lj { // eg: http:server:ui vs. http:server:ui:text
|
||||||
return li > lj // reverse
|
return li > lj // reverse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ SHELL = /usr/bin/env bash
|
|||||||
.PHONY: build clean
|
.PHONY: build clean
|
||||||
default: build
|
default: build
|
||||||
|
|
||||||
WASM_FILE = http_ui/main.wasm
|
WASM_FILE = http_server_ui/main.wasm
|
||||||
|
|
||||||
build: $(WASM_FILE)
|
build: $(WASM_FILE)
|
||||||
|
|
||||||
$(WASM_FILE): http_ui/main.go
|
$(WASM_FILE): http_server_ui/main.go
|
||||||
@echo "Generating: wasm..."
|
@echo "Generating: wasm..."
|
||||||
cd http_ui/ && env GOOS=js GOARCH=wasm go build -o `basename $(WASM_FILE)`
|
cd http_server_ui/ && env GOOS=js GOARCH=wasm go build -o `basename $(WASM_FILE)`
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -f $(WASM_FILE) || true
|
@rm -f $(WASM_FILE) || true
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
814
engine/resources/http_server.go
Normal file
814
engine/resources/http_server.go
Normal file
@@ -0,0 +1,814 @@
|
|||||||
|
// 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"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
|
securefilepath "github.com/cyphar/filepath-securejoin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HTTPUseSecureJoin specifies that we should add in a "secure join" lib
|
||||||
|
// so that we avoid the ../../etc/passwd and symlink problems.
|
||||||
|
HTTPUseSecureJoin = true
|
||||||
|
|
||||||
|
httpServerKind = httpKind + ":server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource(httpServerKind, func() engine.Res { return &HTTPServerRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPServerGroupableRes is the interface that you must implement if you want
|
||||||
|
// to allow a resource the ability to be grouped into the http server resource.
|
||||||
|
// As an added safety, the Kind must also begin with "http:", and not have more
|
||||||
|
// than one colon, or it must begin with http:server:, and not have any further
|
||||||
|
// colons to avoid accidents of unwanted grouping.
|
||||||
|
type HTTPServerGroupableRes 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
|
||||||
|
|
||||||
|
// AcceptHTTP determines whether this will respond to this request.
|
||||||
|
// Return nil to accept, or any error to pass. This should be
|
||||||
|
// deterministic (pure) and fast.
|
||||||
|
AcceptHTTP(req *http.Request) error
|
||||||
|
|
||||||
|
// ServeHTTP is the standard HTTP handler that will be used for this.
|
||||||
|
http.Handler // ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPServerRes is an http server resource. It serves files, but does not
|
||||||
|
// actually apply any state. The name is used as the address to listen on,
|
||||||
|
// unless the Address field is specified, and in that case it is used instead.
|
||||||
|
// This resource can offer up files for serving that are specified either inline
|
||||||
|
// in this resource by specifying an http root, or as http:server:file resources
|
||||||
|
// which will get autogrouped into this resource at runtime. The two methods can
|
||||||
|
// be combined as well.
|
||||||
|
//
|
||||||
|
// This server also supports autogrouping some more magical resources into it.
|
||||||
|
// For example, the http:server:flag and http:server:ui resources add in magic
|
||||||
|
// endpoints.
|
||||||
|
//
|
||||||
|
// This server is not meant as a featureful replacement for the venerable and
|
||||||
|
// modern httpd servers out there, but rather as a simple, dynamic, integrated
|
||||||
|
// alternative for bootstrapping new machines and clusters in an elegant way.
|
||||||
|
//
|
||||||
|
// TODO: add support for TLS
|
||||||
|
// XXX: Make the http:server:ui resource that functions can read data from!
|
||||||
|
// XXX: The http:server:ui resource can also take in values from those functions
|
||||||
|
type HTTPServerRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Edgeable // XXX: add autoedge support
|
||||||
|
traits.Groupable // can have HTTPServerFileRes and others grouped into it
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
// Address is the listen address to use for the http server. It is
|
||||||
|
// common to use `:80` (the standard) to listen on TCP port 80 on all
|
||||||
|
// addresses.
|
||||||
|
Address string `lang:"address" yaml:"address"`
|
||||||
|
|
||||||
|
// Timeout is the maximum duration in seconds to use for unspecified
|
||||||
|
// timeouts. In other words, when this value is specified, it is used as
|
||||||
|
// the value for the other *Timeout values when they aren't used. Put
|
||||||
|
// another way, this makes it easy to set all the different timeouts
|
||||||
|
// with a single parameter.
|
||||||
|
Timeout *uint64 `lang:"timeout" yaml:"timeout"`
|
||||||
|
|
||||||
|
// ReadTimeout is the maximum duration in seconds for reading during the
|
||||||
|
// http request. If it is zero, then there is no timeout. If this is
|
||||||
|
// unspecified, then the value of Timeout is used instead if it is set.
|
||||||
|
// For more information, see the golang net/http Server documentation.
|
||||||
|
ReadTimeout *uint64 `lang:"read_timeout" yaml:"read_timeout"`
|
||||||
|
|
||||||
|
// WriteTimeout is the maximum duration in seconds for writing during
|
||||||
|
// the http request. If it is zero, then there is no timeout. If this is
|
||||||
|
// unspecified, then the value of Timeout is used instead if it is set.
|
||||||
|
// For more information, see the golang net/http Server documentation.
|
||||||
|
WriteTimeout *uint64 `lang:"write_timeout" yaml:"write_timeout"`
|
||||||
|
|
||||||
|
// ShutdownTimeout is the maximum duration in seconds to wait for the
|
||||||
|
// server to shutdown gracefully before calling Close. By default it is
|
||||||
|
// nice to let client connections terminate gracefully, however it might
|
||||||
|
// take longer than we are willing to wait, particularly if one is long
|
||||||
|
// polling or running a very long download. As a result, you can set a
|
||||||
|
// timeout here. The default is zero which means it will wait
|
||||||
|
// indefinitely. The shutdown process can also be cancelled by the
|
||||||
|
// interrupt handler which this resource supports. If this is
|
||||||
|
// unspecified, then the value of Timeout is used instead if it is set.
|
||||||
|
ShutdownTimeout *uint64 `lang:"shutdown_timeout" yaml:"shutdown_timeout"`
|
||||||
|
|
||||||
|
// Root is the root directory that we should serve files from. If it is
|
||||||
|
// not specified, then it is not used. Any http file resources will have
|
||||||
|
// precedence over anything in here, in case the same path exists twice.
|
||||||
|
// TODO: should we have a flag to determine the precedence rules here?
|
||||||
|
Root string `lang:"root" yaml:"root"`
|
||||||
|
|
||||||
|
// TODO: should we allow adding a list of one-of files directly here?
|
||||||
|
|
||||||
|
eventsChanMap map[engine.Res]chan error
|
||||||
|
interruptChan chan struct{}
|
||||||
|
|
||||||
|
conn net.Listener
|
||||||
|
serveMux *http.ServeMux // can't share the global one between resources!
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *HTTPServerRes) Default() engine.Res {
|
||||||
|
return &HTTPServerRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAddress returns the actual address to use. When Address is not specified,
|
||||||
|
// we use the Name.
|
||||||
|
func (obj *HTTPServerRes) getAddress() string {
|
||||||
|
if obj.Address != "" {
|
||||||
|
return obj.Address
|
||||||
|
}
|
||||||
|
return obj.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getReadTimeout determines the value for ReadTimeout, because if unspecified,
|
||||||
|
// this will default to the value of Timeout.
|
||||||
|
func (obj *HTTPServerRes) getReadTimeout() *uint64 {
|
||||||
|
if obj.ReadTimeout != nil {
|
||||||
|
return obj.ReadTimeout
|
||||||
|
}
|
||||||
|
return obj.Timeout // might be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWriteTimeout determines the value for WriteTimeout, because if
|
||||||
|
// unspecified, this will default to the value of Timeout.
|
||||||
|
func (obj *HTTPServerRes) getWriteTimeout() *uint64 {
|
||||||
|
if obj.WriteTimeout != nil {
|
||||||
|
return obj.WriteTimeout
|
||||||
|
}
|
||||||
|
return obj.Timeout // might be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getShutdownTimeout determines the value for ShutdownTimeout, because if
|
||||||
|
// unspecified, this will default to the value of Timeout.
|
||||||
|
func (obj *HTTPServerRes) getShutdownTimeout() *uint64 {
|
||||||
|
if obj.ShutdownTimeout != nil {
|
||||||
|
return obj.ShutdownTimeout
|
||||||
|
}
|
||||||
|
return obj.Timeout // might be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
||||||
|
// accept, or any error to pass. In this particular case, it accepts for the
|
||||||
|
// Root directory handler, but it happens to be implemented with this signature
|
||||||
|
// in case it gets moved. It doesn't intentionally match the
|
||||||
|
// HTTPServerGroupableRes interface.
|
||||||
|
func (obj *HTTPServerRes) AcceptHTTP(req *http.Request) error {
|
||||||
|
// Look in root if we have one, and we haven't got a file yet...
|
||||||
|
if obj.Root == "" {
|
||||||
|
return fmt.Errorf("no Root directory")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP is the standard HTTP handler that will be used here. In this
|
||||||
|
// particular case, it serves the Root directory handler, but it happens to be
|
||||||
|
// implemented with this signature in case it gets moved. It doesn't
|
||||||
|
// intentionally match the HTTPServerGroupableRes interface.
|
||||||
|
func (obj *HTTPServerRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// We only allow GET at the moment.
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
|
|
||||||
|
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
|
||||||
|
if !strings.HasPrefix(p, obj.Root) { // root ends with /
|
||||||
|
// user might have tried a ../../etc/passwd hack
|
||||||
|
obj.init.Logf("join inconsistency: %s", p)
|
||||||
|
http.NotFound(w, req) // lie to them...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if HTTPUseSecureJoin {
|
||||||
|
var err error
|
||||||
|
p, err = securefilepath.SecureJoin(obj.Root, requestPath)
|
||||||
|
if err != nil {
|
||||||
|
obj.init.Logf("secure join fail: %s", p)
|
||||||
|
http.NotFound(w, req) // lie to them...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Got file at root: %s", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, err := os.Open(p)
|
||||||
|
if err != nil {
|
||||||
|
obj.init.Logf("could not open: %s", p)
|
||||||
|
sendHTTPError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer handle.Close() // ignore error
|
||||||
|
|
||||||
|
// Determine the last-modified time if we can.
|
||||||
|
modtime := time.Now()
|
||||||
|
fi, err := handle.Stat()
|
||||||
|
if err == nil {
|
||||||
|
modtime = fi.ModTime()
|
||||||
|
}
|
||||||
|
// TODO: if Stat errors, should we fail the whole thing?
|
||||||
|
|
||||||
|
// XXX: is requestPath what we want for the name field?
|
||||||
|
http.ServeContent(w, req, requestPath, modtime, handle)
|
||||||
|
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
|
func (obj *HTTPServerRes) Validate() error {
|
||||||
|
if obj.getAddress() == "" {
|
||||||
|
return fmt.Errorf("empty address")
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(obj.getAddress())
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress())
|
||||||
|
}
|
||||||
|
if host != "" {
|
||||||
|
// TODO: should we allow fqdn's here?
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("the Address is not a valid IP: %s", host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Root != "" && !strings.HasPrefix(obj.Root, "/") {
|
||||||
|
return fmt.Errorf("the Root must be absolute")
|
||||||
|
}
|
||||||
|
if obj.Root != "" && !strings.HasSuffix(obj.Root, "/") {
|
||||||
|
return fmt.Errorf("the Root must be a dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: validate that the autogrouped resources don't have paths that
|
||||||
|
// conflict with each other. We can only have a single unique entry for
|
||||||
|
// what handles a /whatever URL.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *HTTPServerRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
// No need to error in Validate if Timeout is ignored, but log it.
|
||||||
|
// These are all specified, so Timeout effectively does nothing.
|
||||||
|
a := obj.ReadTimeout != nil
|
||||||
|
b := obj.WriteTimeout != nil
|
||||||
|
c := obj.ShutdownTimeout != nil
|
||||||
|
if obj.Timeout != nil && (a && b && c) {
|
||||||
|
obj.init.Logf("the Timeout param is being ignored")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: If we don't Init anything that's autogrouped, then it won't
|
||||||
|
// even get an Init call on it.
|
||||||
|
|
||||||
|
obj.eventsChanMap = make(map[engine.Res]chan error)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
event := func() {
|
||||||
|
select {
|
||||||
|
case obj.eventsChanMap[r] <- nil:
|
||||||
|
// send!
|
||||||
|
}
|
||||||
|
// 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 {
|
||||||
|
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 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(r.String()+": "+format, v...)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := res.Init(newInit); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "autogrouped Init failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.interruptChan = make(chan struct{})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *HTTPServerRes) Cleanup() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *HTTPServerRes) Watch(ctx context.Context) error {
|
||||||
|
// TODO: I think we could replace all this with:
|
||||||
|
//obj.conn, err := net.Listen("tcp", obj.getAddress())
|
||||||
|
// ...but what is the advantage?
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", obj.getAddress())
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not resolve address")
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.conn, err = net.ListenTCP("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not start listener")
|
||||||
|
}
|
||||||
|
defer obj.conn.Close()
|
||||||
|
|
||||||
|
obj.serveMux = http.NewServeMux() // do it here in case Watch restarts!
|
||||||
|
// TODO: We could consider having the obj.GetGroup loop here, instead of
|
||||||
|
// essentially having our own "router" API with AcceptHTTP.
|
||||||
|
obj.serveMux.HandleFunc("/", obj.handler())
|
||||||
|
|
||||||
|
readTimeout := uint64(0)
|
||||||
|
if i := obj.getReadTimeout(); i != nil {
|
||||||
|
readTimeout = *i
|
||||||
|
}
|
||||||
|
writeTimeout := uint64(0)
|
||||||
|
if i := obj.getWriteTimeout(); i != nil {
|
||||||
|
writeTimeout = *i
|
||||||
|
}
|
||||||
|
obj.server = &http.Server{
|
||||||
|
Addr: obj.getAddress(),
|
||||||
|
Handler: obj.serveMux,
|
||||||
|
ReadTimeout: time.Duration(readTimeout) * time.Second,
|
||||||
|
WriteTimeout: time.Duration(writeTimeout) * time.Second,
|
||||||
|
//MaxHeaderBytes: 1 << 20, XXX: should we add a param for this?
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplexedChan := make(chan error)
|
||||||
|
defer close(multiplexedChan) // closes after everyone below us is finished
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
var closeError error
|
||||||
|
closeSignal := make(chan struct{})
|
||||||
|
|
||||||
|
shutdownChan := make(chan struct{}) // server shutdown finished signal
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
select {
|
||||||
|
case <-obj.interruptChan:
|
||||||
|
// TODO: should we bubble up the error from Close?
|
||||||
|
// TODO: do we need a mutex around this Close?
|
||||||
|
obj.server.Close() // kill it quickly!
|
||||||
|
case <-shutdownChan:
|
||||||
|
// let this exit
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer close(closeSignal)
|
||||||
|
|
||||||
|
err := obj.server.Serve(obj.conn) // blocks until Shutdown() is called!
|
||||||
|
if err == nil || err == http.ErrServerClosed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if this returned on its own, then closeSignal can be used...
|
||||||
|
closeError = errwrap.Wrapf(err, "the server errored")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
|
||||||
|
// immediately return ErrServerClosed. Make sure the program doesn't
|
||||||
|
// exit and waits instead for Shutdown to return.
|
||||||
|
defer func() {
|
||||||
|
defer close(shutdownChan) // signal that shutdown is finished
|
||||||
|
innerCtx := context.Background()
|
||||||
|
if i := obj.getShutdownTimeout(); i != nil && *i > 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
innerCtx, cancel = context.WithTimeout(innerCtx, time.Duration(*i)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
err := obj.server.Shutdown(innerCtx) // shutdown gracefully
|
||||||
|
if err == context.DeadlineExceeded {
|
||||||
|
// TODO: should we bubble up the error from Close?
|
||||||
|
// TODO: do we need a mutex around this Close?
|
||||||
|
obj.server.Close() // kill it now
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
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 := <-multiplexedChan:
|
||||||
|
if !ok { // shouldn't happen
|
||||||
|
multiplexedChan = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
send = true
|
||||||
|
|
||||||
|
case <-closeSignal: // something shut us down early
|
||||||
|
return closeError
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||||
|
// It does however check that certain runtime requirements (such as the Root dir
|
||||||
|
// existing if one was specified) are fulfilled. If there are any autogrouped
|
||||||
|
// resources, those will be recursively called so that they can send/recv.
|
||||||
|
func (obj *HTTPServerRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("CheckApply")
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: We don't want the initial CheckApply to return true until the
|
||||||
|
// Watch has started up, so we must block here until that's the case...
|
||||||
|
|
||||||
|
// Cheap runtime validation!
|
||||||
|
// XXX: maybe only do this only once to avoid repeated, unnecessary checks?
|
||||||
|
if obj.Root != "" {
|
||||||
|
fileInfo, err := os.Stat(obj.Root)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "can't stat Root dir")
|
||||||
|
}
|
||||||
|
if !fileInfo.IsDir() {
|
||||||
|
return false, fmt.Errorf("the Root path is not a dir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *HTTPServerRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare HTTPServerRes to others of the same resource kind
|
||||||
|
res, ok := r.(*HTTPServerRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("res is not the same kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Address != res.Address {
|
||||||
|
return fmt.Errorf("the Address differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.Timeout == nil) != (res.Timeout == nil) { // xor
|
||||||
|
return fmt.Errorf("the Timeout differs")
|
||||||
|
}
|
||||||
|
if obj.Timeout != nil && res.Timeout != nil {
|
||||||
|
if *obj.Timeout != *res.Timeout { // compare the values
|
||||||
|
return fmt.Errorf("the value of Timeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.ReadTimeout == nil) != (res.ReadTimeout == nil) {
|
||||||
|
return fmt.Errorf("the ReadTimeout differs")
|
||||||
|
}
|
||||||
|
if obj.ReadTimeout != nil && res.ReadTimeout != nil {
|
||||||
|
if *obj.ReadTimeout != *res.ReadTimeout {
|
||||||
|
return fmt.Errorf("the value of ReadTimeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.WriteTimeout == nil) != (res.WriteTimeout == nil) {
|
||||||
|
return fmt.Errorf("the WriteTimeout differs")
|
||||||
|
}
|
||||||
|
if obj.WriteTimeout != nil && res.WriteTimeout != nil {
|
||||||
|
if *obj.WriteTimeout != *res.WriteTimeout {
|
||||||
|
return fmt.Errorf("the value of WriteTimeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.ShutdownTimeout == nil) != (res.ShutdownTimeout == nil) {
|
||||||
|
return fmt.Errorf("the ShutdownTimeout differs")
|
||||||
|
}
|
||||||
|
if obj.ShutdownTimeout != nil && res.ShutdownTimeout != nil {
|
||||||
|
if *obj.ShutdownTimeout != *res.ShutdownTimeout {
|
||||||
|
return fmt.Errorf("the value of ShutdownTimeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We could do this sort of thing to skip checking Timeout when it
|
||||||
|
// is not used, but for the moment, this is overkill and not needed yet.
|
||||||
|
//a := obj.ReadTimeout != nil
|
||||||
|
//b := obj.WriteTimeout != nil
|
||||||
|
//c := obj.ShutdownTimeout != nil
|
||||||
|
//if !(obj.Timeout != nil && (a && b && c)) {
|
||||||
|
// // the Timeout param is not being ignored
|
||||||
|
//}
|
||||||
|
|
||||||
|
if obj.Root != res.Root {
|
||||||
|
return fmt.Errorf("the Root differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt is called to ask the execution of this resource to end early. It
|
||||||
|
// will cause the server Shutdown to end abruptly instead of leading open client
|
||||||
|
// connections terminate gracefully. It does this by causing the server Close
|
||||||
|
// method to run.
|
||||||
|
func (obj *HTTPServerRes) Interrupt() error {
|
||||||
|
close(obj.interruptChan) // this should cause obj.server.Close() to run!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
|
||||||
|
// TODO: should this copy internal state?
|
||||||
|
func (obj *HTTPServerRes) Copy() engine.CopyableRes {
|
||||||
|
var timeout, readTimeout, writeTimeout, shutdownTimeout *uint64
|
||||||
|
if obj.Timeout != nil {
|
||||||
|
x := *obj.Timeout
|
||||||
|
timeout = &x
|
||||||
|
}
|
||||||
|
if obj.ReadTimeout != nil {
|
||||||
|
x := *obj.ReadTimeout
|
||||||
|
readTimeout = &x
|
||||||
|
}
|
||||||
|
if obj.WriteTimeout != nil {
|
||||||
|
x := *obj.WriteTimeout
|
||||||
|
writeTimeout = &x
|
||||||
|
}
|
||||||
|
if obj.ShutdownTimeout != nil {
|
||||||
|
x := *obj.ShutdownTimeout
|
||||||
|
shutdownTimeout = &x
|
||||||
|
}
|
||||||
|
return &HTTPServerRes{
|
||||||
|
Address: obj.Address,
|
||||||
|
Timeout: timeout,
|
||||||
|
ReadTimeout: readTimeout,
|
||||||
|
WriteTimeout: writeTimeout,
|
||||||
|
ShutdownTimeout: shutdownTimeout,
|
||||||
|
Root: obj.Root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
|
// primarily useful for setting the defaults.
|
||||||
|
func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes HTTPServerRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*HTTPServerRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to HTTPServerRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = HTTPServerRes(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 *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
|
||||||
|
res, ok := r.(HTTPServerGroupableRes) // 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// http:server:foo is okay, but file or config:etcd is not
|
||||||
|
if !strings.HasPrefix(r.Kind(), httpServerKind+":") {
|
||||||
|
return fmt.Errorf("not one of our children")
|
||||||
|
}
|
||||||
|
|
||||||
|
// http:server:foo is okay, but http:server:foo:bar is not
|
||||||
|
p1 := httpServerKind + ":"
|
||||||
|
s1 := strings.TrimPrefix(r.Kind(), p1)
|
||||||
|
if len(s1) != len(r.Kind()) && strings.Count(s1, ":") > 0 { // has prefix
|
||||||
|
return fmt.Errorf("maximum one resource after `%s` prefix", httpServerKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
//// http:foo is okay, but http:foo:bar is not
|
||||||
|
//p2 := httpServerKind + ":"
|
||||||
|
//s2 := strings.TrimPrefix(r.Kind(), p2)
|
||||||
|
//if len(s2) != len(r.Kind()) && strings.Count(s2, ":") > 0 { // has prefix
|
||||||
|
// return fmt.Errorf("maximum one resource after `%s` prefix", httpServerKind)
|
||||||
|
//}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readHandler handles all the incoming download requests from clients.
|
||||||
|
func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
|
||||||
|
// TODO: we could statically pre-compute some stuff here...
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Client: %s", req.RemoteAddr)
|
||||||
|
}
|
||||||
|
// TODO: would this leak anything security sensitive in our log?
|
||||||
|
obj.init.Logf("URL: %s", req.URL)
|
||||||
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Path: %s", requestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look through the autogrouped resources!
|
||||||
|
// TODO: can we improve performance by only searching here once?
|
||||||
|
for _, x := range obj.GetGroup() { // grouped elements
|
||||||
|
res, ok := x.(HTTPServerGroupableRes) // convert from Res
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Got grouped resource: %s", res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
err := res.AcceptHTTP(req)
|
||||||
|
if err == nil {
|
||||||
|
res.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Could not serve: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//continue // not me
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look in root if we have one, and we haven't got a file yet...
|
||||||
|
err := obj.AcceptHTTP(req)
|
||||||
|
if err == nil {
|
||||||
|
obj.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Could not serve Root: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We never found something to serve...
|
||||||
|
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
||||||
|
obj.init.Logf("File not found: %s", requestPath)
|
||||||
|
}
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
337
engine/resources/http_server_file.go
Normal file
337
engine/resources/http_server_file.go
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/util/safepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
httpServerFileKind = httpServerKind + ":file"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource(httpServerFileKind, func() engine.Res { return &HTTPServerFileRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPServerFileRes is a file that exists within an http server. The name is
|
||||||
|
// used as the public path of the file, unless the filename 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 resource, and in doing so makes the file
|
||||||
|
// associated with this resource available for serving from that http server.
|
||||||
|
type HTTPServerFileRes 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"`
|
||||||
|
|
||||||
|
// Filename is the name of the file this data should appear as on the
|
||||||
|
// http server.
|
||||||
|
Filename string `lang:"filename" yaml:"filename"`
|
||||||
|
|
||||||
|
// Path is the absolute path to a file that should be used as the source
|
||||||
|
// for this file resource. It must not be combined with the data field.
|
||||||
|
// If this corresponds to a directory, then it will used as a root dir
|
||||||
|
// that will be served as long as the resource name or Filename are also
|
||||||
|
// a directory ending with a slash.
|
||||||
|
Path string `lang:"path" yaml:"path"`
|
||||||
|
|
||||||
|
// Data is the file content that should be used as the source for this
|
||||||
|
// file resource. It must not be combined with the path field.
|
||||||
|
// TODO: should this be []byte instead?
|
||||||
|
Data string `lang:"data" yaml:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *HTTPServerFileRes) Default() engine.Res {
|
||||||
|
return &HTTPServerFileRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPath returns the actual path we respond to. When Filename is not
|
||||||
|
// specified, we use the Name. Note that this is the filename that will be seen
|
||||||
|
// on the http server, it is *not* the source path to the actual file contents
|
||||||
|
// being sent by the server.
|
||||||
|
func (obj *HTTPServerFileRes) getPath() string {
|
||||||
|
if obj.Filename != "" {
|
||||||
|
return obj.Filename
|
||||||
|
}
|
||||||
|
return obj.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContent returns the content that we expect from this resource. It depends
|
||||||
|
// on whether the user specified the Path or Data fields, and whether the Path
|
||||||
|
// exists or not.
|
||||||
|
func (obj *HTTPServerFileRes) getContent(requestPath safepath.AbsPath) (io.ReadSeeker, error) {
|
||||||
|
if obj.Path != "" && obj.Data != "" {
|
||||||
|
// programming error! this should have been caught in Validate!
|
||||||
|
return nil, fmt.Errorf("must not specify Path and Data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Data != "" {
|
||||||
|
return bytes.NewReader([]byte(obj.Data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
absFile, err := obj.getContentRelative(requestPath)
|
||||||
|
if err != nil { // on error, we just assume no root/prefix stuff happens
|
||||||
|
return os.Open(obj.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Open(absFile.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContentRelative takes a request, and returns the absolute path to the file
|
||||||
|
// that we want to request, if it's safely under what we can provide.
|
||||||
|
func (obj *HTTPServerFileRes) getContentRelative(requestPath safepath.AbsPath) (safepath.AbsFile, error) {
|
||||||
|
// the location on disk of the data
|
||||||
|
srcPath, err := safepath.SmartParseIntoPath(obj.Path) // (safepath.Path, error)
|
||||||
|
if err != nil {
|
||||||
|
return safepath.AbsFile{}, err
|
||||||
|
}
|
||||||
|
srcAbsDir, ok := srcPath.(safepath.AbsDir)
|
||||||
|
if !ok {
|
||||||
|
return safepath.AbsFile{}, fmt.Errorf("the Path is not an abs dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the public path we respond to (might be a dir prefix or just a file)
|
||||||
|
pubPath, err := safepath.SmartParseIntoPath(obj.getPath()) // (safepath.Path, error)
|
||||||
|
if err != nil {
|
||||||
|
return safepath.AbsFile{}, err
|
||||||
|
}
|
||||||
|
pubAbsDir, ok := pubPath.(safepath.AbsDir)
|
||||||
|
if !ok {
|
||||||
|
return safepath.AbsFile{}, fmt.Errorf("the name is not an abs dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the request underneath what we're providing?
|
||||||
|
if !safepath.HasPrefix(requestPath, pubAbsDir) {
|
||||||
|
return safepath.AbsFile{}, fmt.Errorf("wrong prefix")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the delta
|
||||||
|
delta, err := safepath.StripPrefix(requestPath, pubAbsDir) // (safepath.Path, error)
|
||||||
|
if err != nil {
|
||||||
|
return safepath.AbsFile{}, err
|
||||||
|
}
|
||||||
|
relFile, ok := delta.(safepath.RelFile)
|
||||||
|
if !ok {
|
||||||
|
return safepath.AbsFile{}, fmt.Errorf("the delta is not a rel file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return safepath.JoinToAbsFile(srcAbsDir, relFile), nil // AbsFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *HTTPServerFileRes) 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 *HTTPServerFileRes) AcceptHTTP(req *http.Request) error {
|
||||||
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
|
|
||||||
|
if strings.HasSuffix(obj.Path, "/") { // a dir!
|
||||||
|
if strings.HasPrefix(requestPath, obj.getPath()) {
|
||||||
|
// relative dir root
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestPath != obj.getPath() {
|
||||||
|
return fmt.Errorf("unhandled path")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP is the standard HTTP handler that will be used here.
|
||||||
|
func (obj *HTTPServerFileRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// We only allow GET at the moment.
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
|
|
||||||
|
absPath, err := safepath.ParseIntoAbsPath(requestPath)
|
||||||
|
if err != nil {
|
||||||
|
obj.init.Logf("invalid input path: %s", requestPath)
|
||||||
|
sendHTTPError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, err := obj.getContent(absPath)
|
||||||
|
if err != nil {
|
||||||
|
obj.init.Logf("could not get content for: %s", requestPath)
|
||||||
|
sendHTTPError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//if readSeekCloser, ok := handle.(io.ReadSeekCloser); ok { // same
|
||||||
|
// defer readSeekCloser.Close() // ignore error
|
||||||
|
//}
|
||||||
|
if closer, ok := handle.(io.Closer); ok {
|
||||||
|
defer closer.Close() // ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the last-modified time if we can.
|
||||||
|
modtime := time.Now()
|
||||||
|
if f, ok := handle.(*os.File); ok {
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err == nil {
|
||||||
|
modtime = fi.ModTime()
|
||||||
|
}
|
||||||
|
// TODO: if Stat errors, should we fail the whole thing?
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: is requestPath what we want for the name field?
|
||||||
|
http.ServeContent(w, req, requestPath, modtime, handle)
|
||||||
|
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
|
func (obj *HTTPServerFileRes) Validate() error {
|
||||||
|
if obj.getPath() == "" {
|
||||||
|
return fmt.Errorf("empty filename")
|
||||||
|
}
|
||||||
|
// FIXME: does getPath need to start with a slash?
|
||||||
|
|
||||||
|
if obj.Path != "" && !strings.HasPrefix(obj.Path, "/") {
|
||||||
|
return fmt.Errorf("the Path must be absolute")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Path != "" && obj.Data != "" {
|
||||||
|
return fmt.Errorf("must not specify Path and Data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: if obj.Path == "" && obj.Data == "" then we have an empty file!
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *HTTPServerFileRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *HTTPServerFileRes) 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 *HTTPServerFileRes) Watch(ctx context.Context) error {
|
||||||
|
obj.init.Running() // when started, notify engine that we're running
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done(): // closed by the engine to signal shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
//obj.init.Event() // notify engine of an event (this can block)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||||
|
func (obj *HTTPServerFileRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("CheckApply")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil // always succeeds, with nothing to do!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *HTTPServerFileRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare HTTPServerFileRes to others of the same resource kind
|
||||||
|
res, ok := r.(*HTTPServerFileRes)
|
||||||
|
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.Filename != res.Filename {
|
||||||
|
return fmt.Errorf("the Filename differs")
|
||||||
|
}
|
||||||
|
if obj.Path != res.Path {
|
||||||
|
return fmt.Errorf("the Path differs")
|
||||||
|
}
|
||||||
|
if obj.Data != res.Data {
|
||||||
|
return fmt.Errorf("the Data differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
|
// primarily useful for setting the defaults.
|
||||||
|
func (obj *HTTPServerFileRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes HTTPServerFileRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*HTTPServerFileRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to HTTPServerFileRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = HTTPServerFileRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -41,23 +41,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
httpFlagKind = httpKind + ":flag"
|
httpServerFlagKind = httpServerKind + ":flag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
engine.RegisterResource(httpFlagKind, func() engine.Res { return &HTTPFlagRes{} })
|
engine.RegisterResource(httpServerFlagKind, func() engine.Res { return &HTTPServerFlagRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPFlagRes is a special path that exists within an http server. The name is
|
// HTTPServerFlagRes is a special path that exists within an http server. The
|
||||||
// used as the public path of the flag, unless the path field is specified, and
|
// name is used as the public path of the flag, unless the path field is
|
||||||
// in that case it is used instead. The way this works is that it autogroups at
|
// specified, and in that case it is used instead. The way this works is that it
|
||||||
// runtime with an existing http resource, and in doing so makes the flag
|
// autogroups at runtime with an existing http resource, and in doing so makes
|
||||||
// associated with this resource available to cause actions when it receives a
|
// the flag associated with this resource available to cause actions when it
|
||||||
// request on that http server. If you create a flag which responds to the same
|
// receives a request on that http server. If you create a flag which responds
|
||||||
// type of request as an http:file resource or any other kind of resource, it is
|
// to the same type of request as an http:server:file resource or any other kind
|
||||||
// undefined behaviour which will answer the request. The most common clash will
|
// of resource, it is undefined behaviour which will answer the request. The
|
||||||
// happen if both are present at the same path.
|
// most common clash will happen if both are present at the same path.
|
||||||
type HTTPFlagRes struct {
|
type HTTPServerFlagRes struct {
|
||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
traits.Edgeable // XXX: add autoedge support
|
traits.Edgeable // XXX: add autoedge support
|
||||||
traits.Groupable // can be grouped into HTTPServerRes
|
traits.Groupable // can be grouped into HTTPServerRes
|
||||||
@@ -88,13 +88,13 @@ type HTTPFlagRes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
func (obj *HTTPFlagRes) Default() engine.Res {
|
func (obj *HTTPServerFlagRes) Default() engine.Res {
|
||||||
return &HTTPFlagRes{}
|
return &HTTPServerFlagRes{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPath returns the actual path we respond to. When Path is not specified, we
|
// getPath returns the actual path we respond to. When Path is not specified, we
|
||||||
// use the Name.
|
// use the Name.
|
||||||
func (obj *HTTPFlagRes) getPath() string {
|
func (obj *HTTPServerFlagRes) getPath() string {
|
||||||
if obj.Path != "" {
|
if obj.Path != "" {
|
||||||
return obj.Path
|
return obj.Path
|
||||||
}
|
}
|
||||||
@@ -104,13 +104,13 @@ func (obj *HTTPFlagRes) getPath() string {
|
|||||||
// ParentName is used to limit which resources autogroup into this one. If it's
|
// 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
|
// empty then it's ignored, otherwise it must match the Name of the parent to
|
||||||
// get grouped.
|
// get grouped.
|
||||||
func (obj *HTTPFlagRes) ParentName() string {
|
func (obj *HTTPServerFlagRes) ParentName() string {
|
||||||
return obj.Server
|
return obj.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
||||||
// accept, or any error to pass.
|
// accept, or any error to pass.
|
||||||
func (obj *HTTPFlagRes) AcceptHTTP(req *http.Request) error {
|
func (obj *HTTPServerFlagRes) AcceptHTTP(req *http.Request) error {
|
||||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
if requestPath != obj.getPath() {
|
if requestPath != obj.getPath() {
|
||||||
return fmt.Errorf("unhandled path")
|
return fmt.Errorf("unhandled path")
|
||||||
@@ -125,7 +125,7 @@ func (obj *HTTPFlagRes) AcceptHTTP(req *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP is the standard HTTP handler that will be used here.
|
// ServeHTTP is the standard HTTP handler that will be used here.
|
||||||
func (obj *HTTPFlagRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (obj *HTTPServerFlagRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
// We only allow POST at the moment.
|
// We only allow POST at the moment.
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
@@ -166,7 +166,7 @@ func (obj *HTTPFlagRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the resource data structure was populated correctly.
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
func (obj *HTTPFlagRes) Validate() error {
|
func (obj *HTTPServerFlagRes) Validate() error {
|
||||||
if obj.getPath() == "" {
|
if obj.getPath() == "" {
|
||||||
return fmt.Errorf("empty filename")
|
return fmt.Errorf("empty filename")
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ func (obj *HTTPFlagRes) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init runs some startup code for this resource.
|
// Init runs some startup code for this resource.
|
||||||
func (obj *HTTPFlagRes) Init(init *engine.Init) error {
|
func (obj *HTTPServerFlagRes) Init(init *engine.Init) error {
|
||||||
obj.init = init // save for later
|
obj.init = init // save for later
|
||||||
|
|
||||||
obj.mutex = &sync.Mutex{}
|
obj.mutex = &sync.Mutex{}
|
||||||
@@ -189,7 +189,7 @@ func (obj *HTTPFlagRes) Init(init *engine.Init) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup is run by the engine to clean up after the resource is done.
|
// Cleanup is run by the engine to clean up after the resource is done.
|
||||||
func (obj *HTTPFlagRes) Cleanup() error {
|
func (obj *HTTPServerFlagRes) Cleanup() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ func (obj *HTTPFlagRes) Cleanup() error {
|
|||||||
// particular one listens for events from incoming http requests to the flag,
|
// particular one listens for events from incoming http requests to the flag,
|
||||||
// and notifies the engine so that CheckApply can then run and return the
|
// and notifies the engine so that CheckApply can then run and return the
|
||||||
// correct value on send/recv.
|
// correct value on send/recv.
|
||||||
func (obj *HTTPFlagRes) Watch(ctx context.Context) error {
|
func (obj *HTTPServerFlagRes) Watch(ctx context.Context) error {
|
||||||
obj.init.Running() // when started, notify engine that we're running
|
obj.init.Running() // when started, notify engine that we're running
|
||||||
|
|
||||||
startupChan := make(chan struct{})
|
startupChan := make(chan struct{})
|
||||||
@@ -237,7 +237,7 @@ func (obj *HTTPFlagRes) Watch(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckApply never has anything to do for this resource, so it always succeeds.
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||||
func (obj *HTTPFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
func (obj *HTTPServerFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
||||||
obj.init.Logf("value: %+v", obj.value)
|
obj.init.Logf("value: %+v", obj.value)
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ func (obj *HTTPFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error
|
|||||||
// As a result, we need to run send/recv on the new graph after
|
// As a result, we need to run send/recv on the new graph after
|
||||||
// autogrouping, so that we compare apples to apples, when we do the
|
// autogrouping, so that we compare apples to apples, when we do the
|
||||||
// graphsync!
|
// graphsync!
|
||||||
if err := obj.init.Send(&HTTPFlagSends{
|
if err := obj.init.Send(&HTTPServerFlagSends{
|
||||||
Value: &value,
|
Value: &value,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -287,9 +287,9 @@ func (obj *HTTPFlagRes) CheckApply(ctx context.Context, apply bool) (bool, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *HTTPFlagRes) Cmp(r engine.Res) error {
|
func (obj *HTTPServerFlagRes) Cmp(r engine.Res) error {
|
||||||
// we can only compare HTTPFlagRes to others of the same resource kind
|
// we can only compare HTTPServerFlagRes to others of the same resource kind
|
||||||
res, ok := r.(*HTTPFlagRes)
|
res, ok := r.(*HTTPServerFlagRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("res is not the same kind")
|
return fmt.Errorf("res is not the same kind")
|
||||||
}
|
}
|
||||||
@@ -307,28 +307,29 @@ func (obj *HTTPFlagRes) Cmp(r engine.Res) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPFlagSends is the struct of data which is sent after a successful Apply.
|
// HTTPServerFlagSends is the struct of data which is sent after a successful
|
||||||
type HTTPFlagSends struct {
|
// Apply.
|
||||||
|
type HTTPServerFlagSends struct {
|
||||||
// Value is the received value being sent.
|
// Value is the received value being sent.
|
||||||
Value *string `lang:"value"`
|
Value *string `lang:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends represents the default struct of values we can send using Send/Recv.
|
// Sends represents the default struct of values we can send using Send/Recv.
|
||||||
func (obj *HTTPFlagRes) Sends() interface{} {
|
func (obj *HTTPServerFlagRes) Sends() interface{} {
|
||||||
return &HTTPFlagSends{
|
return &HTTPServerFlagSends{
|
||||||
Value: nil,
|
Value: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *HTTPFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *HTTPServerFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes HTTPFlagRes // indirection to avoid infinite recursion
|
type rawRes HTTPServerFlagRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
def := obj.Default() // get the default
|
def := obj.Default() // get the default
|
||||||
res, ok := def.(*HTTPFlagRes) // put in the right format
|
res, ok := def.(*HTTPServerFlagRes) // put in the right format
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("could not convert to HTTPFlagRes")
|
return fmt.Errorf("could not convert to HTTPServerFlagRes")
|
||||||
}
|
}
|
||||||
raw := rawRes(*res) // convert; the defaults go here
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
@@ -336,6 +337,6 @@ func (obj *HTTPFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
*obj = HTTPFlagRes(raw) // restore from indirection with type conversion!
|
*obj = HTTPServerFlagRes(raw) // restore from indirection with type conversion!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -49,43 +49,44 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
httpProxyKind = httpKind + ":proxy"
|
httpServerProxyKind = httpServerKind + ":proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// httpProxyRWMutex synchronizes against reads and writes to the cache.
|
// httpServerProxyRWMutex synchronizes against reads and writes to the cache.
|
||||||
// TODO: we could instead have a per-cache path individual mutex, but to
|
// TODO: we could instead have a per-cache path individual mutex, but to
|
||||||
// keep things simple for now, we just lumped them all together.
|
// keep things simple for now, we just lumped them all together.
|
||||||
httpProxyRWMutex *sync.RWMutex
|
httpServerProxyRWMutex *sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
httpProxyRWMutex = &sync.RWMutex{}
|
httpServerProxyRWMutex = &sync.RWMutex{}
|
||||||
|
|
||||||
engine.RegisterResource(httpProxyKind, func() engine.Res { return &HTTPProxyRes{} })
|
engine.RegisterResource(httpServerProxyKind, func() engine.Res { return &HTTPServerProxyRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPProxyRes is a resource representing a special path that exists within an
|
// HTTPServerProxyRes is a resource representing a special path that exists
|
||||||
// http server. The name is used as the public path of the endpoint, unless the
|
// within an http server. The name is used as the public path of the endpoint,
|
||||||
// path field is specified, and in that case it is used instead. The way this
|
// unless the path field is specified, and in that case it is used instead. The
|
||||||
// works is that it autogroups at runtime with an existing http resource, and in
|
// way this works is that it autogroups at runtime with an existing http server
|
||||||
// doing so makes the path associated with this resource available when serving
|
// resource, and in doing so makes the path associated with this resource
|
||||||
// files. When something under the path is accessed, this is pulled from the
|
// available when serving files. When something under the path is accessed, this
|
||||||
// backing http server, which makes an http client connection if needed to pull
|
// is pulled from the backing http server, which makes an http client connection
|
||||||
// the authoritative file down, saves it locally for future use, and then
|
// if needed to pull the authoritative file down, saves it locally for future
|
||||||
// returns it to the original http client caller. On a subsequent call, if the
|
// use, and then returns it to the original http client caller. On a subsequent
|
||||||
// cache was not invalidated, the file doesn't need to be fetched from the
|
// call, if the cache was not invalidated, the file doesn't need to be fetched
|
||||||
// network. In effect, this works as a caching http proxy. If you create this as
|
// from the network. In effect, this works as a caching http proxy. If you
|
||||||
// a resource which responds to the same type of request as an http:file
|
// create this as a resource which responds to the same type of request as an
|
||||||
// resource or any other kind of resource, it is undefined behaviour which will
|
// http:server:file resource or any other kind of resource, it is undefined
|
||||||
// answer the request. The most common clash will happen if both are present at
|
// behaviour which will answer the request. The most common clash will happen if
|
||||||
// the same path. This particular implementation stores some file data in memory
|
// both are present at the same path. This particular implementation stores some
|
||||||
// as a convenience instead of streaming directly to clients. This makes locking
|
// file data in memory as a convenience instead of streaming directly to
|
||||||
// much easier, but is wasteful. If you plan on using this for huge files and on
|
// clients. This makes locking much easier, but is wasteful. If you plan on
|
||||||
// systems with low amounts of memory, you might want to optimize this. The
|
// using this for huge files and on systems with low amounts of memory, you
|
||||||
// resultant proxy path is determined by subtracting the `Sub` field from the
|
// might want to optimize this. The resultant proxy path is determined by
|
||||||
// `Path` (and request path) and then appending the result to the `Head` field.
|
// subtracting the `Sub` field from the `Path` (and request path) and then
|
||||||
type HTTPProxyRes struct {
|
// appending the result to the `Head` field.
|
||||||
|
type HTTPServerProxyRes struct {
|
||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
traits.Edgeable // XXX: add autoedge support
|
traits.Edgeable // XXX: add autoedge support
|
||||||
traits.Groupable // can be grouped into HTTPServerRes
|
traits.Groupable // can be grouped into HTTPServerRes
|
||||||
@@ -136,13 +137,13 @@ type HTTPProxyRes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
func (obj *HTTPProxyRes) Default() engine.Res {
|
func (obj *HTTPServerProxyRes) Default() engine.Res {
|
||||||
return &HTTPProxyRes{}
|
return &HTTPServerProxyRes{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPath returns the actual path we respond to. When Path is not specified, we
|
// getPath returns the actual path we respond to. When Path is not specified, we
|
||||||
// use the Name.
|
// use the Name.
|
||||||
func (obj *HTTPProxyRes) getPath() string {
|
func (obj *HTTPServerProxyRes) getPath() string {
|
||||||
if obj.Path != "" {
|
if obj.Path != "" {
|
||||||
return obj.Path
|
return obj.Path
|
||||||
}
|
}
|
||||||
@@ -151,7 +152,7 @@ func (obj *HTTPProxyRes) getPath() string {
|
|||||||
|
|
||||||
// serveHTTP is the real implementation of ServeHTTP, but with a more ergonomic
|
// serveHTTP is the real implementation of ServeHTTP, but with a more ergonomic
|
||||||
// signature.
|
// signature.
|
||||||
func (obj *HTTPProxyRes) serveHTTP(ctx context.Context, requestPath string) (handlerFuncError, error) {
|
func (obj *HTTPServerProxyRes) serveHTTP(ctx context.Context, requestPath string) (handlerFuncError, error) {
|
||||||
// TODO: switch requestPath to use safepath.AbsPath instead of a string
|
// TODO: switch requestPath to use safepath.AbsPath instead of a string
|
||||||
|
|
||||||
result, err := obj.pathParser.parse(requestPath)
|
result, err := obj.pathParser.parse(requestPath)
|
||||||
@@ -237,8 +238,8 @@ func (obj *HTTPProxyRes) serveHTTP(ctx context.Context, requestPath string) (han
|
|||||||
writers := []io.Writer{w} // out to the client
|
writers := []io.Writer{w} // out to the client
|
||||||
|
|
||||||
if obj.Cache != "" { // check in the cache...
|
if obj.Cache != "" { // check in the cache...
|
||||||
httpProxyRWMutex.Lock()
|
httpServerProxyRWMutex.Lock()
|
||||||
defer httpProxyRWMutex.Unlock()
|
defer httpServerProxyRWMutex.Unlock()
|
||||||
|
|
||||||
// store in cachePath
|
// store in cachePath
|
||||||
if err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(cachePath), 0700); err != nil {
|
||||||
@@ -323,11 +324,11 @@ func (obj *HTTPProxyRes) serveHTTP(ctx context.Context, requestPath string) (han
|
|||||||
|
|
||||||
// getCachedFile pulls a file from our local cache if it exists. It returns the
|
// getCachedFile pulls a file from our local cache if it exists. It returns the
|
||||||
// correct http handler on success, which we can then run.
|
// correct http handler on success, which we can then run.
|
||||||
func (obj *HTTPProxyRes) getCachedFile(ctx context.Context, absPath string) (handlerFuncError, error) {
|
func (obj *HTTPServerProxyRes) getCachedFile(ctx context.Context, absPath string) (handlerFuncError, error) {
|
||||||
// TODO: if infinite reads keep coming in, do we indefinitely-postpone
|
// TODO: if infinite reads keep coming in, do we indefinitely-postpone
|
||||||
// the locking so that a new file can be saved in the cache?
|
// the locking so that a new file can be saved in the cache?
|
||||||
httpProxyRWMutex.RLock()
|
httpServerProxyRWMutex.RLock()
|
||||||
defer httpProxyRWMutex.RUnlock()
|
defer httpServerProxyRWMutex.RUnlock()
|
||||||
|
|
||||||
f, err := os.Open(absPath)
|
f, err := os.Open(absPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -361,13 +362,13 @@ func (obj *HTTPProxyRes) getCachedFile(ctx context.Context, absPath string) (han
|
|||||||
// ParentName is used to limit which resources autogroup into this one. If it's
|
// 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
|
// empty then it's ignored, otherwise it must match the Name of the parent to
|
||||||
// get grouped.
|
// get grouped.
|
||||||
func (obj *HTTPProxyRes) ParentName() string {
|
func (obj *HTTPServerProxyRes) ParentName() string {
|
||||||
return obj.Server
|
return obj.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
||||||
// accept, or any error to pass.
|
// accept, or any error to pass.
|
||||||
func (obj *HTTPProxyRes) AcceptHTTP(req *http.Request) error {
|
func (obj *HTTPServerProxyRes) AcceptHTTP(req *http.Request) error {
|
||||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
|
|
||||||
if p := obj.getPath(); strings.HasSuffix(p, "/") { // a dir!
|
if p := obj.getPath(); strings.HasSuffix(p, "/") { // a dir!
|
||||||
@@ -384,7 +385,7 @@ func (obj *HTTPProxyRes) AcceptHTTP(req *http.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP is the standard HTTP handler that will be used here.
|
// ServeHTTP is the standard HTTP handler that will be used here.
|
||||||
func (obj *HTTPProxyRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (obj *HTTPServerProxyRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
// We only allow GET at the moment.
|
// We only allow GET at the moment.
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
@@ -419,7 +420,7 @@ func (obj *HTTPProxyRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the resource data structure was populated correctly.
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
func (obj *HTTPProxyRes) Validate() error {
|
func (obj *HTTPServerProxyRes) Validate() error {
|
||||||
if obj.getPath() == "" {
|
if obj.getPath() == "" {
|
||||||
return fmt.Errorf("empty filename")
|
return fmt.Errorf("empty filename")
|
||||||
}
|
}
|
||||||
@@ -449,7 +450,7 @@ func (obj *HTTPProxyRes) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init runs some startup code for this resource.
|
// Init runs some startup code for this resource.
|
||||||
func (obj *HTTPProxyRes) Init(init *engine.Init) error {
|
func (obj *HTTPServerProxyRes) Init(init *engine.Init) error {
|
||||||
obj.init = init // save for later
|
obj.init = init // save for later
|
||||||
|
|
||||||
obj.pathParser = &pathParser{
|
obj.pathParser = &pathParser{
|
||||||
@@ -463,14 +464,14 @@ func (obj *HTTPProxyRes) Init(init *engine.Init) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup is run by the engine to clean up after the resource is done.
|
// Cleanup is run by the engine to clean up after the resource is done.
|
||||||
func (obj *HTTPProxyRes) Cleanup() error {
|
func (obj *HTTPServerProxyRes) Cleanup() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch is the primary listener for this resource and it outputs events. This
|
// 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
|
// particular one does absolutely nothing but block until we've received a done
|
||||||
// signal.
|
// signal.
|
||||||
func (obj *HTTPProxyRes) Watch(ctx context.Context) error {
|
func (obj *HTTPServerProxyRes) Watch(ctx context.Context) error {
|
||||||
obj.init.Running() // when started, notify engine that we're running
|
obj.init.Running() // when started, notify engine that we're running
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -483,7 +484,7 @@ func (obj *HTTPProxyRes) Watch(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckApply never has anything to do for this resource, so it always succeeds.
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||||
func (obj *HTTPProxyRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
func (obj *HTTPServerProxyRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
if obj.init.Debug {
|
if obj.init.Debug {
|
||||||
obj.init.Logf("CheckApply")
|
obj.init.Logf("CheckApply")
|
||||||
}
|
}
|
||||||
@@ -492,9 +493,9 @@ func (obj *HTTPProxyRes) CheckApply(ctx context.Context, apply bool) (bool, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *HTTPProxyRes) Cmp(r engine.Res) error {
|
func (obj *HTTPServerProxyRes) Cmp(r engine.Res) error {
|
||||||
// we can only compare HTTPProxyRes to others of the same resource kind
|
// we can only compare HTTPServerProxyRes to others of the same resource kind
|
||||||
res, ok := r.(*HTTPProxyRes)
|
res, ok := r.(*HTTPServerProxyRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("res is not the same kind")
|
return fmt.Errorf("res is not the same kind")
|
||||||
}
|
}
|
||||||
@@ -521,13 +522,13 @@ func (obj *HTTPProxyRes) Cmp(r engine.Res) error {
|
|||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *HTTPServerProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes HTTPProxyRes // indirection to avoid infinite recursion
|
type rawRes HTTPServerProxyRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
def := obj.Default() // get the default
|
def := obj.Default() // get the default
|
||||||
res, ok := def.(*HTTPProxyRes) // put in the right format
|
res, ok := def.(*HTTPServerProxyRes) // put in the right format
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("could not convert to HTTPProxyRes")
|
return fmt.Errorf("could not convert to HTTPServerProxyRes")
|
||||||
}
|
}
|
||||||
raw := rawRes(*res) // convert; the defaults go here
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
@@ -535,7 +536,7 @@ func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
*obj = HTTPProxyRes(raw) // restore from indirection with type conversion!
|
*obj = HTTPServerProxyRes(raw) // restore from indirection with type conversion!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHttpProxyPathParser0(t *testing.T) {
|
func TestHttpServerProxyPathParser0(t *testing.T) {
|
||||||
|
|
||||||
type test struct { // an individual test
|
type test struct { // an individual test
|
||||||
fail bool
|
fail bool
|
||||||
@@ -40,8 +40,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/engine"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
"github.com/purpleidea/mgmt/engine/resources/http_ui/common"
|
"github.com/purpleidea/mgmt/engine/resources/http_server_ui/common"
|
||||||
"github.com/purpleidea/mgmt/engine/resources/http_ui/static"
|
"github.com/purpleidea/mgmt/engine/resources/http_server_ui/static"
|
||||||
"github.com/purpleidea/mgmt/engine/traits"
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
"github.com/purpleidea/mgmt/pgraph"
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
@@ -50,30 +50,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
httpUIKind = httpKind + ":ui"
|
httpServerUIKind = httpServerKind + ":ui"
|
||||||
|
|
||||||
httpUIIndexHTMLTmpl = "index.html.tmpl"
|
httpServerUIIndexHTMLTmpl = "index.html.tmpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed http_ui/index.html.tmpl
|
//go:embed http_server_ui/index.html.tmpl
|
||||||
httpUIIndexHTMLTmplData string
|
httpServerUIIndexHTMLTmplData string
|
||||||
|
|
||||||
//go:embed http_ui/wasm_exec.js
|
//go:embed http_server_ui/wasm_exec.js
|
||||||
httpUIWasmExecData []byte
|
httpServerUIWasmExecData []byte
|
||||||
|
|
||||||
//go:embed http_ui/main.wasm
|
//go:embed http_server_ui/main.wasm
|
||||||
httpUIMainWasmData []byte
|
httpServerUIMainWasmData []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
engine.RegisterResource(httpUIKind, func() engine.Res { return &HTTPUIRes{} })
|
engine.RegisterResource(httpServerUIKind, func() engine.Res { return &HTTPServerUIRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPServerUIGroupableRes is the interface that you must implement if you want
|
// 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
|
// 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
|
// resource. As an added safety, the Kind must also begin with
|
||||||
// not have more than one colon to avoid accidents of unwanted grouping.
|
// "http:server:ui:", and not have more than one colon to avoid accidents of
|
||||||
|
// unwanted grouping.
|
||||||
type HTTPServerUIGroupableRes interface {
|
type HTTPServerUIGroupableRes interface {
|
||||||
engine.Res
|
engine.Res
|
||||||
|
|
||||||
@@ -109,8 +110,9 @@ type HTTPServerUIGroupableRes interface {
|
|||||||
GetSort() string
|
GetSort() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPUIResData represents some additional data to attach to the resource.
|
// HTTPServerUIResData represents some additional data to attach to the
|
||||||
type HTTPUIResData struct {
|
// resource.
|
||||||
|
type HTTPServerUIResData struct {
|
||||||
// Title is the generated page title that is displayed to the user.
|
// Title is the generated page title that is displayed to the user.
|
||||||
Title string `lang:"title" yaml:"title"`
|
Title string `lang:"title" yaml:"title"`
|
||||||
|
|
||||||
@@ -121,12 +123,13 @@ type HTTPUIResData struct {
|
|||||||
Head string `lang:"head" yaml:"head"`
|
Head string `lang:"head" yaml:"head"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPUIRes is a web UI resource that exists within an http server. The name is
|
// HTTPServerUIRes is a web UI resource that exists within an http server. The
|
||||||
// used as the public path of the ui, unless the path field is specified, and in
|
// name is used as the public path of the ui, unless the path field is
|
||||||
// that case it is used instead. The way this works is that it autogroups at
|
// specified, and in that case it is used instead. The way this works is that it
|
||||||
// runtime with an existing http server resource, and in doing so makes the form
|
// autogroups at runtime with an existing http server resource, and in doing so
|
||||||
// associated with this resource available for serving from that http server.
|
// makes the form associated with this resource available for serving from that
|
||||||
type HTTPUIRes struct {
|
// http server.
|
||||||
|
type HTTPServerUIRes struct {
|
||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
traits.Edgeable // XXX: add autoedge support
|
traits.Edgeable // XXX: add autoedge support
|
||||||
traits.Groupable // can be grouped into HTTPServerRes
|
traits.Groupable // can be grouped into HTTPServerRes
|
||||||
@@ -146,7 +149,7 @@ type HTTPUIRes struct {
|
|||||||
Path string `lang:"path" yaml:"path"`
|
Path string `lang:"path" yaml:"path"`
|
||||||
|
|
||||||
// Data represents some additional data to attach to the resource.
|
// Data represents some additional data to attach to the resource.
|
||||||
Data *HTTPUIResData `lang:"data" yaml:"data"`
|
Data *HTTPServerUIResData `lang:"data" yaml:"data"`
|
||||||
|
|
||||||
//eventStream chan error
|
//eventStream chan error
|
||||||
eventsChanMap map[engine.Res]chan error
|
eventsChanMap map[engine.Res]chan error
|
||||||
@@ -162,15 +165,15 @@ type HTTPUIRes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
func (obj *HTTPUIRes) Default() engine.Res {
|
func (obj *HTTPServerUIRes) Default() engine.Res {
|
||||||
return &HTTPUIRes{}
|
return &HTTPServerUIRes{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPath returns the actual path we respond to. When Path is not specified, we
|
// 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
|
// 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
|
// root http server, and this ui application might use a querystring and/or POST
|
||||||
// data as well.
|
// data as well.
|
||||||
func (obj *HTTPUIRes) getPath() string {
|
func (obj *HTTPServerUIRes) getPath() string {
|
||||||
if obj.Path != "" {
|
if obj.Path != "" {
|
||||||
return obj.Path
|
return obj.Path
|
||||||
}
|
}
|
||||||
@@ -179,7 +182,7 @@ func (obj *HTTPUIRes) getPath() string {
|
|||||||
|
|
||||||
// routerPath returns an appropriate path for our router based on what we want
|
// routerPath returns an appropriate path for our router based on what we want
|
||||||
// to achieve using our parent prefix.
|
// to achieve using our parent prefix.
|
||||||
func (obj *HTTPUIRes) routerPath(p string) string {
|
func (obj *HTTPServerUIRes) routerPath(p string) string {
|
||||||
if strings.HasPrefix(p, "/") {
|
if strings.HasPrefix(p, "/") {
|
||||||
return obj.getPath() + p[1:]
|
return obj.getPath() + p[1:]
|
||||||
}
|
}
|
||||||
@@ -190,13 +193,13 @@ func (obj *HTTPUIRes) routerPath(p string) string {
|
|||||||
// ParentName is used to limit which resources autogroup into this one. If it's
|
// 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
|
// empty then it's ignored, otherwise it must match the Name of the parent to
|
||||||
// get grouped.
|
// get grouped.
|
||||||
func (obj *HTTPUIRes) ParentName() string {
|
func (obj *HTTPServerUIRes) ParentName() string {
|
||||||
return obj.Server
|
return obj.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
||||||
// accept, or any error to pass.
|
// accept, or any error to pass.
|
||||||
func (obj *HTTPUIRes) AcceptHTTP(req *http.Request) error {
|
func (obj *HTTPServerUIRes) AcceptHTTP(req *http.Request) error {
|
||||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
//if requestPath != obj.getPath() {
|
//if requestPath != obj.getPath() {
|
||||||
// return fmt.Errorf("unhandled path")
|
// return fmt.Errorf("unhandled path")
|
||||||
@@ -209,7 +212,7 @@ func (obj *HTTPUIRes) AcceptHTTP(req *http.Request) error {
|
|||||||
|
|
||||||
// getResByID returns the grouped resource with the id we're searching for if it
|
// getResByID returns the grouped resource with the id we're searching for if it
|
||||||
// exists, otherwise nil and false.
|
// exists, otherwise nil and false.
|
||||||
func (obj *HTTPUIRes) getResByID(id string) (HTTPServerUIGroupableRes, bool) {
|
func (obj *HTTPServerUIRes) getResByID(id string) (HTTPServerUIGroupableRes, bool) {
|
||||||
for _, x := range obj.GetGroup() { // grouped elements
|
for _, x := range obj.GetGroup() { // grouped elements
|
||||||
res, ok := x.(HTTPServerUIGroupableRes) // convert from Res
|
res, ok := x.(HTTPServerUIGroupableRes) // convert from Res
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -227,7 +230,7 @@ func (obj *HTTPUIRes) getResByID(id string) (HTTPServerUIGroupableRes, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ginLogger is a helper to get structured logs out of gin.
|
// ginLogger is a helper to get structured logs out of gin.
|
||||||
func (obj *HTTPUIRes) ginLogger() gin.HandlerFunc {
|
func (obj *HTTPServerUIRes) ginLogger() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
//start := time.Now()
|
//start := time.Now()
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -248,11 +251,11 @@ func (obj *HTTPUIRes) ginLogger() gin.HandlerFunc {
|
|||||||
|
|
||||||
// getTemplate builds the super template that contains the map of each file name
|
// 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.
|
// so that it can be used easily to send out named, templated documents.
|
||||||
func (obj *HTTPUIRes) getTemplate() (*template.Template, error) {
|
func (obj *HTTPServerUIRes) getTemplate() (*template.Template, error) {
|
||||||
// XXX: get this from somewhere
|
// XXX: get this from somewhere
|
||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
//m["foo.tmpl"] = "hello from file1" // TODO: add more content?
|
//m["foo.tmpl"] = "hello from file1" // TODO: add more content?
|
||||||
m[httpUIIndexHTMLTmpl] = httpUIIndexHTMLTmplData // index.html.tmpl
|
m[httpServerUIIndexHTMLTmpl] = httpServerUIIndexHTMLTmplData // index.html.tmpl
|
||||||
|
|
||||||
filenames := []string{}
|
filenames := []string{}
|
||||||
for filename := range m {
|
for filename := range m {
|
||||||
@@ -283,7 +286,7 @@ func (obj *HTTPUIRes) getTemplate() (*template.Template, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP is the standard HTTP handler that will be used here.
|
// ServeHTTP is the standard HTTP handler that will be used here.
|
||||||
func (obj *HTTPUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (obj *HTTPServerUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
// XXX: do all the router bits in Init() if we can...
|
// XXX: do all the router bits in Init() if we can...
|
||||||
gin.SetMode(gin.ReleaseMode) // for production
|
gin.SetMode(gin.ReleaseMode) // for production
|
||||||
@@ -306,32 +309,32 @@ func (obj *HTTPUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
h["program"] = obj.init.Program
|
h["program"] = obj.init.Program
|
||||||
h["version"] = obj.init.Version
|
h["version"] = obj.init.Version
|
||||||
h["hostname"] = obj.init.Hostname
|
h["hostname"] = obj.init.Hostname
|
||||||
h["embedded"] = static.HTTPUIStaticEmbedded // true or false
|
h["embedded"] = static.HTTPServerUIStaticEmbedded // true or false
|
||||||
h["title"] = "" // key must be specified
|
h["title"] = "" // key must be specified
|
||||||
h["path"] = obj.getPath()
|
h["path"] = obj.getPath()
|
||||||
if obj.Data != nil {
|
if obj.Data != nil {
|
||||||
h["title"] = obj.Data.Title // template var
|
h["title"] = obj.Data.Title // template var
|
||||||
h["head"] = template.HTML(obj.Data.Head)
|
h["head"] = template.HTML(obj.Data.Head)
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, httpUIIndexHTMLTmpl, h)
|
c.HTML(http.StatusOK, httpServerUIIndexHTMLTmpl, h)
|
||||||
})
|
})
|
||||||
router.GET(obj.routerPath("/main.wasm"), func(c *gin.Context) {
|
router.GET(obj.routerPath("/main.wasm"), func(c *gin.Context) {
|
||||||
c.Data(http.StatusOK, "application/wasm", httpUIMainWasmData)
|
c.Data(http.StatusOK, "application/wasm", httpServerUIMainWasmData)
|
||||||
})
|
})
|
||||||
router.GET(obj.routerPath("/wasm_exec.js"), func(c *gin.Context) {
|
router.GET(obj.routerPath("/wasm_exec.js"), func(c *gin.Context) {
|
||||||
// the version of this file has to match compiler version
|
// the version of this file has to match compiler version
|
||||||
// the original came from: ~golang/lib/wasm/wasm_exec.js
|
// the original came from: ~golang/lib/wasm/wasm_exec.js
|
||||||
// XXX: add a test to ensure this matches the compiler version
|
// XXX: add a test to ensure this matches the compiler version
|
||||||
// the content-type matters or this won't work in the browser
|
// the content-type matters or this won't work in the browser
|
||||||
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", httpUIWasmExecData)
|
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", httpServerUIWasmExecData)
|
||||||
})
|
})
|
||||||
|
|
||||||
if static.HTTPUIStaticEmbedded {
|
if static.HTTPServerUIStaticEmbedded {
|
||||||
router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapCSS), func(c *gin.Context) {
|
router.GET(obj.routerPath("/"+static.HTTPServerUIIndexBootstrapCSS), func(c *gin.Context) {
|
||||||
c.Data(http.StatusOK, "text/css;charset=UTF-8", static.HTTPUIIndexStaticBootstrapCSS)
|
c.Data(http.StatusOK, "text/css;charset=UTF-8", static.HTTPServerUIIndexStaticBootstrapCSS)
|
||||||
})
|
})
|
||||||
router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapJS), func(c *gin.Context) {
|
router.GET(obj.routerPath("/"+static.HTTPServerUIIndexBootstrapJS), func(c *gin.Context) {
|
||||||
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", static.HTTPUIIndexStaticBootstrapJS)
|
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", static.HTTPServerUIIndexStaticBootstrapJS)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +495,7 @@ func (obj *HTTPUIRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the resource data structure was populated correctly.
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
func (obj *HTTPUIRes) Validate() error {
|
func (obj *HTTPServerUIRes) Validate() error {
|
||||||
if obj.getPath() == "" {
|
if obj.getPath() == "" {
|
||||||
return fmt.Errorf("empty path")
|
return fmt.Errorf("empty path")
|
||||||
}
|
}
|
||||||
@@ -510,7 +513,7 @@ func (obj *HTTPUIRes) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init runs some startup code for this resource.
|
// Init runs some startup code for this resource.
|
||||||
func (obj *HTTPUIRes) Init(init *engine.Init) error {
|
func (obj *HTTPServerUIRes) Init(init *engine.Init) error {
|
||||||
obj.init = init // save for later
|
obj.init = init // save for later
|
||||||
|
|
||||||
//obj.eventStream = make(chan error)
|
//obj.eventStream = make(chan error)
|
||||||
@@ -572,7 +575,7 @@ func (obj *HTTPUIRes) Init(init *engine.Init) error {
|
|||||||
Recv: engine.GenerateRecvFunc(r), // unused
|
Recv: engine.GenerateRecvFunc(r), // unused
|
||||||
|
|
||||||
FilteredGraph: func() (*pgraph.Graph, error) {
|
FilteredGraph: func() (*pgraph.Graph, error) {
|
||||||
panic("FilteredGraph for HTTP:UI not implemented")
|
panic("FilteredGraph for HTTP:Server:UI not implemented")
|
||||||
},
|
},
|
||||||
|
|
||||||
Local: obj.init.Local,
|
Local: obj.init.Local,
|
||||||
@@ -594,14 +597,14 @@ func (obj *HTTPUIRes) Init(init *engine.Init) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup is run by the engine to clean up after the resource is done.
|
// Cleanup is run by the engine to clean up after the resource is done.
|
||||||
func (obj *HTTPUIRes) Cleanup() error {
|
func (obj *HTTPServerUIRes) Cleanup() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch is the primary listener for this resource and it outputs events. This
|
// 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
|
// particular one does absolutely nothing but block until we've received a done
|
||||||
// signal.
|
// signal.
|
||||||
func (obj *HTTPUIRes) Watch(ctx context.Context) error {
|
func (obj *HTTPServerUIRes) Watch(ctx context.Context) error {
|
||||||
|
|
||||||
multiplexedChan := make(chan error)
|
multiplexedChan := make(chan error)
|
||||||
defer close(multiplexedChan) // closes after everyone below us is finished
|
defer close(multiplexedChan) // closes after everyone below us is finished
|
||||||
@@ -708,7 +711,7 @@ func (obj *HTTPUIRes) Watch(ctx context.Context) error {
|
|||||||
|
|
||||||
// CheckApply is responsible for the Send/Recv aspects of the autogrouped
|
// CheckApply is responsible for the Send/Recv aspects of the autogrouped
|
||||||
// resources. It recursively calls any autogrouped children.
|
// resources. It recursively calls any autogrouped children.
|
||||||
func (obj *HTTPUIRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
func (obj *HTTPServerUIRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
if obj.init.Debug {
|
if obj.init.Debug {
|
||||||
obj.init.Logf("CheckApply")
|
obj.init.Logf("CheckApply")
|
||||||
}
|
}
|
||||||
@@ -726,9 +729,9 @@ func (obj *HTTPUIRes) CheckApply(ctx context.Context, apply bool) (bool, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *HTTPUIRes) Cmp(r engine.Res) error {
|
func (obj *HTTPServerUIRes) Cmp(r engine.Res) error {
|
||||||
// we can only compare HTTPUIRes to others of the same resource kind
|
// we can only compare HTTPServerUIRes to others of the same resource kind
|
||||||
res, ok := r.(*HTTPUIRes)
|
res, ok := r.(*HTTPServerUIRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("res is not the same kind")
|
return fmt.Errorf("res is not the same kind")
|
||||||
}
|
}
|
||||||
@@ -745,13 +748,13 @@ func (obj *HTTPUIRes) Cmp(r engine.Res) error {
|
|||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *HTTPUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *HTTPServerUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes HTTPUIRes // indirection to avoid infinite recursion
|
type rawRes HTTPServerUIRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
def := obj.Default() // get the default
|
def := obj.Default() // get the default
|
||||||
res, ok := def.(*HTTPUIRes) // put in the right format
|
res, ok := def.(*HTTPServerUIRes) // put in the right format
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("could not convert to HTTPUIRes")
|
return fmt.Errorf("could not convert to HTTPServerUIRes")
|
||||||
}
|
}
|
||||||
raw := rawRes(*res) // convert; the defaults go here
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
@@ -759,14 +762,14 @@ func (obj *HTTPUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
*obj = HTTPUIRes(raw) // restore from indirection with type conversion!
|
*obj = HTTPServerUIRes(raw) // restore from indirection with type conversion!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupCmp returns whether two resources can be grouped together or not. Can
|
// 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
|
// these two resources be merged, aka, does this resource support doing so? Will
|
||||||
// resource allow itself to be grouped _into_ this obj?
|
// resource allow itself to be grouped _into_ this obj?
|
||||||
func (obj *HTTPUIRes) GroupCmp(r engine.GroupableRes) error {
|
func (obj *HTTPServerUIRes) GroupCmp(r engine.GroupableRes) error {
|
||||||
res, ok := r.(HTTPServerUIGroupableRes) // different from what we usually do!
|
res, ok := r.(HTTPServerUIGroupableRes) // different from what we usually do!
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("resource is not the right kind")
|
return fmt.Errorf("resource is not the right kind")
|
||||||
@@ -778,17 +781,17 @@ func (obj *HTTPUIRes) GroupCmp(r engine.GroupableRes) error {
|
|||||||
return fmt.Errorf("resource groups with a different parent name")
|
return fmt.Errorf("resource groups with a different parent name")
|
||||||
}
|
}
|
||||||
|
|
||||||
p := httpUIKind + ":"
|
p := httpServerUIKind + ":"
|
||||||
|
|
||||||
// http:ui:foo is okay, but http:file is not
|
// http:server:ui:foo is okay, but http:server:file is not
|
||||||
if !strings.HasPrefix(r.Kind(), p) {
|
if !strings.HasPrefix(r.Kind(), p) {
|
||||||
return fmt.Errorf("not one of our children")
|
return fmt.Errorf("not one of our children")
|
||||||
}
|
}
|
||||||
|
|
||||||
// http:ui:foo is okay, but http:ui:foo:bar is not
|
// http:server:ui:foo is okay, but http:server:ui:foo:bar is not
|
||||||
s := strings.TrimPrefix(r.Kind(), p)
|
s := strings.TrimPrefix(r.Kind(), p)
|
||||||
if len(s) != len(r.Kind()) && strings.Count(s, ":") > 0 { // has prefix
|
if len(s) != len(r.Kind()) && strings.Count(s, ":") > 0 { // has prefix
|
||||||
return fmt.Errorf("maximum one resource after `%s` prefix", httpUIKind)
|
return fmt.Errorf("maximum one resource after `%s` prefix", httpServerUIKind)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
This directory contains the golang wasm source for the `http_ui` resource. It
|
This directory contains the golang wasm source for the `http_server_ui`
|
||||||
gets built automatically when you run `make` from the main project root
|
resource. It gets built automatically when you run `make` from the main project
|
||||||
directory.
|
root directory.
|
||||||
|
|
||||||
After it gets built, the compiled artifact gets bundled into the main project
|
After it gets built, the compiled artifact gets bundled into the main project
|
||||||
binary via go embed.
|
binary via go embed.
|
||||||
@@ -28,28 +28,30 @@
|
|||||||
// additional permission.
|
// additional permission.
|
||||||
|
|
||||||
// Package common contains some code that is shared between the wasm and the
|
// Package common contains some code that is shared between the wasm and the
|
||||||
// http:ui packages.
|
// http:server:ui packages.
|
||||||
package common
|
package common
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HTTPUIInputType represents the field in the "Type" map that specifies
|
// HTTPServerUIInputType represents the field in the "Type" map that specifies
|
||||||
// which input type we're using.
|
// which input type we're using.
|
||||||
HTTPUIInputType = "type"
|
HTTPServerUIInputType = "type"
|
||||||
|
|
||||||
// HTTPUIInputTypeText is the representation of the html "text" type.
|
// HTTPServerUIInputTypeText is the representation of the html "text"
|
||||||
HTTPUIInputTypeText = "text"
|
// type.
|
||||||
|
HTTPServerUIInputTypeText = "text"
|
||||||
|
|
||||||
// HTTPUIInputTypeRange is the representation of the html "range" type.
|
// HTTPServerUIInputTypeRange is the representation of the html "range"
|
||||||
HTTPUIInputTypeRange = "range"
|
// type.
|
||||||
|
HTTPServerUIInputTypeRange = "range"
|
||||||
|
|
||||||
// HTTPUIInputTypeRangeMin is the html input "range" min field.
|
// HTTPServerUIInputTypeRangeMin is the html input "range" min field.
|
||||||
HTTPUIInputTypeRangeMin = "min"
|
HTTPServerUIInputTypeRangeMin = "min"
|
||||||
|
|
||||||
// HTTPUIInputTypeRangeMax is the html input "range" max field.
|
// HTTPServerUIInputTypeRangeMax is the html input "range" max field.
|
||||||
HTTPUIInputTypeRangeMax = "max"
|
HTTPServerUIInputTypeRangeMax = "max"
|
||||||
|
|
||||||
// HTTPUIInputTypeRangeStep is the html input "range" step field.
|
// HTTPServerUIInputTypeRangeStep is the html input "range" step field.
|
||||||
HTTPUIInputTypeRangeStep = "step"
|
HTTPServerUIInputTypeRangeStep = "step"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Form represents the entire form containing all the desired elements.
|
// Form represents the entire form containing all the desired elements.
|
||||||
@@ -39,7 +39,7 @@ import (
|
|||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/engine/resources/http_ui/common"
|
"github.com/purpleidea/mgmt/engine/resources/http_server_ui/common"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ func (obj *Main) Run() error {
|
|||||||
}
|
}
|
||||||
//fmt.Printf("%+v\n", element) // debug
|
//fmt.Printf("%+v\n", element) // debug
|
||||||
|
|
||||||
inputType, exists := x.Type[common.HTTPUIInputType] // "text" or "range" ...
|
inputType, exists := x.Type[common.HTTPServerUIInputType] // "text" or "range" ...
|
||||||
if !exists {
|
if !exists {
|
||||||
fmt.Printf("Element has no input type: %+v\n", element)
|
fmt.Printf("Element has no input type: %+v\n", element)
|
||||||
continue
|
continue
|
||||||
@@ -185,14 +185,14 @@ func (obj *Main) Run() error {
|
|||||||
//el.Call("setAttribute", "name", id)
|
//el.Call("setAttribute", "name", id)
|
||||||
el.Set("type", inputType)
|
el.Set("type", inputType)
|
||||||
|
|
||||||
if inputType == common.HTTPUIInputTypeRange {
|
if inputType == common.HTTPServerUIInputTypeRange {
|
||||||
if val, exists := x.Type[common.HTTPUIInputTypeRangeMin]; exists {
|
if val, exists := x.Type[common.HTTPServerUIInputTypeRangeMin]; exists {
|
||||||
el.Set("min", val)
|
el.Set("min", val)
|
||||||
}
|
}
|
||||||
if val, exists := x.Type[common.HTTPUIInputTypeRangeMax]; exists {
|
if val, exists := x.Type[common.HTTPServerUIInputTypeRangeMax]; exists {
|
||||||
el.Set("max", val)
|
el.Set("max", val)
|
||||||
}
|
}
|
||||||
if val, exists := x.Type[common.HTTPUIInputTypeRangeStep]; exists {
|
if val, exists := x.Type[common.HTTPServerUIInputTypeRangeStep]; exists {
|
||||||
el.Set("step", val)
|
el.Set("step", val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
// additional permission if he deems it necessary to achieve the goals of this
|
// additional permission if he deems it necessary to achieve the goals of this
|
||||||
// additional permission.
|
// additional permission.
|
||||||
|
|
||||||
//go:build httpuistatic
|
//go:build httpserveruistatic
|
||||||
|
|
||||||
package static
|
package static
|
||||||
|
|
||||||
@@ -36,16 +36,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HTTPUIStaticEmbedded specifies whether files have been embedded.
|
// HTTPServerUIStaticEmbedded specifies whether files have been
|
||||||
HTTPUIStaticEmbedded = true
|
// embedded.
|
||||||
|
HTTPServerUIStaticEmbedded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// HTTPUIIndexStaticBootstrapCSS is the embedded data. It is embedded.
|
// HTTPServerUIIndexStaticBootstrapCSS is the embedded data. It is
|
||||||
//go:embed http_ui/static/bootstrap.min.css
|
// embedded.
|
||||||
HTTPUIIndexStaticBootstrapCSS []byte
|
//go:embed http_server_ui/static/bootstrap.min.css
|
||||||
|
HTTPServerUIIndexStaticBootstrapCSS []byte
|
||||||
|
|
||||||
// HTTPUIIndexStaticBootstrapJS is the embedded data. It is embedded.
|
// HTTPServerUIIndexStaticBootstrapJS is the embedded data. It is
|
||||||
//go:embed http_ui/static/bootstrap.bundle.min.js
|
// embedded.
|
||||||
HTTPUIIndexStaticBootstrapJS []byte
|
//go:embed http_server_ui/static/bootstrap.bundle.min.js
|
||||||
|
HTTPServerUIIndexStaticBootstrapJS []byte
|
||||||
)
|
)
|
||||||
@@ -27,19 +27,22 @@
|
|||||||
// additional permission if he deems it necessary to achieve the goals of this
|
// additional permission if he deems it necessary to achieve the goals of this
|
||||||
// additional permission.
|
// additional permission.
|
||||||
|
|
||||||
//go:build !httpuistatic
|
//go:build !httpserveruistatic
|
||||||
|
|
||||||
package static
|
package static
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HTTPUIStaticEmbedded specifies whether files have been embedded.
|
// HTTPServerUIStaticEmbedded specifies whether files have been
|
||||||
HTTPUIStaticEmbedded = false
|
// embedded.
|
||||||
|
HTTPServerUIStaticEmbedded = false
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// HTTPUIIndexStaticBootstrapCSS is the embedded data. It is empty here.
|
// HTTPServerUIIndexStaticBootstrapCSS is the embedded data. It is empty
|
||||||
HTTPUIIndexStaticBootstrapCSS []byte
|
// here.
|
||||||
|
HTTPServerUIIndexStaticBootstrapCSS []byte
|
||||||
|
|
||||||
// HTTPUIIndexStaticBootstrapJS is the embedded data. It is empty here.
|
// HTTPServerUIIndexStaticBootstrapJS is the embedded data. It is empty
|
||||||
HTTPUIIndexStaticBootstrapJS []byte
|
// here.
|
||||||
|
HTTPServerUIIndexStaticBootstrapJS []byte
|
||||||
)
|
)
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
package static
|
package static
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HTTPUIIndexBootstrapCSS is the path to the bootstrap css file when
|
// HTTPServerUIIndexBootstrapCSS is the path to the bootstrap css file
|
||||||
// embedded, relative to the parent directory.
|
// when embedded, relative to the parent directory.
|
||||||
HTTPUIIndexBootstrapCSS = "static/bootstrap.min.css"
|
HTTPServerUIIndexBootstrapCSS = "static/bootstrap.min.css"
|
||||||
|
|
||||||
// HTTPUIIndexBootstrapJS is the path to the bootstrap js file when
|
// HTTPServerUIIndexBootstrapJS is the path to the bootstrap js file
|
||||||
// embedded, relative to the parent directory.
|
// when embedded, relative to the parent directory.
|
||||||
HTTPUIIndexBootstrapJS = "static/bootstrap.bundle.min.js"
|
HTTPServerUIIndexBootstrapJS = "static/bootstrap.bundle.min.js"
|
||||||
)
|
)
|
||||||
@@ -37,37 +37,37 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/engine"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
"github.com/purpleidea/mgmt/engine/resources/http_ui/common"
|
"github.com/purpleidea/mgmt/engine/resources/http_server_ui/common"
|
||||||
"github.com/purpleidea/mgmt/engine/traits"
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
httpUIInputKind = httpUIKind + ":input"
|
httpServerUIInputKind = httpServerUIKind + ":input"
|
||||||
|
|
||||||
httpUIInputStoreKey = "key"
|
httpServerUIInputStoreKey = "key"
|
||||||
httpUIInputStoreSchemeLocal = "local"
|
httpServerUIInputStoreSchemeLocal = "local"
|
||||||
httpUIInputStoreSchemeWorld = "world"
|
httpServerUIInputStoreSchemeWorld = "world"
|
||||||
|
|
||||||
httpUIInputTypeText = common.HTTPUIInputTypeText // "text"
|
httpServerUIInputTypeText = common.HTTPServerUIInputTypeText // "text"
|
||||||
httpUIInputTypeRange = common.HTTPUIInputTypeRange // "range"
|
httpServerUIInputTypeRange = common.HTTPServerUIInputTypeRange // "range"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
engine.RegisterResource(httpUIInputKind, func() engine.Res { return &HTTPUIInputRes{} })
|
engine.RegisterResource(httpServerUIInputKind, func() engine.Res { return &HTTPServerUIInputRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPUIInputRes is a form element that exists within a http:ui resource, which
|
// HTTPServerUIInputRes is a form element that exists within a http:server:ui
|
||||||
// exists within an http server. The name is used as the unique id of the field,
|
// resource, which exists within an http server. The name is used as the unique
|
||||||
// unless the id field is specified, and in that case it is used instead. The
|
// id of the field, unless the id field is specified, and in that case it is
|
||||||
// way this works is that it autogroups at runtime with an existing http:ui
|
// used instead. The way this works is that it autogroups at runtime with an
|
||||||
// resource, and in doing so makes the form field associated with this resource
|
// existing http:server:ui resource, and in doing so makes the form field
|
||||||
// available as part of that ui which is itself grouped and served from the http
|
// associated with this resource available as part of that ui which is itself
|
||||||
// server resource.
|
// grouped and served from the http server resource.
|
||||||
type HTTPUIInputRes struct {
|
type HTTPServerUIInputRes struct {
|
||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
traits.Edgeable // XXX: add autoedge support
|
traits.Edgeable // XXX: add autoedge support
|
||||||
traits.Groupable // can be grouped into HTTPUIRes
|
traits.Groupable // can be grouped into HTTPServerUIRes
|
||||||
traits.Sendable
|
traits.Sendable
|
||||||
|
|
||||||
init *engine.Init
|
init *engine.Init
|
||||||
@@ -119,14 +119,14 @@ type HTTPUIInputRes struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
func (obj *HTTPUIInputRes) Default() engine.Res {
|
func (obj *HTTPServerUIInputRes) Default() engine.Res {
|
||||||
return &HTTPUIInputRes{
|
return &HTTPServerUIInputRes{
|
||||||
Type: "text://",
|
Type: "text://",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the resource data structure was populated correctly.
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
func (obj *HTTPUIInputRes) Validate() error {
|
func (obj *HTTPServerUIInputRes) Validate() error {
|
||||||
if obj.GetID() == "" {
|
if obj.GetID() == "" {
|
||||||
return fmt.Errorf("empty id")
|
return fmt.Errorf("empty id")
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ func (obj *HTTPUIInputRes) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init runs some startup code for this resource.
|
// Init runs some startup code for this resource.
|
||||||
func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
|
func (obj *HTTPServerUIInputRes) Init(init *engine.Init) error {
|
||||||
obj.init = init // save for later
|
obj.init = init // save for later
|
||||||
|
|
||||||
u, err := url.Parse(obj.Type)
|
u, err := url.Parse(obj.Type)
|
||||||
@@ -159,7 +159,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
|
|||||||
if u == nil {
|
if u == nil {
|
||||||
return fmt.Errorf("can't parse Type")
|
return fmt.Errorf("can't parse Type")
|
||||||
}
|
}
|
||||||
if u.Scheme != httpUIInputTypeText && u.Scheme != httpUIInputTypeRange {
|
if u.Scheme != httpServerUIInputTypeText && u.Scheme != httpServerUIInputTypeRange {
|
||||||
return fmt.Errorf("unknown scheme: %s", u.Scheme)
|
return fmt.Errorf("unknown scheme: %s", u.Scheme)
|
||||||
}
|
}
|
||||||
values, err := url.ParseQuery(u.RawQuery)
|
values, err := url.ParseQuery(u.RawQuery)
|
||||||
@@ -177,7 +177,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
|
|||||||
if u == nil {
|
if u == nil {
|
||||||
return fmt.Errorf("can't parse Store")
|
return fmt.Errorf("can't parse Store")
|
||||||
}
|
}
|
||||||
if u.Scheme != httpUIInputStoreSchemeLocal && u.Scheme != httpUIInputStoreSchemeWorld {
|
if u.Scheme != httpServerUIInputStoreSchemeLocal && u.Scheme != httpServerUIInputStoreSchemeWorld {
|
||||||
return fmt.Errorf("unknown scheme: %s", u.Scheme)
|
return fmt.Errorf("unknown scheme: %s", u.Scheme)
|
||||||
}
|
}
|
||||||
values, err := url.ParseQuery(u.RawQuery)
|
values, err := url.ParseQuery(u.RawQuery)
|
||||||
@@ -188,7 +188,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
|
|||||||
obj.scheme = u.Scheme // cache for later
|
obj.scheme = u.Scheme // cache for later
|
||||||
obj.key = obj.Name() // default
|
obj.key = obj.Name() // default
|
||||||
|
|
||||||
x, exists := values[httpUIInputStoreKey]
|
x, exists := values[httpServerUIInputStoreKey]
|
||||||
if exists && len(x) > 0 && x[0] != "" { // ignore absent or broken keys
|
if exists && len(x) > 0 && x[0] != "" { // ignore absent or broken keys
|
||||||
obj.key = x[0]
|
obj.key = x[0]
|
||||||
}
|
}
|
||||||
@@ -203,13 +203,13 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup is run by the engine to clean up after the resource is done.
|
// Cleanup is run by the engine to clean up after the resource is done.
|
||||||
func (obj *HTTPUIInputRes) Cleanup() error {
|
func (obj *HTTPServerUIInputRes) Cleanup() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getKey returns the key to be used for this resource. If the Store field is
|
// 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.
|
// specified, it will use that parsed part, otherwise it uses the Name.
|
||||||
func (obj *HTTPUIInputRes) getKey() string {
|
func (obj *HTTPServerUIInputRes) getKey() string {
|
||||||
if obj.Store != "" {
|
if obj.Store != "" {
|
||||||
return obj.key
|
return obj.key
|
||||||
}
|
}
|
||||||
@@ -220,20 +220,20 @@ func (obj *HTTPUIInputRes) getKey() string {
|
|||||||
// ParentName is used to limit which resources autogroup into this one. If it's
|
// 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
|
// empty then it's ignored, otherwise it must match the Name of the parent to
|
||||||
// get grouped.
|
// get grouped.
|
||||||
func (obj *HTTPUIInputRes) ParentName() string {
|
func (obj *HTTPServerUIInputRes) ParentName() string {
|
||||||
return obj.Path
|
return obj.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKind returns the kind of this resource.
|
// GetKind returns the kind of this resource.
|
||||||
func (obj *HTTPUIInputRes) GetKind() string {
|
func (obj *HTTPServerUIInputRes) GetKind() string {
|
||||||
// NOTE: We don't *need* to return such a specific string, and "input"
|
// 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.
|
// would be enough, but we might as well use this because we have it.
|
||||||
return httpUIInputKind
|
return httpServerUIInputKind
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns the actual ID we respond to. When ID is not specified, we use
|
// GetID returns the actual ID we respond to. When ID is not specified, we use
|
||||||
// the Name.
|
// the Name.
|
||||||
func (obj *HTTPUIInputRes) GetID() string {
|
func (obj *HTTPServerUIInputRes) GetID() string {
|
||||||
if obj.ID != "" {
|
if obj.ID != "" {
|
||||||
return obj.ID
|
return obj.ID
|
||||||
}
|
}
|
||||||
@@ -242,7 +242,7 @@ func (obj *HTTPUIInputRes) GetID() string {
|
|||||||
|
|
||||||
// SetValue stores the new value field that was obtained from submitting the
|
// SetValue stores the new value field that was obtained from submitting the
|
||||||
// form. This receives the raw, unsafe value that you must validate first.
|
// form. This receives the raw, unsafe value that you must validate first.
|
||||||
func (obj *HTTPUIInputRes) SetValue(ctx context.Context, vs []string) error {
|
func (obj *HTTPServerUIInputRes) SetValue(ctx context.Context, vs []string) error {
|
||||||
if len(vs) != 1 {
|
if len(vs) != 1 {
|
||||||
return fmt.Errorf("unexpected length of %d", len(vs))
|
return fmt.Errorf("unexpected length of %d", len(vs))
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ func (obj *HTTPUIInputRes) SetValue(ctx context.Context, vs []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setValue is the helper version where the caller must provide the mutex.
|
// setValue is the helper version where the caller must provide the mutex.
|
||||||
func (obj *HTTPUIInputRes) setValue(ctx context.Context, val string) error {
|
func (obj *HTTPServerUIInputRes) setValue(ctx context.Context, val string) error {
|
||||||
obj.value = val
|
obj.value = val
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -270,7 +270,7 @@ func (obj *HTTPUIInputRes) setValue(ctx context.Context, val string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *HTTPUIInputRes) checkValue(value string) error {
|
func (obj *HTTPServerUIInputRes) checkValue(value string) error {
|
||||||
// XXX: validate based on obj.Type
|
// XXX: validate based on obj.Type
|
||||||
// XXX: validate what kind of values are allowed, probably no \n, etc...
|
// XXX: validate what kind of values are allowed, probably no \n, etc...
|
||||||
return nil
|
return nil
|
||||||
@@ -278,7 +278,7 @@ func (obj *HTTPUIInputRes) checkValue(value string) error {
|
|||||||
|
|
||||||
// GetValue gets a string representation for the form value, that we'll use in
|
// GetValue gets a string representation for the form value, that we'll use in
|
||||||
// our html form.
|
// our html form.
|
||||||
func (obj *HTTPUIInputRes) GetValue(ctx context.Context) (string, error) {
|
func (obj *HTTPServerUIInputRes) GetValue(ctx context.Context) (string, error) {
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
defer obj.mutex.Unlock()
|
defer obj.mutex.Unlock()
|
||||||
|
|
||||||
@@ -297,36 +297,36 @@ func (obj *HTTPUIInputRes) GetValue(ctx context.Context) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetType returns a map that you can use to build the input field in the ui.
|
// GetType returns a map that you can use to build the input field in the ui.
|
||||||
func (obj *HTTPUIInputRes) GetType() map[string]string {
|
func (obj *HTTPServerUIInputRes) GetType() map[string]string {
|
||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
|
|
||||||
if obj.typeURL.Scheme == httpUIInputTypeRange {
|
if obj.typeURL.Scheme == httpServerUIInputTypeRange {
|
||||||
m = obj.rangeGetType()
|
m = obj.rangeGetType()
|
||||||
}
|
}
|
||||||
|
|
||||||
m[common.HTTPUIInputType] = obj.typeURL.Scheme
|
m[common.HTTPServerUIInputType] = obj.typeURL.Scheme
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *HTTPUIInputRes) rangeGetType() map[string]string {
|
func (obj *HTTPServerUIInputRes) rangeGetType() map[string]string {
|
||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
base := 10
|
base := 10
|
||||||
bits := 64
|
bits := 64
|
||||||
|
|
||||||
if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeMin]; exists && len(sa) > 0 {
|
if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMin]; exists && len(sa) > 0 {
|
||||||
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
|
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
|
||||||
m[common.HTTPUIInputTypeRangeMin] = strconv.FormatInt(x, base)
|
m[common.HTTPServerUIInputTypeRangeMin] = strconv.FormatInt(x, base)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeMax]; exists && len(sa) > 0 {
|
if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMax]; exists && len(sa) > 0 {
|
||||||
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
|
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
|
||||||
m[common.HTTPUIInputTypeRangeMax] = strconv.FormatInt(x, base)
|
m[common.HTTPServerUIInputTypeRangeMax] = strconv.FormatInt(x, base)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeStep]; exists && len(sa) > 0 {
|
if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeStep]; exists && len(sa) > 0 {
|
||||||
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
|
if x, err := strconv.ParseInt(sa[0], base, bits); err == nil {
|
||||||
m[common.HTTPUIInputTypeRangeStep] = strconv.FormatInt(x, base)
|
m[common.HTTPServerUIInputTypeRangeStep] = strconv.FormatInt(x, base)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,18 +335,18 @@ func (obj *HTTPUIInputRes) rangeGetType() map[string]string {
|
|||||||
|
|
||||||
// GetSort returns a string that you can use to determine the global sorted
|
// GetSort returns a string that you can use to determine the global sorted
|
||||||
// display order of all the elements in a ui.
|
// display order of all the elements in a ui.
|
||||||
func (obj *HTTPUIInputRes) GetSort() string {
|
func (obj *HTTPServerUIInputRes) GetSort() string {
|
||||||
return obj.Sort
|
return obj.Sort
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch is the primary listener for this resource and it outputs events. This
|
// 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
|
// particular one does absolutely nothing but block until we've received a done
|
||||||
// signal.
|
// signal.
|
||||||
func (obj *HTTPUIInputRes) Watch(ctx context.Context) error {
|
func (obj *HTTPServerUIInputRes) Watch(ctx context.Context) error {
|
||||||
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
|
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
|
||||||
return obj.localWatch(ctx)
|
return obj.localWatch(ctx)
|
||||||
}
|
}
|
||||||
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
|
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
|
||||||
return obj.worldWatch(ctx)
|
return obj.worldWatch(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ func (obj *HTTPUIInputRes) Watch(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *HTTPUIInputRes) localWatch(ctx context.Context) error {
|
func (obj *HTTPServerUIInputRes) localWatch(ctx context.Context) error {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ func (obj *HTTPUIInputRes) localWatch(ctx context.Context) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *HTTPUIInputRes) worldWatch(ctx context.Context) error {
|
func (obj *HTTPServerUIInputRes) worldWatch(ctx context.Context) error {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -439,16 +439,16 @@ func (obj *HTTPUIInputRes) worldWatch(ctx context.Context) error {
|
|||||||
// CheckApply performs the send/recv portion of this autogrouped resources. That
|
// 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
|
// 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.
|
// 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) {
|
func (obj *HTTPServerUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
if obj.init.Debug {
|
if obj.init.Debug {
|
||||||
obj.init.Logf("CheckApply")
|
obj.init.Logf("CheckApply")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're in ".Value" mode, we want to look at the incoming value, and
|
// 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.
|
// 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.
|
// The private value gets set by obj.SetValue from the http:server:ui
|
||||||
// If we're in ".Store" mode, then we're reconciling between the "World"
|
// parent. If we're in ".Store" mode, then we're reconciling between the
|
||||||
// and the http:ui "Web".
|
// "World" and the http:server:ui "Web".
|
||||||
|
|
||||||
if obj.Store != "" {
|
if obj.Store != "" {
|
||||||
return obj.storeCheckApply(ctx, apply)
|
return obj.storeCheckApply(ctx, apply)
|
||||||
@@ -458,14 +458,14 @@ func (obj *HTTPUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, er
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (bool, error) {
|
func (obj *HTTPServerUIInputRes) valueCheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
|
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
value := obj.value // gets set by obj.SetValue
|
value := obj.value // gets set by obj.SetValue
|
||||||
obj.mutex.Unlock()
|
obj.mutex.Unlock()
|
||||||
|
|
||||||
if obj.last != nil && *obj.last == value {
|
if obj.last != nil && *obj.last == value {
|
||||||
if err := obj.init.Send(&HTTPUIInputSends{
|
if err := obj.init.Send(&HTTPServerUIInputSends{
|
||||||
Value: &value,
|
Value: &value,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -474,7 +474,7 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !apply {
|
if !apply {
|
||||||
if err := obj.init.Send(&HTTPUIInputSends{
|
if err := obj.init.Send(&HTTPServerUIInputSends{
|
||||||
Value: &value, // XXX: arbitrary since we're in noop mode
|
Value: &value, // XXX: arbitrary since we're in noop mode
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -489,7 +489,7 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
obj.init.Logf("sending: %s", value)
|
obj.init.Logf("sending: %s", value)
|
||||||
|
|
||||||
// send
|
// send
|
||||||
if err := obj.init.Send(&HTTPUIInputSends{
|
if err := obj.init.Send(&HTTPServerUIInputSends{
|
||||||
Value: &value,
|
Value: &value,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -501,11 +501,11 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
|
|
||||||
// storeCheckApply is a tricky function where we attempt to reconcile the state
|
// 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
|
// 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"
|
// "http:server:ui" change by an end user. Basically whoever runs last is the
|
||||||
// value that we want to use. We know who sent the event from reading the
|
// "right" value that we want to use. We know who sent the event from reading
|
||||||
// storeEvent variable, and if it was the World, we want to cache it locally,
|
// the storeEvent variable, and if it was the World, we want to cache it
|
||||||
// and if it was the Web, then we want to push it up to the store.
|
// 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) {
|
func (obj *HTTPServerUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||||
|
|
||||||
v1, exists, err := obj.storeGet(ctx, obj.getKey())
|
v1, exists, err := obj.storeGet(ctx, obj.getKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -519,7 +519,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
obj.mutex.Unlock()
|
obj.mutex.Unlock()
|
||||||
|
|
||||||
if exists && v1 == v2 { // both sides are happy
|
if exists && v1 == v2 { // both sides are happy
|
||||||
if err := obj.init.Send(&HTTPUIInputSends{
|
if err := obj.init.Send(&HTTPServerUIInputSends{
|
||||||
Value: &v2,
|
Value: &v2,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -528,7 +528,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !apply {
|
if !apply {
|
||||||
if err := obj.init.Send(&HTTPUIInputSends{
|
if err := obj.init.Send(&HTTPServerUIInputSends{
|
||||||
Value: &v2, // XXX: arbitrary since we're in noop mode
|
Value: &v2, // XXX: arbitrary since we're in noop mode
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -555,7 +555,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
obj.init.Logf("sending: %s", value)
|
obj.init.Logf("sending: %s", value)
|
||||||
|
|
||||||
// send
|
// send
|
||||||
if err := obj.init.Send(&HTTPUIInputSends{
|
if err := obj.init.Send(&HTTPServerUIInputSends{
|
||||||
Value: &value,
|
Value: &value,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -564,8 +564,8 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) {
|
func (obj *HTTPServerUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) {
|
||||||
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
|
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
|
||||||
val, err := obj.init.Local.ValueGet(ctx, key)
|
val, err := obj.init.Local.ValueGet(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err // real error
|
return "", false, err // real error
|
||||||
@@ -581,7 +581,7 @@ func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bo
|
|||||||
return s, true, nil
|
return s, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
|
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
|
||||||
val, err := obj.init.World.StrGet(ctx, key)
|
val, err := obj.init.World.StrGet(ctx, key)
|
||||||
if err != nil && obj.init.World.StrIsNotExist(err) {
|
if err != nil && obj.init.World.StrIsNotExist(err) {
|
||||||
return "", false, nil // val doesn't exist
|
return "", false, nil // val doesn't exist
|
||||||
@@ -595,13 +595,13 @@ func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bo
|
|||||||
return "", false, nil // something else
|
return "", false, nil // something else
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *HTTPUIInputRes) storeSet(ctx context.Context, key, val string) error {
|
func (obj *HTTPServerUIInputRes) storeSet(ctx context.Context, key, val string) error {
|
||||||
|
|
||||||
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
|
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
|
||||||
return obj.init.Local.ValueSet(ctx, key, val)
|
return obj.init.Local.ValueSet(ctx, key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
|
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
|
||||||
return obj.init.World.StrSet(ctx, key, val)
|
return obj.init.World.StrSet(ctx, key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,9 +609,9 @@ func (obj *HTTPUIInputRes) storeSet(ctx context.Context, key, val string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *HTTPUIInputRes) Cmp(r engine.Res) error {
|
func (obj *HTTPServerUIInputRes) Cmp(r engine.Res) error {
|
||||||
// we can only compare HTTPUIInputRes to others of the same resource kind
|
// we can only compare HTTPServerUIInputRes to others of the same resource kind
|
||||||
res, ok := r.(*HTTPUIInputRes)
|
res, ok := r.(*HTTPServerUIInputRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("res is not the same kind")
|
return fmt.Errorf("res is not the same kind")
|
||||||
}
|
}
|
||||||
@@ -640,13 +640,13 @@ func (obj *HTTPUIInputRes) Cmp(r engine.Res) error {
|
|||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *HTTPUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *HTTPServerUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes HTTPUIInputRes // indirection to avoid infinite recursion
|
type rawRes HTTPServerUIInputRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
def := obj.Default() // get the default
|
def := obj.Default() // get the default
|
||||||
res, ok := def.(*HTTPUIInputRes) // put in the right format
|
res, ok := def.(*HTTPServerUIInputRes) // put in the right format
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("could not convert to HTTPUIInputRes")
|
return fmt.Errorf("could not convert to HTTPServerUIInputRes")
|
||||||
}
|
}
|
||||||
raw := rawRes(*res) // convert; the defaults go here
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
@@ -654,20 +654,20 @@ func (obj *HTTPUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
*obj = HTTPUIInputRes(raw) // restore from indirection with type conversion!
|
*obj = HTTPServerUIInputRes(raw) // restore from indirection with type conversion!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPUIInputSends is the struct of data which is sent after a successful
|
// HTTPServerUIInputSends is the struct of data which is sent after a successful
|
||||||
// Apply.
|
// Apply.
|
||||||
type HTTPUIInputSends struct {
|
type HTTPServerUIInputSends struct {
|
||||||
// Value is the text element value being sent.
|
// Value is the text element value being sent.
|
||||||
Value *string `lang:"value"`
|
Value *string `lang:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends represents the default struct of values we can send using Send/Recv.
|
// Sends represents the default struct of values we can send using Send/Recv.
|
||||||
func (obj *HTTPUIInputRes) Sends() interface{} {
|
func (obj *HTTPServerUIInputRes) Sends() interface{} {
|
||||||
return &HTTPUIInputSends{
|
return &HTTPServerUIInputSends{
|
||||||
Value: nil,
|
Value: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,12 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing
|
|||||||
}
|
}
|
||||||
|
|
||||||
# you can add a raw file like this...
|
# you can add a raw file like this...
|
||||||
http:file "/file1" {
|
http:server:file "/file1" {
|
||||||
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
# wget --post-data 'key=hello&whatever=bye' -O - http://127.0.0.1:8080/flag1
|
# wget --post-data 'key=hello&whatever=bye' -O - http://127.0.0.1:8080/flag1
|
||||||
http:flag "/flag1" {
|
http:server:flag "/flag1" {
|
||||||
#server => ":8080",
|
#server => ":8080",
|
||||||
key => "key",
|
key => "key",
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,8 @@ print "print1" {
|
|||||||
Meta:autogroup => false,
|
Meta:autogroup => false,
|
||||||
}
|
}
|
||||||
|
|
||||||
Http:Flag["/flag1"].value -> Print["print1"].msg
|
Http:Server:Flag["/flag1"].value -> Print["print1"].msg
|
||||||
Http:Flag["/flag1"].value -> Value["value1"].any
|
Http:Server:Flag["/flag1"].value -> Value["value1"].any
|
||||||
|
|
||||||
$ret = value.get_str("value1") # name of value resource
|
$ret = value.get_str("value1") # name of value resource
|
||||||
$val = $ret->value
|
$val = $ret->value
|
||||||
@@ -4,7 +4,7 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing
|
|||||||
}
|
}
|
||||||
|
|
||||||
# you can add a raw file like this...
|
# you can add a raw file like this...
|
||||||
http:file "/file1" {
|
http:server:file "/file1" {
|
||||||
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ file "${distroarch_http_prefix}" { # root http dir
|
|||||||
}
|
}
|
||||||
|
|
||||||
# this one is backed by the (optional) rsync
|
# this one is backed by the (optional) rsync
|
||||||
#http:file "/fedora/releases/${version}/Everything/${arch}/os/" {
|
#http:server:file "/fedora/releases/${version}/Everything/${arch}/os/" {
|
||||||
# path => "${distroarch_http_prefix}",
|
# path => "${distroarch_http_prefix}",
|
||||||
#}
|
#}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ file "${distroarch_http_prefix}" { # root http dir
|
|||||||
# wget http://127.0.0.1:8080/fedora/releases/38/Everything/x86_64/os/Packages/c/cowsay-3.7.0-7.fc38.noarch.rpm
|
# wget http://127.0.0.1:8080/fedora/releases/38/Everything/x86_64/os/Packages/c/cowsay-3.7.0-7.fc38.noarch.rpm
|
||||||
# wget https://mirrors.xtom.de/fedora/releases/38/Everything/x86_64/os/Packages/c/cowsay-3.7.0-7.fc38.noarch.rpm
|
# wget https://mirrors.xtom.de/fedora/releases/38/Everything/x86_64/os/Packages/c/cowsay-3.7.0-7.fc38.noarch.rpm
|
||||||
|
|
||||||
http:proxy "/fedora/releases/${version}/Everything/${arch}/os/" { # same as the http:file path
|
http:server:proxy "/fedora/releases/${version}/Everything/${arch}/os/" { # same as the http:server:file path
|
||||||
|
|
||||||
cache => "${distroarch_http_prefix}", # /tmp/os/
|
cache => "${distroarch_http_prefix}", # /tmp/os/
|
||||||
#force => false, # if true, overwrite or change from dir->file if needed
|
#force => false, # if true, overwrite or change from dir->file if needed
|
||||||
@@ -5,11 +5,11 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing
|
|||||||
}
|
}
|
||||||
|
|
||||||
# you can add a raw file like this...
|
# you can add a raw file like this...
|
||||||
http:file "/file1" {
|
http:server:file "/file1" {
|
||||||
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
http:ui "/ui/" {
|
http:server:ui "/ui/" {
|
||||||
#path => "/ui/", # we can override the name like this if needed
|
#path => "/ui/", # we can override the name like this if needed
|
||||||
|
|
||||||
data => struct{
|
data => struct{
|
||||||
@@ -20,18 +20,18 @@ http:ui "/ui/" {
|
|||||||
|
|
||||||
$text1_id = "text1"
|
$text1_id = "text1"
|
||||||
$range1_id = "range1"
|
$range1_id = "range1"
|
||||||
http:ui:input $text1_id {
|
http:server:ui:input $text1_id {
|
||||||
store => "world://",
|
store => "world://",
|
||||||
sort => "a",
|
sort => "a",
|
||||||
}
|
}
|
||||||
|
|
||||||
http:ui:input $range1_id {
|
http:server:ui:input $range1_id {
|
||||||
store => "world://",
|
store => "world://",
|
||||||
type => "range://?min=0&max=5&step=1",
|
type => "range://?min=0&max=5&step=1",
|
||||||
sort => "b",
|
sort => "b",
|
||||||
}
|
}
|
||||||
|
|
||||||
#Http:Ui:Input[$text1_id].value -> Kv[$text1_id].value
|
#Http:Server:Ui:Input[$text1_id].value -> Kv[$text1_id].value
|
||||||
#kv $text1_id { # store in world
|
#kv $text1_id { # store in world
|
||||||
#}
|
#}
|
||||||
|
|
||||||
@@ -5,11 +5,11 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing
|
|||||||
}
|
}
|
||||||
|
|
||||||
# you can add a raw file like this...
|
# you can add a raw file like this...
|
||||||
http:file "/file1" {
|
http:server:file "/file1" {
|
||||||
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
http:ui "/ui/" {
|
http:server:ui "/ui/" {
|
||||||
#path => "/ui/", # we can override the name like this if needed
|
#path => "/ui/", # we can override the name like this if needed
|
||||||
|
|
||||||
data => struct{
|
data => struct{
|
||||||
@@ -30,17 +30,17 @@ $range1_val = if $ret2->ready {
|
|||||||
} else {
|
} else {
|
||||||
"2" # some default
|
"2" # some default
|
||||||
}
|
}
|
||||||
http:ui:input $text1_id {
|
http:server:ui:input $text1_id {
|
||||||
value => $text1_val, # it passes back into itself!
|
value => $text1_val, # it passes back into itself!
|
||||||
}
|
}
|
||||||
|
|
||||||
http:ui:input $range1_id {
|
http:server:ui:input $range1_id {
|
||||||
value => $range1_val,
|
value => $range1_val,
|
||||||
type => "range://?min=0&max=5&step=1",
|
type => "range://?min=0&max=5&step=1",
|
||||||
sort => "b",
|
sort => "b",
|
||||||
}
|
}
|
||||||
|
|
||||||
Http:Ui:Input[$text1_id].value -> Value[$text1_id].any
|
Http:Server:Ui:Input[$text1_id].value -> Value[$text1_id].any
|
||||||
|
|
||||||
value $text1_id {
|
value $text1_id {
|
||||||
any => "whatever", # TODO: remove the temporary placeholder here
|
any => "whatever", # TODO: remove the temporary placeholder here
|
||||||
@@ -14,7 +14,7 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing
|
|||||||
}
|
}
|
||||||
|
|
||||||
# you can add a raw file like this...
|
# you can add a raw file like this...
|
||||||
http:file "/file1" {
|
http:server:file "/file1" {
|
||||||
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,19 +26,19 @@ file $f2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# you can point to it directly...
|
# you can point to it directly...
|
||||||
http:file "/file2" {
|
http:server:file "/file2" {
|
||||||
path => $f2,
|
path => $f2,
|
||||||
|
|
||||||
Depend => File[$f2], # TODO: add autoedges
|
Depend => File[$f2], # TODO: add autoedges
|
||||||
}
|
}
|
||||||
|
|
||||||
# here's a file in the middle of nowhere that still works...
|
# here's a file in the middle of nowhere that still works...
|
||||||
http:file "/i/am/some/deeply/nested/file" {
|
http:server:file "/i/am/some/deeply/nested/file" {
|
||||||
data => "how did you find me!\n",
|
data => "how did you find me!\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
# and this file won't autogroup with the main http server
|
# and this file won't autogroup with the main http server
|
||||||
http:file "/nope/noway" {
|
http:server:file "/nope/noway" {
|
||||||
data => "i won't be seen!\n",
|
data => "i won't be seen!\n",
|
||||||
server => "someone else!", # normally we don't use this this way
|
server => "someone else!", # normally we don't use this this way
|
||||||
}
|
}
|
||||||
@@ -34,12 +34,12 @@ http:server ":8080" { # by default http uses :80 but using :8080 avoids needing
|
|||||||
}
|
}
|
||||||
|
|
||||||
# you can add a raw file like this...
|
# you can add a raw file like this...
|
||||||
http:file "/file1" {
|
http:server:file "/file1" {
|
||||||
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
data => "hello, world, i'm file1 and i don't exist on disk!\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
# this pulls in a whole folder, since path is a folder!
|
# this pulls in a whole folder, since path is a folder!
|
||||||
http:file "/secret/folder/" {
|
http:server:file "/secret/folder/" {
|
||||||
path => "${root}",
|
path => "${root}",
|
||||||
|
|
||||||
Depend => File["${root}"], # TODO: add autoedges
|
Depend => File["${root}"], # TODO: add autoedges
|
||||||
@@ -211,7 +211,7 @@ class base($config) {
|
|||||||
$vardir = local.vardir("provisioner/")
|
$vardir = local.vardir("provisioner/")
|
||||||
$binary_path = deploy.binary_path()
|
$binary_path = deploy.binary_path()
|
||||||
|
|
||||||
http:file "/mgmt/binary" { # TODO: support different architectures
|
http:server:file "/mgmt/binary" { # TODO: support different architectures
|
||||||
path => $binary_path, # TODO: As long as binary doesn't contain private data!
|
path => $binary_path, # TODO: As long as binary doesn't contain private data!
|
||||||
|
|
||||||
Before => Print["ready"],
|
Before => Print["ready"],
|
||||||
@@ -251,7 +251,7 @@ class base($config) {
|
|||||||
gzip "${abs_gz}" {
|
gzip "${abs_gz}" {
|
||||||
input => "${abs_tar}",
|
input => "${abs_tar}",
|
||||||
}
|
}
|
||||||
http:file "/mgmt/deploy.tar.gz" {
|
http:server:file "/mgmt/deploy.tar.gz" {
|
||||||
path => "${abs_gz}",
|
path => "${abs_gz}",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +406,7 @@ class base:repo($config) {
|
|||||||
|
|
||||||
#Depend => Pkg[$pkgs],
|
#Depend => Pkg[$pkgs],
|
||||||
}
|
}
|
||||||
http:file "/${uid}/vmlinuz" { # when using ipxe
|
http:server:file "/${uid}/vmlinuz" { # when using ipxe
|
||||||
path => $vmlinuz_file, # TODO: add autoedges
|
path => $vmlinuz_file, # TODO: add autoedges
|
||||||
|
|
||||||
#Depend => Pkg[$pkgs],
|
#Depend => Pkg[$pkgs],
|
||||||
@@ -433,7 +433,7 @@ class base:repo($config) {
|
|||||||
|
|
||||||
#Depend => Pkg[$pkgs],
|
#Depend => Pkg[$pkgs],
|
||||||
}
|
}
|
||||||
http:file "/${uid}/initrd.img" { # when using ipxe
|
http:server:file "/${uid}/initrd.img" { # when using ipxe
|
||||||
path => $initrd_file, # TODO: add autoedges
|
path => $initrd_file, # TODO: add autoedges
|
||||||
|
|
||||||
#Depend => Pkg[$pkgs],
|
#Depend => Pkg[$pkgs],
|
||||||
@@ -441,15 +441,15 @@ class base:repo($config) {
|
|||||||
|
|
||||||
# this file resource serves the entire rsync directory over http
|
# this file resource serves the entire rsync directory over http
|
||||||
if $mirror == "" { # and $rsync != ""
|
if $mirror == "" { # and $rsync != ""
|
||||||
http:file "/fedora/releases/${version}/Everything/${arch}/os/" {
|
http:server:file "/fedora/releases/${version}/Everything/${arch}/os/" {
|
||||||
path => $distroarch_release_http_prefix,
|
path => $distroarch_release_http_prefix,
|
||||||
}
|
}
|
||||||
http:file "/fedora/updates/${version}/Everything/${arch}/" {
|
http:server:file "/fedora/updates/${version}/Everything/${arch}/" {
|
||||||
path => $distroarch_updates_http_prefix,
|
path => $distroarch_updates_http_prefix,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
# same as the above http:file path would have been
|
# same as the above http:server:file path would have been
|
||||||
http:proxy "/fedora/releases/${version}/Everything/${arch}/os/" {
|
http:server:proxy "/fedora/releases/${version}/Everything/${arch}/os/" {
|
||||||
sub => "/fedora/", # we remove this from the name!
|
sub => "/fedora/", # we remove this from the name!
|
||||||
head => $mirror,
|
head => $mirror,
|
||||||
|
|
||||||
@@ -457,7 +457,7 @@ class base:repo($config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# XXX: if we had both of these in the same http_prefix, we could overlap them with an rsync :/ hmm...
|
# XXX: if we had both of these in the same http_prefix, we could overlap them with an rsync :/ hmm...
|
||||||
http:proxy "/fedora/updates/${version}/Everything/${arch}/" { # no os/ dir at the end
|
http:server:proxy "/fedora/updates/${version}/Everything/${arch}/" { # no os/ dir at the end
|
||||||
sub => "/fedora/", # we remove this from the name!
|
sub => "/fedora/", # we remove this from the name!
|
||||||
head => $mirror,
|
head => $mirror,
|
||||||
|
|
||||||
@@ -486,10 +486,10 @@ class base:repo($config) {
|
|||||||
#
|
#
|
||||||
# baseurl => "http://${router_ip}:${http_port_str}/fedora/updates/${version}/Everything/${arch}/",
|
# baseurl => "http://${router_ip}:${http_port_str}/fedora/updates/${version}/Everything/${arch}/",
|
||||||
#}
|
#}
|
||||||
#http:file "/fedora/${uid}/fedora.repo" {
|
#http:server:file "/fedora/${uid}/fedora.repo" {
|
||||||
# data => golang.template(deploy.readfile("/files/repo.tmpl"), $fedora_repo_template),
|
# data => golang.template(deploy.readfile("/files/repo.tmpl"), $fedora_repo_template),
|
||||||
#}
|
#}
|
||||||
#http:file "/fedora/${uid}/updates.repo" {
|
#http:server:file "/fedora/${uid}/updates.repo" {
|
||||||
# data => golang.template(deploy.readfile("/files/repo.tmpl"), $updates_repo_template),
|
# data => golang.template(deploy.readfile("/files/repo.tmpl"), $updates_repo_template),
|
||||||
#}
|
#}
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ class base:host($name, $config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http:file "/${ipxe_menu}" { # for ipxe
|
http:server:file "/${ipxe_menu}" { # for ipxe
|
||||||
data => golang.template(deploy.readfile("/files/ipxe-menu.tmpl"), $menu_template),
|
data => golang.template(deploy.readfile("/files/ipxe-menu.tmpl"), $menu_template),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +740,7 @@ class base:host($name, $config) {
|
|||||||
gzip "${abs_gz}" {
|
gzip "${abs_gz}" {
|
||||||
input => "${abs_tar}",
|
input => "${abs_tar}",
|
||||||
}
|
}
|
||||||
http:file "/mgmt/deploy-${provision_key}.tar.gz" {
|
http:server:file "/mgmt/deploy-${provision_key}.tar.gz" {
|
||||||
path => "${abs_gz}",
|
path => "${abs_gz}",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -797,7 +797,7 @@ class base:host($name, $config) {
|
|||||||
"echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-deploy.sh && echo '${handoff_binary_path} deploy lang --seeds=http://127.0.0.1:2379 --no-git --module-path=${deploy_dir_modules} ${deploy_dir}${handoff_code_chunk}' >> ${firstboot_scripts_dir}mgmt-deploy.sh && chmod u+x ${firstboot_scripts_dir}mgmt-deploy.sh"
|
"echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-deploy.sh && echo '${handoff_binary_path} deploy lang --seeds=http://127.0.0.1:2379 --no-git --module-path=${deploy_dir_modules} ${deploy_dir}${handoff_code_chunk}' >> ${firstboot_scripts_dir}mgmt-deploy.sh && chmod u+x ${firstboot_scripts_dir}mgmt-deploy.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: Do we want to signal an http:flag if we're a "default" host?
|
# TODO: Do we want to signal an http:server:flag if we're a "default" host?
|
||||||
$provisioning_done = if $provision_key == "default" {
|
$provisioning_done = if $provision_key == "default" {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
@@ -851,7 +851,7 @@ class base:host($name, $config) {
|
|||||||
content => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template),
|
content => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template),
|
||||||
}
|
}
|
||||||
|
|
||||||
http:file "/fedora/kickstart/${hkey}.ks" { # usually $mac or `default`
|
http:server:file "/fedora/kickstart/${hkey}.ks" { # usually $mac or `default`
|
||||||
#data => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template),
|
#data => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template),
|
||||||
path => $kickstart_file,
|
path => $kickstart_file,
|
||||||
|
|
||||||
@@ -871,7 +871,7 @@ class base:host($name, $config) {
|
|||||||
|
|
||||||
##$str_true = convert.format_bool(true)
|
##$str_true = convert.format_bool(true)
|
||||||
##$str_false = convert.format_bool(false)
|
##$str_false = convert.format_bool(false)
|
||||||
#http:flag "${name}" {
|
#http:server:flag "${name}" {
|
||||||
# key => "done",
|
# key => "done",
|
||||||
# path => "/action/done/mac=${provision_key}",
|
# path => "/action/done/mac=${provision_key}",
|
||||||
# #mapped => {$str_true => $str_true, $str_false => $str_false,},
|
# #mapped => {$str_true => $str_true, $str_false => $str_false,},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
-- main.mcl --
|
-- main.mcl --
|
||||||
http:ui:input "text1" {}
|
http:server:ui:input "text1" {}
|
||||||
|
|
||||||
Http:Ui:Input["text1"].value -> Kv["kv1"].value
|
Http:Server:Ui:Input["text1"].value -> Kv["kv1"].value
|
||||||
|
|
||||||
kv "kv1" {}
|
kv "kv1" {}
|
||||||
|
|
||||||
-- OUTPUT --
|
-- OUTPUT --
|
||||||
Edge: http:ui:input[text1] -> kv[kv1] # http:ui:input[text1] -> kv[kv1]
|
Edge: http:server:ui:input[text1] -> kv[kv1] # http:server:ui:input[text1] -> kv[kv1]
|
||||||
Vertex: http:ui:input[text1]
|
Vertex: http:server:ui:input[text1]
|
||||||
Vertex: kv[kv1]
|
Vertex: kv[kv1]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ hi def link mclComment Comment
|
|||||||
|
|
||||||
syn keyword mclResources augeas aws:ec2 bmc:power config:etcd consul:kv cron deploy:tar dhcp:host
|
syn keyword mclResources augeas aws:ec2 bmc:power config:etcd consul:kv cron deploy:tar dhcp:host
|
||||||
syn keyword mclResources dhcp:range dhcp:server docker:container docker:image exec file
|
syn keyword mclResources dhcp:range dhcp:server docker:container docker:image exec file
|
||||||
syn keyword mclResources firewalld group gzip hetzner:vm hostname http:file http:flag http:proxy
|
syn keyword mclResources firewalld group gzip hetzner:vm hostname http:server:file http:server:flag http:server:proxy
|
||||||
syn keyword mclResources http:server kv mount msg net noop nspawn password pippet pkg print svc
|
syn keyword mclResources http:server kv mount msg net noop nspawn password pippet pkg print svc
|
||||||
syn keyword mclResources sysctl tar test tftp:file tftp:server timer user value virt virt:builder
|
syn keyword mclResources sysctl tar test tftp:file tftp:server timer user value virt virt:builder
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ 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
|
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"
|
echo -e "\ttesting: $pkg"
|
||||||
|
|
||||||
if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_ui" ]; then
|
if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_server_ui" ]; then
|
||||||
continue # skip this special main package
|
continue # skip this special main package
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function reflowed-comments() {
|
|||||||
base=$(go list .)
|
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
|
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
|
if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_server_ui" ]; then
|
||||||
continue # skip this special main package
|
continue # skip this special main package
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user