diff --git a/CHANGES.md b/CHANGES.md index ea2963b9..cdb83ad0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/bootcfg/http/metadata.go b/bootcfg/http/metadata.go index 184a1125..0b484a2a 100644 --- a/bootcfg/http/metadata.go +++ b/bootcfg/http/metadata.go @@ -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) + } + } +} diff --git a/bootcfg/http/metadata_test.go b/bootcfg/http/metadata_test.go index 725ee4bd..e677da88 100644 --- a/bootcfg/http/metadata_test.go +++ b/bootcfg/http/metadata_test.go @@ -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