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:
Dalton Hubble
2016-07-19 00:47:20 -07:00
parent 07751a4bba
commit 95b18ba8b9
3 changed files with 45 additions and 19 deletions

View File

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

View File

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

View File

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