Repository: puppetlabs/wash
Pull request #739 (2020-02-27 10:32:01)

Include core plugin entry sgraph in external plugin entry sgraph


Patch:
diff --git a/docs/_config.yml b/docs/_config.yml
index 220f656..42daa3e 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -7,16 +7,10 @@ google_analytics: UA-144580607-2
 exclude: [vendor, Boltdir, acceptance, modules, .git, Gemfile, Gemfile.lock]
 
 collections:
-  docs:
-    output: true
   tutorials:
     output: true
 
 defaults:
-  - scope:
-      type: docs
-    values:
-      layout: default
   - scope:
       type: tutorials
     values:
diff --git a/docs/_docs/analytics.md b/docs/_docs/analytics.md
deleted file mode 100644
index 1abd949..0000000
--- a/docs/_docs/analytics.md
+++ /dev/null
@@ -1,27 +0,0 @@
----
-title: Analytics
----
-
-Wash collects anonymous data about how you use it. You can opt out of providing this data.
-
-## What data does Wash collect?
-* Version of Wash
-* User locale
-* Architecture
-* Method invocations (for shipped plugins only)
-  * This includes any Wash action invocation
-  * It also includes the entry's plugin
-
-This data is associated with Bolt analytics' UUID (if available); otherwise, the data is associated with a random, non-identifiable user UUID.
-
-## Why does Wash collect data?
-Wash collects data to help us understand how it's being used and make decisions about how to improve it.
-
-## How can I opt out of Wash data collection?
-To disable the collection of analytics data add the following line to `~/.puppetlabs/wash/analytics.yaml`:
-
-```
-disabled: true
-```
-
-You can also disable the collection of analytics data by setting the `WASH_DISABLE_ANALYTICS` environment variable to `true` before starting up the Wash daemon.
diff --git a/docs/_docs/commands.md b/docs/_docs/commands.md
deleted file mode 100644
index f1eed2f..0000000
--- a/docs/_docs/commands.md
+++ /dev/null
@@ -1,99 +0,0 @@
----
-title: Commands
----
-
-* [wash](#wash)
-* [wash clear](#wash-clear)
-* [wash exec](#wash-exec)
-* [wash find](#wash-find)
-* [wash history](#wash-history)
-* [wash info](#wash-info)
-* [wash ls](#wash-ls)
-* [wash meta](#wash-meta)
-* [wash ps](#wash-ps)
-* [wash server](#wash-server)
-* [wash stree](#wash-stree)
-* [wash tail](#wash-tail)
-* [wash validate](#wash-validate)
-* [wash docs](#wash-docs)
-* [wash delete](#wash-delete)
-* [wash signal](#wash-signal)
-
-Wash commands aim to be well-documented in the tool. Try `wash help` and `wash help <command>` for specific options.
-
-Most commands operate on Wash resources, which are addressed by their path in the filesystem.
-
-## wash
-
-The `wash` command can be invoked on its own to enter a Wash shell.
-
-Invoking `wash` starts the daemon as part of the process, then enters your current system shell with shortcuts configured for Wash commands. All the [`wash server`](#wash-server) settings are also supported with `wash` except `socket`; `wash` ignores that setting and creates a temporary location for the socket.
-
-## wash clear
-
-Wash caches most operations. If the resource you're querying appears out-of-date, use this subcommand to reset the cache for resources at or contained within the specified paths. Defaults to the current directory if no path is provided.
-
-## wash exec
-
-For a Wash resource that implements the ability to execute a command, run the specified command and arguments. The results will be forwarded from the target on stdout, stderr, and exit code.
-
-## wash find
-
-Recursively descends the directory tree of the specified paths, evaluating an `expression` composed of `primaries` and `operands` for each entry in the tree.
-
-## wash history
-
-Wash maintains a history of commands executed through it. Print that command history, or specify an `id` to print a log of activity related to a particular command.
-
-Journals are stored in `wash/activity` under your user cache directory, identified by process ID and executable name. The user cache directory is `$XDG_CACHE_HOME` or `$HOME/.cache` on Unix systems, `$HOME/Library/Caches` on macOS, and `%LocalAppData%` on Windows.
-
-## wash info
-
-Prints the entries' info at the specified paths.
-
-## wash ls
-
-Lists the children of the specified paths, or current directory if no path is specified. If the `-l` option is set, then the name, last modified time, and supported actions are displayed for each child.
-
-## wash meta
-
-Prints the metadata of the given entries. By default, meta prints the full metadata as returned by the metadata endpoint. Specify the `--partial` flag to instead print the partial metadata, a (possibly) reduced set of metadata that's returned when entries are enumerated.
-
-## wash ps
-
-Captures /proc/*/{cmdline,stat,statm} on each node by executing 'cat' on them. Collects the output
-to display running processes on all listed nodes. Errors on paths that don't implement exec.
-
-## wash server
-
-Initializes all of the plugins, then sets up the Wash daemon (its API and [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) servers). To stop it, make sure you're not using the filesystem at the specified mountpoint, then enter Ctrl-C.
-
-Server API docs can be found [here](api). The server config is described in the [`config`](#config) section.
-
-## wash stree
-
-Displays the entry's stree (schema-tree), which is a high-level overview of the entry's hierarchy. Non-singleton types are bracketed with "[]".
-
-## wash tail
-
-Output any new updates to files and/or resources (that support the stream action). Currently requires the '-f' option to run. Attempts to mimic the functionality of `tail -f` for remote logs.
-
-## wash validate
-
-Validates an external plugin, using it's schema to limit exploration. The plugin can be one you've configured in Wash's config file, or it can be a script to load as an external plugin. Plugin-specific config from Wash's config file will be used. The Wash daemon does not need to be running to use this command.
-
-Validate starts from the plugin root and does a breadth-first traversal of the plugin hierarchy, invoking all supported methods on an example at each level. If the plugin provides a schema, it will be used to explore one example of each type of entry. Exploration can be stopped with Ctrl-C when needed.
-
-Each line represents validation of an entry type. The `lrsx` fields represent support for `list`, `read`, `stream`, and `execute` methods respectively, with '-' representing lack of support for a method.
-
-## wash docs
-
-Displays the entry's documentation. This is currently its description and any supported signals/signal groups.
-
-## wash delete
-
-Deletes the entries at the specified paths, prompting the user for confirmation before deleting each entry.
-
-## wash signal
-
-Sends the specified signal to the entries at the specified paths.
diff --git a/docs/_docs/concepts.md b/docs/_docs/concepts.md
deleted file mode 100644
index cad745e..0000000
--- a/docs/_docs/concepts.md
+++ /dev/null
@@ -1,339 +0,0 @@
----
-title: Concepts
----
-* [CName](#cname)
-  * [Examples](#examples)
-* [Actions](#actions)
-  * [list](#list)
-    * [Examples](#examples-1)
-  * [read](#read)
-    * [Examples](#examples-2)
-  * [write](#write)
-    * [Examples](#examples-3)
-  * [stream](#stream)
-    * [Examples](#examples-4)
-  * [exec](#exec)
-    * [Examples](#examples-5)
-  * [delete](#delete)
-    * [Examples](#examples-6)
-  * [signal](#signal)
-    * [Examples](#examples-7)
-    * [Common Signals](#common-signals)
-* [Attributes](#attributes)
-  * [crtime](#crtime)
-    * [Example JSON](#example-json)
-  * [mtime](#mtime)
-    * [Example JSON](#example-json-1)
-  * [ctime](#ctime)
-    * [Example JSON](#example-json-2)
-  * [atime](#atime)
-    * [Example JSON](#example-json-3)
-  * [size](#size)
-    * [Example JSON](#example-json-4)
-  * [mode](#mode)
-    * [Example JSON](#example-json-5)
-  * [os](#os)
-    * [Example JSON](#example-json-6)
-
-## CName
-
-CName is short for _canonical name_. An entry's cname is its name with all `/`'es replaced by that entry's _slash replacer_. The default slash replacer is `#`.
-
-Wash uses the cname to construct the entry's path. An entry's path is defined as
-```
-    <mountpoint>/<parent_cname1>/<parent_cname2>/.../<cname>
-```
-
-### Examples
-Consider the entry `/myplugin/foo`. If `bar/baz` is a child of `foo`, then its cname and path would be `bar#baz` and `/myplugin/foo/bar#baz`, respectively. Similarly if `qux` is a child of `foo`, then its cname and path would be `qux` and `/myplugin/foo/qux`.
-
-Conversely, if `bar/baz`'s slash replacer is set to `:`, then its cname and path would now be `bar:baz` and `/myplugin/foo/bar:baz`.
-
-## Actions
-
-### list
-The `list` action lets you list an entry’s children. Entries that support list are represented as directories. Thus, any command that works with directories also work with these entries.
-
-#### Examples
-```
-wash . ❯ ls gcp/Wash/storage/some-wash-stuff
-an example folder reaper.sh
-```
-
-```
-wash . ❯ cd gcp/Wash/storage/some-wash-stuff
-wash gcp/Wash/storage/some-wash-stuff ❯
-```
-
-```
-wash . ❯ tree gcp/Wash/storage/some-wash-stuff
-gcp/Wash/storage/some-wash-stuff
-├── an\ example\ folder
-│   └── static.sh
-└── reaper.sh
-
-1 directory, 2 files
-```
-
-### read
-The `read` action lets you read data from an entry. Thus, any command that reads a file also works with these entries.
-
-#### Examples
-```
-wash . ❯ cat gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
-#!/bin/sh
-
-echo "Hello, world!"
-```
-
-```
-wash . ❯ grep "Hello" gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
-echo "Hello, world!"
-```
-
-### write
-The `write` action lets you write data to an entry. Thus, any command that writes a file also works with these entries.
-
-Note that Wash distinguishes between file-like and non-file-like entries. An entry is file-like if it's readable and writable and defines its size; you can edit it like a file.
-
-If it doesn't define a size then it's non-file-like, and trying to open it with a ReadWrite handle will error; reads from it may not return data you previously wrote to it. You should check its documentation with the `docs` command for that entry's write semantics. We also recommend not using editors with these entries to avoid weird behavior.
-
-#### Examples
-Modifying a file stored in Google Cloud Storage
-```
-wash . ❯ echo 'exit 1' >> gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
-wash . ❯ cat gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
-#!/bin/sh
-
-echo "Hello, world!"
-exit 1
-```
-
-Writing a message to a hypothetical message queue where each write publishes a message and each read consumes a message
-```
-wash > echo 'message 1' >> myqueue
-wash > echo 'message 2' >> myqueue
-wash > cat myqueue
-message 1
-wash > cat myqueue
-message 2
-```
-
-### stream
-The `stream` action lets you stream an entry’s content for updates.
-
-#### Examples
-```
-wash . ❯ tail -f gcp/Wash/compute/instance-1/fs/var/log/messages
-===> gcp/Wash/compute/instance-1/fs/var/log/messages <===
-Aug 27 06:25:01 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="17499" x-info="http://www.rsyslog.com"] rsyslogd was HUPed
-
-Aug 27 13:26:32 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="17499" x-info="http://www.rsyslog.com"] exiting on signal 15.
-
-Aug 27 13:26:32 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="24583" x-info="http://www.rsyslog.com"] start
-Aug 28 00:30:04 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="24583" x-info="http://www.rsyslog.com"] exiting on signal 15.
-Aug 28 00:30:04 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="32147" x-info="http://www.rsyslog.com"] start
-Aug 28 06:25:01 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="32147" x-info="http://www.rsyslog.com"] rsyslogd was HUPed
-
-Aug 28 09:54:34 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="32147" x-info="http://www.rsyslog.com"] exiting on signal 15.
-
-Aug 28 09:54:34 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="6687" x-info="http://www.rsyslog.com"] start
-Aug 28 19:01:21 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="6687" x-info="http://www.rsyslog.com"] exiting on signal 15.
-
-Aug 28 19:01:21 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="12804" x-info="http://www.rsyslog.com"] start
-```
-
-(Hit `Ctrl+C` to cancel `tail -f`)
-
-### exec
-The `exec` action lets you execute a command on an entry.
-
-#### Examples
-```
-wash . ❯ wexec gcp/Wash/compute/instance-1 uname
-Linux
-```
-
-### delete
-The `delete` action lets you delete an entry.
-
-#### Examples
-```
-wash . ❯ delete docker/containers/quizzical_colden
-remove docker/containers/quizzical_colden?: y
-```
-
-### signal
-The `signal` action lets you signal an entry. Use the `docs` command to view an entry's supported signals.
-
-#### Examples
-```
-wash . ❯ docs docker/containers/wash_tutorial_redis_1
-No description provided.
-
-SUPPORTED SIGNALS
-* start
-    Starts the container. Equivalent to 'docker start <container>'
-* stop
-    Stops the container. Equivalent to 'docker stop <container>'
-* pause
-    Suspends all processes in the container. Equivalent to 'docker pause <container>'
-* resume
-    Un-suspends all processes in the container. Equivalent to 'docker unpause <container>'
-* restart
-    Restarts the container. Equivalent to 'docker restart <container>'
-
-SUPPORTED SIGNAL GROUPS
-* linux
-    Consists of all the supported Linux signals like SIGHUP, SIGKILL. Equivalent to
-    'docker kill <container> --signal <signal>'
-```
-
-```
-wash . ❯ signal start docker/containers/wash_tutorial_redis_1
-wash . ❯
-```
-
-#### Common Signals
-* start
-* stop
-* pause
-* resume
-* restart
-* hibernate
-* reset
-
-## Attributes
-
-### crtime
-This is the entry's creation time.
-
-#### Example JSON
-As a stringified date
-
-```
-{
-  "crtime": “2019-09-25T21:39:57-07:00”
-}
-```
-
-In UNIX seconds
-
-```
-{
-  "crtime": 1569472797
-}
-```
-
-### mtime
-This is the entry's last modification time.
-
-#### Example JSON
-As a stringified date
-
-```
-{
-  "mtime": “2019-09-25T21:39:57-07:00”
-}
-```
-
-In UNIX seconds
-
-```
-{
-  "mtime": 1569472797
-}
-```
-
-### ctime
-This is the entry's last change time.
-
-#### Example JSON
-As a stringified date
-
-```
-{
-  "ctime": “2019-09-25T21:39:57-07:00”
-}
-```
-
-In UNIX seconds
-
-```
-{
-  "ctime": 1569472797
-}
-```
-
-### atime
-This is the entry's last access time.
-
-#### Example JSON
-As a stringified date
-
-```
-{
-  "atime": “2019-09-25T21:39:57-07:00”
-}
-```
-
-In UNIX seconds
-
-```
-{
-  "atime": 1569472797
-}
-```
-
-### size
-This is the entry's content size.
-
-#### Example JSON
-```
-{
-  "size": 1024
-}
-```
-
-### mode
-This is the entry's mode.
-
-#### Example JSON
-
-```
-{
-  "mode": 16832
-}
-```
-
-As a hexadecimal string
-
-```
-{
-  "mode": "41C0"
-}
-```
-
-As an octal string
-
-```
-{
-  "mode": "40700"
-}
-```
-
-### os
-This contains information about the operating system of an entry, if it has one.
-
-The `OS` property is an object containing
-* `login_shell` - The shell used when remotely logging into the machine, such as over SSH or WinRM. It can be one of `posixshell` (representing a POSIX-compatible shell such as bash or zsh) or `powershell` (PowerShell). This is commonly used to decide what types of shell is available for scripting; for example the `wps` command runs different commands to find process information based on the login shell.
-
-#### Example JSON
-
-```
-{
-  "os": {
-    "login_shell": "posixshell"
-  }
-}
-```
diff --git a/docs/_docs/config.md b/docs/_docs/config.md
deleted file mode 100644
index 3aa0d55..0000000
--- a/docs/_docs/config.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-title: Config
----
-
-## wash.yaml
-
-The Wash config file is located at `~/.puppetlabs/wash/wash.yaml`, and can be used to configure the [`wash-server`](#wash-server). You can override this location via the `config-file` flag.
-
-Below are all the configurable options.
-
-* `logfile` - The location of the server's log file (default `stdout`)
-* `loglevel` - The server's loglevel (default `info`)
-* `cpuprofile` - The location that the server's CPU profile will be written to (optional)
-* `external-plugins` - The external plugins that will be loaded. See [➠External Plugins]
-* `plugins` - A list of shipped plugins to enable. If omitted or empty, it will load all of the shipped plugins. Note that Wash ships with the `docker`, `kubernetes`, `aws`, and `gcp` plugins.
-* `socket` - The location of the server's socket file (default `<user_cache_dir>/wash/wash-api.sock`)
-
-All options except for `external-plugins` can be overridden by setting the `WASH_<option>` environment variable with option converted to ALL CAPS.
-
-NOTE: Do not override `socket` in a config file. Instead, override it via the `WASH_SOCKET` environment variable. Otherwise, Wash's commands will not be able to interact with the server because they cannot access the socket.
-
-## wash shell
-
-Wash uses your system shell to provide the shell environment. It determines this using the `SHELL` environment variable or falls back to `/bin/sh`, so if you'd like to specify a particular shell set the `SHELL` environment variable before starting Wash.
-
-Wash uses the following environment variables
-
-- `WASH_SOCKET` determines how to communicate with the Wash daemon
-- `W` describes the path to Wash's starting directory on the host filesystem; use `cd $W` to return to the start or `ls $W/...` to list things relative to Wash's root
-- `PATH` is prefixed with the location of the Wash binary and any other executables it creates
-
-For some shells, Wash provides a customized environment. Please [file an issue](https://github.com/puppetlabs/wash/issues/new?assignees=&labels=Feature&template=feature-request.md) if you'd like to add support for new shells.
-
-Wash currently provides a customized environment for
-
-- `bash`
-- `zsh`
-
-Customized environments alias Wash subcommands to save typing out `wash <subcommand>` so they feel like shell builtins. If you want to use an executable or builtin Wash has overridden, please use its full path or the `builtin` command.
-
-Customized environments also supports reading `~/.washenv` and `~/.washrc` files. These files are loaded as follows:
-
-1. If running Wash non-interactively (by piping `stdin` or passing the `-c` option)
-   1. If `~/.washenv` does not exist, load the shell's default non-interactive config (such as `.zshenv` or from `BASH_ENV`)
-   2. Configure subcommand aliases
-   3. If `~/.washenv` exists, load it
-2. If running Wash interactively
-   1. Do all non-interactive config above
-   2. If `~/.washrc` does not exist, load the shell's default interactive config (such as `.bash_profile` or `.zshrc`)
-   3. Re-configure subcommand aliases, and configure the command prompt
-   4. If `~/.washrc` exists, load it
-
-That order ensures that the out-of-box experience of Wash is not adversely impacted by your existing environment while still inheriting most of your config. If you customize your Wash environment with `.washenv` and `.washrc`, be aware that it's possible to override Wash's default prompt and aliases.
-
-For other shells, Wash creates executables for subcommands and does no other customization.
diff --git a/docs/_includes/header.html b/docs/_includes/header.html
deleted file mode 100644
index ca479b2..0000000
--- a/docs/_includes/header.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% if site.google_analytics %}
-<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script>
-<script>
-  window.dataLayer = window.dataLayer || [];
-  function gtag(){dataLayer.push(arguments);}
-  gtag('js', new Date());
-  gtag('config', '{{ site.google_analytics }}');
-</script>
-{% endif %}
-<meta charset="UTF-8">
-
-<!-- Add asciinema-player -->
-<link rel="stylesheet" href="{{ '/assets/css/asciinema-player.css' | relative_url }}">
-<link rel="stylesheet" href="{{ '/assets/css/asciinema-player-custom.css' | relative_url }}">
-<script src="{{ '/assets/js/asciinema-player.js' | relative_url }}"></script>
-
-<!-- Add jQuery -->
-<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
-<script src="{{ '/assets/js/jquery.timeago.js' | relative_url }}"></script>
-
-{% seo %}
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<meta name="theme-color" content="#157878">
-<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
-<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
-<link rel="icon" href="{{ '/favicon.ico' | relative_url }}" type="image/x-icon" />
-{% feed_meta %}
diff --git a/docs/_includes/sidebar.html b/docs/_includes/sidebar.html
deleted file mode 100644
index 986d50b..0000000
--- a/docs/_includes/sidebar.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<nav class="sidebar">
-  <a href="{{ '/' | relative_url }}">
-    <img src="{{ '/assets/images/wash-logo-lg.png' | relative_url }}" class="wash-logo" alt="">
-  </a>
-
-  <ul class="nav-list">
-    <li>
-      <span class="nav-header">
-        <a href="{{ '/getting_started' | relative_url }}" class="center-vertical">Getting Started</a>
-      </span>
-    </li>
-
-    <li>
-      <span class="nav-header center-vertical">Documentation</span>
-      <ul>
-        {% for doc in site.docs %}
-          <li><a href="{{ doc.url | remove: "/index.html" | relative_url }}">{{ doc.title }}</a></li>
-        {% endfor %}
-        <li><a href="{{ '/docs/api.html' | relative_url }}">Server API</a></li>
-      </ul>
-    </li>
-
-    <li>
-      <span class="nav-header">
-        <a href="{{ '/known_issues' | relative_url }}" class="center-vertical">Known Issues</a>
-      </span>
-    </li>
-
-    <li>
-      <span class="nav-header">
-        <a href="{{ '/release_announcements' | relative_url }}" class="center-vertical">Release Announcements</a>
-      </span>
-    </li>
-
-    <li>
-      <span class="nav-header">
-        <a href="{{ '/contributing' | relative_url }}" class="center-vertical">Contributing</a>
-      </span>
-    </li>
-
-    <li>
-      <span class="nav-header">
-        <a href="{{ '/posts' | relative_url }}" class="center-vertical">News</a>
-      </span>
-      <ul>
-        {% for post in site.posts limit: 3 %}
-          <li><a href="{{ post.url | remove: "/index.html" | relative_url }}">{{ post.title }}</a> <small>{{ post.date | date: "%Y-%m-%d" }}</small></li>
-        {% endfor %}
-      </ul>
-    </li>
-
-    <li>
-      <span class="nav-header">
-        <a href="{{ '/tutorials' | relative_url }}" class="center-vertical">Tutorials</a>
-      </span>
-      {% for tutorial in site.tutorials %}
-        <!--
-          The tutorial.main check is here so that Jekyll collections doesn't interpret each
-          section as its own tutorial.
-        -->
-        {% if tutorial.main %}
-          {% assign tutorial_url = tutorial.url | remove: "/index.html" | relative_url %}
-          <ul>
-            <!--
-              Figure out tutorial_link. If the tutorial's 'main' page doesn't have any content, then
-              this will link to the tutorial's first section. Otherwise, we'll link to the tutorial's
-              URL. This avoids unnecessary 'main' pages for tutorials that don't need them (like e.g.
-              'Debugging').
-            -->
-            {% assign tutorial_link = tutorial_url %}
-            {% assign content_size = tutorial.content | strip_newlines | size %}
-            {% if content_size <= 0 %}
-              {% assign tutorial_link = tutorial_url | append: '/' | append: tutorial.sections.first['endpoint'] %}
-            {% endif %}
-            <li><a href="{{ tutorial_link }}" class="center-vertical">{{tutorial.title}}</a>
-              <ol>
-              {% for section in tutorial.sections %}
-                <li style="font-size:16px"><a href="{{ tutorial_url | append: '/' | append: section['endpoint'] }}" class="center-vertical">
-                    {{ section['title'] }}
-                  </a></li>
-              {% endfor %}
-              </ol>
-            </li>
-          </ul>
-        {% endif %}
-      {% endfor %}
-    </li>
-  </ul>
-</nav>
diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index 874635f..02e3724 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -1,11 +1,122 @@
 <!DOCTYPE html>
 <html lang="{{ site.lang | default: "en-US" }}">
   <head>
-    {% include header.html %}
+
+    {% if site.google_analytics %}
+      <script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script>
+      <script>
+        window.dataLayer = window.dataLayer || [];
+        function gtag(){dataLayer.push(arguments);}
+        gtag('js', new Date());
+        gtag('config', '{{ site.google_analytics }}');
+      </script>
+    {% endif %}
+    <meta charset="UTF-8">
+
+    <!-- Add asciinema-player -->
+    <link rel="stylesheet" href="{{ '/assets/css/asciinema-player.css' | relative_url }}">
+    <link rel="stylesheet" href="{{ '/assets/css/asciinema-player-custom.css' | relative_url }}">
+    <script src="{{ '/assets/js/asciinema-player.js' | relative_url }}"></script>
+
+    <!-- Add jQuery -->
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
+    <script src="{{ '/assets/js/jquery.timeago.js' | relative_url }}"></script>
+
+{% seo %}
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="theme-color" content="#157878">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+    <link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
+    <link rel="icon" href="{{ '/favicon.ico' | relative_url }}" type="image/x-icon" />
+    {% feed_meta %}
   </head>
   <body>
     <div class="container">
-      {% include sidebar.html %}
+      <nav class="sidebar">
+        <a href="{{ '/' | relative_url }}">
+          <img src="{{ '/assets/images/wash-logo-lg.png' | relative_url }}" class="wash-logo" alt="">
+        </a>
+
+        <ul class="nav-list">
+          <li>
+            <span class="nav-header">
+              <a href="{{ '/docs' | relative_url }}" class="center-vertical">Docs</a>
+            </span>
+          </li>
+
+          <li>
+            <span class="nav-header">
+              <a href="{{ '/getting_started' | relative_url }}" class="center-vertical">Getting Started</a>
+            </span>
+          </li>
+
+          <li>
+            <span class="nav-header">
+              <a href="{{ '/known_issues' | relative_url }}" class="center-vertical">Known Issues</a>
+            </span>
+          </li>
+
+          <li>
+            <span class="nav-header">
+              <a href="{{ '/release_announcements' | relative_url }}" class="center-vertical">Release Announcements</a>
+            </span>
+          </li>
+
+          <li>
+            <span class="nav-header">
+              <a href="{{ '/contributing' | relative_url }}" class="center-vertical">Contributing</a>
+            </span>
+          </li>
+
+          <li>
+            <span class="nav-header">
+              <a href="{{ '/posts' | relative_url }}" class="center-vertical">News</a>
+            </span>
+            <ul>
+              {% for post in site.posts limit: 3 %}
+                <li><a href="{{ post.url | remove: "/index.html" | relative_url }}">{{ post.title }}</a> <small>{{ post.date | date: "%Y-%m-%d" }}</small></li>
+              {% endfor %}
+            </ul>
+          </li>
+
+          <li>
+            <span class="nav-header">
+              <a href="{{ '/tutorials' | relative_url }}" class="center-vertical">Tutorials</a>
+            </span>
+            {% for tutorial in site.tutorials %}
+              <!--
+                The tutorial.main check is here so that Jekyll collections doesn't interpret each
+                section as its own tutorial.
+              -->
+              {% if tutorial.main %}
+                {% assign tutorial_url = tutorial.url | remove: "/index.html" | relative_url %}
+                <ul>
+                  <!--
+                    Figure out tutorial_link. If the tutorial's 'main' page doesn't have any content, then
+                    this will link to the tutorial's first section. Otherwise, we'll link to the tutorial's
+                    URL. This avoids unnecessary 'main' pages for tutorials that don't need them (like e.g.
+                    'Debugging').
+                  -->
+                  {% assign tutorial_link = tutorial_url %}
+                  {% assign content_size = tutorial.content | strip_newlines | size %}
+                  {% if content_size <= 0 %}
+                    {% assign tutorial_link = tutorial_url | append: '/' | append: tutorial.sections.first['endpoint'] %}
+                  {% endif %}
+                  <li><a href="{{ tutorial_link }}" class="center-vertical">{{tutorial.title}}</a>
+                    <ol>
+                    {% for section in tutorial.sections %}
+                      <li style="font-size:16px"><a href="{{ tutorial_url | append: '/' | append: section['endpoint'] }}" class="center-vertical">
+                          {{ section['title'] }}
+                        </a></li>
+                    {% endfor %}
+                    </ol>
+                  </li>
+                </ul>
+              {% endif %}
+            {% endfor %}
+          </li>
+        </ul>
+      </nav>
 
       <main id="content" class="main-content" role="main">
         <h1>
diff --git a/docs/_docs/api.html b/docs/docs/api.html
similarity index 100%
rename from docs/_docs/api.html
rename to docs/docs/api.html
diff --git a/docs/_docs/api.json b/docs/docs/api.json
similarity index 100%
rename from docs/_docs/api.json
rename to docs/docs/api.json
diff --git a/docs/_docs/external-plugins.md b/docs/docs/external-plugins.md
similarity index 99%
rename from docs/_docs/external-plugins.md
rename to docs/docs/external-plugins.md
index 6c46b0c..6652006 100644
--- a/docs/_docs/external-plugins.md
+++ b/docs/docs/external-plugins.md
@@ -161,7 +161,7 @@ Additionally Wash supports _core entries_ that provide built-in functionality th
 
 _core entries_ can be used by returning an entry object with `type_id` set to the _core entries_ name surrounded by double underscores (`__core::entry__`) and the `name` field to identify the entry. You can also specify options for a _core entry_ using the `state` field containing serialized JSON of the options. If your plugin use _schemas_, then when using a _core entry_ you must still specify the _core entry_ as a child of the entry that lists it in the schema by it's `type_id`.
 
-* `volume::fs`: a representation of your entry's filesystem that uses its `exec` method to access it. The `os.login_shell` attribute is used to determine how to interact with the filesystem; if not set it assumes `posixshell`. _Options_:
+* `volume::fs`: a representation of your entry's filesystem that uses its `exec` method to access it. _Options_:
   * `maxdepth`: identifies how many levels of filesystem to fetch in a single batch to support trade-offs between `exec` latency and file density in the volume.
 
 **EXAMPLES**
diff --git a/docs/docs/index.md b/docs/docs/index.md
new file mode 100644
index 0000000..06bfbc8
--- /dev/null
+++ b/docs/docs/index.md
@@ -0,0 +1,493 @@
+---
+title: Docs
+---
+* [External plugins](external-plugins)
+* [Analytics](#analytics)
+  * [What data does Wash collect?](#what-data-does-wash-collect)
+  * [Why does Wash collect data?](#why-does-wash-collect-data)
+  * [How can I opt out of Wash data collection?](#how-can-i-opt-out-of-wash-data-collection)
+* [Wash Commands](#wash-commands)
+  * [wash](#wash)
+  * [wash clear](#wash-clear)
+  * [wash exec](#wash-exec)
+  * [wash find](#wash-find)
+  * [wash history](#wash-history)
+  * [wash info](#wash-info)
+  * [wash ls](#wash-ls)
+  * [wash meta](#wash-meta)
+  * [wash ps](#wash-ps)
+  * [wash server](#wash-server)
+  * [wash stree](#wash-stree)
+  * [wash tail](#wash-tail)
+  * [wash validate](#wash-validate)
+  * [wash docs](#wash-docs)
+  * [wash delete](#wash-delete)
+  * [wash signal](#wash-signal)
+* [Config](#config)
+  * [wash.yaml](#wash-yaml)
+  * [wash shell](#wash-shell)
+* [CName](#cname)
+* [Actions](#actions)
+  * [list](#list)
+  * [read](#read)
+  * [write](#write)
+  * [stream](#stream)
+  * [exec](#exec)
+  * [delete](#delete)
+  * [signal](#signal)
+* [Attributes](#attributes)
+  * [crtime](#crtime)
+  * [mtime](#mtime)
+  * [ctime](#ctime)
+  * [atime](#atime)
+  * [size](#size)
+  * [mode](#mode)
+* [Server API](api)
+
+## Analytics
+
+Wash collects anonymous data about how you use it. You can opt out of providing this data.
+
+### What data does Wash collect?
+* Version of Wash
+* User locale
+* Architecture
+* Method invocations (for shipped plugins only)
+  * This includes any Wash action invocation
+  * It also includes the entry's plugin
+
+This data is associated with Bolt analytics' UUID (if available); otherwise, the data is associated with a random, non-identifiable user UUID.
+
+### Why does Wash collect data?
+Wash collects data to help us understand how it's being used and make decisions about how to improve it.
+
+### How can I opt out of Wash data collection?
+To disable the collection of analytics data add the following line to `~/.puppetlabs/wash/analytics.yaml`:
+
+```
+disabled: true
+```
+
+You can also disable the collection of analytics data by setting the `WASH_DISABLE_ANALYTICS` environment variable to `true` before starting up the Wash daemon.
+
+## Wash Commands
+
+Wash commands aim to be well-documented in the tool. Try `wash help` and `wash help <command>` for specific options.
+
+Most commands operate on Wash resources, which are addressed by their path in the filesystem.
+
+### wash
+
+The `wash` command can be invoked on its own to enter a Wash shell.
+
+Invoking `wash` starts the daemon as part of the process, then enters your current system shell with shortcuts configured for Wash commands. All the [`wash server`](#wash-server) settings are also supported with `wash` except `socket`; `wash` ignores that setting and creates a temporary location for the socket.
+
+### wash clear
+
+Wash caches most operations. If the resource you're querying appears out-of-date, use this subcommand to reset the cache for resources at or contained within the specified paths. Defaults to the current directory if no path is provided.
+
+### wash exec
+
+For a Wash resource that implements the ability to execute a command, run the specified command and arguments. The results will be forwarded from the target on stdout, stderr, and exit code.
+
+### wash find
+
+Recursively descends the directory tree of the specified paths, evaluating an `expression` composed of `primaries` and `operands` for each entry in the tree.
+
+### wash history
+
+Wash maintains a history of commands executed through it. Print that command history, or specify an `id` to print a log of activity related to a particular command.
+
+Journals are stored in `wash/activity` under your user cache directory, identified by process ID and executable name. The user cache directory is `$XDG_CACHE_HOME` or `$HOME/.cache` on Unix systems, `$HOME/Library/Caches` on macOS, and `%LocalAppData%` on Windows.
+
+### wash info
+
+Prints the entries' info at the specified paths.
+
+### wash ls
+
+Lists the children of the specified paths, or current directory if no path is specified. If the `-l` option is set, then the name, last modified time, and supported actions are displayed for each child.
+
+### wash meta
+
+Prints the metadata of the given entries. By default, meta prints the full metadata as returned by the metadata endpoint. Specify the `--partial` flag to instead print the partial metadata, a (possibly) reduced set of metadata that's returned when entries are enumerated.
+
+### wash ps
+
+Captures /proc/*/{cmdline,stat,statm} on each node by executing 'cat' on them. Collects the output
+to display running processes on all listed nodes. Errors on paths that don't implement exec.
+
+### wash server
+
+Initializes all of the plugins, then sets up the Wash daemon (its API and [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) servers). To stop it, make sure you're not using the filesystem at the specified mountpoint, then enter Ctrl-C.
+
+Server API docs can be found [here](api). The server config is described in the [`config`](#config) section.
+
+### wash stree
+
+Displays the entry's stree (schema-tree), which is a high-level overview of the entry's hierarchy. Non-singleton types are bracketed with "[]".
+
+### wash tail
+
+Output any new updates to files and/or resources (that support the stream action). Currently requires the '-f' option to run. Attempts to mimic the functionality of `tail -f` for remote logs.
+
+### wash validate
+
+Validates an external plugin, using it's schema to limit exploration. The plugin can be one you've configured in Wash's config file, or it can be a script to load as an external plugin. Plugin-specific config from Wash's config file will be used. The Wash daemon does not need to be running to use this command.
+
+Validate starts from the plugin root and does a breadth-first traversal of the plugin hierarchy, invoking all supported methods on an example at each level. If the plugin provides a schema, it will be used to explore one example of each type of entry. Exploration can be stopped with Ctrl-C when needed.
+
+Each line represents validation of an entry type. The `lrsx` fields represent support for `list`, `read`, `stream`, and `execute` methods respectively, with '-' representing lack of support for a method.
+
+### wash docs
+
+Displays the entry's documentation. This is currently its description and any supported signals/signal groups.
+
+### wash delete
+
+Deletes the entries at the specified paths, prompting the user for confirmation before deleting each entry.
+
+### wash signal
+
+Sends the specified signal to the entries at the specified paths.
+
+## Config
+
+### wash.yaml
+
+The Wash config file is located at `~/.puppetlabs/wash/wash.yaml`, and can be used to configure the [`wash-server`](#wash-server). You can override this location via the `config-file` flag.
+
+Below are all the configurable options.
+
+* `logfile` - The location of the server's log file (default `stdout`)
+* `loglevel` - The server's loglevel (default `info`)
+* `cpuprofile` - The location that the server's CPU profile will be written to (optional)
+* `external-plugins` - The external plugins that will be loaded. See [➠External Plugins]
+* `plugins` - A list of shipped plugins to enable. If omitted or empty, it will load all of the shipped plugins. Note that Wash ships with the `docker`, `kubernetes`, `aws`, and `gcp` plugins.
+* `socket` - The location of the server's socket file (default `<user_cache_dir>/wash/wash-api.sock`)
+
+All options except for `external-plugins` can be overridden by setting the `WASH_<option>` environment variable with option converted to ALL CAPS.
+
+NOTE: Do not override `socket` in a config file. Instead, override it via the `WASH_SOCKET` environment variable. Otherwise, Wash's commands will not be able to interact with the server because they cannot access the socket.
+
+### wash shell
+
+Wash uses your system shell to provide the shell environment. It determines this using the `SHELL` environment variable or falls back to `/bin/sh`, so if you'd like to specify a particular shell set the `SHELL` environment variable before starting Wash.
+
+Wash uses the following environment variables
+
+- `WASH_SOCKET` determines how to communicate with the Wash daemon
+- `W` describes the path to Wash's starting directory on the host filesystem; use `cd $W` to return to the start or `ls $W/...` to list things relative to Wash's root
+- `PATH` is prefixed with the location of the Wash binary and any other executables it creates
+
+For some shells, Wash provides a customized environment. Please [file an issue](https://github.com/puppetlabs/wash/issues/new?assignees=&labels=Feature&template=feature-request.md) if you'd like to add support for new shells.
+
+Wash currently provides a customized environment for
+
+- `bash`
+- `zsh`
+
+Customized environments alias Wash subcommands to save typing out `wash <subcommand>` so they feel like shell builtins. If you want to use an executable or builtin Wash has overridden, please use its full path or the `builtin` command.
+
+Customized environments also supports reading `~/.washenv` and `~/.washrc` files. These files are loaded as follows:
+
+1. If running Wash non-interactively (by piping `stdin` or passing the `-c` option)
+   1. If `~/.washenv` does not exist, load the shell's default non-interactive config (such as `.zshenv` or from `BASH_ENV`)
+   2. Configure subcommand aliases
+   3. If `~/.washenv` exists, load it
+2. If running Wash interactively
+   1. Do all non-interactive config above
+   2. If `~/.washrc` does not exist, load the shell's default interactive config (such as `.bash_profile` or `.zshrc`)
+   3. Re-configure subcommand aliases, and configure the command prompt
+   4. If `~/.washrc` exists, load it
+
+That order ensures that the out-of-box experience of Wash is not adversely impacted by your existing environment while still inheriting most of your config. If you customize your Wash environment with `.washenv` and `.washrc`, be aware that it's possible to override Wash's default prompt and aliases.
+
+For other shells, Wash creates executables for subcommands and does no other customization.
+
+## CName
+
+CName is short for _canonical name_. An entry's cname is its name with all `/`'es replaced by that entry's _slash replacer_. The default slash replacer is `#`.
+
+Wash uses the cname to construct the entry's path. An entry's path is defined as
+```
+    <mountpoint>/<parent_cname1>/<parent_cname2>/.../<cname>
+```
+
+### Examples
+Consider the entry `/myplugin/foo`. If `bar/baz` is a child of `foo`, then its cname and path would be `bar#baz` and `/myplugin/foo/bar#baz`, respectively. Similarly if `qux` is a child of `foo`, then its cname and path would be `qux` and `/myplugin/foo/qux`.
+
+Conversely, if `bar/baz`'s slash replacer is set to `:`, then its cname and path would now be `bar:baz` and `/myplugin/foo/bar:baz`.
+
+## Actions
+
+### list
+The `list` action lets you list an entry’s children. Entries that support list are represented as directories. Thus, any command that works with directories also work with these entries.
+
+#### Examples
+```
+wash . ❯ ls gcp/Wash/storage/some-wash-stuff
+an example folder reaper.sh
+```
+
+```
+wash . ❯ cd gcp/Wash/storage/some-wash-stuff
+wash gcp/Wash/storage/some-wash-stuff ❯
+```
+
+```
+wash . ❯ tree gcp/Wash/storage/some-wash-stuff
+gcp/Wash/storage/some-wash-stuff
+├── an\ example\ folder
+│   └── static.sh
+└── reaper.sh
+
+1 directory, 2 files
+```
+
+### read
+The `read` action lets you read data from an entry. Thus, any command that reads a file also works with these entries.
+
+#### Examples
+```
+wash . ❯ cat gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
+#!/bin/sh
+
+echo "Hello, world!"
+```
+
+```
+wash . ❯ grep "Hello" gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
+echo "Hello, world!"
+```
+
+### write
+The `write` action lets you write data to an entry. Thus, any command that writes a file also works with these entries.
+
+Note that Wash distinguishes between file-like and non-file-like entries. An entry is file-like if it's readable and writable and defines its size; you can edit it like a file.
+
+If it doesn't define a size then it's non-file-like, and trying to open it with a ReadWrite handle will error; reads from it may not return data you previously wrote to it. You should check its documentation with the `docs` command for that entry's write semantics. We also recommend not using editors with these entries to avoid weird behavior.
+
+#### Examples
+Modifying a file stored in Google Cloud Storage
+```
+wash . ❯ echo 'exit 1' >> gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
+wash . ❯ cat gcp/Wash/storage/some-wash-stuff/an\ example\ folder/static.sh
+#!/bin/sh
+
+echo "Hello, world!"
+exit 1
+```
+
+Writing a message to a hypothetical message queue where each write publishes a message and each read consumes a message
+```
+wash > echo 'message 1' >> myqueue
+wash > echo 'message 2' >> myqueue
+wash > cat myqueue
+message 1
+wash > cat myqueue
+message 2
+```
+
+### stream
+The `stream` action lets you stream an entry’s content for updates.
+
+#### Examples
+```
+wash . ❯ tail -f gcp/Wash/compute/instance-1/fs/var/log/messages
+===> gcp/Wash/compute/instance-1/fs/var/log/messages <===
+Aug 27 06:25:01 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="17499" x-info="http://www.rsyslog.com"] rsyslogd was HUPed
+
+Aug 27 13:26:32 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="17499" x-info="http://www.rsyslog.com"] exiting on signal 15.
+
+Aug 27 13:26:32 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="24583" x-info="http://www.rsyslog.com"] start
+Aug 28 00:30:04 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="24583" x-info="http://www.rsyslog.com"] exiting on signal 15.
+Aug 28 00:30:04 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="32147" x-info="http://www.rsyslog.com"] start
+Aug 28 06:25:01 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="32147" x-info="http://www.rsyslog.com"] rsyslogd was HUPed
+
+Aug 28 09:54:34 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="32147" x-info="http://www.rsyslog.com"] exiting on signal 15.
+
+Aug 28 09:54:34 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="6687" x-info="http://www.rsyslog.com"] start
+Aug 28 19:01:21 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="6687" x-info="http://www.rsyslog.com"] exiting on signal 15.
+
+Aug 28 19:01:21 instance-1 liblogging-stdlog:  [origin software="rsyslogd" swVersion="8.24.0" x-pid="12804" x-info="http://www.rsyslog.com"] start
+```
+
+(Hit `Ctrl+C` to cancel `tail -f`)
+
+### exec
+The `exec` action lets you execute a command on an entry.
+
+#### Examples
+```
+wash . ❯ wexec gcp/Wash/compute/instance-1 uname
+Linux
+```
+
+### delete
+The `delete` action lets you delete an entry.
+
+#### Examples
+```
+wash . ❯ delete docker/containers/quizzical_colden
+remove docker/containers/quizzical_colden?: y
+```
+
+### signal
+The `signal` action lets you signal an entry. Use the `docs` command to view an entry's supported signals.
+
+#### Examples
+```
+wash . ❯ docs docker/containers/wash_tutorial_redis_1
+No description provided.
+
+SUPPORTED SIGNALS
+* start
+    Starts the container. Equivalent to 'docker start <container>'
+* stop
+    Stops the container. Equivalent to 'docker stop <container>'
+* pause
+    Suspends all processes in the container. Equivalent to 'docker pause <container>'
+* resume
+    Un-suspends all processes in the container. Equivalent to 'docker unpause <container>'
+* restart
+    Restarts the container. Equivalent to 'docker restart <container>'
+
+SUPPORTED SIGNAL GROUPS
+* linux
+    Consists of all the supported Linux signals like SIGHUP, SIGKILL. Equivalent to
+    'docker kill <container> --signal <signal>'
+```
+
+```
+wash . ❯ signal start docker/containers/wash_tutorial_redis_1
+wash . ❯
+```
+
+#### Common Signals
+* start
+* stop
+* pause
+* resume
+* restart
+* hibernate
+* reset
+
+## Attributes
+
+### crtime
+This is the entry's creation time.
+
+#### Example JSON
+As a stringified date
+
+```
+{
+  "crtime": “2019-09-25T21:39:57-07:00”
+}
+```
+
+In UNIX seconds
+
+```
+{
+  "crtime": 1569472797
+}
+```
+
+### mtime
+This is the entry's last modification time.
+
+#### Example JSON
+As a stringified date
+
+```
+{
+  "mtime": “2019-09-25T21:39:57-07:00”
+}
+```
+
+In UNIX seconds
+
+```
+{
+  "mtime": 1569472797
+}
+```
+
+### ctime
+This is the entry's last change time.
+
+#### Example JSON
+As a stringified date
+
+```
+{
+  "ctime": “2019-09-25T21:39:57-07:00”
+}
+```
+
+In UNIX seconds
+
+```
+{
+  "ctime": 1569472797
+}
+```
+
+### atime
+This is the entry's last access time.
+
+#### Example JSON
+As a stringified date
+
+```
+{
+  "atime": “2019-09-25T21:39:57-07:00”
+}
+```
+
+In UNIX seconds
+
+```
+{
+  "atime": 1569472797
+}
+```
+
+### size
+This is the entry's content size.
+
+#### Example JSON
+```
+{
+  "size": 1024
+}
+```
+
+### mode
+This is the entry's mode.
+
+#### Example JSON
+
+```
+{
+  "mode": 16832
+}
+```
+
+As a hexadecimal string
+
+```
+{
+  "mode": "41C0"
+}
+```
+
+As an octal string
+
+```
+{
+  "mode": "40700"
+}
+```
diff --git a/plugin/entrySchema.go b/plugin/entrySchema.go
index 50a8256..9904785 100644
--- a/plugin/entrySchema.go
+++ b/plugin/entrySchema.go
@@ -18,12 +18,31 @@ func TypeID(e Entry) string {
 	pluginName := pluginName(e)
 	rawTypeID := rawTypeID(e)
 	if pluginName == "" {
-		// e is the plugin registry
+		// e is the plugin registry or a core entry used by an external plugin
 		return rawTypeID
 	}
 	return namespace(pluginName, rawTypeID)
 }
 
+// SchemaGraph returns e's schema graph. This is effectively a
+// map[string]plugin.EntrySchema object that preserves insertion
+// order (where the string is each entry's type ID).
+func SchemaGraph(e Entry) (*linkedhashmap.Map, error) {
+	s, err := schema(e)
+	if err != nil {
+		return nil, err
+	}
+	if s == nil {
+		return nil, nil
+	}
+	if s.graph == nil {
+		// e is a core plugin entry so fill its graph
+		s.graph = linkedhashmap.New()
+		s.fill(s.graph)
+	}
+	return s.graph, nil
+}
+
 func schema(e Entry) (*EntrySchema, error) {
 	switch t := e.(type) {
 	case externalPlugin:
@@ -49,7 +68,10 @@ func schema(e Entry) (*EntrySchema, error) {
 			IsSingleton().
 			SetDescription(registryDescription)
 		schema.graph = linkedhashmap.New()
-		schema.graph.Put(TypeID(t), &schema.entrySchema)
+		typeID := TypeID(t)
+		// We start by putting in a stub value for s so that we preserve the insertion
+		// order. We'll then update this value once the "Children" array's been calculated.
+		schema.graph.Put(typeID, EntrySchema{})
 		for _, root := range t.pluginRoots {
 			childSchema, err := Schema(root)
 			if err != nil {
@@ -73,6 +95,8 @@ func schema(e Entry) (*EntrySchema, error) {
 				schema.graph.Put(key, value)
 			})
 		}
+		// Update the graph
+		schema.graph.Put(typeID, schema.clone())
 		return schema, nil
 	default:
 		// e is a core-plugin
@@ -137,9 +161,10 @@ func NewEntrySchema(e Entry, label string) *EntrySchema {
 // how plugin.EntrySchema objects are meant to be used.
 func (s EntrySchema) MarshalJSON() ([]byte, error) {
 	if s.entry == nil {
-		// Nodes in the external plugin graph don't use NewEntrySchema, they directly set the
-		// undocumented fields of EntrySchema. Since graph and entry won't be set - and this is
-		// part of a graph already - directly serialize entrySchema instead of using the graph.
+		// This corresponds to an "EntrySchema" value in another entry's schema graph.
+		// These values directly set the undocumented fields of EntrySchema. Since graph
+		// and entry won't be set - and this is part of a graph already - directly serialize
+		// entrySchema instead of using the graph.
 		return json.Marshal(s.entrySchema)
 	}
 
@@ -253,12 +278,18 @@ func (s *EntrySchema) fill(graph *linkedhashmap.Map) {
 			s.fillPanicf("bad value passed into SetMetadataSchema: %v", err)
 		}
 	}
-	graph.Put(TypeID(s.entry), &s.entrySchema)
+	typeID := TypeID(s.entry)
 
-	// Fill-in the children
 	if !ListAction().IsSupportedOn(s.entry) {
+		graph.Put(typeID, s.clone())
 		return
 	}
+
+	// Fill-in the children. We start by putting in a stub value for s
+	// so that we preserve the insertion order and so that we can mark
+	// the node as visited. We'll then update this value once the "Children"
+	// array's been calculated.
+	graph.Put(typeID, EntrySchema{})
 	// "sParent" is read as "s.parent"
 	sParent := s.entry.(Parent)
 	children := sParent.ChildSchemas()
@@ -280,6 +311,16 @@ func (s *EntrySchema) fill(graph *linkedhashmap.Map) {
 		passAlongWrappedTypes(sParent, child.entry)
 		child.fill(graph)
 	}
+	// Update the graph
+	graph.Put(typeID, s.clone())
+}
+
+// clone returns a copy of s.entrySchema's fields as an EntrySchema
+// object
+func (s *EntrySchema) clone() EntrySchema {
+	var copy EntrySchema
+	copy.entrySchema = s.entrySchema
+	return copy
 }
 
 // This helper's used by CachedList + EntrySchema#fill(). The reason for
@@ -339,16 +380,9 @@ func pluginName(e Entry) string {
 		switch e.(type) {
 		case Root:
 			return CName(e)
-		case *Registry:
-			return ""
 		default:
-			// e has no ID. This is possible if e's from the apifs package. For now,
-			// it is enough to return "__apifs__" here because this is an unlikely
-			// edge case.
-			//
-			// TODO: Panic here once https://github.com/puppetlabs/wash/issues/438
-			// is resolved.
-			return "__apifs__"
+			// e is an apifs entry or a core plugin entry used by an external plugin
+			return ""
 		}
 	}
 	segments := strings.SplitN(trimmedID, "/", 2)
diff --git a/plugin/external/coreEntries.go b/plugin/external/coreEntries.go
index 29c0add..2bdee5c 100644
--- a/plugin/external/coreEntries.go
+++ b/plugin/external/coreEntries.go
@@ -10,7 +10,7 @@ import (
 
 type coreEntry interface {
 	createInstance(parent *pluginEntry, decodedEntry decodedExternalPluginEntry) (plugin.Entry, error)
-	schema() *plugin.EntrySchema
+	template() plugin.Entry
 }
 
 var coreEntries = map[string]coreEntry{
@@ -31,6 +31,6 @@ func (volumeFS) createInstance(parent *pluginEntry, e decodedExternalPluginEntry
 	return volume.NewFS(e.Name, parent, int(opts.Maxdepth)), nil
 }
 
-func (volumeFS) schema() *plugin.EntrySchema {
-	return (&volume.FS{}).Schema()
+func (volumeFS) template() plugin.Entry {
+	return &volume.FS{}
 }
diff --git a/plugin/external/pluginEntry.go b/plugin/external/pluginEntry.go
index 3aa1c8e..fa90521 100644
--- a/plugin/external/pluginEntry.go
+++ b/plugin/external/pluginEntry.go
@@ -13,7 +13,6 @@ import (
 	"time"
 
 	"github.com/getlantern/deepcopy"
-	"github.com/jinzhu/copier"
 
 	"github.com/emirpasic/gods/maps/linkedhashmap"
 	"github.com/puppetlabs/wash/activity"
@@ -711,17 +710,24 @@ func unmarshalSchemaGraph(pluginName, rawTypeID string, stdout []byte) (*linkedh
 	graph := linkedhashmap.New()
 	putNode := func(rawTypeID string, rawSchema interface{}) error {
 		if coreEnt, ok := coreEntries[rawTypeID]; ok {
-			pluginSchema := coreEnt.schema()
-
-			// Copy only the public fields so we serialize it as just data. Uses copier because it uses
-			// reflect to copy public fields, rather than Marshal/UnmarshalJSON which we've overridden.
-			var schema plugin.EntrySchema
-			err := copier.Copy(&schema, pluginSchema)
-			if err != nil {
-				panic(fmt.Sprintf("should always be able to copy from EntrySchema to EntrySchema: %v", err))
+			template := coreEnt.template()
+			rawTypeID = plugin.TypeID(template)
+			if populatedTypeIDs[rawTypeID] {
+				// We've already included this entry's schema-graph
+				return nil
 			}
+			coreEntSchemaGraph, _ := plugin.SchemaGraph(template)
+			coreEntSchemaGraph.Each(func(rawTypeIDV interface{}, schemaV interface{}) {
+				// Munge the schema's children's type IDs
+				schema := schemaV.(plugin.EntrySchema)
+				children := []string{}
+				for _, child := range schema.Children {
+					children = append(children, namespace(pluginName, child))
+				}
+				schema.Children = children
+				graph.Put(namespace(pluginName, rawTypeIDV.(string)), schema)
+			})
 			populatedTypeIDs[rawTypeID] = true
-			graph.Put(namespace(pluginName, rawTypeID), schema)
 			return nil
 		}
 
@@ -764,6 +770,9 @@ func unmarshalSchemaGraph(pluginName, rawTypeID string, stdout []byte) (*linkedh
 			}
 			var namespacedChildren []string
 			for _, child := range node.Children {
+				if coreEnt, ok := coreEntries[child]; ok {
+					child = plugin.TypeID(coreEnt.template())
+				}
 				requiredTypeIDs[child] = true
 				namespacedChildren = append(namespacedChildren, namespace(pluginName, child))
 			}


Test patch:
diff --git a/plugin/external/pluginEntry_test.go b/plugin/external/pluginEntry_test.go
index aa6d165..c842dc8 100644
--- a/plugin/external/pluginEntry_test.go
+++ b/plugin/external/pluginEntry_test.go
@@ -1385,6 +1385,61 @@ func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_ValidInput()
 	}
 }
 
+func (suite *ExternalPluginEntryTestSuite) TestUnmarshalSchemaGraph_CoreEntries() {
+	entry := &pluginEntry{
+		rawTypeID: "foo",
+	}
+	entry.SetTestID("fooPlugin")
+
+	stdout := []byte(`
+{
+	"foo":{
+		"label": "fooLabel",
+		"methods": ["list"],
+		"children": ["__volume::fs__"]
+	},
+	"__volume::fs__": {}
+}
+`)
+
+	graph, err := unmarshalSchemaGraph(pluginName(entry), rawTypeID(entry), stdout)
+	if suite.NoError(err) {
+		assertSchema := func(typeID string, assertFunc func(plugin.EntrySchema)) {
+			schema, found := graph.Get(typeID)
+			if !found {
+				suite.FailNow("expected %v to be present in schema graph", typeID)
+			}
+			assertFunc(schema.(plugin.EntrySchema))
+		}
+
+		// Ensure that only four nodes exist in schema graph -- "foo", volume::fs,
+		// volume::dir, and volume::file
+		suite.Equal(int(4), graph.Size())
+
+		// Now ensure that the right nodes are set in the graph
+		volumeFSTemplate := (&volumeFS{}).template()
+		assertSchema("fooPlugin::foo", func(s plugin.EntrySchema) {
+			expectedVolumeFSTypeID := fmt.Sprintf("fooPlugin::%v", plugin.TypeID(volumeFSTemplate))
+			suite.Equal([]string{expectedVolumeFSTypeID}, s.Children)
+		})
+		// Ensure that volume::fs' schema-graph's been merged with our schema graph
+		// and that all the type IDs are properly namespaced
+		volumeFSGraph, _ := plugin.SchemaGraph(volumeFSTemplate)
+		volumeFSGraph.Each(func(typeIDV interface{}, schemaV interface{}) {
+			typeID := typeIDV.(string)
+			schema := schemaV.(plugin.EntrySchema)
+			expectedChildren := []string{}
+			for _, child := range schema.Children {
+				expectedChildren = append(expectedChildren, "fooPlugin::"+child)
+			}
+			assertSchema("fooPlugin::"+typeID, func(s plugin.EntrySchema) {
+				suite.Equal(schema.Label, s.Label)
+				suite.Equal(expectedChildren, s.Children)
+			})
+		})
+	}
+}
+
 func TestExternalPluginEntry(t *testing.T) {
 	suite.Run(t, new(ExternalPluginEntryTestSuite))
 }