about summary refs log tree commit diff
diff options
context:
space:
mode:
authorTerin Stock <terinjokes@gmail.com>2019-11-20 11:10:58 -0800
committerTerin Stock <terinjokes@gmail.com>2019-11-20 11:10:58 -0800
commitb35b371065165c014bf4f3673d322fb0d7027b04 (patch)
treef7adc076a8e1be432b23d95efe383f0af556344c
parent7a1d9fe71f022268e303a8151558d02dddbabb1b (diff)
feat: add open source code HEAD master
Add the initial open source version of K9P with support for viewing
namespaces and deployments. Current support is entirely read-only.
Performance is pretty terrible. Needs much more love and care.
-rw-r--r--LICENSE22
-rw-r--r--README.md59
-rw-r--r--cmd/k9p/main.go102
-rw-r--r--go.mod14
-rw-r--r--go.sum214
-rw-r--r--pkg/k9p/logger/logger.go195
-rw-r--r--pkg/k9p/session.go205
-rw-r--r--pkg/resources/deployments.go184
-rw-r--r--pkg/resources/files.go60
-rw-r--r--pkg/resources/namespace.go68
-rw-r--r--pkg/resources/namespaces.go110
-rw-r--r--pkg/resources/refs.go13
-rw-r--r--pkg/resources/resources.go12
-rw-r--r--pkg/resources/staticdir.go76
14 files changed, 1334 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..88595de
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2019, Terin Stock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7dc0626
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+# K9P
+
+A virtual filesystem for Kubernetes cluster state.
+
+## Install
+
+K9P uses Go modules, and can be installed with a Go 1.13+. Goal is to allow operators to
+use familiar unix tools to interact with their Kubernetes clusters, rather than learning kubectl.
+
+```console
+$ git clone https://git.terinstock.com/k9p && cd k9p
+$ go get ./cmd/k9p/...
+```
+
+## Mounting
+
+### Plan 9
+
+K9P can be mounded like other remote 9P servers:
+
+```console
+$ import 'tcp!localhost!1564` /k8s
+```
+
+### Linux
+
+Supported on systems compiled with `CONFIG_NET_9P`, otherwise use 9pfuse.
+
+```console
+# mount -t 9p -o trans=tcp,port=1564 127.0.0.1 /mnt/k8s
+```
+
+### FUSE
+
+K9P can be mounted as a remote 9P server using 9pfuse:
+
+```console
+$ 9pfuse 'tcp!127.0.0.1!1564' $HOME/k8s
+```
+
+### Applications
+
+So applications can directly interact with 9P servers, such as 9p from plan9ports:
+
+```console
+$ p9 -a 'tcp!9p.example.com!1564' ls -l /
+d-r-xr-x-r-x I 0 terin terin 0 July 3  2019 cluster-scoped
+d-r-xr-x-r-x I 0 terin terin 0 July 3  2019 namespaces
+```
+
+## Future
+
+K9P is in a very early WIP state. There's lots of improvements, contributions welcome.
+
+* Support passing authentication to the server for the attach mount.
+* Load resources on-demand, rather than creating informers on attach.
+* Add support for way more resource types, including CRDs.
+* Mutate and remove resources.
+* Port forwards modeled after Plan 9's netfs?
diff --git a/cmd/k9p/main.go b/cmd/k9p/main.go
new file mode 100644
index 0000000..dd0ed80
--- /dev/null
+++ b/cmd/k9p/main.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"net"
+	"os"
+
+	p9p "github.com/docker/go-p9p"
+	"github.com/oklog/run"
+	"github.com/rs/zerolog"
+	"go.terinstock.com/k9p/pkg/k9p"
+	"go.terinstock.com/k9p/pkg/k9p/logger"
+	"k8s.io/client-go/kubernetes"
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/klog"
+)
+
+func main() {
+	fs := flag.NewFlagSet("k9p", flag.ExitOnError)
+	klog.InitFlags(fs)
+
+	fs.Set("logtostderr", "false")
+	fs.Set("alsologtostderr", "false")
+
+	var (
+		prettyLog  = fs.Bool("pretty-log", false, "output human-friendly logs")
+		master     = fs.String("master", "", "The address of the Kubernetes API server (overrides any value in kubeconfig).")
+		kubeconfig = fs.String("kubeconfig", "", "Path to kubeconfig file with authorization and master location information.")
+		bind9p     = fs.String("bind-9p", ":564", "The address the 9P server should bind and listen on")
+	)
+	fs.Parse(os.Args[1:])
+
+	ctx := context.Background()
+
+	var log zerolog.Logger
+	if *prettyLog {
+		log = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger()
+	} else {
+		log = zerolog.New(os.Stderr)
+	}
+
+	client, err := createClient(*master, *kubeconfig)
+	if err != nil {
+		log.Fatal().Err(err).Send()
+	}
+
+	klog.SetOutput(log.With().Str("component", "klog").Logger())
+
+	var g run.Group
+	{
+		ln, err := net.Listen("tcp", *bind9p)
+		if err != nil {
+			log.Fatal().Err(err).Msg("error listening")
+		}
+
+		g.Add(func() error {
+			for {
+				c, err := ln.Accept()
+				if err != nil {
+					log.Warn().Err(err).Msg("error accepting")
+					continue
+				}
+
+				go func(conn net.Conn) {
+					defer conn.Close()
+
+					ctx, cancel := context.WithCancel(context.WithValue(ctx, "conn", conn))
+					defer cancel()
+
+					log.Info().Str("remote", conn.RemoteAddr().String()).Msg("connected")
+
+					var session p9p.Session
+					{
+						ksession := k9p.New(ctx, client)
+						ksession.WaitForCacheSync(ctx.Done())
+						session = logger.New(
+							log.With().Str("component", "9p").Logger(),
+							ksession,
+						)
+					}
+					if err := p9p.ServeConn(ctx, conn, p9p.Dispatch(session)); err != nil {
+						log.Warn().Err(err).Msg("ServeConn")
+					}
+				}(c)
+			}
+		}, func(error) {
+			ln.Close()
+		})
+	}
+
+	log.Info().Err(g.Run())
+}
+
+func createClient(master string, kubeconfig string) (kubernetes.Interface, error) {
+	config, err := clientcmd.BuildConfigFromFlags(master, kubeconfig)
+	if err != nil {
+		return nil, err
+	}
+	return kubernetes.NewForConfig(config)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..606f59d
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,14 @@
+module go.terinstock.com/k9p
+
+go 1.13
+
+require (
+	github.com/docker/go-p9p v0.0.0-20191112112554-37d97cf40d03
+	github.com/oklog/run v1.0.0
+	github.com/rs/zerolog v1.17.2
+	k8s.io/api v0.0.0-20191114100352-16d7abae0d2a
+	k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb
+	k8s.io/client-go v0.0.0-20191114101535-6c5935290e33
+	k8s.io/klog v1.0.0
+	sigs.k8s.io/yaml v1.1.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..fa07d88
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,214 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs=
+github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
+github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU=
+github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
+github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM=
+github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
+github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0=
+github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
+github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
+github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
+github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
+github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/docker/go-p9p v0.0.0-20191112112554-37d97cf40d03 h1:HiIKimWyR71ORJgvm/aWL/cqeYMpOy4eObwJogG8FAw=
+github.com/docker/go-p9p v0.0.0-20191112112554-37d97cf40d03/go.mod h1:GDue7j/yh3AtNoUK0ihznL9JiZVn92CV9bUrYaD4NOc=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
+github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
+github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
+github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k=
+github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o=
+github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
+github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
+github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68=
+golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
+gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.0.0-20191114100352-16d7abae0d2a h1:86XISgFlG7lPOWj6wYLxd+xqhhVt/WQjS4Tf39rP09s=
+k8s.io/api v0.0.0-20191114100352-16d7abae0d2a/go.mod h1:qetVJgs5i8jwdFIdoOZ70ks0ecgU+dYwqZ2uD1srwOU=
+k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWcFg9ZP31OKkziqCbiphznI=
+k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ=
+k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 h1:07mhG/2oEoo3N+sHVOo0L9PJ/qvbk3N5n2dj8IWefnQ=
+k8s.io/client-go v0.0.0-20191114101535-6c5935290e33/go.mod h1:4L/zQOBkEf4pArQJ+CMk1/5xjA30B5oyWv+Bzb44DOw=
+k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
+k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
+k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
+k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
+k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
+k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE=
+k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
+sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
diff --git a/pkg/k9p/logger/logger.go b/pkg/k9p/logger/logger.go
new file mode 100644
index 0000000..6b2e437
--- /dev/null
+++ b/pkg/k9p/logger/logger.go
@@ -0,0 +1,195 @@
+package logger
+
+import (
+	"context"
+	"time"
+
+	p9p "github.com/docker/go-p9p"
+	"github.com/rs/zerolog"
+)
+
+type Logger struct {
+	logger  zerolog.Logger
+	session p9p.Session
+}
+
+func New(logger zerolog.Logger, session p9p.Session) *Logger {
+	return &Logger{
+		logger:  logger,
+		session: session,
+	}
+}
+
+func (l *Logger) Auth(ctx context.Context, afid p9p.Fid, uname string, aname string) (qid p9p.Qid, err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "auth").
+			Uint32("afid", uint32(afid)).
+			Str("uname", uname).
+			Str("aname", aname).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Str("qid", qid.String()).
+				Err(err)).
+			Msg("")
+	}(time.Now())
+
+	return l.session.Auth(ctx, afid, uname, aname)
+}
+
+func (l *Logger) Attach(ctx context.Context, fid p9p.Fid, afid p9p.Fid, uname string, aname string) (qid p9p.Qid, err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "attach").
+			Uint32("fid", uint32(fid)).
+			Uint32("afid", uint32(afid)).
+			Str("uname", uname).
+			Str("aname", aname).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Str("qid", qid.String()).
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Attach(ctx, fid, afid, uname, aname)
+}
+
+func (l *Logger) Clunk(ctx context.Context, fid p9p.Fid) (err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "clunk").
+			Uint32("fid", uint32(fid)).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Clunk(ctx, fid)
+}
+
+func (l *Logger) Remove(ctx context.Context, fid p9p.Fid) (err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "remove").
+			Uint32("fid", uint32(fid)).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Remove(ctx, fid)
+}
+
+func (l *Logger) Walk(ctx context.Context, fid p9p.Fid, newfid p9p.Fid, names ...string) (qids []p9p.Qid, err error) {
+	defer func(t1 time.Time) {
+		arr := zerolog.Arr()
+		for _, qid := range qids {
+			arr = arr.Str(qid.String())
+		}
+
+		l.logger.Debug().
+			Str("request", "walk").
+			Uint32("fid", uint32(fid)).
+			Uint32("newfid", uint32(newfid)).
+			Strs("names", names).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Array("qids", arr).
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Walk(ctx, fid, newfid, names...)
+}
+
+// Read follows the semantics of io.ReaderAt.ReadAtt method except it takes
+// a contxt and Fid.
+func (l *Logger) Read(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "read").
+			Uint32("fid", uint32(fid)).
+			Int64("offset", offset).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Int("n", n).
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Read(ctx, fid, p, offset)
+}
+
+// Write follows the semantics of io.WriterAt.WriteAt except takes a context and an Fid.
+//
+// If n == len(p), no error is returned.
+// If n < len(p), io.ErrShortWrite will be returned.
+func (l *Logger) Write(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "write").
+			Uint32("fid", uint32(fid)).
+			Int64("offset", offset).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Int("n", n).
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Write(ctx, fid, p, offset)
+}
+
+func (l *Logger) Open(ctx context.Context, fid p9p.Fid, mode p9p.Flag) (qid p9p.Qid, iounit uint32, err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "open").
+			Uint32("fid", uint32(fid)).
+			Uint8("mode", uint8(mode)).
+			TimeDiff("duration", time.Now(), t1).
+			Dict("ret", zerolog.Dict().
+				Str("qid", qid.String()).
+				Uint32("iounit", iounit).
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Open(ctx, fid, mode)
+}
+
+func (l *Logger) Create(ctx context.Context, parent p9p.Fid, name string, perm uint32, mode p9p.Flag) (p9p.Qid, uint32, error) {
+	l.logger.Debug().
+		Str("request", "create").
+		Msg("")
+	return l.session.Create(ctx, parent, name, perm, mode)
+}
+
+func (l *Logger) Stat(ctx context.Context, fid p9p.Fid) (dir p9p.Dir, err error) {
+	defer func(t1 time.Time) {
+		l.logger.Debug().
+			Str("request", "stat").
+			Uint32("fid", uint32(fid)).
+			Dict("ret", zerolog.Dict().
+				Dict("dir", zerolog.Dict().
+					Str("name", dir.Name).
+					Str("qid", dir.Qid.String()).
+					Uint64("length", dir.Length).
+					Time("modTime", dir.ModTime)).
+				Err(err)).
+			Msg("")
+	}(time.Now())
+	return l.session.Stat(ctx, fid)
+}
+
+func (l *Logger) WStat(ctx context.Context, fid p9p.Fid, dir p9p.Dir) error {
+	l.logger.Debug().
+		Str("request", "stat").
+		Msg("")
+	return l.session.WStat(ctx, fid, dir)
+}
+
+// Version returns the supported version and msize of the session. This
+// can be affected by negotiating or the level of support provided by the
+// session implementation.
+func (l *Logger) Version() (msize int, version string) {
+	l.logger.Debug().
+		Str("request", "stat").
+		Msg("")
+	return l.session.Version()
+}
diff --git a/pkg/k9p/session.go b/pkg/k9p/session.go
new file mode 100644
index 0000000..471a6bc
--- /dev/null
+++ b/pkg/k9p/session.go
@@ -0,0 +1,205 @@
+package k9p
+
+import (
+	"context"
+	"runtime"
+	"sync"
+
+	"github.com/docker/go-p9p"
+	"go.terinstock.com/k9p/pkg/resources"
+	"k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes"
+)
+
+type Session struct {
+	sync.Mutex
+	aname string
+	uname string
+
+	client         kubernetes.Interface
+	sharedInformer informers.SharedInformerFactory
+	refs           map[p9p.Fid]resources.Ref
+}
+
+func New(ctx context.Context, client kubernetes.Interface) *Session {
+	sharedInformer := informers.NewSharedInformerFactory(client, 0)
+
+	sharedInformer.Core().V1().Namespaces().Informer()
+	sharedInformer.Apps().V1().Deployments().Informer()
+
+	go sharedInformer.Start(ctx.Done())
+	runtime.Gosched()
+
+	return &Session{
+		client:         client,
+		sharedInformer: sharedInformer,
+		refs:           make(map[p9p.Fid]resources.Ref),
+	}
+}
+
+func (k *Session) getRef(fid p9p.Fid) (resources.Ref, error) {
+	k.Lock()
+	defer k.Unlock()
+
+	if fid == p9p.NOFID {
+		return nil, p9p.ErrUnknownfid
+	}
+
+	ref, found := k.refs[fid]
+	if !found {
+		return nil, p9p.ErrUnknownfid
+	}
+
+	return ref, nil
+}
+
+func (k *Session) newRef(fid p9p.Fid, resource resources.Ref) (resources.Ref, error) {
+	k.Lock()
+	defer k.Unlock()
+
+	if fid == p9p.NOFID {
+		return nil, p9p.ErrUnknownfid
+	}
+
+	_, ok := k.refs[fid]
+	if ok {
+		return nil, p9p.ErrDupfid
+	}
+
+	ref := resource
+	k.refs[fid] = ref
+	return ref, nil
+}
+
+func (k *Session) Auth(ctx context.Context, afid p9p.Fid, uname string, aname string) (p9p.Qid, error) {
+	return p9p.Qid{}, nil
+}
+
+func (k *Session) Attach(ctx context.Context, fid p9p.Fid, afid p9p.Fid, uname string, aname string) (p9p.Qid, error) {
+	if uname == "" {
+		return p9p.Qid{}, p9p.MessageRerror{Ename: "no user"}
+	}
+
+	if aname == "" {
+		aname = "/"
+	}
+
+	k.uname = uname
+	k.aname = aname
+
+	ref, err := k.newRef(fid, resources.NewDirRef("/", k, map[string]resources.Ref{
+		"namespaces": resources.NewNamespacesRef(k.client, k),
+		"cluster":    resources.NewDirRef("cluster", k, map[string]resources.Ref{}),
+	}))
+	if err != nil {
+		return p9p.Qid{}, err
+	}
+
+	return ref.Info().Qid, nil
+}
+
+func (k *Session) Clunk(ctx context.Context, fid p9p.Fid) error {
+	_, err := k.getRef(fid)
+	if err != nil {
+		return err
+	}
+
+	k.Lock()
+	defer k.Unlock()
+	delete(k.refs, fid)
+
+	return nil
+}
+
+func (k *Session) Remove(ctx context.Context, fid p9p.Fid) error {
+	return p9p.ErrUnknownMsg
+}
+
+func (k *Session) Walk(ctx context.Context, fid p9p.Fid, newfid p9p.Fid, names ...string) ([]p9p.Qid, error) {
+	var qids []p9p.Qid
+
+	ref, err := k.getRef(fid)
+	if err != nil {
+		return qids, err
+	}
+
+	current := ref
+	for _, name := range names {
+		newResource, err := current.Get(name)
+		if err != nil {
+			break
+		}
+
+		qids = append(qids, newResource.Info().Qid)
+		current = newResource
+	}
+
+	if len(qids) != len(names) {
+		return qids, nil
+	}
+
+	_, err = k.newRef(newfid, current)
+	if err != nil {
+		return qids, err
+	}
+
+	return qids, nil
+}
+
+func (k *Session) Read(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) {
+	ref, err := k.getRef(fid)
+	if err != nil {
+		return 0, err
+	}
+
+	return ref.Read(ctx, p, offset)
+}
+
+func (k *Session) Write(ctx context.Context, fid p9p.Fid, p []byte, offset int64) (n int, err error) {
+	return 0, p9p.ErrUnknownMsg
+}
+
+func (k *Session) Open(ctx context.Context, fid p9p.Fid, mode p9p.Flag) (p9p.Qid, uint32, error) {
+	ref, err := k.getRef(fid)
+	if err != nil {
+		return p9p.Qid{}, 0, err
+	}
+
+	return ref.Info().Qid, 0, nil
+}
+
+func (k *Session) Create(ctx context.Context, parent p9p.Fid, name string, perm uint32, mode p9p.Flag) (p9p.Qid, uint32, error) {
+	return p9p.Qid{}, 0, p9p.ErrUnknownMsg
+}
+
+func (k *Session) Stat(ctx context.Context, fid p9p.Fid) (p9p.Dir, error) {
+	ref, err := k.getRef(fid)
+	if err != nil {
+		return p9p.Dir{}, err
+	}
+
+	return ref.Info(), nil
+}
+
+func (k *Session) WStat(ctx context.Context, fid p9p.Fid, dir p9p.Dir) error {
+	return p9p.ErrUnknownMsg
+}
+
+// Version returns the supported version and msize of the session. This
+// can be affected by negotiating or the level of support provided by the
+// session implementation.
+func (k *Session) Version() (msize int, version string) {
+	return p9p.DefaultMSize, p9p.DefaultVersion
+}
+
+func (k *Session) WaitForCacheSync(stopCh <-chan struct{}) {
+	k.sharedInformer.WaitForCacheSync(stopCh)
+}
+
+func (k *Session) GetAuth() (uname, aname string) {
+	return k.uname, k.aname
+}
+
+func (k *Session) Informer() informers.SharedInformerFactory {
+	return k.sharedInformer
+}
diff --git a/pkg/resources/deployments.go b/pkg/resources/deployments.go
new file mode 100644
index 0000000..065d03b
--- /dev/null
+++ b/pkg/resources/deployments.go
@@ -0,0 +1,184 @@
+package resources
+
+import (
+	"context"
+	"io"
+	"math/rand"
+	"strconv"
+	"time"
+
+	"github.com/docker/go-p9p"
+	v1 "k8s.io/api/apps/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	appsv1 "k8s.io/client-go/informers/apps/v1"
+	"sigs.k8s.io/yaml"
+)
+
+type Deployments struct {
+	namespace          string
+	deploymentInformer appsv1.DeploymentInformer
+	session            Session
+	info               *p9p.Dir
+	readdir            *p9p.Readdir
+}
+
+func NewDeployments(namespace string, session Session) *Deployments {
+	deploymentInformer := session.Informer().Apps().V1().Deployments()
+	return &Deployments{
+		namespace:          namespace,
+		deploymentInformer: deploymentInformer,
+		session:            session,
+	}
+}
+
+func (r *Deployments) Info() p9p.Dir {
+	if r.info != nil {
+		return *r.info
+	}
+
+	dir := p9p.Dir{}
+	dir.Qid.Path = rand.Uint64()
+	dir.Qid.Version = 0
+
+	dir.Name = "deployments"
+	dir.Mode = 0664
+	dir.Length = 0
+	dir.AccessTime = time.Now()
+	dir.ModTime = time.Now()
+	dir.MUID = "none"
+
+	uname, _ := r.session.GetAuth()
+	dir.UID = uname
+	dir.GID = uname
+
+	dir.Qid.Type |= p9p.QTDIR
+	dir.Mode |= p9p.DMDIR
+	r.info = &dir
+
+	return dir
+}
+
+func (r *Deployments) Get(name string) (Ref, error) {
+	deployment, err := r.deploymentInformer.Lister().Deployments(r.namespace).Get(name)
+	if apierrors.IsNotFound(err) {
+		return nil, p9p.ErrNotfound
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	return NewDeploymentRef(deployment, r.session), nil
+}
+
+func (r *Deployments) Read(ctx context.Context, p []byte, offset int64) (n int, err error) {
+	if r.readdir != nil {
+		return r.readdir.Read(ctx, p, offset)
+	}
+
+	deployments, err := r.deploymentInformer.Lister().Deployments(r.namespace).List(labels.Everything())
+	if err != nil {
+		return 0, err
+	}
+
+	deploymentRefs := make([]Ref, 0, len(deployments))
+
+	for _, deployment := range deployments {
+		deployment := deployment
+		deploymentRefs = append(deploymentRefs, NewDeploymentRef(deployment, r.session))
+	}
+
+	r.readdir = p9p.NewReaddir(p9p.NewCodec(), func() (p9p.Dir, error) {
+		if len(deploymentRefs) == 0 {
+			return p9p.Dir{}, io.EOF
+		}
+
+		deployment := deploymentRefs[0]
+		deploymentRefs = deploymentRefs[1:]
+
+		return deployment.Info(), nil
+	})
+
+	n, err = r.readdir.Read(ctx, p, offset)
+
+	return n, err
+}
+
+type DeploymentRef struct {
+	deployment *v1.Deployment
+	session    Session
+	info       *p9p.Dir
+	readdir    *p9p.Readdir
+	children   map[string]Ref
+}
+
+func NewDeploymentRef(deployment *v1.Deployment, session Session) *DeploymentRef {
+	y, _ := yaml.Marshal(deployment)
+	children := map[string]Ref{
+		"data.yaml": &Static{
+			name:    "data.yaml",
+			content: y,
+			session: session,
+		},
+		"scale": &Static{
+			name:    "scale",
+			content: []byte(strconv.Itoa(int(*deployment.Spec.Replicas))),
+			session: session,
+		},
+	}
+	return &DeploymentRef{
+		deployment: deployment,
+		session:    session,
+		children:   children,
+	}
+}
+
+func (r *DeploymentRef) Info() p9p.Dir {
+	if r.info != nil {
+		return *r.info
+	}
+
+	dir := p9p.Dir{}
+	dir.Qid.Path = rand.Uint64()
+	dir.Qid.Version = 0
+
+	dir.Name = r.deployment.Name
+	dir.Mode = 0664
+	dir.Length = 0
+	dir.AccessTime = r.deployment.CreationTimestamp.Time
+	dir.ModTime = r.deployment.CreationTimestamp.Time
+	dir.MUID = "none"
+
+	uname, _ := r.session.GetAuth()
+	dir.UID = uname
+	dir.GID = uname
+
+	dir.Qid.Type |= p9p.QTDIR
+	dir.Mode |= p9p.DMDIR
+	r.info = &dir
+
+	return dir
+}
+
+func (r *DeploymentRef) Get(name string) (Ref, error) {
+	ref, ok := r.children[name]
+	if !ok {
+		return nil, p9p.ErrNotfound
+	}
+
+	return ref, nil
+}
+
+func (r *DeploymentRef) Read(ctx context.Context, p []byte, offset int64) (int, error) {
+	if r.readdir != nil {
+		return r.readdir.Read(ctx, p, offset)
+	}
+
+	dir := make([]p9p.Dir, 0, len(r.children))
+	for _, child := range r.children {
+		dir = append(dir, child.Info())
+	}
+
+	r.readdir = p9p.NewFixedReaddir(p9p.NewCodec(), dir)
+	return r.readdir.Read(ctx, p, offset)
+}
diff --git a/pkg/resources/files.go b/pkg/resources/files.go
new file mode 100644
index 0000000..64cf892
--- /dev/null
+++ b/pkg/resources/files.go
@@ -0,0 +1,60 @@
+package resources
+
+import (
+	"context"
+	"math/rand"
+	"time"
+
+	"github.com/docker/go-p9p"
+	"github.com/rs/zerolog/log"
+)
+
+type Static struct {
+	name    string
+	offset  int64
+	content []byte
+	info    *p9p.Dir
+	session Session
+}
+
+func (r *Static) Info() p9p.Dir {
+	if r.info != nil {
+		return *r.info
+	}
+
+	dir := p9p.Dir{}
+	dir.Qid.Path = rand.Uint64()
+	dir.Qid.Version = 0
+
+	dir.Name = r.name
+	dir.Mode = 0664
+	dir.Length = 0
+	dir.AccessTime = time.Now()
+	dir.ModTime = time.Now()
+	dir.MUID = "none"
+
+	uname, _ := r.session.GetAuth()
+	dir.UID = uname
+	dir.GID = uname
+
+	dir.Qid.Type |= p9p.QTFILE
+
+	r.info = &dir
+	return dir
+}
+
+func (r *Static) Get(name string) (Ref, error) {
+	return nil, p9p.ErrWalknodir
+}
+
+func (r *Static) Read(ctx context.Context, p []byte, offset int64) (n int, err error) {
+	log.Debug().Int64("offset", r.offset).Str("name", r.name).Send()
+	if offset != r.offset {
+		return 0, p9p.ErrBadoffset
+	}
+
+	n = copy(p, r.content[offset:])
+	r.offset += int64(n)
+
+	return n, nil
+}
diff --git a/pkg/resources/namespace.go b/pkg/resources/namespace.go
new file mode 100644
index 0000000..df08c2d
--- /dev/null
+++ b/pkg/resources/namespace.go
@@ -0,0 +1,68 @@
+package resources
+
+import (
+	"context"
+	"math/rand"
+
+	"github.com/docker/go-p9p"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type NamespaceRef struct {
+	namespace *v1.Namespace
+	client    kubernetes.Interface
+	session   Session
+	info      *p9p.Dir
+	readdir   *p9p.Readdir
+}
+
+func (r *NamespaceRef) Info() p9p.Dir {
+	if r.info != nil {
+		return *r.info
+	}
+
+	dir := p9p.Dir{}
+	dir.Qid.Path = rand.Uint64()
+	dir.Qid.Version = uint32(r.namespace.Generation)
+
+	dir.Name = r.namespace.Name
+	dir.Mode = 0664
+	dir.Length = 0
+	dir.AccessTime = r.namespace.CreationTimestamp.Time
+	dir.ModTime = r.namespace.CreationTimestamp.Time
+	dir.MUID = "none"
+
+	uname, _ := r.session.GetAuth()
+	dir.UID = uname
+	dir.GID = uname
+
+	dir.Qid.Type |= p9p.QTDIR
+	dir.Mode |= p9p.DMDIR
+
+	r.info = &dir
+	return dir
+}
+
+func (r *NamespaceRef) Get(name string) (Ref, error) {
+	switch name {
+	case "deployments":
+		return NewDeployments(r.namespace.Name, r.session), nil
+	}
+
+	return nil, p9p.ErrNotfound
+}
+
+func (r *NamespaceRef) Read(ctx context.Context, p []byte, offset int64) (n int, err error) {
+	if r.readdir != nil {
+		return r.readdir.Read(ctx, p, offset)
+	}
+
+	deployments := NewDeployments(r.namespace.Name, r.session)
+	dir := []p9p.Dir{
+		deployments.Info(),
+	}
+
+	r.readdir = p9p.NewFixedReaddir(p9p.NewCodec(), dir)
+	return r.readdir.Read(ctx, p, offset)
+}
diff --git a/pkg/resources/namespaces.go b/pkg/resources/namespaces.go
new file mode 100644
index 0000000..15ea9d6
--- /dev/null
+++ b/pkg/resources/namespaces.go
@@ -0,0 +1,110 @@
+package resources
+
+import (
+	"context"
+	"io"
+	"math/rand"
+	"time"
+
+	"github.com/docker/go-p9p"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	corev1 "k8s.io/client-go/informers/core/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type NamespacesRef struct {
+	client            kubernetes.Interface
+	namespaceInformer corev1.NamespaceInformer
+	session           Session
+	info              *p9p.Dir
+	readdir           *p9p.Readdir
+}
+
+func NewNamespacesRef(client kubernetes.Interface, session Session) *NamespacesRef {
+	namespaceInformer := session.Informer().Core().V1().Namespaces()
+
+	return &NamespacesRef{
+		client:            client,
+		namespaceInformer: namespaceInformer,
+		session:           session,
+	}
+}
+
+func (r *NamespacesRef) Info() p9p.Dir {
+	if r.info != nil {
+		return *r.info
+	}
+
+	dir := p9p.Dir{}
+	dir.Qid.Path = rand.Uint64()
+	dir.Qid.Version = 0
+
+	dir.Name = "namespaces"
+	dir.Mode = 0664
+	dir.Length = 0
+	dir.AccessTime = time.Now()
+	dir.ModTime = time.Now()
+	dir.MUID = "none"
+
+	uname, _ := r.session.GetAuth()
+	dir.UID = uname
+	dir.GID = uname
+
+	dir.Qid.Type |= p9p.QTDIR
+	dir.Mode |= p9p.DMDIR
+	r.info = &dir
+
+	return dir
+}
+
+func (r *NamespacesRef) Get(name string) (Ref, error) {
+	namespace, err := r.namespaceInformer.Lister().Get(name)
+	if apierrors.IsNotFound(err) {
+		return nil, p9p.ErrNotfound
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	return &NamespaceRef{
+		namespace: namespace,
+		client:    r.client,
+		session:   r.session,
+	}, nil
+}
+
+func (r *NamespacesRef) Read(ctx context.Context, p []byte, offset int64) (n int, err error) {
+	if r.readdir != nil {
+		return r.readdir.Read(ctx, p, offset)
+	}
+
+	namespaces, err := r.namespaceInformer.Lister().List(labels.Everything())
+	if err != nil {
+		return 0, err
+	}
+
+	namespaceRefs := make([]NamespaceRef, 0, len(namespaces))
+
+	for _, namespace := range namespaces {
+		namespace := namespace
+		namespaceRefs = append(namespaceRefs, NamespaceRef{
+			namespace: namespace,
+			client:    r.client,
+			session:   r.session,
+		})
+	}
+
+	r.readdir = p9p.NewReaddir(p9p.NewCodec(), func() (p9p.Dir, error) {
+		if len(namespaceRefs) == 0 {
+			return p9p.Dir{}, io.EOF
+		}
+
+		ns := namespaceRefs[0]
+		namespaceRefs = namespaceRefs[1:]
+
+		return ns.Info(), nil
+	})
+
+	return r.readdir.Read(ctx, p, offset)
+}
diff --git a/pkg/resources/refs.go b/pkg/resources/refs.go
new file mode 100644
index 0000000..3ede9e7
--- /dev/null
+++ b/pkg/resources/refs.go
@@ -0,0 +1,13 @@
+package resources
+
+import (
+	"context"
+
+	"github.com/docker/go-p9p"
+)
+
+type Ref interface {
+	Info() p9p.Dir
+	Get(name string) (Ref, error)
+	Read(ctx context.Context, p []byte, offset int64) (n int, err error)
+}
diff --git a/pkg/resources/resources.go b/pkg/resources/resources.go
new file mode 100644
index 0000000..8c2eed5
--- /dev/null
+++ b/pkg/resources/resources.go
@@ -0,0 +1,12 @@
+package resources
+
+import (
+	"github.com/docker/go-p9p"
+	"k8s.io/client-go/informers"
+)
+
+type Session interface {
+	p9p.Session
+	GetAuth() (uname, aname string)
+	Informer() informers.SharedInformerFactory
+}
diff --git a/pkg/resources/staticdir.go b/pkg/resources/staticdir.go
new file mode 100644
index 0000000..cfa969a
--- /dev/null
+++ b/pkg/resources/staticdir.go
@@ -0,0 +1,76 @@
+package resources
+
+import (
+	"context"
+	"math/rand"
+	"time"
+
+	"github.com/docker/go-p9p"
+)
+
+type DirRef struct {
+	path     string
+	info     p9p.Dir
+	session  Session
+	children map[string]Ref
+	readdir  *p9p.Readdir
+}
+
+func NewDirRef(path string, session Session, children map[string]Ref) *DirRef {
+	d := &DirRef{
+		path:     path,
+		session:  session,
+		children: children,
+	}
+	d.info = d.createInfo()
+
+	return d
+}
+
+func (d *DirRef) createInfo() p9p.Dir {
+	dir := p9p.Dir{}
+	dir.Qid.Path = rand.Uint64()
+	dir.Qid.Version = 0
+
+	dir.Name = d.path
+	dir.Mode = 0664
+	dir.Length = 0
+	dir.AccessTime = time.Now()
+	dir.ModTime = time.Now()
+	dir.MUID = "none"
+
+	uname, _ := d.session.GetAuth()
+	dir.UID = uname
+	dir.GID = uname
+
+	dir.Qid.Type |= p9p.QTDIR
+	dir.Mode |= p9p.DMDIR
+
+	return dir
+}
+
+func (d *DirRef) Info() p9p.Dir {
+	return d.info
+}
+
+func (d *DirRef) Get(name string) (Ref, error) {
+	child, ok := d.children[name]
+	if !ok {
+		return nil, p9p.ErrNotfound
+	}
+
+	return child, nil
+}
+
+func (d *DirRef) Read(ctx context.Context, p []byte, offset int64) (n int, err error) {
+	if d.readdir != nil {
+		return d.readdir.Read(ctx, p, offset)
+	}
+
+	dir := make([]p9p.Dir, 0, len(d.children))
+	for _, child := range d.children {
+		dir = append(dir, child.Info())
+	}
+	d.readdir = p9p.NewFixedReaddir(p9p.NewCodec(), dir)
+	return d.readdir.Read(ctx, p, offset)
+}