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:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
814
engine/resources/http_server.go
Normal file
814
engine/resources/http_server.go
Normal file
@@ -0,0 +1,814 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Additional permission under GNU GPL version 3 section 7
|
||||
//
|
||||
// If you modify this program, or any covered work, by linking or combining it
|
||||
// with embedded mcl code and modules (and that the embedded mcl code and
|
||||
// modules which link with this program, contain a copy of their source code in
|
||||
// the authoritative form) containing parts covered by the terms of any other
|
||||
// license, the licensors of this program grant you additional permission to
|
||||
// convey the resulting work. Furthermore, the licensors of this program grant
|
||||
// the original author, James Shubin, additional permission to update this
|
||||
// additional permission if he deems it necessary to achieve the goals of this
|
||||
// additional permission.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
securefilepath "github.com/cyphar/filepath-securejoin"
|
||||
)
|
||||
|
||||
const (
|
||||
// HTTPUseSecureJoin specifies that we should add in a "secure join" lib
|
||||
// so that we avoid the ../../etc/passwd and symlink problems.
|
||||
HTTPUseSecureJoin = true
|
||||
|
||||
httpServerKind = httpKind + ":server"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource(httpServerKind, func() engine.Res { return &HTTPServerRes{} })
|
||||
}
|
||||
|
||||
// HTTPServerGroupableRes is the interface that you must implement if you want
|
||||
// to allow a resource the ability to be grouped into the http server resource.
|
||||
// As an added safety, the Kind must also begin with "http:", and not have more
|
||||
// than one colon, or it must begin with http:server:, and not have any further
|
||||
// colons to avoid accidents of unwanted grouping.
|
||||
type HTTPServerGroupableRes interface {
|
||||
engine.Res
|
||||
|
||||
// ParentName is used to limit which resources autogroup into this one.
|
||||
// If it's empty then it's ignored, otherwise it must match the Name of
|
||||
// the parent to get grouped.
|
||||
ParentName() string
|
||||
|
||||
// AcceptHTTP determines whether this will respond to this request.
|
||||
// Return nil to accept, or any error to pass. This should be
|
||||
// deterministic (pure) and fast.
|
||||
AcceptHTTP(req *http.Request) error
|
||||
|
||||
// ServeHTTP is the standard HTTP handler that will be used for this.
|
||||
http.Handler // ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
// HTTPServerRes is an http server resource. It serves files, but does not
|
||||
// actually apply any state. The name is used as the address to listen on,
|
||||
// unless the Address field is specified, and in that case it is used instead.
|
||||
// This resource can offer up files for serving that are specified either inline
|
||||
// in this resource by specifying an http root, or as http:server:file resources
|
||||
// which will get autogrouped into this resource at runtime. The two methods can
|
||||
// be combined as well.
|
||||
//
|
||||
// This server also supports autogrouping some more magical resources into it.
|
||||
// For example, the http:server:flag and http:server:ui resources add in magic
|
||||
// endpoints.
|
||||
//
|
||||
// This server is not meant as a featureful replacement for the venerable and
|
||||
// modern httpd servers out there, but rather as a simple, dynamic, integrated
|
||||
// alternative for bootstrapping new machines and clusters in an elegant way.
|
||||
//
|
||||
// TODO: add support for TLS
|
||||
// XXX: Make the http:server:ui resource that functions can read data from!
|
||||
// XXX: The http:server:ui resource can also take in values from those functions
|
||||
type HTTPServerRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable // XXX: add autoedge support
|
||||
traits.Groupable // can have HTTPServerFileRes and others grouped into it
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Address is the listen address to use for the http server. It is
|
||||
// common to use `:80` (the standard) to listen on TCP port 80 on all
|
||||
// addresses.
|
||||
Address string `lang:"address" yaml:"address"`
|
||||
|
||||
// Timeout is the maximum duration in seconds to use for unspecified
|
||||
// timeouts. In other words, when this value is specified, it is used as
|
||||
// the value for the other *Timeout values when they aren't used. Put
|
||||
// another way, this makes it easy to set all the different timeouts
|
||||
// with a single parameter.
|
||||
Timeout *uint64 `lang:"timeout" yaml:"timeout"`
|
||||
|
||||
// ReadTimeout is the maximum duration in seconds for reading during the
|
||||
// http request. If it is zero, then there is no timeout. If this is
|
||||
// unspecified, then the value of Timeout is used instead if it is set.
|
||||
// For more information, see the golang net/http Server documentation.
|
||||
ReadTimeout *uint64 `lang:"read_timeout" yaml:"read_timeout"`
|
||||
|
||||
// WriteTimeout is the maximum duration in seconds for writing during
|
||||
// the http request. If it is zero, then there is no timeout. If this is
|
||||
// unspecified, then the value of Timeout is used instead if it is set.
|
||||
// For more information, see the golang net/http Server documentation.
|
||||
WriteTimeout *uint64 `lang:"write_timeout" yaml:"write_timeout"`
|
||||
|
||||
// ShutdownTimeout is the maximum duration in seconds to wait for the
|
||||
// server to shutdown gracefully before calling Close. By default it is
|
||||
// nice to let client connections terminate gracefully, however it might
|
||||
// take longer than we are willing to wait, particularly if one is long
|
||||
// polling or running a very long download. As a result, you can set a
|
||||
// timeout here. The default is zero which means it will wait
|
||||
// indefinitely. The shutdown process can also be cancelled by the
|
||||
// interrupt handler which this resource supports. If this is
|
||||
// unspecified, then the value of Timeout is used instead if it is set.
|
||||
ShutdownTimeout *uint64 `lang:"shutdown_timeout" yaml:"shutdown_timeout"`
|
||||
|
||||
// Root is the root directory that we should serve files from. If it is
|
||||
// not specified, then it is not used. Any http file resources will have
|
||||
// precedence over anything in here, in case the same path exists twice.
|
||||
// TODO: should we have a flag to determine the precedence rules here?
|
||||
Root string `lang:"root" yaml:"root"`
|
||||
|
||||
// TODO: should we allow adding a list of one-of files directly here?
|
||||
|
||||
eventsChanMap map[engine.Res]chan error
|
||||
interruptChan chan struct{}
|
||||
|
||||
conn net.Listener
|
||||
serveMux *http.ServeMux // can't share the global one between resources!
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *HTTPServerRes) Default() engine.Res {
|
||||
return &HTTPServerRes{}
|
||||
}
|
||||
|
||||
// getAddress returns the actual address to use. When Address is not specified,
|
||||
// we use the Name.
|
||||
func (obj *HTTPServerRes) getAddress() string {
|
||||
if obj.Address != "" {
|
||||
return obj.Address
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
// getReadTimeout determines the value for ReadTimeout, because if unspecified,
|
||||
// this will default to the value of Timeout.
|
||||
func (obj *HTTPServerRes) getReadTimeout() *uint64 {
|
||||
if obj.ReadTimeout != nil {
|
||||
return obj.ReadTimeout
|
||||
}
|
||||
return obj.Timeout // might be nil
|
||||
}
|
||||
|
||||
// getWriteTimeout determines the value for WriteTimeout, because if
|
||||
// unspecified, this will default to the value of Timeout.
|
||||
func (obj *HTTPServerRes) getWriteTimeout() *uint64 {
|
||||
if obj.WriteTimeout != nil {
|
||||
return obj.WriteTimeout
|
||||
}
|
||||
return obj.Timeout // might be nil
|
||||
}
|
||||
|
||||
// getShutdownTimeout determines the value for ShutdownTimeout, because if
|
||||
// unspecified, this will default to the value of Timeout.
|
||||
func (obj *HTTPServerRes) getShutdownTimeout() *uint64 {
|
||||
if obj.ShutdownTimeout != nil {
|
||||
return obj.ShutdownTimeout
|
||||
}
|
||||
return obj.Timeout // might be nil
|
||||
}
|
||||
|
||||
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
||||
// accept, or any error to pass. In this particular case, it accepts for the
|
||||
// Root directory handler, but it happens to be implemented with this signature
|
||||
// in case it gets moved. It doesn't intentionally match the
|
||||
// HTTPServerGroupableRes interface.
|
||||
func (obj *HTTPServerRes) AcceptHTTP(req *http.Request) error {
|
||||
// Look in root if we have one, and we haven't got a file yet...
|
||||
if obj.Root == "" {
|
||||
return fmt.Errorf("no Root directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP is the standard HTTP handler that will be used here. In this
|
||||
// particular case, it serves the Root directory handler, but it happens to be
|
||||
// implemented with this signature in case it gets moved. It doesn't
|
||||
// intentionally match the HTTPServerGroupableRes interface.
|
||||
func (obj *HTTPServerRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// We only allow GET at the moment.
|
||||
if req.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||
|
||||
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
|
||||
if !strings.HasPrefix(p, obj.Root) { // root ends with /
|
||||
// user might have tried a ../../etc/passwd hack
|
||||
obj.init.Logf("join inconsistency: %s", p)
|
||||
http.NotFound(w, req) // lie to them...
|
||||
return
|
||||
}
|
||||
if HTTPUseSecureJoin {
|
||||
var err error
|
||||
p, err = securefilepath.SecureJoin(obj.Root, requestPath)
|
||||
if err != nil {
|
||||
obj.init.Logf("secure join fail: %s", p)
|
||||
http.NotFound(w, req) // lie to them...
|
||||
return
|
||||
}
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Got file at root: %s", p)
|
||||
}
|
||||
|
||||
handle, err := os.Open(p)
|
||||
if err != nil {
|
||||
obj.init.Logf("could not open: %s", p)
|
||||
sendHTTPError(w, err)
|
||||
return
|
||||
}
|
||||
defer handle.Close() // ignore error
|
||||
|
||||
// Determine the last-modified time if we can.
|
||||
modtime := time.Now()
|
||||
fi, err := handle.Stat()
|
||||
if err == nil {
|
||||
modtime = fi.ModTime()
|
||||
}
|
||||
// TODO: if Stat errors, should we fail the whole thing?
|
||||
|
||||
// XXX: is requestPath what we want for the name field?
|
||||
http.ServeContent(w, req, requestPath, modtime, handle)
|
||||
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Validate checks if the resource data structure was populated correctly.
|
||||
func (obj *HTTPServerRes) Validate() error {
|
||||
if obj.getAddress() == "" {
|
||||
return fmt.Errorf("empty address")
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(obj.getAddress())
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress())
|
||||
}
|
||||
if host != "" {
|
||||
// TODO: should we allow fqdn's here?
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("the Address is not a valid IP: %s", host)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Root != "" && !strings.HasPrefix(obj.Root, "/") {
|
||||
return fmt.Errorf("the Root must be absolute")
|
||||
}
|
||||
if obj.Root != "" && !strings.HasSuffix(obj.Root, "/") {
|
||||
return fmt.Errorf("the Root must be a dir")
|
||||
}
|
||||
|
||||
// XXX: validate that the autogrouped resources don't have paths that
|
||||
// conflict with each other. We can only have a single unique entry for
|
||||
// what handles a /whatever URL.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *HTTPServerRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
// No need to error in Validate if Timeout is ignored, but log it.
|
||||
// These are all specified, so Timeout effectively does nothing.
|
||||
a := obj.ReadTimeout != nil
|
||||
b := obj.WriteTimeout != nil
|
||||
c := obj.ShutdownTimeout != nil
|
||||
if obj.Timeout != nil && (a && b && c) {
|
||||
obj.init.Logf("the Timeout param is being ignored")
|
||||
}
|
||||
|
||||
// NOTE: If we don't Init anything that's autogrouped, then it won't
|
||||
// even get an Init call on it.
|
||||
|
||||
obj.eventsChanMap = make(map[engine.Res]chan error)
|
||||
|
||||
// TODO: should we do this in the engine? Do we want to decide it here?
|
||||
for _, res := range obj.GetGroup() { // grouped elements
|
||||
// NOTE: We build a new init, but it's not complete. We only add
|
||||
// what we're planning to use, and we ignore the rest for now...
|
||||
r := res // bind the variable!
|
||||
|
||||
obj.eventsChanMap[r] = make(chan error)
|
||||
event := func() {
|
||||
select {
|
||||
case obj.eventsChanMap[r] <- nil:
|
||||
// send!
|
||||
}
|
||||
// We don't do this here (why?) we instead read from the
|
||||
// above channel and then send on multiplexedChan to the
|
||||
// main loop, where it runs the obj.init.Event function.
|
||||
//obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
|
||||
newInit := &engine.Init{
|
||||
Program: obj.init.Program,
|
||||
Version: obj.init.Version,
|
||||
Hostname: obj.init.Hostname,
|
||||
|
||||
// Watch:
|
||||
Running: event,
|
||||
Event: event,
|
||||
|
||||
// CheckApply:
|
||||
Refresh: func() bool {
|
||||
innerRes, ok := r.(engine.RefreshableRes)
|
||||
if !ok {
|
||||
panic("res does not support the Refreshable trait")
|
||||
}
|
||||
return innerRes.Refresh()
|
||||
},
|
||||
Send: engine.GenerateSendFunc(r),
|
||||
Recv: engine.GenerateRecvFunc(r), // unused
|
||||
|
||||
FilteredGraph: func() (*pgraph.Graph, error) {
|
||||
panic("FilteredGraph for HTTP not implemented")
|
||||
},
|
||||
|
||||
Local: obj.init.Local,
|
||||
World: obj.init.World,
|
||||
//VarDir: obj.init.VarDir, // TODO: wrap this
|
||||
|
||||
Debug: obj.init.Debug,
|
||||
Logf: func(format string, v ...interface{}) {
|
||||
obj.init.Logf(r.String()+": "+format, v...)
|
||||
},
|
||||
}
|
||||
|
||||
if err := res.Init(newInit); err != nil {
|
||||
return errwrap.Wrapf(err, "autogrouped Init failed")
|
||||
}
|
||||
}
|
||||
|
||||
obj.interruptChan = make(chan struct{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup is run by the engine to clean up after the resource is done.
|
||||
func (obj *HTTPServerRes) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *HTTPServerRes) Watch(ctx context.Context) error {
|
||||
// TODO: I think we could replace all this with:
|
||||
//obj.conn, err := net.Listen("tcp", obj.getAddress())
|
||||
// ...but what is the advantage?
|
||||
addr, err := net.ResolveTCPAddr("tcp", obj.getAddress())
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not resolve address")
|
||||
}
|
||||
|
||||
obj.conn, err = net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not start listener")
|
||||
}
|
||||
defer obj.conn.Close()
|
||||
|
||||
obj.serveMux = http.NewServeMux() // do it here in case Watch restarts!
|
||||
// TODO: We could consider having the obj.GetGroup loop here, instead of
|
||||
// essentially having our own "router" API with AcceptHTTP.
|
||||
obj.serveMux.HandleFunc("/", obj.handler())
|
||||
|
||||
readTimeout := uint64(0)
|
||||
if i := obj.getReadTimeout(); i != nil {
|
||||
readTimeout = *i
|
||||
}
|
||||
writeTimeout := uint64(0)
|
||||
if i := obj.getWriteTimeout(); i != nil {
|
||||
writeTimeout = *i
|
||||
}
|
||||
obj.server = &http.Server{
|
||||
Addr: obj.getAddress(),
|
||||
Handler: obj.serveMux,
|
||||
ReadTimeout: time.Duration(readTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(writeTimeout) * time.Second,
|
||||
//MaxHeaderBytes: 1 << 20, XXX: should we add a param for this?
|
||||
}
|
||||
|
||||
multiplexedChan := make(chan error)
|
||||
defer close(multiplexedChan) // closes after everyone below us is finished
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
|
||||
for _, r := range obj.GetGroup() { // grouped elements
|
||||
res := r // optional in newer golang
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(obj.eventsChanMap[res]) // where Watch sends events
|
||||
if err := res.Watch(ctx); err != nil {
|
||||
select {
|
||||
case multiplexedChan <- err:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}()
|
||||
// wait for Watch first Running() call or immediate error...
|
||||
select {
|
||||
case <-obj.eventsChanMap[res]: // triggers on start or on err...
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
var ok bool
|
||||
var err error
|
||||
select {
|
||||
// receive
|
||||
case err, ok = <-obj.eventsChanMap[res]:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// send (multiplex)
|
||||
select {
|
||||
case multiplexedChan <- err:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
// we block until all the children are started first...
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var closeError error
|
||||
closeSignal := make(chan struct{})
|
||||
|
||||
shutdownChan := make(chan struct{}) // server shutdown finished signal
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case <-obj.interruptChan:
|
||||
// TODO: should we bubble up the error from Close?
|
||||
// TODO: do we need a mutex around this Close?
|
||||
obj.server.Close() // kill it quickly!
|
||||
case <-shutdownChan:
|
||||
// let this exit
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(closeSignal)
|
||||
|
||||
err := obj.server.Serve(obj.conn) // blocks until Shutdown() is called!
|
||||
if err == nil || err == http.ErrServerClosed {
|
||||
return
|
||||
}
|
||||
// if this returned on its own, then closeSignal can be used...
|
||||
closeError = errwrap.Wrapf(err, "the server errored")
|
||||
}()
|
||||
|
||||
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
|
||||
// immediately return ErrServerClosed. Make sure the program doesn't
|
||||
// exit and waits instead for Shutdown to return.
|
||||
defer func() {
|
||||
defer close(shutdownChan) // signal that shutdown is finished
|
||||
innerCtx := context.Background()
|
||||
if i := obj.getShutdownTimeout(); i != nil && *i > 0 {
|
||||
var cancel context.CancelFunc
|
||||
innerCtx, cancel = context.WithTimeout(innerCtx, time.Duration(*i)*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
err := obj.server.Shutdown(innerCtx) // shutdown gracefully
|
||||
if err == context.DeadlineExceeded {
|
||||
// TODO: should we bubble up the error from Close?
|
||||
// TODO: do we need a mutex around this Close?
|
||||
obj.server.Close() // kill it now
|
||||
}
|
||||
}()
|
||||
|
||||
startupChan := make(chan struct{})
|
||||
close(startupChan) // send one initial signal
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Looping...")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-startupChan:
|
||||
startupChan = nil
|
||||
send = true
|
||||
|
||||
case err, ok := <-multiplexedChan:
|
||||
if !ok { // shouldn't happen
|
||||
multiplexedChan = nil
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
send = true
|
||||
|
||||
case <-closeSignal: // something shut us down early
|
||||
return closeError
|
||||
|
||||
case <-ctx.Done(): // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||
// It does however check that certain runtime requirements (such as the Root dir
|
||||
// existing if one was specified) are fulfilled. If there are any autogrouped
|
||||
// resources, those will be recursively called so that they can send/recv.
|
||||
func (obj *HTTPServerRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("CheckApply")
|
||||
}
|
||||
|
||||
// XXX: We don't want the initial CheckApply to return true until the
|
||||
// Watch has started up, so we must block here until that's the case...
|
||||
|
||||
// Cheap runtime validation!
|
||||
// XXX: maybe only do this only once to avoid repeated, unnecessary checks?
|
||||
if obj.Root != "" {
|
||||
fileInfo, err := os.Stat(obj.Root)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "can't stat Root dir")
|
||||
}
|
||||
if !fileInfo.IsDir() {
|
||||
return false, fmt.Errorf("the Root path is not a dir")
|
||||
}
|
||||
}
|
||||
|
||||
checkOK := true
|
||||
for _, res := range obj.GetGroup() { // grouped elements
|
||||
if c, err := res.CheckApply(ctx, apply); err != nil {
|
||||
return false, errwrap.Wrapf(err, "autogrouped CheckApply failed")
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
}
|
||||
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *HTTPServerRes) Cmp(r engine.Res) error {
|
||||
// we can only compare HTTPServerRes to others of the same resource kind
|
||||
res, ok := r.(*HTTPServerRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if obj.Address != res.Address {
|
||||
return fmt.Errorf("the Address differs")
|
||||
}
|
||||
|
||||
if (obj.Timeout == nil) != (res.Timeout == nil) { // xor
|
||||
return fmt.Errorf("the Timeout differs")
|
||||
}
|
||||
if obj.Timeout != nil && res.Timeout != nil {
|
||||
if *obj.Timeout != *res.Timeout { // compare the values
|
||||
return fmt.Errorf("the value of Timeout differs")
|
||||
}
|
||||
}
|
||||
if (obj.ReadTimeout == nil) != (res.ReadTimeout == nil) {
|
||||
return fmt.Errorf("the ReadTimeout differs")
|
||||
}
|
||||
if obj.ReadTimeout != nil && res.ReadTimeout != nil {
|
||||
if *obj.ReadTimeout != *res.ReadTimeout {
|
||||
return fmt.Errorf("the value of ReadTimeout differs")
|
||||
}
|
||||
}
|
||||
if (obj.WriteTimeout == nil) != (res.WriteTimeout == nil) {
|
||||
return fmt.Errorf("the WriteTimeout differs")
|
||||
}
|
||||
if obj.WriteTimeout != nil && res.WriteTimeout != nil {
|
||||
if *obj.WriteTimeout != *res.WriteTimeout {
|
||||
return fmt.Errorf("the value of WriteTimeout differs")
|
||||
}
|
||||
}
|
||||
if (obj.ShutdownTimeout == nil) != (res.ShutdownTimeout == nil) {
|
||||
return fmt.Errorf("the ShutdownTimeout differs")
|
||||
}
|
||||
if obj.ShutdownTimeout != nil && res.ShutdownTimeout != nil {
|
||||
if *obj.ShutdownTimeout != *res.ShutdownTimeout {
|
||||
return fmt.Errorf("the value of ShutdownTimeout differs")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We could do this sort of thing to skip checking Timeout when it
|
||||
// is not used, but for the moment, this is overkill and not needed yet.
|
||||
//a := obj.ReadTimeout != nil
|
||||
//b := obj.WriteTimeout != nil
|
||||
//c := obj.ShutdownTimeout != nil
|
||||
//if !(obj.Timeout != nil && (a && b && c)) {
|
||||
// // the Timeout param is not being ignored
|
||||
//}
|
||||
|
||||
if obj.Root != res.Root {
|
||||
return fmt.Errorf("the Root differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interrupt is called to ask the execution of this resource to end early. It
|
||||
// will cause the server Shutdown to end abruptly instead of leading open client
|
||||
// connections terminate gracefully. It does this by causing the server Close
|
||||
// method to run.
|
||||
func (obj *HTTPServerRes) Interrupt() error {
|
||||
close(obj.interruptChan) // this should cause obj.server.Close() to run!
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
|
||||
// TODO: should this copy internal state?
|
||||
func (obj *HTTPServerRes) Copy() engine.CopyableRes {
|
||||
var timeout, readTimeout, writeTimeout, shutdownTimeout *uint64
|
||||
if obj.Timeout != nil {
|
||||
x := *obj.Timeout
|
||||
timeout = &x
|
||||
}
|
||||
if obj.ReadTimeout != nil {
|
||||
x := *obj.ReadTimeout
|
||||
readTimeout = &x
|
||||
}
|
||||
if obj.WriteTimeout != nil {
|
||||
x := *obj.WriteTimeout
|
||||
writeTimeout = &x
|
||||
}
|
||||
if obj.ShutdownTimeout != nil {
|
||||
x := *obj.ShutdownTimeout
|
||||
shutdownTimeout = &x
|
||||
}
|
||||
return &HTTPServerRes{
|
||||
Address: obj.Address,
|
||||
Timeout: timeout,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
ShutdownTimeout: shutdownTimeout,
|
||||
Root: obj.Root,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes HTTPServerRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*HTTPServerRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to HTTPServerRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = HTTPServerRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not. Can
|
||||
// these two resources be merged, aka, does this resource support doing so? Will
|
||||
// resource allow itself to be grouped _into_ this obj?
|
||||
func (obj *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
|
||||
res, ok := r.(HTTPServerGroupableRes) // different from what we usually do!
|
||||
if !ok {
|
||||
return fmt.Errorf("resource is not the right kind")
|
||||
}
|
||||
|
||||
// If the http resource has the parent name field specified, then it
|
||||
// must match against our name field if we want it to group with us.
|
||||
if pn := res.ParentName(); pn != "" && pn != obj.Name() {
|
||||
return fmt.Errorf("resource groups with a different parent name")
|
||||
}
|
||||
|
||||
// http:server:foo is okay, but file or config:etcd is not
|
||||
if !strings.HasPrefix(r.Kind(), httpServerKind+":") {
|
||||
return fmt.Errorf("not one of our children")
|
||||
}
|
||||
|
||||
// http:server:foo is okay, but http:server:foo:bar is not
|
||||
p1 := httpServerKind + ":"
|
||||
s1 := strings.TrimPrefix(r.Kind(), p1)
|
||||
if len(s1) != len(r.Kind()) && strings.Count(s1, ":") > 0 { // has prefix
|
||||
return fmt.Errorf("maximum one resource after `%s` prefix", httpServerKind)
|
||||
}
|
||||
|
||||
//// http:foo is okay, but http:foo:bar is not
|
||||
//p2 := httpServerKind + ":"
|
||||
//s2 := strings.TrimPrefix(r.Kind(), p2)
|
||||
//if len(s2) != len(r.Kind()) && strings.Count(s2, ":") > 0 { // has prefix
|
||||
// return fmt.Errorf("maximum one resource after `%s` prefix", httpServerKind)
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readHandler handles all the incoming download requests from clients.
|
||||
func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
|
||||
// TODO: we could statically pre-compute some stuff here...
|
||||
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Client: %s", req.RemoteAddr)
|
||||
}
|
||||
// TODO: would this leak anything security sensitive in our log?
|
||||
obj.init.Logf("URL: %s", req.URL)
|
||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Path: %s", requestPath)
|
||||
}
|
||||
|
||||
// Look through the autogrouped resources!
|
||||
// TODO: can we improve performance by only searching here once?
|
||||
for _, x := range obj.GetGroup() { // grouped elements
|
||||
res, ok := x.(HTTPServerGroupableRes) // convert from Res
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Got grouped resource: %s", res.String())
|
||||
}
|
||||
|
||||
err := res.AcceptHTTP(req)
|
||||
if err == nil {
|
||||
res.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Could not serve: %+v", err)
|
||||
}
|
||||
|
||||
//continue // not me
|
||||
}
|
||||
|
||||
// Look in root if we have one, and we haven't got a file yet...
|
||||
err := obj.AcceptHTTP(req)
|
||||
if err == nil {
|
||||
obj.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Could not serve Root: %+v", err)
|
||||
}
|
||||
|
||||
// We never found something to serve...
|
||||
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
||||
obj.init.Logf("File not found: %s", requestPath)
|
||||
}
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
337
engine/resources/http_server_file.go
Normal file
337
engine/resources/http_server_file.go
Normal file
@@ -0,0 +1,337 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Additional permission under GNU GPL version 3 section 7
|
||||
//
|
||||
// If you modify this program, or any covered work, by linking or combining it
|
||||
// with embedded mcl code and modules (and that the embedded mcl code and
|
||||
// modules which link with this program, contain a copy of their source code in
|
||||
// the authoritative form) containing parts covered by the terms of any other
|
||||
// license, the licensors of this program grant you additional permission to
|
||||
// convey the resulting work. Furthermore, the licensors of this program grant
|
||||
// the original author, James Shubin, additional permission to update this
|
||||
// additional permission if he deems it necessary to achieve the goals of this
|
||||
// additional permission.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util/safepath"
|
||||
)
|
||||
|
||||
const (
|
||||
httpServerFileKind = httpServerKind + ":file"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource(httpServerFileKind, func() engine.Res { return &HTTPServerFileRes{} })
|
||||
}
|
||||
|
||||
// HTTPServerFileRes is a file that exists within an http server. The name is
|
||||
// used as the public path of the file, unless the filename field is specified,
|
||||
// and in that case it is used instead. The way this works is that it autogroups
|
||||
// at runtime with an existing http resource, and in doing so makes the file
|
||||
// associated with this resource available for serving from that http server.
|
||||
type HTTPServerFileRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable // XXX: add autoedge support
|
||||
traits.Groupable // can be grouped into HTTPServerRes
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Server is the name of the http server resource to group this into. If
|
||||
// it is omitted, and there is only a single http resource, then it will
|
||||
// be grouped into it automatically. If there is more than one main http
|
||||
// resource being used, then the grouping behaviour is *undefined* when
|
||||
// this is not specified, and it is not recommended to leave this blank!
|
||||
Server string `lang:"server" yaml:"server"`
|
||||
|
||||
// Filename is the name of the file this data should appear as on the
|
||||
// http server.
|
||||
Filename string `lang:"filename" yaml:"filename"`
|
||||
|
||||
// Path is the absolute path to a file that should be used as the source
|
||||
// for this file resource. It must not be combined with the data field.
|
||||
// If this corresponds to a directory, then it will used as a root dir
|
||||
// that will be served as long as the resource name or Filename are also
|
||||
// a directory ending with a slash.
|
||||
Path string `lang:"path" yaml:"path"`
|
||||
|
||||
// Data is the file content that should be used as the source for this
|
||||
// file resource. It must not be combined with the path field.
|
||||
// TODO: should this be []byte instead?
|
||||
Data string `lang:"data" yaml:"data"`
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *HTTPServerFileRes) Default() engine.Res {
|
||||
return &HTTPServerFileRes{}
|
||||
}
|
||||
|
||||
// getPath returns the actual path we respond to. When Filename is not
|
||||
// specified, we use the Name. Note that this is the filename that will be seen
|
||||
// on the http server, it is *not* the source path to the actual file contents
|
||||
// being sent by the server.
|
||||
func (obj *HTTPServerFileRes) getPath() string {
|
||||
if obj.Filename != "" {
|
||||
return obj.Filename
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
// getContent returns the content that we expect from this resource. It depends
|
||||
// on whether the user specified the Path or Data fields, and whether the Path
|
||||
// exists or not.
|
||||
func (obj *HTTPServerFileRes) getContent(requestPath safepath.AbsPath) (io.ReadSeeker, error) {
|
||||
if obj.Path != "" && obj.Data != "" {
|
||||
// programming error! this should have been caught in Validate!
|
||||
return nil, fmt.Errorf("must not specify Path and Data")
|
||||
}
|
||||
|
||||
if obj.Data != "" {
|
||||
return bytes.NewReader([]byte(obj.Data)), nil
|
||||
}
|
||||
|
||||
absFile, err := obj.getContentRelative(requestPath)
|
||||
if err != nil { // on error, we just assume no root/prefix stuff happens
|
||||
return os.Open(obj.Path)
|
||||
}
|
||||
|
||||
return os.Open(absFile.Path())
|
||||
}
|
||||
|
||||
// getContentRelative takes a request, and returns the absolute path to the file
|
||||
// that we want to request, if it's safely under what we can provide.
|
||||
func (obj *HTTPServerFileRes) getContentRelative(requestPath safepath.AbsPath) (safepath.AbsFile, error) {
|
||||
// the location on disk of the data
|
||||
srcPath, err := safepath.SmartParseIntoPath(obj.Path) // (safepath.Path, error)
|
||||
if err != nil {
|
||||
return safepath.AbsFile{}, err
|
||||
}
|
||||
srcAbsDir, ok := srcPath.(safepath.AbsDir)
|
||||
if !ok {
|
||||
return safepath.AbsFile{}, fmt.Errorf("the Path is not an abs dir")
|
||||
}
|
||||
|
||||
// the public path we respond to (might be a dir prefix or just a file)
|
||||
pubPath, err := safepath.SmartParseIntoPath(obj.getPath()) // (safepath.Path, error)
|
||||
if err != nil {
|
||||
return safepath.AbsFile{}, err
|
||||
}
|
||||
pubAbsDir, ok := pubPath.(safepath.AbsDir)
|
||||
if !ok {
|
||||
return safepath.AbsFile{}, fmt.Errorf("the name is not an abs dir")
|
||||
}
|
||||
|
||||
// is the request underneath what we're providing?
|
||||
if !safepath.HasPrefix(requestPath, pubAbsDir) {
|
||||
return safepath.AbsFile{}, fmt.Errorf("wrong prefix")
|
||||
}
|
||||
|
||||
// make the delta
|
||||
delta, err := safepath.StripPrefix(requestPath, pubAbsDir) // (safepath.Path, error)
|
||||
if err != nil {
|
||||
return safepath.AbsFile{}, err
|
||||
}
|
||||
relFile, ok := delta.(safepath.RelFile)
|
||||
if !ok {
|
||||
return safepath.AbsFile{}, fmt.Errorf("the delta is not a rel file")
|
||||
}
|
||||
|
||||
return safepath.JoinToAbsFile(srcAbsDir, relFile), nil // AbsFile
|
||||
}
|
||||
|
||||
// ParentName is used to limit which resources autogroup into this one. If it's
|
||||
// empty then it's ignored, otherwise it must match the Name of the parent to
|
||||
// get grouped.
|
||||
func (obj *HTTPServerFileRes) ParentName() string {
|
||||
return obj.Server
|
||||
}
|
||||
|
||||
// AcceptHTTP determines whether we will respond to this request. Return nil to
|
||||
// accept, or any error to pass.
|
||||
func (obj *HTTPServerFileRes) AcceptHTTP(req *http.Request) error {
|
||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||
|
||||
if strings.HasSuffix(obj.Path, "/") { // a dir!
|
||||
if strings.HasPrefix(requestPath, obj.getPath()) {
|
||||
// relative dir root
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if requestPath != obj.getPath() {
|
||||
return fmt.Errorf("unhandled path")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP is the standard HTTP handler that will be used here.
|
||||
func (obj *HTTPServerFileRes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// We only allow GET at the moment.
|
||||
if req.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||
|
||||
absPath, err := safepath.ParseIntoAbsPath(requestPath)
|
||||
if err != nil {
|
||||
obj.init.Logf("invalid input path: %s", requestPath)
|
||||
sendHTTPError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
handle, err := obj.getContent(absPath)
|
||||
if err != nil {
|
||||
obj.init.Logf("could not get content for: %s", requestPath)
|
||||
sendHTTPError(w, err)
|
||||
return
|
||||
}
|
||||
//if readSeekCloser, ok := handle.(io.ReadSeekCloser); ok { // same
|
||||
// defer readSeekCloser.Close() // ignore error
|
||||
//}
|
||||
if closer, ok := handle.(io.Closer); ok {
|
||||
defer closer.Close() // ignore error
|
||||
}
|
||||
|
||||
// Determine the last-modified time if we can.
|
||||
modtime := time.Now()
|
||||
if f, ok := handle.(*os.File); ok {
|
||||
fi, err := f.Stat()
|
||||
if err == nil {
|
||||
modtime = fi.ModTime()
|
||||
}
|
||||
// TODO: if Stat errors, should we fail the whole thing?
|
||||
}
|
||||
|
||||
// XXX: is requestPath what we want for the name field?
|
||||
http.ServeContent(w, req, requestPath, modtime, handle)
|
||||
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
|
||||
}
|
||||
|
||||
// Validate checks if the resource data structure was populated correctly.
|
||||
func (obj *HTTPServerFileRes) Validate() error {
|
||||
if obj.getPath() == "" {
|
||||
return fmt.Errorf("empty filename")
|
||||
}
|
||||
// FIXME: does getPath need to start with a slash?
|
||||
|
||||
if obj.Path != "" && !strings.HasPrefix(obj.Path, "/") {
|
||||
return fmt.Errorf("the Path must be absolute")
|
||||
}
|
||||
|
||||
if obj.Path != "" && obj.Data != "" {
|
||||
return fmt.Errorf("must not specify Path and Data")
|
||||
}
|
||||
|
||||
// NOTE: if obj.Path == "" && obj.Data == "" then we have an empty file!
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *HTTPServerFileRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup is run by the engine to clean up after the resource is done.
|
||||
func (obj *HTTPServerFileRes) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events. This
|
||||
// particular one does absolutely nothing but block until we've received a done
|
||||
// signal.
|
||||
func (obj *HTTPServerFileRes) Watch(ctx context.Context) error {
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
select {
|
||||
case <-ctx.Done(): // closed by the engine to signal shutdown
|
||||
}
|
||||
|
||||
//obj.init.Event() // notify engine of an event (this can block)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||
func (obj *HTTPServerFileRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("CheckApply")
|
||||
}
|
||||
|
||||
return true, nil // always succeeds, with nothing to do!
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *HTTPServerFileRes) Cmp(r engine.Res) error {
|
||||
// we can only compare HTTPServerFileRes to others of the same resource kind
|
||||
res, ok := r.(*HTTPServerFileRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if obj.Server != res.Server {
|
||||
return fmt.Errorf("the Server field differs")
|
||||
}
|
||||
if obj.Filename != res.Filename {
|
||||
return fmt.Errorf("the Filename differs")
|
||||
}
|
||||
if obj.Path != res.Path {
|
||||
return fmt.Errorf("the Path differs")
|
||||
}
|
||||
if obj.Data != res.Data {
|
||||
return fmt.Errorf("the Data differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *HTTPServerFileRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes HTTPServerFileRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*HTTPServerFileRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to HTTPServerFileRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = HTTPServerFileRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
@@ -41,23 +41,23 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
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
|
||||
def := obj.Default() // get the default
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
def := obj.Default() // get the default
|
||||
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
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHttpProxyPathParser0(t *testing.T) {
|
||||
func TestHttpServerProxyPathParser0(t *testing.T) {
|
||||
|
||||
type test struct { // an individual test
|
||||
fail bool
|
||||
@@ -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["title"] = "" // key must be specified
|
||||
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
|
||||
def := obj.Default() // get the default
|
||||
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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
def := obj.Default() // get the default
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user