Matthew Holt
6 years ago
commit
859b5d7ea3
6 changed files with 360 additions and 0 deletions
@ -0,0 +1 @@ |
|||
_gitignore/ |
@ -0,0 +1,130 @@ |
|||
package caddy2 |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"io" |
|||
"log" |
|||
"net" |
|||
"net/http" |
|||
"strings" |
|||
"sync" |
|||
) |
|||
|
|||
var ( |
|||
cfgEndptSrv *http.Server |
|||
cfgEndptSrvMu sync.Mutex |
|||
) |
|||
|
|||
// Start starts Caddy's administration endpoint.
|
|||
func Start(addr string) error { |
|||
cfgEndptSrvMu.Lock() |
|||
defer cfgEndptSrvMu.Unlock() |
|||
|
|||
ln, err := net.Listen("tcp", addr) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
mux := http.NewServeMux() |
|||
mux.HandleFunc("/load", handleLoadConfig) |
|||
|
|||
for _, m := range GetModules("admin") { |
|||
moduleValue, err := m.New() |
|||
if err != nil { |
|||
return fmt.Errorf("initializing module '%s': %v", m.Name, err) |
|||
} |
|||
route := moduleValue.(AdminRoute) |
|||
mux.Handle(route.Pattern, route) |
|||
} |
|||
|
|||
cfgEndptSrv = &http.Server{ |
|||
Handler: mux, |
|||
} |
|||
|
|||
go cfgEndptSrv.Serve(ln) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// AdminRoute represents a route for the admin endpoint.
|
|||
type AdminRoute struct { |
|||
http.Handler |
|||
Pattern string |
|||
} |
|||
|
|||
// Stop stops the API endpoint.
|
|||
func Stop() error { |
|||
cfgEndptSrvMu.Lock() |
|||
defer cfgEndptSrvMu.Unlock() |
|||
|
|||
if cfgEndptSrv == nil { |
|||
return fmt.Errorf("no server") |
|||
} |
|||
|
|||
err := cfgEndptSrv.Shutdown(context.Background()) // TODO
|
|||
if err != nil { |
|||
return fmt.Errorf("shutting down server: %v", err) |
|||
} |
|||
|
|||
cfgEndptSrv = nil |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func handleLoadConfig(w http.ResponseWriter, r *http.Request) { |
|||
if r.Method != "POST" { |
|||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) |
|||
return |
|||
} |
|||
|
|||
if !strings.Contains(r.Header.Get("Content-Type"), "/json") { |
|||
http.Error(w, "unacceptable Content-Type", http.StatusBadRequest) |
|||
return |
|||
} |
|||
|
|||
err := Load(r.Body) |
|||
if err != nil { |
|||
log.Printf("[ADMIN][ERROR] loading config: %v", err) |
|||
http.Error(w, err.Error(), http.StatusBadRequest) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// Load loads a configuration.
|
|||
func Load(r io.Reader) error { |
|||
gc := globalConfig{modules: make(map[string]interface{})} |
|||
err := json.NewDecoder(r).Decode(&gc) |
|||
if err != nil { |
|||
return fmt.Errorf("decoding config: %v", err) |
|||
} |
|||
|
|||
for modName, rawMsg := range gc.Modules { |
|||
mod, ok := modules[modName] |
|||
if !ok { |
|||
return fmt.Errorf("unrecognized module: %s", modName) |
|||
} |
|||
|
|||
if mod.New != nil { |
|||
val, err := mod.New() |
|||
if err != nil { |
|||
return fmt.Errorf("initializing module '%s': %v", modName, err) |
|||
} |
|||
err = json.Unmarshal(rawMsg, &val) |
|||
if err != nil { |
|||
return fmt.Errorf("decoding module config: %s: %v", modName, err) |
|||
} |
|||
gc.modules[modName] = val |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
type globalConfig struct { |
|||
TestVal string `json:"testval"` |
|||
Modules map[string]json.RawMessage `json:"modules"` |
|||
TestArr []string `json:"test_arr"` |
|||
modules map[string]interface{} |
|||
} |
@ -0,0 +1,21 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"log" |
|||
|
|||
"bitbucket.org/lightcodelabs/caddy2" |
|||
|
|||
// this is where modules get plugged in
|
|||
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" |
|||
_ "bitbucket.org/lightcodelabs/dynamicconfig" |
|||
) |
|||
|
|||
func main() { |
|||
err := caddy2.Start("127.0.0.1:1234") |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
defer caddy2.Stop() |
|||
|
|||
select {} |
|||
} |
@ -0,0 +1,110 @@ |
|||
package caddy2 |
|||
|
|||
import ( |
|||
"fmt" |
|||
"sort" |
|||
"strings" |
|||
"sync" |
|||
) |
|||
|
|||
// Module is a module.
|
|||
type Module struct { |
|||
Name string |
|||
New func() (interface{}, error) |
|||
} |
|||
|
|||
func (m Module) String() string { return m.Name } |
|||
|
|||
// RegisterModule registers a module.
|
|||
func RegisterModule(mod Module) error { |
|||
modulesMu.Lock() |
|||
defer modulesMu.Unlock() |
|||
|
|||
if _, ok := modules[mod.Name]; ok { |
|||
return fmt.Errorf("module already registered: %s", mod.Name) |
|||
} |
|||
modules[mod.Name] = mod |
|||
return nil |
|||
} |
|||
|
|||
// GetModule returns a module by name.
|
|||
func GetModule(name string) (Module, error) { |
|||
modulesMu.Lock() |
|||
defer modulesMu.Unlock() |
|||
|
|||
m, ok := modules[name] |
|||
if !ok { |
|||
return Module{}, fmt.Errorf("module not registered: %s", name) |
|||
} |
|||
return m, nil |
|||
} |
|||
|
|||
// GetModules returns all modules in the given scope/namespace.
|
|||
// For example, a scope of "foo" returns modules named "foo.bar",
|
|||
// "foo.lor", but not "bar", "foo.bar.lor", etc. An empty scope
|
|||
// returns top-level modules, for example "foo" or "bar". Partial
|
|||
// scopes are not matched (i.e. scope "foo.ba" does not match
|
|||
// name "foo.bar").
|
|||
//
|
|||
// Because modules are registered to a map, the returned slice
|
|||
// will be sorted to keep it deterministic.
|
|||
func GetModules(scope string) []Module { |
|||
modulesMu.Lock() |
|||
defer modulesMu.Unlock() |
|||
|
|||
scopeParts := strings.Split(scope, ".") |
|||
|
|||
// handle the special case of an empty scope, which
|
|||
// should match only the top-level modules
|
|||
if len(scopeParts) == 1 && scopeParts[0] == "" { |
|||
scopeParts = []string{} |
|||
} |
|||
|
|||
var mods []Module |
|||
iterateModules: |
|||
for name, m := range modules { |
|||
modParts := strings.Split(name, ".") |
|||
|
|||
// match only the next level of nesting
|
|||
if len(modParts) != len(scopeParts)+1 { |
|||
continue |
|||
} |
|||
|
|||
// specified parts must be exact matches
|
|||
for i := range scopeParts { |
|||
if modParts[i] != scopeParts[i] { |
|||
continue iterateModules |
|||
} |
|||
} |
|||
|
|||
mods = append(mods, m) |
|||
} |
|||
|
|||
// make return value deterministic
|
|||
sort.Slice(mods, func(i, j int) bool { |
|||
return mods[i].Name < mods[j].Name |
|||
}) |
|||
|
|||
return mods |
|||
} |
|||
|
|||
// Modules returns the names of all registered modules
|
|||
// in ascending lexicographical order.
|
|||
func Modules() []string { |
|||
modulesMu.Lock() |
|||
defer modulesMu.Unlock() |
|||
|
|||
var names []string |
|||
for name := range modules { |
|||
names = append(names, name) |
|||
} |
|||
|
|||
sort.Strings(names) |
|||
|
|||
return names |
|||
} |
|||
|
|||
var ( |
|||
modules = make(map[string]Module) |
|||
modulesMu sync.Mutex |
|||
) |
@ -0,0 +1,27 @@ |
|||
package caddyhttp |
|||
|
|||
import ( |
|||
"log" |
|||
|
|||
"bitbucket.org/lightcodelabs/caddy2" |
|||
) |
|||
|
|||
func init() { |
|||
err := caddy2.RegisterModule(caddy2.Module{ |
|||
Name: "http", |
|||
New: func() (interface{}, error) { return httpModuleConfig{}, nil }, |
|||
}) |
|||
if err != nil { |
|||
log.Fatal(err) |
|||
} |
|||
} |
|||
|
|||
type httpModuleConfig struct { |
|||
Servers map[string]httpServerConfig `json:"servers"` |
|||
} |
|||
|
|||
type httpServerConfig struct { |
|||
Listen []string `json:"listen"` |
|||
ReadTimeout string `json:"read_timeout"` |
|||
ReadHeaderTimeout string `json:"read_header_timeout"` |
|||
} |
@ -0,0 +1,71 @@ |
|||
package caddy2 |
|||
|
|||
import ( |
|||
"reflect" |
|||
"testing" |
|||
) |
|||
|
|||
func TestGetModules(t *testing.T) { |
|||
modulesMu.Lock() |
|||
modules = map[string]Module{ |
|||
"a": {Name: "a"}, |
|||
"a.b": {Name: "a.b"}, |
|||
"a.b.c": {Name: "a.b.c"}, |
|||
"a.b.cd": {Name: "a.b.cd"}, |
|||
"a.c": {Name: "a.c"}, |
|||
"a.d": {Name: "a.d"}, |
|||
"b": {Name: "b"}, |
|||
"b.a": {Name: "b.a"}, |
|||
"b.b": {Name: "b.b"}, |
|||
"b.a.c": {Name: "b.a.c"}, |
|||
"c": {Name: "c"}, |
|||
} |
|||
modulesMu.Unlock() |
|||
|
|||
for i, tc := range []struct { |
|||
input string |
|||
expect []Module |
|||
}{ |
|||
{ |
|||
input: "", |
|||
expect: []Module{ |
|||
{Name: "a"}, |
|||
{Name: "b"}, |
|||
{Name: "c"}, |
|||
}, |
|||
}, |
|||
{ |
|||
input: "a", |
|||
expect: []Module{ |
|||
{Name: "a.b"}, |
|||
{Name: "a.c"}, |
|||
{Name: "a.d"}, |
|||
}, |
|||
}, |
|||
{ |
|||
input: "a.b", |
|||
expect: []Module{ |
|||
{Name: "a.b.c"}, |
|||
{Name: "a.b.cd"}, |
|||
}, |
|||
}, |
|||
{ |
|||
input: "a.b.c", |
|||
}, |
|||
{ |
|||
input: "b", |
|||
expect: []Module{ |
|||
{Name: "b.a"}, |
|||
{Name: "b.b"}, |
|||
}, |
|||
}, |
|||
{ |
|||
input: "asdf", |
|||
}, |
|||
} { |
|||
actual := GetModules(tc.input) |
|||
if !reflect.DeepEqual(actual, tc.expect) { |
|||
t.Errorf("Test %d: Expected %v but got %v", i, tc.expect, actual) |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue