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:
James Shubin
2025-05-25 01:40:25 -04:00
parent 1f54253f95
commit 654e958d3f
35 changed files with 1513 additions and 1413 deletions

View File

@@ -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))

View File

@@ -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

View File

@@ -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")

View File

@@ -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())

View File

@@ -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
} }

View File

@@ -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

View 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
}
}

View 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
}

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)
} }
} }

View File

@@ -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
) )

View File

@@ -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
) )

View File

@@ -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"
) )

View File

@@ -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,
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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
#} #}

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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,},

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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