summaryrefslogtreecommitdiff
path: root/dynamicconfig.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 /dynamicconfig.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 'dynamicconfig.go')
-rw-r--r--dynamicconfig.go358
1 files changed, 0 insertions, 358 deletions
diff --git a/dynamicconfig.go b/dynamicconfig.go
deleted file mode 100644
index 0e44376..0000000
--- a/dynamicconfig.go
+++ /dev/null
@@ -1,358 +0,0 @@
-// Copyright 2015 Matthew Holt and The Caddy Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package caddy
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "path"
- "strconv"
- "strings"
- "sync"
-)
-
-func init() {
- RegisterModule(router{})
-}
-
-type router []AdminRoute
-
-// CaddyModule returns the Caddy module information.
-func (router) CaddyModule() ModuleInfo {
- return ModuleInfo{
- Name: "admin.routers.dynamic_config",
- New: func() Module {
- return router{
- {
- Pattern: "/" + rawConfigKey + "/",
- Handler: http.HandlerFunc(handleConfig),
- },
- {
- Pattern: "/id/",
- Handler: http.HandlerFunc(handleConfigID),
- },
- }
- },
- }
-}
-
-func (r router) Routes() []AdminRoute { return r }
-
-// handleConfig handles config changes or exports according to r.
-// This function is safe for concurrent use.
-func handleConfig(w http.ResponseWriter, r *http.Request) {
- rawCfgMu.Lock()
- defer rawCfgMu.Unlock()
- unsyncedHandleConfig(w, r)
-}
-
-// handleConfigID accesses the config through a user-assigned ID
-// that is mapped to its full/expanded path in the JSON structure.
-// It is the same as handleConfig except it replaces the ID in
-// the request path with the full, expanded URL path.
-// This function is safe for concurrent use.
-func handleConfigID(w http.ResponseWriter, r *http.Request) {
- parts := strings.Split(r.URL.Path, "/")
- if len(parts) < 3 || parts[2] == "" {
- http.Error(w, "request path is missing object ID", http.StatusBadRequest)
- return
- }
- id := parts[2]
-
- rawCfgMu.Lock()
- defer rawCfgMu.Unlock()
-
- // map the ID to the expanded path
- expanded, ok := rawCfgIndex[id]
- if !ok {
- http.Error(w, "unknown object ID: "+id, http.StatusBadRequest)
- return
- }
-
- // piece the full URL path back together
- parts = append([]string{expanded}, parts[3:]...)
- r.URL.Path = path.Join(parts...)
-
- unsyncedHandleConfig(w, r)
-}
-
-// configIndex 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; use rawCfgMu.
-func configIndex(ptr interface{}, configPath string, index map[string]string) error {
- switch val := ptr.(type) {
- case map[string]interface{}:
- for k, v := range val {
- if k == "@id" {
- 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: @id field must be a string or number", configPath)
- }
- delete(val, "@id") // field is no longer needed, and will break config if not removed
- continue
- }
- // traverse this object property recursively
- err := configIndex(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 := configIndex(val[i], path.Join(configPath, strconv.Itoa(i)), index)
- if err != nil {
- return err
- }
- }
- }
-
- return nil
-}
-
-// unsycnedHandleConfig handles config accesses without a lock
-// on rawCfgMu. This is NOT safe for concurrent use, so be sure
-// to acquire a lock on rawCfgMu before calling this.
-func unsyncedHandleConfig(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
- default:
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- // perform the mutation with our decoded representation
- // (the map), which may change pointers deep within it
- err := mutateConfig(w, r)
- if err != nil {
- http.Error(w, "mutating config: "+err.Error(), http.StatusBadRequest)
- return
- }
-
- if r.Method != http.MethodGet {
- // find any IDs in this config and index them
- idx := make(map[string]string)
- err = configIndex(rawCfg[rawConfigKey], "/config", idx)
- if err != nil {
- http.Error(w, "indexing config: "+err.Error(), http.StatusInternalServerError)
- return
- }
-
- // the mutation is complete, so encode the entire config as JSON
- newCfg, err := json.Marshal(rawCfg[rawConfigKey])
- if err != nil {
- http.Error(w, "encoding new config: "+err.Error(), http.StatusBadRequest)
- return
- }
-
- // if nothing changed, no need to do a whole reload unless the client forces it
- if r.Header.Get("Cache-Control") != "must-revalidate" && bytes.Equal(rawCfgJSON, newCfg) {
- log.Printf("[ADMIN][INFO] Config is unchanged")
- return
- }
-
- // load this new config; if it fails, we need to revert to
- // our old representation of caddy's actual config
- err = Load(bytes.NewReader(newCfg))
- if err != nil {
- // 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
-
- // report error
- log.Printf("[ADMIN][ERROR] loading config: %v", err)
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- // 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
- }
-}
-
-// mutateConfig changes the rawCfg according to r. It is NOT
-// safe for concurrent use; use rawCfgMu. If the request's
-// method is GET, the config will not be changed.
-func mutateConfig(w http.ResponseWriter, r *http.Request) error {
- var err error
- var val interface{}
-
- // if there is a request body, make sure we recognize its content-type and decode it
- if r.Method != http.MethodGet && r.Method != http.MethodDelete {
- if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
- return fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct)
- }
- err = json.NewDecoder(r.Body).Decode(&val)
- if err != nil {
- return fmt.Errorf("decoding request body: %v", err)
- }
- }
-
- buf := new(bytes.Buffer)
- enc := json.NewEncoder(buf)
-
- cleanPath := strings.Trim(r.URL.Path, "/")
- if cleanPath == "" {
- return fmt.Errorf("no traversable path")
- }
-
- parts := strings.Split(cleanPath, "/")
- if len(parts) == 0 {
- return fmt.Errorf("path missing")
- }
-
- var ptr interface{} = rawCfg
-
-traverseLoop:
- for i, part := range parts {
- switch v := ptr.(type) {
- case map[string]interface{}:
- // if the next part enters a slice, and the slice is our destination,
- // handle it specially (because appending to the slice copies the slice
- // header, which does not replace the original one like we want)
- if arr, ok := v[part].([]interface{}); ok && i == len(parts)-2 {
- var idx int
- if r.Method != http.MethodPost {
- idxStr := parts[len(parts)-1]
- idx, err = strconv.Atoi(idxStr)
- if err != nil {
- return fmt.Errorf("[%s] invalid array index '%s': %v",
- r.URL.Path, idxStr, err)
- }
- if idx < 0 || idx >= len(arr) {
- return fmt.Errorf("[%s] array index out of bounds: %s", r.URL.Path, idxStr)
- }
- }
-
- switch r.Method {
- case http.MethodGet:
- err = enc.Encode(arr[idx])
- if err != nil {
- return fmt.Errorf("encoding config: %v", err)
- }
- case http.MethodPost:
- v[part] = append(arr, val)
- case http.MethodPut:
- // avoid creation of new slice and a second copy (see
- // https://github.com/golang/go/wiki/SliceTricks#insert)
- arr = append(arr, nil)
- copy(arr[idx+1:], arr[idx:])
- arr[idx] = val
- v[part] = arr
- case http.MethodPatch:
- arr[idx] = val
- case http.MethodDelete:
- v[part] = append(arr[:idx], arr[idx+1:]...)
- default:
- return fmt.Errorf("unrecognized method %s", r.Method)
- }
- break traverseLoop
- }
-
- if i == len(parts)-1 {
- switch r.Method {
- case http.MethodGet:
- err = enc.Encode(v[part])
- if err != nil {
- return fmt.Errorf("encoding config: %v", err)
- }
- case http.MethodPost:
- if arr, ok := v[part].([]interface{}); ok {
- // if the part is an existing list, POST appends to it
- // TODO: Do we ever reach this point, since we handle arrays
- // separately above?
- v[part] = append(arr, val)
- } else {
- // otherwise, it simply sets the value
- v[part] = val
- }
- case http.MethodPut:
- if _, ok := v[part]; ok {
- return fmt.Errorf("[%s] key already exists: %s", r.URL.Path, part)
- }
- v[part] = val
- case http.MethodPatch:
- if _, ok := v[part]; !ok {
- return fmt.Errorf("[%s] key does not exist: %s", r.URL.Path, part)
- }
- v[part] = val
- case http.MethodDelete:
- delete(v, part)
- default:
- return fmt.Errorf("unrecognized method %s", r.Method)
- }
- } else {
- ptr = v[part]
- }
-
- case []interface{}:
- partInt, err := strconv.Atoi(part)
- if err != nil {
- return fmt.Errorf("[/%s] invalid array index '%s': %v",
- strings.Join(parts[:i+1], "/"), part, err)
- }
- if partInt < 0 || partInt >= len(v) {
- return fmt.Errorf("[/%s] array index out of bounds: %s",
- strings.Join(parts[:i+1], "/"), part)
- }
- ptr = v[partInt]
-
- default:
- return fmt.Errorf("invalid path: %s", parts[:i+1])
- }
- }
-
- if r.Method == http.MethodGet {
- w.Header().Set("Content-Type", "application/json")
- w.Write(buf.Bytes())
- }
-
- return nil
-}
-
-var (
- // 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 []byte // keeping the encoded form avoids an extra Marshal on changes
- rawCfgIndex map[string]string // map of user-assigned ID to expanded path
- rawCfgMu sync.Mutex // protects rawCfg, rawCfgJSON, and rawCfgIndex
-)
-
-const rawConfigKey = "config"