Added cronjob triggers. Tx dzoeteman! + describe support + job logger + bugs

mine
derailed 2019-02-19 18:12:58 -07:00
parent 787f9ff15c
commit 823169ce79
48 changed files with 658 additions and 115 deletions

View File

@ -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 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! 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!! Thank you so much for your support!!

View File

@ -0,0 +1,33 @@
# Release v0.1.6
<br/>
---
## 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!!
<br/>
---
## Change Logs
<br/>
+ [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

1
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
github.com/evanphx/json-patch v4.1.0+incompatible // indirect github.com/evanphx/json-patch v4.1.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // 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/gdamore/tcell v1.1.0
github.com/go-openapi/spec v0.18.0 // indirect github.com/go-openapi/spec v0.18.0 // indirect
github.com/gogo/protobuf v1.1.1 // indirect github.com/gogo/protobuf v1.1.1 // indirect

2
go.sum
View File

@ -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/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 h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= 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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=

View File

@ -90,7 +90,6 @@ func (c *Config) ActiveView() string {
// SetActiveView set the currently cluster active view // SetActiveView set the currently cluster active view
func (c *Config) SetActiveView(view string) { func (c *Config) SetActiveView(view string) {
c.Dump("ActiveView")
c.K9s.ActiveCluster().View.Active = view c.K9s.ActiveCluster().View.Active = view
} }

View File

@ -12,6 +12,9 @@ import (
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
) )
// KubeConfig represents kubeconfig settings.
var KubeConfig *Config
// Config tracks a kubernetes configuration. // Config tracks a kubernetes configuration.
type Config struct { type Config struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
@ -23,7 +26,13 @@ type Config struct {
// NewConfig returns a new k8s config or an error if the flags are invalid. // NewConfig returns a new k8s config or an error if the flags are invalid.
func NewConfig(f *genericclioptions.ConfigFlags) *Config { 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. // SwitchContext changes the kubeconfig context to a new cluster.

View File

@ -1,9 +1,14 @@
package k8s package k8s
import ( import (
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
) )
const maxJobNameSize = 42
// CronJob represents a Kubernetes CronJob // CronJob represents a Kubernetes CronJob
type CronJob struct{} type CronJob struct{}
@ -40,3 +45,29 @@ func (c *CronJob) Delete(ns, n string) error {
opts := metav1.DeleteOptions{} opts := metav1.DeleteOptions{}
return conn.dialOrDie().BatchV1beta1().CronJobs(ns).Delete(n, &opts) 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
}

View File

@ -1,7 +1,13 @@
package k8s package k8s
import ( import (
"fmt"
"strings"
log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
restclient "k8s.io/client-go/rest"
) )
// Job represents a Kubernetes Job // Job represents a Kubernetes Job
@ -13,13 +19,13 @@ func NewJob() Res {
} }
// Get a Job. // Get a Job.
func (c *Job) Get(ns, n string) (interface{}, error) { func (*Job) Get(ns, n string) (interface{}, error) {
opts := metav1.GetOptions{} opts := metav1.GetOptions{}
return conn.dialOrDie().BatchV1().Jobs(ns).Get(n, opts) return conn.dialOrDie().BatchV1().Jobs(ns).Get(n, opts)
} }
// List all Jobs in a given namespace // 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{} opts := metav1.ListOptions{}
rr, err := conn.dialOrDie().BatchV1().Jobs(ns).List(opts) rr, err := conn.dialOrDie().BatchV1().Jobs(ns).List(opts)
@ -36,7 +42,50 @@ func (c *Job) List(ns string) (Collection, error) {
} }
// Delete a Job // Delete a Job
func (c *Job) Delete(ns, n string) error { func (*Job) Delete(ns, n string) error {
opts := metav1.DeleteOptions{} opts := metav1.DeleteOptions{}
return conn.dialOrDie().BatchV1().Jobs(ns).Delete(n, &opts) 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)
}

182
internal/k8s/mapper.go Normal file
View File

@ -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"}: {},

View File

@ -1,7 +1,7 @@
package k8s package k8s
import ( import (
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
) )
@ -9,11 +9,11 @@ import (
const defaultKillGrace int64 = 5 const defaultKillGrace int64 = 5
type ( type (
// PodRes represents a K8s pod resource. // Loggable represents a K8s resource that has containers and can be logged.
PodRes interface { Loggable interface {
Res Res
Containers(ns, n string) ([]string, error) 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. // 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. // 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{ opts := &v1.PodLogOptions{
Container: co, Container: co,
Follow: true, Follow: true,
TailLines: &lines, TailLines: &lines,
Previous: prev,
} }
return conn.dialOrDie().CoreV1().Pods(ns).GetLogs(n, opts) return conn.dialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
} }

View File

@ -9,6 +9,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions/printers" "k8s.io/cli-runtime/pkg/genericclioptions/printers"
"k8s.io/kubernetes/pkg/kubectl/describe"
versioned "k8s.io/kubernetes/pkg/kubectl/describe/versioned"
) )
type ( type (
@ -66,6 +68,24 @@ func (b *Base) List(ns string) (Columnars, error) {
return cc, nil 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. // Delete a resource by name.
func (b *Base) Delete(path string) error { func (b *Base) Delete(path string) error {
ns, n := namespaced(path) ns, n := namespaced(path)

View File

@ -21,7 +21,7 @@ func NewConfigMapList(ns string) List {
// NewConfigMapListWithArgs returns a new resource list. // NewConfigMapListWithArgs returns a new resource list.
func NewConfigMapListWithArgs(ns string, res 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. // NewConfigMap instantiates a new ConfigMap.

View File

@ -19,7 +19,7 @@ func NewClusterRoleList(ns string) List {
// NewClusterRoleListWithArgs returns a new resource list. // NewClusterRoleListWithArgs returns a new resource list.
func NewClusterRoleListWithArgs(ns string, res 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. // NewClusterRole instantiates a new ClusterRole.

View File

@ -19,7 +19,7 @@ func NewClusterRoleBindingList(ns string) List {
// NewClusterRoleBindingListWithArgs returns a new resource list. // NewClusterRoleBindingListWithArgs returns a new resource list.
func NewClusterRoleBindingListWithArgs(ns string, res 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. // NewClusterRoleBinding instantiates a new ClusterRoleBinding.

View File

@ -23,7 +23,7 @@ func NewCRDList(ns string) List {
// NewCRDListWithArgs returns a new resource list. // NewCRDListWithArgs returns a new resource list.
func NewCRDListWithArgs(ns string, res 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. // NewCRD instantiates a new CRD.

View File

@ -1,6 +1,7 @@
package resource package resource
import ( import (
"fmt"
"strconv" "strconv"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
@ -8,12 +9,24 @@ import (
batchv1beta1 "k8s.io/api/batch/v1beta1" batchv1beta1 "k8s.io/api/batch/v1beta1"
) )
type (
// CronJob tracks a kubernetes resource. // CronJob tracks a kubernetes resource.
type CronJob struct { CronJob struct {
*Base *Base
instance *batchv1beta1.CronJob 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. // NewCronJobList returns a new resource list.
func NewCronJobList(ns string) List { func NewCronJobList(ns string) List {
return NewCronJobListWithArgs(ns, NewCronJob()) return NewCronJobListWithArgs(ns, NewCronJob())
@ -21,7 +34,7 @@ func NewCronJobList(ns string) List {
// NewCronJobListWithArgs returns a new resource list. // NewCronJobListWithArgs returns a new resource list.
func NewCronJobListWithArgs(ns string, res 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. // NewCronJob instantiates a new CronJob.
@ -70,6 +83,15 @@ func (r *CronJob) Marshal(path string) (string, error) {
return r.marshalObject(cj) 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. // Header return resource header.
func (*CronJob) Header(ns string) Row { func (*CronJob) Header(ns string) Row {
hh := Row{} hh := Row{}
@ -107,5 +129,3 @@ func (r *CronJob) Fields(ns string) Row {
func (*CronJob) ExtFields() Properties { func (*CronJob) ExtFields() Properties {
return Properties{} return Properties{}
} }
// Helpers...

View File

@ -17,7 +17,7 @@ func TestCronJobListAccess(t *testing.T) {
l.SetNamespace(ns) l.SetNamespace(ns)
assert.Equal(t, "blee", l.GetNamespace()) 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} { for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a)) assert.True(t, l.Access(a))
} }

View File

@ -21,7 +21,7 @@ func NewDeploymentList(ns string) List {
// NewDeploymentListWithArgs returns a new resource list. // NewDeploymentListWithArgs returns a new resource list.
func NewDeploymentListWithArgs(ns string, res 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. // NewDeployment instantiates a new Deployment.

View File

@ -21,7 +21,7 @@ func NewDaemonSetList(ns string) List {
// NewDaemonSetListWithArgs returns a new resource list. // NewDaemonSetListWithArgs returns a new resource list.
func NewDaemonSetListWithArgs(ns string, res 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. // NewDaemonSet instantiates a new DaemonSet.

View File

@ -23,7 +23,7 @@ func NewEndpointsList(ns string) List {
// NewEndpointsListWithArgs returns a new resource list. // NewEndpointsListWithArgs returns a new resource list.
func NewEndpointsListWithArgs(ns string, res 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. // NewEndpoints instantiates a new Endpoint.

View File

@ -22,7 +22,7 @@ func NewHPAList(ns string) List {
// NewHPAListWithArgs returns a new resource list. // NewHPAListWithArgs returns a new resource list.
func NewHPAListWithArgs(ns string, res 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. // NewHPA instantiates a new Endpoint.

View File

@ -22,7 +22,7 @@ func NewIngressList(ns string) List {
// NewIngressListWithArgs returns a new resource list. // NewIngressListWithArgs returns a new resource list.
func NewIngressListWithArgs(ns string, res 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. // NewIngress instantiates a new Endpoint.

View File

@ -1,6 +1,8 @@
package resource package resource
import ( import (
"bufio"
"context"
"fmt" "fmt"
"time" "time"
@ -23,7 +25,7 @@ func NewJobList(ns string) List {
// NewJobListWithArgs returns a new resource list. // NewJobListWithArgs returns a new resource list.
func NewJobListWithArgs(ns string, res 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. // NewJob instantiates a new Job.
@ -72,6 +74,49 @@ func (r *Job) Marshal(path string) (string, error) {
return r.marshalObject(jo) 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. // Header return resource header.
func (*Job) Header(ns string) Row { func (*Job) Header(ns string) Row {
hh := Row{} hh := Row{}

View File

@ -102,6 +102,7 @@ type (
Get(path string) (Columnar, error) Get(path string) (Columnar, error)
List(ns string) (Columnars, error) List(ns string) (Columnars, error)
Delete(path string) error Delete(path string) error
Describe(kind, pa string) (string, error)
Marshal(pa string) (string, error) Marshal(pa string) (string, error)
Header(ns string) Row Header(ns string) Row
} }

View File

@ -30,7 +30,7 @@ func NewNodeList(ns string) List {
// NewNodeListWithArgs returns a new resource list. // NewNodeListWithArgs returns a new resource list.
func NewNodeListWithArgs(ns string, res 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. // NewNode instantiates a new Endpoint.

View File

@ -19,7 +19,7 @@ func NewNamespaceList(ns string) List {
// NewNamespaceListWithArgs returns a new resource list. // NewNamespaceListWithArgs returns a new resource list.
func NewNamespaceListWithArgs(ns string, res 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. // NewNamespace instantiates a new Endpoint.

View File

@ -22,7 +22,7 @@ type (
// Tailable represents a resource with tailable logs. // Tailable represents a resource with tailable logs.
Tailable interface { 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. // TailableResource is a resource that have tailable logs.
@ -47,7 +47,7 @@ func NewPodList(ns string) List {
// NewPodListWithArgs returns a new resource list. // NewPodListWithArgs returns a new resource list.
func NewPodListWithArgs(ns string, res 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 l.xray = true
return l 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. // Containers lists out all the docker contrainers name contained in a pod.
func (r *Pod) Containers(path string) ([]string, error) { func (r *Pod) Containers(path string) ([]string, error) {
ns, po := namespaced(path) 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 // Logs tails a given container logs
func (r *Pod) Logs(c chan<- string, ns, n, co string, lines int64) (context.CancelFunc, error) { func (r *Pod) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (context.CancelFunc, error) {
req := r.caller.(k8s.PodRes).Logs(ns, n, co, lines) // 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()) ctx, cancel := context.WithCancel(context.TODO())
req.Context(ctx) req.Context(ctx)

View File

@ -22,7 +22,7 @@ func NewPVList(ns string) List {
// NewPVListWithArgs returns a new resource list. // NewPVListWithArgs returns a new resource list.
func NewPVListWithArgs(ns string, res 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. // NewPV instantiates a new Endpoint.

View File

@ -19,7 +19,7 @@ func NewPVCList(ns string) List {
// NewPVCListWithArgs returns a new resource list. // NewPVCListWithArgs returns a new resource list.
func NewPVCListWithArgs(ns string, res 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. // NewPVC instantiates a new Endpoint.

View File

@ -21,7 +21,7 @@ func NewReplicationControllerList(ns string) List {
// NewReplicationControllerListWithArgs returns a new resource list. // NewReplicationControllerListWithArgs returns a new resource list.
func NewReplicationControllerListWithArgs(ns string, res 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. // NewReplicationController instantiates a new Endpoint.

View File

@ -21,7 +21,7 @@ func NewRoleBindingList(ns string) List {
// NewRoleBindingListWithArgs returns a new resource list. // NewRoleBindingListWithArgs returns a new resource list.
func NewRoleBindingListWithArgs(ns string, res 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. // NewRoleBinding instantiates a new Endpoint.

View File

@ -21,7 +21,7 @@ func NewReplicaSetList(ns string) List {
// NewReplicaSetListWithArgs returns a new resource list. // NewReplicaSetListWithArgs returns a new resource list.
func NewReplicaSetListWithArgs(ns string, res 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. // NewReplicaSet instantiates a new Endpoint.

View File

@ -21,7 +21,7 @@ func NewServiceAccountList(ns string) List {
// NewServiceAccountListWithArgs returns a new resource list. // NewServiceAccountListWithArgs returns a new resource list.
func NewServiceAccountListWithArgs(ns string, res 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. // NewServiceAccount instantiates a new Endpoint.

View File

@ -16,7 +16,7 @@ func TestSaListAccess(t *testing.T) {
l := resource.NewServiceAccountList(resource.AllNamespaces) l := resource.NewServiceAccountList(resource.AllNamespaces)
l.SetNamespace(ns) l.SetNamespace(ns)
assert.Equal(t, resource.NotNamespaced, l.GetNamespace()) assert.Equal(t, ns, l.GetNamespace())
assert.Equal(t, "sa", l.GetName()) assert.Equal(t, "sa", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} { for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a)) assert.True(t, l.Access(a))

View File

@ -21,7 +21,7 @@ func NewSecretList(ns string) List {
// NewSecretListWithArgs returns a new resource list. // NewSecretListWithArgs returns a new resource list.
func NewSecretListWithArgs(ns string, res 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. // NewSecret instantiates a new Secret.

View File

@ -21,7 +21,7 @@ func NewStatefulSetList(ns string) List {
// NewStatefulSetListWithArgs returns a new resource list. // NewStatefulSetListWithArgs returns a new resource list.
func NewStatefulSetListWithArgs(ns string, res 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. // NewStatefulSet instantiates a new Endpoint.

View File

@ -25,7 +25,7 @@ func NewServiceList(ns string) List {
// NewServiceListWithArgs returns a new resource list. // NewServiceListWithArgs returns a new resource list.
func NewServiceListWithArgs(ns string, res 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. // NewService instantiates a new Endpoint.

View File

@ -1,9 +1,5 @@
package views package views
import (
log "github.com/sirupsen/logrus"
)
const maxBuff = 10 const maxBuff = 10
type buffWatcher interface { type buffWatcher interface {
@ -27,12 +23,10 @@ func newCmdBuff(key rune) *cmdBuff {
} }
func (c *cmdBuff) isActive() bool { func (c *cmdBuff) isActive() bool {
log.Debugf("Cmd buff `%s Active:%t", string(c.hotKey), c.active)
return c.active return c.active
} }
func (c *cmdBuff) setActive(b bool) { func (c *cmdBuff) setActive(b bool) {
log.Debugf("Cmd buff `%s SetActive:%t", string(c.hotKey), b)
c.active = b c.active = b
c.fireActive(c.active) c.fireActive(c.active)
} }

View File

@ -5,7 +5,6 @@ import (
"github.com/derailed/k9s/internal/resource" "github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
) )
@ -22,7 +21,7 @@ const (
func defaultColorer(ns string, r *resource.RowEvent) tcell.Color { func defaultColorer(ns string, r *resource.RowEvent) tcell.Color {
c := stdColor c := stdColor
switch r.Action { switch r.Action {
case watch.Added: case watch.Added, resource.New:
c = addColor c = addColor
case watch.Modified: case watch.Modified:
c = modColor c = modColor
@ -37,7 +36,6 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color {
if len(ns) != 0 { if len(ns) != 0 {
statusCol = 2 statusCol = 2
} }
log.Debug("Status", strings.TrimSpace(r.Fields[statusCol]))
switch strings.TrimSpace(r.Fields[statusCol]) { switch strings.TrimSpace(r.Fields[statusCol]) {
case "ContainerCreating": case "ContainerCreating":
return addColor return addColor

36
internal/views/cronjob.go Normal file
View File

@ -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}
}

View File

@ -7,13 +7,14 @@ import (
"github.com/k8sland/tview" "github.com/k8sland/tview"
) )
const detailFmt = " [aqua::-]%s [fuchsia::b]YAML " const detailFmt = " [aqua::-]%s [fuchsia::b]%s "
// detailsView display yaml output // detailsView display yaml output
type detailsView struct { type detailsView struct {
*tview.TextView *tview.TextView
actions keyActions actions keyActions
category string
} }
func newDetailsView() *detailsView { func newDetailsView() *detailsView {
@ -25,6 +26,10 @@ func newDetailsView() *detailsView {
return &v return &v
} }
func (v *detailsView) setCategory(n string) {
v.category = n
}
func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey { func (v *detailsView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if evt.Key() == tcell.KeyRune { if evt.Key() == tcell.KeyRune {
if a, ok := v.actions[evt.Key()]; ok { if a, ok := v.actions[evt.Key()]; ok {
@ -54,5 +59,5 @@ func (v *detailsView) hints() hints {
} }
func (v *detailsView) setTitle(t string) { func (v *detailsView) setTitle(t string) {
v.SetTitle(fmt.Sprintf(detailFmt, t)) v.SetTitle(fmt.Sprintf(detailFmt, t, v.category))
} }

57
internal/views/job.go Normal file
View File

@ -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)
}

View File

@ -14,17 +14,17 @@ type logView struct {
*tview.TextView *tview.TextView
} }
func newLogView(title string, pv *podView) *logView { func newLogView(title string, parent loggable) *logView {
v := logView{TextView: tview.NewTextView()} v := logView{TextView: tview.NewTextView()}
{ {
v.SetScrollable(true) v.SetScrollable(true)
v.SetDynamicColors(true) v.SetDynamicColors(true)
v.SetBorder(true) v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1) 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.SetWrap(false)
v.SetChangedFunc(func() { v.SetChangedFunc(func() {
pv.app.Draw() parent.appView().Draw()
}) })
} }
return &v return &v

View File

@ -22,18 +22,18 @@ const (
type logsView struct { type logsView struct {
*tview.Pages *tview.Pages
pv *podView parent loggable
containers []string containers []string
actions keyActions actions keyActions
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
buffer *logBuffer buffer *logBuffer
} }
func newLogsView(pv *podView) *logsView { func newLogsView(parent loggable) *logsView {
maxBuff := config.Root.K9s.LogBufferSize maxBuff := config.Root.K9s.LogBufferSize
v := logsView{ v := logsView{
Pages: tview.NewPages(), Pages: tview.NewPages(),
pv: pv, parent: parent,
containers: []string{}, containers: []string{},
buffer: newLogBuffer(int(maxBuff), true), buffer: newLogBuffer(int(maxBuff), true),
} }
@ -98,7 +98,7 @@ func (v *logsView) hints() hints {
func (v *logsView) addContainer(n string) { func (v *logsView) addContainer(n string) {
v.containers = append(v.containers, n) v.containers = append(v.containers, n)
l := newLogView(n, v.pv) l := newLogView(n, v.parent)
l.SetInputCapture(v.keyboard) l.SetInputCapture(v.keyboard)
v.AddPage(n, l, true, false) v.AddPage(n, l, true, false)
} }
@ -121,14 +121,14 @@ func (v *logsView) load(i int) {
} }
v.SwitchToPage(v.containers[i]) v.SwitchToPage(v.containers[i])
v.buffer.clear() v.buffer.clear()
if err := v.doLoad(v.pv.selectedItem, v.containers[i]); err != nil { if err := v.doLoad(v.parent.getSelection(), v.containers[i]); err != nil {
v.pv.app.flash(flashErr, err.Error()) v.parent.appView().flash(flashErr, err.Error())
v.buffer.add("😂 Doh! No logs are available at this time. Check again later on...") v.buffer.add("😂 Doh! No logs are available at this time. Check again later on...")
l := v.CurrentPage().Item.(*logView) l := v.CurrentPage().Item.(*logView)
l.log(v.buffer) l.log(v.buffer)
return return
} }
v.pv.app.SetFocus(v) v.parent.appView().SetFocus(v)
} }
func (v *logsView) killLogIfAny() { func (v *logsView) killLogIfAny() {
@ -149,6 +149,11 @@ func (v *logsView) doLoad(path, co string) error {
select { select {
case line, ok := <-c: case line, ok := <-c:
if !ok { if !ok {
if v.buffer.length() > 0 {
v.buffer.add("--- No more logs ---")
l.log(v.buffer)
l.ScrollToEnd()
}
return return
} }
v.buffer.add(line) v.buffer.add(line)
@ -173,12 +178,12 @@ func (v *logsView) doLoad(path, co string) error {
}() }()
ns, po := namespaced(path) ns, po := namespaced(path)
res, ok := v.pv.list.Resource().(resource.Tailable) res, ok := v.parent.getList().Resource().(resource.Tailable)
if !ok { 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 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 { if err != nil {
cancelFn() cancelFn()
return err return err
@ -193,40 +198,40 @@ func (v *logsView) doLoad(path, co string) error {
func (v *logsView) back(*tcell.EventKey) { func (v *logsView) back(*tcell.EventKey) {
v.stop() v.stop()
v.pv.switchPage(v.pv.list.GetName()) v.parent.switchPage(v.parent.getList().GetName())
} }
func (v *logsView) top(*tcell.EventKey) { func (v *logsView) top(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.pv.app.flash(flashInfo, "Top logs...") v.parent.appView().flash(flashInfo, "Top logs...")
p.Item.(*logView).ScrollToBeginning() p.Item.(*logView).ScrollToBeginning()
} }
} }
func (v *logsView) bottom(*tcell.EventKey) { func (v *logsView) bottom(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.pv.app.flash(flashInfo, "Bottom logs...") v.parent.appView().flash(flashInfo, "Bottom logs...")
p.Item.(*logView).ScrollToEnd() p.Item.(*logView).ScrollToEnd()
} }
} }
func (v *logsView) pageUp(*tcell.EventKey) { func (v *logsView) pageUp(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil { 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() p.Item.(*logView).PageUp()
} }
} }
func (v *logsView) pageDown(*tcell.EventKey) { func (v *logsView) pageDown(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil { 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() p.Item.(*logView).PageDown()
} }
} }
func (v *logsView) clearLogs(*tcell.EventKey) { func (v *logsView) clearLogs(*tcell.EventKey) {
if p := v.CurrentPage(); p != nil { if p := v.CurrentPage(); p != nil {
v.pv.app.flash(flashInfo, "Clearing logs...") v.parent.appView().flash(flashInfo, "Clearing logs...")
v.buffer.clear() v.buffer.clear()
p.Item.(*logView).Clear() p.Item.(*logView).Clear()
} }

View File

@ -12,10 +12,9 @@ import (
) )
const ( const (
menuFmt = " [dodgerblue::b]%s[white::d]%s " menuSepFmt = " [dodgerblue::b]%-8s [white::d]%s "
menuSepFmt = " [dodgerblue::b]<%s> [white::d]%s "
menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s " menuIndexFmt = " [fuchsia::b]<%d> [white::d]%s "
maxRows = 5 maxRows = 6
colLen = 20 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 { func (v *menuView) item(h hint) string {
i, err := strconv.Atoi(h.mnemonic) i, err := strconv.Atoi(h.mnemonic)
if err == nil { if err == nil {
return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14)) return fmt.Sprintf(menuIndexFmt, i, resource.Truncate(h.display, 14))
} }
var s string return fmt.Sprintf(menuSepFmt, v.toMnemonic(h.mnemonic), h.display)
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
} }
func (a keyActions) toHints() hints { func (a keyActions) toHints() hints {

View File

@ -10,6 +10,13 @@ type podView struct {
*resourceView *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 { func newPodView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
v := podView{newResourceView(t, app, list, c).(*resourceView)} v := podView{newResourceView(t, app, list, c).(*resourceView)}
v.extraActionsFn = v.extraActions v.extraActionsFn = v.extraActions
@ -31,6 +38,18 @@ func newPodView(t string, app *appView, list resource.List, c colorerFn) resourc
return &v 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... // Handlers...
func (v *podView) logs(*tcell.EventKey) { func (v *podView) logs(*tcell.EventKey) {

View File

@ -49,10 +49,10 @@ var cmdMap = map[string]resCmd{
listFn: resource.NewCRDList, listFn: resource.NewCRDList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
}, },
"cjo": { "cron": {
title: "CronJobs", title: "CronJobs",
api: "batch", api: "batch",
viewFn: newResourceView, viewFn: newCronJobView,
listFn: resource.NewCronJobList, listFn: resource.NewCronJobList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
}, },
@ -105,10 +105,10 @@ var cmdMap = map[string]resCmd{
listFn: resource.NewIngressList, listFn: resource.NewIngressList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
}, },
"jo": { "job": {
title: "Jobs", title: "Jobs",
api: "batch", api: "batch",
viewFn: newResourceView, viewFn: newJobView,
listFn: resource.NewJobList, listFn: resource.NewJobList,
colorerFn: defaultColorer, colorerFn: defaultColorer,
}, },
@ -155,7 +155,7 @@ var cmdMap = map[string]resCmd{
colorerFn: defaultColorer, colorerFn: defaultColorer,
}, },
"rc": { "rc": {
title: "ReplicationController", title: "ReplicationControllers",
api: "v1", api: "v1",
viewFn: newResourceView, viewFn: newResourceView,
listFn: resource.NewReplicationControllerList, listFn: resource.NewReplicationControllerList,

View File

@ -145,22 +145,49 @@ func (v *resourceView) delete(*tcell.EventKey) {
v.selectedItem = noSelection v.selectedItem = noSelection
} }
func (v *resourceView) describe(*tcell.EventKey) { // func (v *resourceView) xRay(*tcell.EventKey) {
details := v.GetPrimitive("xray").(details) // details := v.GetPrimitive("xray").(details)
details.clear() // 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() { if !v.rowSelected() {
return 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 { 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 return
} }
details.update(props)
details.setTitle(fmt.Sprintf(" %s ", v.selectedItem)) var re = regexp.MustCompile(`(?m:(^(.+)$))`)
v.switchPage("xray") 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) { func (v *resourceView) view(*tcell.EventKey) {
@ -175,10 +202,12 @@ func (v *resourceView) view(*tcell.EventKey) {
return return
} }
var re = regexp.MustCompile(`([\w|\.|"|\-|\/|\@]+):(.*)`) var re = regexp.MustCompile(`(?m:([\w|\.|"|\-|\/|\@]+):(.*)$)`)
str := re.ReplaceAllString(string(raw), `[aqua]$1: [white]$2`) str := re.ReplaceAllString(string(raw), `[aqua]$1: [white]$2`)
details := v.GetPrimitive("details").(*detailsView) details := v.GetPrimitive("details").(*detailsView)
details.ScrollToBeginning()
details.setCategory("YAML")
details.SetText(str) details.SetText(str)
details.setTitle(v.selectedItem) details.setTitle(v.selectedItem)
v.switchPage("details") v.switchPage("details")
@ -341,7 +370,7 @@ func (v *resourceView) refreshActions() {
aa[KeyV] = newKeyHandler("View", v.view) aa[KeyV] = newKeyHandler("View", v.view)
} }
if v.list.Access(resource.DescribeAccess) { 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) aa[KeyHelp] = newKeyHandler("Help", v.app.noop)