summaryrefslogtreecommitdiff
path: root/caddy.go
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-11-04 12:05:20 -0700
committerMatthew Holt <mholt@users.noreply.github.com>2019-11-04 12:05:20 -0700
commit35f70c98fa1ea13882ee4f0406cd17f5545d0100 (patch)
tree71d8a35e62910802431cc4c650da7365ac21041f /caddy.go
parentfb06c041c4be4eb32f18d54e8e7feff8dd76b0e9 (diff)
core: Major refactor of admin endpoint and config handling
Fixed several bugs and made other improvements. All config changes are now mediated by the global config state manager. It used to be that initial configs given at startup weren't tracked, so you could start caddy with --config caddy.json and then do a GET /config/ and it would return null. That is fixed, along with several other general flow/API enhancements, with more to come.
Diffstat (limited to 'caddy.go')
-rw-r--r--caddy.go247
1 files changed, 229 insertions, 18 deletions
diff --git a/caddy.go b/caddy.go
index 33e6296..7aab8d4 100644
--- a/caddy.go
+++ b/caddy.go
@@ -15,11 +15,16 @@
package caddy
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
+ "io"
"log"
+ "net/http"
+ "path"
"runtime/debug"
+ "strconv"
"strings"
"sync"
"time"
@@ -27,7 +32,18 @@ import (
"github.com/mholt/certmagic"
)
-// Config represents a Caddy configuration.
+// Config represents a Caddy configuration. It is the
+// top of the module structure: all Caddy modules will
+// be loaded, starting with this struct. In order to
+// be loaded and run successfully, a Config and all its
+// modules must be JSON-encodable; i.e. when filling a
+// Config struct manually, its JSON-encodable fields
+// (the ones with JSON struct tags, usually ending with
+// "Raw" if they decode into a separate field) must be
+// set so that they can be unmarshaled and provisioned.
+// Setting the fields for the decoded values instead
+// will result in those values being overwritten at
+// unmarshaling/provisioning.
type Config struct {
Admin *AdminConfig `json:"admin,omitempty"`
Logging *Logging `json:"logging,omitempty"`
@@ -46,13 +62,164 @@ type App interface {
Stop() error
}
-// Run runs Caddy with the given config.
-func Run(newCfg *Config) error {
+// Run runs the given config, replacing any existing config.
+func Run(cfg *Config) error {
+ cfgJSON, err := json.Marshal(cfg)
+ if err != nil {
+ return err
+ }
+ return Load(cfgJSON, true)
+}
+
+// Load loads the given config JSON and runs it only
+// if it is different from the current config or
+// forceReload is true.
+func Load(cfgJSON []byte, forceReload bool) error {
+ return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
+}
+
+// changeConfig changes the current config (rawCfg) according to the
+// method, traversed via the given path, and uses the given input as
+// the new value (if applicable; i.e. "DELETE" doesn't have an input).
+// If the resulting config is the same as the previous, no reload will
+// occur unless forceReload is true. This function is safe for
+// concurrent use.
+func changeConfig(method, path string, input []byte, forceReload bool) error {
+ switch method {
+ case http.MethodGet,
+ http.MethodHead,
+ http.MethodOptions,
+ http.MethodConnect,
+ http.MethodTrace:
+ return fmt.Errorf("method not allowed")
+ }
+
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
+ err := unsyncedConfigAccess(method, path, input, nil)
+ if err != nil {
+ return err
+ }
+
+ // find any IDs in this config and index them
+ idx := make(map[string]string)
+ err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
+ if err != nil {
+ return APIError{
+ Code: http.StatusInternalServerError,
+ Err: fmt.Errorf("indexing config: %v", err),
+ }
+ }
+
+ // the mutation is complete, so encode the entire config as JSON
+ newCfg, err := json.Marshal(rawCfg[rawConfigKey])
+ if err != nil {
+ return APIError{
+ Code: http.StatusBadRequest,
+ Err: fmt.Errorf("encoding new config: %v", err),
+ }
+ }
+
+ // if nothing changed, no need to do a whole reload unless the client forces it
+ if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
+ Log().Named("admin.api.change_config").Info("config is unchanged")
+ return nil
+ }
+
+ // load this new config; if it fails, we need to revert to
+ // our old representation of caddy's actual config
+ err = unsyncedDecodeAndRun(newCfg)
+ if err != nil {
+ if len(rawCfgJSON) > 0 {
+ // restore old config state to keep it consistent
+ // with what caddy is still running; we need to
+ // unmarshal it again because it's likely that
+ // pointers deep in our rawCfg map were modified
+ var oldCfg interface{}
+ err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
+ if err2 != nil {
+ err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
+ }
+ rawCfg[rawConfigKey] = oldCfg
+ }
+
+ return fmt.Errorf("loading new config: %v", err)
+ }
+
+ // success, so update our stored copy of the encoded
+ // config to keep it consistent with what caddy is now
+ // running (storing an encoded copy is not strictly
+ // necessary, but avoids an extra json.Marshal for
+ // each config change)
+ rawCfgJSON = newCfg
+ rawCfgIndex = idx
+
+ return nil
+}
+
+// readConfig traverses the current config to path
+// and writes its JSON encoding to out.
+func readConfig(path string, out io.Writer) error {
+ currentCfgMu.RLock()
+ defer currentCfgMu.RUnlock()
+ return unsyncedConfigAccess(http.MethodGet, path, nil, out)
+}
+
+// indexConfigObjects recurisvely searches ptr for object fields named
+// "@id" and maps that ID value to the full configPath in the index.
+// This function is NOT safe for concurrent access; obtain a write lock
+// on currentCfgMu.
+func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
+ switch val := ptr.(type) {
+ case map[string]interface{}:
+ for k, v := range val {
+ if k == idKey {
+ switch idVal := v.(type) {
+ case string:
+ index[idVal] = configPath
+ case float64: // all JSON numbers decode as float64
+ index[fmt.Sprintf("%v", idVal)] = configPath
+ default:
+ return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
+ }
+ delete(val, idKey) // field is no longer needed, and will break config if not removed
+ continue
+ }
+ // traverse this object property recursively
+ err := indexConfigObjects(val[k], path.Join(configPath, k), index)
+ if err != nil {
+ return err
+ }
+ }
+ case []interface{}:
+ // traverse each element of the array recursively
+ for i := range val {
+ err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// unsyncedDecodeAndRun decodes cfgJSON and runs
+// it as the new config, replacing any other
+// current config. It does not update the raw
+// config state, as this is a lower-level function;
+// most callers will want to use Load instead.
+// A write lock on currentCfgMu is required!
+func unsyncedDecodeAndRun(cfgJSON []byte) error {
+ var newCfg *Config
+ err := strictUnmarshalJSON(cfgJSON, &newCfg)
+ if err != nil {
+ return err
+ }
+
// run the new config and start all its apps
- err := run(newCfg, true)
+ err = run(newCfg, true)
if err != nil {
return err
}
@@ -77,11 +244,11 @@ func Run(newCfg *Config) error {
// the config if you are not going to start it,
// so that each provisioned module will be
// cleaned up.
+//
+// This is a low-level function; most callers
+// will want to use Run instead, which also
+// updates the config's raw state.
func run(newCfg *Config, start bool) error {
- if newCfg == nil {
- return nil
- }
-
// because we will need to roll back any state
// modifications if this function errors, we
// keep a single error value and scope all
@@ -91,6 +258,16 @@ func run(newCfg *Config, start bool) error {
// been set by a short assignment
var err error
+ // start the admin endpoint (and stop any prior one)
+ err = replaceAdmin(newCfg)
+ if err != nil {
+ return fmt.Errorf("starting caddy administration endpoint: %v", err)
+ }
+
+ if newCfg == nil {
+ return nil
+ }
+
// prepare the new config for use
newCfg.apps = make(map[string]App)
@@ -198,22 +375,25 @@ func run(newCfg *Config, start bool) error {
// It is the antithesis of Run(). This function
// will log any errors that occur during the
// stopping of individual apps and continue to
-// stop the others.
+// stop the others. Stop should only be called
+// if not replacing with a new config.
func Stop() error {
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
unsyncedStop(currentCfg)
currentCfg = nil
+ rawCfgJSON = nil
+ rawCfgIndex = nil
+ rawCfg[rawConfigKey] = nil
return nil
}
-// unsyncedStop stops cfg from running, but if
-// applicable, you need to acquire locks yourself.
-// It is a no-op if cfg is nil. If any app
-// returns an error when stopping, it is logged
-// and the function continues with the next app.
-// This function assumes all apps in cfg were
-// successfully started.
+// unsyncedStop stops cfg from running, but has
+// no locking around cfg. It is a no-op if cfg is
+// nil. If any app returns an error when stopping,
+// it is logged and the function continues stopping
+// the next app. This function assumes all apps in
+// cfg were successfully started first.
func unsyncedStop(cfg *Config) {
if cfg == nil {
return
@@ -231,6 +411,17 @@ func unsyncedStop(cfg *Config) {
cfg.cancelFunc()
}
+// stopAndCleanup calls stop and cleans up anything
+// else that is expedient. This should only be used
+// when stopping and not replacing with a new config.
+func stopAndCleanup() error {
+ if err := Stop(); err != nil {
+ return err
+ }
+ certmagic.CleanUpOwnLocks()
+ return nil
+}
+
// Validate loads, provisions, and validates
// cfg, but does not start running it.
func Validate(cfg *Config) error {
@@ -289,8 +480,28 @@ func goModule(mod *debug.Module) *debug.Module {
// CtxKey is a value type for use with context.WithValue.
type CtxKey string
-// currentCfg is the currently-loaded configuration.
+// This group of variables pertains to the current configuration.
var (
- currentCfg *Config
+ // currentCfgMu protects everything in this var block.
currentCfgMu sync.RWMutex
+
+ // currentCfg is the currently-running configuration.
+ currentCfg *Config
+
+ // rawCfg is the current, generic-decoded configuration;
+ // we initialize it as a map with one field ("config")
+ // to maintain parity with the API endpoint and to avoid
+ // the special case of having to access/mutate the variable
+ // directly without traversing into it.
+ rawCfg = map[string]interface{}{
+ rawConfigKey: nil,
+ }
+
+ // rawCfgJSON is the JSON-encoded form of rawCfg. Keeping
+ // this around avoids an extra Marshal call during changes.
+ rawCfgJSON []byte
+
+ // rawCfgIndex is the map of user-assigned ID to expanded
+ // path, for converting /id/ paths to /config/ paths.
+ rawCfgIndex map[string]string
)