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!
GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
MCL_FILES := $(shell find lang/ -name '*.mcl' -not -path 'old/*' -not -path 'tmp/*')
MISC_FILES := $(shell find engine/resources/http_ui/)
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))
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?
// 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!
if res, ok := vertex.(engine.GroupableRes); ok {
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,
// instead of flattening all of them into one arbitrary choice. But if
// we are doing hierarchical grouping, then we want to allow this type
// of grouping, or we won't end up building any hierarchies! This change
// was added for http:ui stuff. Check this condition is really required.
// of grouping, or we won't end up building any hierarchies! This was
// 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.IsGrouped() { // 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
// We sort deterministically, first by kind, and then by name. In
// particular, longer kind chunks sort first. So http:ui:text should
// appear before http:server and http:ui. This is a hack so that if we
// are doing hierarchical automatic grouping, it gives the http:ui:text
// a chance to get grouped into http:ui, before http:ui gets grouped
// into http:server, because once that happens, http:ui:text will never
// get grouped, and this won't work properly. This works, because when
// we start comparing iteratively the list of resources, it does this
// with a O(n^2) loop that compares the X and Y zero indexes first, and
// and then continues along. If the "longer" resources appear first,
// then they'll group together first. We should probably put this into
// a new Grouper struct, but for now we might as well leave it here.
// particular, longer kind chunks sort first. So http:server:ui:input
// should appear before http:server and http:server:ui. This is a
// strategy so that if we are doing hierarchical automatic grouping, it
// gives the http:server:ui:input a chance to get grouped into
// http:server:ui, before http:server:ui gets grouped into http:server,
// because once that happens, http:server:ui:input will never get
// grouped, and this won't work properly. This works, because when we
// start comparing iteratively the list of resources, it does this with
// a O(n^2) loop that compares the X and Y zero indexes first, and then
// continues along. If the "longer" resources appear first, then they'll
// 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 := RHVSort(ag.graph.Vertices())

View File

@@ -181,7 +181,7 @@ func (obj RHVSlice) Less(i, j int) bool {
li := len(si)
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
}

View File

@@ -31,13 +31,13 @@ SHELL = /usr/bin/env bash
.PHONY: build clean
default: build
WASM_FILE = http_ui/main.wasm
WASM_FILE = http_server_ui/main.wasm
build: $(WASM_FILE)
$(WASM_FILE): http_ui/main.go
$(WASM_FILE): http_server_ui/main.go
@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:
@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 (
httpFlagKind = httpKind + ":flag"
httpServerFlagKind = httpServerKind + ":flag"
)
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
// used as the public path of the flag, unless the path field is specified, and
// in that case it is used instead. The way this works is that it autogroups at
// runtime with an existing http resource, and in doing so makes the flag
// associated with this resource available to cause actions when it receives a
// request on that http server. If you create a flag which responds to the same
// type of request as an http:file resource or any other kind of resource, it is
// undefined behaviour which will answer the request. The most common clash will
// happen if both are present at the same path.
type HTTPFlagRes struct {
// HTTPServerFlagRes is a special path that exists within an http server. The
// name is used as the public path of the flag, unless the path field is
// specified, and in that case it is used instead. The way this works is that it
// autogroups at runtime with an existing http resource, and in doing so makes
// the flag associated with this resource available to cause actions when it
// receives a request on that http server. If you create a flag which responds
// to the same type of request as an http:server:file resource or any other kind
// of resource, it is undefined behaviour which will answer the request. The
// most common clash will happen if both are present at the same path.
type HTTPServerFlagRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can be grouped into HTTPServerRes
@@ -88,13 +88,13 @@ type HTTPFlagRes struct {
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPFlagRes) Default() engine.Res {
return &HTTPFlagRes{}
func (obj *HTTPServerFlagRes) Default() engine.Res {
return &HTTPServerFlagRes{}
}
// getPath returns the actual path we respond to. When Path is not specified, we
// use the Name.
func (obj *HTTPFlagRes) getPath() string {
func (obj *HTTPServerFlagRes) getPath() string {
if 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
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPFlagRes) ParentName() string {
func (obj *HTTPServerFlagRes) 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 *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?
if requestPath != obj.getPath() {
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.
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.
if req.Method != http.MethodPost {
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.
func (obj *HTTPFlagRes) Validate() error {
func (obj *HTTPServerFlagRes) Validate() error {
if obj.getPath() == "" {
return fmt.Errorf("empty filename")
}
@@ -179,7 +179,7 @@ func (obj *HTTPFlagRes) Validate() error {
}
// 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.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.
func (obj *HTTPFlagRes) Cleanup() error {
func (obj *HTTPServerFlagRes) Cleanup() error {
return nil
}
@@ -197,7 +197,7 @@ func (obj *HTTPFlagRes) Cleanup() error {
// 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
// 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
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.
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?
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
// autogrouping, so that we compare apples to apples, when we do the
// graphsync!
if err := obj.init.Send(&HTTPFlagSends{
if err := obj.init.Send(&HTTPServerFlagSends{
Value: &value,
}); err != nil {
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.
func (obj *HTTPFlagRes) Cmp(r engine.Res) error {
// we can only compare HTTPFlagRes to others of the same resource kind
res, ok := r.(*HTTPFlagRes)
func (obj *HTTPServerFlagRes) Cmp(r engine.Res) error {
// we can only compare HTTPServerFlagRes to others of the same resource kind
res, ok := r.(*HTTPServerFlagRes)
if !ok {
return fmt.Errorf("res is not the same kind")
}
@@ -307,28 +307,29 @@ func (obj *HTTPFlagRes) Cmp(r engine.Res) error {
return nil
}
// HTTPFlagSends is the struct of data which is sent after a successful Apply.
type HTTPFlagSends struct {
// HTTPServerFlagSends is the struct of data which is sent after a successful
// Apply.
type HTTPServerFlagSends struct {
// Value is the received value being sent.
Value *string `lang:"value"`
}
// Sends represents the default struct of values we can send using Send/Recv.
func (obj *HTTPFlagRes) Sends() interface{} {
return &HTTPFlagSends{
func (obj *HTTPServerFlagRes) Sends() interface{} {
return &HTTPServerFlagSends{
Value: nil,
}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
// primarily useful for setting the defaults.
func (obj *HTTPFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPFlagRes // indirection to avoid infinite recursion
func (obj *HTTPServerFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPServerFlagRes // indirection to avoid infinite recursion
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 {
return fmt.Errorf("could not convert to HTTPFlagRes")
return fmt.Errorf("could not convert to HTTPServerFlagRes")
}
raw := rawRes(*res) // convert; the defaults go here
@@ -336,6 +337,6 @@ func (obj *HTTPFlagRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
*obj = HTTPFlagRes(raw) // restore from indirection with type conversion!
*obj = HTTPServerFlagRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -49,43 +49,44 @@ import (
)
const (
httpProxyKind = httpKind + ":proxy"
httpServerProxyKind = httpServerKind + ":proxy"
)
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
// keep things simple for now, we just lumped them all together.
httpProxyRWMutex *sync.RWMutex
httpServerProxyRWMutex *sync.RWMutex
)
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
// http server. The name is used as the public path of the endpoint, unless the
// path field is specified, and in that case it is used instead. The way this
// works is that it autogroups at runtime with an existing http resource, and in
// doing so makes the path associated with this resource available when serving
// files. When something under the path is accessed, this is pulled from the
// backing http server, which makes an http client connection if needed to pull
// the authoritative file down, saves it locally for future use, and then
// returns it to the original http client caller. On a subsequent call, if the
// cache was not invalidated, the file doesn't need to be fetched from the
// network. In effect, this works as a caching http proxy. If you create this as
// a resource which responds to the same type of request as an http:file
// resource or any other kind of resource, it is undefined behaviour which will
// answer the request. The most common clash will happen if both are present at
// the same path. This particular implementation stores some file data in memory
// as a convenience instead of streaming directly to clients. This makes locking
// much easier, but is wasteful. If you plan on using this for huge files and on
// systems with low amounts of memory, you might want to optimize this. The
// resultant proxy path is determined by subtracting the `Sub` field from the
// `Path` (and request path) and then appending the result to the `Head` field.
type HTTPProxyRes struct {
// HTTPServerProxyRes is a resource representing a special path that exists
// within an http server. The name is used as the public path of the endpoint,
// unless the path field is specified, and in that case it is used instead. The
// way this works is that it autogroups at runtime with an existing http server
// resource, and in doing so makes the path associated with this resource
// available when serving files. When something under the path is accessed, this
// is pulled from the backing http server, which makes an http client connection
// if needed to pull the authoritative file down, saves it locally for future
// use, and then returns it to the original http client caller. On a subsequent
// call, if the cache was not invalidated, the file doesn't need to be fetched
// from the network. In effect, this works as a caching http proxy. If you
// create this as a resource which responds to the same type of request as an
// http:server:file resource or any other kind of resource, it is undefined
// behaviour which will answer the request. The most common clash will happen if
// both are present at the same path. This particular implementation stores some
// file data in memory as a convenience instead of streaming directly to
// clients. This makes locking much easier, but is wasteful. If you plan on
// using this for huge files and on systems with low amounts of memory, you
// might want to optimize this. The resultant proxy path is determined by
// subtracting the `Sub` field from the `Path` (and request path) and then
// appending the result to the `Head` field.
type HTTPServerProxyRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can be grouped into HTTPServerRes
@@ -136,13 +137,13 @@ type HTTPProxyRes struct {
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPProxyRes) Default() engine.Res {
return &HTTPProxyRes{}
func (obj *HTTPServerProxyRes) Default() engine.Res {
return &HTTPServerProxyRes{}
}
// getPath returns the actual path we respond to. When Path is not specified, we
// use the Name.
func (obj *HTTPProxyRes) getPath() string {
func (obj *HTTPServerProxyRes) getPath() string {
if 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
// 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
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
if obj.Cache != "" { // check in the cache...
httpProxyRWMutex.Lock()
defer httpProxyRWMutex.Unlock()
httpServerProxyRWMutex.Lock()
defer httpServerProxyRWMutex.Unlock()
// store in cachePath
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
// 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
// the locking so that a new file can be saved in the cache?
httpProxyRWMutex.RLock()
defer httpProxyRWMutex.RUnlock()
httpServerProxyRWMutex.RLock()
defer httpServerProxyRWMutex.RUnlock()
f, err := os.Open(absPath)
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
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPProxyRes) ParentName() string {
func (obj *HTTPServerProxyRes) 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 *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?
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.
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.
if req.Method != http.MethodGet {
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.
func (obj *HTTPProxyRes) Validate() error {
func (obj *HTTPServerProxyRes) Validate() error {
if obj.getPath() == "" {
return fmt.Errorf("empty filename")
}
@@ -449,7 +450,7 @@ func (obj *HTTPProxyRes) Validate() error {
}
// 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.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.
func (obj *HTTPProxyRes) Cleanup() error {
func (obj *HTTPServerProxyRes) 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 *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
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.
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 {
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.
func (obj *HTTPProxyRes) Cmp(r engine.Res) error {
// we can only compare HTTPProxyRes to others of the same resource kind
res, ok := r.(*HTTPProxyRes)
func (obj *HTTPServerProxyRes) Cmp(r engine.Res) error {
// we can only compare HTTPServerProxyRes to others of the same resource kind
res, ok := r.(*HTTPServerProxyRes)
if !ok {
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
// primarily useful for setting the defaults.
func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPProxyRes // indirection to avoid infinite recursion
func (obj *HTTPServerProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPServerProxyRes // indirection to avoid infinite recursion
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 {
return fmt.Errorf("could not convert to HTTPProxyRes")
return fmt.Errorf("could not convert to HTTPServerProxyRes")
}
raw := rawRes(*res) // convert; the defaults go here
@@ -535,7 +536,7 @@ func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error
return err
}
*obj = HTTPProxyRes(raw) // restore from indirection with type conversion!
*obj = HTTPServerProxyRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -36,7 +36,7 @@ import (
"testing"
)
func TestHttpProxyPathParser0(t *testing.T) {
func TestHttpServerProxyPathParser0(t *testing.T) {
type test struct { // an individual test
fail bool

View File

@@ -40,8 +40,8 @@ import (
"sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources/http_ui/common"
"github.com/purpleidea/mgmt/engine/resources/http_ui/static"
"github.com/purpleidea/mgmt/engine/resources/http_server_ui/common"
"github.com/purpleidea/mgmt/engine/resources/http_server_ui/static"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/errwrap"
@@ -50,30 +50,31 @@ import (
)
const (
httpUIKind = httpKind + ":ui"
httpServerUIKind = httpServerKind + ":ui"
httpUIIndexHTMLTmpl = "index.html.tmpl"
httpServerUIIndexHTMLTmpl = "index.html.tmpl"
)
var (
//go:embed http_ui/index.html.tmpl
httpUIIndexHTMLTmplData string
//go:embed http_server_ui/index.html.tmpl
httpServerUIIndexHTMLTmplData string
//go:embed http_ui/wasm_exec.js
httpUIWasmExecData []byte
//go:embed http_server_ui/wasm_exec.js
httpServerUIWasmExecData []byte
//go:embed http_ui/main.wasm
httpUIMainWasmData []byte
//go:embed http_server_ui/main.wasm
httpServerUIMainWasmData []byte
)
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
// to allow a resource the ability to be grouped into the http server ui
// resource. As an added safety, the Kind must also begin with "http:ui:", and
// not have more than one colon to avoid accidents of unwanted grouping.
// resource. As an added safety, the Kind must also begin with
// "http:server:ui:", and not have more than one colon to avoid accidents of
// unwanted grouping.
type HTTPServerUIGroupableRes interface {
engine.Res
@@ -109,8 +110,9 @@ type HTTPServerUIGroupableRes interface {
GetSort() string
}
// HTTPUIResData represents some additional data to attach to the resource.
type HTTPUIResData struct {
// HTTPServerUIResData represents some additional data to attach to the
// resource.
type HTTPServerUIResData struct {
// Title is the generated page title that is displayed to the user.
Title string `lang:"title" yaml:"title"`
@@ -121,12 +123,13 @@ type HTTPUIResData struct {
Head string `lang:"head" yaml:"head"`
}
// HTTPUIRes is a web UI resource that exists within an http server. The name is
// used as the public path of the ui, unless the path field is specified, and in
// that case it is used instead. The way this works is that it autogroups at
// runtime with an existing http server resource, and in doing so makes the form
// associated with this resource available for serving from that http server.
type HTTPUIRes struct {
// HTTPServerUIRes is a web UI resource that exists within an http server. The
// name is used as the public path of the ui, unless the path field is
// specified, and in that case it is used instead. The way this works is that it
// autogroups at runtime with an existing http server resource, and in doing so
// makes the form associated with this resource available for serving from that
// http server.
type HTTPServerUIRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can be grouped into HTTPServerRes
@@ -146,7 +149,7 @@ type HTTPUIRes struct {
Path string `lang:"path" yaml:"path"`
// 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
eventsChanMap map[engine.Res]chan error
@@ -162,15 +165,15 @@ type HTTPUIRes struct {
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPUIRes) Default() engine.Res {
return &HTTPUIRes{}
func (obj *HTTPServerUIRes) Default() engine.Res {
return &HTTPServerUIRes{}
}
// getPath returns the actual path we respond to. When Path is not specified, we
// use the Name. Note that this is the handler path that will be seen on the
// root http server, and this ui application might use a querystring and/or POST
// data as well.
func (obj *HTTPUIRes) getPath() string {
func (obj *HTTPServerUIRes) getPath() string {
if 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
// to achieve using our parent prefix.
func (obj *HTTPUIRes) routerPath(p string) string {
func (obj *HTTPServerUIRes) routerPath(p string) string {
if strings.HasPrefix(p, "/") {
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
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPUIRes) ParentName() string {
func (obj *HTTPServerUIRes) ParentName() string {
return obj.Server
}
// AcceptHTTP determines whether we will respond to this request. Return nil to
// accept, or any error to pass.
func (obj *HTTPUIRes) AcceptHTTP(req *http.Request) error {
func (obj *HTTPServerUIRes) AcceptHTTP(req *http.Request) error {
requestPath := req.URL.Path // TODO: is this what we want here?
//if requestPath != obj.getPath() {
// return fmt.Errorf("unhandled path")
@@ -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
// 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
res, ok := x.(HTTPServerUIGroupableRes) // convert from Res
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.
func (obj *HTTPUIRes) ginLogger() gin.HandlerFunc {
func (obj *HTTPServerUIRes) ginLogger() gin.HandlerFunc {
return func(c *gin.Context) {
//start := time.Now()
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
// 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
m := make(map[string]string)
//m["foo.tmpl"] = "hello from file1" // TODO: add more content?
m[httpUIIndexHTMLTmpl] = httpUIIndexHTMLTmplData // index.html.tmpl
m[httpServerUIIndexHTMLTmpl] = httpServerUIIndexHTMLTmplData // index.html.tmpl
filenames := []string{}
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.
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...
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["version"] = obj.init.Version
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["path"] = obj.getPath()
if obj.Data != nil {
h["title"] = obj.Data.Title // template var
h["head"] = template.HTML(obj.Data.Head)
}
c.HTML(http.StatusOK, httpUIIndexHTMLTmpl, h)
c.HTML(http.StatusOK, httpServerUIIndexHTMLTmpl, h)
})
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) {
// the version of this file has to match compiler version
// the original came from: ~golang/lib/wasm/wasm_exec.js
// XXX: add a test to ensure this matches the compiler version
// the content-type matters or this won't work in the browser
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", httpUIWasmExecData)
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", httpServerUIWasmExecData)
})
if static.HTTPUIStaticEmbedded {
router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapCSS), func(c *gin.Context) {
c.Data(http.StatusOK, "text/css;charset=UTF-8", static.HTTPUIIndexStaticBootstrapCSS)
if static.HTTPServerUIStaticEmbedded {
router.GET(obj.routerPath("/"+static.HTTPServerUIIndexBootstrapCSS), func(c *gin.Context) {
c.Data(http.StatusOK, "text/css;charset=UTF-8", static.HTTPServerUIIndexStaticBootstrapCSS)
})
router.GET(obj.routerPath("/"+static.HTTPUIIndexBootstrapJS), func(c *gin.Context) {
c.Data(http.StatusOK, "text/javascript;charset=UTF-8", static.HTTPUIIndexStaticBootstrapJS)
router.GET(obj.routerPath("/"+static.HTTPServerUIIndexBootstrapJS), func(c *gin.Context) {
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.
func (obj *HTTPUIRes) Validate() error {
func (obj *HTTPServerUIRes) Validate() error {
if obj.getPath() == "" {
return fmt.Errorf("empty path")
}
@@ -510,7 +513,7 @@ func (obj *HTTPUIRes) Validate() error {
}
// 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.eventStream = make(chan error)
@@ -572,7 +575,7 @@ func (obj *HTTPUIRes) Init(init *engine.Init) error {
Recv: engine.GenerateRecvFunc(r), // unused
FilteredGraph: func() (*pgraph.Graph, error) {
panic("FilteredGraph for HTTP:UI not implemented")
panic("FilteredGraph for HTTP:Server:UI not implemented")
},
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.
func (obj *HTTPUIRes) Cleanup() error {
func (obj *HTTPServerUIRes) Cleanup() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events. This
// particular one does absolutely nothing but block until we've received a done
// signal.
func (obj *HTTPUIRes) Watch(ctx context.Context) error {
func (obj *HTTPServerUIRes) Watch(ctx context.Context) error {
multiplexedChan := make(chan error)
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
// 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 {
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.
func (obj *HTTPUIRes) Cmp(r engine.Res) error {
// we can only compare HTTPUIRes to others of the same resource kind
res, ok := r.(*HTTPUIRes)
func (obj *HTTPServerUIRes) Cmp(r engine.Res) error {
// we can only compare HTTPServerUIRes to others of the same resource kind
res, ok := r.(*HTTPServerUIRes)
if !ok {
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
// primarily useful for setting the defaults.
func (obj *HTTPUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPUIRes // indirection to avoid infinite recursion
func (obj *HTTPServerUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPServerUIRes // indirection to avoid infinite recursion
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 {
return fmt.Errorf("could not convert to HTTPUIRes")
return fmt.Errorf("could not convert to HTTPServerUIRes")
}
raw := rawRes(*res) // convert; the defaults go here
@@ -759,14 +762,14 @@ func (obj *HTTPUIRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
*obj = HTTPUIRes(raw) // restore from indirection with type conversion!
*obj = HTTPServerUIRes(raw) // restore from indirection with type conversion!
return nil
}
// GroupCmp returns whether two resources can be grouped together or not. Can
// these two resources be merged, aka, does this resource support doing so? Will
// resource allow itself to be grouped _into_ this obj?
func (obj *HTTPUIRes) GroupCmp(r engine.GroupableRes) error {
func (obj *HTTPServerUIRes) GroupCmp(r engine.GroupableRes) error {
res, ok := r.(HTTPServerUIGroupableRes) // different from what we usually do!
if !ok {
return fmt.Errorf("resource is not the right kind")
@@ -778,17 +781,17 @@ func (obj *HTTPUIRes) GroupCmp(r engine.GroupableRes) error {
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) {
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)
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

View File

@@ -1,6 +1,6 @@
This directory contains the golang wasm source for the `http_ui` resource. It
gets built automatically when you run `make` from the main project root
directory.
This directory contains the golang wasm source for the `http_server_ui`
resource. It gets built automatically when you run `make` from the main project
root directory.
After it gets built, the compiled artifact gets bundled into the main project
binary via go embed.

View File

@@ -28,28 +28,30 @@
// additional permission.
// Package common contains some code that is shared between the wasm and the
// http:ui packages.
// http:server:ui packages.
package common
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.
HTTPUIInputType = "type"
HTTPServerUIInputType = "type"
// HTTPUIInputTypeText is the representation of the html "text" type.
HTTPUIInputTypeText = "text"
// HTTPServerUIInputTypeText is the representation of the html "text"
// type.
HTTPServerUIInputTypeText = "text"
// HTTPUIInputTypeRange is the representation of the html "range" type.
HTTPUIInputTypeRange = "range"
// HTTPServerUIInputTypeRange is the representation of the html "range"
// type.
HTTPServerUIInputTypeRange = "range"
// HTTPUIInputTypeRangeMin is the html input "range" min field.
HTTPUIInputTypeRangeMin = "min"
// HTTPServerUIInputTypeRangeMin is the html input "range" min field.
HTTPServerUIInputTypeRangeMin = "min"
// HTTPUIInputTypeRangeMax is the html input "range" max field.
HTTPUIInputTypeRangeMax = "max"
// HTTPServerUIInputTypeRangeMax is the html input "range" max field.
HTTPServerUIInputTypeRangeMax = "max"
// HTTPUIInputTypeRangeStep is the html input "range" step field.
HTTPUIInputTypeRangeStep = "step"
// HTTPServerUIInputTypeRangeStep is the html input "range" step field.
HTTPServerUIInputTypeRangeStep = "step"
)
// Form represents the entire form containing all the desired elements.

View File

@@ -39,7 +39,7 @@ import (
"syscall/js"
"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"
)
@@ -168,7 +168,7 @@ func (obj *Main) Run() error {
}
//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 {
fmt.Printf("Element has no input type: %+v\n", element)
continue
@@ -185,14 +185,14 @@ func (obj *Main) Run() error {
//el.Call("setAttribute", "name", id)
el.Set("type", inputType)
if inputType == common.HTTPUIInputTypeRange {
if val, exists := x.Type[common.HTTPUIInputTypeRangeMin]; exists {
if inputType == common.HTTPServerUIInputTypeRange {
if val, exists := x.Type[common.HTTPServerUIInputTypeRangeMin]; exists {
el.Set("min", val)
}
if val, exists := x.Type[common.HTTPUIInputTypeRangeMax]; exists {
if val, exists := x.Type[common.HTTPServerUIInputTypeRangeMax]; exists {
el.Set("max", val)
}
if val, exists := x.Type[common.HTTPUIInputTypeRangeStep]; exists {
if val, exists := x.Type[common.HTTPServerUIInputTypeRangeStep]; exists {
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.
//go:build httpuistatic
//go:build httpserveruistatic
package static
@@ -36,16 +36,19 @@ import (
)
const (
// HTTPUIStaticEmbedded specifies whether files have been embedded.
HTTPUIStaticEmbedded = true
// HTTPServerUIStaticEmbedded specifies whether files have been
// embedded.
HTTPServerUIStaticEmbedded = true
)
var (
// HTTPUIIndexStaticBootstrapCSS is the embedded data. It is embedded.
//go:embed http_ui/static/bootstrap.min.css
HTTPUIIndexStaticBootstrapCSS []byte
// HTTPServerUIIndexStaticBootstrapCSS is the embedded data. It is
// embedded.
//go:embed http_server_ui/static/bootstrap.min.css
HTTPServerUIIndexStaticBootstrapCSS []byte
// HTTPUIIndexStaticBootstrapJS is the embedded data. It is embedded.
//go:embed http_ui/static/bootstrap.bundle.min.js
HTTPUIIndexStaticBootstrapJS []byte
// HTTPServerUIIndexStaticBootstrapJS is the embedded data. It is
// embedded.
//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.
//go:build !httpuistatic
//go:build !httpserveruistatic
package static
const (
// HTTPUIStaticEmbedded specifies whether files have been embedded.
HTTPUIStaticEmbedded = false
// HTTPServerUIStaticEmbedded specifies whether files have been
// embedded.
HTTPServerUIStaticEmbedded = false
)
var (
// HTTPUIIndexStaticBootstrapCSS is the embedded data. It is empty here.
HTTPUIIndexStaticBootstrapCSS []byte
// HTTPServerUIIndexStaticBootstrapCSS is the embedded data. It is empty
// here.
HTTPServerUIIndexStaticBootstrapCSS []byte
// HTTPUIIndexStaticBootstrapJS is the embedded data. It is empty here.
HTTPUIIndexStaticBootstrapJS []byte
// HTTPServerUIIndexStaticBootstrapJS is the embedded data. It is empty
// here.
HTTPServerUIIndexStaticBootstrapJS []byte
)

View File

@@ -32,11 +32,11 @@
package static
const (
// HTTPUIIndexBootstrapCSS is the path to the bootstrap css file when
// embedded, relative to the parent directory.
HTTPUIIndexBootstrapCSS = "static/bootstrap.min.css"
// HTTPServerUIIndexBootstrapCSS is the path to the bootstrap css file
// when embedded, relative to the parent directory.
HTTPServerUIIndexBootstrapCSS = "static/bootstrap.min.css"
// HTTPUIIndexBootstrapJS is the path to the bootstrap js file when
// embedded, relative to the parent directory.
HTTPUIIndexBootstrapJS = "static/bootstrap.bundle.min.js"
// HTTPServerUIIndexBootstrapJS is the path to the bootstrap js file
// when embedded, relative to the parent directory.
HTTPServerUIIndexBootstrapJS = "static/bootstrap.bundle.min.js"
)

View File

@@ -37,37 +37,37 @@ import (
"sync"
"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/util/errwrap"
)
const (
httpUIInputKind = httpUIKind + ":input"
httpServerUIInputKind = httpServerUIKind + ":input"
httpUIInputStoreKey = "key"
httpUIInputStoreSchemeLocal = "local"
httpUIInputStoreSchemeWorld = "world"
httpServerUIInputStoreKey = "key"
httpServerUIInputStoreSchemeLocal = "local"
httpServerUIInputStoreSchemeWorld = "world"
httpUIInputTypeText = common.HTTPUIInputTypeText // "text"
httpUIInputTypeRange = common.HTTPUIInputTypeRange // "range"
httpServerUIInputTypeText = common.HTTPServerUIInputTypeText // "text"
httpServerUIInputTypeRange = common.HTTPServerUIInputTypeRange // "range"
)
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
// exists within an http server. The name is used as the unique id of the field,
// unless the id field is specified, and in that case it is used instead. The
// way this works is that it autogroups at runtime with an existing http:ui
// resource, and in doing so makes the form field associated with this resource
// available as part of that ui which is itself grouped and served from the http
// server resource.
type HTTPUIInputRes struct {
// HTTPServerUIInputRes is a form element that exists within a http:server:ui
// resource, which exists within an http server. The name is used as the unique
// id of the field, unless the id field is specified, and in that case it is
// used instead. The way this works is that it autogroups at runtime with an
// existing http:server:ui resource, and in doing so makes the form field
// associated with this resource available as part of that ui which is itself
// grouped and served from the http server resource.
type HTTPServerUIInputRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable // XXX: add autoedge support
traits.Groupable // can be grouped into HTTPUIRes
traits.Groupable // can be grouped into HTTPServerUIRes
traits.Sendable
init *engine.Init
@@ -119,14 +119,14 @@ type HTTPUIInputRes struct {
}
// Default returns some sensible defaults for this resource.
func (obj *HTTPUIInputRes) Default() engine.Res {
return &HTTPUIInputRes{
func (obj *HTTPServerUIInputRes) Default() engine.Res {
return &HTTPServerUIInputRes{
Type: "text://",
}
}
// Validate checks if the resource data structure was populated correctly.
func (obj *HTTPUIInputRes) Validate() error {
func (obj *HTTPServerUIInputRes) Validate() error {
if obj.GetID() == "" {
return fmt.Errorf("empty id")
}
@@ -149,7 +149,7 @@ func (obj *HTTPUIInputRes) Validate() error {
}
// 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
u, err := url.Parse(obj.Type)
@@ -159,7 +159,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
if u == nil {
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)
}
values, err := url.ParseQuery(u.RawQuery)
@@ -177,7 +177,7 @@ func (obj *HTTPUIInputRes) Init(init *engine.Init) error {
if u == nil {
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)
}
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.key = obj.Name() // default
x, exists := values[httpUIInputStoreKey]
x, exists := values[httpServerUIInputStoreKey]
if exists && len(x) > 0 && x[0] != "" { // ignore absent or broken keys
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.
func (obj *HTTPUIInputRes) Cleanup() error {
func (obj *HTTPServerUIInputRes) Cleanup() error {
return nil
}
// getKey returns the key to be used for this resource. If the Store field is
// specified, it will use that parsed part, otherwise it uses the Name.
func (obj *HTTPUIInputRes) getKey() string {
func (obj *HTTPServerUIInputRes) getKey() string {
if obj.Store != "" {
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
// empty then it's ignored, otherwise it must match the Name of the parent to
// get grouped.
func (obj *HTTPUIInputRes) ParentName() string {
func (obj *HTTPServerUIInputRes) ParentName() string {
return obj.Path
}
// 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"
// 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
// the Name.
func (obj *HTTPUIInputRes) GetID() string {
func (obj *HTTPServerUIInputRes) GetID() string {
if 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
// 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 {
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.
func (obj *HTTPUIInputRes) setValue(ctx context.Context, val string) error {
func (obj *HTTPServerUIInputRes) setValue(ctx context.Context, val string) error {
obj.value = val
select {
@@ -270,7 +270,7 @@ func (obj *HTTPUIInputRes) setValue(ctx context.Context, val string) error {
return nil
}
func (obj *HTTPUIInputRes) checkValue(value string) error {
func (obj *HTTPServerUIInputRes) checkValue(value string) error {
// XXX: validate based on obj.Type
// XXX: validate what kind of values are allowed, probably no \n, etc...
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
// our html form.
func (obj *HTTPUIInputRes) GetValue(ctx context.Context) (string, error) {
func (obj *HTTPServerUIInputRes) GetValue(ctx context.Context) (string, error) {
obj.mutex.Lock()
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.
func (obj *HTTPUIInputRes) GetType() map[string]string {
func (obj *HTTPServerUIInputRes) GetType() map[string]string {
m := make(map[string]string)
if obj.typeURL.Scheme == httpUIInputTypeRange {
if obj.typeURL.Scheme == httpServerUIInputTypeRange {
m = obj.rangeGetType()
}
m[common.HTTPUIInputType] = obj.typeURL.Scheme
m[common.HTTPServerUIInputType] = obj.typeURL.Scheme
return m
}
func (obj *HTTPUIInputRes) rangeGetType() map[string]string {
func (obj *HTTPServerUIInputRes) rangeGetType() map[string]string {
m := make(map[string]string)
base := 10
bits := 64
if sa, exists := obj.typeURLValues[common.HTTPUIInputTypeRangeMin]; exists && len(sa) > 0 {
if sa, exists := obj.typeURLValues[common.HTTPServerUIInputTypeRangeMin]; exists && len(sa) > 0 {
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 {
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 {
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
// display order of all the elements in a ui.
func (obj *HTTPUIInputRes) GetSort() string {
func (obj *HTTPServerUIInputRes) GetSort() string {
return obj.Sort
}
// Watch is the primary listener for this resource and it outputs events. This
// particular one does absolutely nothing but block until we've received a done
// signal.
func (obj *HTTPUIInputRes) Watch(ctx context.Context) error {
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
func (obj *HTTPServerUIInputRes) Watch(ctx context.Context) error {
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
return obj.localWatch(ctx)
}
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
return obj.worldWatch(ctx)
}
@@ -363,7 +363,7 @@ func (obj *HTTPUIInputRes) Watch(ctx context.Context) error {
return nil
}
func (obj *HTTPUIInputRes) localWatch(ctx context.Context) error {
func (obj *HTTPServerUIInputRes) localWatch(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
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)
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
// can fail, but only if the send portion fails for some reason. If we're using
// the Store feature, then it also reads and writes to and from that store.
func (obj *HTTPUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
func (obj *HTTPServerUIInputRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
if obj.init.Debug {
obj.init.Logf("CheckApply")
}
// If we're in ".Value" mode, we want to look at the incoming value, and
// send it onwards. This function mostly exists as a stub in this case.
// The private value gets set by obj.SetValue from the http:ui parent.
// If we're in ".Store" mode, then we're reconciling between the "World"
// and the http:ui "Web".
// The private value gets set by obj.SetValue from the http:server:ui
// parent. If we're in ".Store" mode, then we're reconciling between the
// "World" and the http:server:ui "Web".
if obj.Store != "" {
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()
value := obj.value // gets set by obj.SetValue
obj.mutex.Unlock()
if obj.last != nil && *obj.last == value {
if err := obj.init.Send(&HTTPUIInputSends{
if err := obj.init.Send(&HTTPServerUIInputSends{
Value: &value,
}); err != nil {
return false, err
@@ -474,7 +474,7 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo
}
if !apply {
if err := obj.init.Send(&HTTPUIInputSends{
if err := obj.init.Send(&HTTPServerUIInputSends{
Value: &value, // XXX: arbitrary since we're in noop mode
}); err != nil {
return false, err
@@ -489,7 +489,7 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo
obj.init.Logf("sending: %s", value)
// send
if err := obj.init.Send(&HTTPUIInputSends{
if err := obj.init.Send(&HTTPServerUIInputSends{
Value: &value,
}); err != nil {
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
// between a third-party changing the value in the World database, and a recent
// "http:ui" change by and end user. Basically whoever runs last is the "right"
// value that we want to use. We know who sent the event from reading the
// storeEvent variable, and if it was the World, we want to cache it locally,
// and if it was the Web, then we want to push it up to the store.
func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) {
// "http:server:ui" change by an end user. Basically whoever runs last is the
// "right" value that we want to use. We know who sent the event from reading
// the storeEvent variable, and if it was the World, we want to cache it
// locally, and if it was the Web, then we want to push it up to the store.
func (obj *HTTPServerUIInputRes) storeCheckApply(ctx context.Context, apply bool) (bool, error) {
v1, exists, err := obj.storeGet(ctx, obj.getKey())
if err != nil {
@@ -519,7 +519,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
obj.mutex.Unlock()
if exists && v1 == v2 { // both sides are happy
if err := obj.init.Send(&HTTPUIInputSends{
if err := obj.init.Send(&HTTPServerUIInputSends{
Value: &v2,
}); err != nil {
return false, err
@@ -528,7 +528,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
}
if !apply {
if err := obj.init.Send(&HTTPUIInputSends{
if err := obj.init.Send(&HTTPServerUIInputSends{
Value: &v2, // XXX: arbitrary since we're in noop mode
}); err != nil {
return false, err
@@ -555,7 +555,7 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
obj.init.Logf("sending: %s", value)
// send
if err := obj.init.Send(&HTTPUIInputSends{
if err := obj.init.Send(&HTTPServerUIInputSends{
Value: &value,
}); err != nil {
return false, err
@@ -564,8 +564,8 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
return false, nil
}
func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) {
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeLocal {
func (obj *HTTPServerUIInputRes) storeGet(ctx context.Context, key string) (string, bool, error) {
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeLocal {
val, err := obj.init.Local.ValueGet(ctx, key)
if err != nil {
return "", false, err // real error
@@ -581,7 +581,7 @@ func (obj *HTTPUIInputRes) storeGet(ctx context.Context, key string) (string, bo
return s, true, nil
}
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
val, err := obj.init.World.StrGet(ctx, key)
if err != nil && obj.init.World.StrIsNotExist(err) {
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
}
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)
}
if obj.Store != "" && obj.scheme == httpUIInputStoreSchemeWorld {
if obj.Store != "" && obj.scheme == httpServerUIInputStoreSchemeWorld {
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.
func (obj *HTTPUIInputRes) Cmp(r engine.Res) error {
// we can only compare HTTPUIInputRes to others of the same resource kind
res, ok := r.(*HTTPUIInputRes)
func (obj *HTTPServerUIInputRes) Cmp(r engine.Res) error {
// we can only compare HTTPServerUIInputRes to others of the same resource kind
res, ok := r.(*HTTPServerUIInputRes)
if !ok {
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
// primarily useful for setting the defaults.
func (obj *HTTPUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPUIInputRes // indirection to avoid infinite recursion
func (obj *HTTPServerUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes HTTPServerUIInputRes // indirection to avoid infinite recursion
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 {
return fmt.Errorf("could not convert to HTTPUIInputRes")
return fmt.Errorf("could not convert to HTTPServerUIInputRes")
}
raw := rawRes(*res) // convert; the defaults go here
@@ -654,20 +654,20 @@ func (obj *HTTPUIInputRes) UnmarshalYAML(unmarshal func(interface{}) error) erro
return err
}
*obj = HTTPUIInputRes(raw) // restore from indirection with type conversion!
*obj = HTTPServerUIInputRes(raw) // restore from indirection with type conversion!
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.
type HTTPUIInputSends struct {
type HTTPServerUIInputSends struct {
// Value is the text element value being sent.
Value *string `lang:"value"`
}
// Sends represents the default struct of values we can send using Send/Recv.
func (obj *HTTPUIInputRes) Sends() interface{} {
return &HTTPUIInputSends{
func (obj *HTTPServerUIInputRes) Sends() interface{} {
return &HTTPServerUIInputSends{
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...
http:file "/file1" {
http:server:file "/file1" {
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
http:flag "/flag1" {
http:server:flag "/flag1" {
#server => ":8080",
key => "key",
}
@@ -22,8 +22,8 @@ print "print1" {
Meta:autogroup => false,
}
Http:Flag["/flag1"].value -> Print["print1"].msg
Http:Flag["/flag1"].value -> Value["value1"].any
Http:Server:Flag["/flag1"].value -> Print["print1"].msg
Http:Server:Flag["/flag1"].value -> Value["value1"].any
$ret = value.get_str("value1") # name of value resource
$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...
http:file "/file1" {
http:server:file "/file1" {
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
#http:file "/fedora/releases/${version}/Everything/${arch}/os/" {
#http:server:file "/fedora/releases/${version}/Everything/${arch}/os/" {
# 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 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/
#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...
http:file "/file1" {
http:server:file "/file1" {
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
data => struct{
@@ -20,18 +20,18 @@ http:ui "/ui/" {
$text1_id = "text1"
$range1_id = "range1"
http:ui:input $text1_id {
http:server:ui:input $text1_id {
store => "world://",
sort => "a",
}
http:ui:input $range1_id {
http:server:ui:input $range1_id {
store => "world://",
type => "range://?min=0&max=5&step=1",
sort => "b",
}
#Http:Ui:Input[$text1_id].value -> Kv[$text1_id].value
#Http:Server:Ui:Input[$text1_id].value -> Kv[$text1_id].value
#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...
http:file "/file1" {
http:server:file "/file1" {
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
data => struct{
@@ -30,17 +30,17 @@ $range1_val = if $ret2->ready {
} else {
"2" # some default
}
http:ui:input $text1_id {
http:server:ui:input $text1_id {
value => $text1_val, # it passes back into itself!
}
http:ui:input $range1_id {
http:server:ui:input $range1_id {
value => $range1_val,
type => "range://?min=0&max=5&step=1",
sort => "b",
}
Http:Ui:Input[$text1_id].value -> Value[$text1_id].any
Http:Server:Ui:Input[$text1_id].value -> Value[$text1_id].any
value $text1_id {
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...
http:file "/file1" {
http:server:file "/file1" {
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...
http:file "/file2" {
http:server:file "/file2" {
path => $f2,
Depend => File[$f2], # TODO: add autoedges
}
# 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",
}
# 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",
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...
http:file "/file1" {
http:server:file "/file1" {
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!
http:file "/secret/folder/" {
http:server:file "/secret/folder/" {
path => "${root}",
Depend => File["${root}"], # TODO: add autoedges

View File

@@ -211,7 +211,7 @@ class base($config) {
$vardir = local.vardir("provisioner/")
$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!
Before => Print["ready"],
@@ -251,7 +251,7 @@ class base($config) {
gzip "${abs_gz}" {
input => "${abs_tar}",
}
http:file "/mgmt/deploy.tar.gz" {
http:server:file "/mgmt/deploy.tar.gz" {
path => "${abs_gz}",
}
@@ -406,7 +406,7 @@ class base:repo($config) {
#Depend => Pkg[$pkgs],
}
http:file "/${uid}/vmlinuz" { # when using ipxe
http:server:file "/${uid}/vmlinuz" { # when using ipxe
path => $vmlinuz_file, # TODO: add autoedges
#Depend => Pkg[$pkgs],
@@ -433,7 +433,7 @@ class base:repo($config) {
#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
#Depend => Pkg[$pkgs],
@@ -441,15 +441,15 @@ class base:repo($config) {
# this file resource serves the entire rsync directory over http
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,
}
http:file "/fedora/updates/${version}/Everything/${arch}/" {
http:server:file "/fedora/updates/${version}/Everything/${arch}/" {
path => $distroarch_updates_http_prefix,
}
} else {
# same as the above http:file path would have been
http:proxy "/fedora/releases/${version}/Everything/${arch}/os/" {
# same as the above http:server:file path would have been
http:server:proxy "/fedora/releases/${version}/Everything/${arch}/os/" {
sub => "/fedora/", # we remove this from the name!
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...
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!
head => $mirror,
@@ -486,10 +486,10 @@ class base:repo($config) {
#
# 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),
#}
#http:file "/fedora/${uid}/updates.repo" {
#http:server:file "/fedora/${uid}/updates.repo" {
# 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),
}
@@ -740,7 +740,7 @@ class base:host($name, $config) {
gzip "${abs_gz}" {
input => "${abs_tar}",
}
http:file "/mgmt/deploy-${provision_key}.tar.gz" {
http:server:file "/mgmt/deploy-${provision_key}.tar.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"
}
# 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" {
""
} else {
@@ -851,7 +851,7 @@ class base:host($name, $config) {
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),
path => $kickstart_file,
@@ -871,7 +871,7 @@ class base:host($name, $config) {
##$str_true = convert.format_bool(true)
##$str_false = convert.format_bool(false)
#http:flag "${name}" {
#http:server:flag "${name}" {
# key => "done",
# path => "/action/done/mac=${provision_key}",
# #mapped => {$str_true => $str_true, $str_false => $str_false,},

View File

@@ -1,11 +1,11 @@
-- 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" {}
-- OUTPUT --
Edge: http:ui:input[text1] -> kv[kv1] # http:ui:input[text1] -> kv[kv1]
Vertex: http:ui:input[text1]
Edge: http:server:ui:input[text1] -> kv[kv1] # http:server:ui:input[text1] -> kv[kv1]
Vertex: http:server:ui:input[text1]
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 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 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
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
fi

View File

@@ -130,7 +130,7 @@ function reflowed-comments() {
base=$(go list .)
for pkg in `go list -e ./... | grep -v "^${base}/vendor/" | grep -v "^${base}/examples/" | grep -v "^${base}/test/" | grep -v "^${base}/old" | grep -v "^${base}/old/" | grep -v "^${base}/tmp" | grep -v "^${base}/tmp/"`; do
if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_ui" ]; then
if [ "$pkg" = "github.com/purpleidea/mgmt/engine/resources/http_server_ui" ]; then
continue # skip this special main package
fi