Initial commit

This commit is contained in:
Matthew Holt 2019-03-26 12:00:54 -06:00
commit 859b5d7ea3
6 changed files with 360 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_gitignore/

130
admin.go Normal file
View File

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

21
cmd/caddy2/main.go Normal file
View File

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

110
modules.go Normal file
View File

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

View File

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

71
modules_test.go Normal file
View File

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