Repository: puppetlabs/wash
Pull request #262 (2019-05-17 01:14:51)

Clarify meta; cleanup some plugins; add `wash info`


Patch:
diff --git a/api/fs/core.go b/api/fs/core.go
index 6df85ce..bf7daf4 100644
--- a/api/fs/core.go
+++ b/api/fs/core.go
@@ -20,19 +20,17 @@ func newFSNode(ctx context.Context, finfo os.FileInfo, path string) *fsnode {
 	// TODO: finfo.Sys() contains more detailed file attributes,
 	// but it's platform-specific. We should eventually use it for
 	// a more complete implementation of apifs.
-	attr := plugin.EntryAttributes{}
-	attr.
-		SetMtime(finfo.ModTime()).
-		SetMode(finfo.Mode()).
-		SetSize(uint64(finfo.Size())).
-		SetMeta(plugin.ToMeta(newFileInfo(finfo)))
-
 	n := &fsnode{
 		EntryBase: plugin.NewEntry(finfo.Name()),
 		path:      path,
 	}
-	n.DisableDefaultCaching()
-	n.SetAttributes(attr)
+	n.
+		DisableDefaultCaching().
+		Attributes().
+		SetMtime(finfo.ModTime()).
+		SetMode(finfo.Mode()).
+		SetSize(uint64(finfo.Size())).
+		SetMeta(plugin.ToJSONObject(newFileInfo(finfo)))
 	return n
 }
 
diff --git a/api/metadata.go b/api/metadata.go
index ddbfa30..8ab45d5 100644
--- a/api/metadata.go
+++ b/api/metadata.go
@@ -12,7 +12,7 @@ import (
 // swagger:response
 //nolint:deadcode,unused
 type entryMetadata struct {
-	EntryMetadata plugin.EntryMetadata
+	JSONObject plugin.JSONObject
 }
 
 // swagger:route GET /fs/metadata metadata getMetadata
diff --git a/cmd/info.go b/cmd/info.go
new file mode 100644
index 0000000..e5ca2ea
--- /dev/null
+++ b/cmd/info.go
@@ -0,0 +1,56 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/Benchkram/errz"
+	"github.com/puppetlabs/wash/api/client"
+	cmdutil "github.com/puppetlabs/wash/cmd/util"
+	"github.com/puppetlabs/wash/config"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+func infoCommand() *cobra.Command {
+	infoCmd := &cobra.Command{
+		Use:   "info <path>",
+		Short: "Prints the entry's info at the specified path",
+		Args:  cobra.ExactArgs(1),
+	}
+
+	infoCmd.Flags().StringP("output", "o", "json", "Set the output format (json or yaml)")
+	errz.Fatal(viper.BindPFlag("output", infoCmd.Flags().Lookup("output")))
+
+	infoCmd.RunE = toRunE(infoMain)
+
+	return infoCmd
+}
+
+func infoMain(cmd *cobra.Command, args []string) exitCode {
+	path := args[0]
+
+	output := viper.GetString("output")
+	marshaller, err := cmdutil.NewMarshaller(output)
+	if err != nil {
+		cmdutil.ErrPrintf(err.Error())
+		return exitCode{1}
+	}
+
+	conn := client.ForUNIXSocket(config.Socket)
+
+	entry, err := conn.Info(path)
+	if err != nil {
+		cmdutil.ErrPrintf("%v\n", err)
+		return exitCode{1}
+	}
+
+	marshalledEntry, err := marshaller.Marshal(entry)
+	if err != nil {
+		cmdutil.ErrPrintf("%v\n", err)
+		return exitCode{1}
+	}
+
+	fmt.Println(marshalledEntry)
+
+	return exitCode{0}
+}
diff --git a/cmd/meta.go b/cmd/meta.go
index 221f00c..cf3eb58 100644
--- a/cmd/meta.go
+++ b/cmd/meta.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"encoding/json"
 	"fmt"
 
 	"github.com/puppetlabs/wash/api/client"
@@ -9,7 +8,6 @@ import (
 	"github.com/puppetlabs/wash/config"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
-	"gopkg.in/yaml.v2"
 )
 
 func metaCommand() *cobra.Command {
@@ -33,14 +31,9 @@ func metaMain(cmd *cobra.Command, args []string) exitCode {
 	path := args[0]
 
 	output := viper.GetString("output")
-	var marshaller func(interface{}) ([]byte, error)
-	switch output {
-	case "json":
-		marshaller = func(in interface{}) ([]byte, error) { return json.MarshalIndent(in, "", "  ") }
-	case "yaml":
-		marshaller = yaml.Marshal
-	default:
-		cmdutil.ErrPrintf("output must be either 'json' or 'yaml'\n")
+	marshaller, err := cmdutil.NewMarshaller(output)
+	if err != nil {
+		cmdutil.ErrPrintf(err.Error())
 		return exitCode{1}
 	}
 
@@ -52,13 +45,13 @@ func metaMain(cmd *cobra.Command, args []string) exitCode {
 		return exitCode{1}
 	}
 
-	prettyMetadata, err := marshaller(metadata)
+	prettyMetadata, err := marshaller.Marshal(metadata)
 	if err != nil {
 		cmdutil.ErrPrintf("%v\n", err)
 		return exitCode{1}
 	}
 
-	fmt.Println(string(prettyMetadata))
+	fmt.Println(prettyMetadata)
 
 	return exitCode{0}
 }
diff --git a/cmd/root.go b/cmd/root.go
index 6340fe9..4414b0c 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -65,6 +65,7 @@ func rootCommand() *cobra.Command {
 	rootCmd.AddCommand(clearCommand())
 	rootCmd.AddCommand(tailCommand())
 	rootCmd.AddCommand(historyCommand())
+	rootCmd.AddCommand(infoCommand())
 
 	return rootCmd
 }
diff --git a/cmd/util/marshal.go b/cmd/util/marshal.go
new file mode 100644
index 0000000..7d53aec
--- /dev/null
+++ b/cmd/util/marshal.go
@@ -0,0 +1,51 @@
+package cmdutil
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/ghodss/yaml"
+)
+
+const (
+	// JSON represents the json marshaller
+	JSON = "json"
+	// YAML represents the YAML marshaller
+	YAML = "yaml"
+)
+
+// Marshaller is a type that marshals a given value
+type Marshaller func(interface{}) ([]byte, error)
+
+// NewMarshaller returns a marshaller that marshals values
+// into the specified format. Currently, only JSON or YAML
+// are supported.
+func NewMarshaller(format string) (Marshaller, error) {
+	switch format {
+	case JSON:
+		return Marshaller(func(v interface{}) ([]byte, error) {
+			return json.MarshalIndent(v, "", "  ")
+		}), nil
+	case YAML:
+		return Marshaller(func(v interface{}) ([]byte, error) {
+			// Use a JSONToYAML style encoding so that objects do not
+			// have to implement multiple Marshal* interfaces.
+			jsonBytes, err := json.Marshal(v)
+			if err != nil {
+				return nil, err
+			}
+			return yaml.JSONToYAML(jsonBytes)
+		}), nil
+	default:
+		return nil, fmt.Errorf("the %v format is not supported. Supported formats are 'json' or 'yaml'", format)
+	}
+}
+
+// Marshal marshals the given value
+func (m Marshaller) Marshal(v interface{}) (string, error) {
+	bytes, err := m(v)
+	if err != nil {
+		return "", err
+	}
+	return string(bytes), nil
+}
diff --git a/docs/api/api.json b/docs/api/api.json
index 321d90c..173de87 100644
--- a/docs/api/api.json
+++ b/docs/api/api.json
@@ -399,8 +399,8 @@
       "title": "EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.",
       "x-go-package": "github.com/puppetlabs/wash/plugin"
     },
-    "EntryMetadata": {
-      "description": "EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.",
+    "JSONObject": {
+      "description": "JSONObject represents the entry's metadata\nas returned by Entry#Metadata.",
       "type": "object",
       "additionalProperties": {
         "type": "object"
@@ -520,10 +520,10 @@
     },
     "entryMetadata": {
       "schema": {
-        "$ref": "#/definitions/EntryMetadata"
+        "$ref": "#/definitions/JSONObject"
       },
       "headers": {
-        "EntryMetadata": {}
+        "JSONObject": {}
       }
     },
     "errorResp": {
diff --git a/docs/api/index.html b/docs/api/index.html
index 4e327c1..b350c1a 100644
--- a/docs/api/index.html
+++ b/docs/api/index.html
@@ -283,7 +283,7 @@ If TypeField is Exitcode, Data will be an int (or float64 if deserialized from J
 index within its activity history.</p>
 </div></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history/{id}</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history/{id}</div></div></div></div></div></div></div></div></div><div class="sc-13p9gjn-2 egwcPy"></div></div></div>
     <script>
-    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"EntryMetadata":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryMetadata"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"EntryMetadata":{"description":"EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
+    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"JSONObject":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/JSONObject"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"JSONObject":{"description":"JSONObject represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
 
     var container = document.getElementById('redoc');
     Redoc.hydrate(__redoc_state, container);;
diff --git a/docs/docs/api/api.json b/docs/docs/api/api.json
index d998979..dac88f9 100644
--- a/docs/docs/api/api.json
+++ b/docs/docs/api/api.json
@@ -415,8 +415,8 @@
       "title": "EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.",
       "x-go-package": "github.com/puppetlabs/wash/plugin"
     },
-    "EntryMetadata": {
-      "description": "EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.",
+    "JSONObject": {
+      "description": "JSONObject represents the entry's metadata\nas returned by Entry#Metadata.",
       "type": "object",
       "additionalProperties": {
         "type": "object"
@@ -536,10 +536,10 @@
     },
     "entryMetadata": {
       "schema": {
-        "$ref": "#/definitions/EntryMetadata"
+        "$ref": "#/definitions/JSONObject"
       },
       "headers": {
-        "EntryMetadata": {}
+        "JSONObject": {}
       }
     },
     "errorResp": {
diff --git a/docs/docs/api/index.html b/docs/docs/api/index.html
index a2c7f91..e0ce615 100644
--- a/docs/docs/api/index.html
+++ b/docs/docs/api/index.html
@@ -285,7 +285,7 @@ index within its activity history.</p>
 </div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Follow"><span class="dz44d2-5 jwaijW"></span>Follow</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">boolean</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>stream updates when true</p>
 </div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history/{id}</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history/{id}</div></div></div></div></div></div></div></div></div><div class="sc-13p9gjn-2 egwcPy"></div></div></div>
     <script>
-    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"EntryMetadata":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryMetadata"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"EntryMetadata":{"description":"EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
+    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"JSONObject":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/JSONObject"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"JSONObject":{"description":"JSONObject represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
 
     var container = document.getElementById('redoc');
     Redoc.hydrate(__redoc_state, container);;
diff --git a/go.mod b/go.mod
index 1e6d174..53204f8 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
 	github.com/ekinanp/go-cache v2.1.0+incompatible
 	github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a // indirect
 	github.com/fatih/color v1.7.0
+	github.com/ghodss/yaml v1.0.0
 	github.com/go-ole/go-ole v1.2.4 // indirect
 	github.com/gobwas/glob v0.2.3
 	github.com/gogo/protobuf v1.2.1 // indirect
diff --git a/go.sum b/go.sum
index 2d2df56..5ef9c95 100644
--- a/go.sum
+++ b/go.sum
@@ -52,6 +52,8 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
 github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
diff --git a/plugin/aws/ec2Instance.go b/plugin/aws/ec2Instance.go
index c0d78a0..43ca4e8 100644
--- a/plugin/aws/ec2Instance.go
+++ b/plugin/aws/ec2Instance.go
@@ -56,10 +56,10 @@ func newEC2Instance(ctx context.Context, inst *ec2Client.Instance, session *sess
 		session:   session,
 		client:    client,
 	}
-	ec2Instance.SetTTLOf(plugin.ListOp, 30*time.Second)
-	ec2Instance.DisableCachingFor(plugin.MetadataOp)
-
-	ec2Instance.SetAttributes(getAttributes(inst))
+	ec2Instance.
+		SetTTLOf(plugin.ListOp, 30*time.Second).
+		DisableCachingFor(plugin.MetadataOp).
+		SetAttributes(getAttributes(inst))
 
 	return ec2Instance
 }
@@ -87,7 +87,7 @@ func getAttributes(inst *ec2Client.Instance) plugin.EntryAttributes {
 		}
 	}
 
-	meta := plugin.ToMeta(inst)
+	meta := plugin.ToJSONObject(inst)
 	meta["CreationTime"] = ctime
 	meta["LastModifiedTime"] = mtime
 
diff --git a/plugin/aws/ec2InstanceConsoleOutput.go b/plugin/aws/ec2InstanceConsoleOutput.go
index 275308e..f85c30d 100644
--- a/plugin/aws/ec2InstanceConsoleOutput.go
+++ b/plugin/aws/ec2InstanceConsoleOutput.go
@@ -37,13 +37,12 @@ func newEC2InstanceConsoleOutput(ctx context.Context, inst *ec2Instance, latest
 		return nil, err
 	}
 
-	attr := plugin.EntryAttributes{}
-	attr.
+	cl.
+		Attributes().
 		SetCtime(output.mtime).
 		SetMtime(output.mtime).
 		SetAtime(output.mtime).
 		SetSize(uint64(len(output.content)))
-	cl.SetAttributes(attr)
 
 	return cl, nil
 }
diff --git a/plugin/aws/ec2InstanceMetadataJSON.go b/plugin/aws/ec2InstanceMetadataJSON.go
index 01f9460..aca7769 100644
--- a/plugin/aws/ec2InstanceMetadataJSON.go
+++ b/plugin/aws/ec2InstanceMetadataJSON.go
@@ -27,10 +27,7 @@ func newEC2InstanceMetadataJSON(ctx context.Context, inst *ec2Instance) (*ec2Ins
 		return nil, err
 	}
 
-	attr := plugin.EntryAttributes{}
-	attr.SetSize(uint64(content.Size()))
-	im.SetAttributes(attr)
-
+	im.Attributes().SetSize(uint64(content.Size()))
 	return im, nil
 }
 
diff --git a/plugin/aws/s3Bucket.go b/plugin/aws/s3Bucket.go
index 82fd44a..45b94aa 100644
--- a/plugin/aws/s3Bucket.go
+++ b/plugin/aws/s3Bucket.go
@@ -158,13 +158,11 @@ func newS3Bucket(name string, ctime time.Time, session *session.Session) *s3Buck
 		client:    s3Client.New(session),
 		session:   session,
 	}
-
-	attr := plugin.EntryAttributes{}
-	attr.
+	bucket.
+		Attributes().
 		SetCtime(bucket.ctime).
 		SetMtime(bucket.ctime).
 		SetAtime(bucket.ctime)
-	bucket.SetAttributes(attr)
 
 	return bucket
 }
@@ -176,16 +174,16 @@ func (b *s3Bucket) List(ctx context.Context) ([]plugin.Entry, error) {
 	return listObjects(ctx, b.client, b.Name(), "")
 }
 
-func (b *s3Bucket) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
+func (b *s3Bucket) Metadata(ctx context.Context) (plugin.JSONObject, error) {
 	request := &s3Client.GetBucketTaggingInput{
 		Bucket: awsSDK.String(b.Name()),
 	}
 
 	resp, err := b.client.GetBucketTaggingWithContext(ctx, request)
 
-	var metadata plugin.EntryMetadata
+	var metadata plugin.JSONObject
 	if err == nil {
-		metadata = plugin.ToMeta(resp)
+		metadata = plugin.ToJSONObject(resp)
 	} else if awserr, ok := err.(awserr.Error); ok {
 		// Check if this is a NoSuchTagSet error. If yes, then that means
 		// this bucket doesn't have any tags.
@@ -194,7 +192,7 @@ func (b *s3Bucket) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
 		// if you're interested in knowing why AWS does not return
 		// an empty TagSet instead of a NoSuchTagSet error
 		if awserr.Code() == "NoSuchTagSet" {
-			metadata = plugin.EntryMetadata{}
+			metadata = plugin.JSONObject{}
 		} else {
 			return nil, err
 		}
diff --git a/plugin/aws/s3Object.go b/plugin/aws/s3Object.go
index b960f14..dbbae7c 100644
--- a/plugin/aws/s3Object.go
+++ b/plugin/aws/s3Object.go
@@ -39,13 +39,13 @@ func newS3Object(o *s3Client.Object, name string, bucket string, key string, cli
 	// TODO: Export a mungeSize helper to abstract away the common
 	// logic of validating a negative size
 	mtime := awsSDK.TimeValue(o.LastModified)
-	attr := plugin.EntryAttributes{}
-	attr.
+	s3Obj.
+		Attributes().
 		SetCtime(mtime).
 		SetMtime(mtime).
 		SetAtime(mtime).
-		SetSize(uint64(awsSDK.Int64Value(o.Size)))
-	s3Obj.SetAttributes(attr)
+		SetSize(uint64(awsSDK.Int64Value(o.Size))).
+		SetMeta(o)
 
 	return s3Obj
 }
@@ -67,13 +67,13 @@ func (o *s3Object) cachedHeadObject(ctx context.Context) (*s3Client.HeadObjectOu
 	return resp.(*s3Client.HeadObjectOutput), nil
 }
 
-func (o *s3Object) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
+func (o *s3Object) Metadata(ctx context.Context) (plugin.JSONObject, error) {
 	metadata, err := o.cachedHeadObject(ctx)
 	if err != nil {
 		return nil, nil
 	}
 
-	return plugin.ToMeta(metadata), nil
+	return plugin.ToJSONObject(metadata), nil
 }
 
 func (o *s3Object) fetchContent(off int64) (io.ReadCloser, error) {
diff --git a/plugin/cache.go b/plugin/cache.go
index afad34f..5c15b7d 100644
--- a/plugin/cache.go
+++ b/plugin/cache.go
@@ -206,7 +206,7 @@ func CachedOpen(ctx context.Context, r Readable) (SizedReader, error) {
 }
 
 // CachedMetadata caches an entry's Metadata method
-func CachedMetadata(ctx context.Context, e Entry) (EntryMetadata, error) {
+func CachedMetadata(ctx context.Context, e Entry) (JSONObject, error) {
 	cachedMetadata, err := cachedDefaultOp(ctx, MetadataOp, e, func() (interface{}, error) {
 		return e.Metadata(ctx)
 	})
@@ -215,7 +215,7 @@ func CachedMetadata(ctx context.Context, e Entry) (EntryMetadata, error) {
 		return nil, err
 	}
 
-	return cachedMetadata.(EntryMetadata), nil
+	return cachedMetadata.(JSONObject), nil
 }
 
 // Common helper for CachedList, CachedOpen and CachedMetadata
diff --git a/plugin/docker/container.go b/plugin/docker/container.go
index e27ec3b..ea903d5 100644
--- a/plugin/docker/container.go
+++ b/plugin/docker/container.go
@@ -19,14 +19,14 @@ type container struct {
 	client *client.Client
 }
 
-func (c *container) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
+func (c *container) Metadata(ctx context.Context) (plugin.JSONObject, error) {
 	// Use raw to also get the container size.
 	_, raw, err := c.client.ContainerInspectWithRaw(ctx, c.id, true)
 	if err != nil {
 		return nil, err
 	}
 
-	return plugin.ToMeta(raw), nil
+	return plugin.ToJSONObject(raw), nil
 }
 
 func (c *container) List(ctx context.Context) ([]plugin.Entry, error) {
@@ -38,9 +38,7 @@ func (c *container) List(ctx context.Context) ([]plugin.Entry, error) {
 	if err != nil {
 		return nil, err
 	}
-	cmAttr := plugin.EntryAttributes{}
-	cmAttr.SetSize(uint64(content.Size()))
-	cm.SetAttributes(cmAttr)
+	cm.Attributes().SetSize(uint64(content.Size()))
 
 	clf := &containerLogFile{plugin.NewEntry("log"), c.id, c.client}
 	clf.DisableCachingFor(plugin.MetadataOp)
@@ -48,9 +46,7 @@ func (c *container) List(ctx context.Context) ([]plugin.Entry, error) {
 	if err != nil {
 		return nil, err
 	}
-	clfAttr := plugin.EntryAttributes{}
-	clfAttr.SetSize(uint64(content.Size()))
-	clf.SetAttributes(clfAttr)
+	clf.Attributes().SetSize(uint64(content.Size()))
 
 	// Include a view of the remote filesystem using volume.FS
 	return []plugin.Entry{cm, clf, vol.NewFS("fs", c)}, nil
diff --git a/plugin/docker/containers.go b/plugin/docker/containers.go
index bdb8124..c043962 100644
--- a/plugin/docker/containers.go
+++ b/plugin/docker/containers.go
@@ -42,12 +42,12 @@ func (cs *containers) List(ctx context.Context) ([]plugin.Entry, error) {
 		}
 
 		startTime := time.Unix(inst.Created, 0)
-		attr := plugin.EntryAttributes{}
-		attr.
+		cont.
+			Attributes().
 			SetCtime(startTime).
 			SetMtime(startTime).
-			SetAtime(startTime)
-		cont.SetAttributes(attr)
+			SetAtime(startTime).
+			SetMeta(inst)
 
 		keys[i] = cont
 	}
diff --git a/plugin/docker/volume.go b/plugin/docker/volume.go
index 887a6a7..eeafcda 100644
--- a/plugin/docker/volume.go
+++ b/plugin/docker/volume.go
@@ -37,25 +37,16 @@ func newVolume(c *client.Client, v *types.Volume) (*volume, error) {
 		client:    c,
 	}
 
-	attr := plugin.EntryAttributes{}
-	attr.
+	vol.
+		Attributes().
 		SetCtime(startTime).
 		SetMtime(startTime).
 		SetAtime(startTime).
-		SetMeta(plugin.ToMeta(v))
-	vol.SetAttributes(attr)
+		SetMeta(v)
 
 	return vol, nil
 }
 
-func (v *volume) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
-	_, raw, err := v.client.VolumeInspectWithRaw(ctx, v.Name())
-	if err != nil {
-		return nil, err
-	}
-	return plugin.ToMeta(raw), nil
-}
-
 func (v *volume) List(ctx context.Context) ([]plugin.Entry, error) {
 	return vol.List(ctx, v, "")
 }
diff --git a/plugin/entryAttributes.go b/plugin/entryAttributes.go
index 61b03de..0cdc4b6 100644
--- a/plugin/entryAttributes.go
+++ b/plugin/entryAttributes.go
@@ -10,28 +10,30 @@ import (
 	"github.com/puppetlabs/wash/munge"
 )
 
-// EntryMetadata represents the entry's metadata
-// as returned by Entry#Metadata.
-type EntryMetadata map[string]interface{}
+// JSONObject is a typedef to a map[string]interface{}
+// object.
+type JSONObject = map[string]interface{}
 
-// ToMeta converts an object to an EntryMetadata object. If the input is already an
-// array of bytes, it must contain a serialized JSON object. Will panic if given something
-// besides a struct or []byte.
-func ToMeta(obj interface{}) EntryMetadata {
+// ToJSONObject serializes v to a JSON object. It will panic
+// if the serialization fails.
+func ToJSONObject(v interface{}) JSONObject {
+	if obj, ok := v.(JSONObject); ok {
+		return obj
+	}
 	var err error
 	var inrec []byte
-	if arr, ok := obj.([]byte); ok {
+	if arr, ok := v.([]byte); ok {
 		inrec = arr
 	} else {
-		if inrec, err = json.Marshal(obj); err != nil {
+		if inrec, err = json.Marshal(v); err != nil {
 			// Internal error if we can't marshal an object
 			panic(err)
 		}
 	}
-	var meta EntryMetadata
+	var obj JSONObject
 	// Internal error if not a JSON object
-	errz.Fatal(json.Unmarshal(inrec, &meta))
-	return meta
+	errz.Fatal(json.Unmarshal(inrec, &obj))
+	return obj
 }
 
 /*
@@ -57,7 +59,7 @@ type EntryAttributes struct {
 	hasMode bool
 	size    uint64
 	hasSize bool
-	meta    EntryMetadata
+	meta    JSONObject
 }
 
 // We can't just export EntryAttributes' fields because there's no way
@@ -160,13 +162,14 @@ func (a *EntryAttributes) SetSize(size uint64) *EntryAttributes {
 	return a
 }
 
-// Meta returns the portion of the entry's metadata that's returned by the
-// plugin API's List endpoint. This may or may not match what's returned by
-// e.Metadata (i.e. the entry's full metadata). That detail is plugin-specific.
-//
-// If a.SetMeta(m) was called, then this returns m. Otherwise, it returns
+// Meta returns the entry's meta attribute. If a.SetMeta(obj) was called,
+// then this returns obj serialized to JSONObject. Otherwise, it returns
 // a.ToMap(false).
-func (a *EntryAttributes) Meta() EntryMetadata {
+//
+// NOTE: The meta attribute is a subset of the entry's full metadata, which
+// is what e.Metadata returns. It is typically provided by the plugin API's
+// List endpoint.
+func (a *EntryAttributes) Meta() JSONObject {
 	if a.meta == nil {
 		return a.ToMap(false)
 	}
@@ -174,9 +177,15 @@ func (a *EntryAttributes) Meta() EntryMetadata {
 	return a.meta
 }
 
-// SetMeta sets the entry's metadata
-func (a *EntryAttributes) SetMeta(meta EntryMetadata) *EntryAttributes {
-	a.meta = meta
+// SetMeta sets the entry's meta attribute to obj. This is typically the
+// raw object that's returned by the plugin API's List endpoint, or a wrapper
+// that includes the raw object + some additional information. For example, if
+// the entry represents a Docker container, then obj would be a Container struct.
+// If the entry represents a Docker volume, then obj would be a Volume struct.
+//
+// SetMeta will panic if obj does not serialize to a JSON object.
+func (a *EntryAttributes) SetMeta(obj interface{}) *EntryAttributes {
+	a.meta = ToJSONObject(obj)
 	return a
 }
 
@@ -205,8 +214,11 @@ func (a *EntryAttributes) ToMap(includeMeta bool) map[string]interface{} {
 	return mp
 }
 
-// MarshalJSON marshals the entry's attributes to JSON.
-func (a *EntryAttributes) MarshalJSON() ([]byte, error) {
+// MarshalJSON marshals the entry's attributes to JSON. It takes
+// a value receiver so that the attributes are still marshaled
+// when they're referenced as interface{} objects. See
+// https://stackoverflow.com/a/21394657 for more details.
+func (a EntryAttributes) MarshalJSON() ([]byte, error) {
 	return json.Marshal(a.ToMap(true))
 }
 
@@ -252,12 +264,9 @@ func (a *EntryAttributes) UnmarshalJSON(data []byte) error {
 		}
 		a.SetSize(sz)
 	}
-	var meta EntryMetadata
-	if rawMeta, ok := mp["meta"]; ok {
-		meta, ok = rawMeta.(map[string]interface{})
-		if !ok {
-			return fmt.Errorf("meta is not a JSON object")
-		}
+	meta, ok := mp["meta"].(JSONObject)
+	if !ok {
+		return fmt.Errorf("meta is not a JSON object")
 	}
 	a.SetMeta(meta)
 	return nil
diff --git a/plugin/entryBase.go b/plugin/entryBase.go
index 998e347..15ebe4b 100644
--- a/plugin/entryBase.go
+++ b/plugin/entryBase.go
@@ -19,8 +19,21 @@ const (
 
 var defaultOpCodeToNameMap = [3]string{"List", "Open", "Metadata"}
 
-// EntryBase implements Entry, making it easy to create new entries.
-// You should use plugin.NewEntry to create new EntryBase objects.
+/*
+EntryBase implements Entry, making it easy to create new entries.
+You should use plugin.NewEntry to create new EntryBase objects.
+
+Each of the setters supports the builder pattern, which enables you
+to do something like
+
+	e := plugin.NewEntry("foo")
+	e.
+		DisableCachingFor(plugin.ListOp).
+		Attributes().
+		SetCtime(ctime).
+		SetMtime(mtime).
+		SetMeta(meta)
+*/
 type EntryBase struct {
 	entryName          string
 	attr               EntryAttributes
@@ -49,10 +62,8 @@ func NewEntry(name string) EntryBase {
 // ENTRY INTERFACE
 
 // Metadata returns the entry's meta attribute (see plugin.EntryAttributes).
-// This should be overridden if e's full metadata does not match the meta
-// attribute, which is true if the plugin API's List endpoint returns only a
-// subset of the full metadata.
-func (e *EntryBase) Metadata(ctx context.Context) (EntryMetadata, error) {
+// Override this if e has additional metadata.
+func (e *EntryBase) Metadata(ctx context.Context) (JSONObject, error) {
 	// Disable Metadata's caching in case the plugin author forgot to do this
 	e.DisableCachingFor(MetadataOp)
 
@@ -94,10 +105,18 @@ func (e *EntryBase) Name() string {
 	return e.name()
 }
 
-// SetAttributes sets the entry's attributes. Use it
-// after creating the entry via a call to NewEntry.
-func (e *EntryBase) SetAttributes(attr EntryAttributes) {
+// Attributes returns a pointer to the entry's attributes. Use it
+// to individually set the entry's attributes
+func (e *EntryBase) Attributes() *EntryAttributes {
+	return &e.attr
+}
+
+// SetAttributes sets the entry's attributes. Use it to set
+// the entry's attributes in a single operation, which is useful
+// when you've already pre-computed them.
+func (e *EntryBase) SetAttributes(attr EntryAttributes) *EntryBase {
 	e.attr = attr
+	return e
 }
 
 /*
@@ -106,30 +125,34 @@ character of '#' to char. The '/' replacement character is used
 when determining the entry's cname. See plugin.CName for more
 details.
 */
-func (e *EntryBase) SetSlashReplacementChar(char rune) {
+func (e *EntryBase) SetSlashReplacementChar(char rune) *EntryBase {
 	if char == '/' {
 		panic("e.SetSlashReplacementChar called with '/'")
 	}
 
 	e.slashReplacementCh = char
+	return e
 }
 
 // SetTTLOf sets the specified op's TTL
-func (e *EntryBase) SetTTLOf(op defaultOpCode, ttl time.Duration) {
+func (e *EntryBase) SetTTLOf(op defaultOpCode, ttl time.Duration) *EntryBase {
 	e.ttl[op] = ttl
+	return e
 }
 
 // DisableCachingFor disables caching for the specified op
-func (e *EntryBase) DisableCachingFor(op defaultOpCode) {
+func (e *EntryBase) DisableCachingFor(op defaultOpCode) *EntryBase {
 	e.SetTTLOf(op, -1)
+	return e
 }
 
 // DisableDefaultCaching disables the default caching
 // for List, Open and Metadata.
-func (e *EntryBase) DisableDefaultCaching() {
+func (e *EntryBase) DisableDefaultCaching() *EntryBase {
 	for op := range e.ttl {
 		e.DisableCachingFor(defaultOpCode(op))
 	}
+	return e
 }
 
 // SetTestID sets the entry's cache ID for testing.
diff --git a/plugin/externalPluginEntry.go b/plugin/externalPluginEntry.go
index ae37645..16501ae 100644
--- a/plugin/externalPluginEntry.go
+++ b/plugin/externalPluginEntry.go
@@ -124,13 +124,13 @@ func (e *externalPluginEntry) Open(ctx context.Context) (SizedReader, error) {
 }
 
 // Metadata displays the entry's metadata
-func (e *externalPluginEntry) Metadata(ctx context.Context) (EntryMetadata, error) {
+func (e *externalPluginEntry) Metadata(ctx context.Context) (JSONObject, error) {
 	stdout, err := e.script.InvokeAndWait(ctx, "metadata", e.id(), e.state)
 	if err != nil {
 		return nil, err
 	}
 
-	var metadata EntryMetadata
+	var metadata JSONObject
 	if err := json.Unmarshal(stdout, &metadata); err != nil {
 		activity.Record(
 			ctx,
diff --git a/plugin/kubernetes/namespace.go b/plugin/kubernetes/namespace.go
index 831402e..3bcfe0a 100644
--- a/plugin/kubernetes/namespace.go
+++ b/plugin/kubernetes/namespace.go
@@ -12,30 +12,23 @@ import (
 
 type namespace struct {
 	plugin.EntryBase
-	metadata      *corev1.Namespace
 	client        *k8s.Clientset
 	config        *rest.Config
 	resourcetypes []plugin.Entry
 }
 
 func newNamespace(name string, meta *corev1.Namespace, c *k8s.Clientset, cfg *rest.Config) *namespace {
-	ns := &namespace{EntryBase: plugin.NewEntry(name), metadata: meta, client: c, config: cfg}
+	ns := &namespace{EntryBase: plugin.NewEntry(name), client: c, config: cfg}
 	ns.resourcetypes = []plugin.Entry{
 		&pods{plugin.NewEntry("pods"), c, cfg, name},
 		&pvcs{plugin.NewEntry("persistentvolumeclaims"), c, name},
 	}
+	// TODO: Figure out other attributes that we could set here, if any.
+	ns.Attributes().SetMeta(meta)
 	return ns
 }
 
 func (n *namespace) List(ctx context.Context) ([]plugin.Entry, error) {
 	activity.Record(ctx, "Listing resource types for namespace %v: %v", n.Name(), n.resourcetypes)
 	return n.resourcetypes, nil
-}
-
-func (n *namespace) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
-	activity.Record(ctx, "Metadata for namespace %v: %+v", n.Name(), n.metadata)
-	if n.metadata != nil {
-		return plugin.ToMeta(n.metadata), nil
-	}
-	return plugin.EntryMetadata{}, nil
-}
+}
\ No newline at end of file
diff --git a/plugin/kubernetes/pod.go b/plugin/kubernetes/pod.go
index cbf7b13..5ad55f7 100644
--- a/plugin/kubernetes/pod.go
+++ b/plugin/kubernetes/pod.go
@@ -40,13 +40,12 @@ func newPod(ctx context.Context, client *k8s.Clientset, config *rest.Config, ns
 	}
 
 	meta := pdInfo.toMeta()
-	attr := plugin.EntryAttributes{}
-	attr.
+	pd.
+		Attributes().
 		SetCtime(p.CreationTimestamp.Time).
-		SetAtime(attr.Ctime()).
+		SetAtime(p.CreationTimestamp.Time).
 		SetSize(uint64(meta["LogSize"].(int))).
 		SetMeta(meta)
-	pd.SetAttributes(attr)
 
 	return pd, nil
 }
@@ -56,8 +55,8 @@ type podInfoResult struct {
 	logContent []byte
 }
 
-func (pdInfo podInfoResult) toMeta() plugin.EntryMetadata {
-	meta := plugin.ToMeta(pdInfo.pd)
+func (pdInfo podInfoResult) toMeta() plugin.JSONObject {
+	meta := plugin.ToJSONObject(pdInfo.pd)
 	meta["LogSize"] = len(pdInfo.logContent)
 	return meta
 }
@@ -95,15 +94,6 @@ func (p *pod) cachedPodInfo(ctx context.Context) (podInfoResult, error) {
 	return cachedPdInfo.(podInfoResult), nil
 }
 
-func (p *pod) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
-	pdInfo, err := p.cachedPodInfo(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return pdInfo.toMeta(), nil
-}
-
 func (p *pod) Open(ctx context.Context) (plugin.SizedReader, error) {
 	pdInfo, err := p.cachedPodInfo(ctx)
 	if err != nil {
diff --git a/plugin/kubernetes/pvc.go b/plugin/kubernetes/pvc.go
index 147650c..dcdbb9e 100644
--- a/plugin/kubernetes/pvc.go
+++ b/plugin/kubernetes/pvc.go
@@ -35,26 +35,16 @@ func newPVC(pi typedv1.PersistentVolumeClaimInterface, pd typedv1.PodInterface,
 		podi:      pd,
 	}
 
-	attr := plugin.EntryAttributes{}
-	attr.
+	vol.
+		Attributes().
 		SetCtime(p.CreationTimestamp.Time).
-		SetMtime(attr.Ctime()).
-		SetAtime(attr.Ctime())
-	vol.SetAttributes(attr)
+		SetMtime(p.CreationTimestamp.Time).
+		SetAtime(p.CreationTimestamp.Time).
+		SetMeta(p)
 
 	return vol
 }
 
-func (v *pvc) Metadata(ctx context.Context) (plugin.EntryMetadata, error) {
-	obj, err := v.pvci.Get(v.Name(), metav1.GetOptions{})
-	if err != nil {
-		return nil, err
-	}
-	activity.Record(ctx, "Metadata for persistent volume claim %v: %+v", v.Name(), obj)
-
-	return plugin.ToMeta(obj), nil
-}
-
 func (v *pvc) List(ctx context.Context) ([]plugin.Entry, error) {
 	return volume.List(ctx, v, "")
 }
diff --git a/plugin/types.go b/plugin/types.go
index ce53808..d614e4f 100644
--- a/plugin/types.go
+++ b/plugin/types.go
@@ -40,7 +40,7 @@ import (
 // Metadata returns a complete description of the entry. See the EntryBase documentation for more
 // details on when to override it.
 type Entry interface {
-	Metadata(ctx context.Context) (EntryMetadata, error)
+	Metadata(ctx context.Context) (JSONObject, error)
 	name() string
 	attributes() EntryAttributes
 	slashReplacementChar() rune
diff --git a/website/static/docs/api/api.json b/website/static/docs/api/api.json
index a630df9..a98e15e 100644
--- a/website/static/docs/api/api.json
+++ b/website/static/docs/api/api.json
@@ -415,8 +415,8 @@
       "title": "EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.",
       "x-go-package": "github.com/puppetlabs/wash/plugin"
     },
-    "EntryMetadata": {
-      "description": "EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.",
+    "JSONObject": {
+      "description": "JSONObject represents the entry's metadata\nas returned by Entry#Metadata.",
       "type": "object",
       "additionalProperties": {
         "type": "object"
@@ -536,10 +536,10 @@
     },
     "entryMetadata": {
       "schema": {
-        "$ref": "#/definitions/EntryMetadata"
+        "$ref": "#/definitions/JSONObject"
       },
       "headers": {
-        "EntryMetadata": {}
+        "JSONObject": {}
       }
     },
     "errorResp": {
diff --git a/website/static/docs/api/index.html b/website/static/docs/api/index.html
index a2c7f91..e0ce615 100644
--- a/website/static/docs/api/index.html
+++ b/website/static/docs/api/index.html
@@ -285,7 +285,7 @@ index within its activity history.</p>
 </div></div><div><h5 class="shv3r-4 gLzrjW">query<!-- --> Parameters</h5><table class="dz44d2-7 iYGBSY"><tbody><tr class="last undefined"><td class="dz44d2-1 dz44d2-3 ehjTDN" kind="field" title="Follow"><span class="dz44d2-5 jwaijW"></span>Follow</td><td class="dz44d2-4 iWnyBt"><div><div><span class="sc-1noysbl-1 sc-1noysbl-2 hYZGfb"></span><span class="sc-1noysbl-1 sc-1noysbl-3 Hidzy">boolean</span></div> <div><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"><p>stream updates when true</p>
 </div></div></div></td></tr></tbody></table></div><div><h3 class="zr04gj-0 jKmdQj"> Responses </h3><div><div class="sc-1kbihq-0 kXBEML"><svg class="sc-1g5rdgs-0 fgqOex" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>200<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>400<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>404<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div><div><div class="sc-1kbihq-0 jtbdMW"><svg class="sc-1g5rdgs-0 hkGyhI" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg><strong>500<!-- --> </strong><span class="sc-1m0b31p-0-span kdxwcC"></span></div></div></div></div><div class="sc-6itmo6-2 sc-6itmo6-3 iJUwRU"><div class="nrhuz6-0 kmORFN"><div class="nrhuz6-2 jmkkdP"><span class="nrhuz6-3 http-verb get klEXcZ" type="get"> <!-- -->get</span> <span class="nrhuz6-1 LxZmB">/history/{id}</span><svg class="sc-1g5rdgs-0 gtRsjv" style="margin-right:-25px" version="1.1" viewBox="0 0 24 24" x="0" xmlns="http://www.w3.org/2000/svg" y="0"><polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 "></polygon></svg></div><div class="nrhuz6-4 hKgcyu"><div class="nrhuz6-5 kjYYZz"><div class="sc-107yc6q-0 sc-1m0b31p-0 eabDir"></div><div><div class="nrhuz6-6 iuqUuR"><span></span>/history/{id}</div></div></div></div></div></div></div></div></div><div class="sc-13p9gjn-2 egwcPy"></div></div></div>
     <script>
-    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"EntryMetadata":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryMetadata"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"EntryMetadata":{"description":"EntryMetadata represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
+    const __redoc_state = {"menu":{"activeItemIdx":-1},"spec":{"data":{"openapi":"3.0.0","paths":{"/cache":{"delete":{"description":"Removes the specified entry and its children from the cache.","tags":["cache"],"summary":"Remove items from the cache","operationId":"cacheDelete","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/exec":{"post":{"description":"Executes a command on the remote system described by the supplied path.","tags":["exec"],"summary":"Execute a command on a remote system","operationId":"executeCommand","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"description":"Name of the executable to invoke","x-go-name":"Cmd","x-s2o-overloaded":true},"responses":{"200":{"$ref":"#/components/responses/ExecPacket"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"x-s2o-warning":"Operation executeCommand has multiple requestBodies","servers":[]}},"/fs/info":{"get":{"description":"Returns an Entry object describing the given path.","tags":["info"],"summary":"Info about entry at path","operationId":"entryInfo","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/Entry"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/list":{"get":{"description":"Returns a list of Entry objects describing children of the given path.","tags":["list"],"summary":"Lists children of a path","operationId":"listEntries","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryList"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/metadata":{"get":{"description":"Get metadata about the specified entry.","tags":["metadata"],"summary":"Get metadata","operationId":"getMetadata","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/entryMetadata"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/read":{"get":{"description":"Read content from the specified entry.","tags":["read"],"summary":"Read content","operationId":"readContent","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/fs/stream":{"get":{"description":"Get a stream of new updates to the specified entry.","tags":["stream"],"summary":"Stream updates","operationId":"streamUpdates","parameters":[{"description":"uniquely identifies an entry","name":"Path","in":"query","schema":{"type":"string"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history":{"get":{"description":"Get a list of commands that have been run via 'wash' and when they were run.","tags":["history"],"summary":"Get command history","operationId":"retrieveHistory","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/HistoryResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}},"/history/{id}":{"get":{"description":"Get the logs related to a particular command run via 'wash', requested by\nindex within its activity history.","tags":["journal"],"summary":"Get logs for a particular entry in history","operationId":"getJournal","parameters":[{"description":"stream updates when true","name":"Follow","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"$ref":"#/components/responses/octetResponse"},"400":{"$ref":"#/components/responses/errorResp"},"404":{"$ref":"#/components/responses/errorResp"},"500":{"$ref":"#/components/responses/errorResp"}},"servers":[]}}},"info":{"version":"","title":""},"components":{"responses":{"Entry":{"description":"Entry represents a Wash entry as interpreted by the API.","headers":{"actions":{"schema":{"type":"array","items":{"type":"string"}}},"attributes":{},"cname":{"schema":{"type":"string"}},"name":{"schema":{"type":"string"}},"path":{"schema":{"type":"string"}}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/EntryAttributes"}}}},"ExecPacket":{"description":"ExecPacket is a single packet of results from an exec.\nIf TypeField is Stdout or Stderr, Data will be a string.\nIf TypeField is Exitcode, Data will be an int (or float64 if deserialized from JSON).","headers":{"data":{},"error":{},"timestamp":{"schema":{"type":"string","format":"date-time"}},"type":{}},"content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorObj"}}}},"HistoryResponse":{"description":"HistoryResponse describes the result returned by the `/history` endpoint.","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Activity"}}}}},"entryList":{"description":"","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}}},"entryMetadata":{"headers":{"JSONObject":{}},"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/JSONObject"}}}},"errorResp":{"headers":{"Body":{}},"description":"","content":{"*/*":{"schema":{"type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}}}}}},"octetResponse":{"description":"","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Reader"}}}}},"schemas":{"Activity":{"type":"object","title":"Activity describes an activity from wash's `activity.History`.","properties":{"description":{"type":"string","x-go-name":"Description"},"start":{"type":"string","format":"date-time","x-go-name":"Start"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"Entry":{"type":"object","title":"Entry represents a Wash entry as interpreted by the API.","properties":{"actions":{"type":"array","items":{"type":"string"},"x-go-name":"Actions"},"attributes":{"$ref":"#/components/schemas/EntryAttributes"},"cname":{"type":"string","x-go-name":"CName"},"name":{"type":"string","x-go-name":"Name"},"path":{"type":"string","x-go-name":"Path"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"EntryAttributes":{"description":"Each of the setters supports the builder pattern, which enables you\nto do something like\n\nattr := plugin.EntryAttributes{}\nattr.\nSetCtime(ctime).\nSetMtime(mtime).\nSetMeta(meta)\nentry.SetAttributes(attr)","type":"object","title":"EntryAttributes represents an entry's attributes. We use a struct\ninstead of a map for efficient memory allocation/deallocation,\nwhich is needed to make Group#List fast.","x-go-package":"github.com/puppetlabs/wash/plugin"},"JSONObject":{"description":"JSONObject represents the entry's metadata\nas returned by Entry#Metadata.","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/plugin"},"ErrorFields":{"description":"ErrorFields represents the fields of an ErrorObj","type":"object","additionalProperties":{"type":"object"},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ErrorObj":{"description":"ErrorObj represents an API error object","type":"object","properties":{"fields":{"$ref":"#/components/schemas/ErrorFields"},"kind":{"description":"Identifies the kind of error.","type":"string","x-go-name":"Kind"},"msg":{"description":"A description of what failed.","type":"string","x-go-name":"Msg"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecOptions":{"description":"These are not identical to plugin.ExecOptions because initially the API only\nsupports receiving a string of input, not a reader.","type":"object","title":"ExecOptions are options that can be passed as part of an Exec call.","properties":{"input":{"description":"Input to pass on stdin when executing the command","type":"string","x-go-name":"Input"}},"x-go-package":"github.com/puppetlabs/wash/api/types"},"ExecPacketType":{"type":"string","title":"ExecPacketType identifies the packet type.","x-go-package":"github.com/puppetlabs/wash/plugin"},"Reader":{"description":"Read reads up to len(p) bytes into p. It returns the number of bytes\nread (0 <= n <= len(p)) and any error encountered. Even if Read\nreturns n < len(p), it may use all of p as scratch space during the call.\nIf some data is available but not len(p) bytes, Read conventionally\nreturns what is available instead of waiting for more.\n\nWhen Read encounters an error or end-of-file condition after\nsuccessfully reading n > 0 bytes, it returns the number of\nbytes read. It may return the (non-nil) error from the same call\nor return the error (and n == 0) from a subsequent call.\nAn instance of this general case is that a Reader returning\na non-zero number of bytes at the end of the input stream may\nreturn either err == EOF or err == nil. The next Read should\nreturn 0, EOF.\n\nCallers should always process the n > 0 bytes returned before\nconsidering the error err. Doing so correctly handles I/O errors\nthat happen after reading some bytes and also both of the\nallowed EOF behaviors.\n\nImplementations of Read are discouraged from returning a\nzero byte count with a nil error, except when len(p) == 0.\nCallers should treat a return of 0 and nil as indicating that\nnothing happened; in particular it does not indicate EOF.\n\nImplementations must not retain p.","type":"object","title":"Reader is the interface that wraps the basic Read method.","x-go-package":"io"}}}}},"searchIndex":{"store":["tag/cache","operation/cacheDelete","tag/exec","operation/executeCommand","tag/info","operation/entryInfo","tag/list","operation/listEntries","tag/metadata","operation/getMetadata","tag/read","operation/readContent","tag/stream","operation/streamUpdates","tag/history","operation/retrieveHistory","tag/journal","operation/getJournal"],"index":{"version":"2.3.6","fields":["title","description"],"fieldVectors":[["title/0",[0,2.078]],["description/0",[]],["title/1",[0,1.342,1,1.609,2,2.015]],["description/1",[0,1.355,1,1.625,3,1.154,4,0.644,5,1.355]],["title/2",[6,3.119]],["description/2",[]],["title/3",[7,1.367,8,0.836,9,1.367,10,1.367]],["description/3",[7,1.342,8,0.82,9,1.342,10,1.342,11,1.119,12,1.68,13,0.82]],["title/4",[14,2.491]],["description/4",[]],["title/5",[4,0.638,13,0.984,14,1.609]],["description/5",[4,0.583,11,1.226,13,0.898,15,1.47,16,1.47,17,1.47]],["title/6",[18,1.769]],["description/6",[]],["title/7",[5,1.342,13,0.984,18,1.143]],["description/7",[4,0.49,5,1.03,11,1.03,13,0.755,15,1.235,16,1.235,17,1.235,18,0.877]],["title/8",[19,2.078]],["description/8",[]],["title/9",[19,2.078]],["description/9",[3,1.462,4,0.816,19,1.717]],["title/10",[20,2.078]],["description/10",[]],["title/11",[20,1.631,21,1.955]],["description/11",[3,1.29,4,0.72,20,1.515,21,1.816]],["title/12",[22,2.078]],["description/12",[]],["title/13",[22,1.631,23,1.955]],["description/13",[3,1.154,4,0.644,22,1.355,23,1.625,24,2.034]],["title/14",[25,1.769]],["description/14",[]],["title/15",[8,1.195,25,1.389]],["description/15",[8,0.898,18,1.044,26,2.211,27,1.47,28,1.47]],["title/16",[29,3.119]],["description/16",[]],["title/17",[4,0.542,25,0.971,30,1.367,31,1.367]],["description/17",[8,0.572,25,0.664,26,0.935,27,0.935,28,0.935,30,0.935,31,0.935,32,1.171,33,1.171,34,1.171,35,1.171,36,1.171]]],"invertedIndex":[["activ",{"_index":36,"title":{},"description":{"17":{}}}],["cach",{"_index":0,"title":{"0":{},"1":{}},"description":{"1":{}}}],["children",{"_index":5,"title":{"7":{}},"description":{"1":{},"7":{}}}],["command",{"_index":8,"title":{"3":{},"15":{}},"description":{"3":{},"15":{},"17":{}}}],["content",{"_index":21,"title":{"11":{}},"description":{"11":{}}}],["describ",{"_index":11,"title":{},"description":{"3":{},"5":{},"7":{}}}],["entri",{"_index":4,"title":{"5":{},"17":{}},"description":{"1":{},"5":{},"7":{},"9":{},"11":{},"13":{}}}],["exec",{"_index":6,"title":{"2":{}},"description":{}}],["execut",{"_index":7,"title":{"3":{}},"description":{"3":{}}}],["given",{"_index":17,"title":{},"description":{"5":{},"7":{}}}],["histori",{"_index":25,"title":{"14":{},"15":{},"17":{}},"description":{"17":{}}}],["index",{"_index":34,"title":{},"description":{"17":{}}}],["info",{"_index":14,"title":{"4":{},"5":{}},"description":{}}],["item",{"_index":2,"title":{"1":{}},"description":{}}],["journal",{"_index":29,"title":{"16":{}},"description":{}}],["list",{"_index":18,"title":{"6":{},"7":{}},"description":{"7":{},"15":{}}}],["log",{"_index":30,"title":{"17":{}},"description":{"17":{}}}],["metadata",{"_index":19,"title":{"8":{},"9":{}},"description":{"9":{}}}],["new",{"_index":24,"title":{},"description":{"13":{}}}],["object",{"_index":16,"title":{},"description":{"5":{},"7":{}}}],["particular",{"_index":31,"title":{"17":{}},"description":{"17":{}}}],["path",{"_index":13,"title":{"5":{},"7":{}},"description":{"3":{},"5":{},"7":{}}}],["read",{"_index":20,"title":{"10":{},"11":{}},"description":{"11":{}}}],["relat",{"_index":32,"title":{},"description":{"17":{}}}],["remot",{"_index":9,"title":{"3":{}},"description":{"3":{}}}],["remov",{"_index":1,"title":{"1":{}},"description":{"1":{}}}],["request",{"_index":33,"title":{},"description":{"17":{}}}],["return",{"_index":15,"title":{},"description":{"5":{},"7":{}}}],["run",{"_index":26,"title":{},"description":{"15":{},"17":{}}}],["specifi",{"_index":3,"title":{},"description":{"1":{},"9":{},"11":{},"13":{}}}],["stream",{"_index":22,"title":{"12":{},"13":{}},"description":{"13":{}}}],["suppli",{"_index":12,"title":{},"description":{"3":{}}}],["system",{"_index":10,"title":{"3":{}},"description":{"3":{}}}],["updat",{"_index":23,"title":{"13":{}},"description":{"13":{}}}],["via",{"_index":27,"title":{},"description":{"15":{},"17":{}}}],["wash",{"_index":28,"title":{},"description":{"15":{},"17":{}}}],["within",{"_index":35,"title":{},"description":{"17":{}}}]],"pipeline":[]}},"options":{"nativeScrollbars":true}};
 
     var container = document.getElementById('redoc');
     Redoc.hydrate(__redoc_state, container);;


Test patch:
diff --git a/plugin/cache_test.go b/plugin/cache_test.go
index 43b2ac1..2adc54b 100644
--- a/plugin/cache_test.go
+++ b/plugin/cache_test.go
@@ -137,9 +137,9 @@ func (e *cacheTestsMockEntry) Open(ctx context.Context) (SizedReader, error) {
 	return args.Get(0).(SizedReader), args.Error(1)
 }
 
-func (e *cacheTestsMockEntry) Metadata(ctx context.Context) (EntryMetadata, error) {
+func (e *cacheTestsMockEntry) Metadata(ctx context.Context) (JSONObject, error) {
 	args := e.Called(ctx)
-	return args.Get(0).(EntryMetadata), args.Error(1)
+	return args.Get(0).(JSONObject), args.Error(1)
 }
 
 type cachedDefaultOpFunc func(ctx context.Context, e Entry) (interface{}, error)
@@ -337,8 +337,8 @@ func (suite *CacheTestSuite) TestCachedOpen() {
 }
 
 func (suite *CacheTestSuite) TestCachedMetadata() {
-	mockEntryMetadata := EntryMetadata{"foo": "bar"}
-	suite.testCachedDefaultOp(MetadataOp, "Metadata", mockEntryMetadata, mockEntryMetadata, func(ctx context.Context, e Entry) (interface{}, error) {
+	mockJSONObject := JSONObject{"foo": "bar"}
+	suite.testCachedDefaultOp(MetadataOp, "Metadata", mockJSONObject, mockJSONObject, func(ctx context.Context, e Entry) (interface{}, error) {
 		return CachedMetadata(ctx, e)
 	})
 }
diff --git a/plugin/entryAttributes_test.go b/plugin/entryAttributes_test.go
index 06ebae8..6384e9f 100644
--- a/plugin/entryAttributes_test.go
+++ b/plugin/entryAttributes_test.go
@@ -13,19 +13,21 @@ type EntryAttributesTestSuite struct {
 	suite.Suite
 }
 
-func (suite *EntryAttributesTestSuite) TestToMeta() {
+func (suite *EntryAttributesTestSuite) TestToJSONObject() {
 	cases := []struct {
 		input    interface{}
-		expected EntryMetadata
+		expected JSONObject
 	}{
-		{[]byte(`{"hello": [1, 2, 3]}`), EntryMetadata{"hello": []interface{}{1.0, 2.0, 3.0}}},
+		{[]byte(`{"hello": [1, 2, 3]}`), JSONObject{"hello": []interface{}{1.0, 2.0, 3.0}}},
 		{struct {
 			Name  string
 			Value []int
-		}{"me", []int{1, 2, 3}}, EntryMetadata{"Name": "me", "Value": []interface{}{1.0, 2.0, 3.0}}},
+		}{"me", []int{1, 2, 3}}, JSONObject{"Name": "me", "Value": []interface{}{1.0, 2.0, 3.0}}},
+		{map[string]interface{}{"1": 2}, JSONObject{"1": 2}},
+		{JSONObject{"1": 2}, JSONObject{"1": 2}},
 	}
 	for _, c := range cases {
-		actual := ToMeta(c.input)
+		actual := ToJSONObject(c.input)
 		suite.Equal(c.expected, actual)
 	}
 }
@@ -46,9 +48,9 @@ func (suite *EntryAttributesTestSuite) TestEntryAttributes() {
 		return unmarshaledTime
 	}
 	attr := EntryAttributes{}
-	attr.meta = EntryMetadata{}
+	attr.meta = JSONObject{}
 	expectedMp := make(map[string]interface{})
-	expectedMp["meta"] = EntryMetadata{}
+	expectedMp["meta"] = JSONObject{}
 	doUnmarshalJSONTests := func() {
 		attrJSON, err := json.Marshal(expectedMp)
 		if err != nil {
@@ -117,8 +119,8 @@ func (suite *EntryAttributesTestSuite) TestEntryAttributes() {
 	doUnmarshalJSONTests()
 
 	// Tests for Meta
-	suite.Equal(EntryMetadata{}, attr.Meta())
-	meta := EntryMetadata{"foo": "bar"}
+	suite.Equal(JSONObject{}, attr.Meta())
+	meta := JSONObject{"foo": "bar"}
 	attr.SetMeta(meta)
 	expectedMp["meta"] = meta
 	suite.Equal(meta, attr.Meta())
diff --git a/plugin/entryBase_test.go b/plugin/entryBase_test.go
index ced270e..8cea35a 100644
--- a/plugin/entryBase_test.go
+++ b/plugin/entryBase_test.go
@@ -61,7 +61,7 @@ func (suite *EntryBaseTestSuite) TestMetadata() {
 
 	meta, err := e.Metadata(context.Background())
 	if suite.NoError(err) {
-		suite.Equal(EntryMetadata{}, meta)
+		suite.Equal(JSONObject{}, meta)
 	}
 	suite.assertOpTTL(e, MetadataOp, "Metadata", -1)
 }
diff --git a/plugin/externalPluginEntry_test.go b/plugin/externalPluginEntry_test.go
index 8018409..5a1bd4d 100644
--- a/plugin/externalPluginEntry_test.go
+++ b/plugin/externalPluginEntry_test.go
@@ -234,7 +234,7 @@ func (suite *ExternalPluginEntryTestSuite) TestMetadata() {
 	mockInvokeAndWait([]byte(stdout), nil)
 	metadata, err := entry.Metadata(ctx)
 	if suite.NoError(err) {
-		expectedMetadata := EntryMetadata{"key": "value"}
+		expectedMetadata := JSONObject{"key": "value"}
 		suite.Equal(expectedMetadata, metadata)
 	}
 }