add resource ref support for sec, sa and cm

mine
derailed 2020-05-27 21:23:02 -06:00
parent 01cdc5b86e
commit 678143e2a3
41 changed files with 1284 additions and 53 deletions

View File

@ -79,7 +79,7 @@ linters-settings:
# exclude: /path/to/file.txt # exclude: /path/to/file.txt
funlen: funlen:
lines: 75 lines: 100
statements: 40 statements: 40
govet: govet:

View File

@ -0,0 +1,57 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.19.8
## Notes
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
Also if you dig this tool, consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
---
## Music Behind This Release
And now for something a `beat` different...
I figured, why not share one of the tunes I was spinning when powering thru teh bugs.
I've just discovered this Turkish band and thought perhaps you can listen when reading this release notes?
[Ruh - She Past Away](https://www.youtube.com/watch?v=B7f-opGKOyI)
NOTE! Mind you I grew up with the `The Cure`, so likely not for everyone here 🙀
NOTE! If you dig this please lmk and will make this a k9s release tradition...
## PortForward revisited
While performing port-forwards it could be convenient to use a different IP address on a per cluster basis. For this reason, we are introducing a configuration setting that allows you to set the host IP address for the port-forward dialog on a given cluster. The address currently defaults to `localhost`.
Big Thanks and all credits goes to [Stowe4077](https://github.com/Stowe4077) and that very cute dog for raising this issue in the first place!!
In order to change the configuration, edit your k9s config file as follows:
```yaml
k9s:
...
clusters:
blee:
namespace:
active: ""
favorites:
- fred
- default
view:
active: po
portForwardAddress: 1.2.3.4
```
## Resolved Bugs/Features/PRs
* [Issue #734](https://github.com/derailed/k9s/issues/734)
* [Issue #733](https://github.com/derailed/k9s/issues/733)
* [Issue #716](https://github.com/derailed/k9s/issues/716)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

1
go.mod
View File

@ -3,6 +3,7 @@ module github.com/derailed/k9s
go 1.13 go 1.13
require ( require (
9fans.net/go v0.0.2
github.com/atotto/clipboard v0.1.2 github.com/atotto/clipboard v0.1.2
github.com/derailed/popeye v0.8.3 github.com/derailed/popeye v0.8.3
github.com/derailed/tview v0.3.10 github.com/derailed/tview v0.3.10

2
go.sum
View File

@ -1,3 +1,5 @@
9fans.net/go v0.0.2 h1:RYM6lWITV8oADrwLfdzxmt8ucfW6UtP9v1jg4qAbqts=
9fans.net/go v0.0.2/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM=
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=

View File

@ -1,8 +1,9 @@
package config package config
import ( import "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/client"
) // DefaultPFAddress specifies the default PortForward host address.
const DefaultPFAddress = "localhost"
// Cluster tracks K9s cluster configuration. // Cluster tracks K9s cluster configuration.
type Cluster struct { type Cluster struct {
@ -10,6 +11,7 @@ type Cluster struct {
View *View `yaml:"view"` View *View `yaml:"view"`
FeatureGates *FeatureGates `yaml:"featureGates"` FeatureGates *FeatureGates `yaml:"featureGates"`
ShellPod *ShellPod `yaml:"shellPod"` ShellPod *ShellPod `yaml:"shellPod"`
PortForwardAddress string `yaml:"portForwardAddress"`
} }
// NewCluster creates a new cluster configuration. // NewCluster creates a new cluster configuration.
@ -17,6 +19,7 @@ func NewCluster() *Cluster {
return &Cluster{ return &Cluster{
Namespace: NewNamespace(), Namespace: NewNamespace(),
View: NewView(), View: NewView(),
PortForwardAddress: DefaultPFAddress,
FeatureGates: NewFeatureGates(), FeatureGates: NewFeatureGates(),
ShellPod: NewShellPod(), ShellPod: NewShellPod(),
} }
@ -24,6 +27,10 @@ func NewCluster() *Cluster {
// Validate a cluster config. // Validate a cluster config.
func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) { func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) {
if c.PortForwardAddress == "" {
c.PortForwardAddress = DefaultPFAddress
}
if c.Namespace == nil { if c.Namespace == nil {
c.Namespace = NewNamespace() c.Namespace = NewNamespace()
} }

View File

@ -289,6 +289,7 @@ var expectedConfig = `k9s:
limits: limits:
cpu: 100m cpu: 100m
memory: 100Mi memory: 100Mi
portForwardAddress: localhost
fred: fred:
namespace: namespace:
active: default active: default
@ -308,6 +309,7 @@ var expectedConfig = `k9s:
limits: limits:
cpu: 100m cpu: 100m
memory: 100Mi memory: 100Mi
portForwardAddress: localhost
minikube: minikube:
namespace: namespace:
active: kube-system active: kube-system
@ -327,6 +329,7 @@ var expectedConfig = `k9s:
limits: limits:
cpu: 100m cpu: 100m
memory: 100Mi memory: 100Mi
portForwardAddress: localhost
thresholds: thresholds:
cpu: cpu:
critical: 90 critical: 90
@ -366,6 +369,7 @@ var resetConfig = `k9s:
limits: limits:
cpu: 100m cpu: 100m
memory: 100Mi memory: 100Mi
portForwardAddress: localhost
thresholds: thresholds:
cpu: cpu:
critical: 90 critical: 90

View File

@ -87,13 +87,15 @@ func (a *Alias) load() error {
if _, ok := a.Alias[meta.Kind]; ok || IsK9sMeta(meta) { if _, ok := a.Alias[meta.Kind]; ok || IsK9sMeta(meta) {
continue continue
} }
a.Define(gvr.String(), strings.ToLower(meta.Kind), meta.Name) gvrs := gvr.String()
a.Define(gvrs, strings.ToLower(meta.Kind), meta.Name)
if meta.SingularName != "" { if meta.SingularName != "" {
a.Define(gvr.String(), meta.SingularName) a.Define(gvrs, meta.SingularName)
} }
if meta.ShortNames != nil { if meta.ShortNames != nil {
a.Define(gvr.String(), meta.ShortNames...) a.Define(gvrs, meta.ShortNames...)
} }
a.Define(gvrs, gvrs)
} }
return nil return nil

150
internal/dao/cluster.go Normal file
View File

@ -0,0 +1,150 @@
package dao
import (
"context"
"errors"
"sync"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
)
// RefScanner represents a resource reference scanner.
type RefScanner interface {
// Init initializes the scanner
Init(Factory, client.GVR)
// Scan scan the resource for references.
Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error)
ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error)
}
// Ref represents a resource reference.
type Ref struct {
GVR string
FQN string
}
// Refs represents a collection of resource references.
type Refs []Ref
var (
_ RefScanner = (*Deployment)(nil)
_ RefScanner = (*StatefulSet)(nil)
_ RefScanner = (*DaemonSet)(nil)
_ RefScanner = (*Job)(nil)
_ RefScanner = (*CronJob)(nil)
_ RefScanner = (*Pod)(nil)
)
func scanners() map[string]RefScanner {
return map[string]RefScanner{
"apps/v1/deployments": &Deployment{},
"apps/v1/statefulsets": &StatefulSet{},
"apps/v1/daemonsets": &DaemonSet{},
"batch/v1/jobs": &Job{},
"batch/v1beta1/cronjobs": &CronJob{},
"v1/pods": &Pod{},
}
}
func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
defer func(t time.Time) {
log.Debug().Msgf("Cluster Scan %v", time.Since(t))
}(time.Now())
gvr, ok := ctx.Value(internal.KeyGVR).(string)
if !ok {
return nil, errors.New("expecting context GVR")
}
fqn, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, errors.New("expecting context Path")
}
wait, ok := ctx.Value(internal.KeyWait).(bool)
if !ok {
return nil, errors.New("expecting context Wait")
}
ss := scanners()
var wg sync.WaitGroup
wg.Add(len(ss))
out := make(chan Refs)
for k, s := range ss {
go func(ctx context.Context, kind string, s RefScanner, out chan Refs, wait bool) {
defer wg.Done()
s.Init(f, client.NewGVR(kind))
refs, err := s.Scan(ctx, gvr, fqn, wait)
if err != nil {
log.Error().Err(err).Msgf("scan failed for %T", s)
return
}
select {
case out <- refs:
case <-ctx.Done():
return
}
}(ctx, k, s, out, wait)
}
go func() {
wg.Wait()
close(out)
}()
res := make(Refs, 0, 10)
for refs := range out {
res = append(res, refs...)
}
return res, nil
}
func ScanForSARefs(ctx context.Context, f Factory) (Refs, error) {
defer func(t time.Time) {
log.Debug().Msgf("Cluster Scan %v", time.Since(t))
}(time.Now())
fqn, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, errors.New("expecting context Path")
}
wait, ok := ctx.Value(internal.KeyWait).(bool)
if !ok {
return nil, errors.New("expecting context Wait")
}
ss := scanners()
var wg sync.WaitGroup
wg.Add(len(ss))
out := make(chan Refs)
for k, s := range ss {
go func(ctx context.Context, kind string, s RefScanner, out chan Refs, wait bool) {
defer wg.Done()
s.Init(f, client.NewGVR(kind))
refs, err := s.ScanSA(ctx, fqn, wait)
if err != nil {
log.Error().Err(err).Msgf("scan failed for %T", s)
return
}
select {
case out <- refs:
case <-ctx.Done():
return
}
}(ctx, k, s, out, wait)
}
go func() {
wg.Wait()
close(out)
}()
res := make(Refs, 0, 10)
for refs := range out {
res = append(res, refs...)
}
return res, nil
}

View File

@ -111,7 +111,7 @@ func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
} }
func (c *Container) fetchPod(fqn string) (*v1.Pod, error) { func (c *Container) fetchPod(fqn string) (*v1.Pod, error) {
o, err := c.Factory.Get("v1/pods", fqn, false, labels.Everything()) o, err := c.Factory.Get("v1/pods", fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,11 +2,17 @@ package dao
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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/util/rand" "k8s.io/apimachinery/pkg/util/rand"
) )
@ -24,7 +30,7 @@ type CronJob struct {
// Run a CronJob. // Run a CronJob.
func (c *CronJob) Run(path string) error { func (c *CronJob) Run(path string) error {
ns, n := client.Namespaced(path) ns, _ := client.Namespaced(path)
auth, err := c.Client().CanI(ns, "batch/v1beta1/cronjobs", []string{client.GetVerb, client.CreateVerb}) auth, err := c.Client().CanI(ns, "batch/v1beta1/cronjobs", []string{client.GetVerb, client.CreateVerb})
if err != nil { if err != nil {
return err return err
@ -36,11 +42,17 @@ func (c *CronJob) Run(path string) error {
// BOZO!! Factory resource?? // BOZO!! Factory resource??
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
defer cancel() defer cancel()
cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(ctx, n, metav1.GetOptions{}) o, err := c.Factory.Get("batch/v1/cronjobs", path, true, labels.Everything())
if err != nil { if err != nil {
return err return err
} }
var cj batchv1beta1.CronJob
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)
if err != nil {
return errors.New("expecting CronJob resource")
}
var jobName = cj.Name var jobName = cj.Name
if len(cj.Name) >= maxJobNameSize { if len(cj.Name) >= maxJobNameSize {
jobName = cj.Name[0:maxJobNameSize] jobName = cj.Name[0:maxJobNameSize]
@ -58,3 +70,70 @@ func (c *CronJob) Run(path string) error {
return err return err
} }
func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := c.Factory.List(c.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var cj batchv1beta1.CronJob
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)
if err != nil {
return nil, errors.New("expecting CronJob resource")
}
if cj.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName == n {
refs = append(refs, Ref{
GVR: c.GVR(),
FQN: client.FQN(cj.Namespace, cj.Name),
})
}
}
return refs, nil
}
func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := c.Factory.List(c.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var cj batchv1beta1.CronJob
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)
if err != nil {
return nil, errors.New("expecting CronJob resource")
}
switch gvr {
case "v1/configmaps":
if !hasConfigMap(&cj.Spec.JobTemplate.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: c.GVR(),
FQN: client.FQN(cj.Namespace, cj.Name),
})
case "v1/secrets":
found, err := hasSecret(c.Factory, &cj.Spec.JobTemplate.Spec.Template.Spec, cj.Namespace, n, wait)
if err != nil {
log.Warn().Err(err).Msgf("locate secret %q", fqn)
continue
}
if !found {
continue
}
refs = append(refs, Ref{
GVR: c.GVR(),
FQN: client.FQN(cj.Namespace, cj.Name),
})
}
}
return refs, nil
}

View File

@ -35,6 +35,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error)
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping) log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
return "", err return "", err
} }
log.Debug().Msgf("Describing %q -- %q", ns, n)
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true}) return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
} }

View File

@ -6,7 +6,9 @@ import (
"fmt" "fmt"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@ -123,3 +125,171 @@ func (*Deployment) Load(f Factory, fqn string) (*appsv1.Deployment, error) {
return &dp, nil return &dp, nil
} }
func (d *Deployment) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := d.Factory.List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var dp appsv1.Deployment
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
if err != nil {
return nil, errors.New("expecting Deployment resource")
}
if dp.Spec.Template.Spec.ServiceAccountName == n {
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name),
})
}
}
return refs, nil
}
func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := d.Factory.List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var dp appsv1.Deployment
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)
if err != nil {
return nil, errors.New("expecting Deployment resource")
}
switch gvr {
case "v1/configmaps":
if !hasConfigMap(&dp.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name),
})
case "v1/secrets":
found, err := hasSecret(d.Factory, &dp.Spec.Template.Spec, dp.Namespace, n, wait)
if err != nil {
log.Warn().Err(err).Msgf("scanning secret %q", fqn)
continue
}
if !found {
continue
}
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name),
})
}
}
return refs, nil
}
func hasConfigMap(spec *v1.PodSpec, name string) bool {
for _, c := range spec.InitContainers {
if containerHasConfigMap(c, name) {
return true
}
}
for _, c := range spec.Containers {
if containerHasConfigMap(c, name) {
return true
}
}
for _, v := range spec.Volumes {
if cm := v.VolumeSource.ConfigMap; cm != nil {
if cm.LocalObjectReference.Name == name {
return true
}
}
}
return false
}
// BOZO !! Need to deal with ephemeral containers.
func hasSecret(f Factory, spec *v1.PodSpec, ns, name string, wait bool) (bool, error) {
for _, c := range spec.InitContainers {
if containerHasSecret(c, name) {
return true, nil
}
}
for _, c := range spec.Containers {
if containerHasSecret(c, name) {
return true, nil
}
}
saName := spec.ServiceAccountName
if saName != "" {
o, err := f.Get("v1/serviceaccounts", client.FQN(ns, saName), wait, labels.Everything())
if err != nil {
return false, err
}
var sa v1.ServiceAccount
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sa)
if err != nil {
return false, errors.New("expecting ServiceAccount resource")
}
for _, ref := range sa.Secrets {
if ref.Namespace == ns && ref.Name == name {
return true, nil
}
}
}
for _, v := range spec.Volumes {
if sec := v.VolumeSource.Secret; sec != nil {
if sec.SecretName == name {
return true, nil
}
}
}
return false, nil
}
func containerHasSecret(c v1.Container, name string) bool {
for _, e := range c.EnvFrom {
if e.SecretRef != nil && e.SecretRef.Name == name {
return true
}
}
for _, e := range c.Env {
if e.ValueFrom == nil || e.ValueFrom.SecretKeyRef == nil {
continue
}
if e.ValueFrom.SecretKeyRef.Name == name {
return true
}
}
return false
}
func containerHasConfigMap(c v1.Container, name string) bool {
for _, e := range c.EnvFrom {
if e.ConfigMapRef != nil && e.ConfigMapRef.Name == name {
return true
}
}
for _, e := range c.Env {
if e.ValueFrom == nil || e.ValueFrom.ConfigMapKeyRef == nil {
continue
}
if e.ValueFrom.ConfigMapKeyRef.Name == name {
return true
}
}
return false
}

View File

@ -9,6 +9,7 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/watch" "github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -143,6 +144,73 @@ func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) {
return &ds, nil return &ds, nil
} }
func (d *DaemonSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := d.Factory.List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var ds appsv1.DaemonSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
if err != nil {
return nil, errors.New("expecting DaemonSet resource")
}
if ds.Spec.Template.Spec.ServiceAccountName == n {
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name),
})
}
}
return refs, nil
}
func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := d.Factory.List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var ds appsv1.DaemonSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
if err != nil {
return nil, errors.New("expecting StatefulSet resource")
}
switch gvr {
case "v1/configmaps":
if !hasConfigMap(&ds.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name),
})
case "v1/secrets":
found, err := hasSecret(d.Factory, &ds.Spec.Template.Spec, ds.Namespace, n, wait)
if err != nil {
log.Warn().Err(err).Msgf("locate secret %q", fqn)
continue
}
if !found {
continue
}
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name),
})
}
}
return refs, nil
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...

View File

@ -57,7 +57,6 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error)
// Get returns a given resource. // Get returns a given resource.
func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) { func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) {
log.Debug().Msgf("GENERIC-GET %q", path)
var opts metav1.GetOptions var opts metav1.GetOptions
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
dial := g.dynClient() dial := g.dynClient()

View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@ -41,3 +43,70 @@ func (j *Job) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
return podLogs(ctx, c, job.Spec.Selector.MatchLabels, opts) return podLogs(ctx, c, job.Spec.Selector.MatchLabels, opts)
} }
func (j *Job) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := j.Factory.List(j.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var job batchv1.Job
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)
if err != nil {
return nil, errors.New("expecting Job resource")
}
if job.Spec.Template.Spec.ServiceAccountName == n {
refs = append(refs, Ref{
GVR: j.GVR(),
FQN: client.FQN(job.Namespace, job.Name),
})
}
}
return refs, nil
}
func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := j.Factory.List(j.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var job batchv1.Job
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)
if err != nil {
return nil, errors.New("expecting Job resource")
}
switch gvr {
case "v1/configmaps":
if !hasConfigMap(&job.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: j.GVR(),
FQN: client.FQN(job.Namespace, job.Name),
})
case "v1/secrets":
found, err := hasSecret(j.Factory, &job.Spec.Template.Spec, job.Namespace, n, wait)
if err != nil {
log.Warn().Err(err).Msgf("locate secret %q", fqn)
continue
}
if !found {
continue
}
refs = append(refs, Ref{
GVR: j.GVR(),
FQN: client.FQN(job.Namespace, job.Name),
})
}
}
return refs, nil
}

View File

@ -69,7 +69,7 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
sel, ok := ctx.Value(internal.KeyFields).(string) sel, ok := ctx.Value(internal.KeyFields).(string)
if !ok { if !ok {
return nil, fmt.Errorf("expecting a fieldSelector in context") sel = ""
} }
fsel, err := labels.ConvertSelectorToLabelsMap(sel) fsel, err := labels.ConvertSelectorToLabelsMap(sel)
if err != nil { if err != nil {
@ -155,7 +155,7 @@ func (p *Pod) Pod(fqn string) (string, error) {
// GetInstance returns a pod instance. // GetInstance returns a pod instance.
func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) { func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
o, err := p.Factory.Get(p.gvr.String(), fqn, false, labels.Everything()) o, err := p.Factory.Get(p.gvr.String(), fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -226,6 +226,84 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
return nil return nil
} }
func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := p.Factory.List(p.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var pod v1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
if err != nil {
return nil, errors.New("expecting Deployment resource")
}
// Just pick controller less pods...
if len(pod.ObjectMeta.OwnerReferences) > 0 {
continue
}
if pod.Spec.ServiceAccountName == n {
refs = append(refs, Ref{
GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
}
}
return refs, nil
}
func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := p.Factory.List(p.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var pod v1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
if err != nil {
return nil, errors.New("expecting Pod resource")
}
// Just pick controller less pods...
if len(pod.ObjectMeta.OwnerReferences) > 0 {
continue
}
switch gvr {
case "v1/configmaps":
if !hasConfigMap(&pod.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
case "v1/secrets":
found, err := hasSecret(p.Factory, &pod.Spec, pod.Namespace, n, wait)
if err != nil {
log.Warn().Err(err).Msgf("locate secret %q", fqn)
continue
}
if !found {
continue
}
refs = append(refs, Ref{
GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
}
}
return refs, nil
}
// ----------------------------------------------------------------------------
// Helpers...
func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) error { func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) error {
log.Debug().Msgf("Tailing logs for %q:%q", opts.Path, opts.Container) log.Debug().Msgf("Tailing logs for %q:%q", opts.Path, opts.Container)
req, err := logger.Logs(opts.Path, opts.ToPodLogOptions()) req, err := logger.Logs(opts.Path, opts.ToPodLogOptions())
@ -270,9 +348,6 @@ func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) {
} }
} }
// ----------------------------------------------------------------------------
// Helpers...
func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.PodMetrics { func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.PodMetrics {
if mmx == nil { if mmx == nil {
return nil return nil

84
internal/dao/reference.go Normal file
View File

@ -0,0 +1,84 @@
package dao
import (
"context"
"errors"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"k8s.io/apimachinery/pkg/runtime"
)
var (
_ Accessor = (*Reference)(nil)
)
type Reference struct {
NonResource
}
func (r *Reference) List(ctx context.Context, ns string) ([]runtime.Object, error) {
gvr, ok := ctx.Value(internal.KeyGVR).(string)
if !ok {
return nil, errors.New("No context GVR found")
}
switch gvr {
case "v1/serviceaccounts":
return r.ScanSA(ctx)
default:
return r.Scan(ctx)
}
}
func (c *Reference) Get(ctx context.Context, path string) (runtime.Object, error) {
panic("NYI")
}
func (r *Reference) Scan(ctx context.Context) ([]runtime.Object, error) {
refs, err := ScanForRefs(ctx, r.Factory)
if err != nil {
return nil, err
}
fqn, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, errors.New("expecting context Path")
}
ns, _ := client.Namespaced(fqn)
oo := make([]runtime.Object, 0, len(refs))
for _, ref := range refs {
_, n := client.Namespaced(ref.FQN)
oo = append(oo, render.ReferenceRes{
Namespace: ns,
Name: n,
GVR: ref.GVR,
})
}
return oo, nil
}
func (r *Reference) ScanSA(ctx context.Context) ([]runtime.Object, error) {
refs, err := ScanForSARefs(ctx, r.Factory)
if err != nil {
return nil, err
}
fqn, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, errors.New("expecting context Path")
}
ns, _ := client.Namespaced(fqn)
oo := make([]runtime.Object, 0, len(refs))
for _, ref := range refs {
_, n := client.Namespaced(ref.FQN)
oo = append(oo, render.ReferenceRes{
Namespace: ns,
Name: n,
GVR: ref.GVR,
})
}
return oo, nil
}

View File

@ -158,6 +158,13 @@ func loadK9s(m ResourceMetas) {
SingularName: "xray", SingularName: "xray",
Categories: []string{"k9s"}, Categories: []string{"k9s"},
} }
m[client.NewGVR("references")] = metav1.APIResource{
Name: "references",
Kind: "References",
SingularName: "reference",
Verbs: []string{},
Categories: []string{"k9s"},
}
m[client.NewGVR("aliases")] = metav1.APIResource{ m[client.NewGVR("aliases")] = metav1.APIResource{
Name: "aliases", Name: "aliases",
Kind: "Aliases", Kind: "Aliases",

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -110,7 +111,7 @@ func (s *StatefulSet) Pod(fqn string) (string, error) {
} }
func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) { func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) {
o, err := s.Factory.Get(s.gvr.String(), fqn, false, labels.Everything()) o, err := s.Factory.Get(s.gvr.String(), fqn, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -123,3 +124,70 @@ func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) {
return &sts, nil return &sts, nil
} }
func (s *StatefulSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := s.Factory.List(s.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var sts appsv1.StatefulSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts)
if err != nil {
return nil, errors.New("expecting StatefulSet resource")
}
if sts.Spec.Template.Spec.ServiceAccountName == n {
refs = append(refs, Ref{
GVR: s.GVR(),
FQN: client.FQN(sts.Namespace, sts.Name),
})
}
}
return refs, nil
}
func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
oo, err := s.Factory.List(s.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
refs := make(Refs, 0, len(oo))
for _, o := range oo {
var sts appsv1.StatefulSet
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts)
if err != nil {
return nil, errors.New("expecting StatefulSet resource")
}
switch gvr {
case "v1/configmaps":
if !hasConfigMap(&sts.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: s.GVR(),
FQN: client.FQN(sts.Namespace, sts.Name),
})
case "v1/secrets":
found, err := hasSecret(s.Factory, &sts.Spec.Template.Spec, sts.Namespace, n, wait)
if err != nil {
log.Warn().Err(err).Msgf("locate secret %q", fqn)
continue
}
if !found {
continue
}
refs = append(refs, Ref{
GVR: s.GVR(),
FQN: client.FQN(sts.Namespace, sts.Name),
})
}
}
return refs, nil
}

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -45,7 +44,7 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
labelSel, ok := ctx.Value(internal.KeyLabels).(string) labelSel, ok := ctx.Value(internal.KeyLabels).(string)
if !ok { if !ok {
log.Debug().Msgf("No label selector found in context. Listing all resources") labelSel = ""
} }
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)

View File

@ -29,4 +29,5 @@ const (
KeyToast ContextKey = "toast" KeyToast ContextKey = "toast"
KeyWithMetrics ContextKey = "withMetrics" KeyWithMetrics ContextKey = "withMetrics"
KeyViewConfig ContextKey = "viewConfig" KeyViewConfig ContextKey = "viewConfig"
KeyWait ContextKey = "wait"
) )

View File

@ -10,6 +10,10 @@ import (
// BOZO!! Break up deps and merge into single registrar // BOZO!! Break up deps and merge into single registrar
var Registry = map[string]ResourceMeta{ var Registry = map[string]ResourceMeta{
// Custom... // Custom...
"references": {
DAO: &dao.Reference{},
Renderer: &render.Reference{},
},
"helm": { "helm": {
DAO: &dao.Helm{}, DAO: &dao.Helm{},
Renderer: &render.Helm{}, Renderer: &render.Helm{},

View File

@ -202,7 +202,7 @@ func (t *Table) refresh(ctx context.Context) {
defer atomic.StoreInt32(&t.inUpdate, 0) defer atomic.StoreInt32(&t.inUpdate, 0)
if err := t.reconcile(ctx); err != nil { if err := t.reconcile(ctx); err != nil {
log.Error().Err(err).Msg("Reconcile failed") log.Error().Err(err).Msgf("reconcile failed %q::%q", t.gvr, t.instance)
t.fireTableLoadFailed(err) t.fireTableLoadFailed(err)
return return
} }
@ -237,7 +237,6 @@ func (t *Table) reconcile(ctx context.Context) error {
oo, err = []runtime.Object{o}, e oo, err = []runtime.Object{o}, e
} }
if err != nil { if err != nil {
log.Error().Err(err).Msg("Reconcile failed to list resource")
return err return err
} }

View File

@ -0,0 +1,67 @@
package render
import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Reference renders a reference to screen.
type Reference struct{}
// ColorerFunc colors a resource row.
func (Reference) ColorerFunc() ColorerFunc {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorCadetBlue
}
}
// Header returns a header row.
func (Reference) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "GVR"},
}
}
// Render renders a K8s resource to screen.
// BOZO!! Pass in a row with pre-alloc fields??
func (Reference) Render(o interface{}, ns string, r *Row) error {
ref, ok := o.(ReferenceRes)
if !ok {
return fmt.Errorf("expected ReferenceRes, but got %T", o)
}
r.ID = client.FQN(ref.Namespace, ref.Name)
r.Fields = append(r.Fields,
ref.Namespace,
ref.Name,
ref.GVR,
)
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
// ReferenceRes represents a reference resource.
type ReferenceRes struct {
Namespace string
Name string
GVR string
}
// GetObjectKind returns a schema object.
func (ReferenceRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (a ReferenceRes) DeepCopyObject() runtime.Object {
return a
}

View File

@ -0,0 +1,28 @@
package render_test
import (
"testing"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestReferenceRender(t *testing.T) {
o := render.ReferenceRes{
Namespace: "ns1",
Name: "blee",
GVR: "v1/secrets",
}
var (
ref = render.Reference{}
r render.Row
)
assert.Nil(t, ref.Render(o, "fred", &r))
assert.Equal(t, "ns1/blee", r.ID)
assert.Equal(t, render.Fields{
"ns1",
"blee",
"v1/secrets",
}, r.Fields)
}

View File

@ -32,12 +32,13 @@ func NewAlias(gvr client.GVR) ResourceViewer {
return &a return &a
} }
// Init initialiazes the view. // Init initializes the view.
func (a *Alias) Init(ctx context.Context) error { func (a *Alias) Init(ctx context.Context) error {
if err := a.ResourceViewer.Init(ctx); err != nil { if err := a.ResourceViewer.Init(ctx); err != nil {
return err return err
} }
a.GetTable().GetModel().SetNamespace("*") a.GetTable().GetModel().SetNamespace("*")
return nil return nil
} }

View File

@ -394,16 +394,18 @@ func (b *Browser) setNamespace(ns string) {
} }
func (b *Browser) defaultContext() context.Context { func (b *Browser) defaultContext() context.Context {
ctx := context.Background() ctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory)
ctx = context.WithValue(ctx, internal.KeyFactory, b.app.factory)
ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR().String()) ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR().String())
if b.Path != "" {
ctx = context.WithValue(ctx, internal.KeyPath, b.Path) ctx = context.WithValue(ctx, internal.KeyPath, b.Path)
}
ctx = context.WithValue(ctx, internal.KeyLabels, "") // BOZO!!
// ctx = context.WithValue(ctx, internal.KeyLabels, "")
if ui.IsLabelSelector(b.CmdBuff().GetText()) { if ui.IsLabelSelector(b.CmdBuff().GetText()) {
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText())) ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText()))
} }
ctx = context.WithValue(ctx, internal.KeyFields, "") // BOZO!!
// ctx = context.WithValue(ctx, internal.KeyFields, "")
ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace()))
return ctx return ctx

71
internal/view/cm.go Normal file
View File

@ -0,0 +1,71 @@
package view
import (
"context"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
)
// ConfigMap represents a configmap viewer.
type ConfigMap struct {
ResourceViewer
}
// NewConfigMap returns a new viewer.
func NewConfigMap(gvr client.GVR) ResourceViewer {
s := ConfigMap{
ResourceViewer: NewBrowser(gvr),
}
s.SetBindKeysFn(s.bindKeys)
return &s
}
func (s *ConfigMap) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyR: ui.NewKeyAction("Referenced by", s.refCmd, true),
})
}
func (s *ConfigMap) refCmd(evt *tcell.EventKey) *tcell.EventKey {
return scanRefs(evt, s.App(), s.GetTable(), "v1/configmaps")
}
func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr string) *tcell.EventKey {
path := t.GetSelectedItem()
if path == "" {
return evt
}
ctx := context.Background()
refs, err := dao.ScanForRefs(refContext(gvr, path, true)(ctx), a.factory)
if err != nil {
a.Flash().Err(err)
return nil
}
if len(refs) == 0 {
a.Flash().Warnf("No references found at this time for %s::%s. Check again later!", gvr, path)
return nil
} else {
a.Flash().Infof("Viewing references for %s::%s", gvr, path)
}
view := NewReference(client.NewGVR("references"))
view.SetContextFn(refContext(gvr, path, false))
if err := a.inject(view); err != nil {
a.Flash().Err(err)
}
return nil
}
func refContext(gvr, path string, wait bool) ContextFunc {
return func(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, internal.KeyPath, path)
ctx = context.WithValue(ctx, internal.KeyGVR, gvr)
return context.WithValue(ctx, internal.KeyWait, wait)
}
}

17
internal/view/cm_test.go Normal file
View File

@ -0,0 +1,17 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestConfigMapNew(t *testing.T) {
s := view.NewConfigMap(client.NewGVR("v1/configmaps"))
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "ConfigMaps", s.Name())
assert.Equal(t, 5, len(s.Hints()))
}

View File

@ -27,7 +27,8 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortFo
SetLabelColor(styles.K9s.Info.FgColor.Color()). SetLabelColor(styles.K9s.Info.FgColor.Color()).
SetFieldTextColor(styles.K9s.Info.SectionColor.Color()) SetFieldTextColor(styles.K9s.Info.SectionColor.Color())
p1, p2, address := ports[0], extractPort(ports[0]), "localhost" address := v.App().Config.CurrentCluster().PortForwardAddress
p1, p2 := ports[0], extractPort(ports[0])
f.AddInputField("Container Port:", p1, 30, nil, func(p string) { f.AddInputField("Container Port:", p1, 30, nil, func(p string) {
p1 = p p1 = p
}) })

View File

@ -147,7 +147,7 @@ func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, error) { func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, error) {
log.Debug().Msgf("Fetching ports on pod %q", path) log.Debug().Msgf("Fetching ports on pod %q", path)
o, err := f.Get("v1/pods", path, false, labels.Everything()) o, err := f.Get("v1/pods", path, true, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,63 @@
package view
import (
"context"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
)
// Reference represents resource references.
type Reference struct {
ResourceViewer
}
// NewReference returns a new alias view.
func NewReference(gvr client.GVR) ResourceViewer {
r := Reference{
ResourceViewer: NewBrowser(gvr),
}
r.GetTable().SetColorerFn(render.Reference{}.ColorerFunc())
r.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)
r.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone)
r.SetBindKeysFn(r.bindKeys)
return &r
}
// Init initializes the view.
func (r *Reference) Init(ctx context.Context) error {
if err := r.ResourceViewer.Init(ctx); err != nil {
return err
}
r.GetTable().GetModel().SetNamespace(client.AllNamespaces)
return nil
}
func (r *Reference) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlZ)
aa.Add(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Goto", r.gotoCmd, true),
ui.KeyShiftV: ui.NewKeyAction("Sort GVR", r.GetTable().SortColCmd("GVR", true), false),
})
}
func (r *Reference) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
row, _ := r.GetTable().GetSelection()
if row == 0 {
return evt
}
path := r.GetTable().GetSelectedItem()
gvr := ui.TrimCell(r.GetTable().SelectTable, row, 2)
if err := r.App().gotoResource(client.NewGVR(gvr).R(), path, false); err != nil {
r.App().Flash().Err(err)
}
return evt
}

View File

@ -0,0 +1,17 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestReferenceNew(t *testing.T) {
s := view.NewReference(client.NewGVR("references"))
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "References", s.Name())
assert.Equal(t, 3, len(s.Hints()))
}

View File

@ -45,6 +45,12 @@ func coreViewers(vv MetaViewers) {
vv[client.NewGVR("v1/secrets")] = MetaViewer{ vv[client.NewGVR("v1/secrets")] = MetaViewer{
viewerFn: NewSecret, viewerFn: NewSecret,
} }
vv[client.NewGVR("v1/configmaps")] = MetaViewer{
viewerFn: NewConfigMap,
}
vv[client.NewGVR("v1/serviceaccounts")] = MetaViewer{
viewerFn: NewServiceAccount,
}
vv[client.NewGVR("v1/persistentvolumeclaims")] = MetaViewer{ vv[client.NewGVR("v1/persistentvolumeclaims")] = MetaViewer{
viewerFn: NewPersistentVolumeClaim, viewerFn: NewPersistentVolumeClaim,
} }
@ -72,6 +78,9 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("aliases")] = MetaViewer{ vv[client.NewGVR("aliases")] = MetaViewer{
viewerFn: NewAlias, viewerFn: NewAlias,
} }
vv[client.NewGVR("references")] = MetaViewer{
viewerFn: NewReference,
}
vv[client.NewGVR("pulses")] = MetaViewer{ vv[client.NewGVR("pulses")] = MetaViewer{
viewerFn: NewPulse, viewerFn: NewPulse,
} }

62
internal/view/sa.go Normal file
View File

@ -0,0 +1,62 @@
package view
import (
"context"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
)
// ServiceAccount represents a serviceaccount viewer.
type ServiceAccount struct {
ResourceViewer
}
// NewServiceAccount returns a new viewer.
func NewServiceAccount(gvr client.GVR) ResourceViewer {
s := ServiceAccount{
ResourceViewer: NewBrowser(gvr),
}
s.SetBindKeysFn(s.bindKeys)
return &s
}
func (s *ServiceAccount) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyR: ui.NewKeyAction("Referenced by", s.refCmd, true),
})
}
func (s *ServiceAccount) refCmd(evt *tcell.EventKey) *tcell.EventKey {
return scanSARefs(evt, s.App(), s.GetTable(), "v1/serviceaccounts")
}
func scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr string) *tcell.EventKey {
path := t.GetSelectedItem()
if path == "" {
return evt
}
ctx := context.Background()
refs, err := dao.ScanForSARefs(refContext(gvr, path, true)(ctx), a.factory)
if err != nil {
a.Flash().Err(err)
return nil
}
if len(refs) == 0 {
a.Flash().Warnf("No references found at this time for %s::%s. Check again later!", gvr, path)
return nil
} else {
a.Flash().Infof("Viewing references for %s::%s", gvr, path)
}
view := NewReference(client.NewGVR("references"))
view.SetContextFn(refContext(gvr, path, false))
if err := a.inject(view); err != nil {
a.Flash().Err(err)
}
return nil
}

View File

@ -29,10 +29,15 @@ func NewSecret(gvr client.GVR) ResourceViewer {
func (s *Secret) bindKeys(aa ui.KeyActions) { func (s *Secret) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
tcell.KeyCtrlX: ui.NewKeyAction("Decode", s.decodeCmd, true), ui.KeyX: ui.NewKeyAction("Decode", s.decodeCmd, true),
ui.KeyR: ui.NewKeyAction("Referenced by", s.refCmd, true),
}) })
} }
func (s *Secret) refCmd(evt *tcell.EventKey) *tcell.EventKey {
return scanRefs(evt, s.App(), s.GetTable(), "v1/secrets")
}
func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {
path := s.GetTable().GetSelectedItem() path := s.GetTable().GetSelectedItem()
if path == "" { if path == "" {

View File

@ -13,5 +13,5 @@ func TestSecretNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx())) assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "Secrets", s.Name()) assert.Equal(t, "Secrets", s.Name())
assert.Equal(t, 5, len(s.Hints())) assert.Equal(t, 6, len(s.Hints()))
} }

View File

@ -43,7 +43,23 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"}, Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"}, Categories: []string{"k9s"},
}) })
dao.MetaAccess.RegisterMeta("v1/configmaps", metav1.APIResource{
Name: "configmaps",
SingularName: "configmap",
Namespaced: true,
Kind: "ConfigMaps",
Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"},
})
dao.MetaAccess.RegisterMeta("references", metav1.APIResource{
Name: "references",
SingularName: "reference",
Namespaced: true,
Kind: "References",
Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"},
})
dao.MetaAccess.RegisterMeta("aliases", metav1.APIResource{ dao.MetaAccess.RegisterMeta("aliases", metav1.APIResource{
Name: "aliases", Name: "aliases",
SingularName: "alias", SingularName: "alias",

View File

@ -70,19 +70,38 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run
if err != nil { if err != nil {
return nil, err return nil, err
} }
if wait { log.Debug().Msgf("LIST %q::%q -- %t::%t", gvr, ns, wait, inf.Informer().HasSynced())
f.waitForCacheSync(ns)
}
if client.IsClusterScoped(ns) {
return inf.Lister().List(labels)
}
if client.IsAllNamespace(ns) { if client.IsAllNamespace(ns) {
ns = client.AllNamespaces ns = client.AllNamespaces
} }
var oo []runtime.Object
if client.IsClusterScoped(ns) {
oo, err = inf.Lister().List(labels)
} else {
oo, err = inf.Lister().ByNamespace(ns).List(labels)
}
if !wait || (wait && inf.Informer().HasSynced()) {
return oo, err
}
f.waitForCacheSync(ns)
if client.IsClusterScoped(ns) {
return inf.Lister().List(labels)
}
return inf.Lister().ByNamespace(ns).List(labels) return inf.Lister().ByNamespace(ns).List(labels)
} }
// HasSynced checks if given informer is up to date.
func (f *Factory) HasSynced(gvr, ns string) (bool, error) {
inf, err := f.CanForResource(ns, gvr, client.MonitorAccess)
if err != nil {
return false, err
}
return inf.Informer().HasSynced(), nil
}
// Get retrieves a given resource. // Get retrieves a given resource.
func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
ns, n := namespaced(path) ns, n := namespaced(path)
@ -90,14 +109,21 @@ func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debug().Msgf("GET %q::%q -- %t::%t", gvr, path, wait, inf.Informer().HasSynced())
if wait { var o runtime.Object
f.waitForCacheSync(ns) if client.IsClusterScoped(ns) {
o, err = inf.Lister().Get(n)
} else {
o, err = inf.Lister().ByNamespace(ns).Get(n)
} }
if !wait || (wait && inf.Informer().HasSynced()) {
return o, err
}
f.waitForCacheSync(ns)
if client.IsClusterScoped(ns) { if client.IsClusterScoped(ns) {
return inf.Lister().Get(n) return inf.Lister().Get(n)
} }
return inf.Lister().ByNamespace(ns).Get(n) return inf.Lister().ByNamespace(ns).Get(n)
} }

View File

@ -89,7 +89,7 @@ func addRef(f dao.Factory, parent *TreeNode, gvr, id string, optional *bool) {
} }
func validate(f dao.Factory, n *TreeNode, optional *bool) { func validate(f dao.Factory, n *TreeNode, optional *bool) {
res, err := f.Get(n.GVR, n.ID, false, labels.Everything()) res, err := f.Get(n.GVR, n.ID, true, labels.Everything())
if err != nil || res == nil { if err != nil || res == nil {
if optional == nil || !*optional { if optional == nil || !*optional {
log.Warn().Err(err).Msgf("Missing ref %q::%q", n.GVR, n.ID) log.Warn().Err(err).Msgf("Missing ref %q::%q", n.GVR, n.ID)

View File

@ -103,7 +103,7 @@ func (*Pod) serviceAccountRef(ctx context.Context, f dao.Factory, parent *TreeNo
} }
id := client.FQN(ns, spec.ServiceAccountName) id := client.FQN(ns, spec.ServiceAccountName)
o, err := f.Get("v1/serviceaccounts", id, false, labels.Everything()) o, err := f.Get("v1/serviceaccounts", id, true, labels.Everything())
if err != nil { if err != nil {
return err return err
} }