checkpoint

mine
derailed 2019-12-26 13:53:49 -07:00
parent add0d678f0
commit 99fa0e9952
40 changed files with 541 additions and 413 deletions

2
go.mod
View File

@ -36,7 +36,7 @@ require (
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell v1.3.0
github.com/ghodss/yaml v1.0.0 // indirect
github.com/ghodss/yaml v1.0.0
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
github.com/golang/mock v1.2.0
github.com/google/btree v1.0.0 // indirect

View File

@ -64,11 +64,11 @@ func (g GVR) ToRAndG() (string, string) {
tokens := strings.Split(string(g), "/")
switch len(tokens) {
case 3:
return tokens[0], tokens[2]
return tokens[2], tokens[0]
case 2:
return "", tokens[1]
return tokens[1], "core"
default:
return "", tokens[0]
return tokens[0], "core"
}
}

View File

@ -24,9 +24,9 @@ type Aliases struct {
// NewAliases return a new alias.
func NewAliases() Aliases {
aa := Aliases{Alias: make(Alias, 50)}
aa.loadDefaults()
return aa
return Aliases{
Alias: make(Alias, 50),
}
}
func (a Aliases) loadDefaults() {
@ -81,6 +81,7 @@ func (a Aliases) loadDefaults() {
// Load K9s aliases.
func (a Aliases) Load() error {
a.loadDefaults()
return a.LoadAliases(K9sAlias)
}

View File

@ -73,7 +73,7 @@ func TestAliasesLoad(t *testing.T) {
a := config.NewAliases()
assert.Nil(t, a.LoadAliases("test_assets/alias.yml"))
assert.Equal(t, 27, len(a.Alias))
assert.Equal(t, 2, len(a.Alias))
}
func TestAliasesSave(t *testing.T) {
@ -83,5 +83,5 @@ func TestAliasesSave(t *testing.T) {
assert.Nil(t, a.SaveAliases("/tmp/a.yml"))
assert.Nil(t, a.LoadAliases("/tmp/a.yml"))
assert.Equal(t, 28, len(a.Alias))
assert.Equal(t, 2, len(a.Alias))
}

View File

@ -1,8 +1,64 @@
package dao
// Alias represents an alias resource.
import (
"strings"
"github.com/derailed/k9s/internal/config"
)
// Alias tracks standard and custom command aliases.
type Alias struct {
Generic
config.Aliases
factory Factory
}
var _ Accessor = &Alias{}
// NewAlias returns a new set of aliases.
func NewAlias(f Factory) *Alias {
return &Alias{
Aliases: config.NewAliases(),
factory: f,
}
}
// ClearAliases remove all aliases.
func (a *Alias) Clear() {
for k := range a.Alias {
delete(a.Alias, k)
}
}
// Ensure makes sure alias are loaded.
func (a *Alias) Ensure() (config.Alias, error) {
if len(a.Alias) == 0 {
if err := LoadResources(a.factory); err != nil {
return config.Alias{}, err
}
return a.Alias, a.load()
}
return a.Alias, nil
}
func (a *Alias) load() error {
if err := a.Load(); err != nil {
return err
}
for _, gvr := range AllGVRs() {
meta, err := MetaFor(gvr)
if err != nil {
return err
}
if _, ok := a.Alias[meta.Kind]; ok {
continue
}
a.Define(string(gvr), strings.ToLower(meta.Kind), meta.Name)
if meta.SingularName != "" {
a.Define(string(gvr), meta.SingularName)
}
if meta.ShortNames != nil {
a.Define(string(gvr), meta.ShortNames...)
}
}
return nil
}

View File

@ -2,8 +2,6 @@ package dao
import (
"os"
"github.com/rs/zerolog/log"
)
// Benchmark represents a benchmark resource.
@ -16,6 +14,5 @@ var _ Nuker = &Benchmark{}
// Delete a Benchmark.
func (d *Benchmark) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("Benchmark DELETE %q", path)
return os.Remove(path)
}

View File

@ -4,12 +4,11 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
type Context struct {
@ -29,7 +28,7 @@ func (c *Context) Get(_, n string) (runtime.Object, error) {
if err != nil {
return nil, err
}
return &NamedContext{Name: n, Context: ctx}, nil
return &render.NamedContext{Name: n, Context: ctx}, nil
}
// List all Contexts on the current cluster.
@ -40,7 +39,7 @@ func (c *Context) List(string, metav1.ListOptions) ([]runtime.Object, error) {
}
cc := make([]runtime.Object, 0, len(ctxs))
for k, v := range ctxs {
cc = append(cc, NewNamedContext(c.config(), k, v))
cc = append(cc, render.NewNamedContext(c.config(), k, v))
}
return cc, nil
@ -90,33 +89,33 @@ func (c *Context) KubeUpdate(n string) error {
// ----------------------------------------------------------------------------
// NamedContext represents a named cluster context.
type NamedContext struct {
Name string
Context *api.Context
config *client.Config
}
// // NamedContext represents a named cluster context.
// type NamedContext struct {
// Name string
// Context *api.Context
// config *client.Config
// }
// NewNamedContext returns a new named context.
func NewNamedContext(c *client.Config, n string, ctx *api.Context) *NamedContext {
return &NamedContext{Name: n, Context: ctx, config: c}
}
// // NewNamedContext returns a new named context.
// func NewNamedContext(c *client.Config, n string, ctx *api.Context) *NamedContext {
// return &NamedContext{Name: n, Context: ctx, config: c}
// }
// MustCurrentContextName return the active context name.
func (c *NamedContext) MustCurrentContextName() string {
cl, err := c.config.CurrentContextName()
if err != nil {
log.Fatal().Err(err).Msg("Fetching current context")
}
return cl
}
// // MustCurrentContextName return the active context name.
// func (c *NamedContext) MustCurrentContextName() string {
// cl, err := c.config.CurrentContextName()
// if err != nil {
// log.Fatal().Err(err).Msg("Fetching current context")
// }
// return cl
// }
// GetObjectKind returns a schema object.
func (c *NamedContext) GetObjectKind() schema.ObjectKind {
return nil
}
// // GetObjectKind returns a schema object.
// func (c *NamedContext) GetObjectKind() schema.ObjectKind {
// return nil
// }
// DeepCopyObject returns a container copy.
func (c *NamedContext) DeepCopyObject() runtime.Object {
return c
}
// // DeepCopyObject returns a container copy.
// func (c *NamedContext) DeepCopyObject() runtime.Object {
// return c
// }

View File

@ -6,7 +6,6 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -62,7 +61,6 @@ func (d *Deployment) Restart(path string) error {
// Logs tail logs for all pods represented by this Deployment.
func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing Deployment %q", opts.Path)
o, err := d.Get(string(d.gvr), opts.Path, labels.Everything())
if err != nil {
return err

View File

@ -52,7 +52,6 @@ func (d *DaemonSet) Restart(path string) error {
// Logs tail logs for all pods represented by this DaemonSet.
func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing DaemonSet %q", opts.Path)
o, err := d.Get("apps/v1/daemonsets", opts.Path, labels.Everything())
if err != nil {
return err
@ -112,6 +111,7 @@ func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts L
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func toSelector(m map[string]string) string {

View File

@ -2,7 +2,6 @@ package dao
import (
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
)
@ -25,7 +24,6 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
}
ns, n := client.Namespaced(path)
log.Debug().Msgf("DELETING %q:%q -- %q", ns, n, path)
opts := metav1.DeleteOptions{PropagationPolicy: &p}
if ns != "-" {
return g.dynClient().Namespace(ns).Delete(n, &opts)

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
@ -21,7 +20,6 @@ var _ Loggable = &Job{}
// Logs tail logs for all pods represented by this Job.
func (j *Job) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing Job %#v", opts)
o, err := j.Get(string(j.gvr), opts.Path, labels.Everything())
if err != nil {
return err

View File

@ -1,9 +1,5 @@
package dao
import (
"github.com/rs/zerolog/log"
)
type PortForward struct {
Generic
}
@ -13,7 +9,6 @@ var _ Nuker = &PortForward{}
// Delete a portforward.
func (p *PortForward) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("PortForward DELETE %q", path)
p.Factory.DeleteForwarder(path)
return nil
}

View File

@ -5,7 +5,6 @@ import (
"sort"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -26,7 +25,6 @@ var resMetas = ResourceMetas{}
// Customize here for non resource types or types with metrics or logs.
func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
m := Accessors{
"alias": &Alias{},
"contexts": &Context{},
"containers": &Container{},
"screendumps": &ScreenDump{},
@ -88,7 +86,8 @@ func IsK9sMeta(m metav1.APIResource) bool {
}
// Load hydrates server preferred+CRDs resource metadata.
func Load(f *watch.Factory) error {
func LoadResources(f Factory) error {
log.Debug().Msgf("LOAD RES")
resMetas = make(ResourceMetas, 100)
if err := loadPreferred(f, resMetas); err != nil {
return err
@ -136,12 +135,12 @@ func loadNonResource(m ResourceMetas) error {
Categories: []string{"k9s"},
}
m["rbac"] = metav1.APIResource{
Name: "Rbac",
Name: "rbacs",
Kind: "Rules",
Categories: []string{"k9s"},
}
m["policy"] = metav1.APIResource{
Name: "Policy",
Name: "policies",
Kind: "Rules",
Namespaced: true,
Categories: []string{"k9s"},
@ -158,14 +157,14 @@ func loadNonResource(m ResourceMetas) error {
}
m["groups"] = metav1.APIResource{
Name: "groups",
Kind: "group",
Kind: "Group",
Categories: []string{"k9s"},
}
return nil
}
func loadPreferred(f *watch.Factory, m ResourceMetas) error {
func loadPreferred(f Factory, m ResourceMetas) error {
discovery, err := f.Client().CachedDiscovery()
if err != nil {
return err
@ -185,7 +184,7 @@ func loadPreferred(f *watch.Factory, m ResourceMetas) error {
return nil
}
func loadCRDs(f *watch.Factory, m ResourceMetas) error {
func loadCRDs(f Factory, m ResourceMetas) error {
oo, err := f.List("apiextensions.k8s.io/v1beta1/customresourcedefinitions", "", labels.Everything())
if err != nil {
return err

View File

@ -2,8 +2,6 @@ package dao
import (
"os"
"github.com/rs/zerolog/log"
)
type ScreenDump struct {
@ -15,6 +13,5 @@ var _ Nuker = &ScreenDump{}
// Delete a ScreenDump.
func (d *ScreenDump) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("ScreenDump DELETE %q", path)
return os.Remove(path)
}

View File

@ -6,7 +6,6 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -62,7 +61,6 @@ func (s *StatefulSet) Restart(path string) error {
// Logs tail logs for all pods represented by this StatefulSet.
func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing StatefulSet %q", opts.Path)
o, err := s.Get(string(s.gvr), opts.Path, labels.Everything())
if err != nil {
return err

View File

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
@ -21,7 +20,6 @@ var _ Loggable = &Service{}
// Logs tail logs for all pods represented by this Service.
func (s *Service) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing Service %q", opts.Path)
o, err := s.Get(string(s.gvr), opts.Path, labels.Everything())
if err != nil {
return err

View File

@ -7,6 +7,7 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"k8s.io/apimachinery/pkg/runtime"
)
@ -18,13 +19,13 @@ type Alias struct {
// List returns a collection of screen dumps.
func (b *Alias) List(ctx context.Context) ([]runtime.Object, error) {
aa, ok := ctx.Value(internal.KeyAliases).(config.Alias)
a, ok := ctx.Value(internal.KeyAliases).(*dao.Alias)
if !ok {
return nil, errors.New("no aliases found in context")
}
m := make(config.ShortNames, len(aa))
for alias, gvr := range aa {
m := make(config.ShortNames, len(a.Alias))
for alias, gvr := range a.Alias {
if _, ok := m[gvr]; ok {
m[gvr] = append(m[gvr], alias)
} else {
@ -40,13 +41,3 @@ func (b *Alias) List(ctx context.Context) ([]runtime.Object, error) {
return oo, nil
}
// Hydrate returns a pod as container rows.
func (b *Alias) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
if err := re.Render(o, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,84 @@
package model_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
)
func TestAliasList(t *testing.T) {
a := model.Alias{}
a.Init(render.ClusterScope, "aliases", makeFactory())
ctx := context.WithValue(context.Background(), internal.KeyAliases, makeAliases())
oo, err := a.List(ctx)
assert.Nil(t, err)
assert.Equal(t, 2, len(oo))
assert.Equal(t, 2, len(oo[0].(render.AliasRes).Aliases))
}
func TestAliasHydrate(t *testing.T) {
a := model.Alias{}
a.Init(render.ClusterScope, "aliases", makeFactory())
ctx := context.WithValue(context.Background(), internal.KeyAliases, makeAliases())
oo, err := a.List(ctx)
assert.Nil(t, err)
rr := make(render.Rows, len(oo))
assert.Nil(t, a.Hydrate(oo, rr, render.Alias{}))
assert.Equal(t, 2, len(rr))
}
// ----------------------------------------------------------------------------
// Helpers...
func makeAliases() *dao.Alias {
return &dao.Alias{
Aliases: config.Aliases{
Alias: config.Alias{
"fred": "v1/fred",
"f": "v1/fred",
"blee": "v1/blee",
"b": "v1/blee",
},
},
}
}
type testFactory struct{}
var _ model.Factory = testFactory{}
func (f testFactory) Client() client.Connection {
return nil
}
func (f testFactory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) {
return nil, nil
}
func (f testFactory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) {
return nil, nil
}
func (f testFactory) ForResource(ns, gvr string) informers.GenericInformer {
return nil
}
func (f testFactory) WaitForCacheSync() {}
func (f testFactory) Forwarders() watch.Forwarders {
return nil
}
func makeFactory() model.Factory {
return testFactory{}
}

View File

@ -35,13 +35,3 @@ func (b *Benchmark) List(ctx context.Context) ([]runtime.Object, error) {
return oo, nil
}
// Hydrate returns a pod as container rows.
func (b *Benchmark) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
if err := re.Render(o, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,49 @@
package model_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestBenchmarkList(t *testing.T) {
a := model.Benchmark{}
a.Init(render.ClusterScope, "benchmarks", makeFactory())
ctx := context.WithValue(context.Background(), internal.KeyDir, "test_assets/bench")
oo, err := a.List(ctx)
assert.Nil(t, err)
assert.Equal(t, 1, len(oo))
assert.Equal(t, "test_assets/bench/default_fred_1577308050814961000.txt", oo[0].(render.BenchInfo).Path)
}
func TestBenchmarkHydrate(t *testing.T) {
a := model.Benchmark{}
a.Init(render.ClusterScope, "benchmarks", makeFactory())
ctx := context.WithValue(context.Background(), internal.KeyDir, "test_assets/bench")
oo, err := a.List(ctx)
assert.Nil(t, err)
rr := make(render.Rows, len(oo))
assert.Nil(t, a.Hydrate(oo, rr, render.Benchmark{}))
assert.Equal(t, 1, len(rr))
assert.Equal(t, "test_assets/bench/default_fred_1577308050814961000.txt", rr[0].ID)
assert.Equal(t, render.Fields{
"default",
"fred",
"fail",
"816.6403",
"0.0122",
"0",
"0",
"default_fred_1577308050814961000.txt",
},
rr[0].Fields[:len(rr[0].Fields)-1],
)
}

View File

@ -9,16 +9,12 @@ import (
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
var _ render.ContainerWithMetrics = &ContainerWithMetrics{}
// Container represents a container model.
type Container struct {
Resource
@ -46,77 +42,61 @@ func (c *Container) List(ctx context.Context) ([]runtime.Object, error) {
return nil, err
}
c.pod = &po
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
mx := client.NewMetricsServer(c.factory.Client())
var pmx *mv1beta1.PodMetrics
if c.factory.Client() != nil {
var err error
pmx, err = mx.FetchPodMetrics(c.namespace, c.pod.Name)
if err != nil {
log.Warn().Err(err).Msgf("No metrics found for pod %q:%q", c.namespace, c.pod.Name)
}
}
for _, co := range po.Spec.InitContainers {
res = append(res, ContainerRes{co})
res = append(res, makeContainerRes(co, po, pmx, true))
}
for _, co := range po.Spec.Containers {
res = append(res, ContainerRes{co})
res = append(res, makeContainerRes(co, po, pmx, false))
}
return res, nil
}
// Hydrate returns a pod as container rows.
func (c *Container) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
mx := client.NewMetricsServer(c.factory.Client().(client.Connection))
mmx, err := mx.FetchPodMetrics(c.namespace, c.pod.Name)
// ----------------------------------------------------------------------------
// Helpers...
func makeContainerRes(co v1.Container, po v1.Pod, pmx *mv1beta1.PodMetrics, isInit bool) render.ContainerRes {
cmx, err := containerMetrics(co.Name, pmx)
if err != nil {
log.Warn().Err(err).Msgf("No metrics found for pod %q:%q", c.namespace, c.pod.Name)
log.Warn().Err(err).Msgf("Container metrics for %s", co.Name)
}
var index int
for _, o := range oo {
co, ok := o.(ContainerRes)
if !ok {
return fmt.Errorf("expecting containerres but got `%T", o)
}
row, err := renderCoRow(co.Container.Name, coMetricsFor(co.Container, c.pod, mmx, true), re)
if err != nil {
return err
}
rr[index] = row
index++
}
return nil
}
func renderCoRow(n string, pmx *ContainerWithMetrics, re Renderer) (render.Row, error) {
var row render.Row
if err := re.Render(pmx, n, &row); err != nil {
return render.Row{}, err
}
return row, nil
}
func coMetricsFor(co v1.Container, po *v1.Pod, mmx *mv1beta1.PodMetrics, isInit bool) *ContainerWithMetrics {
return &ContainerWithMetrics{
container: &co,
status: getContainerStatus(co.Name, po.Status),
metrics: containerMetrics(co.Name, mmx),
isInit: isInit,
age: po.ObjectMeta.CreationTimestamp,
return render.ContainerRes{
Container: co,
Status: getContainerStatus(co.Name, po.Status),
Metrics: cmx,
IsInit: isInit,
Age: po.ObjectMeta.CreationTimestamp,
}
}
func containerMetrics(n string, mx runtime.Object) *mv1beta1.ContainerMetrics {
func containerMetrics(n string, mx runtime.Object) (*mv1beta1.ContainerMetrics, error) {
pmx, ok := mx.(*mv1beta1.PodMetrics)
if !ok {
log.Error().Err(fmt.Errorf("expecting podmetrics but got `%T", mx))
return nil
return nil, fmt.Errorf("expecting podmetrics but got `%T", mx)
}
if pmx == nil {
return nil, fmt.Errorf("no metrics for container %s", n)
}
for _, m := range pmx.Containers {
if m.Name == n {
return &m
return &m, nil
}
}
return nil
return nil, nil
}
// ----------------------------------------------------------------------------
func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
for _, c := range status.ContainerStatuses {
if c.Name == co {
@ -132,50 +112,3 @@ func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
return nil
}
// ContainerWithMetrics represents a container and its metrics.
type ContainerWithMetrics struct {
container *v1.Container
status *v1.ContainerStatus
metrics *mv1beta1.ContainerMetrics
isInit bool
age metav1.Time
}
func (c *ContainerWithMetrics) IsInit() bool {
return c.isInit
}
func (c *ContainerWithMetrics) Container() *v1.Container {
return c.container
}
func (c *ContainerWithMetrics) ContainerStatus() *v1.ContainerStatus {
return c.status
}
// Metrics returns the metrics associated with the pod.
func (c *ContainerWithMetrics) Metrics() *mv1beta1.ContainerMetrics {
return c.metrics
}
func (c *ContainerWithMetrics) Age() metav1.Time {
return c.age
}
// ----------------------------------------------------------------------------
// ContainerRes represents a container K8s resource.
type ContainerRes struct {
v1.Container
}
// GetObjectKind returns a schema object.
func (c ContainerRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (c ContainerRes) DeepCopyObject() runtime.Object {
return c
}

View File

@ -0,0 +1,113 @@
package model_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
)
func TestContainerList(t *testing.T) {
c := model.Container{}
c.Init(render.ClusterScope, "containers", makePodFactory())
ctx := context.WithValue(context.Background(), internal.KeyPath, "fred/p1")
oo, err := c.List(ctx)
assert.Nil(t, err)
assert.Equal(t, 1, len(oo))
}
func TestContainerHydrate(t *testing.T) {
c := model.Container{}
c.Init(render.ClusterScope, "containers", makePodFactory())
ctx := context.WithValue(context.Background(), internal.KeyPath, "fred/p1")
oo, err := c.List(ctx)
assert.Nil(t, err)
rr := make(render.Rows, len(oo))
assert.Nil(t, c.Hydrate(oo, rr, render.Container{}))
assert.Equal(t, 1, len(rr))
assert.Equal(t, "fred", rr[0].ID)
assert.Equal(t, render.Fields{"fred", "blee", "false", "Running", "false", "0", "off:off", "n/a", "n/a", "n/a", "n/a", ""}, rr[0].Fields[0:len(rr[0].Fields)-1])
}
// ----------------------------------------------------------------------------
// Helpers...
type podFactory struct{}
var _ model.Factory = testFactory{}
func (f podFactory) Client() client.Connection {
return nil
}
func (f podFactory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) {
var m map[string]interface{}
if err := yaml.Unmarshal([]byte(poYaml()), &m); err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: m}, nil
}
func (f podFactory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) {
return nil, nil
}
func (f podFactory) ForResource(ns, gvr string) informers.GenericInformer { return nil }
func (f podFactory) WaitForCacheSync() {}
func (f podFactory) Forwarders() watch.Forwarders { return nil }
func makePodFactory() model.Factory {
return podFactory{}
}
func poYaml() string {
return `apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2018-12-14T17:36:43Z"
labels:
blee: duh
name: fred
namespace: blee
spec:
containers:
- env:
- name: fred
value: "1"
valueFrom:
configMapKeyRef:
key: blee
image: blee
name: fred
resources: {}
priority: 1
priorityClassName: bozo
volumes:
- hostPath:
path: /blee
type: Directory
name: fred
status:
containerStatuses:
- image: ""
imageID: ""
lastState: {}
name: fred
ready: false
restartCount: 0
state:
running:
startedAt: null
phase: Running
`
}

View File

@ -2,7 +2,6 @@ package model
import (
"context"
"fmt"
"github.com/derailed/k9s/internal/render"
"k8s.io/apimachinery/pkg/runtime"
@ -27,23 +26,3 @@ func (c *Context) List(_ context.Context) ([]runtime.Object, error) {
return cc, nil
}
// Hydrate returns nodes as rows.
func (n *Context) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
var index int
for _, o := range oo {
ctx, ok := o.(*render.NamedContext)
if !ok {
return fmt.Errorf("expecting named context but got %T", o)
}
var row render.Row
if err := re.Render(ctx, "", &row); err != nil {
return err
}
rr[index] = row
index++
}
return nil
}

View File

@ -7,7 +7,6 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -62,15 +61,8 @@ func (c *Job) List(ctx context.Context) ([]runtime.Object, error) {
return jj, nil
}
// Hydrate returns a pod as container rows.
func (c *Job) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
if err := re.Render(o, c.namespace, &rr[i]); err != nil {
return err
}
}
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func isControlledBy(cuid, id string) bool {
tokens := strings.Split(cuid, "-")

View File

@ -44,22 +44,6 @@ func (c *PortForward) List(ctx context.Context) ([]runtime.Object, error) {
return oo, nil
}
// Hydrate returns a pod as container rows.
func (c *PortForward) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
res, ok := o.(render.ForwardRes)
if !ok {
return fmt.Errorf("expecting a forwardres but got %T", o)
}
if err := re.Render(res, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}
// ----------------------------------------------------------------------------
// Helpers...

View File

@ -1,4 +1,4 @@
package dao
package model
import (
"context"
@ -7,7 +7,6 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
)
@ -27,16 +26,16 @@ func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (ren
if !ok {
return table, fmt.Errorf("no factory found for %s", gvr)
}
m, ok := model.Registry[string(gvr)]
m, ok := Registry[string(gvr)]
if !ok {
log.Warn().Msgf("Resource %s not found in registry. Going generic!", gvr)
m = model.ResourceMeta{
Model: &model.Generic{},
m = ResourceMeta{
Model: &Generic{},
Renderer: &render.Generic{},
}
}
if m.Model == nil {
m.Model = &model.Resource{}
m.Model = &Resource{}
}
m.Model.Init(table.Namespace, string(gvr), factory)

View File

@ -41,14 +41,10 @@ func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) err
log.Debug().Msgf("HYDRATE elapsed: %v", time.Since(t))
}(time.Now())
var index int
for _, o := range oo {
var row render.Row
if err := re.Render(o, r.namespace, &row); err != nil {
for i, o := range oo {
if err := re.Render(o, r.namespace, &rr[i]); err != nil {
return err
}
rr[index] = row
index++
}
return nil

View File

@ -34,13 +34,3 @@ func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
return oo, nil
}
// Hydrate returns a pod as container rows.
func (c *ScreenDump) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
if err := re.Render(o, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}

View File

@ -3,7 +3,6 @@ package model
import (
"context"
"errors"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render"
@ -60,21 +59,8 @@ func (s *Subject) List(ctx context.Context) ([]runtime.Object, error) {
return oo, nil
}
// Hydrate returns a pod as container rows.
func (s *Subject) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
res, ok := o.(render.SubjectRef)
if !ok {
return fmt.Errorf("expecting unstructured but got %T", o)
}
if err := re.Render(res, render.AllNamespaces, &rr[i]); err != nil {
return err
}
}
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func inSubjectRes(oo []runtime.Object, match string) bool {
for _, o := range oo {

View File

@ -0,0 +1,24 @@
Summary:
Total: 816.6403 secs
Slowest: 0.0000 secs
Fastest: 0.0000 secs
Average: NaN secs
Requests/sec: 0.0122
Response time histogram:
Latency distribution:
Details (average, fastest, slowest):
DNS+dialup: NaN secs, 0.0000 secs, 0.0000 secs
DNS-lookup: NaN secs, 0.0000 secs, 0.0000 secs
req write: NaN secs, 0.0000 secs, 0.0000 secs
resp wait: NaN secs, 0.0000 secs, 0.0000 secs
resp read: NaN secs, 0.0000 secs, 0.0000 secs
Status code distribution:
Error distribution:
[10] Get http://192.168.64.126:30805/: dial tcp 192.168.64.126:30805: connect: operation timed out

View File

@ -31,16 +31,15 @@ func (Alias) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen.
// BOZO!! Pass in a row with pre-alloc fields??
func (Alias) Render(o interface{}, gvr string, r *Row) error {
func (Alias) Render(o interface{}, ns string, r *Row) error {
a, ok := o.(AliasRes)
if !ok {
return fmt.Errorf("expected AliasRes, but got %T", o)
}
_ = a
r.ID = gvr
gvr1 := client.GVR(a.GVR)
grp, res := gvr1.ToRAndG()
r.ID = a.GVR
gvr := client.GVR(a.GVR)
res, grp := gvr.ToRAndG()
r.Fields = append(r.Fields,
res,
strings.Join(a.Aliases, ","),
@ -50,6 +49,7 @@ func (Alias) Render(o interface{}, gvr string, r *Row) error {
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
// AliasRes represents an alias resource.

View File

@ -9,6 +9,8 @@ import (
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
@ -81,35 +83,33 @@ func (Container) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen.
func (c Container) Render(o interface{}, name string, r *Row) error {
oo, ok := o.(ContainerWithMetrics)
co, ok := o.(ContainerRes)
if !ok {
return fmt.Errorf("Expected ContainerWithMetrics, but got %T", o)
return fmt.Errorf("Expected ContainerRes, but got %T", o)
}
co, cs := oo.Container(), oo.ContainerStatus()
cur, perc := gatherMetrics(co, oo.Metrics())
cur, perc := gatherMetrics(co)
ready, state, restarts := "false", MissingValue, "0"
if cs != nil {
ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))
if co.Status != nil {
ready, state, restarts = boolToStr(co.Status.Ready), toState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount))
}
r.ID = co.Name
r.ID = co.Container.Name
r.Fields = make(Fields, 0, len(c.Header(AllNamespaces)))
r.Fields = append(r.Fields,
co.Name,
co.Image,
co.Container.Name,
co.Container.Image,
ready,
state,
boolToStr(oo.IsInit()),
boolToStr(co.IsInit),
restarts,
probe(co.LivenessProbe)+":"+probe(co.ReadinessProbe),
probe(co.Container.LivenessProbe)+":"+probe(co.Container.ReadinessProbe),
cur.cpu,
cur.mem,
perc.cpu,
perc.mem,
toStrPorts(co.Ports),
toAge(oo.Age()),
toStrPorts(co.Container.Ports),
toAge(co.Age),
)
return nil
@ -118,20 +118,20 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
// ----------------------------------------------------------------------------
// Helpers...
func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p metric) {
func gatherMetrics(co ContainerRes) (c, p metric) {
c, p = noMetric(), noMetric()
if mx == nil {
if co.Metrics == nil {
return
}
cpu := mx.Usage.Cpu().MilliValue()
mem := ToMB(mx.Usage.Memory().Value())
cpu := co.Metrics.Usage.Cpu().MilliValue()
mem := ToMB(co.Metrics.Usage.Memory().Value())
c = metric{
cpu: ToMillicore(cpu),
mem: ToMi(mem),
}
rcpu, rmem := containerResources(*co)
rcpu, rmem := containerResources(co.Container)
if rcpu != nil {
p.cpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue())))
}
@ -183,3 +183,22 @@ func probe(p *v1.Probe) string {
}
return "on"
}
// ContainerRes represents a container and its metrics.
type ContainerRes struct {
Container v1.Container
Status *v1.ContainerStatus
Metrics *mv1beta1.ContainerMetrics
IsInit bool
Age metav1.Time
}
// GetObjectKind returns a schema object.
func (c ContainerRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (c ContainerRes) DeepCopyObject() runtime.Object {
return c
}

View File

@ -16,9 +16,15 @@ import (
func TestContainer(t *testing.T) {
var c render.Container
var cm coMX
cres := render.ContainerRes{
Container: makeContainer(),
Status: makeContainerStatus(),
Metrics: makeContainerMetrics(),
IsInit: false,
Age: makeAge(),
}
var r render.Row
assert.Nil(t, c.Render(cm, "blee", &r))
assert.Nil(t, c.Render(cres, "blee", &r))
assert.Equal(t, "fred", r.ID)
assert.Equal(t, render.Fields{
"fred",
@ -47,19 +53,7 @@ func toQty(s string) resource.Quantity {
}
type coMX struct{}
var _ render.ContainerWithMetrics = coMX{}
func (c coMX) Container() *v1.Container {
return makeContainer()
}
func (c coMX) ContainerStatus() *v1.ContainerStatus {
return makeContainerStatus()
}
func (c coMX) Metrics() *mv1beta1.ContainerMetrics {
func makeContainerMetrics() *mv1beta1.ContainerMetrics {
return &mv1beta1.ContainerMetrics{
Name: "fred",
Usage: v1.ResourceList{
@ -69,16 +63,12 @@ func (c coMX) Metrics() *mv1beta1.ContainerMetrics {
}
}
func (c coMX) Age() metav1.Time {
func makeAge() metav1.Time {
return metav1.Time{Time: testTime()}
}
func (c coMX) IsInit() bool {
return false
}
func makeContainer() *v1.Container {
return &v1.Container{
func makeContainer() v1.Container {
return v1.Container{
Name: "fred",
Image: "img",
Resources: v1.ResourceRequirements{

View File

@ -34,7 +34,7 @@ func NewAlias(gvr client.GVR) ResourceViewer {
}
func (a *Alias) aliasContext(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeyAliases, aliases.Alias)
return context.WithValue(ctx, internal.KeyAliases, a.App().command.alias)
}
func (a *Alias) bindKeys(aa ui.KeyActions) {

View File

@ -43,7 +43,6 @@ func NewApp(cfg *config.Config) *App {
}
a.Config = cfg
a.InitBench(cfg.K9s.CurrentCluster)
a.command = newCommand(&a)
a.Views()["indicator"] = ui.NewIndicatorView(a.App, a.Styles)
a.Views()["clusterInfo"] = newClusterInfoView(&a, client.NewMetricsServer(cfg.GetConnection()))
@ -57,7 +56,6 @@ func (a *App) ActiveView() model.Component {
}
func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("PREVIOUS!!!")
a.Content.DumpStack()
a.Content.DumpPages()
if !a.Content.IsLast() {
@ -92,6 +90,11 @@ func (a *App) Init(version string, rate int) error {
a.factory = watch.NewFactory(a.Conn())
a.initFactory(ns)
a.command = newCommand(a)
if err := a.command.Init(); err != nil {
return err
}
a.clusterInfo().init(version)
if a.Config.K9s.GetHeadless() {
a.refreshIndicator()
@ -107,10 +110,6 @@ func (a *App) Init(version string, rate int) error {
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
a.toggleHeader(!a.Config.K9s.GetHeadless())
if err := a.command.Init(); err != nil {
panic(err)
}
return nil
}
@ -266,6 +265,9 @@ func (a *App) switchCtx(name string, loadPods bool) error {
}
a.initFactory(ns)
if err := a.command.Reset(); err != nil {
return err
}
a.Config.Reset()
if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")

View File

@ -14,6 +14,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
@ -392,7 +393,7 @@ func (b *Browser) refresh() {
if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" {
b.Path = path
}
data, err := dao.Reconcile(ctx, b.Table.Data, b.gvr)
data, err := model.Reconcile(ctx, b.Table.Data, b.gvr)
b.app.QueueUpdateDraw(func() {
if err != nil {
b.app.Flash().Err(err)

View File

@ -15,17 +15,20 @@ var customViewers MetaViewers
type command struct {
app *App
alias *dao.Alias
}
func newCommand(app *App) *command {
return &command{app: app}
return &command{
app: app,
}
}
func (c *command) Init() error {
if err := dao.Load(c.app.factory); err != nil {
return err
}
if err := loadAliases(); err != nil {
log.Debug().Msgf("COMMAND INIT")
c.alias = dao.NewAlias(c.app.factory)
if _, err := c.alias.Ensure(); err != nil {
return err
}
customViewers = loadCustomViewers()
@ -33,6 +36,16 @@ func (c *command) Init() error {
return nil
}
// Reset resets command and reload aliases.
func (c *command) Reset() error {
c.alias.Clear()
if _, err := c.alias.Ensure(); err != nil {
return err
}
return nil
}
func (c *command) defaultCmd() error {
return c.run(c.app.Config.ActiveView())
}
@ -68,7 +81,7 @@ func (c *command) isK9sCmd(cmd string) bool {
}
func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) {
gvr, ok := aliases.Get(cmd)
gvr, ok := c.alias.Get(cmd)
if !ok {
return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd)
}

View File

@ -11,13 +11,12 @@ import (
)
const (
group = "Group"
user = "User"
sa = "ServiceAccount"
allVerbs = "*"
group = "Group"
user = "User"
sa = "ServiceAccount"
)
// Policy presents a RBAC rules viewer.
// Policy presents a RBAC rules viewer based on what a given user/group or sa can do.
type Policy struct {
ResourceViewer

View File

@ -1,47 +1,7 @@
package view
import (
"strings"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
var aliases = config.NewAliases()
func ToResource(o *unstructured.Unstructured, obj interface{}) error {
return runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &obj)
}
func loadAliases() error {
if err := aliases.Load(); err != nil {
return err
}
for _, gvr := range dao.AllGVRs() {
meta, err := dao.MetaFor(gvr)
if err != nil {
return err
}
if _, ok := aliases.Alias[meta.Kind]; ok {
continue
}
aliases.Define(string(gvr), strings.ToLower(meta.Kind), meta.Name)
if meta.SingularName != "" {
aliases.Define(string(gvr), meta.SingularName)
}
if meta.ShortNames != nil {
aliases.Define(string(gvr), meta.ShortNames...)
}
}
return nil
}
func loadCustomViewers() MetaViewers {
m := make(MetaViewers, 30)
coreRes(m)
miscRes(m)
appsRes(m)
@ -88,12 +48,6 @@ func miscRes(vv MetaViewers) {
vv["aliases"] = MetaViewer{
viewerFn: NewAlias,
}
vv["users"] = MetaViewer{
viewerFn: NewUser,
}
vv["groups"] = MetaViewer{
viewerFn: NewGroup,
}
}
func appsRes(vv MetaViewers) {
@ -118,6 +72,12 @@ func rbacRes(vv MetaViewers) {
vv["rbac"] = MetaViewer{
enterFn: showRules,
}
vv["users"] = MetaViewer{
viewerFn: NewUser,
}
vv["groups"] = MetaViewer{
viewerFn: NewGroup,
}
vv["rbac.authorization.k8s.io/v1/clusterroles"] = MetaViewer{
enterFn: showRules,
}

View File

@ -41,30 +41,6 @@ func NewFactory(client client.Connection) *Factory {
}
}
func (f *Factory) Dump() {
log.Debug().Msgf("----------- FACTORIES -------------")
for ns := range f.factories {
log.Debug().Msgf(" Factory for NS %q", ns)
}
log.Debug().Msgf("-----------------------------------")
}
func (f *Factory) Debug(gvr string) {
log.Debug().Msgf("----------- DEBUG FACTORY (%s) -------------", gvr)
inf := f.factories[allNamespaces].ForResource(toGVR(gvr))
for i, k := range inf.Informer().GetStore().ListKeys() {
log.Debug().Msgf("%d -- %s", i, k)
}
}
func (f *Factory) Show(ns, gvr string) {
log.Debug().Msgf("----------- SHOW FACTORIES %q -------------", ns)
inf := f.ForResource(ns, gvr)
for _, k := range inf.Informer().GetStore().ListKeys() {
log.Debug().Msgf(" Key: %s", k)
}
}
func (f *Factory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) {
auth, err := f.Client().CanI(ns, gvr, []string{"list"})
if err != nil {
@ -237,6 +213,30 @@ func (f *Factory) Client() client.Connection {
// ----------------------------------------------------------------------------
// Helpers...
func (f *Factory) Dump() {
log.Debug().Msgf("----------- FACTORIES -------------")
for ns := range f.factories {
log.Debug().Msgf(" Factory for NS %q", ns)
}
log.Debug().Msgf("-----------------------------------")
}
func (f *Factory) Debug(gvr string) {
log.Debug().Msgf("----------- DEBUG FACTORY (%s) -------------", gvr)
inf := f.factories[allNamespaces].ForResource(toGVR(gvr))
for i, k := range inf.Informer().GetStore().ListKeys() {
log.Debug().Msgf("%d -- %s", i, k)
}
}
func (f *Factory) Show(ns, gvr string) {
log.Debug().Msgf("----------- SHOW FACTORIES %q -------------", ns)
inf := f.ForResource(ns, gvr)
for _, k := range inf.Informer().GetStore().ListKeys() {
log.Debug().Msgf(" Key: %s", k)
}
}
func namespaced(n string) (string, string) {
ns, po := path.Split(n)