diff --git a/.golangci.yml b/.golangci.yml index 0b122496..12b5c191 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -79,7 +79,7 @@ linters-settings: # exclude: /path/to/file.txt funlen: - lines: 75 + lines: 100 statements: 40 govet: diff --git a/change_logs/release_v0.19.8.md b/change_logs/release_v0.19.8.md new file mode 100644 index 00000000..072a25ad --- /dev/null +++ b/change_logs/release_v0.19.8.md @@ -0,0 +1,57 @@ + + +# 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) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/go.mod b/go.mod index 37e42bfa..cde6a541 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index cce19a6e..54056a66 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/config/cluster.go b/internal/config/cluster.go index 16ed54c9..3829cb9b 100644 --- a/internal/config/cluster.go +++ b/internal/config/cluster.go @@ -1,29 +1,36 @@ 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 { - Namespace *Namespace `yaml:"namespace"` - View *View `yaml:"view"` - FeatureGates *FeatureGates `yaml:"featureGates"` - ShellPod *ShellPod `yaml:"shellPod"` + Namespace *Namespace `yaml:"namespace"` + View *View `yaml:"view"` + FeatureGates *FeatureGates `yaml:"featureGates"` + ShellPod *ShellPod `yaml:"shellPod"` + PortForwardAddress string `yaml:"portForwardAddress"` } // NewCluster creates a new cluster configuration. func NewCluster() *Cluster { return &Cluster{ - Namespace: NewNamespace(), - View: NewView(), - FeatureGates: NewFeatureGates(), - ShellPod: NewShellPod(), + Namespace: NewNamespace(), + View: NewView(), + PortForwardAddress: DefaultPFAddress, + FeatureGates: NewFeatureGates(), + ShellPod: NewShellPod(), } } // 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() } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 97631b05..353534cf 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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 diff --git a/internal/dao/alias.go b/internal/dao/alias.go index c7ea723f..045a4615 100644 --- a/internal/dao/alias.go +++ b/internal/dao/alias.go @@ -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 diff --git a/internal/dao/cluster.go b/internal/dao/cluster.go new file mode 100644 index 00000000..99d4168e --- /dev/null +++ b/internal/dao/cluster.go @@ -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 +} diff --git a/internal/dao/container.go b/internal/dao/container.go index 5fe6bb20..690422ec 100644 --- a/internal/dao/container.go +++ b/internal/dao/container.go @@ -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 } diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index 4feebb30..b11efb30 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -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 +} diff --git a/internal/dao/describe.go b/internal/dao/describe.go index 6d40d482..4e5cc0b4 100644 --- a/internal/dao/describe.go +++ b/internal/dao/describe.go @@ -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}) } diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 5ec26343..b0304085 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -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 +} diff --git a/internal/dao/ds.go b/internal/dao/ds.go index e23d8580..19c4b7d5 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -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... diff --git a/internal/dao/generic.go b/internal/dao/generic.go index dd5856f5..1a8078ae 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -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() diff --git a/internal/dao/job.go b/internal/dao/job.go index f243ed99..4958f681 100644 --- a/internal/dao/job.go +++ b/internal/dao/job.go @@ -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 +} diff --git a/internal/dao/pod.go b/internal/dao/pod.go index d28bf78e..2bd4bca6 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -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 diff --git a/internal/dao/reference.go b/internal/dao/reference.go new file mode 100644 index 00000000..3425892b --- /dev/null +++ b/internal/dao/reference.go @@ -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 +} diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 36993a70..7310effd 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -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", diff --git a/internal/dao/sts.go b/internal/dao/sts.go index 025866b0..2706b24a 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -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 +} diff --git a/internal/dao/table.go b/internal/dao/table.go index 6b49a06f..444ed79e 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -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) diff --git a/internal/keys.go b/internal/keys.go index 89c60f52..522d0f9b 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -29,4 +29,5 @@ const ( KeyToast ContextKey = "toast" KeyWithMetrics ContextKey = "withMetrics" KeyViewConfig ContextKey = "viewConfig" + KeyWait ContextKey = "wait" ) diff --git a/internal/model/registry.go b/internal/model/registry.go index 80b39f3b..40c47a12 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -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{}, diff --git a/internal/model/table.go b/internal/model/table.go index 12ea6736..c55d00b6 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -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 } diff --git a/internal/render/reference.go b/internal/render/reference.go new file mode 100644 index 00000000..f9d78dab --- /dev/null +++ b/internal/render/reference.go @@ -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 +} diff --git a/internal/render/reference_test.go b/internal/render/reference_test.go new file mode 100644 index 00000000..697ffcc1 --- /dev/null +++ b/internal/render/reference_test.go @@ -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) +} diff --git a/internal/view/alias.go b/internal/view/alias.go index 23e1c53a..0fce9e19 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -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 } diff --git a/internal/view/browser.go b/internal/view/browser.go index 97c9800d..27d37447 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -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()) - ctx = context.WithValue(ctx, internal.KeyPath, b.Path) - - ctx = context.WithValue(ctx, internal.KeyLabels, "") + if b.Path != "" { + ctx = context.WithValue(ctx, internal.KeyPath, b.Path) + } + // 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 diff --git a/internal/view/cm.go b/internal/view/cm.go new file mode 100644 index 00000000..5859d762 --- /dev/null +++ b/internal/view/cm.go @@ -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) + } +} diff --git a/internal/view/cm_test.go b/internal/view/cm_test.go new file mode 100644 index 00000000..d4371acd --- /dev/null +++ b/internal/view/cm_test.go @@ -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())) +} diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index 3774d119..5d4b3b01 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -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 }) diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index 5f9ae132..ad548a66 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -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 } diff --git a/internal/view/reference.go b/internal/view/reference.go new file mode 100644 index 00000000..6f675303 --- /dev/null +++ b/internal/view/reference.go @@ -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 +} diff --git a/internal/view/reference_test.go b/internal/view/reference_test.go new file mode 100644 index 00000000..e4d8e089 --- /dev/null +++ b/internal/view/reference_test.go @@ -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())) +} diff --git a/internal/view/registrar.go b/internal/view/registrar.go index cfeb4922..fc7b5e2f 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -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, } diff --git a/internal/view/sa.go b/internal/view/sa.go new file mode 100644 index 00000000..d059e887 --- /dev/null +++ b/internal/view/sa.go @@ -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 +} diff --git a/internal/view/secret.go b/internal/view/secret.go index b76948fd..c1604f57 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -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 == "" { diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go index bc773457..d75cd10f 100644 --- a/internal/view/secret_test.go +++ b/internal/view/secret_test.go @@ -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())) } diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index 2d98591d..0d7d3a3a 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -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", diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 6acaca42..19b44842 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -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) } diff --git a/internal/xray/container.go b/internal/xray/container.go index 98b1d5f7..67b8e013 100644 --- a/internal/xray/container.go +++ b/internal/xray/container.go @@ -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) diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 434253a0..1e3daadb 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -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 }