mirror of
https://github.com/outbackdingo/matchbox.git
synced 2026-01-27 10:19:35 +00:00
bootstrap/http: Format nested metadata in env file
* Serve /metadata including group metadata, selectors, and query variables in KEY=value "env file" format lines * Recurse into nested maps (e.g. OUTER_INNER=val)
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
* Add/improve rkt, Docker, Kubernetes, and binary/systemd deployment docs
|
||||
* Add DialTimeout to gRPC client config (#273)
|
||||
* Allow query parameters to be used as template variables as `{{.request.query.foo}}` (#182)
|
||||
* Support nested metadata in responses from the "env file" style metadata endpoint (#84)
|
||||
|
||||
#### Changes
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -39,9 +40,32 @@ func (s *Server) metadataHandler() ContextHandler {
|
||||
}
|
||||
|
||||
w.Header().Set(contentType, plainContentType)
|
||||
for key, value := range data {
|
||||
fmt.Fprintf(w, "%s=%v\n", strings.ToUpper(key), value)
|
||||
}
|
||||
renderAsEnvFile(w, "", data)
|
||||
}
|
||||
return ContextHandlerFunc(fn)
|
||||
}
|
||||
|
||||
// renderAsEnvFile writes map data into a KEY=value\n "env file" format,
|
||||
// descending recursively into nested maps and prepending parent keys.
|
||||
//
|
||||
// For example, {"outer":{"inner":"val"}} -> OUTER_INNER=val). Note that
|
||||
// structure is lost in this transformation, the inverse transfom has two
|
||||
// possible outputs.
|
||||
func renderAsEnvFile(w io.Writer, prefix string, root map[string]interface{}) {
|
||||
for key, value := range root {
|
||||
name := prefix + key
|
||||
switch val := value.(type) {
|
||||
case string, bool, float64:
|
||||
// simple JSON unmarshal types
|
||||
fmt.Fprintf(w, "%s=%v\n", strings.ToUpper(name), val)
|
||||
case map[string]string:
|
||||
m := map[string]interface{}{}
|
||||
for k, v := range val {
|
||||
m[k] = v
|
||||
}
|
||||
renderAsEnvFile(w, name+"_", m)
|
||||
case map[string]interface{}:
|
||||
renderAsEnvFile(w, name+"_", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,23 +25,30 @@ func TestMetadataHandler(t *testing.T) {
|
||||
h := srv.metadataHandler()
|
||||
ctx := withGroup(context.Background(), group)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/?mac=52-54-00-a1-9c-ae", nil)
|
||||
req, _ := http.NewRequest("GET", "/?mac=52-54-00-a1-9c-ae&foo=bar&count=3&gate=true", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
// assert that:
|
||||
// - the Group's custom metadata and selectors are served
|
||||
// - Group selectors, metadata, and query variables are formatted
|
||||
// - nested metadata are namespaced
|
||||
// - key names are upper case
|
||||
expectedData := map[string]string{
|
||||
// - key/value pairs are newline separated
|
||||
expectedLines := map[string]string{
|
||||
// group metadata
|
||||
"META": "data",
|
||||
"ETCD": "map[name:node1]",
|
||||
"SOME": "map[nested:map[data:some-value]]",
|
||||
"META": "data",
|
||||
"ETCD_NAME": "node1",
|
||||
"SOME_NESTED_DATA": "some-value",
|
||||
// group selector
|
||||
"MAC": "52:54:00:a1:9c:ae",
|
||||
// HACK(dghubble): Not testing query params until #84
|
||||
// request
|
||||
"REQUEST_QUERY_MAC": "52:54:00:a1:9c:ae",
|
||||
"REQUEST_QUERY_FOO": "bar",
|
||||
"REQUEST_QUERY_COUNT": "3",
|
||||
"REQUEST_QUERY_GATE": "true",
|
||||
"REQUEST_RAW_QUERY": "mac=52-54-00-a1-9c-ae&foo=bar&count=3&gate=true",
|
||||
}
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
// convert response (random order) to map (tests compare in order)
|
||||
assert.Equal(t, expectedData, metadataToMap(w.Body.String()))
|
||||
assert.Equal(t, expectedLines, metadataToMap(w.Body.String()))
|
||||
assert.Equal(t, plainContentType, w.HeaderMap.Get(contentType))
|
||||
}
|
||||
|
||||
@@ -57,16 +64,14 @@ func TestMetadataHandler_MetadataEdgeCases(t *testing.T) {
|
||||
{&storagepb.Group{Metadata: []byte(`{"num":3}`)}, "NUM=3\n"},
|
||||
{&storagepb.Group{Metadata: []byte(`{"yes":true}`)}, "YES=true\n"},
|
||||
{&storagepb.Group{Metadata: []byte(`{"no":false}`)}, "NO=false\n"},
|
||||
// Issue #84 - improve list and map printouts
|
||||
{&storagepb.Group{Metadata: []byte(`{"list":["3","d"]}`)}, "LIST=[3 d]\n"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
ctx := withGroup(context.Background(), c.group)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(ctx, w, req)
|
||||
// assert that each Group's metadata is formatted:
|
||||
// - key names are upper case
|
||||
// assert that:
|
||||
// - Group metadata key names are upper case
|
||||
// - key/value pairs are newline separated
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), c.expected)
|
||||
@@ -95,10 +100,6 @@ func metadataToMap(metadata string) map[string]string {
|
||||
if len(pair) != 2 {
|
||||
continue
|
||||
}
|
||||
// HACK(dghubble) - Skip map unwinding until #84
|
||||
if pair[0] == "REQUEST" {
|
||||
continue
|
||||
}
|
||||
data[pair[0]] = pair[1]
|
||||
}
|
||||
return data
|
||||
|
||||
Reference in New Issue
Block a user