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
funlen:
lines: 75
lines: 100
statements: 40
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
require (
9fans.net/go v0.0.2
github.com/atotto/clipboard v0.1.2
github.com/derailed/popeye v0.8.3
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=
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=

View File

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

View File

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

View File

@ -87,13 +87,15 @@ func (a *Alias) load() error {
if _, ok := a.Alias[meta.Kind]; ok || IsK9sMeta(meta) {
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 != "" {
a.Define(gvr.String(), meta.SingularName)
a.Define(gvrs, meta.SingularName)
}
if meta.ShortNames != nil {
a.Define(gvr.String(), meta.ShortNames...)
a.Define(gvrs, meta.ShortNames...)
}
a.Define(gvrs, gvrs)
}
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) {
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 {
return nil, err
}

View File

@ -2,11 +2,17 @@ package dao
import (
"context"
"errors"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
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"
)
@ -24,7 +30,7 @@ type CronJob struct {
// Run a CronJob.
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})
if err != nil {
return err
@ -36,11 +42,17 @@ func (c *CronJob) Run(path string) error {
// BOZO!! Factory resource??
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
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 {
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
if len(cj.Name) >= maxJobNameSize {
jobName = cj.Name[0:maxJobNameSize]
@ -58,3 +70,70 @@ func (c *CronJob) Run(path string) error {
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)
return "", err
}
log.Debug().Msgf("Describing %q -- %q", ns, n)
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
}

View File

@ -6,7 +6,9 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
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"
@ -123,3 +125,171 @@ func (*Deployment) Load(f Factory, fqn string) (*appsv1.Deployment, error) {
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/client"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/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
}
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...

View File

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

View File

@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"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)
}
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) {
sel, ok := ctx.Value(internal.KeyFields).(string)
if !ok {
return nil, fmt.Errorf("expecting a fieldSelector in context")
sel = ""
}
fsel, err := labels.ConvertSelectorToLabelsMap(sel)
if err != nil {
@ -155,7 +155,7 @@ func (p *Pod) Pod(fqn string) (string, error) {
// GetInstance returns a pod instance.
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 {
return nil, err
}
@ -226,6 +226,84 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
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 {
log.Debug().Msgf("Tailing logs for %q:%q", opts.Path, opts.Container)
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 {
if mmx == 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",
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{
Name: "aliases",
Kind: "Aliases",

View File

@ -6,6 +6,7 @@ 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"
@ -110,7 +111,7 @@ func (s *StatefulSet) Pod(fqn string) (string, 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 {
return nil, err
}
@ -123,3 +124,70 @@ func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) {
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/client"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"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) {
labelSel, ok := ctx.Value(internal.KeyLabels).(string)
if !ok {
log.Debug().Msgf("No label selector found in context. Listing all resources")
labelSel = ""
}
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)

View File

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

View File

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

View File

@ -202,7 +202,7 @@ func (t *Table) refresh(ctx context.Context) {
defer atomic.StoreInt32(&t.inUpdate, 0)
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)
return
}
@ -237,7 +237,6 @@ func (t *Table) reconcile(ctx context.Context) error {
oo, err = []runtime.Object{o}, e
}
if err != nil {
log.Error().Err(err).Msg("Reconcile failed to list resource")
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
}
// Init initialiazes the view.
// Init initializes the view.
func (a *Alias) Init(ctx context.Context) error {
if err := a.ResourceViewer.Init(ctx); err != nil {
return err
}
a.GetTable().GetModel().SetNamespace("*")
return nil
}

View File

@ -394,16 +394,18 @@ func (b *Browser) setNamespace(ns string) {
}
func (b *Browser) defaultContext() context.Context {
ctx := context.Background()
ctx = context.WithValue(ctx, internal.KeyFactory, b.app.factory)
ctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory)
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.KeyLabels, "")
}
// BOZO!!
// ctx = context.WithValue(ctx, internal.KeyLabels, "")
if ui.IsLabelSelector(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()))
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()).
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) {
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) {
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 {
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{
viewerFn: NewSecret,
}
vv[client.NewGVR("v1/configmaps")] = MetaViewer{
viewerFn: NewConfigMap,
}
vv[client.NewGVR("v1/serviceaccounts")] = MetaViewer{
viewerFn: NewServiceAccount,
}
vv[client.NewGVR("v1/persistentvolumeclaims")] = MetaViewer{
viewerFn: NewPersistentVolumeClaim,
}
@ -72,6 +78,9 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("aliases")] = MetaViewer{
viewerFn: NewAlias,
}
vv[client.NewGVR("references")] = MetaViewer{
viewerFn: NewReference,
}
vv[client.NewGVR("pulses")] = MetaViewer{
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) {
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 {
path := s.GetTable().GetSelectedItem()
if path == "" {

View File

@ -13,5 +13,5 @@ func TestSecretNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
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"},
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{
Name: "aliases",
SingularName: "alias",

View File

@ -70,19 +70,38 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run
if err != nil {
return nil, err
}
if wait {
f.waitForCacheSync(ns)
}
if client.IsClusterScoped(ns) {
return inf.Lister().List(labels)
}
log.Debug().Msgf("LIST %q::%q -- %t::%t", gvr, ns, wait, inf.Informer().HasSynced())
if client.IsAllNamespace(ns) {
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)
}
// 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.
func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
ns, n := namespaced(path)
@ -90,14 +109,21 @@ func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime
if err != nil {
return nil, err
}
if wait {
f.waitForCacheSync(ns)
log.Debug().Msgf("GET %q::%q -- %t::%t", gvr, path, wait, inf.Informer().HasSynced())
var o runtime.Object
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) {
return inf.Lister().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) {
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 optional == nil || !*optional {
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)
o, err := f.Get("v1/serviceaccounts", id, false, labels.Everything())
o, err := f.Get("v1/serviceaccounts", id, true, labels.Everything())
if err != nil {
return err
}