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 - atomicalign
enable-all: false enable-all: false
disable: disable:
- shadow # - shadow
disable-all: false disable-all: false
golint: golint:
# minimal confidence for issues, default is 0.8 # 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["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings" a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles" 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["np"] = "networking.k8s.io/v1/networkpolicies"
{ {
a.Alias["ctx"] = contexts a.Alias["ctx"] = contexts

View File

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

View File

@ -132,7 +132,7 @@ func newStyle() Style {
Body: newBody(), Body: newBody(),
Frame: newFrame(), Frame: newFrame(),
Info: newInfo(), Info: newInfo(),
Table: newTable(), Table: newGetTable(),
Views: newViews(), Views: newViews(),
} }
} }
@ -211,7 +211,7 @@ func newInfo() Info {
} }
// NewTable returns a new table style. // NewTable returns a new table style.
func newTable() Table { func newGetTable() Table {
return Table{ return Table{
FgColor: "aqua", FgColor: "aqua",
BgColor: "black", BgColor: "black",
@ -293,7 +293,7 @@ func (s *Styles) Title() Title {
} }
// Table returns table styles. // Table returns table styles.
func (s *Styles) Table() Table { func (s *Styles) GetTable() Table {
return s.K9s.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, "cadetblue", s.Body().FgColor)
assert.Equal(t, "black", s.Body().BgColor) 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.ColorCadetBlue, s.FgColor())
assert.Equal(t, tcell.ColorBlack, s.BgColor()) assert.Equal(t, tcell.ColorBlack, s.BgColor())
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor) 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, "white", s.Body().FgColor)
assert.Equal(t, "black", s.Body().BgColor) 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.ColorWhite, s.FgColor())
assert.Equal(t, tcell.ColorBlack, s.BgColor()) assert.Equal(t, tcell.ColorBlack, s.BgColor())
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor) 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 { type Context struct {
Resource Generic
} }
var _ Accessor = &Context{} var _ Accessor = &Context{}
@ -47,16 +47,16 @@ func (c *Context) List(string, metav1.ListOptions) ([]runtime.Object, error) {
} }
// Delete a Context. // 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() ctx, err := c.config().CurrentContextName()
if err != nil { if err != nil {
return err return err
} }
if ctx == n { if ctx == path {
return fmt.Errorf("trying to delete your current context %s", n) 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. // 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 return "", err
} }
log.Debug().Msgf("DESCRIBE FOR %q -- %q:%q", gvr, ns, n)
return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true}) return d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})
} }

View File

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

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch" "github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
@ -20,7 +21,7 @@ import (
) )
type DaemonSet struct { type DaemonSet struct {
Resource Generic
} }
var _ Accessor = &DaemonSet{} var _ Accessor = &DaemonSet{}
@ -28,8 +29,8 @@ var _ Loggable = &DaemonSet{}
var _ Restartable = &DaemonSet{} var _ Restartable = &DaemonSet{}
// Restart a DaemonSet rollout. // Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(ns, n string) error { func (d *DaemonSet) Restart(path string) error {
o, err := d.Get(ns, string(d.gvr), n, labels.Everything()) o, err := d.Get(string(d.gvr), path, labels.Everything())
if err != nil { if err != nil {
return err return err
} }
@ -45,14 +46,14 @@ func (d *DaemonSet) Restart(ns, n string) error {
return err 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 return err
} }
// Logs tail logs for all pods represented by this DaemonSet. // Logs tail logs for all pods represented by this DaemonSet.
func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing DaemonSet %q -- %q", opts.Namespace, opts.Name) log.Debug().Msgf("Tailing DaemonSet %q", opts.Path)
o, err := d.Get(opts.Namespace, "apps/v1/daemonsets", opts.Name, labels.Everything()) o, err := d.Get("apps/v1/daemonsets", opts.Path, labels.Everything())
if err != nil { if err != nil {
return err 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 { 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) 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 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 { if err != nil {
return err return err
} }
@ -94,17 +96,17 @@ func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts L
} }
po := Pod{} po := Pod{}
po.Init(f, "v1/pods")
for _, o := range oo { for _, o := range oo {
var pod v1.Pod var pod v1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
if err != nil { if err != nil {
return err return err
} }
if pod.Status.Phase == v1.PodRunning { log.Debug().Msgf("TAILING logs on pod %q", pod.Name)
opts.Namespace, opts.Name = pod.Namespace, pod.Name opts.Path = k8s.FQN(pod.Namespace, pod.Name)
if err := po.TailLogs(ctx, c, opts); err != nil { if err := po.TailLogs(ctx, c, opts); err != nil {
return err return err
}
} }
} }
return nil 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 package dao
import ( import (
"path"
"strings" "strings"
"github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/tview" "github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
) )
type ( // LogOptions represent logger options.
// Fqn uniquely describes a container type LogOptions struct {
Fqn struct { Path string
Namespace, Name, Container string Container string
} Lines int64
Color color.Paint
// LogOptions represent logger options. Previous bool
LogOptions struct { SingleContainer bool
Fqn MultiPods bool
}
Lines int64
Color color.Paint
Previous bool
SingleContainer bool
MultiPods bool
}
)
// HasContainer checks if a container is present. // HasContainer checks if a container is present.
func (o LogOptions) HasContainer() bool { func (o LogOptions) HasContainer() bool {
return o.Container != "" 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. // FixedSizeName returns a normalize fixed size pod name if possible.
func (o LogOptions) FixedSizeName() string { func (o LogOptions) FixedSizeName() string {
tokens := strings.Split(o.Name, "-") _, n := k8s.Namespaced(o.Path)
tokens := strings.Split(n, "-")
if len(tokens) < 3 { if len(tokens) < 3 {
return o.Name return n
} }
var s []string var s []string
for i := 0; i < len(tokens)-1; i++ { for i := 0; i < len(tokens)-1; i++ {
s = append(s, tokens[i]) s = append(s, tokens[i])
} }
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1] 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. // DecorateLog add a log header to display po/co information along with the log message.
func (o LogOptions) DecorateLog(msg string) string { func (o LogOptions) DecorateLog(msg string) string {
_, n := k8s.Namespaced(o.Path)
if msg == "" { if msg == "" {
return msg return msg
} }
if o.MultiPods { if o.MultiPods {
return colorize(o.Color, o.Name+":"+o.Container+" ") + msg return colorize(o.Color, n+":"+o.Container+" ") + msg
} }
if !o.SingleContainer { if !o.SingleContainer {
@ -88,17 +74,18 @@ func Truncate(str string, width int) string {
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
} }
// Namespaced return a namesapace and a name. // BOZO!!
func Namespaced(n string) (string, string) { // // Namespaced return a namesapace and a name.
ns, po := path.Split(n) // 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. // // FQN returns a fully qualified resource name.
func FQN(ns, n string) string { // func FQN(ns, n string) string {
if ns == "" { // if ns == "" {
return n // return n
} // }
return ns + "/" + 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"
"github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch" "github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@ -22,24 +23,23 @@ import (
const defaultTimeout = 1 * time.Second const defaultTimeout = 1 * time.Second
type Logger interface { // Pod represents a pod resource.
Logs(ns, n string, opts *v1.PodLogOptions) *restclient.Request
}
type Pod struct { type Pod struct {
Resource Generic
} }
var _ Accessor = &Pod{} var _ Accessor = &Pod{}
var _Loggable = &Pod{}
// Logs fetch container logs for a given pod and container. // 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) return p.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts)
} }
// Containers returns all container names on pod // Containers returns all container names on pod
func (p *Pod) Containers(ns, n string, includeInit bool) ([]string, error) { func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
o, err := p.Get(ns, "v1/pod", n, labels.Everything()) o, err := p.Get("v1/pod", path, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,8 +78,7 @@ func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error
if !ok { if !ok {
return errors.New("Expecting an informer") return errors.New("Expecting an informer")
} }
ns, n := Namespaced(opts.FQN()) o, err := fac.Get("v1/pods", opts.Path, labels.Everything())
o, err := fac.Get(ns, "v1/pods", n, labels.Everything())
if err != nil { if err != nil {
return err 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 { 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{ o := v1.PodLogOptions{
Container: opts.Container, Container: opts.Container,
Follow: true, Follow: true,
TailLines: &opts.Lines, TailLines: &opts.Lines,
Previous: opts.Previous, Previous: opts.Previous,
} }
req := logger.Logs(opts.Namespace, opts.Name, &o) req := logger.Logs(opts.Path, &o)
ctxt, cancelFunc := context.WithCancel(ctx) ctxt, cancelFunc := context.WithCancel(ctx)
req.Context(ctxt) req.Context(ctxt)
@ -132,8 +131,8 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptio
stream, err := req.Stream() stream, err := req.Stream()
atomic.StoreInt32(&blocked, 0) atomic.StoreInt32(&blocked, 0)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Log stream failed 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()) return fmt.Errorf("Unable to obtain log stream for %s", opts.Path)
} }
go readLogs(ctx, stream, c, opts) 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) { func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) {
defer func() { defer func() {
log.Debug().Msgf(">>> Closing stream `%s", opts.Path()) log.Debug().Msgf(">>> Closing stream `%s", opts.Path)
if err := stream.Close(); err != nil { if err := stream.Close(); err != nil {
log.Error().Err(err).Msg("Cloing stream") 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 ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
@ -12,14 +13,14 @@ import (
// Reconcile previous vs current state and emits delta events. // Reconcile previous vs current state and emits delta events.
func Reconcile(ctx context.Context, table render.TableData, gvr GVR) (render.TableData, error) { 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 { if !ok {
return table, fmt.Errorf("no path specified for %s", gvr) 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) log.Debug().Msgf(" Reconcile %q in ns %q with path %q", gvr, table.Namespace, path)
factory, ok := ctx.Value(internal.KeyFactory).(Factory) factory, ok := ctx.Value(internal.KeyFactory).(Factory)
if !ok { 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) table.Header = m.Renderer.Header(table.Namespace)
oo, err := m.Model.List(ctx) oo, err := m.Model.List(ctx)
if err != nil { if err != nil {
panic(err) return table, err
} }
log.Debug().Msgf("Model returned [%d] items", len(oo)) log.Debug().Msgf("Model returned [%d] items", len(oo))
rows := make(render.Rows, 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 { if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil {
return table, err return table, err
} }
@ -65,7 +67,7 @@ func update(table *render.TableData, rows render.Rows) {
continue continue
} }
if index, ok := table.RowEvents.FindIndex(row.ID); ok { 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() { if delta.IsBlank() {
table.RowEvents[index].Kind, table.RowEvents[index].Deltas = render.EventUnchanged, blankDelta table.RowEvents[index].Kind, table.RowEvents[index].Deltas = render.EventUnchanged, blankDelta
} else { } else {

View File

@ -15,21 +15,35 @@ import (
// MetaViewers represents a collection of meta viewers. // MetaViewers represents a collection of meta viewers.
type ResourceMetas map[GVR]metav1.APIResource type ResourceMetas map[GVR]metav1.APIResource
// Accessors represents a collection of dao accessors.
type Accessors map[GVR]Accessor
var resMetas ResourceMetas 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) { func AccessorFor(f Factory, gvr GVR) (Accessor, error) {
m := map[GVR]Accessor{ m := Accessors{
"alias": &Alias{},
"contexts": &Context{}, "contexts": &Context{},
"containers": &Container{},
"screendumps": &ScreenDump{}, "screendumps": &ScreenDump{},
"benchmarks": &Benchmark{},
"portforwards": &PortForward{},
"v1/services": &Service{},
"v1/pods": &Pod{},
"apps/v1/deployments": &Deployment{}, "apps/v1/deployments": &Deployment{},
"apps/v1/daemonsets": &DaemonSet{}, "apps/v1/daemonsets": &DaemonSet{},
"extensions/v1beta1/daemonsets": &DaemonSet{}, "extensions/v1beta1/daemonsets": &DaemonSet{},
"apps/v1/statefulsets": &StatefulSet{}, "apps/v1/statefulsets": &StatefulSet{},
"batch/v1beta1/cronjobs": &CronJob{},
"batch/v1/jobs": &Job{},
} }
r, ok := m[gvr] r, ok := m[gvr]
if !ok { if !ok {
r = &Resource{} r = &Generic{}
log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr) log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr)
} }
r.Init(f, gvr) r.Init(f, gvr)
@ -56,6 +70,17 @@ func MetaFor(gvr GVR) (metav1.APIResource, error) {
return m, nil 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. // Load hydrates server preferred+CRDs resource metadata.
func Load(f *watch.Factory) error { func Load(f *watch.Factory) error {
resMetas = make(ResourceMetas, 100) resMetas = make(ResourceMetas, 100)
@ -70,23 +95,82 @@ func Load(f *watch.Factory) error {
} }
func loadNonResource(m ResourceMetas) 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{ m["contexts"] = metav1.APIResource{
Name: "contexts", Name: "contexts",
SingularName: "context", SingularName: "context",
Namespaced: false, Namespaced: false,
Kind: "Context", Kind: "Contexts",
ShortNames: []string{"ctx"}, ShortNames: []string{"ctx"},
Verbs: []string{}, Verbs: []string{},
Categories: []string{"K9s"}, Categories: []string{"k9s"},
} }
m["screendumps"] = metav1.APIResource{ m["screendumps"] = metav1.APIResource{
Name: "screendumps", Name: "screendumps",
SingularName: "screendump", SingularName: "screendump",
Namespaced: false, Namespaced: false,
Kind: "ScreenDump", Kind: "ScreenDumps",
ShortNames: []string{"sd"}, ShortNames: []string{"sd"},
Verbs: []string{"delete"}, 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 return nil
@ -113,7 +197,7 @@ func loadPreferred(f *watch.Factory, m ResourceMetas) error {
} }
func loadCRDs(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 { if err != nil {
return err 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 ( import (
"os" "os"
"path/filepath"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type ScreenDump struct { type ScreenDump struct {
Resource Generic
} }
var _ Accessor = &ScreenDump{} var _ Accessor = &ScreenDump{}
var _ Nuker = &ScreenDump{} var _ Nuker = &ScreenDump{}
// Delete a ScreenDump. // Delete a ScreenDump.
func (d *ScreenDump) Delete(dir, sel string, cascade, force bool) error { func (d *ScreenDump) Delete(path string, cascade, force bool) error {
log.Debug().Msgf("ScreenDump DELETE %q:%q", dir, sel) log.Debug().Msgf("ScreenDump DELETE %q", path)
return os.Remove(filepath.Join("/"+dir, sel)) return os.Remove(path)
} }

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/derailed/k9s/internal/k8s"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -16,7 +17,7 @@ import (
) )
type StatefulSet struct { type StatefulSet struct {
Resource Generic
} }
var _ Accessor = &StatefulSet{} var _ Accessor = &StatefulSet{}
@ -25,7 +26,8 @@ var _ Restartable = &StatefulSet{}
var _ Scalable = &StatefulSet{} var _ Scalable = &StatefulSet{}
// Scale a 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{}) scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{})
if err != nil { if err != nil {
return err return err
@ -37,8 +39,8 @@ func (s *StatefulSet) Scale(ns, n string, replicas int32) error {
} }
// Restart a StatefulSet rollout. // Restart a StatefulSet rollout.
func (s *StatefulSet) Restart(ns, n string) error { func (s *StatefulSet) Restart(path string) error {
o, err := s.Get(ns, string(s.gvr), n, labels.Everything()) o, err := s.Get(string(s.gvr), path, labels.Everything())
if err != nil { if err != nil {
return err return err
} }
@ -54,14 +56,14 @@ func (s *StatefulSet) Restart(ns, n string) error {
return err 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 return err
} }
// Logs tail logs for all pods represented by this StatefulSet. // Logs tail logs for all pods represented by this StatefulSet.
func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
log.Debug().Msgf("Tailing StatefulSet %q -- %q", opts.Namespace, opts.Name) log.Debug().Msgf("Tailing StatefulSet %q", opts.Path)
o, err := s.Get(opts.Namespace, string(s.gvr), opts.Name, labels.Everything()) o, err := s.Get(string(s.gvr), opts.Path, labels.Everything())
if err != nil { if err != nil {
return err 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 { 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) 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" "context"
"github.com/derailed/k9s/internal/k8s" "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/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
restclient "k8s.io/client-go/rest"
) )
type Factory interface { type Factory interface {
@ -15,7 +18,7 @@ type Factory interface {
Client() k8s.Connection Client() k8s.Connection
// Get fetch a given resource. // 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 fetch a collection of resources.
List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error) List(ns, gvr string, sel labels.Selector) ([]runtime.Object, error)
@ -25,6 +28,12 @@ type Factory interface {
// WaitForCacheSync synchronize the cache. // WaitForCacheSync synchronize the cache.
WaitForCacheSync() map[schema.GroupVersionResource]bool 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. // Accessor represents an accessible k8s resource.
@ -42,13 +51,13 @@ type Loggable interface {
} }
type Scalable interface { type Scalable interface {
Scale(ns, n string, replicas int32) error Scale(path string, replicas int32) error
} }
// Nuker represents a resource deleter. // Nuker represents a resource deleter.
type Nuker interface { type Nuker interface {
// Delete removes a resource from the api server. // 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. // Switchable represents a switchable resource.
@ -60,5 +69,16 @@ type Switchable interface {
// Restartable represents a restartable resource. // Restartable represents a restartable resource.
type Restartable interface { type Restartable interface {
// Restart performs a rollout restart. // 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) ServerVersion() (*version.Info, error)
FetchNodes() (*v1.NodeList, error) FetchNodes() (*v1.NodeList, error)
CurrentNamespaceName() (string, error) CurrentNamespaceName() (string, error)
CheckNSAccess(ns string) error
CheckListNSAccess() error
CanI(ns, gvr string, verbs []string) (bool, error) CanI(ns, gvr string, verbs []string) (bool, error)
} }
@ -85,25 +83,6 @@ func InitConnectionOrDie(config *Config) *APIClient {
return &conn 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 { func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
res := GVR(gvr).AsGVR() res := GVR(gvr).AsGVR()
return &authorizationv1.SelfSubjectAccessReview{ 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 package k8s
import ( // BOZO!!
"fmt" // import (
// "fmt"
"github.com/rs/zerolog/log" // "github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd" // "k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api" // "k8s.io/client-go/tools/clientcmd/api"
) // )
// NamedContext represents a named cluster context. // // NamedContext represents a named cluster context.
type NamedContext struct { // type NamedContext struct {
Name string // Name string
Context *api.Context // Context *api.Context
config *Config // config *Config
} // }
// NewNamedContext returns a new named context. // // NewNamedContext returns a new named context.
func NewNamedContext(c *Config, n string, ctx *api.Context) *NamedContext { // func NewNamedContext(c *Config, n string, ctx *api.Context) *NamedContext {
return &NamedContext{Name: n, Context: ctx, config: c} // return &NamedContext{Name: n, Context: ctx, config: c}
} // }
// MustCurrentContextName return the active context name. // // MustCurrentContextName return the active context name.
func (c *NamedContext) MustCurrentContextName() string { // func (c *NamedContext) MustCurrentContextName() string {
cl, err := c.config.CurrentContextName() // cl, err := c.config.CurrentContextName()
if err != nil { // if err != nil {
log.Fatal().Err(err).Msg("Fetching current context") // log.Fatal().Err(err).Msg("Fetching current context")
} // }
return cl // return cl
} // }
// ---------------------------------------------------------------------------- // // ----------------------------------------------------------------------------
// Context represents a Kubernetes Context. // // Context represents a Kubernetes Context.
type Context struct { // type Context struct {
*base // *base
Connection // Connection
} // }
// NewContext returns a new Context. // // NewContext returns a new Context.
func NewContext(c Connection) *Context { // func NewContext(c Connection) *Context {
return &Context{&base{}, c} // return &Context{&base{}, c}
} // }
// Get a Context. // // Get a Context.
func (c *Context) Get(_, n string) (interface{}, error) { // func (c *Context) Get(_, n string) (interface{}, error) {
ctx, err := c.Config().GetContext(n) // ctx, err := c.Config().GetContext(n)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return &NamedContext{Name: n, Context: ctx}, nil // return &NamedContext{Name: n, Context: ctx}, nil
} // }
// List all Contexts on the current cluster. // // List all Contexts on the current cluster.
func (c *Context) List(string, metav1.ListOptions) (Collection, error) { // func (c *Context) List(string, metav1.ListOptions) (Collection, error) {
ctxs, err := c.Config().Contexts() // ctxs, err := c.Config().Contexts()
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
cc := make([]interface{}, 0, len(ctxs)) // cc := make([]interface{}, 0, len(ctxs))
for k, v := range ctxs { // for k, v := range ctxs {
cc = append(cc, NewNamedContext(c.Config(), k, v)) // cc = append(cc, NewNamedContext(c.Config(), k, v))
} // }
return cc, nil // return cc, nil
} // }
// Delete a Context. // // Delete a Context.
func (c *Context) Delete(_, n string, cascade, force bool) error { // func (c *Context) Delete(_, n string, cascade, force bool) error {
ctx, err := c.Config().CurrentContextName() // ctx, err := c.Config().CurrentContextName()
if err != nil { // if err != nil {
return err // return err
} // }
if ctx == n { // if ctx == n {
return fmt.Errorf("trying to delete your current context %s", n) // return fmt.Errorf("trying to delete your current context %s", n)
} // }
return c.Config().DelContext(n) // return c.Config().DelContext(n)
} // }
// MustCurrentContextName return the active context name. // // MustCurrentContextName return the active context name.
func (c *Context) MustCurrentContextName() string { // func (c *Context) MustCurrentContextName() string {
cl, err := c.Config().CurrentContextName() // cl, err := c.Config().CurrentContextName()
if err != nil { // if err != nil {
log.Fatal().Err(err).Msg("Fetching current context") // log.Fatal().Err(err).Msg("Fetching current context")
} // }
return cl // return cl
} // }
// Switch to another context. // // Switch to another context.
func (c *Context) Switch(ctx string) error { // func (c *Context) Switch(ctx string) error {
c.SwitchContextOrDie(ctx) // c.SwitchContextOrDie(ctx)
return nil // return nil
} // }
// KubeUpdate modifies kubeconfig default context. // // KubeUpdate modifies kubeconfig default context.
func (c *Context) KubeUpdate(n string) error { // func (c *Context) KubeUpdate(n string) error {
config, err := c.Config().RawConfig() // config, err := c.Config().RawConfig()
if err != nil { // if err != nil {
return err // return err
} // }
if err := c.Switch(n); err != nil { // if err := c.Switch(n); err != nil {
return err // return err
} // }
return clientcmd.ModifyConfig( // return clientcmd.ModifyConfig(
clientcmd.NewDefaultPathOptions(), config, true, // 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) 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) 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
}

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) { 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() 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{}) pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{})
if err != nil { if err != nil {
return nil, err return nil, err

View File

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

View File

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

View File

@ -1,103 +1,105 @@
package k8s package k8s
import ( // BOZO!!
"fmt" // import (
// "fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // "github.com/derailed/k9s/internal/dao"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" // metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime/serializer" // "k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic" // "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/rest" // "k8s.io/client-go/dynamic"
) // "k8s.io/client-go/rest"
// )
// Resource represents a Kubernetes Resource // // Resource represents a Kubernetes Resource
type Resource struct { // type Resource struct {
*base // *base
Connection // Connection
gvr GVR // gvr dao.GVR
} // }
// NewResource returns a new Resource. // // NewResource returns a new Resource.
func NewResource(c Connection, gvr GVR) *Resource { // func NewResource(c Connection, gvr GVR) *Resource {
return &Resource{base: &base{}, Connection: c, gvr: gvr} // return &Resource{base: &base{}, Connection: c, gvr: gvr}
} // }
// GetInfo returns info about apigroup. // // GetInfo returns info about apigroup.
func (r *Resource) GetInfo() GVR { // func (r *Resource) GetInfo() GVR {
return r.gvr // return r.gvr
} // }
func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface { // func (r *Resource) nsRes() dynamic.NamespaceableResourceInterface {
return r.DynDialOrDie().Resource(r.gvr.AsGVR()) // return r.DynDialOrDie().Resource(r.gvr.AsGVR())
} // }
// Get a Resource. // // Get a Resource.
func (r *Resource) Get(ns, n string) (interface{}, error) { // func (r *Resource) Get(ns, n string) (interface{}, error) {
return r.nsRes().Namespace(ns).Get(n, metav1.GetOptions{}) // return r.nsRes().Namespace(ns).Get(n, metav1.GetOptions{})
} // }
// List all Resources in a given namespace. // // List all Resources in a given namespace.
func (r *Resource) List(ns string, opts metav1.ListOptions) (Collection, error) { // func (r *Resource) List(ns string, opts metav1.ListOptions) (Collection, error) {
obj, err := r.listAll(ns, r.gvr.ToR()) // obj, err := r.listAll(ns, r.gvr.ToR())
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return Collection{obj.(*metav1beta1.Table)}, nil // return Collection{obj.(*metav1beta1.Table)}, nil
} // }
// Delete a Resource. // // Delete a Resource.
func (r *Resource) Delete(ns, n string, cascade, force bool) error { // func (r *Resource) Delete(ns, n string, cascade, force bool) error {
return r.nsRes().Namespace(ns).Delete(n, nil) // 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) { // func (r *Resource) listAll(ns, n string) (runtime.Object, error) {
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) // a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
_, codec := r.codec() // _, codec := r.codec()
c, err := r.getClient() // c, err := r.getClient()
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return c.Get(). // return c.Get().
SetHeader("Accept", a). // SetHeader("Accept", a).
Namespace(ns). // Namespace(ns).
Resource(n). // Resource(n).
VersionedParams(&metav1beta1.TableOptions{}, codec). // VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get() // Do().Get()
} // }
func (r *Resource) getClient() (*rest.RESTClient, error) { // func (r *Resource) getClient() (*rest.RESTClient, error) {
crConfig := r.RestConfigOrDie() // crConfig := r.RestConfigOrDie()
gv := r.gvr.AsGV() // gv := r.gvr.AsGV()
crConfig.GroupVersion = &gv // crConfig.GroupVersion = &gv
crConfig.APIPath = "/apis" // crConfig.APIPath = "/apis"
if len(r.gvr.ToG()) == 0 { // if len(r.gvr.ToG()) == 0 {
crConfig.APIPath = "/api" // crConfig.APIPath = "/api"
} // }
codec, _ := r.codec() // codec, _ := r.codec()
crConfig.NegotiatedSerializer = codec.WithoutConversion() // crConfig.NegotiatedSerializer = codec.WithoutConversion()
crRestClient, err := rest.RESTClientFor(crConfig) // crRestClient, err := rest.RESTClientFor(crConfig)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
return crRestClient, nil // return crRestClient, nil
} // }
func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) { // func (r *Resource) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
scheme := runtime.NewScheme() // scheme := runtime.NewScheme()
gv := r.gvr.AsGV() // gv := r.gvr.AsGV()
metav1.AddToGroupVersion(scheme, gv) // metav1.AddToGroupVersion(scheme, gv)
scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) // scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{})
scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &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 ( const (
// Factory represents a factory context key. // Factory represents a factory context key.
KeyFactory ContextKey = "factory" KeyFactory ContextKey = "factory"
KeySelection = "selection" KeyLabels = "labels"
KeyLabels = "labels" KeyFields = "fields"
KeyFields = "fields" KeyTable = "table"
KeyTable = "table" KeyDir = "dir"
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 ( import (
"context" "context"
"fmt"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
@ -28,10 +29,13 @@ type Container struct {
// List returns a collection of containers // List returns a collection of containers
func (c *Container) List(ctx context.Context) ([]runtime.Object, error) { func (c *Container) List(ctx context.Context) ([]runtime.Object, error) {
c.pod = nil c.pod = nil
sel := ctx.Value(internal.KeySelection).(string) path, ok := ctx.Value(internal.KeyPath).(string)
ns, n := render.Namespaced(sel) if !ok {
return nil, fmt.Errorf("no context path for %q", c.gvr)
}
ns, _ := render.Namespaced(path)
c.namespace = ns 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 { if err != nil {
return nil, err 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 // BOZO!! Need to know if gvr is namespaced or not
o, err := c.Get(). o, err := c.Get().
SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)). SetHeader("Accept", fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)).
// Namespace(g.namespace). Namespace(g.namespace).
Resource(gvr.ToR()). Resource(gvr.ToR()).
VersionedParams(&metav1beta1.TableOptions{}, codec). VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get() 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)) mx := k8s.NewMetricsServer(n.factory.Client().(k8s.Connection))
mmx, err := mx.FetchNodesMetrics() mmx, err := mx.FetchNodesMetrics()
if err != nil { if err != nil {
return err log.Warn().Err(err).Msg("No node metrics")
} }
var index int 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,7 +2,6 @@ package model
import ( import (
"context" "context"
"fmt"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
@ -26,27 +25,21 @@ func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
return oo, err return oo, err
} }
fieldSel, ok := ctx.Value(internal.KeyFields).(string) sel, ok := ctx.Value(internal.KeyFields).(string)
if !ok { if !ok {
return oo, nil return oo, nil
} }
fsel, err := labels.ConvertSelectorToLabelsMap(sel)
sel, err := labels.ConvertSelectorToLabelsMap(fieldSel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nodeName := fsel["spec.nodeName"]
nodeName, ok := sel["spec.nodeName"]
if !ok {
return nil, fmt.Errorf("NYI field selector %q", nodeName)
}
var res []runtime.Object var res []runtime.Object
for _, o := range oo { for _, o := range oo {
u := o.(*unstructured.Unstructured) u := o.(*unstructured.Unstructured)
spec := u.Object["spec"].(map[string]interface{}) spec := u.Object["spec"].(map[string]interface{})
log.Debug().Msgf("Spec node %q -- %q", nodeName, spec["nodeName"]) if nodeName == "" || spec["nodeName"] == nodeName {
if spec["nodeName"] == nodeName {
res = append(res, o) 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 // BOZO!! Break up deps and merge into single registrar
var Registry = map[string]ResourceMeta{ var Registry = map[string]ResourceMeta{
// Custom...
"containers": ResourceMeta{ "containers": ResourceMeta{
Model: &Container{}, Model: &Container{},
Renderer: &render.Container{}, Renderer: &render.Container{},
@ -18,7 +19,24 @@ var Registry = map[string]ResourceMeta{
Model: &ScreenDump{}, Model: &ScreenDump{},
Renderer: &render.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{ "v1/pods": ResourceMeta{
Model: &Pod{}, Model: &Pod{},
Renderer: &render.Pod{}, Renderer: &render.Pod{},
@ -30,7 +48,20 @@ var Registry = map[string]ResourceMeta{
"v1/namespaces": ResourceMeta{ "v1/namespaces": ResourceMeta{
Renderer: &render.Namespace{}, 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{ "apps/v1/deployments": ResourceMeta{
Renderer: &render.Deployment{}, Renderer: &render.Deployment{},
}, },
@ -43,31 +74,32 @@ var Registry = map[string]ResourceMeta{
"apps/v1/daemonsets": ResourceMeta{ "apps/v1/daemonsets": ResourceMeta{
Renderer: &render.DaemonSet{}, Renderer: &render.DaemonSet{},
}, },
// Extensions...
"extensions/v1beta1/daemonsets": ResourceMeta{ "extensions/v1beta1/daemonsets": ResourceMeta{
Renderer: &render.DaemonSet{}, Renderer: &render.DaemonSet{},
}, },
"extensions/v1beta1/ingresses": ResourceMeta{
Renderer: &render.Ingress{},
},
// "v1/services": ResourceMeta{ // Batch...
// Renderer: &render.Service{}, "batch/v1beta1/cronjobs": ResourceMeta{
// }, Renderer: &render.CronJob{},
// "v1/configmaps": ResourceMeta{ },
// Renderer: &render.ConfigMap{}, "batch/v1/jobs": ResourceMeta{
// }, Model: &Job{},
// "v1/secrets": ResourceMeta{ Renderer: &render.Job{},
// Renderer: &render.ConfigMap{}, },
// },
// "batch/v1beta1/cronjobs": ResourceMeta{
// Renderer: &render.CronJob{},
// },
// "batch/v1/jobs": ResourceMeta{
// Renderer: &render.Job{},
// },
// CRDs...
"apiextensions.k8s.io/v1beta1/customresourcedefinitions": ResourceMeta{ "apiextensions.k8s.io/v1beta1/customresourcedefinitions": ResourceMeta{
Renderer: &render.CustomResourceDefinition{}, Renderer: &render.CustomResourceDefinition{},
}, },
// RBAC...
"rbac.authorization.k8s.io/v1/clusterroles": ResourceMeta{ "rbac.authorization.k8s.io/v1/clusterroles": ResourceMeta{
Model: &Rbac{},
Renderer: &render.ClusterRole{}, Renderer: &render.ClusterRole{},
}, },
"rbac.authorization.k8s.io/v1/clusterrolebindings": ResourceMeta{ "rbac.authorization.k8s.io/v1/clusterrolebindings": ResourceMeta{

View File

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

View File

@ -3,25 +3,22 @@ package model
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"os"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime" "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 { type ScreenDump struct {
Resource Resource
pod *v1.Pod 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) { func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
dir, ok := ctx.Value(internal.KeyDir).(string) dir, ok := ctx.Value(internal.KeyDir).(string)
if !ok { if !ok {
@ -35,7 +32,7 @@ func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
oo := make([]runtime.Object, len(ff)) oo := make([]runtime.Object, len(ff))
for i, f := range ff { for i, f := range ff {
oo[i] = FileRes{file: f, dir: dir} oo[i] = render.FileRes{File: f, Dir: dir}
} }
return oo, nil return oo, nil
@ -44,38 +41,9 @@ func (c *ScreenDump) List(ctx context.Context) ([]runtime.Object, error) {
// Hydrate returns a pod as container rows. // Hydrate returns a pod as container rows.
func (c *ScreenDump) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { func (c *ScreenDump) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo { for i, o := range oo {
res, ok := o.(FileRes) if err := re.Render(o, render.NonResource, &rr[i]); err != nil {
if !ok {
return fmt.Errorf("expecting a file resource but got %T", o)
}
if err := re.Render(res, render.NonResource, &rr[i]); err != nil {
return err return err
} }
} }
return nil 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()] c := s.components[s.size()]
s.components = s.components[:s.size()] s.components = s.components[:s.size()]
s.notify(StackPop, c) 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 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/k8s"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/derailed/tview" "github.com/derailed/tview"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -69,45 +70,24 @@ type Lister interface {
Hydrate([]runtime.Object, render.Rows, Renderer) error 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 { type Factory interface {
// Client retrieves an api client. // Client retrieves an api client.
Client() k8s.Connection Client() k8s.Connection
// Get fetch a given resource. // 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 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 fetch an informer for a given resource.
ForResource(ns, gvr string) informers.GenericInformer ForResource(ns, gvr string) informers.GenericInformer
// WaitForCacheSync synchronize the cache. // WaitForCacheSync synchronize the cache.
WaitForCacheSync() map[schema.GroupVersionResource]bool WaitForCacheSync() map[schema.GroupVersionResource]bool
// Forwards returns all portforwards.
Forwarders() watch.Forwarders
} }
// ResourceMeta represents model info about a resource. // ResourceMeta represents model info about a resource.

View File

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

View File

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

View File

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

View File

@ -35,7 +35,7 @@ func (CronJob) Header(ns string) HeaderRow {
} }
// Render renders a K8s resource to screen. // 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) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("Expected CronJob, but got %T", o) 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)) 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) { 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.Name,
cj.Spec.Schedule, cj.Spec.Schedule,
boolPtrToStr(cj.Spec.Suspend), boolPtrToStr(cj.Spec.Suspend),
@ -64,7 +65,5 @@ func (CronJob) Render(o interface{}, ns string, r *Row) error {
toAge(cj.ObjectMeta.CreationTimestamp), toAge(cj.ObjectMeta.CreationTimestamp),
) )
r.ID, r.Fields = MetaFQN(cj.ObjectMeta), fields
return nil return nil
} }

View File

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

View File

@ -7,9 +7,7 @@ import (
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1" 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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@ -17,10 +15,6 @@ import (
// Deployment renders a K8s Deployment to screen. // Deployment renders a K8s Deployment to screen.
type Deployment struct{} type Deployment struct{}
func isAllNamespace(ns string) bool {
return ns == ""
}
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
func (Deployment) ColorerFunc() ColorerFunc { func (Deployment) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color { return func(ns string, r RowEvent) tcell.Color {
@ -54,7 +48,6 @@ func (Deployment) Header(ns string) HeaderRow {
Header{Name: "READY"}, Header{Name: "READY"},
Header{Name: "UP-TO-DATE", Align: tview.AlignRight}, Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight}, Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "SELECTOR"},
Header{Name: "AGE", Decorator: ageDecorator}, 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.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)),
strconv.Itoa(int(dp.Status.UpdatedReplicas)), strconv.Itoa(int(dp.Status.UpdatedReplicas)),
strconv.Itoa(int(dp.Status.AvailableReplicas)), strconv.Itoa(int(dp.Status.AvailableReplicas)),
asSelector(dp.Spec.Selector),
toAge(dp.ObjectMeta.CreationTimestamp), toAge(dp.ObjectMeta.CreationTimestamp),
) )
return nil 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: "READY", Align: tview.AlignRight},
Header{Name: "UP-TO-DATE", Align: tview.AlignRight}, Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight}, Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "NODE_SELECTOR"},
Header{Name: "AGE", Decorator: ageDecorator}, 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.NumberReady)),
strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)), strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),
strconv.Itoa(int(ds.Status.NumberAvailable)), strconv.Itoa(int(ds.Status.NumberAvailable)),
mapToStr(ds.Spec.Template.Spec.NodeSelector),
toAge(ds.ObjectMeta.CreationTimestamp), 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. // DefaultColorer set the default table row colors.
func DefaultColorer(ns string, evt RowEvent) tcell.Color { func DefaultColorer(ns string, evt RowEvent) tcell.Color {
var col = StdColor
switch evt.Kind { switch evt.Kind {
case EventAdd: case EventAdd:
return AddColor col = AddColor
case EventUpdate: case EventUpdate:
return ModColor col = ModColor
case EventDelete: case EventDelete:
return KillColor col = KillColor
default:
return StdColor
} }
return col
} }
type StringSet []string type StringSet []string

View File

@ -26,8 +26,11 @@ func (Generic) ColorerFunc() ColorerFunc {
// Header returns a header row. // Header returns a header row.
func (g *Generic) Header(ns string) HeaderRow { 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 == "" { if ns == "" {
h = append(h, Header{Name: "NAMESPACE"}) h = append(h, Header{Name: "NAMESPACE"})
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/derailed/tview" "github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/duration"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
@ -26,6 +27,20 @@ const (
NAValue = "n/a" 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 { type metric struct {
cpu, mem string cpu, mem string
} }
@ -217,3 +232,16 @@ func in(ll []string, s string) bool {
} }
return false 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. // 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) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("Expected Ingress, but got %T", o) 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 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) { 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, ing.Name,
toHosts(ing.Spec.Rules), toHosts(ing.Spec.Rules),
toAddress(ing.Status.LoadBalancer), toAddress(ing.Status.LoadBalancer),
@ -58,8 +59,6 @@ func (Ingress) Render(o interface{}, ns string, r *Row) error {
toAge(ing.ObjectMeta.CreationTimestamp), toAge(ing.ObjectMeta.CreationTimestamp),
) )
r.ID, r.Fields = MetaFQN(ing.ObjectMeta), fields
return nil return nil
} }
@ -89,15 +88,13 @@ func toTLSPorts(tls []v1beta1.IngressTLS) string {
} }
func toHosts(rr []v1beta1.IngressRule) string { func toHosts(rr []v1beta1.IngressRule) string {
var s string hh := make([]string, 0, len(rr))
var i int
for _, r := range rr { for _, r := range rr {
s += r.Host if r.Host == "" {
if i < len(rr)-1 { r.Host = "*"
s += ","
} }
i++ hh = append(hh, r.Host)
} }
return s return strings.Join(hh, ",")
} }

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log"
batchv1 "k8s.io/api/batch/v1" batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "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. // 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) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("Expected Job, but got %T", o) return fmt.Errorf("Expected Job, but got %T", o)
} }
var j batchv1.Job var job batchv1.Job
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &j) err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job)
if err != nil { if err != nil {
return err return err
} }
cc, ii := toContainers(j.Spec.Template.Spec) r.ID = MetaFQN(job.ObjectMeta)
r.Fields = make(Fields, 0, len(j.Header(ns)))
fields := make(Fields, 0, len(r.Fields))
if isAllNamespace(ns) { if isAllNamespace(ns) {
fields = append(fields, j.Namespace) r.Fields = append(r.Fields, job.Namespace)
} }
fields = append(fields, cc, ii := toContainers(job.Spec.Template.Spec)
j.Name, r.Fields = append(r.Fields,
toCompletion(j.Spec, j.Status), job.Name,
toDuration(j.Status), toCompletion(job.Spec, job.Status),
toDuration(job.Status),
cc, cc,
ii, ii,
toAge(j.ObjectMeta.CreationTimestamp), toAge(job.ObjectMeta.CreationTimestamp),
) )
r.ID, r.Fields = MetaFQN(j.ObjectMeta), fields
return nil return nil
} }
// ----------------------------------------------------------------------------
// Helpers... // Helpers...
const maxShow = 2 const maxShow = 2

View File

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

View File

@ -5,6 +5,8 @@ import (
"strings" "strings"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
) )
// Forwarder represents a port forwarder. // Forwarder represents a port forwarder.
@ -25,18 +27,18 @@ type Forwarder interface {
Age() string Age() string
} }
// Forward renders a portforwards to screen. // PortForward renders a portforwards to screen.
type Forward struct{} type PortForward struct{}
// ColorerFunc colors a resource row. // ColorerFunc colors a resource row.
func (Forward) ColorerFunc() ColorerFunc { func (PortForward) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color { return func(ns string, re RowEvent) tcell.Color {
return tcell.ColorSkyblue return tcell.ColorSkyblue
} }
} }
// Header returns a header row. // Header returns a header row.
func (Forward) Header(ns string) HeaderRow { func (PortForward) Header(ns string) HeaderRow {
return HeaderRow{ return HeaderRow{
Header{Name: "NAMESPACE"}, Header{Name: "NAMESPACE"},
Header{Name: "NAME"}, Header{Name: "NAME"},
@ -50,10 +52,10 @@ func (Forward) Header(ns string) HeaderRow {
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (f Forward) Render(o interface{}, gvr string, r *Row) error { func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
pf, ok := o.(PortForwarder) pf, ok := o.(ForwardRes)
if !ok { 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], ":") ports := strings.Split(pf.Ports()[0], ":")
@ -65,9 +67,9 @@ func (f Forward) Render(o interface{}, gvr string, r *Row) error {
na, na,
pf.Container(), pf.Container(),
strings.Join(pf.Ports(), ","), strings.Join(pf.Ports(), ","),
UrlFor(pf.Host(), pf.HttpPath(), ports[0]), UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
asNum(pf.C()), asNum(pf.Config.C),
asNum(pf.N()), asNum(pf.Config.N),
pf.Age(), pf.Age(),
} }
@ -76,26 +78,27 @@ func (f Forward) Render(o interface{}, gvr string, r *Row) error {
// Helpers... // Helpers...
type PortForwarder interface { // type PortForwarder interface {
Forwarder // Forwarder
BenchConfigurator // BenchConfigurator
} // }
type BenchConfigurators map[string]BenchConfigurator // type BenchConfigurators map[string]BenchConfigurator
type BenchConfigurator interface { // BOZO!!
// C returns the number of concurent connections. // type BenchConfigurator interface {
C() int // // C returns the number of concurent connections.
// C() int
// N returns the number of requests. // // N returns the number of requests.
N() int // N() int
// Host returns the forward host address. // // Host returns the forward host address.
Host() string // Host() string
// Path returns the http path. // // Path returns the http path.
HttpPath() string // HttpPath() string
} // }
// UrlFor computes fq url for a given benchmark configuration. // UrlFor computes fq url for a given benchmark configuration.
func UrlFor(host, path, port string) string { func UrlFor(host, path, port string) string {
@ -108,3 +111,25 @@ func UrlFor(host, path, port string) string {
return "http://" + host + ":" + port + path 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 package render
import ( import (
"fmt"
"strings"
"github.com/gdamore/tcell" "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. // Rbac renders a rbac to screen.
@ -26,8 +51,95 @@ func (Rbac) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (Rbac) Render(o interface{}, gvr string, r *Row) error { 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 return nil
} }
// Helpers... // 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. // 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) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("Expected Role, but got %T", o) 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 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) { 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, ro.Name,
toAge(ro.ObjectMeta.CreationTimestamp), toAge(ro.ObjectMeta.CreationTimestamp),
) )
r.ID, r.Fields = MetaFQN(ro.ObjectMeta), fields
return nil return nil
} }

View File

@ -34,7 +34,7 @@ func (RoleBinding) Header(ns string) HeaderRow {
} }
// Render renders a K8s resource to screen. // 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) raw, ok := o.(*unstructured.Unstructured)
if !ok { if !ok {
return fmt.Errorf("Expected RoleBinding, but got %T", o) 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) 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) { 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.Name,
rb.RoleRef.Name, rb.RoleRef.Name,
kind, kind,
ss, ss,
toAge(rb.ObjectMeta.CreationTimestamp), toAge(rb.ObjectMeta.CreationTimestamp),
) )
r.ID, r.Fields = MetaFQN(rb.ObjectMeta), fields
return nil return nil
} }

View File

@ -7,6 +7,8 @@ import (
"vbom.ml/util/sortorder" "vbom.ml/util/sortorder"
) )
const ageCol = "AGE"
// Fields represents a collection of row fields. // Fields represents a collection of row fields.
type Fields []string type Fields []string
@ -29,7 +31,21 @@ type Header struct {
// HeaderRow represents a table header. // HeaderRow represents a table header.
type HeaderRow []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 { func (h HeaderRow) AgeCol(col int) bool {
if !h.HasAge() {
return false
}
return col == len(h)-1 return col == len(h)-1
} }

View File

@ -7,6 +7,8 @@ import (
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
) )
// ScreenDump renders a screendumps to screen. // ScreenDump renders a screendumps to screen.
@ -35,27 +37,39 @@ func (ScreenDump) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (b ScreenDump) Render(o interface{}, ns string, r *Row) error { func (b ScreenDump) Render(o interface{}, ns string, r *Row) error {
f, ok := o.(ScreenDumper) f, ok := o.(FileRes)
if !ok { 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{ r.Fields = Fields{
f.GetFile().Name(), f.File.Name(),
timeToAge(f.GetFile().ModTime()), timeToAge(f.File.ModTime()),
} }
return nil return nil
} }
// ----------------------------------------------------------------------------
// Helpers... // Helpers...
func timeToAge(timestamp time.Time) string { func timeToAge(timestamp time.Time) string {
return time.Since(timestamp).String() return time.Since(timestamp).String()
} }
type ScreenDumper interface { // FileRes represents a file resource.
GetFile() os.FileInfo type FileRes struct {
GetDir() string 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 ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt"
"path" "path"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/k8s" "github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
genericprinters "k8s.io/cli-runtime/pkg/printers" genericprinters "k8s.io/cli-runtime/pkg/printers"
"k8s.io/kubectl/pkg/describe" "k8s.io/kubectl/pkg/describe"
@ -163,43 +157,44 @@ func (*Base) marshalObject(o runtime.Object) (string, error) {
return buff.String(), nil return buff.String(), nil
} }
func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error { // BOZO!!
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) // func (b *Base) podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error {
if !ok { // f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
return fmt.Errorf("no factory in context for pod logs") // if !ok {
} // return fmt.Errorf("no factory in context for pod logs")
// }
ls, err := metav1.ParseToLabelSelector(toSelector(sel)) // ls, err := metav1.ParseToLabelSelector(toSelector(sel))
if err != nil { // if err != nil {
return err // return err
} // }
lsel, err := metav1.LabelSelectorAsSelector(ls) // lsel, err := metav1.LabelSelectorAsSelector(ls)
if err != nil { // if err != nil {
return err // return err
} // }
inf := f.ForResource(opts.Namespace, "v1/pods") // inf := f.ForResource(opts.Namespace, "v1/pods")
pods, err := inf.Lister().List(lsel) // pods, err := inf.Lister().List(lsel)
if err != nil { // if err != nil {
return err // return err
} // }
if len(pods) > 1 { // if len(pods) > 1 {
opts.MultiPods = true // opts.MultiPods = true
} // }
pr := NewPod(b.Connection) // pr := NewPod(b.Connection)
for _, p := range pods { // for _, p := range pods {
var po v1.Pod // var po v1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(p.(*unstructured.Unstructured).Object, &po) // err := runtime.DefaultUnstructuredConverter.FromUnstructured(p.(*unstructured.Unstructured).Object, &po)
if err != nil { // if err != nil {
// BOZO!! // // BOZO!!
panic(err) // panic(err)
} // }
if po.Status.Phase == v1.PodRunning { // if po.Status.Phase == v1.PodRunning {
opts.Namespace, opts.Name = po.Namespace, po.Name // opts.Namespace, opts.Name = po.Namespace, po.Name
if err := pr.PodLogs(ctx, c, opts); err != nil { // if err := pr.PodLogs(ctx, c, opts); err != nil {
return err // return err
} // }
} // }
} // }
return nil // return nil
} // }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,136 +1,137 @@
package resource_test 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!! // BOZO!!
// func TestCTXList(t *testing.T) { // import (
// mc := NewMockConnection() // "testing"
// mr := NewMockSwitchableCruder()
// m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sNamedCTX()}, nil)
// ctx := NewContextWithArgs(mc, mr) // "github.com/derailed/k9s/internal/k8s"
// cc, err := ctx.List("blee", metav1.ListOptions{}) // "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) // func NewContextListWithArgs(ns string, ctx *resource.Context) resource.List {
// c, err := ctx.New(k8sNamedCTX()) // return resource.NewList(resource.NotNamespaced, "ctx", ctx, resource.SwitchAccess)
// assert.Nil(t, err)
// assert.Equal(t, resource.Columnars{c}, cc)
// mr.VerifyWasCalledOnce().List("blee", metav1.ListOptions{})
// } // }
func TestCTXDelete(t *testing.T) { // func NewContextWithArgs(c k8s.Connection, s resource.SwitchableCruder) *resource.Context {
mc := NewMockConnection() // ctx := &resource.Context{Base: resource.NewBase(c, s)}
mr := NewMockSwitchableCruder() // ctx.Factory = ctx
m.When(mr.Delete("", "fred", true, true)).ThenReturn(nil) // 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)) // ctx := NewContextWithArgs(mc, mr)
mr.VerifyWasCalledOnce().Delete("", "fred", true, true) // err := ctx.Switch("fred")
}
func TestCTXListHasName(t *testing.T) { // assert.Nil(t, err)
mc := NewMockConnection() // mr.VerifyWasCalledOnce().Switch("fred")
mr := NewMockSwitchableCruder() // }
ctx := NewContextWithArgs(mc, mr) // // BOZO!!
l := NewContextListWithArgs("blee", ctx) // // 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) { // // assert.Nil(t, err)
mc := NewMockConnection() // // c, err := ctx.New(k8sNamedCTX())
mr := NewMockSwitchableCruder() // // assert.Nil(t, err)
// // assert.Equal(t, resource.Columnars{c}, cc)
// // mr.VerifyWasCalledOnce().List("blee", metav1.ListOptions{})
// // }
ctx := NewContextWithArgs(mc, mr) // func TestCTXDelete(t *testing.T) {
l := NewContextListWithArgs("blee", ctx) // 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) { // assert.Nil(t, ctx.Delete("fred", true, true))
mc := NewMockConnection() // mr.VerifyWasCalledOnce().Delete("", "fred", true, true)
mr := NewMockSwitchableCruder() // }
ctx := NewContextWithArgs(mc, mr) // func TestCTXListHasName(t *testing.T) {
l := NewContextListWithArgs("blee", ctx) // mc := NewMockConnection()
// mr := NewMockSwitchableCruder()
assert.NotNil(t, l.Resource()) // ctx := NewContextWithArgs(mc, mr)
} // l := NewContextListWithArgs("blee", ctx)
func TestCTXHeader(t *testing.T) { // assert.Equal(t, "ctx", l.GetName())
mc := NewMockConnection() // }
mr := NewMockSwitchableCruder()
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) { // assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
mc := NewMockConnection() // }
m.When(mc.Config()).ThenReturn(k8sConfig())
mr := NewMockSwitchableCruder()
m.When(mr.MustCurrentContextName()).ThenReturn("test")
ctx := NewContextWithArgs(mc, mr) // func TestCTXListHasResource(t *testing.T) {
c, err := ctx.New(k8sNamedCTX()) // mc := NewMockConnection()
assert.Nil(t, err) // mr := NewMockSwitchableCruder()
assert.Equal(t, 4, len(c.Fields(""))) // ctx := NewContextWithArgs(mc, mr)
assert.Equal(t, "test*", c.Fields("")[0]) // l := NewContextListWithArgs("blee", ctx)
}
// Helpers... // assert.NotNil(t, l.Resource())
// }
func k8sConfig() *k8s.Config { // func TestCTXHeader(t *testing.T) {
ctx := "test" // mc := NewMockConnection()
f := genericclioptions.ConfigFlags{ // mr := NewMockSwitchableCruder()
Context: &ctx,
}
return k8s.NewConfig(&f)
}
func k8sNamedCTX() *k8s.NamedContext { // ctx := NewContextWithArgs(mc, mr)
return k8s.NewNamedContext(
k8sConfig(), // assert.Equal(t, 4, len(ctx.Header("")))
"test", // }
&api.Context{
LocationOfOrigin: "fred", // func TestCTXFields(t *testing.T) {
Cluster: "blee", // mc := NewMockConnection()
AuthInfo: "secret", // 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 package resource
import ( // BOZO!!
"errors" // import (
"fmt" // "errors"
"strconv" // "fmt"
// "strconv"
"github.com/derailed/k9s/internal/k8s" // "github.com/derailed/k9s/internal/k8s"
batchv1beta1 "k8s.io/api/batch/v1beta1" // batchv1beta1 "k8s.io/api/batch/v1beta1"
) // )
type ( // type (
// CronJob tracks a kubernetes resource. // // CronJob tracks a kubernetes resource.
CronJob struct { // CronJob struct {
*Base // *Base
instance *batchv1beta1.CronJob // instance *batchv1beta1.CronJob
} // }
// Runner can run jobs. // // Runner can run jobs.
Runner interface { // Runner interface {
Run(path string) error // Run(path string) error
} // }
// Runnable can run jobs. // // Runnable can run jobs.
Runnable interface { // Runnable interface {
Run(ns, n string) error // Run(ns, n string) error
} // }
) // )
// NewCronJobList returns a new resource list. // // NewCronJobList returns a new resource list.
func NewCronJobList(c Connection, ns string) List { // func NewCronJobList(c Connection, ns string) List {
return NewList( // return NewList(
ns, // ns,
"cronjob", // "cronjob",
NewCronJob(c), // NewCronJob(c),
AllVerbsAccess|DescribeAccess, // AllVerbsAccess|DescribeAccess,
) // )
} // }
// NewCronJob instantiates a new CronJob. // // NewCronJob instantiates a new CronJob.
func NewCronJob(c Connection) *CronJob { // func NewCronJob(c Connection) *CronJob {
cj := &CronJob{&Base{Connection: c, Resource: k8s.NewCronJob(c)}, nil} // cj := &CronJob{&Base{Connection: c, Resource: k8s.NewCronJob(c)}, nil}
cj.Factory = cj // cj.Factory = cj
return cj // return cj
} // }
// New builds a new CronJob instance from a k8s resource. // // New builds a new CronJob instance from a k8s resource.
func (r *CronJob) New(i interface{}) (Columnar, error) { // func (r *CronJob) New(i interface{}) (Columnar, error) {
c := NewCronJob(r.Connection) // c := NewCronJob(r.Connection)
switch instance := i.(type) { // switch instance := i.(type) {
case *batchv1beta1.CronJob: // case *batchv1beta1.CronJob:
c.instance = instance // c.instance = instance
case batchv1beta1.CronJob: // case batchv1beta1.CronJob:
c.instance = &instance // c.instance = &instance
default: // default:
return nil, fmt.Errorf("Expecting CronJob but got %T", instance) // return nil, fmt.Errorf("Expecting CronJob but got %T", instance)
} // }
c.path = c.namespacedName(c.instance.ObjectMeta) // c.path = c.namespacedName(c.instance.ObjectMeta)
return c, nil // return c, nil
} // }
// Marshal resource to yaml. // // Marshal resource to yaml.
func (r *CronJob) Marshal(path string) (string, error) { // func (r *CronJob) Marshal(path string) (string, error) {
ns, n := Namespaced(path) // ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n) // i, err := r.Resource.Get(ns, n)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
cj, ok := i.(*batchv1beta1.CronJob) // cj, ok := i.(*batchv1beta1.CronJob)
if !ok { // if !ok {
return "", errors.New("expecting cronjob resource") // return "", errors.New("expecting cronjob resource")
} // }
cj.TypeMeta.APIVersion = "extensions/batchv1beta1" // cj.TypeMeta.APIVersion = "extensions/batchv1beta1"
cj.TypeMeta.Kind = "CronJob" // cj.TypeMeta.Kind = "CronJob"
return r.marshalObject(cj) // return r.marshalObject(cj)
} // }
// Run a given cronjob. // // Run a given cronjob.
func (r *CronJob) Run(pa string) error { // func (r *CronJob) Run(pa string) error {
ns, n := Namespaced(pa) // ns, n := Namespaced(pa)
if c, ok := r.Resource.(Runnable); ok { // if c, ok := r.Resource.(Runnable); ok {
return c.Run(ns, n) // 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. // // Header return resource header.
func (*CronJob) Header(ns string) Row { // func (*CronJob) Header(ns string) Row {
hh := Row{} // hh := Row{}
if ns == AllNamespaces { // if ns == AllNamespaces {
hh = append(hh, "NAMESPACE") // 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. // // Fields retrieves displayable fields.
func (r *CronJob) Fields(ns string) Row { // func (r *CronJob) Fields(ns string) Row {
ff := make([]string, 0, len(r.Header(ns))) // ff := make([]string, 0, len(r.Header(ns)))
i := r.instance // i := r.instance
if ns == AllNamespaces { // if ns == AllNamespaces {
ff = append(ff, i.Namespace) // ff = append(ff, i.Namespace)
} // }
lastScheduled := MissingValue // lastScheduled := MissingValue
if i.Status.LastScheduleTime != nil { // if i.Status.LastScheduleTime != nil {
lastScheduled = toAgeHuman(toAge(*i.Status.LastScheduleTime)) // lastScheduled = toAgeHuman(toAge(*i.Status.LastScheduleTime))
} // }
return append(ff, // return append(ff,
i.Name, // i.Name,
i.Spec.Schedule, // i.Spec.Schedule,
boolPtrToStr(i.Spec.Suspend), // boolPtrToStr(i.Spec.Suspend),
strconv.Itoa(len(i.Status.Active)), // strconv.Itoa(len(i.Status.Active)),
lastScheduled, // lastScheduled,
toAge(i.ObjectMeta.CreationTimestamp), // toAge(i.ObjectMeta.CreationTimestamp),
) // )
} // }

View File

@ -1,131 +1,132 @@
package resource_test 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!! // BOZO!!
// import (
// "testing"
// func TestCronJobListData(t *testing.T) { // "github.com/derailed/k9s/internal/k8s"
// mc := NewMockConnection() // "github.com/derailed/k9s/internal/resource"
// mr := NewMockCruder() // m "github.com/petergtz/pegomock"
// m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sCronJob()}, nil) // "github.com/stretchr/testify/assert"
// batchv1beta1 "k8s.io/api/batch/v1beta1"
// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
// l := NewCronJobListWithArgs("-", NewCronJobWithArgs(mc, mr)) // func NewCronJobListWithArgs(ns string, r *resource.CronJob) resource.List {
// // Make sure we can get deltas! // return resource.NewList(ns, "cj", r, resource.AllVerbsAccess|resource.DescribeAccess)
// 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 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 { // func TestCronJobListAccess(t *testing.T) {
var b bool // mc := NewMockConnection()
return &batchv1beta1.CronJob{ // mr := NewMockCruder()
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 { // ns := "blee"
mc := NewMockConnection() // r := NewCronJobWithArgs(mc, mr)
c, _ := resource.NewCronJob(mc).New(k8sCronJob()) // l := NewCronJobListWithArgs(resource.AllNamespaces, r)
return c // l.SetNamespace(ns)
}
func cronjobYaml() string { // assert.Equal(t, ns, l.GetNamespace())
return `apiVersion: extensions/batchv1beta1 // assert.Equal(t, "cj", l.GetName())
kind: CronJob // for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
metadata: // assert.True(t, l.Access(a))
creationTimestamp: "2018-12-14T17:36:43Z" // }
name: fred // }
namespace: blee
spec: // func TestCronJobFields(t *testing.T) {
jobTemplate: // r := newCronJob().Fields("blee")
metadata: // assert.Equal(t, "fred", r[0])
creationTimestamp: null // }
spec:
template: // func TestCronJobMarshal(t *testing.T) {
metadata: // mc := NewMockConnection()
creationTimestamp: null // mr := NewMockCruder()
spec: // m.When(mr.Get("blee", "fred")).ThenReturn(k8sCronJob(), nil)
containers: null
schedule: '*/1 * * * *' // cm := NewCronJobWithArgs(mc, mr)
suspend: false // ma, err := cm.Marshal("blee/fred")
status: // mr.VerifyWasCalledOnce().Get("blee", "fred")
lastScheduleTime: "2018-12-14T17:36:43Z" // 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 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!! // BOZO!!
// List all resources // import (
// func (r *Custom) List(ns string, opts v1.ListOptions) (Columnars, error) { // "encoding/json"
// ii, err := r.Resource.List(ns, opts) // "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 { // if err != nil {
// return nil, err // 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 cr, nil
// 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
// } // }
// Header return resource header. // // Marshal resource to yaml.
func (r *Custom) Header(ns string) Row { // func (r *Custom) Marshal(path string) (string, error) {
hh := make(Row, 0, len(r.headers)+1) // 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 { // raw, err := yaml.Marshal(i)
hh = append(hh, "NAMESPACE") // if err != nil {
} // return "", err
for _, h := range r.headers { // }
hh = append(hh, strings.ToUpper(h))
}
return hh // return string(raw), nil
} // }
// Fields retrieves displayable fields. // // BOZO!!
func (r *Custom) Fields(ns string) Row { // // List all resources
ff := make(Row, 0, len(r.Header(ns))) // // 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{} // // if len(ii) == 0 {
err := json.Unmarshal(r.instance.Object.Raw, &obj) // // return Columnars{}, errors.New("no resources found")
if err != nil { // // }
log.Error().Err(err)
return Row{}
}
meta, ok := obj["metadata"].(map[string]interface{}) // // table, ok := ii[0].(*metav1beta1.Table)
if !ok { // // if !ok {
log.Fatal().Msg("expecting interface map meta") // // return nil, errors.New("expecting a table resource")
} // // }
rns, ok := meta["namespace"].(string) // // r.headers = make(Row, len(table.ColumnDefinitions))
if ns == AllNamespaces { // // for i, h := range table.ColumnDefinitions {
if ok { // // r.headers[i] = h.Name
ff = append(ff, rns) // // }
} // // 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 { // // return cc, nil
ff = append(ff, fmt.Sprintf("%v", c)) // // }
}
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 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!! // 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() // mc := NewMockConnection()
// mr := NewMockCruder() // 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) // cm := NewCustomWithArgs(mc, mr)
// ma, err := cm.Marshal("blee/fred") // ma, err := cm.Marshal("blee/fred")
// mr.VerifyWasCalledOnce().Get("blee", "fred") // mr.VerifyWasCalledOnce().Get("blee", "fred")
// assert.Nil(t, err) // assert.Nil(t, err)
// assert.Equal(t, customYaml(), ma) // assert.Equal(t, unstructuredYAML(), ma)
// } // }
func TestCustomMarshalWithUnstructured(t *testing.T) { // // BOZO!!
mc := NewMockConnection() // // func TestCustomListData(t *testing.T) {
mr := NewMockCruder() // // mc := NewMockConnection()
m.When(mr.Get("blee", "fred")).ThenReturn(k8sUnstructured(), nil) // // mr := NewMockCruder()
// // m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{k8sCustomGetTable()}, nil)
cm := NewCustomWithArgs(mc, mr) // // l := NewCustomListWithArgs("blee", "fred", NewCustomWithArgs(mc, mr))
ma, err := cm.Marshal("blee/fred") // // // Make sure we can get deltas!
mr.VerifyWasCalledOnce().Get("blee", "fred") // // for i := 0; i < 2; i++ {
// // err := l.Reconcile(nil, "", "")
// // assert.Nil(t, err)
// // }
assert.Nil(t, err) // // mr.VerifyWasCalled(m.Times(2)).List("blee", metav1.ListOptions{})
assert.Equal(t, unstructuredYAML(), ma) // // 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!! // // Helpers...
// func TestCustomListData(t *testing.T) {
// 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() // mc := NewMockConnection()
// mr := NewMockCruder() // c, _ := resource.NewCustom(mc, "g/v1/fred").New(k8sCustomRow())
// m.When(mr.List("blee", metav1.ListOptions{})).ThenReturn(k8s.Collection{k8sCustomTable()}, nil) // return c
// 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])
// } // }
// Helpers... // func customYaml() string {
// return `typemeta:
func k8sCustomTable() *metav1beta1.Table { // kind: ""
return &metav1beta1.Table{ // apiversion: ""
ColumnDefinitions: []metav1beta1.TableColumnDefinition{ // listmeta:
{Name: "A"}, // selflink: ""
{Name: "B"}, // resourceversion: ""
{Name: "C"}, // continue: ""
}, // remainingitemcount: null
Rows: []metav1beta1.TableRow{ // columndefinitions:
{ // - name: A
Object: runtime.RawExtension{ // type: ""
Raw: []byte(`{ // format: ""
"kind": "fred", // description: ""
"apiVersion": "v1", // priority: 0
"metadata": { // - name: B
"namespace": "blee", // type: ""
"name": "fred" // format: ""
}}`), // description: ""
}, // priority: 0
Cells: []interface{}{ // - name: C
"a", // type: ""
"b", // format: ""
"c", // description: ""
}, // priority: 0
}, // rows:
}, // - cells:
} // - a
} // - b
// - c
func k8sUnstructured() *unstructured.Unstructured { // conditions: []
return &unstructured.Unstructured{ // object:
Object: map[string]interface{}{ // raw:
"kind": "fred", // - 123
"apiVersion": "v1", // - 10
"metadata": map[string]interface{}{ // - 32
"namespace": "blee", // - 32
"name": "fred", // - 32
}, // - 32
}, // - 32
} // - 32
} // - 32
// - 32
func unstructuredYAML() string { // - 34
return `apiVersion: v1 // - 107
kind: fred // - 105
metadata: // - 110
name: fred // - 100
namespace: blee // - 34
` // - 58
} // - 32
// - 34
func k8sCustomRow() *metav1beta1.TableRow { // - 102
return &metav1beta1.TableRow{ // - 114
Object: runtime.RawExtension{ // - 101
Raw: []byte(`{ // - 100
"kind": "fred", // - 34
"apiVersion": "v1", // - 44
"metadata": { // - 10
"namespace": "blee", // - 32
"name": "fred" // - 32
}}`), // - 32
}, // - 32
Cells: []interface{}{ // - 32
"a", // - 32
"b", // - 32
"c", // - 32
}, // - 34
} // - 97
} // - 112
// - 105
func newCustom() resource.Columnar { // - 86
mc := NewMockConnection() // - 101
c, _ := resource.NewCustom(mc, "g/v1/fred").New(k8sCustomRow()) // - 114
return c // - 115
} // - 105
// - 111
func customYaml() string { // - 110
return `typemeta: // - 34
kind: "" // - 58
apiversion: "" // - 32
listmeta: // - 34
selflink: "" // - 118
resourceversion: "" // - 49
continue: "" // - 34
remainingitemcount: null // - 44
columndefinitions: // - 10
- name: A // - 32
type: "" // - 32
format: "" // - 32
description: "" // - 32
priority: 0 // - 32
- name: B // - 32
type: "" // - 32
format: "" // - 32
description: "" // - 34
priority: 0 // - 109
- name: C // - 101
type: "" // - 116
format: "" // - 97
description: "" // - 100
priority: 0 // - 97
rows: // - 116
- cells: // - 97
- a // - 34
- b // - 58
- c // - 32
conditions: [] // - 123
object: // - 10
raw: // - 32
- 123 // - 32
- 10 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 34
- 34 // - 110
- 107 // - 97
- 105 // - 109
- 110 // - 101
- 100 // - 115
- 34 // - 112
- 58 // - 97
- 32 // - 99
- 34 // - 101
- 102 // - 34
- 114 // - 58
- 101 // - 32
- 100 // - 34
- 34 // - 98
- 44 // - 108
- 10 // - 101
- 32 // - 101
- 32 // - 34
- 32 // - 44
- 32 // - 10
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 34 // - 32
- 97 // - 32
- 112 // - 32
- 105 // - 32
- 86 // - 32
- 101 // - 32
- 114 // - 34
- 115 // - 110
- 105 // - 97
- 111 // - 109
- 110 // - 101
- 34 // - 34
- 58 // - 58
- 32 // - 32
- 34 // - 34
- 118 // - 102
- 49 // - 114
- 34 // - 101
- 44 // - 100
- 10 // - 34
- 32 // - 10
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 32 // - 32
- 34 // - 32
- 109 // - 125
- 101 // - 125
- 116 // object: null
- 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 package resource
import ( // BOZO!!
"context" // import (
"errors" // "context"
"fmt" // "errors"
"strconv" // "fmt"
// "strconv"
"github.com/derailed/k9s/internal/k8s" // "github.com/derailed/k9s/internal/k8s"
appsv1 "k8s.io/api/apps/v1" // appsv1 "k8s.io/api/apps/v1"
) // )
// Compile time checks to ensure type satisfies interface // // Compile time checks to ensure type satisfies interface
var _ Restartable = (*Deployment)(nil) // var _ Restartable = (*Deployment)(nil)
var _ Scalable = (*Deployment)(nil) // var _ Scalable = (*Deployment)(nil)
// Deployment tracks a kubernetes resource. // // Deployment tracks a kubernetes resource.
type Deployment struct { // type Deployment struct {
*Base // *Base
instance *appsv1.Deployment // instance *appsv1.Deployment
} // }
// NewDeploymentList returns a new resource list. // // NewDeploymentList returns a new resource list.
func NewDeploymentList(c Connection, ns string) List { // func NewDeploymentList(c Connection, ns string) List {
return NewList( // return NewList(
ns, // ns,
"deploy", // "deploy",
NewDeployment(c), // NewDeployment(c),
AllVerbsAccess|DescribeAccess, // AllVerbsAccess|DescribeAccess,
) // )
} // }
// NewDeployment instantiates a new Deployment. // // NewDeployment instantiates a new Deployment.
func NewDeployment(c Connection) *Deployment { // func NewDeployment(c Connection) *Deployment {
d := &Deployment{&Base{Connection: c, Resource: k8s.NewDeployment(c)}, nil} // d := &Deployment{&Base{Connection: c, Resource: k8s.NewDeployment(c)}, nil}
d.Factory = d // d.Factory = d
return d // return d
} // }
// New builds a new Deployment instance from a k8s resource. // // New builds a new Deployment instance from a k8s resource.
func (r *Deployment) New(i interface{}) (Columnar, error) { // func (r *Deployment) New(i interface{}) (Columnar, error) {
c := NewDeployment(r.Connection) // c := NewDeployment(r.Connection)
switch instance := i.(type) { // switch instance := i.(type) {
case *appsv1.Deployment: // case *appsv1.Deployment:
c.instance = instance // c.instance = instance
case appsv1.Deployment: // case appsv1.Deployment:
c.instance = &instance // c.instance = &instance
default: // default:
return nil, fmt.Errorf("Expecting Deployment but got %T", instance) // return nil, fmt.Errorf("Expecting Deployment but got %T", instance)
} // }
c.path = c.namespacedName(c.instance.ObjectMeta) // c.path = c.namespacedName(c.instance.ObjectMeta)
return c, nil // return c, nil
} // }
// Marshal resource to yaml. // // Marshal resource to yaml.
func (r *Deployment) Marshal(path string) (string, error) { // func (r *Deployment) Marshal(path string) (string, error) {
ns, n := Namespaced(path) // ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n) // i, err := r.Resource.Get(ns, n)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
dp, ok := i.(*appsv1.Deployment) // dp, ok := i.(*appsv1.Deployment)
if !ok { // if !ok {
return "", errors.New("expecting dp resource") // return "", errors.New("expecting dp resource")
} // }
dp.TypeMeta.APIVersion = "apps/v1" // dp.TypeMeta.APIVersion = "apps/v1"
dp.TypeMeta.Kind = "Deployment" // dp.TypeMeta.Kind = "Deployment"
return r.marshalObject(dp) // return r.marshalObject(dp)
} // }
// Logs tail logs for all pods represented by this deployment. // // Logs tail logs for all pods represented by this deployment.
func (r *Deployment) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { // func (r *Deployment) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
instance, err := r.Resource.Get(opts.Namespace, opts.Name) // instance, err := r.Resource.Get(opts.Namespace, opts.Name)
if err != nil { // if err != nil {
return err // return err
} // }
dp, ok := instance.(*appsv1.Deployment) // dp, ok := instance.(*appsv1.Deployment)
if !ok { // if !ok {
return errors.New("Expecting valid deployment") // return errors.New("Expecting valid deployment")
} // }
if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { // if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on deployment %s", opts.Name) // 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. // // Header return resource header.
func (*Deployment) Header(ns string) Row { // func (*Deployment) Header(ns string) Row {
var hh Row // var hh Row
if ns == AllNamespaces { // if ns == AllNamespaces {
hh = append(hh, "NAMESPACE") // hh = append(hh, "NAMESPACE")
} // }
return append(hh, // return append(hh,
"NAME", // "NAME",
"DESIRED", // "DESIRED",
"CURRENT", // "CURRENT",
"UP-TO-DATE", // "UP-TO-DATE",
"AVAILABLE", // "AVAILABLE",
"AGE", // "AGE",
) // )
} // }
// NumCols designates if column is numerical. // // NumCols designates if column is numerical.
func (*Deployment) NumCols(n string) map[string]bool { // func (*Deployment) NumCols(n string) map[string]bool {
return map[string]bool{ // return map[string]bool{
"DESIRED": true, // "DESIRED": true,
"CURRENT": true, // "CURRENT": true,
"UP-TO-DATE": true, // "UP-TO-DATE": true,
"AVAILABLE": true, // "AVAILABLE": true,
} // }
} // }
// Fields retrieves displayable fields. // // Fields retrieves displayable fields.
func (r *Deployment) Fields(ns string) Row { // func (r *Deployment) Fields(ns string) Row {
ff := make([]string, 0, len(r.Header(ns))) // ff := make([]string, 0, len(r.Header(ns)))
i := r.instance // i := r.instance
if ns == AllNamespaces { // if ns == AllNamespaces {
ff = append(ff, i.Namespace) // ff = append(ff, i.Namespace)
} // }
return append(ff, // return append(ff,
i.Name, // i.Name,
strconv.Itoa(int(*i.Spec.Replicas)), // strconv.Itoa(int(*i.Spec.Replicas)),
strconv.Itoa(int(i.Status.Replicas)), // strconv.Itoa(int(i.Status.Replicas)),
strconv.Itoa(int(i.Status.UpdatedReplicas)), // strconv.Itoa(int(i.Status.UpdatedReplicas)),
strconv.Itoa(int(i.Status.AvailableReplicas)), // strconv.Itoa(int(i.Status.AvailableReplicas)),
toAge(i.ObjectMeta.CreationTimestamp), // toAge(i.ObjectMeta.CreationTimestamp),
) // )
} // }
// Scale the specified resource. // // Scale the specified resource.
func (r *Deployment) Scale(ns, n string, replicas int32) error { // func (r *Deployment) Scale(ns, n string, replicas int32) error {
return r.Resource.(Scalable).Scale(ns, n, replicas) // return r.Resource.(Scalable).Scale(ns, n, replicas)
} // }
// Restart the rollout of the specified resource. // // Restart the rollout of the specified resource.
func (r *Deployment) Restart(ns, n string) error { // func (r *Deployment) Restart(ns, n string) error {
return r.Resource.(Restartable).Restart(ns, n) // return r.Resource.(Restartable).Restart(ns, n)
} // }

View File

@ -1,122 +1,123 @@
package resource_test 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!! // BOZO!!
// func TestDeploymentListData(t *testing.T) { // import (
// mc := NewMockConnection() // "testing"
// mr := NewMockCruder()
// m.When(mr.List(resource.NotNamespaced, metav1.ListOptions{})).ThenReturn(k8s.Collection{*k8sDeployment()}, nil)
// l := NewDeploymentListWithArgs("-", NewDeploymentWithArgs(mc, mr)) // "github.com/derailed/k9s/internal/k8s"
// // Make sure we can get deltas! // "github.com/derailed/k9s/internal/resource"
// for i := 0; i < 2; i++ { // m "github.com/petergtz/pegomock"
// err := l.Reconcile(nil, "", "") // "github.com/stretchr/testify/assert"
// assert.Nil(t, err) // appsv1 "k8s.io/api/apps/v1"
// } // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
// mr.VerifyWasCalled(m.Times(2)).List(resource.NotNamespaced, metav1.ListOptions{}) // func NewDeploymentListWithArgs(ns string, r *resource.Deployment) resource.List {
// td := l.Data() // return resource.NewList(ns, "deploy", r, resource.AllVerbsAccess|resource.DescribeAccess)
// 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 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 { // func TestDeploymentListAccess(t *testing.T) {
var i int32 = 1 // mc := NewMockConnection()
return &appsv1.Deployment{ // mr := NewMockCruder()
ObjectMeta: metav1.ObjectMeta{
Namespace: "blee",
Name: "fred",
CreationTimestamp: metav1.Time{Time: testTime()},
},
Spec: appsv1.DeploymentSpec{
Replicas: &i,
},
}
}
func newDeployment() resource.Columnar { // ns := "blee"
mc := NewMockConnection() // l := NewDeploymentListWithArgs(resource.AllNamespaces, NewDeploymentWithArgs(mc, mr))
c, _ := resource.NewDeployment(mc).New(k8sDeployment()) // l.SetNamespace(ns)
return c
}
func dpYaml() string { // assert.Equal(t, "blee", l.GetNamespace())
return `apiVersion: apps/v1 // assert.Equal(t, "deploy", l.GetName())
kind: Deployment // for _, a := range []int{resource.GetAccess, resource.ListAccess, resource.DeleteAccess, resource.ViewAccess, resource.EditAccess} {
metadata: // assert.True(t, l.Access(a))
creationTimestamp: "2018-12-14T17:36:43Z" // }
name: fred // }
namespace: blee
spec: // func TestDeploymentFields(t *testing.T) {
replicas: 1 // r := newDeployment().Fields("blee")
selector: null // assert.Equal(t, "fred", r[0])
strategy: {} // }
template:
metadata: // func TestDeploymentMarshal(t *testing.T) {
creationTimestamp: null // mc := NewMockConnection()
spec: // mr := NewMockCruder()
containers: null // m.When(mr.Get("blee", "fred")).ThenReturn(k8sDeployment(), nil)
status: {}
` // 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 package resource
import ( // BOZO!!
"context" // import (
"errors" // "context"
"fmt" // "errors"
"strconv" // "fmt"
"strings" // "strconv"
"time" // "strings"
// "time"
"github.com/derailed/k9s/internal/k8s" // "github.com/derailed/k9s/internal/k8s"
batchv1 "k8s.io/api/batch/v1" // batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1" // v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/duration" // "k8s.io/apimachinery/pkg/util/duration"
) // )
// Job tracks a kubernetes resource. // // Job tracks a kubernetes resource.
type Job struct { // type Job struct {
*Base // *Base
instance *batchv1.Job // instance *batchv1.Job
} // }
// NewJobList returns a new resource list. // // NewJobList returns a new resource list.
func NewJobList(c Connection, ns string) List { // func NewJobList(c Connection, ns string) List {
return NewList( // return NewList(
ns, // ns,
"job", // "job",
NewJob(c), // NewJob(c),
AllVerbsAccess|DescribeAccess, // AllVerbsAccess|DescribeAccess,
) // )
} // }
// NewJob instantiates a new Job. // // NewJob instantiates a new Job.
func NewJob(c Connection) *Job { // func NewJob(c Connection) *Job {
j := &Job{ // j := &Job{
Base: &Base{Connection: c, Resource: k8s.NewJob(c)}, // Base: &Base{Connection: c, Resource: k8s.NewJob(c)},
} // }
j.Factory = j // j.Factory = j
return j // return j
} // }
// New builds a new Job instance from a k8s resource. // // New builds a new Job instance from a k8s resource.
func (r *Job) New(i interface{}) (Columnar, error) { // func (r *Job) New(i interface{}) (Columnar, error) {
c := NewJob(r.Connection) // c := NewJob(r.Connection)
switch instance := i.(type) { // switch instance := i.(type) {
case *batchv1.Job: // case *batchv1.Job:
c.instance = instance // c.instance = instance
case batchv1.Job: // case batchv1.Job:
c.instance = &instance // c.instance = &instance
default: // default:
return nil, fmt.Errorf("Expecting Job but got %T", instance) // return nil, fmt.Errorf("Expecting Job but got %T", instance)
} // }
c.path = c.namespacedName(c.instance.ObjectMeta) // c.path = c.namespacedName(c.instance.ObjectMeta)
return c, nil // return c, nil
} // }
// Marshal resource to yaml. // // Marshal resource to yaml.
func (r *Job) Marshal(path string) (string, error) { // func (r *Job) Marshal(path string) (string, error) {
ns, n := Namespaced(path) // ns, n := Namespaced(path)
i, err := r.Resource.Get(ns, n) // i, err := r.Resource.Get(ns, n)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
jo, ok := i.(*batchv1.Job) // jo, ok := i.(*batchv1.Job)
if !ok { // if !ok {
return "", errors.New("expecting job resource") // return "", errors.New("expecting job resource")
} // }
jo.TypeMeta.APIVersion = "extensions/v1beta1" // jo.TypeMeta.APIVersion = "extensions/v1beta1"
jo.TypeMeta.Kind = "Job" // jo.TypeMeta.Kind = "Job"
return r.marshalObject(jo) // return r.marshalObject(jo)
} // }
// Containers fetch all the containers on this job, may include init containers. // // Containers fetch all the containers on this job, may include init containers.
func (r *Job) Containers(path string, includeInit bool) ([]string, error) { // func (r *Job) Containers(path string, includeInit bool) ([]string, error) {
ns, n := Namespaced(path) // 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. // // Logs retrieves logs for a given container.
func (r *Job) Logs(ctx context.Context, c chan<- string, opts LogOptions) error { // func (r *Job) Logs(ctx context.Context, c chan<- string, opts LogOptions) error {
instance, err := r.Resource.Get(opts.Namespace, opts.Name) // instance, err := r.Resource.Get(opts.Namespace, opts.Name)
if err != nil { // if err != nil {
return err // return err
} // }
jo, ok := instance.(*batchv1.Job) // jo, ok := instance.(*batchv1.Job)
if !ok { // if !ok {
return errors.New("expecting job resource") // return errors.New("expecting job resource")
} // }
if jo.Spec.Selector == nil || len(jo.Spec.Selector.MatchLabels) == 0 { // if jo.Spec.Selector == nil || len(jo.Spec.Selector.MatchLabels) == 0 {
return fmt.Errorf("No valid selector found on job %s", opts.FQN()) // 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. // // Header return resource header.
func (*Job) Header(ns string) Row { // func (*Job) Header(ns string) Row {
hh := Row{} // hh := Row{}
if ns == AllNamespaces { // if ns == AllNamespaces {
hh = append(hh, "NAMESPACE") // 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. // // Fields retrieves displayable fields.
func (r *Job) Fields(ns string) Row { // func (r *Job) Fields(ns string) Row {
ff := make([]string, 0, len(r.Header(ns))) // ff := make([]string, 0, len(r.Header(ns)))
i := r.instance // i := r.instance
if ns == AllNamespaces { // if ns == AllNamespaces {
ff = append(ff, i.Namespace) // ff = append(ff, i.Namespace)
} // }
cc, ii := r.toContainers(i.Spec.Template.Spec) // cc, ii := r.toContainers(i.Spec.Template.Spec)
return append(ff, // return append(ff,
i.Name, // i.Name,
r.toCompletion(i.Spec, i.Status), // r.toCompletion(i.Spec, i.Status),
r.toDuration(i.Status), // r.toDuration(i.Status),
cc, // cc,
ii, // ii,
toAge(i.ObjectMeta.CreationTimestamp), // toAge(i.ObjectMeta.CreationTimestamp),
) // )
} // }
// ---------------------------------------------------------------------------- // // ----------------------------------------------------------------------------
// Helpers... // // Helpers...
const maxShow = 2 // const maxShow = 2
func (*Job) toContainers(p v1.PodSpec) (string, string) { // func (*Job) toContainers(p v1.PodSpec) (string, string) {
cc, ii := parseContainers(p.InitContainers) // cc, ii := parseContainers(p.InitContainers)
cn, ci := parseContainers(p.Containers) // 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... // // Limit to 2 of each...
if len(cc) > maxShow { // if len(cc) > maxShow {
cc = append(cc[:2], "(+"+strconv.Itoa(len(cc)-maxShow)+")...") // cc = append(cc[:2], "(+"+strconv.Itoa(len(cc)-maxShow)+")...")
} // }
if len(ii) > maxShow { // if len(ii) > maxShow {
ii = append(ii[:2], "(+"+strconv.Itoa(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) { // func parseContainers(cos []v1.Container) (nn, ii []string) {
for _, co := range cos { // for _, co := range cos {
nn = append(nn, co.Name) // nn = append(nn, co.Name)
ii = append(ii, co.Image) // ii = append(ii, co.Image)
} // }
return nn, ii // return nn, ii
} // }
func (*Job) toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) { // func (*Job) toCompletion(spec batchv1.JobSpec, status batchv1.JobStatus) (s string) {
if spec.Completions != nil { // if spec.Completions != nil {
return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions)) // return strconv.Itoa(int(status.Succeeded)) + "/" + strconv.Itoa(int(*spec.Completions))
} // }
if spec.Parallelism == nil { // if spec.Parallelism == nil {
return strconv.Itoa(int(status.Succeeded)) + "/1" // return strconv.Itoa(int(status.Succeeded)) + "/1"
} // }
p := *spec.Parallelism // p := *spec.Parallelism
if p > 1 { // if p > 1 {
return strconv.Itoa(int(status.Succeeded)) + "/1 of " + strconv.Itoa(int(p)) // 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 { // func (*Job) toDuration(status batchv1.JobStatus) string {
if status.StartTime == nil { // if status.StartTime == nil {
return MissingValue // return MissingValue
} // }
var d time.Duration // var d time.Duration
switch { // switch {
case status.CompletionTime == nil: // case status.CompletionTime == nil:
d = time.Since(status.StartTime.Time) // d = time.Since(status.StartTime.Time)
default: // default:
d = status.CompletionTime.Sub(status.StartTime.Time) // d = status.CompletionTime.Sub(status.StartTime.Time)
} // }
return duration.HumanDuration(d) // return duration.HumanDuration(d)
} // }

View File

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

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