diff --git a/change_logs/release_0.1.3.md b/change_logs/release_0.1.3.md
index 60e92c0e..3370a661 100644
--- a/change_logs/release_0.1.3.md
+++ b/change_logs/release_0.1.3.md
@@ -9,7 +9,7 @@ Thank you to all that contributed with flushing out issues with 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 file an issue please help me verify and close.
+If you've filed an issue please help me verify and close.
Thank you so much for your support!!
diff --git a/change_logs/release_0.1.6.md b/change_logs/release_0.1.6.md
new file mode 100644
index 00000000..2c8685a2
--- /dev/null
+++ b/change_logs/release_0.1.6.md
@@ -0,0 +1,33 @@
+# Release v0.1.6
+
+
+
+---
+## Notes
+
+Thank you to all that contributed with flushing out issues with 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.
+
+Thank you so much for your support!!
+
+
+
+---
+## Change Logs
+
+
+
++ [Feature request #43](https://github.com/derailed/k9s/issues/43) Add CronJob Manual Trigger.
+ All of this work is attributed to [dzoeteman](https://github.com/dzoeteman). Thank you!
++ Added ability to view logs on Job resource.
++ [Feature request #37](https://github.com/derailed/k9s/issues/37) Added Describe on resources as
+ in kubectl describe xxx
++ NOTE! Changed alias to `job` and `cron` vs `jo` and `cjo`
+
+---
+## Resolved Bugs
+
+- Fix issue with ServiceAccounts not displaying
diff --git a/go.mod b/go.mod
index e628fb31..fce43f80 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
github.com/evanphx/json-patch v4.1.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
+ github.com/fatih/camelcase v1.0.0 // indirect
github.com/gdamore/tcell v1.1.0
github.com/go-openapi/spec v0.18.0 // indirect
github.com/gogo/protobuf v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index 289256b3..b6c4a235 100644
--- a/go.sum
+++ b/go.sum
@@ -32,6 +32,8 @@ github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7Vpz
github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
+github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
+github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
diff --git a/internal/config/config.go b/internal/config/config.go
index 19e5631d..9a2a815e 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -90,7 +90,6 @@ func (c *Config) ActiveView() string {
// SetActiveView set the currently cluster active view
func (c *Config) SetActiveView(view string) {
- c.Dump("ActiveView")
c.K9s.ActiveCluster().View.Active = view
}
diff --git a/internal/k8s/config.go b/internal/k8s/config.go
index 860b6905..cff3fde8 100644
--- a/internal/k8s/config.go
+++ b/internal/k8s/config.go
@@ -12,6 +12,9 @@ import (
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
+// KubeConfig represents kubeconfig settings.
+var KubeConfig *Config
+
// Config tracks a kubernetes configuration.
type Config struct {
flags *genericclioptions.ConfigFlags
@@ -23,7 +26,13 @@ type Config struct {
// NewConfig returns a new k8s config or an error if the flags are invalid.
func NewConfig(f *genericclioptions.ConfigFlags) *Config {
- return &Config{flags: f}
+ KubeConfig = &Config{flags: f}
+ return KubeConfig
+}
+
+// Flags returns configuration flags.
+func (c *Config) Flags() *genericclioptions.ConfigFlags {
+ return c.flags
}
// SwitchContext changes the kubeconfig context to a new cluster.
diff --git a/internal/k8s/cronjob.go b/internal/k8s/cronjob.go
index 163ca2fa..67b52091 100644
--- a/internal/k8s/cronjob.go
+++ b/internal/k8s/cronjob.go
@@ -1,9 +1,14 @@
package k8s
import (
+ batchv1 "k8s.io/api/batch/v1"
+ batchv1beta1 "k8s.io/api/batch/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/rand"
)
+const maxJobNameSize = 42
+
// CronJob represents a Kubernetes CronJob
type CronJob struct{}
@@ -40,3 +45,29 @@ func (c *CronJob) Delete(ns, n string) error {
opts := metav1.DeleteOptions{}
return conn.dialOrDie().BatchV1beta1().CronJobs(ns).Delete(n, &opts)
}
+
+// Run the job associated with this cronjob.
+func (c *CronJob) Run(ns, n string) error {
+ i, err := c.Get(ns, n)
+ if err != nil {
+ return err
+ }
+
+ cronJob := i.(*batchv1beta1.CronJob)
+
+ var jobName = cronJob.Name
+ if len(cronJob.Name) >= maxJobNameSize {
+ jobName = cronJob.Name[0:41]
+ }
+
+ job := &batchv1.Job{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: jobName + "-manual-" + rand.String(3),
+ Namespace: ns,
+ Labels: cronJob.Spec.JobTemplate.Labels,
+ },
+ Spec: cronJob.Spec.JobTemplate.Spec,
+ }
+ _, err = conn.dialOrDie().BatchV1().Jobs(ns).Create(job)
+ return err
+}
diff --git a/internal/k8s/job.go b/internal/k8s/job.go
index ba562c41..8b485f27 100644
--- a/internal/k8s/job.go
+++ b/internal/k8s/job.go
@@ -1,7 +1,13 @@
package k8s
import (
+ "fmt"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+ v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ restclient "k8s.io/client-go/rest"
)
// Job represents a Kubernetes Job
@@ -13,13 +19,13 @@ func NewJob() Res {
}
// Get a Job.
-func (c *Job) Get(ns, n string) (interface{}, error) {
+func (*Job) Get(ns, n string) (interface{}, error) {
opts := metav1.GetOptions{}
return conn.dialOrDie().BatchV1().Jobs(ns).Get(n, opts)
}
// List all Jobs in a given namespace
-func (c *Job) List(ns string) (Collection, error) {
+func (*Job) List(ns string) (Collection, error) {
opts := metav1.ListOptions{}
rr, err := conn.dialOrDie().BatchV1().Jobs(ns).List(opts)
@@ -36,7 +42,50 @@ func (c *Job) List(ns string) (Collection, error) {
}
// Delete a Job
-func (c *Job) Delete(ns, n string) error {
+func (*Job) Delete(ns, n string) error {
opts := metav1.DeleteOptions{}
return conn.dialOrDie().BatchV1().Jobs(ns).Delete(n, &opts)
}
+
+// Containers returns all container names on pod
+func (j *Job) Containers(ns, n string) ([]string, error) {
+ pod, err := j.assocPod(ns, n)
+ if err != nil {
+ return nil, err
+ }
+ log.Debug("Containers found assoc pod", pod)
+ return NewPod().(Loggable).Containers(ns, pod)
+}
+
+// Logs fetch container logs for a given pod and container.
+func (j *Job) Logs(ns, n, co string, lines int64, prev bool) *restclient.Request {
+ pod, err := j.assocPod(ns, n)
+ if err != nil {
+ return nil
+ }
+ log.Println("Logs found assoc pod", pod)
+ return NewPod().(Loggable).Logs(ns, pod, co, lines, prev)
+}
+
+// Events retrieved jobs events.
+func (*Job) Events(ns, n string) (*v1.EventList, error) {
+ e := conn.dialOrDie().Core().Events(ns)
+ sel := e.GetFieldSelector(&n, &ns, nil, nil)
+ opts := metav1.ListOptions{FieldSelector: sel.String()}
+ ee, err := e.List(opts)
+ return ee, err
+}
+
+func (j *Job) assocPod(ns, n string) (string, error) {
+ ee, err := j.Events(ns, n)
+ if err != nil {
+ return "", err
+ }
+
+ for _, e := range ee.Items {
+ if strings.Contains(e.Message, "Created pod: ") {
+ return strings.TrimSpace(strings.Replace(e.Message, "Created pod: ", "", 1)), nil
+ }
+ }
+ return "", fmt.Errorf("unable to find associated pod name for job: %s/%s", ns, n)
+}
diff --git a/internal/k8s/mapper.go b/internal/k8s/mapper.go
new file mode 100644
index 00000000..ed02de42
--- /dev/null
+++ b/internal/k8s/mapper.go
@@ -0,0 +1,182 @@
+package k8s
+
+import (
+ "fmt"
+
+ "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// RestMapping holds k8s resource mapping
+// BOZO!! Has to be a better way...
+var RestMapping = &RestMapper{}
+
+// RestMapper map resource to REST mapping ie kind, group, version.
+type RestMapper struct{}
+
+// Find a mapping given a resource name.
+func (*RestMapper) Find(res string) (*meta.RESTMapping, error) {
+ if m, ok := resMap[res]; ok {
+ return m, nil
+ }
+ return nil, fmt.Errorf("no mapping for resource %s", res)
+}
+
+// Name protocol returns rest scope name.
+func (*RestMapper) Name() meta.RESTScopeName {
+ return meta.RESTScopeNameNamespace
+}
+
+var resMap = map[string]*meta.RESTMapping{
+ "ConfigMaps": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmap"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
+ Scope: RestMapping,
+ },
+ "Pods": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pod"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
+ Scope: RestMapping,
+ },
+ "Services": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "service"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"},
+ Scope: RestMapping,
+ },
+ "EndPoints": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "endpoints"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"},
+ Scope: RestMapping,
+ },
+ "Namespaces": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespace"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"},
+ Scope: RestMapping,
+ },
+ "Nodes": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "node"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"},
+ Scope: RestMapping,
+ },
+ "PersistentVolumes": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolume"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolume"},
+ Scope: RestMapping,
+ },
+ "PersistentVolumeClaims": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaim"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolumeClaim"},
+ Scope: RestMapping,
+ },
+ "ReplicationControllers": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "replicationcontroller"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ReplicationController"},
+ Scope: RestMapping,
+ },
+ "Secrets": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secret"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"},
+ Scope: RestMapping,
+ },
+ "ServiceAccounts": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "serviceaccount"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ServiceAccount"},
+ Scope: RestMapping,
+ },
+
+ "Deployments": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
+ Scope: RestMapping,
+ },
+ "ReplicaSets": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicaset"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"},
+ Scope: RestMapping,
+ },
+ "StatefulSets": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"},
+ Scope: RestMapping,
+ },
+
+ "HorizontalPodAutoscalers": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "autoscaling", Version: "v1", Resource: "horizontalpodautoscaler"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"},
+ Scope: RestMapping,
+ },
+
+ "Jobs": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "job"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"},
+ Scope: RestMapping,
+ },
+ "CronJobs": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "cronjob"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"},
+ Scope: RestMapping,
+ },
+
+ "DaemonSets": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "daemonset"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"},
+ Scope: RestMapping,
+ },
+ "Ingress": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "ingress"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"},
+ Scope: RestMapping,
+ },
+
+ "ClusterRoles": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterrole"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"},
+ Scope: RestMapping,
+ },
+ "ClusterRoleBindings": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterrolebinding"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"},
+ Scope: RestMapping,
+ },
+ "Roles": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "role"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"},
+ Scope: RestMapping,
+ },
+ "RoleBindings": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebinding"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"},
+ Scope: RestMapping,
+ },
+
+ "CustomResourceDefinitions": &meta.RESTMapping{
+ Resource: schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"},
+ GroupVersionKind: schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"},
+ Scope: RestMapping,
+ },
+}
+
+// {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"}: {},
+// {Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequestList"}: {},
+// {Group: "kubeadm.k8s.io", Version: "v1alpha1", Kind: "MasterConfiguration"}: {},
+// {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {},
+// {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {},
+// {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}: {},
+// {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicyList"}: {},
+// {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {},
+// {Group: "policy", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {},
+// {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPreset"}: {},
+// {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPresetList"}: {},
+// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {},
+// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {},
+// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {},
+// {Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfigurationList"}: {},
+// {Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSink"}: {},
+// {Group: "auditregistration.k8s.io", Version: "v1alpha1", Kind: "AuditSinkList"}: {},
+// {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}: {},
+// {Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicyList"}: {},
+// {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClass"}: {},
+// {Group: "storage.k8s.io", Version: "v1beta1", Kind: "StorageClassList"}: {},
+// {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}: {},
+// {Group: "storage.k8s.io", Version: "v1", Kind: "StorageClassList"}: {},
+// {Group: "authentication.k8s.io", Version: "v1", Kind: "TokenRequest"}: {},
diff --git a/internal/k8s/pod.go b/internal/k8s/pod.go
index b0ad37aa..25a88555 100644
--- a/internal/k8s/pod.go
+++ b/internal/k8s/pod.go
@@ -1,7 +1,7 @@
package k8s
import (
- "k8s.io/api/core/v1"
+ v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
restclient "k8s.io/client-go/rest"
)
@@ -9,11 +9,11 @@ import (
const defaultKillGrace int64 = 5
type (
- // PodRes represents a K8s pod resource.
- PodRes interface {
+ // Loggable represents a K8s resource that has containers and can be logged.
+ Loggable interface {
Res
Containers(ns, n string) ([]string, error)
- Logs(ns, n, co string, lines int64) *restclient.Request
+ Logs(ns, n, co string, lines int64, previous bool) *restclient.Request
}
// Pod represents a Kubernetes resource.
@@ -73,12 +73,14 @@ func (*Pod) Containers(ns, n string) ([]string, error) {
}
// Logs fetch container logs for a given pod and container.
-func (*Pod) Logs(ns, n, co string, lines int64) *restclient.Request {
+func (*Pod) Logs(ns, n, co string, lines int64, prev bool) *restclient.Request {
opts := &v1.PodLogOptions{
Container: co,
Follow: true,
TailLines: &lines,
+ Previous: prev,
}
+
return conn.dialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
}
diff --git a/internal/resource/base.go b/internal/resource/base.go
index bef78920..c7c186f4 100644
--- a/internal/resource/base.go
+++ b/internal/resource/base.go
@@ -9,6 +9,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
+ "k8s.io/kubernetes/pkg/kubectl/describe"
+ versioned "k8s.io/kubernetes/pkg/kubectl/describe/versioned"
)
type (
@@ -66,6 +68,24 @@ func (b *Base) List(ns string) (Columnars, error) {
return cc, nil
}
+// Describe a given resource.
+func (b *Base) Describe(kind, pa string) (string, error) {
+
+ ns, n := namespaced(pa)
+ mapping, err := k8s.RestMapping.Find(kind)
+ if err != nil {
+ return "", err
+ }
+ d, err := versioned.Describer(k8s.KubeConfig.Flags(), mapping)
+ if err != nil {
+ return "", err
+ }
+ opts := describe.DescriberSettings{
+ ShowEvents: true,
+ }
+ return d.Describe(ns, n, opts)
+}
+
// Delete a resource by name.
func (b *Base) Delete(path string) error {
ns, n := namespaced(path)
diff --git a/internal/resource/cm.go b/internal/resource/cm.go
index fec22ce5..0d50881b 100644
--- a/internal/resource/cm.go
+++ b/internal/resource/cm.go
@@ -21,7 +21,7 @@ func NewConfigMapList(ns string) List {
// NewConfigMapListWithArgs returns a new resource list.
func NewConfigMapListWithArgs(ns string, res Resource) List {
- return newList(ns, "cm", res, AllVerbsAccess)
+ return newList(ns, "cm", res, AllVerbsAccess|DescribeAccess)
}
// NewConfigMap instantiates a new ConfigMap.
diff --git a/internal/resource/cr.go b/internal/resource/cr.go
index 304e1910..6d4ca633 100644
--- a/internal/resource/cr.go
+++ b/internal/resource/cr.go
@@ -19,7 +19,7 @@ func NewClusterRoleList(ns string) List {
// NewClusterRoleListWithArgs returns a new resource list.
func NewClusterRoleListWithArgs(ns string, res Resource) List {
- return newList(NotNamespaced, "clusterrole", res, CRUDAccess)
+ return newList(NotNamespaced, "clusterrole", res, CRUDAccess|DescribeAccess)
}
// NewClusterRole instantiates a new ClusterRole.
diff --git a/internal/resource/cr_binding.go b/internal/resource/cr_binding.go
index 937a2a11..53c69a45 100644
--- a/internal/resource/cr_binding.go
+++ b/internal/resource/cr_binding.go
@@ -19,7 +19,7 @@ func NewClusterRoleBindingList(ns string) List {
// NewClusterRoleBindingListWithArgs returns a new resource list.
func NewClusterRoleBindingListWithArgs(ns string, res Resource) List {
- return newList(NotNamespaced, "ctx", res, SwitchAccess|ViewAccess|DeleteAccess)
+ return newList(NotNamespaced, "ctx", res, SwitchAccess|ViewAccess|DeleteAccess|DescribeAccess)
}
// NewClusterRoleBinding instantiates a new ClusterRoleBinding.
diff --git a/internal/resource/crd.go b/internal/resource/crd.go
index 5632ce06..4f6fdd84 100644
--- a/internal/resource/crd.go
+++ b/internal/resource/crd.go
@@ -23,7 +23,7 @@ func NewCRDList(ns string) List {
// NewCRDListWithArgs returns a new resource list.
func NewCRDListWithArgs(ns string, res Resource) List {
- return newList(NotNamespaced, "crd", res, CRUDAccess)
+ return newList(NotNamespaced, "crd", res, CRUDAccess|DescribeAccess)
}
// NewCRD instantiates a new CRD.
diff --git a/internal/resource/cronjob.go b/internal/resource/cronjob.go
index 4e72e705..9ad73630 100644
--- a/internal/resource/cronjob.go
+++ b/internal/resource/cronjob.go
@@ -1,6 +1,7 @@
package resource
import (
+ "fmt"
"strconv"
"github.com/derailed/k9s/internal/k8s"
@@ -8,11 +9,23 @@ import (
batchv1beta1 "k8s.io/api/batch/v1beta1"
)
-// CronJob tracks a kubernetes resource.
-type CronJob struct {
- *Base
- instance *batchv1beta1.CronJob
-}
+type (
+ // CronJob tracks a kubernetes resource.
+ CronJob struct {
+ *Base
+ instance *batchv1beta1.CronJob
+ }
+
+ // Runner can run jobs.
+ Runner interface {
+ Run(path string) error
+ }
+
+ // Runnable can run jobs.
+ Runnable interface {
+ Run(ns, n string) error
+ }
+)
// NewCronJobList returns a new resource list.
func NewCronJobList(ns string) List {
@@ -21,7 +34,7 @@ func NewCronJobList(ns string) List {
// NewCronJobListWithArgs returns a new resource list.
func NewCronJobListWithArgs(ns string, res Resource) List {
- return newList(ns, "job", res, AllVerbsAccess)
+ return newList(ns, "cronjob", res, AllVerbsAccess|DescribeAccess)
}
// NewCronJob instantiates a new CronJob.
@@ -70,6 +83,15 @@ func (r *CronJob) Marshal(path string) (string, error) {
return r.marshalObject(cj)
}
+// Run a given cronjob.
+func (r *CronJob) Run(pa string) error {
+ ns, n := namespaced(pa)
+ if c, ok := r.caller.(Runnable); ok {
+ return c.Run(ns, n)
+ }
+ return fmt.Errorf("unable to run cronjob %s", pa)
+}
+
// Header return resource header.
func (*CronJob) Header(ns string) Row {
hh := Row{}
@@ -107,5 +129,3 @@ func (r *CronJob) Fields(ns string) Row {
func (*CronJob) ExtFields() Properties {
return Properties{}
}
-
-// Helpers...
diff --git a/internal/resource/cronjob_test.go b/internal/resource/cronjob_test.go
index 1823b944..d55ea25d 100644
--- a/internal/resource/cronjob_test.go
+++ b/internal/resource/cronjob_test.go
@@ -17,7 +17,7 @@ func TestCronJobListAccess(t *testing.T) {
l.SetNamespace(ns)
assert.Equal(t, "blee", l.GetNamespace())
- assert.Equal(t, "job", l.GetName())
+ assert.Equal(t, "cronjob", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a))
}
diff --git a/internal/resource/dp.go b/internal/resource/dp.go
index 7d2f4386..08cb5e44 100644
--- a/internal/resource/dp.go
+++ b/internal/resource/dp.go
@@ -21,7 +21,7 @@ func NewDeploymentList(ns string) List {
// NewDeploymentListWithArgs returns a new resource list.
func NewDeploymentListWithArgs(ns string, res Resource) List {
- return newList(ns, "deploy", res, AllVerbsAccess)
+ return newList(ns, "deploy", res, AllVerbsAccess|DescribeAccess)
}
// NewDeployment instantiates a new Deployment.
diff --git a/internal/resource/ds.go b/internal/resource/ds.go
index 56c7753c..52f7df30 100644
--- a/internal/resource/ds.go
+++ b/internal/resource/ds.go
@@ -21,7 +21,7 @@ func NewDaemonSetList(ns string) List {
// NewDaemonSetListWithArgs returns a new resource list.
func NewDaemonSetListWithArgs(ns string, res Resource) List {
- return newList(ns, "ds", res, AllVerbsAccess)
+ return newList(ns, "ds", res, AllVerbsAccess|DescribeAccess)
}
// NewDaemonSet instantiates a new DaemonSet.
diff --git a/internal/resource/ep.go b/internal/resource/ep.go
index eecff004..8d7ec704 100644
--- a/internal/resource/ep.go
+++ b/internal/resource/ep.go
@@ -23,7 +23,7 @@ func NewEndpointsList(ns string) List {
// NewEndpointsListWithArgs returns a new resource list.
func NewEndpointsListWithArgs(ns string, res Resource) List {
- return newList(ns, "ep", res, AllVerbsAccess)
+ return newList(ns, "ep", res, AllVerbsAccess|DescribeAccess)
}
// NewEndpoints instantiates a new Endpoint.
diff --git a/internal/resource/hpa.go b/internal/resource/hpa.go
index ea7aa718..037deb93 100644
--- a/internal/resource/hpa.go
+++ b/internal/resource/hpa.go
@@ -22,7 +22,7 @@ func NewHPAList(ns string) List {
// NewHPAListWithArgs returns a new resource list.
func NewHPAListWithArgs(ns string, res Resource) List {
- return newList(ns, "hpa", res, AllVerbsAccess)
+ return newList(ns, "hpa", res, AllVerbsAccess|DescribeAccess)
}
// NewHPA instantiates a new Endpoint.
diff --git a/internal/resource/ing.go b/internal/resource/ing.go
index dc848e2e..967f1ce2 100644
--- a/internal/resource/ing.go
+++ b/internal/resource/ing.go
@@ -22,7 +22,7 @@ func NewIngressList(ns string) List {
// NewIngressListWithArgs returns a new resource list.
func NewIngressListWithArgs(ns string, res Resource) List {
- return newList(ns, "ing", res, AllVerbsAccess)
+ return newList(ns, "ing", res, AllVerbsAccess|DescribeAccess)
}
// NewIngress instantiates a new Endpoint.
diff --git a/internal/resource/job.go b/internal/resource/job.go
index 0940c552..1c5c65cc 100644
--- a/internal/resource/job.go
+++ b/internal/resource/job.go
@@ -1,6 +1,8 @@
package resource
import (
+ "bufio"
+ "context"
"fmt"
"time"
@@ -23,7 +25,7 @@ func NewJobList(ns string) List {
// NewJobListWithArgs returns a new resource list.
func NewJobListWithArgs(ns string, res Resource) List {
- return newList(ns, "job", res, AllVerbsAccess)
+ return newList(ns, "job", res, AllVerbsAccess|DescribeAccess)
}
// NewJob instantiates a new Job.
@@ -72,6 +74,49 @@ func (r *Job) Marshal(path string) (string, error) {
return r.marshalObject(jo)
}
+func (r *Job) Containers(path string) ([]string, error) {
+ ns, n := namespaced(path)
+ return r.caller.(k8s.Loggable).Containers(ns, n)
+}
+
+func (r *Job) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (context.CancelFunc, error) {
+ req := r.caller.(k8s.Loggable).Logs(ns, n, co, lines, prev)
+ ctx, cancel := context.WithCancel(context.TODO())
+ req.Context(ctx)
+
+ blocked := true
+ go func() {
+ select {
+ case <-time.After(defaultTimeout):
+ if blocked {
+ close(c)
+ cancel()
+ }
+ }
+ }()
+
+ // This call will block if nothing is in the stream!!
+ stream, err := req.Stream()
+ blocked = false
+ if err != nil {
+ return cancel, fmt.Errorf("Log tail request failed for job `%s/%s:%s", ns, n, co)
+ }
+
+ go func() {
+ defer func() {
+ stream.Close()
+ cancel()
+ close(c)
+ }()
+
+ scanner := bufio.NewScanner(stream)
+ for scanner.Scan() {
+ c <- scanner.Text()
+ }
+ }()
+ return cancel, nil
+}
+
// Header return resource header.
func (*Job) Header(ns string) Row {
hh := Row{}
diff --git a/internal/resource/list.go b/internal/resource/list.go
index e3e749e9..c5f922f4 100644
--- a/internal/resource/list.go
+++ b/internal/resource/list.go
@@ -102,6 +102,7 @@ type (
Get(path string) (Columnar, error)
List(ns string) (Columnars, error)
Delete(path string) error
+ Describe(kind, pa string) (string, error)
Marshal(pa string) (string, error)
Header(ns string) Row
}
diff --git a/internal/resource/no.go b/internal/resource/no.go
index 7687cd07..e4d26ae6 100644
--- a/internal/resource/no.go
+++ b/internal/resource/no.go
@@ -30,7 +30,7 @@ func NewNodeList(ns string) List {
// NewNodeListWithArgs returns a new resource list.
func NewNodeListWithArgs(ns string, res Resource) List {
- return newList(NotNamespaced, "no", res, ViewAccess)
+ return newList(NotNamespaced, "no", res, ViewAccess|DescribeAccess)
}
// NewNode instantiates a new Endpoint.
diff --git a/internal/resource/ns.go b/internal/resource/ns.go
index 478dd87e..a02c3b03 100644
--- a/internal/resource/ns.go
+++ b/internal/resource/ns.go
@@ -19,7 +19,7 @@ func NewNamespaceList(ns string) List {
// NewNamespaceListWithArgs returns a new resource list.
func NewNamespaceListWithArgs(ns string, res Resource) List {
- return newList(NotNamespaced, "ns", res, CRUDAccess)
+ return newList(NotNamespaced, "ns", res, CRUDAccess|DescribeAccess)
}
// NewNamespace instantiates a new Endpoint.
diff --git a/internal/resource/pod.go b/internal/resource/pod.go
index 3ee6975e..820177de 100644
--- a/internal/resource/pod.go
+++ b/internal/resource/pod.go
@@ -22,7 +22,7 @@ type (
// Tailable represents a resource with tailable logs.
Tailable interface {
- Logs(c chan<- string, ns, na, co string, lines int64) (context.CancelFunc, error)
+ Logs(c chan<- string, ns, na, co string, lines int64, prev bool) (context.CancelFunc, error)
}
// TailableResource is a resource that have tailable logs.
@@ -47,7 +47,7 @@ func NewPodList(ns string) List {
// NewPodListWithArgs returns a new resource list.
func NewPodListWithArgs(ns string, res Resource) List {
- l := newList(ns, "po", res, AllVerbsAccess)
+ l := newList(ns, "po", res, AllVerbsAccess|DescribeAccess)
l.xray = true
return l
}
@@ -116,12 +116,21 @@ func (r *Pod) Marshal(path string) (string, error) {
// Containers lists out all the docker contrainers name contained in a pod.
func (r *Pod) Containers(path string) ([]string, error) {
ns, po := namespaced(path)
- return r.caller.(k8s.PodRes).Containers(ns, po)
+ return r.caller.(k8s.Loggable).Containers(ns, po)
}
// Logs tails a given container logs
-func (r *Pod) Logs(c chan<- string, ns, n, co string, lines int64) (context.CancelFunc, error) {
- req := r.caller.(k8s.PodRes).Logs(ns, n, co, lines)
+func (r *Pod) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (context.CancelFunc, error) {
+ // var ctn v1.Container
+ // for _, c := range r.instance.Spec.Containers {
+ // if c.Name == co {
+ // ctn = c
+ // }
+ // }
+
+ // if ctn.Status.ContainerStatus
+
+ req := r.caller.(k8s.Loggable).Logs(ns, n, co, lines, prev)
ctx, cancel := context.WithCancel(context.TODO())
req.Context(ctx)
diff --git a/internal/resource/pv.go b/internal/resource/pv.go
index cb79dcfd..375911c1 100644
--- a/internal/resource/pv.go
+++ b/internal/resource/pv.go
@@ -22,7 +22,7 @@ func NewPVList(ns string) List {
// NewPVListWithArgs returns a new resource list.
func NewPVListWithArgs(ns string, res Resource) List {
- return newList(NotNamespaced, "pv", res, CRUDAccess)
+ return newList(NotNamespaced, "pv", res, CRUDAccess|DescribeAccess)
}
// NewPV instantiates a new Endpoint.
diff --git a/internal/resource/pvc.go b/internal/resource/pvc.go
index a8cdb1b7..681ea3e4 100644
--- a/internal/resource/pvc.go
+++ b/internal/resource/pvc.go
@@ -19,7 +19,7 @@ func NewPVCList(ns string) List {
// NewPVCListWithArgs returns a new resource list.
func NewPVCListWithArgs(ns string, res Resource) List {
- return newList(ns, "pvc", res, AllVerbsAccess)
+ return newList(ns, "pvc", res, AllVerbsAccess|DescribeAccess)
}
// NewPVC instantiates a new Endpoint.
diff --git a/internal/resource/rc.go b/internal/resource/rc.go
index 5d53218b..091934b0 100644
--- a/internal/resource/rc.go
+++ b/internal/resource/rc.go
@@ -21,7 +21,7 @@ func NewReplicationControllerList(ns string) List {
// NewReplicationControllerListWithArgs returns a new resource list.
func NewReplicationControllerListWithArgs(ns string, res Resource) List {
- return newList(ns, "rc", res, AllVerbsAccess)
+ return newList(ns, "rc", res, AllVerbsAccess|DescribeAccess)
}
// NewReplicationController instantiates a new Endpoint.
diff --git a/internal/resource/ro_binding.go b/internal/resource/ro_binding.go
index 3e53b4a7..53d0eae2 100644
--- a/internal/resource/ro_binding.go
+++ b/internal/resource/ro_binding.go
@@ -21,7 +21,7 @@ func NewRoleBindingList(ns string) List {
// NewRoleBindingListWithArgs returns a new resource list.
func NewRoleBindingListWithArgs(ns string, res Resource) List {
- return newList(ns, "rolebinding", res, AllVerbsAccess)
+ return newList(ns, "rolebinding", res, AllVerbsAccess|DescribeAccess)
}
// NewRoleBinding instantiates a new Endpoint.
diff --git a/internal/resource/rs.go b/internal/resource/rs.go
index 901f3c63..19d6b8db 100644
--- a/internal/resource/rs.go
+++ b/internal/resource/rs.go
@@ -21,7 +21,7 @@ func NewReplicaSetList(ns string) List {
// NewReplicaSetListWithArgs returns a new resource list.
func NewReplicaSetListWithArgs(ns string, res Resource) List {
- return newList(ns, "rs", res, AllVerbsAccess)
+ return newList(ns, "rs", res, AllVerbsAccess|DescribeAccess)
}
// NewReplicaSet instantiates a new Endpoint.
diff --git a/internal/resource/sa.go b/internal/resource/sa.go
index 0c7cf6f0..440b8bdc 100644
--- a/internal/resource/sa.go
+++ b/internal/resource/sa.go
@@ -21,7 +21,7 @@ func NewServiceAccountList(ns string) List {
// NewServiceAccountListWithArgs returns a new resource list.
func NewServiceAccountListWithArgs(ns string, res Resource) List {
- return newList(NotNamespaced, "sa", res, CRUDAccess)
+ return newList(ns, "sa", res, AllVerbsAccess|DescribeAccess)
}
// NewServiceAccount instantiates a new Endpoint.
diff --git a/internal/resource/sa_test.go b/internal/resource/sa_test.go
index 100835c7..f42a2920 100644
--- a/internal/resource/sa_test.go
+++ b/internal/resource/sa_test.go
@@ -16,7 +16,7 @@ func TestSaListAccess(t *testing.T) {
l := resource.NewServiceAccountList(resource.AllNamespaces)
l.SetNamespace(ns)
- assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
+ assert.Equal(t, ns, l.GetNamespace())
assert.Equal(t, "sa", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a))
diff --git a/internal/resource/secret.go b/internal/resource/secret.go
index 9cd20380..dc1105da 100644
--- a/internal/resource/secret.go
+++ b/internal/resource/secret.go
@@ -21,7 +21,7 @@ func NewSecretList(ns string) List {
// NewSecretListWithArgs returns a new resource list.
func NewSecretListWithArgs(ns string, res Resource) List {
- return newList(ns, "secret", res, AllVerbsAccess)
+ return newList(ns, "secret", res, AllVerbsAccess|DescribeAccess)
}
// NewSecret instantiates a new Secret.
diff --git a/internal/resource/sts.go b/internal/resource/sts.go
index b9aa6e75..3235cebd 100644
--- a/internal/resource/sts.go
+++ b/internal/resource/sts.go
@@ -21,7 +21,7 @@ func NewStatefulSetList(ns string) List {
// NewStatefulSetListWithArgs returns a new resource list.
func NewStatefulSetListWithArgs(ns string, res Resource) List {
- return newList(ns, "sts", res, AllVerbsAccess)
+ return newList(ns, "sts", res, AllVerbsAccess|DescribeAccess)
}
// NewStatefulSet instantiates a new Endpoint.
diff --git a/internal/resource/svc.go b/internal/resource/svc.go
index 65378986..89084842 100644
--- a/internal/resource/svc.go
+++ b/internal/resource/svc.go
@@ -25,7 +25,7 @@ func NewServiceList(ns string) List {
// NewServiceListWithArgs returns a new resource list.
func NewServiceListWithArgs(ns string, res Resource) List {
- return newList(ns, "svc", res, AllVerbsAccess)
+ return newList(ns, "svc", res, AllVerbsAccess|DescribeAccess)
}
// NewService instantiates a new Endpoint.
diff --git a/internal/views/cmd_buff.go b/internal/views/cmd_buff.go
index 5c50d6df..27262419 100644
--- a/internal/views/cmd_buff.go
+++ b/internal/views/cmd_buff.go
@@ -1,9 +1,5 @@
package views
-import (
- log "github.com/sirupsen/logrus"
-)
-
const maxBuff = 10
type buffWatcher interface {
@@ -27,12 +23,10 @@ func newCmdBuff(key rune) *cmdBuff {
}
func (c *cmdBuff) isActive() bool {
- log.Debugf("Cmd buff `%s Active:%t", string(c.hotKey), c.active)
return c.active
}
func (c *cmdBuff) setActive(b bool) {
- log.Debugf("Cmd buff `%s SetActive:%t", string(c.hotKey), b)
c.active = b
c.fireActive(c.active)
}
diff --git a/internal/views/colorer.go b/internal/views/colorer.go
index dcde2182..262c6568 100644
--- a/internal/views/colorer.go
+++ b/internal/views/colorer.go
@@ -5,7 +5,6 @@ import (
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
- log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/watch"
)
@@ -22,7 +21,7 @@ const (
func defaultColorer(ns string, r *resource.RowEvent) tcell.Color {
c := stdColor
switch r.Action {
- case watch.Added:
+ case watch.Added, resource.New:
c = addColor
case watch.Modified:
c = modColor
@@ -37,7 +36,6 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color {
if len(ns) != 0 {
statusCol = 2
}
- log.Debug("Status", strings.TrimSpace(r.Fields[statusCol]))
switch strings.TrimSpace(r.Fields[statusCol]) {
case "ContainerCreating":
return addColor
diff --git a/internal/views/cronjob.go b/internal/views/cronjob.go
new file mode 100644
index 00000000..99aa9650
--- /dev/null
+++ b/internal/views/cronjob.go
@@ -0,0 +1,36 @@
+package views
+
+import (
+ "fmt"
+
+ "github.com/derailed/k9s/internal/resource"
+ "github.com/gdamore/tcell"
+)
+
+type cronJobView struct {
+ *resourceView
+}
+
+func newCronJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
+ v := cronJobView{
+ resourceView: newResourceView(t, app, list, c).(*resourceView),
+ }
+ v.extraActionsFn = v.extraActions
+ v.switchPage("cronjob")
+ return &v
+}
+
+func (v *cronJobView) trigger(*tcell.EventKey) {
+ if !v.rowSelected() {
+ return
+ }
+
+ v.app.flash(flashInfo, fmt.Sprintf("Triggering %s %s", v.list.GetName(), v.selectedItem))
+ if err := v.list.Resource().(resource.Runner).Run(v.selectedItem); err != nil {
+ v.app.flash(flashErr, "Boom!", err.Error())
+ }
+}
+
+func (v *cronJobView) extraActions(aa keyActions) {
+ aa[tcell.KeyCtrlT] = keyAction{description: "Trigger", action: v.trigger}
+}
diff --git a/internal/views/details.go b/internal/views/details.go
index b122a54c..82c81d82 100644
--- a/internal/views/details.go
+++ b/internal/views/details.go
@@ -7,13 +7,14 @@ import (
"github.com/k8sland/tview"
)
-const detailFmt = " [aqua::-]%s [fuchsia::b]YAML "
+const detailFmt = " [aqua::-]%s [fuchsia::b]%s "
// detailsView display yaml output
type detailsView struct {
*tview.TextView
- actions keyActions
+ actions keyActions
+ category string
}
func newDetailsView() *detailsView {
@@ -25,6 +26,10 @@ func newDetailsView() *detailsView {
return &v
}
+func (v *detailsView) setCategory(n string) {
+ v.category = n
+}
+
func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if evt.Key() == tcell.KeyRune {
if a, ok := v.actions[evt.Key()]; ok {
@@ -54,5 +59,5 @@ func (v *detailsView) hints() hints {
}
func (v *detailsView) setTitle(t string) {
- v.SetTitle(fmt.Sprintf(detailFmt, t))
+ v.SetTitle(fmt.Sprintf(detailFmt, t, v.category))
}
diff --git a/internal/views/job.go b/internal/views/job.go
new file mode 100644
index 00000000..c7bec26a
--- /dev/null
+++ b/internal/views/job.go
@@ -0,0 +1,57 @@
+package views
+
+import (
+ "github.com/derailed/k9s/internal/resource"
+ "github.com/gdamore/tcell"
+ log "github.com/sirupsen/logrus"
+)
+
+type jobView struct {
+ *resourceView
+}
+
+func newJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
+ v := jobView{newResourceView(t, app, list, c).(*resourceView)}
+ v.extraActionsFn = v.extraActions
+ v.AddPage("logs", newLogsView(&v), true, false)
+ v.switchPage("job")
+ return &v
+}
+
+// Protocol...
+
+func (v *jobView) appView() *appView {
+ return v.app
+}
+func (v *jobView) getList() resource.List {
+ return v.list
+}
+func (v *jobView) getSelection() string {
+ return v.selectedItem
+}
+
+// Handlers...
+
+func (v *jobView) logs(*tcell.EventKey) {
+ if !v.rowSelected() {
+ return
+ }
+
+ cc, err := fetchContainers(v.list, v.selectedItem)
+ if err != nil {
+ log.Error(err)
+ }
+
+ l := v.GetPrimitive("logs").(*logsView)
+ l.deleteAllPages()
+ for _, c := range cc {
+ l.addContainer(c)
+ }
+
+ v.switchPage("logs")
+ l.init()
+}
+
+func (v *jobView) extraActions(aa keyActions) {
+ aa[KeyL] = newKeyHandler("Logs", v.logs)
+}
diff --git a/internal/views/log.go b/internal/views/log.go
index bbaa09a3..41f02b4e 100644
--- a/internal/views/log.go
+++ b/internal/views/log.go
@@ -14,17 +14,17 @@ type logView struct {
*tview.TextView
}
-func newLogView(title string, pv *podView) *logView {
+func newLogView(title string, parent loggable) *logView {
v := logView{TextView: tview.NewTextView()}
{
v.SetScrollable(true)
v.SetDynamicColors(true)
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
- v.SetTitle(fmt.Sprintf(logTitleFmt, pv.selectedItem, title))
+ v.SetTitle(fmt.Sprintf(logTitleFmt, parent.getSelection(), title))
v.SetWrap(false)
v.SetChangedFunc(func() {
- pv.app.Draw()
+ parent.appView().Draw()
})
}
return &v
diff --git a/internal/views/logs.go b/internal/views/logs.go
index 82949934..615e93b5 100644
--- a/internal/views/logs.go
+++ b/internal/views/logs.go
@@ -22,18 +22,18 @@ const (
type logsView struct {
*tview.Pages
- pv *podView
+ parent loggable
containers []string
actions keyActions
cancelFunc context.CancelFunc
buffer *logBuffer
}
-func newLogsView(pv *podView) *logsView {
+func newLogsView(parent loggable) *logsView {
maxBuff := config.Root.K9s.LogBufferSize
v := logsView{
Pages: tview.NewPages(),
- pv: pv,
+ parent: parent,
containers: []string{},
buffer: newLogBuffer(int(maxBuff), true),
}
@@ -42,8 +42,8 @@ func newLogsView(pv *podView) *logsView {
KeyC: {description: "Clear", action: v.clearLogs},
KeyU: {description: "Top", action: v.top},
KeyD: {description: "Bottom", action: v.bottom},
- KeyF: {description: "Page Up", action: v.pageUp},
- KeyB: {description: "Page Down", action: v.pageDown},
+ KeyF: {description: "PageUp", action: v.pageUp},
+ KeyB: {description: "PageDown", action: v.pageDown},
})
v.SetInputCapture(v.keyboard)
@@ -98,7 +98,7 @@ func (v *logsView) hints() hints {
func (v *logsView) addContainer(n string) {
v.containers = append(v.containers, n)
- l := newLogView(n, v.pv)
+ l := newLogView(n, v.parent)
l.SetInputCapture(v.keyboard)
v.AddPage(n, l, true, false)
}
@@ -121,14 +121,14 @@ func (v *logsView) load(i int) {
}
v.SwitchToPage(v.containers[i])
v.buffer.clear()
- if err := v.doLoad(v.pv.selectedItem, v.containers[i]); err != nil {
- v.pv.app.flash(flashErr, err.Error())
+ if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
+ v.parent.appView().flash(flashErr, err.Error())
v.buffer.add("😂 Doh! No logs are available at this time. Check again later on...")
l := v.CurrentPage().Item.(*logView)
l.log(v.buffer)
return
}
- v.pv.app.SetFocus(v)
+ v.parent.appView().SetFocus(v)
}
func (v *logsView) killLogIfAny() {
@@ -149,6 +149,11 @@ func (v *logsView) doLoad(path, co string) error {
select {
case line, ok := <-c:
if !ok {
+ if v.buffer.length() > 0 {
+ v.buffer.add("--- No more logs ---")
+ l.log(v.buffer)
+ l.ScrollToEnd()
+ }
return
}
v.buffer.add(line)
@@ -173,12 +178,12 @@ func (v *logsView) doLoad(path, co string) error {
}()
ns, po := namespaced(path)
- res, ok := v.pv.list.Resource().(resource.Tailable)
+ res, ok := v.parent.getList().Resource().(resource.Tailable)
if !ok {
- return fmt.Errorf("Resource %T is not tailable", v.pv.list.Resource)
+ return fmt.Errorf("Resource %T is not tailable", v.parent.getList().Resource)
}
maxBuff := config.Root.K9s.LogBufferSize
- cancelFn, err := res.Logs(c, ns, po, co, int64(maxBuff))
+ cancelFn, err := res.Logs(c, ns, po, co, int64(maxBuff), false)
if err != nil {
cancelFn()
return err
@@ -193,40 +198,40 @@ func (v *logsView) doLoad(path, co string) error {
func (v *logsView) back(*tcell.EventKey) {
v.stop()
- v.pv.switchPage(v.pv.list.GetName())
+ v.parent.switchPage(v.parent.getList().GetName())
}
func (v *logsView) top(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil {
- v.pv.app.flash(flashInfo, "Top logs...")
+ v.parent.appView().flash(flashInfo, "Top logs...")
p.Item.(*logView).ScrollToBeginning()
}
}
func (v *logsView) bottom(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil {
- v.pv.app.flash(flashInfo, "Bottom logs...")
+ v.parent.appView().flash(flashInfo, "Bottom logs...")
p.Item.(*logView).ScrollToEnd()
}
}
func (v *logsView) pageUp(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil {
- v.pv.app.flash(flashInfo, "Page Up logs...")
+ v.parent.appView().flash(flashInfo, "Page Up logs...")
p.Item.(*logView).PageUp()
}
}
func (v *logsView) pageDown(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil {
- v.pv.app.flash(flashInfo, "Page Down logs...")
+ v.parent.appView().flash(flashInfo, "Page Down logs...")
p.Item.(*logView).PageDown()
}
}
func (v *logsView) clearLogs(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil {
- v.pv.app.flash(flashInfo, "Clearing logs...")
+ v.parent.appView().flash(flashInfo, "Clearing logs...")
v.buffer.clear()
p.Item.(*logView).Clear()
}
diff --git a/internal/views/menu.go b/internal/views/menu.go
index 0a12c43d..38e62233 100644
--- a/internal/views/menu.go
+++ b/internal/views/menu.go
@@ -12,10 +12,9 @@ import (
)
const (
- menuFmt = " [dodgerblue::b]%s[white::d]%s "
- menuSepFmt = " [dodgerblue::b]<%s> [white::d]%s "
+ menuSepFmt = " [dodgerblue::b]%-8s [white::d]%s "
menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s "
- maxRows = 5
+ maxRows = 6
colLen = 20
)
@@ -98,19 +97,17 @@ func (v *menuView) setMenu(hh hints) {
}
}
+func (*menuView) toMnemonic(s string) string {
+ return "<" + strings.ToLower(s) + ">"
+}
+
func (v *menuView) item(h hint) string {
i, err := strconv.Atoi(h.mnemonic)
if err == nil {
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14))
}
- var s string
- if len(h.mnemonic) == 1 {
- s = fmt.Sprintf(menuSepFmt, strings.ToLower(h.mnemonic), h.display)
- } else {
- s = fmt.Sprintf(menuSepFmt, strings.ToUpper(h.mnemonic), h.display)
- }
- return s
+ return fmt.Sprintf(menuSepFmt, v.toMnemonic(h.mnemonic), h.display)
}
func (a keyActions) toHints() hints {
diff --git a/internal/views/pod.go b/internal/views/pod.go
index caf37482..6e76a49d 100644
--- a/internal/views/pod.go
+++ b/internal/views/pod.go
@@ -10,6 +10,13 @@ type podView struct {
*resourceView
}
+type loggable interface {
+ appView() *appView
+ getSelection() string
+ getList() resource.List
+ switchPage(n string)
+}
+
func newPodView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
v := podView{newResourceView(t, app, list, c).(*resourceView)}
v.extraActionsFn = v.extraActions
@@ -31,6 +38,18 @@ func newPodView(t string, app *appView, list resource.List, c colorerFn) resourc
return &v
}
+// Protocol...
+
+func (v *podView) appView() *appView {
+ return v.app
+}
+func (v *podView) getList() resource.List {
+ return v.list
+}
+func (v *podView) getSelection() string {
+ return v.selectedItem
+}
+
// Handlers...
func (v *podView) logs(*tcell.EventKey) {
diff --git a/internal/views/registrar.go b/internal/views/registrar.go
index d3ff87f4..d7ffce64 100644
--- a/internal/views/registrar.go
+++ b/internal/views/registrar.go
@@ -22,37 +22,37 @@ type (
var cmdMap = map[string]resCmd{
"cm": {
- title: "Config Maps",
+ title: "ConfigMaps",
api: "core",
viewFn: newResourceView,
listFn: resource.NewConfigMapList,
colorerFn: defaultColorer,
},
"cr": {
- title: "Cluster Roles",
+ title: "ClusterRoles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleList,
colorerFn: defaultColorer,
},
"crb": {
- title: "Cluster Role Bindings",
+ title: "ClusterRoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleBindingList,
colorerFn: defaultColorer,
},
"crd": {
- title: "Custom Resource Definitions",
+ title: "CustomResourceDefinitions",
api: "apiextensions.k8s.io",
viewFn: newResourceView,
listFn: resource.NewCRDList,
colorerFn: defaultColorer,
},
- "cjo": {
+ "cron": {
title: "CronJobs",
api: "batch",
- viewFn: newResourceView,
+ viewFn: newCronJobView,
listFn: resource.NewCronJobList,
colorerFn: defaultColorer,
},
@@ -92,7 +92,7 @@ var cmdMap = map[string]resCmd{
colorerFn: evColorer,
},
"hpa": {
- title: "Horizontal Pod Autoscalers",
+ title: "HorizontalPodAutoscalers",
api: "autoscaling",
viewFn: newResourceView,
listFn: resource.NewHPAList,
@@ -105,10 +105,10 @@ var cmdMap = map[string]resCmd{
listFn: resource.NewIngressList,
colorerFn: defaultColorer,
},
- "jo": {
+ "job": {
title: "Jobs",
api: "batch",
- viewFn: newResourceView,
+ viewFn: newJobView,
listFn: resource.NewJobList,
colorerFn: defaultColorer,
},
@@ -134,28 +134,28 @@ var cmdMap = map[string]resCmd{
colorerFn: podColorer,
},
"pv": {
- title: "Persistent Volumes",
+ title: "PersistentVolumes",
api: "core",
viewFn: newResourceView,
listFn: resource.NewPVList,
colorerFn: pvColorer,
},
"pvc": {
- title: "Persistent Volume Claims",
+ title: "PersistentVolumeClaims",
api: "core",
viewFn: newResourceView,
listFn: resource.NewPVCList,
colorerFn: pvcColorer,
},
"rb": {
- title: "Role Bindings",
+ title: "RoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleBindingList,
colorerFn: defaultColorer,
},
"rc": {
- title: "ReplicationController",
+ title: "ReplicationControllers",
api: "v1",
viewFn: newResourceView,
listFn: resource.NewReplicationControllerList,
@@ -169,14 +169,14 @@ var cmdMap = map[string]resCmd{
colorerFn: defaultColorer,
},
"rs": {
- title: "Replica Sets",
+ title: "ReplicaSets",
api: "apps",
viewFn: newResourceView,
listFn: resource.NewReplicaSetList,
colorerFn: rsColorer,
},
"sa": {
- title: "Service Accounts",
+ title: "ServiceAccounts",
api: "core",
viewFn: newResourceView,
listFn: resource.NewServiceAccountList,
diff --git a/internal/views/resource.go b/internal/views/resource.go
index b060c8ca..726a1274 100644
--- a/internal/views/resource.go
+++ b/internal/views/resource.go
@@ -145,22 +145,49 @@ func (v *resourceView) delete(*tcell.EventKey) {
v.selectedItem = noSelection
}
-func (v *resourceView) describe(*tcell.EventKey) {
- details := v.GetPrimitive("xray").(details)
- details.clear()
+// func (v *resourceView) xRay(*tcell.EventKey) {
+// details := v.GetPrimitive("xray").(details)
+// details.clear()
+// if !v.rowSelected() {
+// return
+// }
+
+// props, err := v.list.Describe(v.selectedItem)
+// if err != nil {
+// v.app.flash(flashErr, "Unable to get xray fields", err.Error())
+// return
+// }
+// details.update(props)
+// details.setTitle(fmt.Sprintf(" %s ", v.selectedItem))
+// v.switchPage("xray")
+// }
+
+func (v *resourceView) describe(*tcell.EventKey) {
if !v.rowSelected() {
return
}
- props, err := v.list.Describe(v.selectedItem)
+ selected := v.selectedItem
+ selected = strings.Replace(selected, "+", "", -1)
+ selected = strings.Replace(selected, "(*)", "", -1)
+
+ raw, err := v.list.Resource().Describe(v.title, selected)
if err != nil {
- v.app.flash(flashErr, "Unable to get xray fields", err.Error())
+ v.app.flash(flashErr, "Unable to describe this resource", err.Error())
+ log.Error(err)
return
}
- details.update(props)
- details.setTitle(fmt.Sprintf(" %s ", v.selectedItem))
- v.switchPage("xray")
+
+ var re = regexp.MustCompile(`(?m:(^(.+)$))`)
+ str := re.ReplaceAllString(string(raw), `[aqua]$1`)
+
+ details := v.GetPrimitive("details").(*detailsView)
+ details.ScrollToBeginning()
+ details.setCategory("DESC")
+ details.SetText(str)
+ details.setTitle(selected)
+ v.switchPage("details")
}
func (v *resourceView) view(*tcell.EventKey) {
@@ -175,10 +202,12 @@ func (v *resourceView) view(*tcell.EventKey) {
return
}
- var re = regexp.MustCompile(`([\w|\.|"|\-|\/|\@]+):(.*)`)
+ var re = regexp.MustCompile(`(?m:([\w|\.|"|\-|\/|\@]+):(.*)$)`)
str := re.ReplaceAllString(string(raw), `[aqua]$1: [white]$2`)
details := v.GetPrimitive("details").(*detailsView)
+ details.ScrollToBeginning()
+ details.setCategory("YAML")
details.SetText(str)
details.setTitle(v.selectedItem)
v.switchPage("details")
@@ -341,7 +370,7 @@ func (v *resourceView) refreshActions() {
aa[KeyV] = newKeyHandler("View", v.view)
}
if v.list.Access(resource.DescribeAccess) {
- aa[tcell.KeyCtrlX] = newKeyHandler("Describe", v.describe)
+ aa[KeyD] = newKeyHandler("Describe", v.describe)
}
aa[KeyHelp] = newKeyHandler("Help", v.app.noop)