checkpoint

mine
derailed 2019-12-13 20:10:03 -07:00
parent 4c2c4793dc
commit e293e1af90
167 changed files with 8925 additions and 9393 deletions

View File

@ -100,7 +100,7 @@ linters-settings:
- atomicalign
enable-all: false
disable:
- shadow
# - shadow
disable-all: false
golint:
# minimal confidence for issues, default is 0.8

View File

@ -45,7 +45,7 @@ func (a Aliases) loadDefaults() {
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
a.Alias["rob"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
{
a.Alias["ctx"] = contexts

View File

@ -6,7 +6,7 @@ import (
const (
// MaxFavoritesNS number # favorite namespaces to keep in the configuration.
MaxFavoritesNS = 10
MaxFavoritesNS = 9
defaultNS = "default"
allNS = "all"
)

View File

@ -132,7 +132,7 @@ func newStyle() Style {
Body: newBody(),
Frame: newFrame(),
Info: newInfo(),
Table: newTable(),
Table: newGetTable(),
Views: newViews(),
}
}
@ -211,7 +211,7 @@ func newInfo() Info {
}
// NewTable returns a new table style.
func newTable() Table {
func newGetTable() Table {
return Table{
FgColor: "aqua",
BgColor: "black",
@ -293,7 +293,7 @@ func (s *Styles) Title() Title {
}
// Table returns table styles.
func (s *Styles) Table() Table {
func (s *Styles) GetTable() Table {
return s.K9s.Table
}

View File

@ -16,7 +16,7 @@ func TestSkinNone(t *testing.T) {
assert.Equal(t, "cadetblue", s.Body().FgColor)
assert.Equal(t, "black", s.Body().BgColor)
assert.Equal(t, "black", s.Table().BgColor)
assert.Equal(t, "black", s.GetTable().BgColor)
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
assert.Equal(t, tcell.ColorBlack, s.BgColor())
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
@ -32,7 +32,7 @@ func TestSkin(t *testing.T) {
assert.Equal(t, "white", s.Body().FgColor)
assert.Equal(t, "black", s.Body().BgColor)
assert.Equal(t, "black", s.Table().BgColor)
assert.Equal(t, "black", s.GetTable().BgColor)
assert.Equal(t, tcell.ColorWhite, s.FgColor())
assert.Equal(t, tcell.ColorBlack, s.BgColor())
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)

8
internal/dao/alias.go Normal file
View File

@ -0,0 +1,8 @@
package dao
// Alias represents an alias resource.
type Alias struct {
Generic
}
var _ Accessor = &Alias{}

21
internal/dao/benchmark.go Normal file
View File

@ -0,0 +1,21 @@
package dao
import (
"os"
"github.com/rs/zerolog/log"
)
// Benchmark represents a benchmark resource.
type Benchmark struct {
Generic
}
var _ Accessor = &Benchmark{}
var _ Nuker = &Benchmark{}
// Delete a Benchmark.
func (d *Benchmark) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("Benchmark DELETE %q", path)
return os.Remove(path)
}

51
internal/dao/container.go Normal file
View File

@ -0,0 +1,51 @@
package dao
import (
"context"
"errors"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
restclient "k8s.io/client-go/rest"
)
type Container struct {
Generic
}
var _ Accessor = &Container{}
var _ Loggable = &Container{}
// Logs tails a given container logs
func (c *Container) TailLogs(ctx context.Context, logChan chan<- string, opts LogOptions) error {
log.Debug().Msgf("CO TAILLOGS %#v", ctx)
log.Debug().Msgf("CO TAILLOGS %#v", opts)
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
if !ok {
return errors.New("Expecting an informer")
}
o, err := fac.Get("v1/pods", opts.Path, labels.Everything())
if err != nil {
return err
}
var po v1.Pod
if runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
return err
}
return tailLogs(ctx, c, logChan, opts)
}
// Logs fetch container logs for a given pod and container.
func (c *Container) Logs(path string, opts *v1.PodLogOptions) *restclient.Request {
ns, n := k8s.Namespaced(path)
return c.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
}

View File

@ -13,7 +13,7 @@ import (
)
type Context struct {
Resource
Generic
}
var _ Accessor = &Context{}
@ -47,16 +47,16 @@ func (c *Context) List(string, metav1.ListOptions) ([]runtime.Object, error) {
}
// Delete a Context.
func (c *Context) Delete(ns, n string, cascade, force bool) error {
func (c *Context) Delete(path string, cascade, force bool) error {
ctx, err := c.config().CurrentContextName()
if err != nil {
return err
}
if ctx == n {
return fmt.Errorf("trying to delete your current context %s", n)
if ctx == path {
return fmt.Errorf("trying to delete your current context %s", path)
}
return c.config().DelContext(n)
return c.config().DelContext(path)
}
// MustCurrentContextName return the active context name.

43
internal/dao/cronjob.go Normal file
View File

@ -0,0 +1,43 @@
package dao
import (
"github.com/derailed/k9s/internal/k8s"
batchv1 "k8s.io/api/batch/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
)
const maxJobNameSize = 42
type CronJob struct {
Generic
}
var _ Accessor = &CronJob{}
var _ Runnable = &CronJob{}
// Run a CronJob.
func (c *CronJob) Run(path string) error {
ns, n := k8s.Namespaced(path)
cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(n, metav1.GetOptions{})
if err != nil {
return err
}
var jobName = cj.Name
if len(cj.Name) >= maxJobNameSize {
jobName = cj.Name[0:maxJobNameSize]
}
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName + "-manual-" + rand.String(3),
Namespace: ns,
Labels: cj.Spec.JobTemplate.Labels,
},
Spec: cj.Spec.JobTemplate.Spec,
}
_, err = c.Client().DialOrDie().BatchV1().Jobs(ns).Create(job)
return err
}

View File

@ -33,5 +33,6 @@ func Describe(c k8s.Connection, gvr GVR, ns, n string) (string, error) {
return "", err
}
log.Debug().Msgf("DESCRIBE FOR %q -- %q:%q", gvr, ns, n)
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -16,7 +17,7 @@ import (
)
type Deployment struct {
Resource
Generic
}
var _ Accessor = &Deployment{}
@ -25,7 +26,8 @@ var _ Restartable = &Deployment{}
var _ Scalable = &Deployment{}
// Scale a Deployment.
func (d *Deployment) Scale(ns, n string, replicas int32) error {
func (d *Deployment) Scale(path string, replicas int32) error {
ns, n := k8s.Namespaced(path)
scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{})
if err != nil {
return err
@ -37,8 +39,8 @@ func (d *Deployment) Scale(ns, n string, replicas int32) error {
}
// Restart a Deployment rollout.
func (d *Deployment) Restart(ns, n string) error {
o, err := d.Get(ns, string(d.gvr), n, labels.Everything())
func (d *Deployment) Restart(path string) error {
o, err := d.Get(string(d.gvr), path, labels.Everything())
if err != nil {
return err
}
@ -54,14 +56,14 @@ func (d *Deployment) Restart(ns, n string) error {
return err
}
_, err = d.Client().DialOrDie().AppsV1().Deployments(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
_, err = d.Client().DialOrDie().AppsV1().Deployments(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update)
return err
}
// Logs tail logs for all pods represented by this Deployment.
func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing Deployment %q -- %q", opts.Namespace, opts.Name)
o, err := d.Get(opts.Namespace, string(d.gvr), opts.Name, labels.Everything())
log.Debug().Msgf("Tailing Deployment %q -- %q", opts.Path)
o, err := d.Get(string(d.gvr), opts.Path, labels.Everything())
if err != nil {
return err
}
@ -73,7 +75,7 @@ func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOpti
}
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on Deployment %s", opts.FQN())
return fmt.Errorf("No valid selector found on Deployment %s", opts.Path)
}
return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
@ -20,7 +21,7 @@ import (
)
type DaemonSet struct {
Resource
Generic
}
var _ Accessor = &DaemonSet{}
@ -28,8 +29,8 @@ var _ Loggable = &DaemonSet{}
var _ Restartable = &DaemonSet{}
// Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(ns, n string) error {
o, err := d.Get(ns, string(d.gvr), n, labels.Everything())
func (d *DaemonSet) Restart(path string) error {
o, err := d.Get(string(d.gvr), path, labels.Everything())
if err != nil {
return err
}
@ -45,14 +46,14 @@ func (d *DaemonSet) Restart(ns, n string) error {
return err
}
_, err = d.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
_, err = d.Client().DialOrDie().AppsV1().DaemonSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update)
return err
}
// Logs tail logs for all pods represented by this DaemonSet.
func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing DaemonSet %q -- %q", opts.Namespace, opts.Name)
o, err := d.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything())
log.Debug().Msgf("Tailing DaemonSet %q", opts.Path)
o, err := d.Get("apps/v1/daemonsets", opts.Path, labels.Everything())
if err != nil {
return err
}
@ -64,7 +65,7 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptio
}
if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on daemonset %s", opts.FQN())
return fmt.Errorf("no valid selector found on daemonset %q", opts.Path)
}
return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts)
@ -84,7 +85,8 @@ func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts L
return err
}
oo, err := f.List(opts.Namespace, "v1/pods", lsel)
ns, _ := k8s.Namespaced(opts.Path)
oo, err := f.List("v1/pods", ns, lsel)
if err != nil {
return err
}
@ -94,17 +96,17 @@ func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts L
}
po := Pod{}
po.Init(f, "v1/pods")
for _, o := range oo {
var pod v1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
if err != nil {
return err
}
if pod.Status.Phase == v1.PodRunning {
opts.Namespace, opts.Name = pod.Namespace, pod.Name
if err := po.TailLogs(ctx, c, opts); err != nil {
return err
}
log.Debug().Msgf("TAILING logs on pod %q", pod.Name)
opts.Path = k8s.FQN(pod.Namespace, pod.Name)
if err := po.TailLogs(ctx, c, opts); err != nil {
return err
}
}
return nil

34
internal/dao/generic.go Normal file
View File

@ -0,0 +1,34 @@
package dao
import (
"github.com/derailed/k9s/internal/k8s"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
)
type Generic struct {
Factory
gvr GVR
}
func (r *Generic) Init(f Factory, gvr GVR) {
r.Factory, r.gvr = f, gvr
}
// Delete a Generic.
func (g *Generic) Delete(path string, cascade, force bool) error {
p := metav1.DeletePropagationOrphan
if cascade {
p = metav1.DeletePropagationBackground
}
ns, n := k8s.Namespaced(path)
return g.dynClient().Namespace(ns).Delete(n, &metav1.DeleteOptions{
PropagationPolicy: &p,
})
}
func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface {
return g.Client().DynDialOrDie().Resource(g.gvr.AsGVR())
}

41
internal/dao/job.go Normal file
View File

@ -0,0 +1,41 @@
package dao
import (
"context"
"errors"
"fmt"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
type Job struct {
Generic
}
var _ Accessor = &Job{}
var _ Loggable = &Job{}
// Logs tail logs for all pods represented by this Job.
func (j *Job) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing Job %#v", opts)
o, err := j.Get(string(j.gvr), opts.Path, labels.Everything())
if err != nil {
return err
}
var job batchv1.Job
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)
if err != nil {
return errors.New("expecting a job resource")
}
if job.Spec.Selector == nil || len(job.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on Job %s", opts.Path)
}
return podLogs(ctx, c, job.Spec.Selector.MatchLabels, opts)
}

View File

@ -1,57 +1,42 @@
package dao
import (
"path"
"strings"
"github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
)
type (
// Fqn uniquely describes a container
Fqn struct {
Namespace, Name, Container string
}
// LogOptions represent logger options.
LogOptions struct {
Fqn
Lines int64
Color color.Paint
Previous bool
SingleContainer bool
MultiPods bool
}
)
// LogOptions represent logger options.
type LogOptions struct {
Path string
Container string
Lines int64
Color color.Paint
Previous bool
SingleContainer bool
MultiPods bool
}
// HasContainer checks if a container is present.
func (o LogOptions) HasContainer() bool {
return o.Container != ""
}
// FQN returns resource fully qualified name.
func (o LogOptions) FQN() string {
return FQN(o.Namespace, o.Name)
}
// Path returns resource descriptor path.
func (o LogOptions) Path() string {
return o.FQN() + ":" + o.Container
}
// FixedSizeName returns a normalize fixed size pod name if possible.
func (o LogOptions) FixedSizeName() string {
tokens := strings.Split(o.Name, "-")
_, n := k8s.Namespaced(o.Path)
tokens := strings.Split(n, "-")
if len(tokens) < 3 {
return o.Name
return n
}
var s []string
for i := 0; i < len(tokens)-1; i++ {
s = append(s, tokens[i])
}
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
}
@ -65,12 +50,13 @@ func colorize(c color.Paint, txt string) string {
// DecorateLog add a log header to display po/co information along with the log message.
func (o LogOptions) DecorateLog(msg string) string {
_, n := k8s.Namespaced(o.Path)
if msg == "" {
return msg
}
if o.MultiPods {
return colorize(o.Color, o.Name+":"+o.Container+" ") + msg
return colorize(o.Color, n+":"+o.Container+" ") + msg
}
if !o.SingleContainer {
@ -88,17 +74,18 @@ func Truncate(str string, width int) string {
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
}
// Namespaced return a namesapace and a name.
func Namespaced(n string) (string, string) {
ns, po := path.Split(n)
// BOZO!!
// // Namespaced return a namesapace and a name.
// func Namespaced(n string) (string, string) {
// ns, po := path.Split(n)
return strings.Trim(ns, "/"), po
}
// return strings.Trim(ns, "/"), po
// }
// FQN returns a fully qualified resource name.
func FQN(ns, n string) string {
if ns == "" {
return n
}
return ns + "/" + n
}
// // FQN returns a fully qualified resource name.
// func FQN(ns, n string) string {
// if ns == "" {
// return n
// }
// return ns + "/" + n
// }

View File

@ -1 +0,0 @@
package dao

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
@ -22,24 +23,23 @@ import (
const defaultTimeout = 1 * time.Second
type Logger interface {
Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request
}
// Pod represents a pod resource.
type Pod struct {
Resource
Generic
}
var _ Accessor = &Pod{}
var _Loggable = &Pod{}
// Logs fetch container logs for a given pod and container.
func (p *Pod) Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request {
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) *restclient.Request {
ns, n := k8s.Namespaced(path)
return p.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
}
// Containers returns all container names on pod
func (p *Pod) Containers(ns, n string, includeInit bool) ([]string, error) {
o, err := p.Get(ns, "v1/pod", n, labels.Everything())
func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
o, err := p.Get("v1/pod", path, labels.Everything())
if err != nil {
return nil, err
}
@ -78,8 +78,7 @@ func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error
if !ok {
return errors.New("Expecting an informer")
}
ns, n := Namespaced(opts.FQN())
o, err := fac.Get(ns, "v1/pods", n, labels.Everything())
o, err := fac.Get("v1/pods", opts.Path, labels.Everything())
if err != nil {
return err
}
@ -114,14 +113,14 @@ func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error
}
func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing logs for %q -- %q -- %q", opts.Namespace, opts.Name, opts.Container)
log.Debug().Msgf("Tailing logs for %q -- %q", opts.Path, opts.Container)
o := v1.PodLogOptions{
Container: opts.Container,
Follow: true,
TailLines: &opts.Lines,
Previous: opts.Previous,
}
req := logger.Logs(opts.Namespace, opts.Name, &o)
req := logger.Logs(opts.Path, &o)
ctxt, cancelFunc := context.WithCancel(ctx)
req.Context(ctxt)
@ -132,8 +131,8 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptio
stream, err := req.Stream()
atomic.StoreInt32(&blocked, 0)
if err != nil {
log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path())
return fmt.Errorf("Unable to obtain log stream for %s", opts.Path())
log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path)
return fmt.Errorf("Unable to obtain log stream for %s", opts.Path)
}
go readLogs(ctx, stream, c, opts)
@ -150,7 +149,7 @@ func logsTimeout(cancel context.CancelFunc, blocked *int32) {
func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) {
defer func() {
log.Debug().Msgf(">>> Closing stream `%s", opts.Path())
log.Debug().Msgf(">>> Closing stream `%s", opts.Path)
if err := stream.Close(); err != nil {
log.Error().Err(err).Msg("Cloing stream")
}

View File

@ -0,0 +1,19 @@
package dao
import (
"github.com/rs/zerolog/log"
)
type PortForward struct {
Generic
}
var _ Accessor = &PortForward{}
var _ Nuker = &PortForward{}
// Delete a portforward.
func (p *PortForward) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("PortForward DELETE %q", path)
p.Factory.DeleteForwarder(path)
return nil
}

View File

@ -3,6 +3,7 @@ package dao
import (
"context"
"fmt"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/model"
@ -12,14 +13,14 @@ import (
// Reconcile previous vs current state and emits delta events.
func Reconcile(ctx context.Context, table render.TableData, gvr GVR) (render.TableData, error) {
path, ok := ctx.Value(internal.KeySelection).(string)
defer func(t time.Time) {
log.Debug().Msgf("Reconcile elapsed: %v", time.Since(t))
}(time.Now())
path, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return table, fmt.Errorf("no path specified for %s", gvr)
}
if path != "" {
log.Debug().Msgf("########## OVERRIDING NS %q", path)
table.Namespace = path
}
log.Debug().Msgf(" Reconcile %q in ns %q with path %q", gvr, table.Namespace, path)
factory, ok := ctx.Value(internal.KeyFactory).(Factory)
if !ok {
@ -41,10 +42,11 @@ func Reconcile(ctx context.Context, table render.TableData, gvr GVR) (render.Tab
table.Header = m.Renderer.Header(table.Namespace)
oo, err := m.Model.List(ctx)
if err != nil {
panic(err)
return table, err
}
log.Debug().Msgf("Model returned [%d] items", len(oo))
rows := make(render.Rows, len(oo))
// BOZO!! Pass in header len to avoid recomputing the header.
if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil {
return table, err
}
@ -65,7 +67,7 @@ func update(table *render.TableData, rows render.Rows) {
continue
}
if index, ok := table.RowEvents.FindIndex(row.ID); ok {
delta := render.NewDeltaRow(table.RowEvents[index].Row, row)
delta := render.NewDeltaRow(table.RowEvents[index].Row, row, table.Header.HasAge())
if delta.IsBlank() {
table.RowEvents[index].Kind, table.RowEvents[index].Deltas = render.EventUnchanged, blankDelta
} else {

View File

@ -15,21 +15,35 @@ import (
// MetaViewers represents a collection of meta viewers.
type ResourceMetas map[GVR]metav1.APIResource
// Accessors represents a collection of dao accessors.
type Accessors map[GVR]Accessor
var resMetas ResourceMetas
// AccessorFor returns a client accessor for a resource if registered.
// Otherwise it returns a generic accessor.
// Customize here for non resource types or types with metrics or logs.
func AccessorFor(f Factory, gvr GVR) (Accessor, error) {
m := map[GVR]Accessor{
m := Accessors{
"alias": &Alias{},
"contexts": &Context{},
"containers": &Container{},
"screendumps": &ScreenDump{},
"benchmarks": &Benchmark{},
"portforwards": &PortForward{},
"v1/services": &Service{},
"v1/pods": &Pod{},
"apps/v1/deployments": &Deployment{},
"apps/v1/daemonsets": &DaemonSet{},
"extensions/v1beta1/daemonsets": &DaemonSet{},
"apps/v1/statefulsets": &StatefulSet{},
"batch/v1beta1/cronjobs": &CronJob{},
"batch/v1/jobs": &Job{},
}
r, ok := m[gvr]
if !ok {
r = &Resource{}
r = &Generic{}
log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr)
}
r.Init(f, gvr)
@ -56,6 +70,17 @@ func MetaFor(gvr GVR) (metav1.APIResource, error) {
return m, nil
}
// IsK9sMeta checks for non resource meta.
func IsK9sMeta(m metav1.APIResource) bool {
for _, c := range m.Categories {
if c == "k9s" {
return true
}
}
return false
}
// Load hydrates server preferred+CRDs resource metadata.
func Load(f *watch.Factory) error {
resMetas = make(ResourceMetas, 100)
@ -70,23 +95,82 @@ func Load(f *watch.Factory) error {
}
func loadNonResource(m ResourceMetas) error {
m["aliases"] = metav1.APIResource{
Name: "aliases",
SingularName: "alias",
Namespaced: false,
Kind: "Aliases",
Verbs: []string{},
Categories: []string{"k9s"},
}
m["contexts"] = metav1.APIResource{
Name: "contexts",
SingularName: "context",
Namespaced: false,
Kind: "Context",
Kind: "Contexts",
ShortNames: []string{"ctx"},
Verbs: []string{},
Categories: []string{"K9s"},
Categories: []string{"k9s"},
}
m["screendumps"] = metav1.APIResource{
Name: "screendumps",
SingularName: "screendump",
Namespaced: false,
Kind: "ScreenDump",
Kind: "ScreenDumps",
ShortNames: []string{"sd"},
Verbs: []string{"delete"},
Categories: []string{"K9s"},
Categories: []string{"k9s"},
}
m["benchmarks"] = metav1.APIResource{
Name: "benchmarks",
SingularName: "benchmark",
Namespaced: false,
Kind: "Benchmarks",
ShortNames: []string{"be"},
Verbs: []string{"delete"},
Categories: []string{"k9s"},
}
m["portforwards"] = metav1.APIResource{
Name: "portforwards",
SingularName: "portforward",
Namespaced: true,
Kind: "PortForwards",
ShortNames: []string{"pf"},
Verbs: []string{"delete"},
Categories: []string{"k9s"},
}
// BOZO!! policies can't be launch on command
m["rbac"] = metav1.APIResource{
Name: "Rbac",
SingularName: "Rbac",
Namespaced: false,
Kind: "RBAC",
Categories: []string{"k9s"},
}
// BOZO!! Containers can't be launch on command
m["containers"] = metav1.APIResource{
Name: "containers",
SingularName: "container",
Namespaced: false,
Kind: "Containers",
Verbs: []string{},
Categories: []string{"k9s"},
}
m["users"] = metav1.APIResource{
Name: "users",
SingularName: "user",
Namespaced: false,
Kind: "User",
Verbs: []string{},
Categories: []string{"k9s"},
}
m["groups"] = metav1.APIResource{
Name: "groups",
SingularName: "group",
Namespaced: false,
Kind: "group",
Verbs: []string{},
Categories: []string{"k9s"},
}
return nil
@ -113,7 +197,7 @@ func loadPreferred(f *watch.Factory, m ResourceMetas) error {
}
func loadCRDs(f *watch.Factory, m ResourceMetas) error {
oo, err := f.List("", "apiextensions.k8s.io/v1beta1/customresourcedefinitions", labels.Everything())
oo, err := f.List("apiextensions.k8s.io/v1beta1/customresourcedefinitions", "", labels.Everything())
if err != nil {
return err
}

View File

@ -1,32 +0,0 @@
package dao
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
)
type Resource struct {
Factory
gvr GVR
}
func (r *Resource) Init(f Factory, gvr GVR) {
r.Factory, r.gvr = f, gvr
}
// Delete a Generic.
func (r *Resource) Delete(ns, n string, cascade, force bool) error {
p := metav1.DeletePropagationOrphan
if cascade {
p = metav1.DeletePropagationBackground
}
return r.dynClient().Namespace(ns).Delete(n, &metav1.DeleteOptions{
PropagationPolicy: &p,
})
}
func (r *Resource) dynClient() dynamic.NamespaceableResourceInterface {
return r.Client().DynDialOrDie().Resource(r.gvr.AsGVR())
}

View File

@ -2,20 +2,19 @@ package dao
import (
"os"
"path/filepath"
"github.com/rs/zerolog/log"
)
type ScreenDump struct {
Resource
Generic
}
var _ Accessor = &ScreenDump{}
var _ Nuker = &ScreenDump{}
// Delete a ScreenDump.
func (d *ScreenDump) Delete(dir, sel string, cascade, force bool) error {
log.Debug().Msgf("ScreenDump DELETE %q:%q", dir, sel)
return os.Remove(filepath.Join("/"+dir, sel))
func (d *ScreenDump) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("ScreenDump DELETE %q", path)
return os.Remove(path)
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -16,7 +17,7 @@ import (
)
type StatefulSet struct {
Resource
Generic
}
var _ Accessor = &StatefulSet{}
@ -25,7 +26,8 @@ var _ Restartable = &StatefulSet{}
var _ Scalable = &StatefulSet{}
// Scale a StatefulSet.
func (s *StatefulSet) Scale(ns, n string, replicas int32) error {
func (s *StatefulSet) Scale(path string, replicas int32) error {
ns, n := k8s.Namespaced(path)
scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{})
if err != nil {
return err
@ -37,8 +39,8 @@ func (s *StatefulSet) Scale(ns, n string, replicas int32) error {
}
// Restart a StatefulSet rollout.
func (s *StatefulSet) Restart(ns, n string) error {
o, err := s.Get(ns, string(s.gvr), n, labels.Everything())
func (s *StatefulSet) Restart(path string) error {
o, err := s.Get(string(s.gvr), path, labels.Everything())
if err != nil {
return err
}
@ -54,14 +56,14 @@ func (s *StatefulSet) Restart(ns, n string) error {
return err
}
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
_, err = s.Client().DialOrDie().AppsV1().StatefulSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update)
return err
}
// Logs tail logs for all pods represented by this StatefulSet.
func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing StatefulSet %q -- %q", opts.Namespace, opts.Name)
o, err := s.Get(opts.Namespace, string(s.gvr), opts.Name, labels.Everything())
log.Debug().Msgf("Tailing StatefulSet %q", opts.Path)
o, err := s.Get(string(s.gvr), opts.Path, labels.Everything())
if err != nil {
return err
}
@ -73,7 +75,7 @@ func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOpt
}
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on StatefulSet %s", opts.FQN())
return fmt.Errorf("No valid selector found on StatefulSet %s", opts.Path)
}
return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)

40
internal/dao/svc.go Normal file
View File

@ -0,0 +1,40 @@
package dao
import (
"context"
"errors"
"fmt"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
type Service struct {
Generic
}
var _ Accessor = &Service{}
var _ Loggable = &Service{}
// Logs tail logs for all pods represented by this Service.
func (s *Service) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing Service %q", opts.Path)
o, err := s.Get(string(s.gvr), opts.Path, labels.Everything())
if err != nil {
return err
}
var svc v1.Service
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc)
if err != nil {
return errors.New("expecting Service resource")
}
if svc.Spec.Selector == nil || len(svc.Spec.Selector) == 0 {
return fmt.Errorf("no valid selector found on Service %s", opts.Path)
}
return podLogs(ctx, c, svc.Spec.Selector, opts)
}

View File

@ -4,10 +4,13 @@ import (
"context"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/informers"
restclient "k8s.io/client-go/rest"
)
type Factory interface {
@ -15,7 +18,7 @@ type Factory interface {
Client() k8s.Connection
// Get fetch a given resource.
Get(ns, gvr, n string, sel labels.Selector) (runtime.Object, error)
Get(gvr, path string, sel labels.Selector) (runtime.Object, error)
// List fetch a collection of resources.
List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error)
@ -25,6 +28,12 @@ type Factory interface {
// WaitForCacheSync synchronize the cache.
WaitForCacheSync() map[schema.GroupVersionResource]bool
// DeleteForwarder deletes a pod forwarder.
DeleteForwarder(path string)
// Forwards returns all portforwards.
Forwarders() watch.Forwarders
}
// Accessor represents an accessible k8s resource.
@ -42,13 +51,13 @@ type Loggable interface {
}
type Scalable interface {
Scale(ns, n string, replicas int32) error
Scale(path string, replicas int32) error
}
// Nuker represents a resource deleter.
type Nuker interface {
// Delete removes a resource from the api server.
Delete(ns, n string, cascade, force bool) error
Delete(path string, cascade, force bool) error
}
// Switchable represents a switchable resource.
@ -60,5 +69,16 @@ type Switchable interface {
// Restartable represents a restartable resource.
type Restartable interface {
// Restart performs a rollout restart.
Restart(ns, n string) error
Restart(path string) error
}
// Runnable represents a runnable resource.
type Runnable interface {
// Run triggers a run.
Run(path string) error
}
// Loggers represents a resource that exposes logs.
type Logger interface {
Logs(path string, opts *v1.PodLogOptions) *restclient.Request
}

View File

@ -58,8 +58,6 @@ type (
ServerVersion() (*version.Info, error)
FetchNodes() (*v1.NodeList, error)
CurrentNamespaceName() (string, error)
CheckNSAccess(ns string) error
CheckListNSAccess() error
CanI(ns, gvr string, verbs []string) (bool, error)
}
@ -85,25 +83,6 @@ func InitConnectionOrDie(config *Config) *APIClient {
return &conn
}
// CheckListNSAccess check if current user can list namespaces.
func (a *APIClient) CheckListNSAccess() error {
ns := NewNamespace(a)
_, err := ns.List("", metav1.ListOptions{})
return err
}
// CheckNSAccess asserts if user can access a namespace.
func (a *APIClient) CheckNSAccess(n string) error {
ns := NewNamespace(a)
if n == "" {
_, err := ns.List(n, metav1.ListOptions{})
return err
}
_, err := ns.Get("", n)
return err
}
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
res := GVR(gvr).AsGVR()
return &authorizationv1.SelfSubjectAccessReview{

View File

@ -1,42 +0,0 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ClusterRole represents a Kubernetes ClusterRole
type ClusterRole struct {
*base
Connection
}
// NewClusterRole returns a new ClusterRole.
func NewClusterRole(c Connection) *ClusterRole {
return &ClusterRole{&base{}, c}
}
// Get a cluster role.
func (c *ClusterRole) Get(_, n string) (interface{}, error) {
panic("NYI")
return c.DialOrDie().RbacV1().ClusterRoles().Get(n, metav1.GetOptions{})
}
// List all ClusterRoles on a cluster.
func (c *ClusterRole) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := c.DialOrDie().RbacV1().ClusterRoles().List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a ClusterRole.
func (c *ClusterRole) Delete(_, n string, cascade, force bool) error {
return c.DialOrDie().RbacV1().ClusterRoles().Delete(n, nil)
}

View File

@ -1,42 +0,0 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ClusterRoleBinding represents a Kubernetes ClusterRoleBinding
type ClusterRoleBinding struct {
*base
Connection
}
// NewClusterRoleBinding returns a new ClusterRoleBinding.
func NewClusterRoleBinding(c Connection) *ClusterRoleBinding {
return &ClusterRoleBinding{&base{}, c}
}
// Get a service.
func (c *ClusterRoleBinding) Get(_, n string) (interface{}, error) {
panic("NYI")
return c.DialOrDie().RbacV1().ClusterRoleBindings().Get(n, metav1.GetOptions{})
}
// List all ClusterRoleBindings on a cluster.
func (c *ClusterRoleBinding) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := c.DialOrDie().RbacV1().ClusterRoleBindings().List(opts)
if err != nil {
return Collection{}, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a ClusterRoleBinding.
func (c *ClusterRoleBinding) Delete(_, n string, cascade, force bool) error {
return c.DialOrDie().RbacV1().ClusterRoleBindings().Delete(n, nil)
}

View File

@ -1,108 +1,109 @@
package k8s
import (
"fmt"
// BOZO!!
// import (
// "fmt"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
// "github.com/rs/zerolog/log"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// "k8s.io/client-go/tools/clientcmd"
// "k8s.io/client-go/tools/clientcmd/api"
// )
// NamedContext represents a named cluster context.
type NamedContext struct {
Name string
Context *api.Context
config *Config
}
// // NamedContext represents a named cluster context.
// type NamedContext struct {
// Name string
// Context *api.Context
// config *Config
// }
// NewNamedContext returns a new named context.
func NewNamedContext(c *Config, n string, ctx *api.Context) *NamedContext {
return &NamedContext{Name: n, Context: ctx, config: c}
}
// // NewNamedContext returns a new named context.
// func NewNamedContext(c *Config, n string, ctx *api.Context) *NamedContext {
// return &NamedContext{Name: n, Context: ctx, config: c}
// }
// MustCurrentContextName return the active context name.
func (c *NamedContext) MustCurrentContextName() string {
cl, err := c.config.CurrentContextName()
if err != nil {
log.Fatal().Err(err).Msg("Fetching current context")
}
return cl
}
// // MustCurrentContextName return the active context name.
// func (c *NamedContext) MustCurrentContextName() string {
// cl, err := c.config.CurrentContextName()
// if err != nil {
// log.Fatal().Err(err).Msg("Fetching current context")
// }
// return cl
// }
// ----------------------------------------------------------------------------
// // ----------------------------------------------------------------------------
// Context represents a Kubernetes Context.
type Context struct {
*base
Connection
}
// // Context represents a Kubernetes Context.
// type Context struct {
// *base
// Connection
// }
// NewContext returns a new Context.
func NewContext(c Connection) *Context {
return &Context{&base{}, c}
}
// // NewContext returns a new Context.
// func NewContext(c Connection) *Context {
// return &Context{&base{}, c}
// }
// Get a Context.
func (c *Context) Get(_, n string) (interface{}, error) {
ctx, err := c.Config().GetContext(n)
if err != nil {
return nil, err
}
return &NamedContext{Name: n, Context: ctx}, nil
}
// // Get a Context.
// func (c *Context) Get(_, n string) (interface{}, error) {
// ctx, err := c.Config().GetContext(n)
// if err != nil {
// return nil, err
// }
// return &NamedContext{Name: n, Context: ctx}, nil
// }
// List all Contexts on the current cluster.
func (c *Context) List(string, metav1.ListOptions) (Collection, error) {
ctxs, err := c.Config().Contexts()
if err != nil {
return nil, err
}
cc := make([]interface{}, 0, len(ctxs))
for k, v := range ctxs {
cc = append(cc, NewNamedContext(c.Config(), k, v))
}
// // List all Contexts on the current cluster.
// func (c *Context) List(string, metav1.ListOptions) (Collection, error) {
// ctxs, err := c.Config().Contexts()
// if err != nil {
// return nil, err
// }
// cc := make([]interface{}, 0, len(ctxs))
// for k, v := range ctxs {
// cc = append(cc, NewNamedContext(c.Config(), k, v))
// }
return cc, nil
}
// return cc, nil
// }
// Delete a Context.
func (c *Context) Delete(_, n string, cascade, force bool) error {
ctx, err := c.Config().CurrentContextName()
if err != nil {
return err
}
if ctx == n {
return fmt.Errorf("trying to delete your current context %s", n)
}
return c.Config().DelContext(n)
}
// // Delete a Context.
// func (c *Context) Delete(_, n string, cascade, force bool) error {
// ctx, err := c.Config().CurrentContextName()
// if err != nil {
// return err
// }
// if ctx == n {
// return fmt.Errorf("trying to delete your current context %s", n)
// }
// return c.Config().DelContext(n)
// }
// MustCurrentContextName return the active context name.
func (c *Context) MustCurrentContextName() string {
cl, err := c.Config().CurrentContextName()
if err != nil {
log.Fatal().Err(err).Msg("Fetching current context")
}
return cl
}
// // MustCurrentContextName return the active context name.
// func (c *Context) MustCurrentContextName() string {
// cl, err := c.Config().CurrentContextName()
// if err != nil {
// log.Fatal().Err(err).Msg("Fetching current context")
// }
// return cl
// }
// Switch to another context.
func (c *Context) Switch(ctx string) error {
c.SwitchContextOrDie(ctx)
return nil
}
// // Switch to another context.
// func (c *Context) Switch(ctx string) error {
// c.SwitchContextOrDie(ctx)
// return nil
// }
// KubeUpdate modifies kubeconfig default context.
func (c *Context) KubeUpdate(n string) error {
config, err := c.Config().RawConfig()
if err != nil {
return err
}
if err := c.Switch(n); err != nil {
return err
}
return clientcmd.ModifyConfig(
clientcmd.NewDefaultPathOptions(), config, true,
)
}
// // KubeUpdate modifies kubeconfig default context.
// func (c *Context) KubeUpdate(n string) error {
// config, err := c.Config().RawConfig()
// if err != nil {
// return err
// }
// if err := c.Switch(n); err != nil {
// return err
// }
// return clientcmd.ModifyConfig(
// clientcmd.NewDefaultPathOptions(), config, true,
// )
// }

View File

@ -1,76 +0,0 @@
package k8s
import (
"errors"
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 {
*base
Connection
}
// NewCronJob returns a new CronJob.
func NewCronJob(c Connection) *CronJob {
return &CronJob{&base{}, c}
}
// Get a CronJob.
func (c *CronJob) Get(ns, n string) (interface{}, error) {
return c.DialOrDie().BatchV1beta1().CronJobs(ns).Get(n, metav1.GetOptions{})
}
// List all CronJobs in a given namespace.
func (c *CronJob) List(ns string, opts metav1.ListOptions) (Collection, error) {
rr, err := c.DialOrDie().BatchV1beta1().CronJobs(ns).List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a CronJob.
func (c *CronJob) Delete(ns, n string, cascade, force bool) error {
return c.DialOrDie().BatchV1beta1().CronJobs(ns).Delete(n, nil)
}
// Run the job associated with this cronjob.
func (c *CronJob) Run(ns, n string) error {
cj, err := c.Get(ns, n)
if err != nil {
return err
}
cronJob, ok := cj.(*batchv1beta1.CronJob)
if !ok {
return errors.New("Expecting valid cronjob")
}
var jobName = cronJob.Name
if len(cronJob.Name) >= maxJobNameSize {
jobName = cronJob.Name[0:maxJobNameSize]
}
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 = c.DialOrDie().BatchV1().Jobs(ns).Create(job)
return err
}

View File

@ -1,72 +0,0 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubectl/pkg/polymorphichelpers"
)
// Deployment represents a Kubernetes Deployment.
type Deployment struct {
*base
Connection
}
// NewDeployment returns a new Deployment.
func NewDeployment(c Connection) *Deployment {
return &Deployment{&base{}, c}
}
// Get a deployment.
func (d *Deployment) Get(ns, n string) (interface{}, error) {
panic("NYI")
return d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{})
}
// List all Deployments in a given namespace.
func (d *Deployment) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := d.DialOrDie().AppsV1().Deployments(ns).List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a Deployment.
func (d *Deployment) Delete(ns, n string, cascade, force bool) error {
return d.DialOrDie().AppsV1().Deployments(ns).Delete(n, nil)
}
// Scale a Deployment.
func (d *Deployment) Scale(ns, n string, replicas int32) error {
scale, err := d.DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{})
if err != nil {
return err
}
scale.Spec.Replicas = replicas
_, err = d.DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale)
return err
}
// Restart a Deployment rollout.
func (d *Deployment) Restart(ns, n string) error {
dp, err := d.DialOrDie().AppsV1().Deployments(ns).Get(n, metav1.GetOptions{})
if err != nil {
return err
}
update, err := polymorphichelpers.ObjectRestarterFn(dp)
if err != nil {
return err
}
_, err = d.DialOrDie().AppsV1().Deployments(ns).Patch(dp.Name, types.StrategicMergePatchType, update)
return err
}

View File

@ -1,73 +0,0 @@
package k8s
// BOZO!!
// import (
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// "k8s.io/apimachinery/pkg/types"
// "k8s.io/kubectl/pkg/polymorphichelpers"
// )
// // DaemonSet represents a Kubernetes DaemonSet
// type DaemonSet struct {
// *base
// Connection
// }
// // NewDaemonSet returns a new DaemonSet.
// func NewDaemonSet(c Connection) *DaemonSet {
// return &DaemonSet{&base{}, c}
// }
// // Get a DaemonSet.
// func (d *DaemonSet) Get(ns, n string) (interface{}, error) {
// panic("NYI")
// return d.DialOrDie().AppsV1().DaemonSets(ns).Get(n, metav1.GetOptions{})
// }
// // List all DaemonSets in a given namespace.
// func (d *DaemonSet) List(ns string, opts metav1.ListOptions) (Collection, error) {
// panic("NYI")
// rr, err := d.DialOrDie().AppsV1().DaemonSets(ns).List(opts)
// if err != nil {
// return nil, err
// }
// cc := make(Collection, len(rr.Items))
// for i, r := range rr.Items {
// cc[i] = r
// }
// return cc, nil
// }
// // Delete a DaemonSet.
// func (d *DaemonSet) Delete(ns, n string, cascade, force bool) error {
// p := metav1.DeletePropagationOrphan
// if cascade {
// p = metav1.DeletePropagationBackground
// }
// return d.DialOrDie().AppsV1().DaemonSets(ns).Delete(n, &metav1.DeleteOptions{
// PropagationPolicy: &p,
// })
// }
// // Restart a DaemonSet rollout.
// func (d *DaemonSet) Restart(f *watch.Factory, ns, n string) error {
// o, err := f.Get(ns, "apps/v1/deamonsets", n, labels.Everything())
// if err != nil {
// return err
// }
// var ds appsv1.DaemonSet
// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)
// if err != nil {
// return err
// }
// update, err := polymorphichelpers.ObjectRestarterFn(ds)
// if err != nil {
// return err
// }
// _, err = f.Client().DialOrDie().AppsV1().DaemonSets(ns).Patch(ds.Name, types.StrategicMergePatchType, update)
// return err
// }

View File

@ -20,8 +20,17 @@ func toPerc(v1, v2 float64) float64 {
return math.Round((v1 / v2) * 100)
}
func namespaced(n string) (string, string) {
// Namespaced converts a resource path to namespace and resource name.
func Namespaced(n string) (string, string) {
ns, po := path.Split(n)
return strings.Trim(ns, "/"), po
}
// FQN returns a fully qualified resource name.
func FQN(ns, n string) string {
if ns == "" {
return n
}
return ns + "/" + n
}

View File

@ -1,94 +0,0 @@
package k8s
import (
"fmt"
"strings"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
restclient "k8s.io/client-go/rest"
)
type (
// Job represents a Kubernetes Job.
Job struct {
*base
Connection
}
// Loggable represents a K8s resource that has containers and can be logged.
Loggable interface {
Containers(ns, n string, includeInit bool) ([]string, error)
Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request
}
)
// NewJob returns a new Job.
func NewJob(c Connection) *Job {
return &Job{&base{}, c}
}
// Get a Job.
func (j *Job) Get(ns, n string) (interface{}, error) {
return j.DialOrDie().BatchV1().Jobs(ns).Get(n, metav1.GetOptions{})
}
// List all Jobs in a given namespace.
func (j *Job) List(ns string, opts metav1.ListOptions) (Collection, error) {
rr, err := j.DialOrDie().BatchV1().Jobs(ns).List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a Job.
func (j *Job) Delete(ns, n string, cascade, force bool) error {
return j.DialOrDie().BatchV1().Jobs(ns).Delete(n, nil)
}
// Containers returns all container names on job.
func (j *Job) Containers(ns, n string, includeInit bool) ([]string, error) {
pod, err := j.assocPod(ns, n)
if err != nil {
return nil, err
}
return NewPod(j).Containers(ns, pod, includeInit)
}
// Logs fetch container logs for a given job and container.
func (j *Job) Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request {
pod, err := j.assocPod(ns, n)
if err != nil {
return nil
}
return NewPod(j).Logs(ns, pod, opts)
}
// Events retrieved jobs events.
func (j *Job) Events(ns, n string) (*v1.EventList, error) {
e := j.DialOrDie().CoreV1().Events(ns)
return e.List(metav1.ListOptions{
FieldSelector: e.GetFieldSelector(&n, &ns, nil, nil).String(),
})
}
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)
}

View File

@ -1,40 +0,0 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Node represents a Kubernetes node.
type Node struct {
*base
Connection
}
// NewNode returns a new Node.
func NewNode(c Connection) *Node {
return &Node{&base{}, c}
}
// Get a node.
func (n *Node) Get(_, name string) (interface{}, error) {
return n.DialOrDie().CoreV1().Nodes().Get(name, metav1.GetOptions{})
}
// List all nodes on the cluster.
func (n *Node) List(ns string, opts metav1.ListOptions) (Collection, error) {
rr, err := n.DialOrDie().CoreV1().Nodes().List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a node.
func (n *Node) Delete(_, name string, cascade, force bool) error {
return n.DialOrDie().CoreV1().Nodes().Delete(name, nil)
}

View File

@ -1,41 +0,0 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Namespace represents a Kubernetes namespace.
type Namespace struct {
*base
Connection
}
// NewNamespace returns a new Namespace.
func NewNamespace(c Connection) *Namespace {
return &Namespace{&base{}, c}
}
// Get a active namespace.
func (n *Namespace) Get(_, name string) (interface{}, error) {
panic("NYI")
return n.DialOrDie().CoreV1().Namespaces().Get(name, metav1.GetOptions{})
}
// List all active namespaces on the cluster.
func (n *Namespace) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := n.DialOrDie().CoreV1().Namespaces().List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a namespace.
func (n *Namespace) Delete(_, name string, cascade, force bool) error {
return n.DialOrDie().CoreV1().Namespaces().Delete(name, nil)
}

View File

@ -91,7 +91,7 @@ func (p *PortForward) FQN() string {
func (p *PortForward) Start(path, co string, ports []string) (*portforward.PortForwarder, error) {
p.path, p.container, p.ports, p.age = path, co, ports, time.Now()
ns, n := namespaced(path)
ns, n := Namespaced(path)
pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{})
if err != nil {
return nil, err

View File

@ -1,41 +1,42 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BOZO!!
// import (
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
// PersistentVolume represents a Kubernetes PersistentVolume.
type PersistentVolume struct {
*base
Connection
}
// // PersistentVolume represents a Kubernetes PersistentVolume.
// type PersistentVolume struct {
// *base
// Connection
// }
// NewPersistentVolume returns a new PersistentVolume.
func NewPersistentVolume(c Connection) *PersistentVolume {
return &PersistentVolume{&base{}, c}
}
// // NewPersistentVolume returns a new PersistentVolume.
// func NewPersistentVolume(c Connection) *PersistentVolume {
// return &PersistentVolume{&base{}, c}
// }
// Get a PersistentVolume.
func (p *PersistentVolume) Get(_, n string) (interface{}, error) {
return p.DialOrDie().CoreV1().PersistentVolumes().Get(n, metav1.GetOptions{})
}
// // Get a PersistentVolume.
// func (p *PersistentVolume) Get(_, n string) (interface{}, error) {
// return p.DialOrDie().CoreV1().PersistentVolumes().Get(n, metav1.GetOptions{})
// }
// List all PersistentVolumes in a given namespace.
func (p *PersistentVolume) List(ns string, opts metav1.ListOptions) (Collection, error) {
rr, err := p.DialOrDie().CoreV1().PersistentVolumes().List(opts)
if err != nil {
return nil, err
}
// // List all PersistentVolumes in a given namespace.
// func (p *PersistentVolume) List(ns string, opts metav1.ListOptions) (Collection, error) {
// rr, err := p.DialOrDie().CoreV1().PersistentVolumes().List(opts)
// if err != nil {
// return nil, err
// }
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
// cc := make(Collection, len(rr.Items))
// for i, r := range rr.Items {
// cc[i] = r
// }
return cc, nil
}
// return cc, nil
// }
// Delete a PersistentVolume.
func (p *PersistentVolume) Delete(_, n string, cascade, force bool) error {
return p.DialOrDie().CoreV1().PersistentVolumes().Delete(n, nil)
}
// // Delete a PersistentVolume.
// func (p *PersistentVolume) Delete(_, n string, cascade, force bool) error {
// return p.DialOrDie().CoreV1().PersistentVolumes().Delete(n, nil)
// }

View File

@ -1,40 +1,41 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BOZO!!
// import (
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
// PersistentVolumeClaim represents a Kubernetes PersistentVolumeClaim.
type PersistentVolumeClaim struct {
*base
Connection
}
// // PersistentVolumeClaim represents a Kubernetes PersistentVolumeClaim.
// type PersistentVolumeClaim struct {
// *base
// Connection
// }
// NewPersistentVolumeClaim returns a new PersistentVolumeClaim.
func NewPersistentVolumeClaim(c Connection) *PersistentVolumeClaim {
return &PersistentVolumeClaim{&base{}, c}
}
// // NewPersistentVolumeClaim returns a new PersistentVolumeClaim.
// func NewPersistentVolumeClaim(c Connection) *PersistentVolumeClaim {
// return &PersistentVolumeClaim{&base{}, c}
// }
// Get a PersistentVolumeClaim.
func (p *PersistentVolumeClaim) Get(ns, n string) (interface{}, error) {
return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Get(n, metav1.GetOptions{})
}
// // Get a PersistentVolumeClaim.
// func (p *PersistentVolumeClaim) Get(ns, n string) (interface{}, error) {
// return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Get(n, metav1.GetOptions{})
// }
// List all PersistentVolumeClaims in a given namespace.
func (p *PersistentVolumeClaim) List(ns string, opts metav1.ListOptions) (Collection, error) {
rr, err := p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
// // List all PersistentVolumeClaims in a given namespace.
// func (p *PersistentVolumeClaim) List(ns string, opts metav1.ListOptions) (Collection, error) {
// rr, err := p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).List(opts)
// if err != nil {
// return nil, err
// }
// cc := make(Collection, len(rr.Items))
// for i, r := range rr.Items {
// cc[i] = r
// }
return cc, nil
}
// return cc, nil
// }
// Delete a PersistentVolumeClaim.
func (p *PersistentVolumeClaim) Delete(ns, n string, cascade, force bool) error {
return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Delete(n, nil)
}
// // Delete a PersistentVolumeClaim.
// func (p *PersistentVolumeClaim) Delete(ns, n string, cascade, force bool) error {
// return p.DialOrDie().CoreV1().PersistentVolumeClaims(ns).Delete(n, nil)
// }

View File

@ -1,103 +1,105 @@
package k8s
import (
"fmt"
// BOZO!!
// import (
// "fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
)
// "github.com/derailed/k9s/internal/dao"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
// "k8s.io/apimachinery/pkg/runtime"
// "k8s.io/apimachinery/pkg/runtime/serializer"
// "k8s.io/client-go/dynamic"
// "k8s.io/client-go/rest"
// )
// Resource represents a Kubernetes Resource
type Resource struct {
*base
Connection
// // Resource represents a Kubernetes Resource
// type Resource struct {
// *base
// Connection
gvr GVR
}
// gvr dao.GVR
// }
// NewResource returns a new Resource.
func NewResource(c Connection, gvr GVR) *Resource {
return &Resource{base: &base{}, Connection: c, gvr: gvr}
}
// // NewResource returns a new Resource.
// func NewResource(c Connection, gvr GVR) *Resource {
// return &Resource{base: &base{}, Connection: c, gvr: gvr}
// }
// GetInfo returns info about apigroup.
func (r *Resource) GetInfo() GVR {
return r.gvr
}
// // GetInfo returns info about apigroup.
// func (r *Resource) GetInfo() GVR {
// return r.gvr
// }
func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface {
return r.DynDialOrDie().Resource(r.gvr.AsGVR())
}
// func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface {
// return r.DynDialOrDie().Resource(r.gvr.AsGVR())
// }
// Get a Resource.
func (r *Resource) Get(ns, n string) (interface{}, error) {
return r.nsRes().Namespace(ns).Get(n, metav1.GetOptions{})
}
// // Get a Resource.
// func (r *Resource) Get(ns, n string) (interface{}, error) {
// return r.nsRes().Namespace(ns).Get(n, metav1.GetOptions{})
// }
// List all Resources in a given namespace.
func (r *Resource) List(ns string, opts metav1.ListOptions) (Collection, error) {
obj, err := r.listAll(ns, r.gvr.ToR())
if err != nil {
return nil, err
}
return Collection{obj.(*metav1beta1.Table)}, nil
}
// // List all Resources in a given namespace.
// func (r *Resource) List(ns string, opts metav1.ListOptions) (Collection, error) {
// obj, err := r.listAll(ns, r.gvr.ToR())
// if err != nil {
// return nil, err
// }
// return Collection{obj.(*metav1beta1.Table)}, nil
// }
// Delete a Resource.
func (r *Resource) Delete(ns, n string, cascade, force bool) error {
return r.nsRes().Namespace(ns).Delete(n, nil)
}
// // Delete a Resource.
// func (r *Resource) Delete(ns, n string, cascade, force bool) error {
// return r.nsRes().Namespace(ns).Delete(n, nil)
// }
// ----------------------------------------------------------------------------
// Helpers...
// // ----------------------------------------------------------------------------
// // Helpers...
const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
// const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
func (r *Resource) listAll(ns, n string) (runtime.Object, error) {
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
_, codec := r.codec()
// func (r *Resource) listAll(ns, n string) (runtime.Object, error) {
// a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
// _, codec := r.codec()
c, err := r.getClient()
if err != nil {
return nil, err
}
// c, err := r.getClient()
// if err != nil {
// return nil, err
// }
return c.Get().
SetHeader("Accept", a).
Namespace(ns).
Resource(n).
VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get()
}
// return c.Get().
// SetHeader("Accept", a).
// Namespace(ns).
// Resource(n).
// VersionedParams(&metav1beta1.TableOptions{}, codec).
// Do().Get()
// }
func (r *Resource) getClient() (*rest.RESTClient, error) {
crConfig := r.RestConfigOrDie()
gv := r.gvr.AsGV()
crConfig.GroupVersion = &gv
crConfig.APIPath = "/apis"
if len(r.gvr.ToG()) == 0 {
crConfig.APIPath = "/api"
}
codec, _ := r.codec()
crConfig.NegotiatedSerializer = codec.WithoutConversion()
// func (r *Resource) getClient() (*rest.RESTClient, error) {
// crConfig := r.RestConfigOrDie()
// gv := r.gvr.AsGV()
// crConfig.GroupVersion = &gv
// crConfig.APIPath = "/apis"
// if len(r.gvr.ToG()) == 0 {
// crConfig.APIPath = "/api"
// }
// codec, _ := r.codec()
// crConfig.NegotiatedSerializer = codec.WithoutConversion()
crRestClient, err := rest.RESTClientFor(crConfig)
if err != nil {
return nil, err
}
return crRestClient, nil
}
// crRestClient, err := rest.RESTClientFor(crConfig)
// if err != nil {
// return nil, err
// }
// return crRestClient, nil
// }
func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
scheme := runtime.NewScheme()
gv := r.gvr.AsGV()
metav1.AddToGroupVersion(scheme, gv)
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
// func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
// scheme := runtime.NewScheme()
// gv := r.gvr.AsGV()
// metav1.AddToGroupVersion(scheme, gv)
// scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
// scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme)
}
// return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme)
// }

View File

@ -1,76 +0,0 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubectl/pkg/polymorphichelpers"
)
// StatefulSet manages a Kubernetes StatefulSet.
type StatefulSet struct {
*base
Connection
}
// NewStatefulSet instantiates a new StatefulSet.
func NewStatefulSet(c Connection) *StatefulSet {
return &StatefulSet{&base{}, c}
}
// Get a StatefulSet.
func (s *StatefulSet) Get(ns, n string) (interface{}, error) {
return s.DialOrDie().AppsV1().StatefulSets(ns).Get(n, metav1.GetOptions{})
}
// List all StatefulSets in a given namespace.
func (s *StatefulSet) List(ns string, opts metav1.ListOptions) (Collection, error) {
rr, err := s.DialOrDie().AppsV1().StatefulSets(ns).List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a StatefulSet.
func (s *StatefulSet) Delete(ns, n string, cascade, force bool) error {
p := metav1.DeletePropagationOrphan
if cascade {
p = metav1.DeletePropagationBackground
}
return s.DialOrDie().AppsV1().StatefulSets(ns).Delete(n, &metav1.DeleteOptions{
PropagationPolicy: &p,
})
}
// Scale a StatefulSet.
func (s *StatefulSet) Scale(ns, n string, replicas int32) error {
scale, err := s.DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{})
if err != nil {
return err
}
scale.Spec.Replicas = replicas
_, err = s.DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale)
return err
}
// Restart a StatefulSet rollout.
func (s *StatefulSet) Restart(ns, n string) error {
sts, err := s.DialOrDie().AppsV1().StatefulSets(ns).Get(n, metav1.GetOptions{})
if err != nil {
return err
}
update, err := polymorphichelpers.ObjectRestarterFn(sts)
if err != nil {
return err
}
_, err = s.DialOrDie().AppsV1().StatefulSets(ns).Patch(sts.Name, types.StrategicMergePatchType, update)
return err
}

View File

@ -1,42 +0,0 @@
package k8s
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Service represents a Kubernetes Service.
type Service struct {
*base
Connection
}
// NewService returns a new Service.
func NewService(c Connection) *Service {
return &Service{&base{}, c}
}
// Get a service.
func (s *Service) Get(ns, n string) (interface{}, error) {
panic("NYI")
return s.DialOrDie().CoreV1().Services(ns).Get(n, metav1.GetOptions{})
}
// List all Services in a given namespace.
func (s *Service) List(ns string, opts metav1.ListOptions) (Collection, error) {
panic("NYI")
rr, err := s.DialOrDie().CoreV1().Services(ns).List(opts)
if err != nil {
return nil, err
}
cc := make(Collection, len(rr.Items))
for i, r := range rr.Items {
cc[i] = r
}
return cc, nil
}
// Delete a Service.
func (s *Service) Delete(ns, n string, cascade, force bool) error {
return s.DialOrDie().CoreV1().Services(ns).Delete(n, nil)
}

View File

@ -5,10 +5,17 @@ type ContextKey string
const (
// Factory represents a factory context key.
KeyFactory ContextKey = "factory"
KeySelection = "selection"
KeyLabels = "labels"
KeyFields = "fields"
KeyTable = "table"
KeyDir = "dir"
KeyFactory ContextKey = "factory"
KeyLabels = "labels"
KeyFields = "fields"
KeyTable = "table"
KeyDir = "dir"
KeyPath = "path"
KeySubject = "subject"
KeyGVR = "gvr"
KeyForwards = "forwards"
KeyContainers = "containers"
KeyBenchCfg = "benchcfg"
KeyAliases = "aliases"
KeyUID = "uid"
)

52
internal/model/alias.go Normal file
View File

@ -0,0 +1,52 @@
package model
import (
"context"
"errors"
"sort"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render"
"k8s.io/apimachinery/pkg/runtime"
)
// Alias represents a collection of aliases.
type Alias struct {
Resource
}
// List returns a collection of screen dumps.
func (b *Alias) List(ctx context.Context) ([]runtime.Object, error) {
aa, ok := ctx.Value(internal.KeyAliases).(config.Alias)
if !ok {
return nil, errors.New("no aliases found in context")
}
m := make(config.ShortNames, len(aa))
for alias, gvr := range aa {
if _, ok := m[gvr]; ok {
m[gvr] = append(m[gvr], alias)
} else {
m[gvr] = []string{alias}
}
}
oo := make([]runtime.Object, 0, len(m))
for gvr, aliases := range m {
sort.StringSlice(aliases).Sort()
oo = append(oo, render.AliasRes{GVR: gvr, Aliases: aliases})
}
return oo, nil
}
// Hydrate returns a pod as container rows.
func (b *Alias) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
if err := re.Render(o, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,47 @@
package model
import (
"context"
"errors"
"io/ioutil"
"path/filepath"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render"
"k8s.io/apimachinery/pkg/runtime"
)
// Benchmark represents a collection of benchmarks.
type Benchmark struct {
Resource
}
// List returns a collection of screen dumps.
func (b *Benchmark) List(ctx context.Context) ([]runtime.Object, error) {
dir, ok := ctx.Value(internal.KeyDir).(string)
if !ok {
return nil, errors.New("no benchmark dir found in context")
}
ff, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
oo := make([]runtime.Object, len(ff))
for i, f := range ff {
oo[i] = render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())}
}
return oo, nil
}
// Hydrate returns a pod as container rows.
func (b *Benchmark) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
if err := re.Render(o, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}

View File

@ -2,6 +2,7 @@ package model
import (
"context"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
@ -28,10 +29,13 @@ type Container struct {
// List returns a collection of containers
func (c *Container) List(ctx context.Context) ([]runtime.Object, error) {
c.pod = nil
sel := ctx.Value(internal.KeySelection).(string)
ns, n := render.Namespaced(sel)
path, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, fmt.Errorf("no context path for %q", c.gvr)
}
ns, _ := render.Namespaced(path)
c.namespace = ns
o, err := c.factory.Get(ns, "v1/pods", n, labels.Everything())
o, err := c.factory.Get("v1/pods", path, labels.Everything())
if err != nil {
return nil, err
}

View File

@ -40,7 +40,7 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
// BOZO!! Need to know if gvr is namespaced or not
o, err := c.Get().
SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
// Namespace(g.namespace).
Namespace(g.namespace).
Resource(gvr.ToR()).
VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get()

87
internal/model/job.go Normal file
View File

@ -0,0 +1,87 @@
package model
import (
"context"
"errors"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
// Job represents a collections of jobs.
type Job struct {
Resource
}
// List returns a collection of screen dumps.
func (c *Job) List(ctx context.Context) ([]runtime.Object, error) {
uid, ok := ctx.Value(internal.KeyUID).(string)
if !ok {
log.Debug().Msgf("NO UID in context")
}
path, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, errors.New("no cronjob path found in context")
}
oo, err := c.Resource.List(ctx)
if err != nil {
return nil, err
}
if uid == "" {
return oo, nil
}
_, cronName := k8s.Namespaced(path)
jj := make([]runtime.Object, 0, len(oo))
for _, j := range oo {
var job batchv1.Job
err = runtime.DefaultUnstructuredConverter.FromUnstructured(j.(*unstructured.Unstructured).Object, &job)
if err != nil {
return nil, err
}
if !isNamedAfter(cronName, job.Name) {
continue
}
id, ok := job.Spec.Selector.MatchLabels["controller-uid"]
if !ok {
continue
}
if isControlledBy(uid, id) {
log.Debug().Msgf("Job %s -- %#v -- %q -- %q", job.Name, job.Labels, uid, path)
jj = append(jj, j)
}
}
return jj, nil
}
// Hydrate returns a pod as container rows.
func (c *Job) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
if err := re.Render(o, c.namespace, &rr[i]); err != nil {
return err
}
}
return nil
}
func isControlledBy(cuid, id string) bool {
tokens := strings.Split(cuid, "-")
root := strings.Join(tokens[2:], "-")
return strings.Contains(id, root)
}
func isNamedAfter(p, n string) bool {
tokens := strings.Split(n, "-")
if len(tokens) == 0 || tokens[0] != p {
return false
}
return true
}

View File

@ -44,7 +44,7 @@ func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
mx := k8s.NewMetricsServer(n.factory.Client().(k8s.Connection))
mmx, err := mx.FetchNodesMetrics()
if err != nil {
return err
log.Warn().Err(err).Msg("No node metrics")
}
var index int
@ -80,7 +80,7 @@ func nodeMetricsFor(o runtime.Object, mmx *mv1beta1.NodeMetricsList) *mv1beta1.N
}
func (n *Node) nodePods(f Factory, node string) ([]*v1.Pod, error) {
pp, err := f.List("", "v1/pods", labels.Everything())
pp, err := f.List("v1/pods", render.AllNamespaces, labels.Everything())
if err != nil {
return nil, err
}

View File

@ -2,7 +2,6 @@ package model
import (
"context"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
@ -26,27 +25,21 @@ func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
return oo, err
}
fieldSel, ok := ctx.Value(internal.KeyFields).(string)
sel, ok := ctx.Value(internal.KeyFields).(string)
if !ok {
return oo, nil
}
sel, err := labels.ConvertSelectorToLabelsMap(fieldSel)
fsel, err := labels.ConvertSelectorToLabelsMap(sel)
if err != nil {
return nil, err
}
nodeName, ok := sel["spec.nodeName"]
if !ok {
return nil, fmt.Errorf("NYI field selector %q", nodeName)
}
nodeName := fsel["spec.nodeName"]
var res []runtime.Object
for _, o := range oo {
u := o.(*unstructured.Unstructured)
spec := u.Object["spec"].(map[string]interface{})
log.Debug().Msgf("Spec node %q -- %q", nodeName, spec["nodeName"])
if spec["nodeName"] == nodeName {
if nodeName == "" || spec["nodeName"] == nodeName {
res = append(res, o)
}
}

View File

@ -0,0 +1,77 @@
package model
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// PortForward represents a portforward model.
type PortForward struct {
Resource
pod *v1.Pod
}
// List returns a collection of screen dumps.
func (c *PortForward) List(ctx context.Context) ([]runtime.Object, error) {
config, ok := ctx.Value(internal.KeyBenchCfg).(*config.Bench)
if !ok {
return nil, fmt.Errorf("no benchconfig found in context")
}
cc := config.Benchmarks.Containers
oo := make([]runtime.Object, 0, len(c.factory.Forwarders()))
for _, f := range c.factory.Forwarders() {
cfg := render.BenchCfg{
C: config.Benchmarks.Defaults.C,
N: config.Benchmarks.Defaults.N,
}
if config, ok := cc[containerID(f.Path(), f.Container())]; ok {
cfg.C, cfg.N = config.C, config.N
cfg.Host, cfg.Path = config.HTTP.Host, config.HTTP.Path
}
oo = append(oo, render.ForwardRes{
Forwarder: f,
Config: cfg,
})
}
return oo, nil
}
// Hydrate returns a pod as container rows.
func (c *PortForward) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
log.Debug().Msgf("PortFWD GOT %#v", o)
res, ok := o.(render.ForwardRes)
if !ok {
return fmt.Errorf("expecting a forwardres but got %T", o)
}
if err := re.Render(res, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
// ContainerID computes container ID based on ns/po/co.
func containerID(path, co string) string {
ns, n := k8s.Namespaced(path)
po := strings.Split(n, "-")[0]
return ns + "/" + po + ":" + co
}

196
internal/model/rbac.go Normal file
View File

@ -0,0 +1,196 @@
package model
import (
"context"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
type Rbac struct {
Resource
}
func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) {
gvr, ok := ctx.Value(internal.KeyGVR).(string)
if !ok {
return nil, fmt.Errorf("expecting a context gvr")
}
r.gvr = gvr
path, ok := ctx.Value(internal.KeyPath).(string)
log.Debug().Msgf("LISTING RBACK %q--%q", r.gvr, path)
if !ok || path == "" {
return r.Resource.List(ctx)
}
switch k8s.GVR(r.gvr).ToR() {
case "clusterrolebindings":
return r.loadClusterRoleBinding(path)
case "rolebindings":
return r.loadRoleBinding(path)
case "clusterroles":
return r.loadClusterRole(path)
case "roles":
return r.loadRole(path)
default:
return nil, fmt.Errorf("expecting clusterrole/role but found %s", k8s.GVR(r.gvr).ToR())
}
}
func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
o, err := r.factory.Get("rbac.authorization.k8s.io/v1/clusterrolebindings", path, labels.Everything())
if err != nil {
return nil, err
}
var crb rbacv1.ClusterRoleBinding
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb)
if err != nil {
return nil, err
}
kind := "rbac.authorization.k8s.io/v1/clusterroles"
crbo, err := r.factory.Get(kind, k8s.FQN("-", crb.RoleRef.Name), labels.Everything())
if err != nil {
return nil, err
}
var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(crbo.(*unstructured.Unstructured).Object, &cr)
if err != nil {
return nil, err
}
return r.parseRules(cr.Rules), nil
}
func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
o, err := r.factory.Get("rbac.authorization.k8s.io/v1/rolebindings", path, labels.Everything())
if err != nil {
return nil, err
}
var rb rbacv1.RoleBinding
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb)
if err != nil {
return nil, err
}
if rb.RoleRef.Kind == "ClusterRole" {
kind := "rbac.authorization.k8s.io/v1/clusterroles"
o, err := r.factory.Get(kind, k8s.FQN("-", rb.RoleRef.Name), labels.Everything())
if err != nil {
return nil, err
}
var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr)
if err != nil {
return nil, err
}
return r.parseRules(cr.Rules), nil
}
kind := "rbac.authorization.k8s.io/v1/roles"
ro, err := r.factory.Get(kind, k8s.FQN(rb.Namespace, rb.RoleRef.Name), labels.Everything())
if err != nil {
return nil, err
}
var role rbacv1.Role
err = runtime.DefaultUnstructuredConverter.FromUnstructured(ro.(*unstructured.Unstructured).Object, &role)
if err != nil {
return nil, err
}
return r.parseRules(role.Rules), nil
}
func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) {
o, err := r.factory.Get("rbac.authorization.k8s.io/v1/clusterroles", path, labels.Everything())
if err != nil {
return nil, err
}
var cr rbacv1.ClusterRole
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr)
if err != nil {
return nil, err
}
return r.parseRules(cr.Rules), nil
}
func (r *Rbac) loadRole(path string) ([]runtime.Object, error) {
o, err := r.factory.Get("rbac.authorization.k8s.io/v1/roles", path, labels.Everything())
if err != nil {
return nil, err
}
var ro rbacv1.Role
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro)
if err != nil {
return nil, err
}
return r.parseRules(ro.Rules), nil
}
func makeRes(res, grp string, vv []string) *render.PolicyRes {
return &render.PolicyRes{
Resource: res,
Group: grp,
Verbs: vv,
}
}
func (r *Rbac) parseRules(rules []rbacv1.PolicyRule) []runtime.Object {
m := make([]runtime.Object, 0, len(rules))
for _, rule := range rules {
for _, grp := range rule.APIGroups {
for _, res := range rule.Resources {
k := res
if grp != "" {
k = res + "." + grp
}
for _, na := range rule.ResourceNames {
m = upsert(m, makeRes(FQN(k, na), grp, rule.Verbs))
}
m = upsert(m, makeRes(k, grp, rule.Verbs))
}
}
for _, nres := range rule.NonResourceURLs {
if nres[0] != '/' {
nres = "/" + nres
}
m = upsert(m, makeRes(nres, "", rule.Verbs))
}
}
return m
}
func upsert(rr []runtime.Object, p *render.PolicyRes) []runtime.Object {
idx, ok := find(rr, p.Resource)
if !ok {
return append(rr, p)
}
rr[idx] = p
return rr
}
// Find locates a row by id. Retturns false is not found.
func find(rr []runtime.Object, res string) (int, bool) {
for i, r := range rr {
p := r.(*render.PolicyRes)
if p.Resource == res {
return i, true
}
}
return 0, false
}

View File

@ -6,6 +6,7 @@ import (
// BOZO!! Break up deps and merge into single registrar
var Registry = map[string]ResourceMeta{
// Custom...
"containers": ResourceMeta{
Model: &Container{},
Renderer: &render.Container{},
@ -18,7 +19,24 @@ var Registry = map[string]ResourceMeta{
Model: &ScreenDump{},
Renderer: &render.ScreenDump{},
},
"rbac": ResourceMeta{
Model: &Rbac{},
Renderer: &render.Rbac{},
},
"portforwards": ResourceMeta{
Model: &PortForward{},
Renderer: &render.PortForward{},
},
"benchmarks": ResourceMeta{
Model: &Benchmark{},
Renderer: &render.Benchmark{},
},
"aliases": ResourceMeta{
Model: &Alias{},
Renderer: &render.Alias{},
},
// Core...
"v1/pods": ResourceMeta{
Model: &Pod{},
Renderer: &render.Pod{},
@ -30,7 +48,20 @@ var Registry = map[string]ResourceMeta{
"v1/namespaces": ResourceMeta{
Renderer: &render.Namespace{},
},
"v1/endpoints": ResourceMeta{
Renderer: &render.Endpoints{},
},
"v1/services": ResourceMeta{
Renderer: &render.Service{},
},
"v1/configmaps": ResourceMeta{
Renderer: &render.ConfigMap{},
},
"v1/secrets": ResourceMeta{
Renderer: &render.Secret{},
},
// Apps...
"apps/v1/deployments": ResourceMeta{
Renderer: &render.Deployment{},
},
@ -43,31 +74,32 @@ var Registry = map[string]ResourceMeta{
"apps/v1/daemonsets": ResourceMeta{
Renderer: &render.DaemonSet{},
},
// Extensions...
"extensions/v1beta1/daemonsets": ResourceMeta{
Renderer: &render.DaemonSet{},
},
"extensions/v1beta1/ingresses": ResourceMeta{
Renderer: &render.Ingress{},
},
// "v1/services": ResourceMeta{
// Renderer: &render.Service{},
// },
// "v1/configmaps": ResourceMeta{
// Renderer: &render.ConfigMap{},
// },
// "v1/secrets": ResourceMeta{
// Renderer: &render.ConfigMap{},
// },
// "batch/v1beta1/cronjobs": ResourceMeta{
// Renderer: &render.CronJob{},
// },
// "batch/v1/jobs": ResourceMeta{
// Renderer: &render.Job{},
// },
// Batch...
"batch/v1beta1/cronjobs": ResourceMeta{
Renderer: &render.CronJob{},
},
"batch/v1/jobs": ResourceMeta{
Model: &Job{},
Renderer: &render.Job{},
},
// CRDs...
"apiextensions.k8s.io/v1beta1/customresourcedefinitions": ResourceMeta{
Renderer: &render.CustomResourceDefinition{},
},
// RBAC...
"rbac.authorization.k8s.io/v1/clusterroles": ResourceMeta{
Model: &Rbac{},
Renderer: &render.ClusterRole{},
},
"rbac.authorization.k8s.io/v1/clusterrolebindings": ResourceMeta{

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
@ -28,8 +27,8 @@ func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) {
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {
lsel = sel.AsSelector()
}
oo, err := r.factory.List(r.namespace, r.gvr, lsel)
log.Debug().Msgf("^^^^^Listing with selector %q:%q--%#v", r.namespace, r.gvr, lsel)
oo, err := r.factory.List(r.gvr, r.namespace, lsel)
r.factory.WaitForCacheSync()
return oo, err
@ -41,9 +40,8 @@ func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) err
var index int
for _, o := range oo {
res := o.(*unstructured.Unstructured)
var row render.Row
if err := re.Render(res, r.namespace, &row); err != nil {
if err := re.Render(o, r.namespace, &row); err != nil {
return err
}
rr[index] = row

View File

@ -3,25 +3,22 @@ package model
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// ScreenDump represents a container model.
// ScreenDump represents a collections of screendumps.
type ScreenDump struct {
Resource
pod *v1.Pod
}
// List returns a collection of containers
// List returns a collection of screen dumps.
func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
dir, ok := ctx.Value(internal.KeyDir).(string)
if !ok {
@ -35,7 +32,7 @@ func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
oo := make([]runtime.Object, len(ff))
for i, f := range ff {
oo[i] = FileRes{file: f, dir: dir}
oo[i] = render.FileRes{File: f, Dir: dir}
}
return oo, nil
@ -44,38 +41,9 @@ func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
// Hydrate returns a pod as container rows.
func (c *ScreenDump) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
res, ok := o.(FileRes)
if !ok {
return fmt.Errorf("expecting a file resource but got %T", o)
}
if err := re.Render(res, render.NonResource, &rr[i]); err != nil {
if err := re.Render(o, render.NonResource, &rr[i]); err != nil {
return err
}
}
return nil
}
// ----------------------------------------------------------------------------
// FileRes represents a file resource.
type FileRes struct {
file os.FileInfo
dir string
}
func (c FileRes) GetFile() os.FileInfo { return c.file }
func (c FileRes) GetDir() string { return c.dir }
// GetObjectKind returns a schema object.
func (c FileRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (c FileRes) DeepCopyObject() runtime.Object {
return c
}

View File

@ -106,12 +106,6 @@ func (s *Stack) Pop() (Component, bool) {
c := s.components[s.size()]
s.components = s.components[:s.size()]
s.notify(StackPop, c)
c.Stop()
if top := s.Top(); top != nil {
log.Debug().Msgf("Calling Start on %s", top.Name())
top.Start()
}
return c, true
}

133
internal/model/subject.go Normal file
View File

@ -0,0 +1,133 @@
package model
import (
"context"
"errors"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Subject represents a subject model.
type Subject struct {
Resource
subjectKind string
}
// List returns a collection of subjects.
func (s *Subject) List(ctx context.Context) ([]runtime.Object, error) {
var ok bool
s.subjectKind, ok = ctx.Value(internal.KeySubject).(string)
if !ok {
return nil, errors.New("expecting a subject")
}
crbs, err := s.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything())
if err != nil {
return nil, err
}
rbs, err := s.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/rolebindings", labels.Everything())
if err != nil {
return nil, err
}
return append(crbs, rbs...), nil
}
// Hydrate returns a pod as container rows.
func (s *Subject) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo {
res, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expecting unstructured but got %T", o)
}
if err := re.Render(res, render.AllNamespaces, &rr[i]); err != nil {
return err
}
}
return nil
}
func (s *Subject) fetchClusterRoleBindings() ([]runtime.Object, error) {
oo, err := s.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything())
if err != nil {
return nil, err
}
rows := make([]runtime.Object, 0, len(oo))
for _, o := range oo {
var crb rbacv1.ClusterRoleBinding
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb)
if err != nil {
return nil, err
}
for _, subject := range crb.Subjects {
if subject.Kind != s.subjectKind {
continue
}
rows = append(rows, SubjectRes{
id: subject.Name,
fields: render.Fields{subject.Name, "ClusterRoleBinding", crb.Name},
})
}
}
return rows, nil
}
func (s *Subject) fetchRoleBindings() ([]runtime.Object, error) {
oo, err := s.factory.List(render.ClusterWide, "rbac.authorization.k8s.io/v1/rolebindings", labels.Everything())
if err != nil {
return nil, err
}
rows := make([]runtime.Object, 0, len(oo))
for _, o := range oo {
var rb rbacv1.RoleBinding
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb)
if err != nil {
return nil, err
}
for _, subject := range rb.Subjects {
if subject.Kind == s.subjectKind {
rows = append(rows, SubjectRes{
id: subject.Name,
fields: render.Fields{subject.Name, "RoleBinding", rb.Name},
})
}
}
}
return rows, nil
}
// ----------------------------------------------------------------------------
// SubjectRes represents a subject resource.
type SubjectRes struct {
id string
fields render.Fields
}
func (s SubjectRes) GetID() string { return s.id }
func (s SubjectRes) GetFields() render.Fields { return s.fields }
// GetObjectKind returns a schema object.
func (s SubjectRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (s SubjectRes) DeepCopyObject() runtime.Object {
return s
}

View File

@ -5,6 +5,7 @@ import (
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/derailed/tview"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
@ -69,45 +70,24 @@ type Lister interface {
Hydrate([]runtime.Object, render.Rows, Renderer) error
}
// BOZO!!
// type Connection interface {
// // DialOrDie dials client api.
// DialOrDie() kubernetes.Interface
// // MXDial dials metrics api.
// MXDial() (*versioned.Clientset, error)
// // DynDialOrDie dials dynamic client api.
// DynDialOrDie() dynamic.Interface
// // RestConfigOrDie return a client configuration.
// RestConfigOrDie() *restclient.Config
// // Config returns the current kubeconfig.
// Config() *k8s.Config
// // CachedDiscovery returns a cached client.
// CachedDiscovery() (*disk.CachedDiscoveryClient, error)
// // SwithContextOrDie switch to a new kube context.
// SwitchContextOrDie(ctx string)
// }
type Factory interface {
// Client retrieves an api client.
Client() k8s.Connection
// Get fetch a given resource.
Get(ns, gvr, n string, sel labels.Selector) (runtime.Object, error)
Get(gvr, path string, sel labels.Selector) (runtime.Object, error)
// List fetch a collection of resources.
List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error)
List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error)
// ForResource fetch an informer for a given resource.
ForResource(ns, gvr string) informers.GenericInformer
// WaitForCacheSync synchronize the cache.
WaitForCacheSync() map[schema.GroupVersionResource]bool
// Forwards returns all portforwards.
Forwarders() watch.Forwarders
}
// ResourceMeta represents model info about a resource.

View File

@ -6,6 +6,8 @@ import (
"github.com/derailed/k9s/internal/k8s"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Alias renders a aliases to screen.
@ -24,25 +26,24 @@ func (Alias) Header(ns string) HeaderRow {
Header{Name: "RESOURCE"},
Header{Name: "COMMAND"},
Header{Name: "APIGROUP"},
// Header{Name: "AGE", Decorator: ageDecorator},
}
}
// Render renders a K8s resource to screen.
func (Alias) Render(o interface{}, gvr string, r *Row) error {
aliases, ok := o.([]string)
a, ok := o.(AliasRes)
if !ok {
return fmt.Errorf("Expected Alias, but got %T", o)
return fmt.Errorf("expected aliasres, but got %T", o)
}
g := k8s.GVR(gvr)
r.ID = string(gvr)
g := k8s.GVR(a.GVR)
r.ID = string(g)
r.Fields = Fields{
g.ToR(),
strings.Join(aliases, ","),
strings.Join(a.Aliases, ","),
g.ToG(),
// Pad(g.ToR(), 30),
// Pad(strings.Join(aliases, ","), 70),
// Pad(g.ToG(), 30),
// time.Now().String(),
}
return nil
@ -50,15 +51,18 @@ func (Alias) Render(o interface{}, gvr string, r *Row) error {
// Helpers...
// Pad a string up to the given length or truncates if greater than length.
func Pad(s string, width int) string {
if len(s) == width {
return s
}
if len(s) > width {
return Truncate(s, width)
}
return s + strings.Repeat(" ", width-len(s))
// AliasRes represents an alias resource.
type AliasRes struct {
GVR string
Aliases []string
}
// GetObjectKind returns a schema object.
func (AliasRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (a AliasRes) DeepCopyObject() runtime.Object {
return a
}

View File

@ -7,12 +7,13 @@ import (
"regexp"
"strconv"
"strings"
"time"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"golang.org/x/text/language"
"golang.org/x/text/message"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
@ -23,17 +24,11 @@ var (
toastRx = regexp.MustCompile(`Error distribution`)
)
// BenchInfo represents benchmark run info.
type BenchInfo struct {
File os.FileInfo
Path string
}
// Bench renders a benchmarks to screen.
type Bench struct{}
// Benchmark renders a benchmarks to screen.
type Benchmark struct{}
// ColorerFunc colors a resource row.
func (Bench) ColorerFunc() ColorerFunc {
func (Benchmark) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := tcell.ColorPaleGreen
statusCol := 2
@ -45,25 +40,25 @@ func (Bench) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Bench) Header(ns string) HeaderRow {
func (Benchmark) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAMESPACE", Align: tview.AlignLeft},
Header{Name: "NAME", Align: tview.AlignLeft},
Header{Name: "STATUS", Align: tview.AlignLeft},
Header{Name: "TIME", Align: tview.AlignLeft},
Header{Name: "NAMESPACE"},
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "TIME"},
Header{Name: "REQ/S", Align: tview.AlignRight},
Header{Name: "2XX", Align: tview.AlignRight},
Header{Name: "4XX/5XX", Align: tview.AlignRight},
Header{Name: "REPORT", Align: tview.AlignLeft},
Header{Name: "AGE", Align: tview.AlignLeft},
Header{Name: "REPORT"},
Header{Name: "AGE", Decorator: ageDecorator},
}
}
// Render renders a K8s resource to screen.
func (b Bench) Render(o interface{}, ns string, r *Row) error {
func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
bench, ok := o.(BenchInfo)
if !ok {
return fmt.Errorf("Expected string, but got %T", o)
return fmt.Errorf("expecting benchinfo but got `%T", o)
}
data, err := b.readFile(bench.Path)
@ -71,19 +66,20 @@ func (b Bench) Render(o interface{}, ns string, r *Row) error {
return fmt.Errorf("Unable to load bench file %s", bench.Path)
}
r.ID = bench.Path
r.Fields = make(Fields, len(b.Header(ns)))
if err := b.initRow(r.Fields, bench.File); err != nil {
return err
}
b.augmentRow(r.Fields, data)
r.ID = bench.Path
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func (Bench) readFile(file string) (string, error) {
func (Benchmark) readFile(file string) (string, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return "", err
@ -91,7 +87,7 @@ func (Bench) readFile(file string) (string, error) {
return string(data), nil
}
func (Bench) initRow(row Fields, f os.FileInfo) error {
func (Benchmark) initRow(row Fields, f os.FileInfo) error {
tokens := strings.Split(f.Name(), "_")
if len(tokens) < 2 {
return fmt.Errorf("Invalid file name %s", f.Name())
@ -99,12 +95,12 @@ func (Bench) initRow(row Fields, f os.FileInfo) error {
row[0] = tokens[0]
row[1] = tokens[1]
row[7] = f.Name()
row[8] = time.Since(f.ModTime()).String()
row[8] = timeToAge(f.ModTime())
return nil
}
func (b Bench) augmentRow(fields Fields, data string) {
func (b Benchmark) augmentRow(fields Fields, data string) {
if len(data) == 0 {
return
}
@ -137,7 +133,7 @@ func (b Bench) augmentRow(fields Fields, data string) {
fields[col] = b.countReq(me)
}
func (Bench) countReq(rr [][]string) string {
func (Benchmark) countReq(rr [][]string) string {
if len(rr) == 0 {
return "0"
}
@ -156,3 +152,19 @@ func asNum(n int) string {
p := message.NewPrinter(language.English)
return p.Sprintf("%d", n)
}
// BenchInfo represents benchmark run info.
type BenchInfo struct {
File os.FileInfo
Path string
}
// GetObjectKind returns a schema object.
func (BenchInfo) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (b BenchInfo) DeepCopyObject() runtime.Object {
return b
}

View File

@ -3,6 +3,7 @@ package render
import (
"fmt"
"github.com/derailed/k9s/internal/k8s"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -28,7 +29,7 @@ func (ClusterRole) Header(string) HeaderRow {
func (ClusterRole) Render(o interface{}, ns string, r *Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected ClusterRole, but got %T", o)
return fmt.Errorf("expecting clusterrole, but got %T", o)
}
var cr rbacv1.ClusterRole
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cr)
@ -36,12 +37,11 @@ func (ClusterRole) Render(o interface{}, ns string, r *Row) error {
return err
}
fields := make(Fields, 0, len(r.Fields))
fields = append(fields,
r.ID = k8s.FQN("-", cr.ObjectMeta.Name)
r.Fields = Fields{
cr.Name,
toAge(cr.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(cr.ObjectMeta), fields
}
return nil
}

View File

@ -3,6 +3,7 @@ package render
import (
"fmt"
"github.com/derailed/k9s/internal/k8s"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -20,7 +21,7 @@ func (ClusterRoleBinding) ColorerFunc() ColorerFunc {
func (ClusterRoleBinding) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "ROLE"},
Header{Name: "CLUSTERROLE"},
Header{Name: "KIND"},
Header{Name: "SUBJECTS"},
Header{Name: "AGE", Decorator: ageDecorator},
@ -41,15 +42,14 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
kind, ss := renderSubjects(crb.Subjects)
fields := make(Fields, 0, len(r.Fields))
fields = append(fields,
r.ID = k8s.FQN("-", crb.ObjectMeta.Name)
r.Fields = Fields{
crb.Name,
crb.RoleRef.Name,
kind,
ss,
toAge(crb.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(crb.ObjectMeta), fields
}
return nil
}

View File

@ -38,13 +38,11 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error {
log.Error().Err(err).Msgf("Fields timestamp %v", err)
}
fields := make(Fields, 0, len(r.Fields))
fields = append(fields,
r.ID = FQN(ClusterWide, meta["name"].(string))
r.Fields = Fields{
meta["name"].(string),
toAge(metav1.Time{t}),
)
r.ID, r.Fields = FQN("", meta["name"].(string)), fields
}
return nil
}

View File

@ -35,7 +35,7 @@ func (CronJob) Header(ns string) HeaderRow {
}
// Render renders a K8s resource to screen.
func (CronJob) Render(o interface{}, ns string, r *Row) error {
func (c CronJob) Render(o interface{}, ns string, r *Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected CronJob, but got %T", o)
@ -51,11 +51,12 @@ func (CronJob) Render(o interface{}, ns string, r *Row) error {
lastScheduled = toAgeHuman(toAge(*cj.Status.LastScheduleTime))
}
fields := make(Fields, 0, len(r.Fields))
r.ID = MetaFQN(cj.ObjectMeta)
r.Fields = make(Fields, 0, len(c.Header(ns)))
if isAllNamespace(ns) {
fields = append(fields, cj.Namespace)
r.Fields = append(r.Fields, cj.Namespace)
}
fields = append(fields,
r.Fields = append(r.Fields,
cj.Name,
cj.Spec.Schedule,
boolPtrToStr(cj.Spec.Suspend),
@ -64,7 +65,5 @@ func (CronJob) Render(o interface{}, ns string, r *Row) error {
toAge(cj.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(cj.ObjectMeta), fields
return nil
}

View File

@ -1,18 +1,18 @@
package render
import "github.com/rs/zerolog/log"
// DeltaRow represents a collection of row detlas between old and new row.
type DeltaRow []string
// NewDeltaRow computes the delta between 2 rows.
func NewDeltaRow(o, n Row) DeltaRow {
func NewDeltaRow(o, n Row, excludeLast bool) DeltaRow {
deltas := make(DeltaRow, len(o.Fields))
// Exclude age col
oldFields := o.Fields[:len(o.Fields)-1]
if !excludeLast {
oldFields = o.Fields[:len(o.Fields)]
}
for i, old := range oldFields {
if old != "" && old != n.Fields[i] {
log.Debug().Msgf("OLD VS NEW %q:%q", old, n.Fields[i])
deltas[i] = old
}
}

View File

@ -7,9 +7,7 @@ import (
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
@ -17,10 +15,6 @@ import (
// Deployment renders a K8s Deployment to screen.
type Deployment struct{}
func isAllNamespace(ns string) bool {
return ns == ""
}
// ColorerFunc colors a resource row.
func (Deployment) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
@ -54,7 +48,6 @@ func (Deployment) Header(ns string) HeaderRow {
Header{Name: "READY"},
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "SELECTOR"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}
@ -81,21 +74,8 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)),
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
strconv.Itoa(int(dp.Status.AvailableReplicas)),
asSelector(dp.Spec.Selector),
toAge(dp.ObjectMeta.CreationTimestamp),
)
return nil
}
//Helpers...
func asSelector(s *metav1.LabelSelector) string {
sel, err := metav1.LabelSelectorAsSelector(s)
if err != nil {
log.Error().Err(err).Msg("Selector conversion failed")
return NAValue
}
return sel.String()
}

View File

@ -49,7 +49,6 @@ func (DaemonSet) Header(ns string) HeaderRow {
Header{Name: "READY", Align: tview.AlignRight},
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "NODE_SELECTOR"},
Header{Name: "AGE", Decorator: ageDecorator},
)
}
@ -78,7 +77,6 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
strconv.Itoa(int(ds.Status.NumberReady)),
strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),
strconv.Itoa(int(ds.Status.NumberAvailable)),
mapToStr(ds.Spec.Template.Spec.NodeSelector),
toAge(ds.ObjectMeta.CreationTimestamp),
)

View File

@ -224,16 +224,17 @@ type ColorerFunc func(ns string, evt RowEvent) tcell.Color
// DefaultColorer set the default table row colors.
func DefaultColorer(ns string, evt RowEvent) tcell.Color {
var col = StdColor
switch evt.Kind {
case EventAdd:
return AddColor
col = AddColor
case EventUpdate:
return ModColor
col = ModColor
case EventDelete:
return KillColor
default:
return StdColor
col = KillColor
}
return col
}
type StringSet []string

View File

@ -26,8 +26,11 @@ func (Generic) ColorerFunc() ColorerFunc {
// Header returns a header row.
func (g *Generic) Header(ns string) HeaderRow {
h := make(HeaderRow, 0, len(g.table.ColumnDefinitions))
if g.table == nil {
return HeaderRow{}
}
h := make(HeaderRow, 0, len(g.table.ColumnDefinitions))
if ns == "" {
h = append(h, Header{Name: "NAMESPACE"})
}

View File

@ -9,6 +9,7 @@ import (
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/apimachinery/pkg/watch"
@ -26,6 +27,20 @@ const (
NAValue = "n/a"
)
func asSelector(s *metav1.LabelSelector) string {
sel, err := metav1.LabelSelectorAsSelector(s)
if err != nil {
log.Error().Err(err).Msg("Selector conversion failed")
return NAValue
}
return sel.String()
}
func isAllNamespace(ns string) bool {
return ns == AllNamespaces
}
type metric struct {
cpu, mem string
}
@ -217,3 +232,16 @@ func in(ll []string, s string) bool {
}
return false
}
// Pad a string up to the given length or truncates if greater than length.
func Pad(s string, width int) string {
if len(s) == width {
return s
}
if len(s) > width {
return Truncate(s, width)
}
return s + strings.Repeat(" ", width-len(s))
}

View File

@ -35,7 +35,7 @@ func (Ingress) Header(ns string) HeaderRow {
}
// Render renders a K8s resource to screen.
func (Ingress) Render(o interface{}, ns string, r *Row) error {
func (i Ingress) Render(o interface{}, ns string, r *Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Ingress, but got %T", o)
@ -46,11 +46,12 @@ func (Ingress) Render(o interface{}, ns string, r *Row) error {
return err
}
fields := make(Fields, 0, len(r.Fields))
r.ID = MetaFQN(ing.ObjectMeta)
r.Fields = make(Fields, 0, len(i.Header(ns)))
if isAllNamespace(ns) {
fields = append(fields, ing.Namespace)
r.Fields = append(r.Fields, ing.Namespace)
}
fields = append(fields,
r.Fields = append(r.Fields,
ing.Name,
toHosts(ing.Spec.Rules),
toAddress(ing.Status.LoadBalancer),
@ -58,8 +59,6 @@ func (Ingress) Render(o interface{}, ns string, r *Row) error {
toAge(ing.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(ing.ObjectMeta), fields
return nil
}
@ -89,15 +88,13 @@ func toTLSPorts(tls []v1beta1.IngressTLS) string {
}
func toHosts(rr []v1beta1.IngressRule) string {
var s string
var i int
hh := make([]string, 0, len(rr))
for _, r := range rr {
s += r.Host
if i < len(rr)-1 {
s += ","
if r.Host == "" {
r.Host = "*"
}
i++
hh = append(hh, r.Host)
}
return s
return strings.Join(hh, ",")
}

View File

@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -39,37 +40,37 @@ func (Job) Header(ns string) HeaderRow {
}
// Render renders a K8s resource to screen.
func (Job) Render(o interface{}, ns string, r *Row) error {
func (j Job) Render(o interface{}, ns string, r *Row) error {
log.Debug().Msgf("JOB RENDER %q", ns)
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Job, but got %T", o)
}
var j batchv1.Job
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &j)
var job batchv1.Job
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job)
if err != nil {
return err
}
cc, ii := toContainers(j.Spec.Template.Spec)
fields := make(Fields, 0, len(r.Fields))
r.ID = MetaFQN(job.ObjectMeta)
r.Fields = make(Fields, 0, len(j.Header(ns)))
if isAllNamespace(ns) {
fields = append(fields, j.Namespace)
r.Fields = append(r.Fields, job.Namespace)
}
fields = append(fields,
j.Name,
toCompletion(j.Spec, j.Status),
toDuration(j.Status),
cc, ii := toContainers(job.Spec.Template.Spec)
r.Fields = append(r.Fields,
job.Name,
toCompletion(job.Spec, job.Status),
toDuration(job.Status),
cc,
ii,
toAge(j.ObjectMeta.CreationTimestamp),
toAge(job.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(j.ObjectMeta), fields
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
const maxShow = 2

View File

@ -17,10 +17,13 @@ type Namespace struct{}
func (Namespace) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
c := DefaultColorer(ns, r)
if r.Kind == EventAdd || r.Kind == EventUpdate {
if r.Kind == EventAdd {
return c
}
if r.Kind == EventUpdate {
c = StdColor
}
switch strings.TrimSpace(r.Row.Fields[1]) {
case "Inactive", Terminating:
c = ErrColor
@ -54,13 +57,12 @@ func (Namespace) Render(o interface{}, _ string, r *Row) error {
return err
}
fields := make(Fields, 0, len(r.Fields))
fields = append(fields,
r.ID = MetaFQN(ns.ObjectMeta)
r.Fields = Fields{
ns.Name,
string(ns.Status.Phase),
toAge(ns.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(ns.ObjectMeta), fields
}
return nil
}

View File

@ -5,6 +5,8 @@ import (
"strings"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Forwarder represents a port forwarder.
@ -25,18 +27,18 @@ type Forwarder interface {
Age() string
}
// Forward renders a portforwards to screen.
type Forward struct{}
// PortForward renders a portforwards to screen.
type PortForward struct{}
// ColorerFunc colors a resource row.
func (Forward) ColorerFunc() ColorerFunc {
func (PortForward) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return tcell.ColorSkyblue
}
}
// Header returns a header row.
func (Forward) Header(ns string) HeaderRow {
func (PortForward) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAMESPACE"},
Header{Name: "NAME"},
@ -50,10 +52,10 @@ func (Forward) Header(ns string) HeaderRow {
}
// Render renders a K8s resource to screen.
func (f Forward) Render(o interface{}, gvr string, r *Row) error {
pf, ok := o.(PortForwarder)
func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
pf, ok := o.(ForwardRes)
if !ok {
return fmt.Errorf("expecting a portforward but got %T", o)
return fmt.Errorf("expecting a ForwardRes but got %T", o)
}
ports := strings.Split(pf.Ports()[0], ":")
@ -65,9 +67,9 @@ func (f Forward) Render(o interface{}, gvr string, r *Row) error {
na,
pf.Container(),
strings.Join(pf.Ports(), ","),
UrlFor(pf.Host(), pf.HttpPath(), ports[0]),
asNum(pf.C()),
asNum(pf.N()),
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
asNum(pf.Config.C),
asNum(pf.Config.N),
pf.Age(),
}
@ -76,26 +78,27 @@ func (f Forward) Render(o interface{}, gvr string, r *Row) error {
// Helpers...
type PortForwarder interface {
Forwarder
BenchConfigurator
}
// type PortForwarder interface {
// Forwarder
// BenchConfigurator
// }
type BenchConfigurators map[string]BenchConfigurator
// type BenchConfigurators map[string]BenchConfigurator
type BenchConfigurator interface {
// C returns the number of concurent connections.
C() int
// BOZO!!
// type BenchConfigurator interface {
// // C returns the number of concurent connections.
// C() int
// N returns the number of requests.
N() int
// // N returns the number of requests.
// N() int
// Host returns the forward host address.
Host() string
// // Host returns the forward host address.
// Host() string
// Path returns the http path.
HttpPath() string
}
// // Path returns the http path.
// HttpPath() string
// }
// UrlFor computes fq url for a given benchmark configuration.
func UrlFor(host, path, port string) string {
@ -108,3 +111,25 @@ func UrlFor(host, path, port string) string {
return "http://" + host + ":" + port + path
}
// BenchCfg represents a benchmark configuration.
type BenchCfg struct {
C, N int
Host, Path string
}
// ForwardRes represents a benchmark resource.
type ForwardRes struct {
Forwarder
Config BenchCfg
}
// GetObjectKind returns a schema object.
func (f ForwardRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (f ForwardRes) DeepCopyObject() runtime.Object {
return f
}

View File

@ -1,7 +1,32 @@
package render
import (
"fmt"
"strings"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const allVerbs = "*"
var (
k8sVerbs = []string{
"get",
"list",
"watch",
"create",
"patch",
"update",
"delete",
"deletecollection",
}
httpTok8sVerbs = map[string]string{
"post": "create",
"put": "update",
}
)
// Rbac renders a rbac to screen.
@ -26,8 +51,95 @@ func (Rbac) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen.
func (Rbac) Render(o interface{}, gvr string, r *Row) error {
panic("NYI")
p, ok := o.(*PolicyRes)
if !ok {
return fmt.Errorf("expecting policyres in renderer for %q", gvr)
}
if p.Group != "" {
p.Group = toGroup(p.Group)
} else {
p.Group = "core"
}
r.Fields = append(r.Fields, p.Resource, p.Group)
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
r.ID = p.Resource
return nil
}
// Helpers...
func asVerbs(verbs []string) []string {
const (
verbLen = 4
unknownLen = 30
)
r := make([]string, 0, len(k8sVerbs)+1)
for _, v := range k8sVerbs {
r = append(r, toVerbIcon(hasVerb(verbs, v)))
}
var unknowns []string
for _, v := range verbs {
if hv, ok := httpTok8sVerbs[v]; ok {
v = hv
}
if !hasVerb(k8sVerbs, v) && v != allVerbs {
unknowns = append(unknowns, v)
}
}
return append(r, Truncate(strings.Join(unknowns, ","), unknownLen))
}
func toVerbIcon(ok bool) string {
if ok {
return "[green::b] ✓ [::]"
}
return "[orangered::b] 𐄂 [::]"
}
func hasVerb(verbs []string, verb string) bool {
if len(verbs) == 1 && verbs[0] == allVerbs {
return true
}
for _, v := range verbs {
if hv, ok := httpTok8sVerbs[v]; ok {
if hv == verb {
return true
}
}
if v == verb {
return true
}
}
return false
}
func toGroup(g string) string {
if g == "" {
return "v1"
}
return g
}
type PolicyRes struct {
Resource, Group string
ResourceName string
NonResourceURL string
Verbs []string
}
// GetObjectKind returns a schema object.
func (p PolicyRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (p PolicyRes) DeepCopyObject() runtime.Object {
return p
}

View File

@ -30,7 +30,7 @@ func (Role) Header(ns string) HeaderRow {
}
// Render renders a K8s resource to screen.
func (Role) Render(o interface{}, ns string, r *Row) error {
func (r Role) Render(o interface{}, ns string, row *Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Role, but got %T", o)
@ -41,15 +41,15 @@ func (Role) Render(o interface{}, ns string, r *Row) error {
return err
}
fields := make(Fields, 0, len(r.Fields))
row.ID = MetaFQN(ro.ObjectMeta)
row.Fields = make(Fields, 0, len(r.Header(ns)))
if isAllNamespace(ns) {
fields = append(fields, ro.Namespace)
row.Fields = append(row.Fields, ro.Namespace)
}
fields = append(fields,
row.Fields = append(row.Fields,
ro.Name,
toAge(ro.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(ro.ObjectMeta), fields
return nil
}

View File

@ -34,7 +34,7 @@ func (RoleBinding) Header(ns string) HeaderRow {
}
// Render renders a K8s resource to screen.
func (RoleBinding) Render(o interface{}, ns string, r *Row) error {
func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected RoleBinding, but got %T", o)
@ -47,18 +47,18 @@ func (RoleBinding) Render(o interface{}, ns string, r *Row) error {
kind, ss := renderSubjects(rb.Subjects)
fields := make(Fields, 0, len(r.Fields))
row.ID = MetaFQN(rb.ObjectMeta)
row.Fields = make(Fields, 0, len(r.Header(ns)))
if isAllNamespace(ns) {
fields = append(fields, rb.Namespace)
row.Fields = append(row.Fields, rb.Namespace)
}
fields = append(fields,
row.Fields = append(row.Fields,
rb.Name,
rb.RoleRef.Name,
kind,
ss,
toAge(rb.ObjectMeta.CreationTimestamp),
)
r.ID, r.Fields = MetaFQN(rb.ObjectMeta), fields
return nil
}

View File

@ -7,6 +7,8 @@ import (
"vbom.ml/util/sortorder"
)
const ageCol = "AGE"
// Fields represents a collection of row fields.
type Fields []string
@ -29,7 +31,21 @@ type Header struct {
// HeaderRow represents a table header.
type HeaderRow []Header
// HasAge returns true if table has an age column.
func (h HeaderRow) HasAge() bool {
for _, r := range h {
if r.Name == ageCol {
return true
}
}
return false
}
func (h HeaderRow) AgeCol(col int) bool {
if !h.HasAge() {
return false
}
return col == len(h)-1
}

View File

@ -7,6 +7,8 @@ import (
"time"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// ScreenDump renders a screendumps to screen.
@ -35,27 +37,39 @@ func (ScreenDump) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen.
func (b ScreenDump) Render(o interface{}, ns string, r *Row) error {
f, ok := o.(ScreenDumper)
f, ok := o.(FileRes)
if !ok {
return fmt.Errorf("Expected string, but got %T", o)
return fmt.Errorf("expecting screendumper, but got %T", o)
}
r.ID = filepath.Join(f.GetDir(), f.GetFile().Name())
r.ID = filepath.Join(f.Dir, f.File.Name())
r.Fields = Fields{
f.GetFile().Name(),
timeToAge(f.GetFile().ModTime()),
f.File.Name(),
timeToAge(f.File.ModTime()),
}
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func timeToAge(timestamp time.Time) string {
return time.Since(timestamp).String()
}
type ScreenDumper interface {
GetFile() os.FileInfo
GetDir() string
// FileRes represents a file resource.
type FileRes struct {
File os.FileInfo
Dir string
}
// GetObjectKind returns a schema object.
func (c FileRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (c FileRes) DeepCopyObject() runtime.Object {
return c
}

View File

@ -2,18 +2,12 @@ package resource
import (
"bytes"
"context"
"errors"
"fmt"
"path"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
genericprinters "k8s.io/cli-runtime/pkg/printers"
"k8s.io/kubectl/pkg/describe"
@ -163,43 +157,44 @@ func (*Base) marshalObject(o runtime.Object) (string, error) {
return buff.String(), nil
}
func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error {
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
if !ok {
return fmt.Errorf("no factory in context for pod logs")
}
// BOZO!!
// func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error {
// f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
// if !ok {
// return fmt.Errorf("no factory in context for pod logs")
// }
ls, err := metav1.ParseToLabelSelector(toSelector(sel))
if err != nil {
return err
}
lsel, err := metav1.LabelSelectorAsSelector(ls)
if err != nil {
return err
}
inf := f.ForResource(opts.Namespace, "v1/pods")
pods, err := inf.Lister().List(lsel)
if err != nil {
return err
}
// ls, err := metav1.ParseToLabelSelector(toSelector(sel))
// if err != nil {
// return err
// }
// lsel, err := metav1.LabelSelectorAsSelector(ls)
// if err != nil {
// return err
// }
// inf := f.ForResource(opts.Namespace, "v1/pods")
// pods, err := inf.Lister().List(lsel)
// if err != nil {
// return err
// }
if len(pods) > 1 {
opts.MultiPods = true
}
pr := NewPod(b.Connection)
for _, p := range pods {
var po v1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(p.(*unstructured.Unstructured).Object, &po)
if err != nil {
// BOZO!!
panic(err)
}
if po.Status.Phase == v1.PodRunning {
opts.Namespace, opts.Name = po.Namespace, po.Name
if err := pr.PodLogs(ctx, c, opts); err != nil {
return err
}
}
}
return nil
}
// if len(pods) > 1 {
// opts.MultiPods = true
// }
// pr := NewPod(b.Connection)
// for _, p := range pods {
// var po v1.Pod
// err := runtime.DefaultUnstructuredConverter.FromUnstructured(p.(*unstructured.Unstructured).Object, &po)
// if err != nil {
// // BOZO!!
// panic(err)
// }
// if po.Status.Phase == v1.PodRunning {
// opts.Namespace, opts.Name = po.Namespace, po.Name
// if err := pr.PodLogs(ctx, c, opts); err != nil {
// return err
// }
// }
// }
// return nil
// }

View File

@ -1,6 +1,7 @@
package resource
// NewConfigMapList returns a new resource list.
func NewConfigMapList(c Connection, ns string) List {
return NewCustomList(c, true, "", "v1/configmaps")
}
// BOZO!!
// // NewConfigMapList returns a new resource list.
// func NewConfigMapList(c Connection, ns string) List {
// return NewCustomList(c, true, "", "v1/configmaps")
// }

View File

@ -1,264 +1,265 @@
package resource
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
// BOZO!!
// import (
// "context"
// "errors"
// "fmt"
// "strconv"
// "strings"
"github.com/derailed/k9s/internal/k8s"
v1 "k8s.io/api/core/v1"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
// "github.com/derailed/k9s/internal/k8s"
// v1 "k8s.io/api/core/v1"
// mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
// )
type (
// Container represents a container on a pod.
Container struct {
*Base
// type (
// // Container represents a container on a pod.
// Container struct {
// *Base
pod *v1.Pod
instance v1.Container
metrics *mv1beta1.PodMetrics
}
)
// pod *v1.Pod
// instance v1.Container
// metrics *mv1beta1.PodMetrics
// }
// )
// NewContainerList returns a collection of container.
func NewContainerList(c Connection, pod *v1.Pod) List {
return NewList(
NotNamespaced,
"containers",
NewContainer(c, pod),
0,
)
}
// // NewContainerList returns a collection of container.
// func NewContainerList(c Connection, pod *v1.Pod) List {
// return NewList(
// NotNamespaced,
// "containers",
// NewContainer(c, pod),
// 0,
// )
// }
// NewContainer returns a new set of containers.
func NewContainer(c Connection, pod *v1.Pod) *Container {
co := Container{
Base: &Base{Connection: c, Resource: k8s.NewPod(c)},
pod: pod,
}
co.Factory = &co
// // NewContainer returns a new set of containers.
// func NewContainer(c Connection, pod *v1.Pod) *Container {
// co := Container{
// Base: &Base{Connection: c, Resource: k8s.NewPod(c)},
// pod: pod,
// }
// co.Factory = &co
return &co
}
// return &co
// }
// New builds a new Container instance from a k8s resource.
func (r *Container) New(i interface{}) (Columnar, error) {
co := NewContainer(r.Connection, r.pod)
coi, ok := i.(v1.Container)
if !ok {
return nil, errors.New("Expecting a container resource")
}
co.instance = coi
co.path = r.namespacedName(r.pod.ObjectMeta) + ":" + co.instance.Name
// // New builds a new Container instance from a k8s resource.
// func (r *Container) New(i interface{}) (Columnar, error) {
// co := NewContainer(r.Connection, r.pod)
// coi, ok := i.(v1.Container)
// if !ok {
// return nil, errors.New("Expecting a container resource")
// }
// co.instance = coi
// co.path = r.namespacedName(r.pod.ObjectMeta) + ":" + co.instance.Name
return co, nil
}
// return co, nil
// }
// SetPodMetrics set the current k8s resource metrics on associated pod.
func (r *Container) SetPodMetrics(m *mv1beta1.PodMetrics) {
r.metrics = m
}
// // SetPodMetrics set the current k8s resource metrics on associated pod.
// func (r *Container) SetPodMetrics(m *mv1beta1.PodMetrics) {
// r.metrics = m
// }
// Marshal resource to yaml.
func (r *Container) Marshal(path string) (string, error) {
return "", nil
}
// // Marshal resource to yaml.
// func (r *Container) Marshal(path string) (string, error) {
// return "", nil
// }
// Logs tails a given container logs
func (r *Container) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
res, ok := r.Resource.(k8s.Loggable)
if !ok {
return fmt.Errorf("Resource %T is not Loggable", r.Resource)
}
// // Logs tails a given container logs
// func (r *Container) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
// res, ok := r.Resource.(k8s.Loggable)
// if !ok {
// return fmt.Errorf("Resource %T is not Loggable", r.Resource)
// }
return tailLogs(ctx, res, c, opts)
}
// return tailLogs(ctx, res, c, opts)
// }
// List resources for a given namespace.
func (r *Container) List(ctx context.Context, ns string) (Columnars, error) {
icos := r.pod.Spec.InitContainers
cos := r.pod.Spec.Containers
// // List resources for a given namespace.
// func (r *Container) List(ctx context.Context, ns string) (Columnars, error) {
// icos := r.pod.Spec.InitContainers
// cos := r.pod.Spec.Containers
cc := make(Columnars, 0, len(icos)+len(cos))
for _, co := range icos {
res, err := r.New(co)
if err != nil {
return nil, err
}
cc = append(cc, res)
}
for _, co := range cos {
res, err := r.New(co)
if err != nil {
return nil, err
}
cc = append(cc, res)
}
// cc := make(Columnars, 0, len(icos)+len(cos))
// for _, co := range icos {
// res, err := r.New(co)
// if err != nil {
// return nil, err
// }
// cc = append(cc, res)
// }
// for _, co := range cos {
// res, err := r.New(co)
// if err != nil {
// return nil, err
// }
// cc = append(cc, res)
// }
return cc, nil
}
// return cc, nil
// }
// Header return resource header.
func (*Container) Header(ns string) Row {
return append(Row{},
"NAME",
"IMAGE",
"READY",
"STATE",
"RS",
"PROBES(L:R)",
"CPU",
"MEM",
"%CPU",
"%MEM",
"PORTS",
"AGE",
)
}
// // Header return resource header.
// func (*Container) Header(ns string) Row {
// return append(Row{},
// "NAME",
// "IMAGE",
// "READY",
// "STATE",
// "RS",
// "PROBES(L:R)",
// "CPU",
// "MEM",
// "%CPU",
// "%MEM",
// "PORTS",
// "AGE",
// )
// }
// NumCols designates if column is numerical.
func (*Container) NumCols(n string) map[string]bool {
return map[string]bool{
"CPU": true,
"MEM": true,
"%CPU": true,
"%MEM": true,
"RS": true,
}
}
// // NumCols designates if column is numerical.
// func (*Container) NumCols(n string) map[string]bool {
// return map[string]bool{
// "CPU": true,
// "MEM": true,
// "%CPU": true,
// "%MEM": true,
// "RS": true,
// }
// }
// Fields retrieves displayable fields.
func (r *Container) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
// // Fields retrieves displayable fields.
// func (r *Container) Fields(ns string) Row {
// ff := make(Row, 0, len(r.Header(ns)))
// i := r.instance
c, p := gatherMetrics(i, r.metrics)
// c, p := gatherMetrics(i, r.metrics)
ready, state, restarts := "false", MissingValue, "0"
cs := getContainerStatus(i.Name, r.pod.Status)
if cs != nil {
ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))
}
// ready, state, restarts := "false", MissingValue, "0"
// cs := getContainerStatus(i.Name, r.pod.Status)
// if cs != nil {
// ready, state, restarts = boolToStr(cs.Ready), toState(cs.State), strconv.Itoa(int(cs.RestartCount))
// }
return append(ff,
i.Name,
i.Image,
ready,
state,
restarts,
probe(i.LivenessProbe)+":"+probe(i.ReadinessProbe),
c.cpu,
c.mem,
p.cpu,
p.mem,
toStrPorts(i.Ports),
toAge(r.pod.CreationTimestamp),
)
}
// return append(ff,
// i.Name,
// i.Image,
// ready,
// state,
// restarts,
// probe(i.LivenessProbe)+":"+probe(i.ReadinessProbe),
// c.cpu,
// c.mem,
// p.cpu,
// p.mem,
// toStrPorts(i.Ports),
// toAge(r.pod.CreationTimestamp),
// )
// }
// ----------------------------------------------------------------------------
// Helpers...
// // ----------------------------------------------------------------------------
// // Helpers...
func gatherMetrics(co v1.Container, mx *mv1beta1.PodMetrics) (c, p metric) {
c, p = noMetric(), noMetric()
if mx == nil {
return
}
// func gatherMetrics(co v1.Container, mx *mv1beta1.PodMetrics) (c, p metric) {
// c, p = noMetric(), noMetric()
// if mx == nil {
// return
// }
var (
cpu int64
mem float64
)
for _, c := range mx.Containers {
if c.Name == co.Name {
cpu = c.Usage.Cpu().MilliValue()
mem = k8s.ToMB(c.Usage.Memory().Value())
break
}
}
c = metric{
cpu: ToMillicore(cpu),
mem: ToMi(mem),
}
// var (
// cpu int64
// mem float64
// )
// for _, c := range mx.Containers {
// if c.Name == co.Name {
// cpu = c.Usage.Cpu().MilliValue()
// mem = k8s.ToMB(c.Usage.Memory().Value())
// break
// }
// }
// c = metric{
// cpu: ToMillicore(cpu),
// mem: ToMi(mem),
// }
rcpu, rmem := containerResources(co)
if rcpu != nil {
p.cpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue())))
}
if rmem != nil {
p.mem = AsPerc(toPerc(mem, k8s.ToMB(rmem.Value())))
}
// rcpu, rmem := containerResources(co)
// if rcpu != nil {
// p.cpu = AsPerc(toPerc(float64(cpu), float64(rcpu.MilliValue())))
// }
// if rmem != nil {
// p.mem = AsPerc(toPerc(mem, k8s.ToMB(rmem.Value())))
// }
return
}
// return
// }
func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
for _, c := range status.ContainerStatuses {
if c.Name == co {
return &c
}
}
// func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
// for _, c := range status.ContainerStatuses {
// if c.Name == co {
// return &c
// }
// }
for _, c := range status.InitContainerStatuses {
if c.Name == co {
return &c
}
}
// for _, c := range status.InitContainerStatuses {
// if c.Name == co {
// return &c
// }
// }
return nil
}
// return nil
// }
func toStrPorts(pp []v1.ContainerPort) string {
ports := make([]string, len(pp))
for i, p := range pp {
if len(p.Name) > 0 {
ports[i] = p.Name + ":"
}
ports[i] += strconv.Itoa(int(p.ContainerPort))
if p.Protocol != "TCP" {
ports[i] += "" + string(p.Protocol)
}
}
// func toStrPorts(pp []v1.ContainerPort) string {
// ports := make([]string, len(pp))
// for i, p := range pp {
// if len(p.Name) > 0 {
// ports[i] = p.Name + ":"
// }
// ports[i] += strconv.Itoa(int(p.ContainerPort))
// if p.Protocol != "TCP" {
// ports[i] += "" + string(p.Protocol)
// }
// }
return strings.Join(ports, ",")
}
// return strings.Join(ports, ",")
// }
func toState(s v1.ContainerState) string {
switch {
case s.Waiting != nil:
if s.Waiting.Reason != "" {
return s.Waiting.Reason
}
return "Waiting"
// func toState(s v1.ContainerState) string {
// switch {
// case s.Waiting != nil:
// if s.Waiting.Reason != "" {
// return s.Waiting.Reason
// }
// return "Waiting"
case s.Terminated != nil:
if s.Terminated.Reason != "" {
return s.Terminated.Reason
}
return Terminating
case s.Running != nil:
return Running
default:
return MissingValue
}
}
// case s.Terminated != nil:
// if s.Terminated.Reason != "" {
// return s.Terminated.Reason
// }
// return Terminating
// case s.Running != nil:
// return Running
// default:
// return MissingValue
// }
// }
func toRes(r v1.ResourceList) (string, string) {
cpu, mem := r[v1.ResourceCPU], r[v1.ResourceMemory]
// func toRes(r v1.ResourceList) (string, string) {
// cpu, mem := r[v1.ResourceCPU], r[v1.ResourceMemory]
return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value()))
}
// return ToMillicore(cpu.MilliValue()), ToMi(k8s.ToMB(mem.Value()))
// }
func probe(p *v1.Probe) string {
if p == nil {
return "off"
}
return "on"
}
// func probe(p *v1.Probe) string {
// if p == nil {
// return "off"
// }
// return "on"
// }
func asMi(v int64) float64 {
return float64(v) / 1024 * 1024
}
// func asMi(v int64) float64 {
// return float64(v) / 1024 * 1024
// }

View File

@ -1,114 +1,115 @@
package resource
import (
"testing"
// BOZO!!
// import (
// "testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
// "github.com/stretchr/testify/assert"
// v1 "k8s.io/api/core/v1"
// "k8s.io/apimachinery/pkg/api/resource"
// )
func TestProbe(t *testing.T) {
uu := map[string]struct {
probe *v1.Probe
e string
}{
"defined": {&v1.Probe{}, "on"},
"undefined": {nil, "off"},
}
// func TestProbe(t *testing.T) {
// uu := map[string]struct {
// probe *v1.Probe
// e string
// }{
// "defined": {&v1.Probe{}, "on"},
// "undefined": {nil, "off"},
// }
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, probe(u.probe))
})
}
}
// for k := range uu {
// u := uu[k]
// t.Run(k, func(t *testing.T) {
// assert.Equal(t, u.e, probe(u.probe))
// })
// }
// }
func TestAsMi(t *testing.T) {
uu := map[string]struct {
mem int64
e float64
}{
"zero": {0, 0},
"1Mb": {1024 * 1024, 1.048576e+06},
"10Mb": {10 * 1024 * 1024, 1.048576e+07},
}
// func TestAsMi(t *testing.T) {
// uu := map[string]struct {
// mem int64
// e float64
// }{
// "zero": {0, 0},
// "1Mb": {1024 * 1024, 1.048576e+06},
// "10Mb": {10 * 1024 * 1024, 1.048576e+07},
// }
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, asMi(u.mem))
})
}
}
// for k := range uu {
// u := uu[k]
// t.Run(k, func(t *testing.T) {
// assert.Equal(t, u.e, asMi(u.mem))
// })
// }
// }
func TestToRes(t *testing.T) {
uu := map[string]struct {
res v1.ResourceList
ecpu, emem string
}{
"cool": {v1.ResourceList{
v1.ResourceCPU: toQty("10m"),
v1.ResourceMemory: toQty("20Mi"),
},
"10", "20"},
"noRes": {v1.ResourceList{},
"0", "0"},
}
// func TestToRes(t *testing.T) {
// uu := map[string]struct {
// res v1.ResourceList
// ecpu, emem string
// }{
// "cool": {v1.ResourceList{
// v1.ResourceCPU: toQty("10m"),
// v1.ResourceMemory: toQty("20Mi"),
// },
// "10", "20"},
// "noRes": {v1.ResourceList{},
// "0", "0"},
// }
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
cpu, mem := toRes(u.res)
assert.Equal(t, u.ecpu, cpu)
assert.Equal(t, u.emem, mem)
})
}
}
// for k := range uu {
// u := uu[k]
// t.Run(k, func(t *testing.T) {
// cpu, mem := toRes(u.res)
// assert.Equal(t, u.ecpu, cpu)
// assert.Equal(t, u.emem, mem)
// })
// }
// }
func TestToState(t *testing.T) {
uu := map[string]struct {
state v1.ContainerState
e string
}{
"empty": {v1.ContainerState{},
MissingValue},
"running": {
v1.ContainerState{Running: &v1.ContainerStateRunning{}},
"Running",
},
"waiting": {
v1.ContainerState{Waiting: &v1.ContainerStateWaiting{}},
"Waiting",
},
"waitingReason": {
v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "blee"}},
"blee",
},
"terminating": {
v1.ContainerState{Terminated: &v1.ContainerStateTerminated{}},
"Terminating",
},
"terminatedReason": {
v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "blee"}},
"blee",
},
}
// func TestToState(t *testing.T) {
// uu := map[string]struct {
// state v1.ContainerState
// e string
// }{
// "empty": {v1.ContainerState{},
// MissingValue},
// "running": {
// v1.ContainerState{Running: &v1.ContainerStateRunning{}},
// "Running",
// },
// "waiting": {
// v1.ContainerState{Waiting: &v1.ContainerStateWaiting{}},
// "Waiting",
// },
// "waitingReason": {
// v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "blee"}},
// "blee",
// },
// "terminating": {
// v1.ContainerState{Terminated: &v1.ContainerStateTerminated{}},
// "Terminating",
// },
// "terminatedReason": {
// v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "blee"}},
// "blee",
// },
// }
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, toState(u.state))
})
}
}
// for k := range uu {
// u := uu[k]
// t.Run(k, func(t *testing.T) {
// assert.Equal(t, u.e, toState(u.state))
// })
// }
// }
// ----------------------------------------------------------------------------
// Helpers...
// // ----------------------------------------------------------------------------
// // Helpers...
func toQty(s string) resource.Quantity {
q, _ := resource.ParseQuantity(s)
// func toQty(s string) resource.Quantity {
// q, _ := resource.ParseQuantity(s)
return q
}
// return q
// }

View File

@ -1,88 +1,89 @@
package resource
import (
"fmt"
// BOZO!!
// import (
// "fmt"
"github.com/derailed/k9s/internal/k8s"
)
// "github.com/derailed/k9s/internal/k8s"
// )
type (
// Switchable represents a switchable resource.
Switchable interface {
Switch(ctx string) error
MustCurrentContextName() string
}
// type (
// // Switchable represents a switchable resource.
// Switchable interface {
// Switch(ctx string) error
// MustCurrentContextName() string
// }
// SwitchableCruder represents a resource that can be switched.
SwitchableCruder interface {
Cruder
Switchable
}
// // SwitchableCruder represents a resource that can be switched.
// SwitchableCruder interface {
// Cruder
// Switchable
// }
// Context tracks a kubernetes resource.
Context struct {
*Base
instance *k8s.NamedContext
}
)
// // Context tracks a kubernetes resource.
// Context struct {
// *Base
// instance *k8s.NamedContext
// }
// )
// NewContextList returns a new resource list.
func NewContextList(c Connection, ns string) List {
return NewList(NotNamespaced, "ctx", NewContext(c), SwitchAccess)
}
// // NewContextList returns a new resource list.
// func NewContextList(c Connection, ns string) List {
// return NewList(NotNamespaced, "ctx", NewContext(c), SwitchAccess)
// }
// NewContext instantiates a new Context.
func NewContext(c Connection) *Context {
ctx := &Context{Base: NewBase(c, k8s.NewContext(c))}
ctx.Factory = ctx
// // NewContext instantiates a new Context.
// func NewContext(c Connection) *Context {
// ctx := &Context{Base: NewBase(c, k8s.NewContext(c))}
// ctx.Factory = ctx
return ctx
}
// return ctx
// }
// New builds a new Context instance from a k8s resource.
func (r *Context) New(i interface{}) (Columnar, error) {
c := NewContext(r.Connection)
switch instance := i.(type) {
case *k8s.NamedContext:
c.instance = instance
case k8s.NamedContext:
c.instance = &instance
default:
return nil, fmt.Errorf("unknown context type %T", instance)
}
c.path = c.instance.Name
// // New builds a new Context instance from a k8s resource.
// func (r *Context) New(i interface{}) (Columnar, error) {
// c := NewContext(r.Connection)
// switch instance := i.(type) {
// case *k8s.NamedContext:
// c.instance = instance
// case k8s.NamedContext:
// c.instance = &instance
// default:
// return nil, fmt.Errorf("unknown context type %T", instance)
// }
// c.path = c.instance.Name
return c, nil
}
// return c, nil
// }
// Switch out current context.
func (r *Context) Switch(c string) error {
return r.Resource.(Switchable).Switch(c)
}
// // Switch out current context.
// func (r *Context) Switch(c string) error {
// return r.Resource.(Switchable).Switch(c)
// }
// Marshal the resource to yaml.
func (r *Context) Marshal(path string) (string, error) {
return "", nil
}
// // Marshal the resource to yaml.
// func (r *Context) Marshal(path string) (string, error) {
// return "", nil
// }
// Header return resource header.
func (*Context) Header(string) Row {
return append(Row{}, "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE")
}
// // Header return resource header.
// func (*Context) Header(string) Row {
// return append(Row{}, "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE")
// }
// Fields retrieves displayable fields.
func (r *Context) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
// // Fields retrieves displayable fields.
// func (r *Context) Fields(ns string) Row {
// ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
if i.MustCurrentContextName() == i.Name {
i.Name += "*"
}
// i := r.instance
// if i.MustCurrentContextName() == i.Name {
// i.Name += "*"
// }
return append(ff,
i.Name,
i.Context.Cluster,
i.Context.AuthInfo,
i.Context.Namespace,
)
}
// return append(ff,
// i.Name,
// i.Context.Cluster,
// i.Context.AuthInfo,
// i.Context.Namespace,
// )
// }

View File

@ -1,136 +1,137 @@
package resource_test
import (
"testing"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
"k8s.io/cli-runtime/pkg/genericclioptions"
api "k8s.io/client-go/tools/clientcmd/api"
)
func NewContextListWithArgs(ns string, ctx *resource.Context) resource.List {
return resource.NewList(resource.NotNamespaced, "ctx", ctx, resource.SwitchAccess)
}
func NewContextWithArgs(c k8s.Connection, s resource.SwitchableCruder) *resource.Context {
ctx := &resource.Context{Base: resource.NewBase(c, s)}
ctx.Factory = ctx
return ctx
}
func TestCTXSwitch(t *testing.T) {
mc := NewMockConnection()
mr := NewMockSwitchableCruder()
m.When(mr.Switch("fred")).ThenReturn(nil)
ctx := NewContextWithArgs(mc, mr)
err := ctx.Switch("fred")
assert.Nil(t, err)
mr.VerifyWasCalledOnce().Switch("fred")
}
// BOZO!!
// func TestCTXList(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
// m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil)
// import (
// "testing"
// ctx := NewContextWithArgs(mc, mr)
// cc, err := ctx.List("blee", metav1.ListOptions{})
// "github.com/derailed/k9s/internal/k8s"
// "github.com/derailed/k9s/internal/resource"
// m "github.com/petergtz/pegomock"
// "github.com/stretchr/testify/assert"
// "k8s.io/cli-runtime/pkg/genericclioptions"
// api "k8s.io/client-go/tools/clientcmd/api"
// )
// assert.Nil(t, err)
// c, err := ctx.New(k8sNamedCTX())
// assert.Nil(t, err)
// assert.Equal(t, resource.Columnars{c}, cc)
// mr.VerifyWasCalledOnce().List("blee", metav1.ListOptions{})
// func NewContextListWithArgs(ns string, ctx *resource.Context) resource.List {
// return resource.NewList(resource.NotNamespaced, "ctx", ctx, resource.SwitchAccess)
// }
func TestCTXDelete(t *testing.T) {
mc := NewMockConnection()
mr := NewMockSwitchableCruder()
m.When(mr.Delete("", "fred", true, true)).ThenReturn(nil)
// func NewContextWithArgs(c k8s.Connection, s resource.SwitchableCruder) *resource.Context {
// ctx := &resource.Context{Base: resource.NewBase(c, s)}
// ctx.Factory = ctx
// return ctx
// }
ctx := NewContextWithArgs(mc, mr)
// func TestCTXSwitch(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
// m.When(mr.Switch("fred")).ThenReturn(nil)
assert.Nil(t, ctx.Delete("fred", true, true))
mr.VerifyWasCalledOnce().Delete("", "fred", true, true)
}
// ctx := NewContextWithArgs(mc, mr)
// err := ctx.Switch("fred")
func TestCTXListHasName(t *testing.T) {
mc := NewMockConnection()
mr := NewMockSwitchableCruder()
// assert.Nil(t, err)
// mr.VerifyWasCalledOnce().Switch("fred")
// }
ctx := NewContextWithArgs(mc, mr)
l := NewContextListWithArgs("blee", ctx)
// // BOZO!!
// // func TestCTXList(t *testing.T) {
// // mc := NewMockConnection()
// // mr := NewMockSwitchableCruder()
// // m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil)
assert.Equal(t, "ctx", l.GetName())
}
// // ctx := NewContextWithArgs(mc, mr)
// // cc, err := ctx.List("blee", metav1.ListOptions{})
func TestCTXListHasNamespace(t *testing.T) {
mc := NewMockConnection()
mr := NewMockSwitchableCruder()
// // assert.Nil(t, err)
// // c, err := ctx.New(k8sNamedCTX())
// // assert.Nil(t, err)
// // assert.Equal(t, resource.Columnars{c}, cc)
// // mr.VerifyWasCalledOnce().List("blee", metav1.ListOptions{})
// // }
ctx := NewContextWithArgs(mc, mr)
l := NewContextListWithArgs("blee", ctx)
// func TestCTXDelete(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
// m.When(mr.Delete("", "fred", true, true)).ThenReturn(nil)
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
}
// ctx := NewContextWithArgs(mc, mr)
func TestCTXListHasResource(t *testing.T) {
mc := NewMockConnection()
mr := NewMockSwitchableCruder()
// assert.Nil(t, ctx.Delete("fred", true, true))
// mr.VerifyWasCalledOnce().Delete("", "fred", true, true)
// }
ctx := NewContextWithArgs(mc, mr)
l := NewContextListWithArgs("blee", ctx)
// func TestCTXListHasName(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
assert.NotNil(t, l.Resource())
}
// ctx := NewContextWithArgs(mc, mr)
// l := NewContextListWithArgs("blee", ctx)
func TestCTXHeader(t *testing.T) {
mc := NewMockConnection()
mr := NewMockSwitchableCruder()
// assert.Equal(t, "ctx", l.GetName())
// }
ctx := NewContextWithArgs(mc, mr)
// func TestCTXListHasNamespace(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
assert.Equal(t, 4, len(ctx.Header("")))
}
// ctx := NewContextWithArgs(mc, mr)
// l := NewContextListWithArgs("blee", ctx)
func TestCTXFields(t *testing.T) {
mc := NewMockConnection()
m.When(mc.Config()).ThenReturn(k8sConfig())
mr := NewMockSwitchableCruder()
m.When(mr.MustCurrentContextName()).ThenReturn("test")
// assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
// }
ctx := NewContextWithArgs(mc, mr)
c, err := ctx.New(k8sNamedCTX())
assert.Nil(t, err)
// func TestCTXListHasResource(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
assert.Equal(t, 4, len(c.Fields("")))
assert.Equal(t, "test*", c.Fields("")[0])
}
// ctx := NewContextWithArgs(mc, mr)
// l := NewContextListWithArgs("blee", ctx)
// Helpers...
// assert.NotNil(t, l.Resource())
// }
func k8sConfig() *k8s.Config {
ctx := "test"
f := genericclioptions.ConfigFlags{
Context: &ctx,
}
return k8s.NewConfig(&f)
}
// func TestCTXHeader(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
func k8sNamedCTX() *k8s.NamedContext {
return k8s.NewNamedContext(
k8sConfig(),
"test",
&api.Context{
LocationOfOrigin: "fred",
Cluster: "blee",
AuthInfo: "secret",
},
)
}
// ctx := NewContextWithArgs(mc, mr)
// assert.Equal(t, 4, len(ctx.Header("")))
// }
// func TestCTXFields(t *testing.T) {
// mc := NewMockConnection()
// m.When(mc.Config()).ThenReturn(k8sConfig())
// mr := NewMockSwitchableCruder()
// m.When(mr.MustCurrentContextName()).ThenReturn("test")
// ctx := NewContextWithArgs(mc, mr)
// c, err := ctx.New(k8sNamedCTX())
// assert.Nil(t, err)
// assert.Equal(t, 4, len(c.Fields("")))
// assert.Equal(t, "test*", c.Fields("")[0])
// }
// // Helpers...
// func k8sConfig() *k8s.Config {
// ctx := "test"
// f := genericclioptions.ConfigFlags{
// Context: &ctx,
// }
// return k8s.NewConfig(&f)
// }
// func k8sNamedCTX() *k8s.NamedContext {
// return k8s.NewNamedContext(
// k8sConfig(),
// "test",
// &api.Context{
// LocationOfOrigin: "fred",
// Cluster: "blee",
// AuthInfo: "secret",
// },
// )
// }

View File

@ -1,83 +0,0 @@
package resource
import (
"errors"
"fmt"
"github.com/derailed/k9s/internal/k8s"
v1 "k8s.io/api/rbac/v1"
)
// ClusterRole tracks a kubernetes resource.
type ClusterRole struct {
*Base
instance *v1.ClusterRole
}
// NewClusterRoleList returns a new resource list.
func NewClusterRoleList(c Connection, ns string) List {
return NewList(
NotNamespaced,
"clusterrole",
NewClusterRole(c),
CRUDAccess|DescribeAccess,
)
}
// NewClusterRole instantiates a new ClusterRole.
func NewClusterRole(c Connection) *ClusterRole {
cr := &ClusterRole{&Base{Connection: c, Resource: k8s.NewClusterRole(c)}, nil}
cr.Factory = cr
return cr
}
// New builds a new ClusterRole instance from a k8s resource.
func (r *ClusterRole) New(i interface{}) (Columnar, error) {
c := NewClusterRole(r.Connection)
switch instance := i.(type) {
case *v1.ClusterRole:
c.instance = instance
case v1.ClusterRole:
c.instance = &instance
default:
return nil, fmt.Errorf("unknown context type %T", instance)
}
c.path = c.instance.Name
return c, nil
}
// Marshal resource to yaml.
func (r *ClusterRole) Marshal(path string) (string, error) {
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
cr, ok := i.(*v1.ClusterRole)
if !ok {
return "", errors.New("Expecting a cr resource")
}
cr.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1"
cr.TypeMeta.Kind = "ClusterRole"
return r.marshalObject(cr)
}
// Header return resource header.
func (*ClusterRole) Header(ns string) Row {
return append(Row{}, "NAME", "AGE")
}
// Fields retrieves displayable fields.
func (r *ClusterRole) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
return append(ff,
i.Name,
toAge(i.ObjectMeta.CreationTimestamp),
)
}

View File

@ -1,122 +0,0 @@
package resource
import (
"errors"
"fmt"
"strings"
"github.com/derailed/k9s/internal/k8s"
v1 "k8s.io/api/rbac/v1"
)
// ClusterRoleBinding tracks a kubernetes resource.
type ClusterRoleBinding struct {
*Base
instance *v1.ClusterRoleBinding
}
// NewClusterRoleBindingList returns a new resource list.
func NewClusterRoleBindingList(c Connection, _ string) List {
return NewList(
NotNamespaced,
"clusterrolebinding",
NewClusterRoleBinding(c),
ViewAccess|DeleteAccess|DescribeAccess,
)
}
// NewClusterRoleBinding instantiates a new ClusterRoleBinding.
func NewClusterRoleBinding(c Connection) *ClusterRoleBinding {
crb := &ClusterRoleBinding{&Base{Connection: c, Resource: k8s.NewClusterRoleBinding(c)}, nil}
crb.Factory = crb
return crb
}
// New builds a new tabular instance from a k8s resource.
func (r *ClusterRoleBinding) New(i interface{}) (Columnar, error) {
crb := NewClusterRoleBinding(r.Connection)
switch instance := i.(type) {
case *v1.ClusterRoleBinding:
crb.instance = instance
case v1.ClusterRoleBinding:
crb.instance = &instance
default:
return nil, fmt.Errorf("unknown context type %T", instance)
}
crb.path = crb.instance.Name
return crb, nil
}
// Marshal resource to yaml.
func (r *ClusterRoleBinding) Marshal(path string) (string, error) {
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
crb, ok := i.(*v1.ClusterRoleBinding)
if !ok {
return "", errors.New("Expecting a crb resource")
}
crb.TypeMeta.APIVersion = "rbac.authorization.k8s.io/v1"
crb.TypeMeta.Kind = "ClusterRoleBinding"
return r.marshalObject(crb)
}
// Header return resource header.
func (*ClusterRoleBinding) Header(_ string) Row {
return append(Row{}, "NAME", "ROLE", "KIND", "SUBJECTS", "AGE")
}
// Fields retrieves displayable fields.
func (r *ClusterRoleBinding) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
i := r.instance
kind, ss := renderSubjects(i.Subjects)
return append(ff,
i.Name,
i.RoleRef.Name,
kind,
ss,
toAge(i.ObjectMeta.CreationTimestamp),
)
}
// ----------------------------------------------------------------------------
// Helpers...
func renderSubjects(ss []v1.Subject) (kind string, subjects string) {
if len(ss) == 0 {
return NAValue, ""
}
var tt []string
for _, s := range ss {
kind = toSubjectAlias(s.Kind)
tt = append(tt, s.Name)
}
return kind, strings.Join(tt, ",")
}
func toSubjectAlias(s string) string {
if len(s) == 0 {
return s
}
switch s {
case v1.UserKind:
return "USR"
case v1.GroupKind:
return "GRP"
case v1.ServiceAccountKind:
return "SA"
default:
return strings.ToUpper(s)
}
}

View File

@ -1,106 +0,0 @@
package resource_test
import (
"testing"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func NewClusterRoleBindingListWithArgs(ns string, r *resource.ClusterRoleBinding) resource.List {
return resource.NewList(resource.NotNamespaced, "clusterrolebinding", r, resource.ViewAccess|resource.DeleteAccess|resource.DescribeAccess)
}
func NewClusterRoleBindingWithArgs(conn k8s.Connection, res resource.Cruder) *resource.ClusterRoleBinding {
r := &resource.ClusterRoleBinding{Base: resource.NewBase(conn, res)}
r.Factory = r
return r
}
func TestCRBFields(t *testing.T) {
conn := NewMockConnection()
r := newCRB(conn).Fields(resource.AllNamespaces)
assert.Equal(t, "fred", r[0])
}
func TestCRBMarshal(t *testing.T) {
conn := NewMockConnection()
ca := NewMockCruder()
m.When(ca.Get("blee", "fred")).ThenReturn(k8sCRB(), nil)
cm := NewClusterRoleBindingWithArgs(conn, ca)
ma, err := cm.Marshal("blee/fred")
ca.VerifyWasCalledOnce().Get("blee", "fred")
assert.Nil(t, err)
assert.Equal(t, crbYaml(), ma)
}
// BOZO!!
// func TestCRBListData(t *testing.T) {
// conn := NewMockConnection()
// ca := NewMockCruder()
// m.When(ca.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCRB()}, nil)
// l := NewClusterRoleBindingListWithArgs("-", NewClusterRoleBindingWithArgs(conn, ca))
// // Make sure we can get deltas!
// for i := 0; i < 2; i++ {
// err := l.Reconcile(nil, "", "")
// assert.Nil(t, err)
// }
// ca.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{})
// td := l.Data()
// assert.Equal(t, 1, len(td.Rows))
// assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
// row := td.Rows["fred"]
// assert.Equal(t, 5, len(row.Deltas))
// for _, d := range row.Deltas {
// assert.Equal(t, "", d)
// }
// assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
// }
// Helpers...
func k8sCRB() *rbacv1.ClusterRoleBinding {
return &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "fred",
Namespace: "blee",
CreationTimestamp: metav1.Time{Time: testTime()},
},
Subjects: []rbacv1.Subject{
{Kind: "test", Name: "fred", Namespace: "blee"},
},
}
}
func newCRB(c resource.Connection) resource.Columnar {
co, _ := resource.NewClusterRoleBinding(c).New(k8sCRB())
return co
}
func crbYaml() string {
return `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
creationTimestamp: "2018-12-14T17:36:43Z"
name: fred
namespace: blee
roleRef:
apiGroup: ""
kind: ""
name: ""
subjects:
- kind: test
name: fred
namespace: blee
`
}

View File

@ -1,139 +0,0 @@
package resource_test
import (
"fmt"
"testing"
"time"
"github.com/derailed/k9s/internal/resource"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func NewClusterRoleListWithArgs(ns string, r *resource.ClusterRole) resource.List {
return resource.NewList(resource.NotNamespaced, "clusterrole", r, resource.CRUDAccess|resource.DescribeAccess)
}
func NewClusterRoleWithArgs(mc resource.Connection, res resource.Cruder) *resource.ClusterRole {
r := &resource.ClusterRole{Base: resource.NewBase(mc, res)}
r.Factory = r
return r
}
func TestCRListAccess(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
ns := "blee"
r := NewClusterRoleWithArgs(mc, mr)
l := NewClusterRoleListWithArgs(resource.AllNamespaces, r)
l.SetNamespace(ns)
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
assert.Equal(t, "clusterrole", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a))
}
}
func TestCRFields(t *testing.T) {
r := newClusterRole().Fields("blee")
assert.Equal(t, "fred", r[0])
}
func TestCRFieldsAllNS(t *testing.T) {
r := newClusterRole().Fields(resource.AllNamespaces)
assert.Equal(t, "fred", r[0])
}
func TestCRMarshal(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
m.When(mr.Get("blee", "fred")).ThenReturn(k8sCR(), nil)
cr := NewClusterRoleWithArgs(mc, mr)
ma, err := cr.Marshal("blee/fred")
mr.VerifyWasCalledOnce().Get("blee", "fred")
assert.Nil(t, err)
assert.Equal(t, mrYaml(), ma)
}
// BOZO!!
// func TestCRListData(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCR()}, nil)
// l := NewClusterRoleListWithArgs("-", NewClusterRoleWithArgs(mc, mr))
// // Make sure we mcn get deltas!
// for i := 0; i < 2; i++ {
// err := l.Reconcile(nil, "", "")
// assert.Nil(t, err)
// }
// mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{})
// td := l.Data()
// assert.Equal(t, 1, len(td.Rows))
// assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
// row := td.Rows["fred"]
// assert.Equal(t, 2, len(row.Deltas))
// for _, d := range row.Deltas {
// assert.Equal(t, "", d)
// }
// assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
// }
// Helpers...
func k8sCR() *rbacv1.ClusterRole {
return &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: "fred",
Namespace: "blee",
CreationTimestamp: metav1.Time{Time: testTime()},
},
Rules: []rbacv1.PolicyRule{
{
Verbs: []string{"get", "list"},
APIGroups: []string{""},
ResourceNames: []string{"pod"},
},
},
}
}
func newClusterRole() resource.Columnar {
conn := NewMockConnection()
c, _ := resource.NewClusterRole(conn).New(k8sCR())
return c
}
func testTime() time.Time {
t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00")
if err != nil {
fmt.Println("TestTime Failed", err)
}
return t
}
func mrYaml() string {
return `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: "2018-12-14T17:36:43Z"
name: fred
namespace: blee
rules:
- apiGroups:
- ""
resourceNames:
- pod
verbs:
- get
- list
`
}

View File

@ -1,124 +1,125 @@
package resource
import (
"errors"
"fmt"
"strconv"
// BOZO!!
// import (
// "errors"
// "fmt"
// "strconv"
"github.com/derailed/k9s/internal/k8s"
batchv1beta1 "k8s.io/api/batch/v1beta1"
)
// "github.com/derailed/k9s/internal/k8s"
// batchv1beta1 "k8s.io/api/batch/v1beta1"
// )
type (
// CronJob tracks a kubernetes resource.
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
}
// // Runner can run jobs.
// Runner interface {
// Run(path string) error
// }
// Runnable can run jobs.
Runnable interface {
Run(ns, n string) error
}
)
// // Runnable can run jobs.
// Runnable interface {
// Run(ns, n string) error
// }
// )
// NewCronJobList returns a new resource list.
func NewCronJobList(c Connection, ns string) List {
return NewList(
ns,
"cronjob",
NewCronJob(c),
AllVerbsAccess|DescribeAccess,
)
}
// // NewCronJobList returns a new resource list.
// func NewCronJobList(c Connection, ns string) List {
// return NewList(
// ns,
// "cronjob",
// NewCronJob(c),
// AllVerbsAccess|DescribeAccess,
// )
// }
// NewCronJob instantiates a new CronJob.
func NewCronJob(c Connection) *CronJob {
cj := &CronJob{&Base{Connection: c, Resource: k8s.NewCronJob(c)}, nil}
cj.Factory = cj
// // NewCronJob instantiates a new CronJob.
// func NewCronJob(c Connection) *CronJob {
// cj := &CronJob{&Base{Connection: c, Resource: k8s.NewCronJob(c)}, nil}
// cj.Factory = cj
return cj
}
// return cj
// }
// New builds a new CronJob instance from a k8s resource.
func (r *CronJob) New(i interface{}) (Columnar, error) {
c := NewCronJob(r.Connection)
switch instance := i.(type) {
case *batchv1beta1.CronJob:
c.instance = instance
case batchv1beta1.CronJob:
c.instance = &instance
default:
return nil, fmt.Errorf("Expecting CronJob but got %T", instance)
}
c.path = c.namespacedName(c.instance.ObjectMeta)
// // New builds a new CronJob instance from a k8s resource.
// func (r *CronJob) New(i interface{}) (Columnar, error) {
// c := NewCronJob(r.Connection)
// switch instance := i.(type) {
// case *batchv1beta1.CronJob:
// c.instance = instance
// case batchv1beta1.CronJob:
// c.instance = &instance
// default:
// return nil, fmt.Errorf("Expecting CronJob but got %T", instance)
// }
// c.path = c.namespacedName(c.instance.ObjectMeta)
return c, nil
}
// return c, nil
// }
// Marshal resource to yaml.
func (r *CronJob) Marshal(path string) (string, error) {
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
// // Marshal resource to yaml.
// func (r *CronJob) Marshal(path string) (string, error) {
// ns, n := Namespaced(path)
// i, err := r.Resource.Get(ns, n)
// if err != nil {
// return "", err
// }
cj, ok := i.(*batchv1beta1.CronJob)
if !ok {
return "", errors.New("expecting cronjob resource")
}
cj.TypeMeta.APIVersion = "extensions/batchv1beta1"
cj.TypeMeta.Kind = "CronJob"
// cj, ok := i.(*batchv1beta1.CronJob)
// if !ok {
// return "", errors.New("expecting cronjob resource")
// }
// cj.TypeMeta.APIVersion = "extensions/batchv1beta1"
// cj.TypeMeta.Kind = "CronJob"
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.Resource.(Runnable); ok {
return c.Run(ns, n)
}
// // Run a given cronjob.
// func (r *CronJob) Run(pa string) error {
// ns, n := Namespaced(pa)
// if c, ok := r.Resource.(Runnable); ok {
// return c.Run(ns, n)
// }
return fmt.Errorf("unable to run cronjob %s", pa)
}
// return fmt.Errorf("unable to run cronjob %s", pa)
// }
// Header return resource header.
func (*CronJob) Header(ns string) Row {
hh := Row{}
if ns == AllNamespaces {
hh = append(hh, "NAMESPACE")
}
// // Header return resource header.
// func (*CronJob) Header(ns string) Row {
// hh := Row{}
// if ns == AllNamespaces {
// hh = append(hh, "NAMESPACE")
// }
return append(hh, "NAME", "SCHEDULE", "SUSPEND", "ACTIVE", "LAST_SCHEDULE", "AGE")
}
// return append(hh, "NAME", "SCHEDULE", "SUSPEND", "ACTIVE", "LAST_SCHEDULE", "AGE")
// }
// Fields retrieves displayable fields.
func (r *CronJob) Fields(ns string) Row {
ff := make([]string, 0, len(r.Header(ns)))
// // Fields retrieves displayable fields.
// func (r *CronJob) Fields(ns string) Row {
// ff := make([]string, 0, len(r.Header(ns)))
i := r.instance
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
// i := r.instance
// if ns == AllNamespaces {
// ff = append(ff, i.Namespace)
// }
lastScheduled := MissingValue
if i.Status.LastScheduleTime != nil {
lastScheduled = toAgeHuman(toAge(*i.Status.LastScheduleTime))
}
// lastScheduled := MissingValue
// if i.Status.LastScheduleTime != nil {
// lastScheduled = toAgeHuman(toAge(*i.Status.LastScheduleTime))
// }
return append(ff,
i.Name,
i.Spec.Schedule,
boolPtrToStr(i.Spec.Suspend),
strconv.Itoa(len(i.Status.Active)),
lastScheduled,
toAge(i.ObjectMeta.CreationTimestamp),
)
}
// return append(ff,
// i.Name,
// i.Spec.Schedule,
// boolPtrToStr(i.Spec.Suspend),
// strconv.Itoa(len(i.Status.Active)),
// lastScheduled,
// toAge(i.ObjectMeta.CreationTimestamp),
// )
// }

View File

@ -1,131 +1,132 @@
package resource_test
import (
"testing"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
batchv1beta1 "k8s.io/api/batch/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func NewCronJobListWithArgs(ns string, r *resource.CronJob) resource.List {
return resource.NewList(ns, "cj", r, resource.AllVerbsAccess|resource.DescribeAccess)
}
func NewCronJobWithArgs(conn k8s.Connection, res resource.Cruder) *resource.CronJob {
r := &resource.CronJob{Base: resource.NewBase(conn, res)}
r.Factory = r
return r
}
func TestCronJobListAccess(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
ns := "blee"
r := NewCronJobWithArgs(mc, mr)
l := NewCronJobListWithArgs(resource.AllNamespaces, r)
l.SetNamespace(ns)
assert.Equal(t, ns, l.GetNamespace())
assert.Equal(t, "cj", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a))
}
}
func TestCronJobFields(t *testing.T) {
r := newCronJob().Fields("blee")
assert.Equal(t, "fred", r[0])
}
func TestCronJobMarshal(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
m.When(mr.Get("blee", "fred")).ThenReturn(k8sCronJob(), nil)
cm := NewCronJobWithArgs(mc, mr)
ma, err := cm.Marshal("blee/fred")
mr.VerifyWasCalledOnce().Get("blee", "fred")
assert.Nil(t, err)
assert.Equal(t, cronjobYaml(), ma)
}
// BOZO!!
// import (
// "testing"
// func TestCronJobListData(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCronJob()}, nil)
// "github.com/derailed/k9s/internal/k8s"
// "github.com/derailed/k9s/internal/resource"
// m "github.com/petergtz/pegomock"
// "github.com/stretchr/testify/assert"
// batchv1beta1 "k8s.io/api/batch/v1beta1"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
// l := NewCronJobListWithArgs("-", NewCronJobWithArgs(mc, mr))
// // Make sure we can get deltas!
// for i := 0; i < 2; i++ {
// err := l.Reconcile(nil, "", "")
// assert.Nil(t, err)
// }
// mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{})
// td := l.Data()
// assert.Equal(t, 1, len(td.Rows))
// assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
// row := td.Rows["blee/fred"]
// assert.Equal(t, 6, len(row.Deltas))
// for _, d := range row.Deltas {
// assert.Equal(t, "", d)
// }
// assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
// func NewCronJobListWithArgs(ns string, r *resource.CronJob) resource.List {
// return resource.NewList(ns, "cj", r, resource.AllVerbsAccess|resource.DescribeAccess)
// }
// Helpers...
// func NewCronJobWithArgs(conn k8s.Connection, res resource.Cruder) *resource.CronJob {
// r := &resource.CronJob{Base: resource.NewBase(conn, res)}
// r.Factory = r
// return r
// }
func k8sCronJob() *batchv1beta1.CronJob {
var b bool
return &batchv1beta1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Namespace: "blee",
Name: "fred",
CreationTimestamp: metav1.Time{Time: testTime()},
},
Spec: batchv1beta1.CronJobSpec{
Schedule: "*/1 * * * *",
Suspend: &b,
},
Status: batchv1beta1.CronJobStatus{
LastScheduleTime: &metav1.Time{Time: testTime()},
},
}
}
// func TestCronJobListAccess(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
func newCronJob() resource.Columnar {
mc := NewMockConnection()
c, _ := resource.NewCronJob(mc).New(k8sCronJob())
return c
}
// ns := "blee"
// r := NewCronJobWithArgs(mc, mr)
// l := NewCronJobListWithArgs(resource.AllNamespaces, r)
// l.SetNamespace(ns)
func cronjobYaml() string {
return `apiVersion: extensions/batchv1beta1
kind: CronJob
metadata:
creationTimestamp: "2018-12-14T17:36:43Z"
name: fred
namespace: blee
spec:
jobTemplate:
metadata:
creationTimestamp: null
spec:
template:
metadata:
creationTimestamp: null
spec:
containers: null
schedule: '*/1 * * * *'
suspend: false
status:
lastScheduleTime: "2018-12-14T17:36:43Z"
`
}
// assert.Equal(t, ns, l.GetNamespace())
// assert.Equal(t, "cj", l.GetName())
// for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
// assert.True(t, l.Access(a))
// }
// }
// func TestCronJobFields(t *testing.T) {
// r := newCronJob().Fields("blee")
// assert.Equal(t, "fred", r[0])
// }
// func TestCronJobMarshal(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.Get("blee", "fred")).ThenReturn(k8sCronJob(), nil)
// cm := NewCronJobWithArgs(mc, mr)
// ma, err := cm.Marshal("blee/fred")
// mr.VerifyWasCalledOnce().Get("blee", "fred")
// assert.Nil(t, err)
// assert.Equal(t, cronjobYaml(), ma)
// }
// // BOZO!!
// // func TestCronJobListData(t *testing.T) {
// // mc := NewMockConnection()
// // mr := NewMockCruder()
// // m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCronJob()}, nil)
// // l := NewCronJobListWithArgs("-", NewCronJobWithArgs(mc, mr))
// // // Make sure we can get deltas!
// // for i := 0; i < 2; i++ {
// // err := l.Reconcile(nil, "", "")
// // assert.Nil(t, err)
// // }
// // mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{})
// // td := l.Data()
// // assert.Equal(t, 1, len(td.Rows))
// // assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
// // row := td.Rows["blee/fred"]
// // assert.Equal(t, 6, len(row.Deltas))
// // for _, d := range row.Deltas {
// // assert.Equal(t, "", d)
// // }
// // assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
// // }
// // Helpers...
// func k8sCronJob() *batchv1beta1.CronJob {
// var b bool
// return &batchv1beta1.CronJob{
// ObjectMeta: metav1.ObjectMeta{
// Namespace: "blee",
// Name: "fred",
// CreationTimestamp: metav1.Time{Time: testTime()},
// },
// Spec: batchv1beta1.CronJobSpec{
// Schedule: "*/1 * * * *",
// Suspend: &b,
// },
// Status: batchv1beta1.CronJobStatus{
// LastScheduleTime: &metav1.Time{Time: testTime()},
// },
// }
// }
// func newCronJob() resource.Columnar {
// mc := NewMockConnection()
// c, _ := resource.NewCronJob(mc).New(k8sCronJob())
// return c
// }
// func cronjobYaml() string {
// return `apiVersion: extensions/batchv1beta1
// kind: CronJob
// metadata:
// creationTimestamp: "2018-12-14T17:36:43Z"
// name: fred
// namespace: blee
// spec:
// jobTemplate:
// metadata:
// creationTimestamp: null
// spec:
// template:
// metadata:
// creationTimestamp: null
// spec:
// containers: null
// schedule: '*/1 * * * *'
// suspend: false
// status:
// lastScheduleTime: "2018-12-14T17:36:43Z"
// `
// }

View File

@ -1,177 +1,178 @@
package resource
import (
"encoding/json"
"fmt"
"path"
"strings"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
)
// Custom tracks a kubernetes resource.
type Custom struct {
*Base
instance *metav1beta1.TableRow
gvr k8s.GVR
headers Row
}
// NewCustomList returns a new resource list.
func NewCustomList(c k8s.Connection, namespaced bool, ns, gvr string) List {
if !namespaced {
ns = NotNamespaced
}
g := k8s.GVR(gvr)
return NewList(
ns,
g.ToR(),
NewCustom(c, g), AllVerbsAccess|DescribeAccess,
)
}
// NewCustom instantiates a new Kubernetes Resource.
func NewCustom(c k8s.Connection, gvr k8s.GVR) *Custom {
cr := &Custom{Base: &Base{Connection: c, Resource: k8s.NewResource(c, gvr)}}
cr.Factory = cr
cr.gvr = gvr
return cr
}
// New builds a new Custom instance from a k8s resource.
func (r *Custom) New(i interface{}) (Columnar, error) {
cr := NewCustom(r.Connection, "")
switch instance := i.(type) {
case *metav1beta1.TableRow:
cr.instance = instance
case metav1beta1.TableRow:
cr.instance = &instance
default:
return nil, fmt.Errorf("Expecting TableRow but got %T", instance)
}
var obj map[string]interface{}
err := json.Unmarshal(cr.instance.Object.Raw, &obj)
if err != nil {
return nil, err
}
meta, err := extractMeta(obj)
if err != nil {
return nil, err
}
ns, err := extractString(meta, "namespace")
if err != nil {
return nil, err
}
n, err := extractString(meta, "name")
if err != nil {
return nil, err
}
cr.path = path.Join(ns, n)
cr.gvr = k8s.NewGVR(obj["kind"].(string), obj["apiVersion"].(string), n)
return cr, nil
}
// Marshal resource to yaml.
func (r *Custom) Marshal(path string) (string, error) {
panic("NYI")
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
switch v := i.(type) {
case *unstructured.Unstructured:
i = v.Object
}
raw, err := yaml.Marshal(i)
if err != nil {
return "", err
}
return string(raw), nil
}
// BOZO!!
// List all resources
// func (r *Custom) List(ns string, opts v1.ListOptions) (Columnars, error) {
// ii, err := r.Resource.List(ns, opts)
// import (
// "encoding/json"
// "fmt"
// "path"
// "strings"
// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
// "github.com/derailed/k9s/internal/k8s"
// "github.com/rs/zerolog/log"
// "gopkg.in/yaml.v2"
// metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
// )
// // Custom tracks a kubernetes resource.
// type Custom struct {
// *Base
// instance *metav1beta1.TableRow
// gvr k8s.GVR
// headers Row
// }
// // NewCustomList returns a new resource list.
// func NewCustomList(c k8s.Connection, namespaced bool, ns, gvr string) List {
// if !namespaced {
// ns = NotNamespaced
// }
// g := k8s.GVR(gvr)
// return NewList(
// ns,
// g.ToR(),
// NewCustom(c, g), AllVerbsAccess|DescribeAccess,
// )
// }
// // NewCustom instantiates a new Kubernetes Resource.
// func NewCustom(c k8s.Connection, gvr k8s.GVR) *Custom {
// cr := &Custom{Base: &Base{Connection: c, Resource: k8s.NewResource(c, gvr)}}
// cr.Factory = cr
// cr.gvr = gvr
// return cr
// }
// // New builds a new Custom instance from a k8s resource.
// func (r *Custom) New(i interface{}) (Columnar, error) {
// cr := NewCustom(r.Connection, "")
// switch instance := i.(type) {
// case *metav1beta1.TableRow:
// cr.instance = instance
// case metav1beta1.TableRow:
// cr.instance = &instance
// default:
// return nil, fmt.Errorf("Expecting TableRow but got %T", instance)
// }
// var obj map[string]interface{}
// err := json.Unmarshal(cr.instance.Object.Raw, &obj)
// if err != nil {
// return nil, err
// }
// meta, err := extractMeta(obj)
// if err != nil {
// return nil, err
// }
// ns, err := extractString(meta, "namespace")
// if err != nil {
// return nil, err
// }
// n, err := extractString(meta, "name")
// if err != nil {
// return nil, err
// }
// cr.path = path.Join(ns, n)
// cr.gvr = k8s.NewGVR(obj["kind"].(string), obj["apiVersion"].(string), n)
// if len(ii) == 0 {
// return Columnars{}, errors.New("no resources found")
// }
// table, ok := ii[0].(*metav1beta1.Table)
// if !ok {
// return nil, errors.New("expecting a table resource")
// }
// r.headers = make(Row, len(table.ColumnDefinitions))
// for i, h := range table.ColumnDefinitions {
// r.headers[i] = h.Name
// }
// rows := table.Rows
// cc := make(Columnars, 0, len(rows))
// for i := 0; i < len(rows); i++ {
// res, err := r.New(rows[i])
// if err != nil {
// return nil, err
// }
// cc = append(cc, res)
// }
// return cc, nil
// return cr, nil
// }
// Header return resource header.
func (r *Custom) Header(ns string) Row {
hh := make(Row, 0, len(r.headers)+1)
// // Marshal resource to yaml.
// func (r *Custom) Marshal(path string) (string, error) {
// panic("NYI")
// ns, n := Namespaced(path)
// i, err := r.Resource.Get(ns, n)
// if err != nil {
// return "", err
// }
// switch v := i.(type) {
// case *unstructured.Unstructured:
// i = v.Object
// }
if ns == AllNamespaces {
hh = append(hh, "NAMESPACE")
}
for _, h := range r.headers {
hh = append(hh, strings.ToUpper(h))
}
// raw, err := yaml.Marshal(i)
// if err != nil {
// return "", err
// }
return hh
}
// return string(raw), nil
// }
// Fields retrieves displayable fields.
func (r *Custom) Fields(ns string) Row {
ff := make(Row, 0, len(r.Header(ns)))
// // BOZO!!
// // List all resources
// // func (r *Custom) List(ns string, opts v1.ListOptions) (Columnars, error) {
// // ii, err := r.Resource.List(ns, opts)
// // if err != nil {
// // return nil, err
// // }
var obj map[string]interface{}
err := json.Unmarshal(r.instance.Object.Raw, &obj)
if err != nil {
log.Error().Err(err)
return Row{}
}
// // if len(ii) == 0 {
// // return Columnars{}, errors.New("no resources found")
// // }
meta, ok := obj["metadata"].(map[string]interface{})
if !ok {
log.Fatal().Msg("expecting interface map meta")
}
rns, ok := meta["namespace"].(string)
if ns == AllNamespaces {
if ok {
ff = append(ff, rns)
}
}
// // table, ok := ii[0].(*metav1beta1.Table)
// // if !ok {
// // return nil, errors.New("expecting a table resource")
// // }
// // r.headers = make(Row, len(table.ColumnDefinitions))
// // for i, h := range table.ColumnDefinitions {
// // r.headers[i] = h.Name
// // }
// // rows := table.Rows
// // cc := make(Columnars, 0, len(rows))
// // for i := 0; i < len(rows); i++ {
// // res, err := r.New(rows[i])
// // if err != nil {
// // return nil, err
// // }
// // cc = append(cc, res)
// // }
for _, c := range r.instance.Cells {
ff = append(ff, fmt.Sprintf("%v", c))
}
// // return cc, nil
// // }
return ff
}
// // Header return resource header.
// func (r *Custom) Header(ns string) Row {
// hh := make(Row, 0, len(r.headers)+1)
// if ns == AllNamespaces {
// hh = append(hh, "NAMESPACE")
// }
// for _, h := range r.headers {
// hh = append(hh, strings.ToUpper(h))
// }
// return hh
// }
// // Fields retrieves displayable fields.
// func (r *Custom) Fields(ns string) Row {
// ff := make(Row, 0, len(r.Header(ns)))
// var obj map[string]interface{}
// err := json.Unmarshal(r.instance.Object.Raw, &obj)
// if err != nil {
// log.Error().Err(err)
// return Row{}
// }
// meta, ok := obj["metadata"].(map[string]interface{})
// if !ok {
// log.Fatal().Msg("expecting interface map meta")
// }
// rns, ok := meta["namespace"].(string)
// if ns == AllNamespaces {
// if ok {
// ff = append(ff, rns)
// }
// }
// for _, c := range r.instance.Cells {
// ff = append(ff, fmt.Sprintf("%v", c))
// }
// return ff
// }

View File

@ -1,353 +1,354 @@
package resource_test
import (
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
func NewCustomListWithArgs(ns, name string, r *resource.Custom) resource.List {
return resource.NewList(ns, name, r, resource.AllVerbsAccess)
}
func NewCustomWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Custom {
r := &resource.Custom{Base: resource.NewBase(conn, res)}
r.Factory = r
return r
}
func TestCustomListAccess(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
ns := "blee"
r := NewCustomWithArgs(mc, mr)
l := NewCustomListWithArgs(resource.AllNamespaces, "fred", r)
l.SetNamespace(ns)
assert.Equal(t, ns, l.GetNamespace())
assert.Equal(t, "fred", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a))
}
}
func TestCustomFields(t *testing.T) {
r := newCustom().Fields("blee")
assert.Equal(t, "a", r[0])
}
// BOZO!!
// func TestCustomMarshal(t *testing.T) {
// import (
// "testing"
// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
// "github.com/derailed/k9s/internal/k8s"
// "github.com/derailed/k9s/internal/resource"
// m "github.com/petergtz/pegomock"
// "github.com/stretchr/testify/assert"
// metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
// "k8s.io/apimachinery/pkg/runtime"
// )
// func NewCustomListWithArgs(ns, name string, r *resource.Custom) resource.List {
// return resource.NewList(ns, name, r, resource.AllVerbsAccess)
// }
// func NewCustomWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Custom {
// r := &resource.Custom{Base: resource.NewBase(conn, res)}
// r.Factory = r
// return r
// }
// func TestCustomListAccess(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomTable(), nil)
// ns := "blee"
// r := NewCustomWithArgs(mc, mr)
// l := NewCustomListWithArgs(resource.AllNamespaces, "fred", r)
// l.SetNamespace(ns)
// assert.Equal(t, ns, l.GetNamespace())
// assert.Equal(t, "fred", l.GetName())
// for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
// assert.True(t, l.Access(a))
// }
// }
// func TestCustomFields(t *testing.T) {
// r := newCustom().Fields("blee")
// assert.Equal(t, "a", r[0])
// }
// // BOZO!!
// // func TestCustomMarshal(t *testing.T) {
// // mc := NewMockConnection()
// // mr := NewMockCruder()
// // m.When(mr.Get("blee", "fred")).ThenReturn(k8sCustomGetTable(), nil)
// // cm := NewCustomWithArgs(mc, mr)
// // ma, err := cm.Marshal("blee/fred")
// // mr.VerifyWasCalledOnce().Get("blee", "fred")
// // assert.Nil(t, err)
// // assert.Equal(t, customYaml(), ma)
// // }
// func TestCustomMarshalWithUnstructured(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.Get("blee", "fred")).ThenReturn(k8sUnstructured(), nil)
// cm := NewCustomWithArgs(mc, mr)
// ma, err := cm.Marshal("blee/fred")
// mr.VerifyWasCalledOnce().Get("blee", "fred")
// assert.Nil(t, err)
// assert.Equal(t, customYaml(), ma)
// assert.Equal(t, unstructuredYAML(), ma)
// }
func TestCustomMarshalWithUnstructured(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
m.When(mr.Get("blee", "fred")).ThenReturn(k8sUnstructured(), nil)
// // BOZO!!
// // func TestCustomListData(t *testing.T) {
// // mc := NewMockConnection()
// // mr := NewMockCruder()
// // m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{k8sCustomGetTable()}, nil)
cm := NewCustomWithArgs(mc, mr)
ma, err := cm.Marshal("blee/fred")
mr.VerifyWasCalledOnce().Get("blee", "fred")
// // l := NewCustomListWithArgs("blee", "fred", NewCustomWithArgs(mc, mr))
// // // Make sure we can get deltas!
// // for i := 0; i < 2; i++ {
// // err := l.Reconcile(nil, "", "")
// // assert.Nil(t, err)
// // }
assert.Nil(t, err)
assert.Equal(t, unstructuredYAML(), ma)
}
// // mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{})
// // td := l.Data()
// // assert.Equal(t, 1, len(td.Rows))
// // assert.Equal(t, "blee", l.GetNamespace())
// // row := td.Rows["blee/fred"]
// // assert.Equal(t, 3, len(row.Deltas))
// // for _, d := range row.Deltas {
// // assert.Equal(t, "", d)
// // }
// // assert.Equal(t, resource.Row{"a"}, row.Fields[:1])
// // }
// BOZO!!
// func TestCustomListData(t *testing.T) {
// // Helpers...
// func k8sCustomGetTable() *metav1beta1.Table {
// return &metav1beta1.Table{
// ColumnDefinitions: []metav1beta1.TableColumnDefinition{
// {Name: "A"},
// {Name: "B"},
// {Name: "C"},
// },
// Rows: []metav1beta1.TableRow{
// {
// Object: runtime.RawExtension{
// Raw: []byte(`{
// "kind": "fred",
// "apiVersion": "v1",
// "metadata": {
// "namespace": "blee",
// "name": "fred"
// }}`),
// },
// Cells: []interface{}{
// "a",
// "b",
// "c",
// },
// },
// },
// }
// }
// func k8sUnstructured() *unstructured.Unstructured {
// return &unstructured.Unstructured{
// Object: map[string]interface{}{
// "kind": "fred",
// "apiVersion": "v1",
// "metadata": map[string]interface{}{
// "namespace": "blee",
// "name": "fred",
// },
// },
// }
// }
// func unstructuredYAML() string {
// return `apiVersion: v1
// kind: fred
// metadata:
// name: fred
// namespace: blee
// `
// }
// func k8sCustomRow() *metav1beta1.TableRow {
// return &metav1beta1.TableRow{
// Object: runtime.RawExtension{
// Raw: []byte(`{
// "kind": "fred",
// "apiVersion": "v1",
// "metadata": {
// "namespace": "blee",
// "name": "fred"
// }}`),
// },
// Cells: []interface{}{
// "a",
// "b",
// "c",
// },
// }
// }
// func newCustom() resource.Columnar {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{k8sCustomTable()}, nil)
// l := NewCustomListWithArgs("blee", "fred", NewCustomWithArgs(mc, mr))
// // Make sure we can get deltas!
// for i := 0; i < 2; i++ {
// err := l.Reconcile(nil, "", "")
// assert.Nil(t, err)
// }
// mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{})
// td := l.Data()
// assert.Equal(t, 1, len(td.Rows))
// assert.Equal(t, "blee", l.GetNamespace())
// row := td.Rows["blee/fred"]
// assert.Equal(t, 3, len(row.Deltas))
// for _, d := range row.Deltas {
// assert.Equal(t, "", d)
// }
// assert.Equal(t, resource.Row{"a"}, row.Fields[:1])
// c, _ := resource.NewCustom(mc, "g/v1/fred").New(k8sCustomRow())
// return c
// }
// Helpers...
func k8sCustomTable() *metav1beta1.Table {
return &metav1beta1.Table{
ColumnDefinitions: []metav1beta1.TableColumnDefinition{
{Name: "A"},
{Name: "B"},
{Name: "C"},
},
Rows: []metav1beta1.TableRow{
{
Object: runtime.RawExtension{
Raw: []byte(`{
"kind": "fred",
"apiVersion": "v1",
"metadata": {
"namespace": "blee",
"name": "fred"
}}`),
},
Cells: []interface{}{
"a",
"b",
"c",
},
},
},
}
}
func k8sUnstructured() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "fred",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"namespace": "blee",
"name": "fred",
},
},
}
}
func unstructuredYAML() string {
return `apiVersion: v1
kind: fred
metadata:
name: fred
namespace: blee
`
}
func k8sCustomRow() *metav1beta1.TableRow {
return &metav1beta1.TableRow{
Object: runtime.RawExtension{
Raw: []byte(`{
"kind": "fred",
"apiVersion": "v1",
"metadata": {
"namespace": "blee",
"name": "fred"
}}`),
},
Cells: []interface{}{
"a",
"b",
"c",
},
}
}
func newCustom() resource.Columnar {
mc := NewMockConnection()
c, _ := resource.NewCustom(mc, "g/v1/fred").New(k8sCustomRow())
return c
}
func customYaml() string {
return `typemeta:
kind: ""
apiversion: ""
listmeta:
selflink: ""
resourceversion: ""
continue: ""
remainingitemcount: null
columndefinitions:
- name: A
type: ""
format: ""
description: ""
priority: 0
- name: B
type: ""
format: ""
description: ""
priority: 0
- name: C
type: ""
format: ""
description: ""
priority: 0
rows:
- cells:
- a
- b
- c
conditions: []
object:
raw:
- 123
- 10
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 34
- 107
- 105
- 110
- 100
- 34
- 58
- 32
- 34
- 102
- 114
- 101
- 100
- 34
- 44
- 10
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 34
- 97
- 112
- 105
- 86
- 101
- 114
- 115
- 105
- 111
- 110
- 34
- 58
- 32
- 34
- 118
- 49
- 34
- 44
- 10
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 34
- 109
- 101
- 116
- 97
- 100
- 97
- 116
- 97
- 34
- 58
- 32
- 123
- 10
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 34
- 110
- 97
- 109
- 101
- 115
- 112
- 97
- 99
- 101
- 34
- 58
- 32
- 34
- 98
- 108
- 101
- 101
- 34
- 44
- 10
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 34
- 110
- 97
- 109
- 101
- 34
- 58
- 32
- 34
- 102
- 114
- 101
- 100
- 34
- 10
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 32
- 125
- 125
object: null
`
}
// func customYaml() string {
// return `typemeta:
// kind: ""
// apiversion: ""
// listmeta:
// selflink: ""
// resourceversion: ""
// continue: ""
// remainingitemcount: null
// columndefinitions:
// - name: A
// type: ""
// format: ""
// description: ""
// priority: 0
// - name: B
// type: ""
// format: ""
// description: ""
// priority: 0
// - name: C
// type: ""
// format: ""
// description: ""
// priority: 0
// rows:
// - cells:
// - a
// - b
// - c
// conditions: []
// object:
// raw:
// - 123
// - 10
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 34
// - 107
// - 105
// - 110
// - 100
// - 34
// - 58
// - 32
// - 34
// - 102
// - 114
// - 101
// - 100
// - 34
// - 44
// - 10
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 34
// - 97
// - 112
// - 105
// - 86
// - 101
// - 114
// - 115
// - 105
// - 111
// - 110
// - 34
// - 58
// - 32
// - 34
// - 118
// - 49
// - 34
// - 44
// - 10
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 34
// - 109
// - 101
// - 116
// - 97
// - 100
// - 97
// - 116
// - 97
// - 34
// - 58
// - 32
// - 123
// - 10
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 34
// - 110
// - 97
// - 109
// - 101
// - 115
// - 112
// - 97
// - 99
// - 101
// - 34
// - 58
// - 32
// - 34
// - 98
// - 108
// - 101
// - 101
// - 34
// - 44
// - 10
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 34
// - 110
// - 97
// - 109
// - 101
// - 34
// - 58
// - 32
// - 34
// - 102
// - 114
// - 101
// - 100
// - 34
// - 10
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 32
// - 125
// - 125
// object: null
// `
// }

View File

@ -1,146 +1,147 @@
package resource
import (
"context"
"errors"
"fmt"
"strconv"
// BOZO!!
// import (
// "context"
// "errors"
// "fmt"
// "strconv"
"github.com/derailed/k9s/internal/k8s"
appsv1 "k8s.io/api/apps/v1"
)
// "github.com/derailed/k9s/internal/k8s"
// appsv1 "k8s.io/api/apps/v1"
// )
// Compile time checks to ensure type satisfies interface
var _ Restartable = (*Deployment)(nil)
var _ Scalable = (*Deployment)(nil)
// // Compile time checks to ensure type satisfies interface
// var _ Restartable = (*Deployment)(nil)
// var _ Scalable = (*Deployment)(nil)
// Deployment tracks a kubernetes resource.
type Deployment struct {
*Base
instance *appsv1.Deployment
}
// // Deployment tracks a kubernetes resource.
// type Deployment struct {
// *Base
// instance *appsv1.Deployment
// }
// NewDeploymentList returns a new resource list.
func NewDeploymentList(c Connection, ns string) List {
return NewList(
ns,
"deploy",
NewDeployment(c),
AllVerbsAccess|DescribeAccess,
)
}
// // NewDeploymentList returns a new resource list.
// func NewDeploymentList(c Connection, ns string) List {
// return NewList(
// ns,
// "deploy",
// NewDeployment(c),
// AllVerbsAccess|DescribeAccess,
// )
// }
// NewDeployment instantiates a new Deployment.
func NewDeployment(c Connection) *Deployment {
d := &Deployment{&Base{Connection: c, Resource: k8s.NewDeployment(c)}, nil}
d.Factory = d
// // NewDeployment instantiates a new Deployment.
// func NewDeployment(c Connection) *Deployment {
// d := &Deployment{&Base{Connection: c, Resource: k8s.NewDeployment(c)}, nil}
// d.Factory = d
return d
}
// return d
// }
// New builds a new Deployment instance from a k8s resource.
func (r *Deployment) New(i interface{}) (Columnar, error) {
c := NewDeployment(r.Connection)
switch instance := i.(type) {
case *appsv1.Deployment:
c.instance = instance
case appsv1.Deployment:
c.instance = &instance
default:
return nil, fmt.Errorf("Expecting Deployment but got %T", instance)
}
c.path = c.namespacedName(c.instance.ObjectMeta)
// // New builds a new Deployment instance from a k8s resource.
// func (r *Deployment) New(i interface{}) (Columnar, error) {
// c := NewDeployment(r.Connection)
// switch instance := i.(type) {
// case *appsv1.Deployment:
// c.instance = instance
// case appsv1.Deployment:
// c.instance = &instance
// default:
// return nil, fmt.Errorf("Expecting Deployment but got %T", instance)
// }
// c.path = c.namespacedName(c.instance.ObjectMeta)
return c, nil
}
// return c, nil
// }
// Marshal resource to yaml.
func (r *Deployment) Marshal(path string) (string, error) {
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
// // Marshal resource to yaml.
// func (r *Deployment) Marshal(path string) (string, error) {
// ns, n := Namespaced(path)
// i, err := r.Resource.Get(ns, n)
// if err != nil {
// return "", err
// }
dp, ok := i.(*appsv1.Deployment)
if !ok {
return "", errors.New("expecting dp resource")
}
dp.TypeMeta.APIVersion = "apps/v1"
dp.TypeMeta.Kind = "Deployment"
// dp, ok := i.(*appsv1.Deployment)
// if !ok {
// return "", errors.New("expecting dp resource")
// }
// dp.TypeMeta.APIVersion = "apps/v1"
// dp.TypeMeta.Kind = "Deployment"
return r.marshalObject(dp)
}
// return r.marshalObject(dp)
// }
// Logs tail logs for all pods represented by this deployment.
func (r *Deployment) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
instance, err := r.Resource.Get(opts.Namespace, opts.Name)
if err != nil {
return err
}
dp, ok := instance.(*appsv1.Deployment)
if !ok {
return errors.New("Expecting valid deployment")
}
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on deployment %s", opts.Name)
}
// // Logs tail logs for all pods represented by this deployment.
// func (r *Deployment) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
// instance, err := r.Resource.Get(opts.Namespace, opts.Name)
// if err != nil {
// return err
// }
// dp, ok := instance.(*appsv1.Deployment)
// if !ok {
// return errors.New("Expecting valid deployment")
// }
// if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
// return fmt.Errorf("No valid selector found on deployment %s", opts.Name)
// }
return r.podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)
}
// return r.podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts)
// }
// Header return resource header.
func (*Deployment) Header(ns string) Row {
var hh Row
if ns == AllNamespaces {
hh = append(hh, "NAMESPACE")
}
// // Header return resource header.
// func (*Deployment) Header(ns string) Row {
// var hh Row
// if ns == AllNamespaces {
// hh = append(hh, "NAMESPACE")
// }
return append(hh,
"NAME",
"DESIRED",
"CURRENT",
"UP-TO-DATE",
"AVAILABLE",
"AGE",
)
}
// return append(hh,
// "NAME",
// "DESIRED",
// "CURRENT",
// "UP-TO-DATE",
// "AVAILABLE",
// "AGE",
// )
// }
// NumCols designates if column is numerical.
func (*Deployment) NumCols(n string) map[string]bool {
return map[string]bool{
"DESIRED": true,
"CURRENT": true,
"UP-TO-DATE": true,
"AVAILABLE": true,
}
}
// // NumCols designates if column is numerical.
// func (*Deployment) NumCols(n string) map[string]bool {
// return map[string]bool{
// "DESIRED": true,
// "CURRENT": true,
// "UP-TO-DATE": true,
// "AVAILABLE": true,
// }
// }
// Fields retrieves displayable fields.
func (r *Deployment) Fields(ns string) Row {
ff := make([]string, 0, len(r.Header(ns)))
// // Fields retrieves displayable fields.
// func (r *Deployment) Fields(ns string) Row {
// ff := make([]string, 0, len(r.Header(ns)))
i := r.instance
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
// i := r.instance
// if ns == AllNamespaces {
// ff = append(ff, i.Namespace)
// }
return append(ff,
i.Name,
strconv.Itoa(int(*i.Spec.Replicas)),
strconv.Itoa(int(i.Status.Replicas)),
strconv.Itoa(int(i.Status.UpdatedReplicas)),
strconv.Itoa(int(i.Status.AvailableReplicas)),
toAge(i.ObjectMeta.CreationTimestamp),
)
}
// return append(ff,
// i.Name,
// strconv.Itoa(int(*i.Spec.Replicas)),
// strconv.Itoa(int(i.Status.Replicas)),
// strconv.Itoa(int(i.Status.UpdatedReplicas)),
// strconv.Itoa(int(i.Status.AvailableReplicas)),
// toAge(i.ObjectMeta.CreationTimestamp),
// )
// }
// Scale the specified resource.
func (r *Deployment) Scale(ns, n string, replicas int32) error {
return r.Resource.(Scalable).Scale(ns, n, replicas)
}
// // Scale the specified resource.
// func (r *Deployment) Scale(ns, n string, replicas int32) error {
// return r.Resource.(Scalable).Scale(ns, n, replicas)
// }
// Restart the rollout of the specified resource.
func (r *Deployment) Restart(ns, n string) error {
return r.Resource.(Restartable).Restart(ns, n)
}
// // Restart the rollout of the specified resource.
// func (r *Deployment) Restart(ns, n string) error {
// return r.Resource.(Restartable).Restart(ns, n)
// }

View File

@ -1,122 +1,123 @@
package resource_test
import (
"testing"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func NewDeploymentListWithArgs(ns string, r *resource.Deployment) resource.List {
return resource.NewList(ns, "deploy", r, resource.AllVerbsAccess|resource.DescribeAccess)
}
func NewDeploymentWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Deployment {
r := &resource.Deployment{Base: resource.NewBase(conn, res)}
r.Factory = r
return r
}
func TestDeploymentListAccess(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
ns := "blee"
l := NewDeploymentListWithArgs(resource.AllNamespaces, NewDeploymentWithArgs(mc, mr))
l.SetNamespace(ns)
assert.Equal(t, "blee", l.GetNamespace())
assert.Equal(t, "deploy", l.GetName())
for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
assert.True(t, l.Access(a))
}
}
func TestDeploymentFields(t *testing.T) {
r := newDeployment().Fields("blee")
assert.Equal(t, "fred", r[0])
}
func TestDeploymentMarshal(t *testing.T) {
mc := NewMockConnection()
mr := NewMockCruder()
m.When(mr.Get("blee", "fred")).ThenReturn(k8sDeployment(), nil)
cm := NewDeploymentWithArgs(mc, mr)
ma, err := cm.Marshal("blee/fred")
mr.VerifyWasCalledOnce().Get("blee", "fred")
assert.Nil(t, err)
assert.Equal(t, dpYaml(), ma)
}
// BOZO!!
// func TestDeploymentListData(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sDeployment()}, nil)
// import (
// "testing"
// l := NewDeploymentListWithArgs("-", NewDeploymentWithArgs(mc, mr))
// // Make sure we can get deltas!
// for i := 0; i < 2; i++ {
// err := l.Reconcile(nil, "", "")
// assert.Nil(t, err)
// }
// "github.com/derailed/k9s/internal/k8s"
// "github.com/derailed/k9s/internal/resource"
// m "github.com/petergtz/pegomock"
// "github.com/stretchr/testify/assert"
// appsv1 "k8s.io/api/apps/v1"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
// mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{})
// td := l.Data()
// assert.Equal(t, 1, len(td.Rows))
// assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
// row := td.Rows["blee/fred"]
// assert.Equal(t, 6, len(row.Deltas))
// for _, d := range row.Deltas {
// assert.Equal(t, "", d)
// }
// assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
// func NewDeploymentListWithArgs(ns string, r *resource.Deployment) resource.List {
// return resource.NewList(ns, "deploy", r, resource.AllVerbsAccess|resource.DescribeAccess)
// }
// Helpers...
// func NewDeploymentWithArgs(conn k8s.Connection, res resource.Cruder) *resource.Deployment {
// r := &resource.Deployment{Base: resource.NewBase(conn, res)}
// r.Factory = r
// return r
// }
func k8sDeployment() *appsv1.Deployment {
var i int32 = 1
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "blee",
Name: "fred",
CreationTimestamp: metav1.Time{Time: testTime()},
},
Spec: appsv1.DeploymentSpec{
Replicas: &i,
},
}
}
// func TestDeploymentListAccess(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
func newDeployment() resource.Columnar {
mc := NewMockConnection()
c, _ := resource.NewDeployment(mc).New(k8sDeployment())
return c
}
// ns := "blee"
// l := NewDeploymentListWithArgs(resource.AllNamespaces, NewDeploymentWithArgs(mc, mr))
// l.SetNamespace(ns)
func dpYaml() string {
return `apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: "2018-12-14T17:36:43Z"
name: fred
namespace: blee
spec:
replicas: 1
selector: null
strategy: {}
template:
metadata:
creationTimestamp: null
spec:
containers: null
status: {}
`
}
// assert.Equal(t, "blee", l.GetNamespace())
// assert.Equal(t, "deploy", l.GetName())
// for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
// assert.True(t, l.Access(a))
// }
// }
// func TestDeploymentFields(t *testing.T) {
// r := newDeployment().Fields("blee")
// assert.Equal(t, "fred", r[0])
// }
// func TestDeploymentMarshal(t *testing.T) {
// mc := NewMockConnection()
// mr := NewMockCruder()
// m.When(mr.Get("blee", "fred")).ThenReturn(k8sDeployment(), nil)
// cm := NewDeploymentWithArgs(mc, mr)
// ma, err := cm.Marshal("blee/fred")
// mr.VerifyWasCalledOnce().Get("blee", "fred")
// assert.Nil(t, err)
// assert.Equal(t, dpYaml(), ma)
// }
// // BOZO!!
// // func TestDeploymentListData(t *testing.T) {
// // mc := NewMockConnection()
// // mr := NewMockCruder()
// // m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sDeployment()}, nil)
// // l := NewDeploymentListWithArgs("-", NewDeploymentWithArgs(mc, mr))
// // // Make sure we can get deltas!
// // for i := 0; i < 2; i++ {
// // err := l.Reconcile(nil, "", "")
// // assert.Nil(t, err)
// // }
// // mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{})
// // td := l.Data()
// // assert.Equal(t, 1, len(td.Rows))
// // assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
// // row := td.Rows["blee/fred"]
// // assert.Equal(t, 6, len(row.Deltas))
// // for _, d := range row.Deltas {
// // assert.Equal(t, "", d)
// // }
// // assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
// // }
// // Helpers...
// func k8sDeployment() *appsv1.Deployment {
// var i int32 = 1
// return &appsv1.Deployment{
// ObjectMeta: metav1.ObjectMeta{
// Namespace: "blee",
// Name: "fred",
// CreationTimestamp: metav1.Time{Time: testTime()},
// },
// Spec: appsv1.DeploymentSpec{
// Replicas: &i,
// },
// }
// }
// func newDeployment() resource.Columnar {
// mc := NewMockConnection()
// c, _ := resource.NewDeployment(mc).New(k8sDeployment())
// return c
// }
// func dpYaml() string {
// return `apiVersion: apps/v1
// kind: Deployment
// metadata:
// creationTimestamp: "2018-12-14T17:36:43Z"
// name: fred
// namespace: blee
// spec:
// replicas: 1
// selector: null
// strategy: {}
// template:
// metadata:
// creationTimestamp: null
// spec:
// containers: null
// status: {}
// `
// }

View File

@ -1,195 +1,196 @@
package resource
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
// BOZO!!
// import (
// "context"
// "errors"
// "fmt"
// "strconv"
// "strings"
// "time"
"github.com/derailed/k9s/internal/k8s"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/duration"
)
// "github.com/derailed/k9s/internal/k8s"
// batchv1 "k8s.io/api/batch/v1"
// v1 "k8s.io/api/core/v1"
// "k8s.io/apimachinery/pkg/util/duration"
// )
// Job tracks a kubernetes resource.
type Job struct {
*Base
// // Job tracks a kubernetes resource.
// type Job struct {
// *Base
instance *batchv1.Job
}
// instance *batchv1.Job
// }
// NewJobList returns a new resource list.
func NewJobList(c Connection, ns string) List {
return NewList(
ns,
"job",
NewJob(c),
AllVerbsAccess|DescribeAccess,
)
}
// // NewJobList returns a new resource list.
// func NewJobList(c Connection, ns string) List {
// return NewList(
// ns,
// "job",
// NewJob(c),
// AllVerbsAccess|DescribeAccess,
// )
// }
// NewJob instantiates a new Job.
func NewJob(c Connection) *Job {
j := &Job{
Base: &Base{Connection: c, Resource: k8s.NewJob(c)},
}
j.Factory = j
// // NewJob instantiates a new Job.
// func NewJob(c Connection) *Job {
// j := &Job{
// Base: &Base{Connection: c, Resource: k8s.NewJob(c)},
// }
// j.Factory = j
return j
}
// return j
// }
// New builds a new Job instance from a k8s resource.
func (r *Job) New(i interface{}) (Columnar, error) {
c := NewJob(r.Connection)
switch instance := i.(type) {
case *batchv1.Job:
c.instance = instance
case batchv1.Job:
c.instance = &instance
default:
return nil, fmt.Errorf("Expecting Job but got %T", instance)
}
c.path = c.namespacedName(c.instance.ObjectMeta)
// // New builds a new Job instance from a k8s resource.
// func (r *Job) New(i interface{}) (Columnar, error) {
// c := NewJob(r.Connection)
// switch instance := i.(type) {
// case *batchv1.Job:
// c.instance = instance
// case batchv1.Job:
// c.instance = &instance
// default:
// return nil, fmt.Errorf("Expecting Job but got %T", instance)
// }
// c.path = c.namespacedName(c.instance.ObjectMeta)
return c, nil
}
// return c, nil
// }
// Marshal resource to yaml.
func (r *Job) Marshal(path string) (string, error) {
ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n)
if err != nil {
return "", err
}
// // Marshal resource to yaml.
// func (r *Job) Marshal(path string) (string, error) {
// ns, n := Namespaced(path)
// i, err := r.Resource.Get(ns, n)
// if err != nil {
// return "", err
// }
jo, ok := i.(*batchv1.Job)
if !ok {
return "", errors.New("expecting job resource")
}
jo.TypeMeta.APIVersion = "extensions/v1beta1"
jo.TypeMeta.Kind = "Job"
// jo, ok := i.(*batchv1.Job)
// if !ok {
// return "", errors.New("expecting job resource")
// }
// jo.TypeMeta.APIVersion = "extensions/v1beta1"
// jo.TypeMeta.Kind = "Job"
return r.marshalObject(jo)
}
// return r.marshalObject(jo)
// }
// Containers fetch all the containers on this job, may include init containers.
func (r *Job) Containers(path string, includeInit bool) ([]string, error) {
ns, n := Namespaced(path)
// // Containers fetch all the containers on this job, may include init containers.
// func (r *Job) Containers(path string, includeInit bool) ([]string, error) {
// ns, n := Namespaced(path)
return r.Resource.(k8s.Loggable).Containers(ns, n, includeInit)
}
// return r.Resource.(k8s.Loggable).Containers(ns, n, includeInit)
// }
// Logs retrieves logs for a given container.
func (r *Job) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
instance, err := r.Resource.Get(opts.Namespace, opts.Name)
if err != nil {
return err
}
jo, ok := instance.(*batchv1.Job)
if !ok {
return errors.New("expecting job resource")
}
if jo.Spec.Selector == nil || len(jo.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on job %s", opts.FQN())
}
// // Logs retrieves logs for a given container.
// func (r *Job) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
// instance, err := r.Resource.Get(opts.Namespace, opts.Name)
// if err != nil {
// return err
// }
// jo, ok := instance.(*batchv1.Job)
// if !ok {
// return errors.New("expecting job resource")
// }
// if jo.Spec.Selector == nil || len(jo.Spec.Selector.MatchLabels) == 0 {
// return fmt.Errorf("No valid selector found on job %s", opts.FQN())
// }
return r.podLogs(ctx, c, jo.Spec.Selector.MatchLabels, opts)
}
// return r.podLogs(ctx, c, jo.Spec.Selector.MatchLabels, opts)
// }
// Header return resource header.
func (*Job) Header(ns string) Row {
hh := Row{}
if ns == AllNamespaces {
hh = append(hh, "NAMESPACE")
}
// // Header return resource header.
// func (*Job) Header(ns string) Row {
// hh := Row{}
// if ns == AllNamespaces {
// hh = append(hh, "NAMESPACE")
// }
return append(hh, "NAME", "COMPLETIONS", "DURATION", "CONTAINERS", "IMAGES", "AGE")
}
// return append(hh, "NAME", "COMPLETIONS", "DURATION", "CONTAINERS", "IMAGES", "AGE")
// }
// Fields retrieves displayable fields.
func (r *Job) Fields(ns string) Row {
ff := make([]string, 0, len(r.Header(ns)))
// // Fields retrieves displayable fields.
// func (r *Job) Fields(ns string) Row {
// ff := make([]string, 0, len(r.Header(ns)))
i := r.instance
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
}
// i := r.instance
// if ns == AllNamespaces {
// ff = append(ff, i.Namespace)
// }
cc, ii := r.toContainers(i.Spec.Template.Spec)
// cc, ii := r.toContainers(i.Spec.Template.Spec)
return append(ff,
i.Name,
r.toCompletion(i.Spec, i.Status),
r.toDuration(i.Status),
cc,
ii,
toAge(i.ObjectMeta.CreationTimestamp),
)
}
// return append(ff,
// i.Name,
// r.toCompletion(i.Spec, i.Status),
// r.toDuration(i.Status),
// cc,
// ii,
// toAge(i.ObjectMeta.CreationTimestamp),
// )
// }
// ----------------------------------------------------------------------------
// Helpers...
// // ----------------------------------------------------------------------------
// // Helpers...
const maxShow = 2
// const maxShow = 2
func (*Job) toContainers(p v1.PodSpec) (string, string) {
cc, ii := parseContainers(p.InitContainers)
cn, ci := parseContainers(p.Containers)
// func (*Job) toContainers(p v1.PodSpec) (string, string) {
// cc, ii := parseContainers(p.InitContainers)
// cn, ci := parseContainers(p.Containers)
cc, ii = append(cc, cn...), append(ii, ci...)
// cc, ii = append(cc, cn...), append(ii, ci...)
// Limit to 2 of each...
if len(cc) > maxShow {
cc = append(cc[:2], "(+"+strconv.Itoa(len(cc)-maxShow)+")...")
}
if len(ii) > maxShow {
ii = append(ii[:2], "(+"+strconv.Itoa(len(ii)-maxShow)+")...")
}
// // Limit to 2 of each...
// if len(cc) > maxShow {
// cc = append(cc[:2], "(+"+strconv.Itoa(len(cc)-maxShow)+")...")
// }
// if len(ii) > maxShow {
// ii = append(ii[:2], "(+"+strconv.Itoa(len(ii)-maxShow)+")...")
// }
return strings.Join(cc, ","), strings.Join(ii, ",")
}
// return strings.Join(cc, ","), strings.Join(ii, ",")
// }
func parseContainers(cos []v1.Container) (nn, ii []string) {
for _, co := range cos {
nn = append(nn, co.Name)
ii = append(ii, co.Image)
}
// func parseContainers(cos []v1.Container) (nn, ii []string) {
// for _, co := range cos {
// nn = append(nn, co.Name)
// ii = append(ii, co.Image)
// }
return nn, ii
}
// return nn, ii
// }
func (*Job) toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) {
if spec.Completions != nil {
return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions))
}
// func (*Job) toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) {
// if spec.Completions != nil {
// return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions))
// }
if spec.Parallelism == nil {
return strconv.Itoa(int(status.Succeeded)) + "/1"
}
// if spec.Parallelism == nil {
// return strconv.Itoa(int(status.Succeeded)) + "/1"
// }
p := *spec.Parallelism
if p > 1 {
return strconv.Itoa(int(status.Succeeded)) + "/1 of " + strconv.Itoa(int(p))
}
// p := *spec.Parallelism
// if p > 1 {
// return strconv.Itoa(int(status.Succeeded)) + "/1 of " + strconv.Itoa(int(p))
// }
return strconv.Itoa(int(status.Succeeded)) + "/1"
}
// return strconv.Itoa(int(status.Succeeded)) + "/1"
// }
func (*Job) toDuration(status batchv1.JobStatus) string {
if status.StartTime == nil {
return MissingValue
}
// func (*Job) toDuration(status batchv1.JobStatus) string {
// if status.StartTime == nil {
// return MissingValue
// }
var d time.Duration
switch {
case status.CompletionTime == nil:
d = time.Since(status.StartTime.Time)
default:
d = status.CompletionTime.Sub(status.StartTime.Time)
}
// var d time.Duration
// switch {
// case status.CompletionTime == nil:
// d = time.Since(status.StartTime.Time)
// default:
// d = status.CompletionTime.Sub(status.StartTime.Time)
// }
return duration.HumanDuration(d)
}
// return duration.HumanDuration(d)
// }

View File

@ -1,153 +1,154 @@
package resource
import (
"testing"
"time"
// BOZO!!
// import (
// "testing"
// "time"
"github.com/stretchr/testify/assert"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// "github.com/stretchr/testify/assert"
// batchv1 "k8s.io/api/batch/v1"
// v1 "k8s.io/api/core/v1"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
func TestJobToCompletion(t *testing.T) {
t0 := testTime()
t1, t2 := metav1.Time{Time: t0}, metav1.Time{Time: t0.Add(10 * time.Second)}
var c, p int32 = 10, 20
// func TestJobToCompletion(t *testing.T) {
// t0 := testTime()
// t1, t2 := metav1.Time{Time: t0}, metav1.Time{Time: t0.Add(10 * time.Second)}
// var c, p int32 = 10, 20
uu := []struct {
j batchv1.JobSpec
s batchv1.JobStatus
e string
}{
{
batchv1.JobSpec{
Completions: &c,
Parallelism: &p,
},
batchv1.JobStatus{
Succeeded: 1,
Active: 1,
Failed: 0,
StartTime: &t1,
CompletionTime: &t2,
},
"1/10",
},
{
batchv1.JobSpec{
Parallelism: &p,
},
batchv1.JobStatus{
Succeeded: 1,
Active: 1,
Failed: 0,
StartTime: &t1,
CompletionTime: &t2,
},
"1/1 of 20",
},
{
batchv1.JobSpec{
Completions: &c,
},
batchv1.JobStatus{
Succeeded: 1,
Active: 1,
Failed: 0,
StartTime: &t1,
CompletionTime: &t2,
},
"1/10",
},
{
batchv1.JobSpec{},
batchv1.JobStatus{
Succeeded: 1,
Active: 1,
Failed: 0,
StartTime: &t1,
CompletionTime: &t2,
},
"1/1",
},
}
// uu := []struct {
// j batchv1.JobSpec
// s batchv1.JobStatus
// e string
// }{
// {
// batchv1.JobSpec{
// Completions: &c,
// Parallelism: &p,
// },
// batchv1.JobStatus{
// Succeeded: 1,
// Active: 1,
// Failed: 0,
// StartTime: &t1,
// CompletionTime: &t2,
// },
// "1/10",
// },
// {
// batchv1.JobSpec{
// Parallelism: &p,
// },
// batchv1.JobStatus{
// Succeeded: 1,
// Active: 1,
// Failed: 0,
// StartTime: &t1,
// CompletionTime: &t2,
// },
// "1/1 of 20",
// },
// {
// batchv1.JobSpec{
// Completions: &c,
// },
// batchv1.JobStatus{
// Succeeded: 1,
// Active: 1,
// Failed: 0,
// StartTime: &t1,
// CompletionTime: &t2,
// },
// "1/10",
// },
// {
// batchv1.JobSpec{},
// batchv1.JobStatus{
// Succeeded: 1,
// Active: 1,
// Failed: 0,
// StartTime: &t1,
// CompletionTime: &t2,
// },
// "1/1",
// },
// }
var j *Job
for _, u := range uu {
assert.Equal(t, u.e, j.toCompletion(u.j, u.s))
}
}
// var j *Job
// for _, u := range uu {
// assert.Equal(t, u.e, j.toCompletion(u.j, u.s))
// }
// }
func TestJobToDuration(t *testing.T) {
t0 := testTime().UTC()
t1, t2 := metav1.Time{Time: t0}, metav1.Time{Time: t0.Add(10 * time.Second)}
// func TestJobToDuration(t *testing.T) {
// t0 := testTime().UTC()
// t1, t2 := metav1.Time{Time: t0}, metav1.Time{Time: t0.Add(10 * time.Second)}
uu := []struct {
s batchv1.JobStatus
e string
}{
{
batchv1.JobStatus{
StartTime: &t1,
CompletionTime: &t2,
},
"10s",
},
{
batchv1.JobStatus{
StartTime: &metav1.Time{Time: time.Now().Add(-10 * time.Second)},
},
"10s",
},
{
batchv1.JobStatus{
CompletionTime: &t2,
},
MissingValue,
},
}
// uu := []struct {
// s batchv1.JobStatus
// e string
// }{
// {
// batchv1.JobStatus{
// StartTime: &t1,
// CompletionTime: &t2,
// },
// "10s",
// },
// {
// batchv1.JobStatus{
// StartTime: &metav1.Time{Time: time.Now().Add(-10 * time.Second)},
// },
// "10s",
// },
// {
// batchv1.JobStatus{
// CompletionTime: &t2,
// },
// MissingValue,
// },
// }
var j *Job
for _, u := range uu {
assert.Equal(t, u.e, j.toDuration(u.s))
}
}
// var j *Job
// for _, u := range uu {
// assert.Equal(t, u.e, j.toDuration(u.s))
// }
// }
func TestJobToContainers(t *testing.T) {
uu := []struct {
s v1.PodSpec
c, i string
}{
{
v1.PodSpec{
InitContainers: []v1.Container{
{Name: "i1", Image: "fred"},
},
Containers: []v1.Container{
{Name: "c1", Image: "blee"},
},
},
"i1,c1", "fred,blee",
},
{
v1.PodSpec{
InitContainers: []v1.Container{
{Name: "i1", Image: "fred"},
},
Containers: []v1.Container{
{Name: "c1", Image: "blee"},
{Name: "c2", Image: "duh"},
},
},
"i1,c1,(+1)...", "fred,blee,(+1)...",
},
}
// func TestJobToContainers(t *testing.T) {
// uu := []struct {
// s v1.PodSpec
// c, i string
// }{
// {
// v1.PodSpec{
// InitContainers: []v1.Container{
// {Name: "i1", Image: "fred"},
// },
// Containers: []v1.Container{
// {Name: "c1", Image: "blee"},
// },
// },
// "i1,c1", "fred,blee",
// },
// {
// v1.PodSpec{
// InitContainers: []v1.Container{
// {Name: "i1", Image: "fred"},
// },
// Containers: []v1.Container{
// {Name: "c1", Image: "blee"},
// {Name: "c2", Image: "duh"},
// },
// },
// "i1,c1,(+1)...", "fred,blee,(+1)...",
// },
// }
var j *Job
for _, u := range uu {
c, i := j.toContainers(u.s)
assert.Equal(t, u.c, c)
assert.Equal(t, u.i, i)
}
}
// var j *Job
// for _, u := range uu {
// c, i := j.toContainers(u.s)
// assert.Equal(t, u.c, c)
// assert.Equal(t, u.i, i)
// }
// }

Some files were not shown because too many files have changed in this diff Show More