add resource navigation + dp rollbacks
parent
bb42c39645
commit
cd950e04c6
2
go.mod
2
go.mod
|
|
@ -13,12 +13,14 @@ require (
|
|||
github.com/rs/zerolog v1.12.0
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/stretchr/testify v1.2.2
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
k8s.io/api v0.0.0-20190202010724-74b699b93c15
|
||||
k8s.io/apimachinery v0.0.0-20190207091153-095b9d203467
|
||||
k8s.io/cli-runtime v0.0.0-20190207094101-a32b78e5dd0a
|
||||
k8s.io/client-go v10.0.0+incompatible
|
||||
k8s.io/klog v0.2.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c // indirect
|
||||
k8s.io/kubernetes v1.13.3
|
||||
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190404181321-646549c5a231 // indirect
|
||||
|
|
|
|||
5
go.sum
5
go.sum
|
|
@ -240,7 +240,10 @@ golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnf
|
|||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc=
|
||||
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
|
@ -361,6 +364,8 @@ k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
|
|||
k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668 h1:M80qeWaBNOX2Uc4plRHcb6k+3YE5VWMaJXKZo+tX9aU=
|
||||
k8s.io/kube-openapi v0.0.0-20190215190454-ea82251f3668/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||
k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c h1:kJCzg2vGCzah5icgkKR7O1Dzn0NA2iGlym27sb0ZfGE=
|
||||
k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
|
||||
k8s.io/kubernetes v1.13.3 h1:46t44D87wKtdKFgr/lXM60K8xPrW0wO67Woof3Vsv6E=
|
||||
k8s.io/kubernetes v1.13.3/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
||||
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f h1:HyUoIBzks9xTaSnMJ6kv/SSmwaQQccokuiriu2cV0aA=
|
||||
|
|
|
|||
|
|
@ -14,3 +14,7 @@ func (b *base) SetFieldSelector(s string) {
|
|||
func (b *base) SetLabelSelector(s string) {
|
||||
b.labelSelector = s
|
||||
}
|
||||
|
||||
func (b *base) HasSelectors() bool {
|
||||
return b.labelSelector != "" || b.fieldSelector != ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ func (p *Pod) List(ns string) (Collection, error) {
|
|||
LabelSelector: p.labelSelector,
|
||||
FieldSelector: p.fieldSelector,
|
||||
}
|
||||
// FieldSelector: "spec.nodeName=gke-k9s-default-pool-0fa2fb89-lbtf",
|
||||
|
||||
rr, err := p.DialOrDie().CoreV1().Pods(ns).List(opts)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type (
|
|||
Delete(ns string, name string) error
|
||||
SetLabelSelector(string)
|
||||
SetFieldSelector(string)
|
||||
HasSelectors() bool
|
||||
}
|
||||
|
||||
// Connection represents a Kubenetes apiserver connection.
|
||||
|
|
@ -47,6 +48,11 @@ func NewBase(c Connection, r Cruder) *Base {
|
|||
return &Base{Connection: c, Resource: r}
|
||||
}
|
||||
|
||||
// HasSelectors returns true if field or label selectors are set.
|
||||
func (b *Base) HasSelectors() bool {
|
||||
return b.Resource.HasSelectors()
|
||||
}
|
||||
|
||||
// SetFieldSelector refines query results via selector.
|
||||
func (b *Base) SetFieldSelector(s string) {
|
||||
b.Resource.SetFieldSelector(s)
|
||||
|
|
@ -99,6 +105,7 @@ func (b *Base) Describe(kind, pa string, flags *genericclioptions.ConfigFlags) (
|
|||
|
||||
mapping, err := k8s.RestMapping.Find(kind)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("Unable to find mapper for %s %s", kind, pa)
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,11 @@ type (
|
|||
Reconcile() error
|
||||
GetName() string
|
||||
Access(flag int) bool
|
||||
GetAccess() int
|
||||
SetAccess(int)
|
||||
SetFieldSelector(string)
|
||||
SetLabelSelector(string)
|
||||
HasSelectors() bool
|
||||
}
|
||||
|
||||
// Columnar tracks resources that can be diplayed in a tabular fashion.
|
||||
|
|
@ -97,6 +100,7 @@ type (
|
|||
Header(ns string) Row
|
||||
SetFieldSelector(string)
|
||||
SetLabelSelector(string)
|
||||
HasSelectors() bool
|
||||
}
|
||||
|
||||
list struct {
|
||||
|
|
@ -122,6 +126,10 @@ func NewList(ns, name string, res Resource, verbs int) *list {
|
|||
}
|
||||
}
|
||||
|
||||
func (l *list) HasSelectors() bool {
|
||||
return l.resource.HasSelectors()
|
||||
}
|
||||
|
||||
// SetFieldSelector narrows down resource query given fields selection.
|
||||
func (l *list) SetFieldSelector(s string) {
|
||||
l.resource.SetFieldSelector(s)
|
||||
|
|
@ -137,6 +145,16 @@ func (l *list) Access(f int) bool {
|
|||
return l.verbs&f == f
|
||||
}
|
||||
|
||||
// Access check access control on a given resource.
|
||||
func (l *list) GetAccess() int {
|
||||
return l.verbs
|
||||
}
|
||||
|
||||
// Access check access control on a given resource.
|
||||
func (l *list) SetAccess(f int) {
|
||||
l.verbs = f
|
||||
}
|
||||
|
||||
// Namespaced checks if k8s resource is namespaced.
|
||||
func (l *list) Namespaced() bool {
|
||||
return l.namespace != NotNamespaced
|
||||
|
|
|
|||
|
|
@ -52,6 +52,21 @@ func (mock *MockCruder) Get(_param0 string, _param1 string) (interface{}, error)
|
|||
return ret0, ret1
|
||||
}
|
||||
|
||||
func (mock *MockCruder) HasSelectors() bool {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockCruder().")
|
||||
}
|
||||
params := []pegomock.Param{}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("HasSelectors", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})
|
||||
var ret0 bool
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(bool)
|
||||
}
|
||||
}
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (mock *MockCruder) List(_param0 string) (k8s.Collection, error) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockCruder().")
|
||||
|
|
@ -186,6 +201,23 @@ func (c *Cruder_Get_OngoingVerification) GetAllCapturedArguments() (_param0 []st
|
|||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierCruder) HasSelectors() *Cruder_HasSelectors_OngoingVerification {
|
||||
params := []pegomock.Param{}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasSelectors", params, verifier.timeout)
|
||||
return &Cruder_HasSelectors_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type Cruder_HasSelectors_OngoingVerification struct {
|
||||
mock *MockCruder
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *Cruder_HasSelectors_OngoingVerification) GetCapturedArguments() {
|
||||
}
|
||||
|
||||
func (c *Cruder_HasSelectors_OngoingVerification) GetAllCapturedArguments() {
|
||||
}
|
||||
|
||||
func (verifier *VerifierCruder) List(_param0 string) *Cruder_List_OngoingVerification {
|
||||
params := []pegomock.Param{_param0}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,21 @@ func (mock *MockSwitchableCruder) Get(_param0 string, _param1 string) (interface
|
|||
return ret0, ret1
|
||||
}
|
||||
|
||||
func (mock *MockSwitchableCruder) HasSelectors() bool {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().")
|
||||
}
|
||||
params := []pegomock.Param{}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("HasSelectors", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})
|
||||
var ret0 bool
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(bool)
|
||||
}
|
||||
}
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (mock *MockSwitchableCruder) List(_param0 string) (k8s.Collection, error) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockSwitchableCruder().")
|
||||
|
|
@ -216,6 +231,23 @@ func (c *SwitchableCruder_Get_OngoingVerification) GetAllCapturedArguments() (_p
|
|||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierSwitchableCruder) HasSelectors() *SwitchableCruder_HasSelectors_OngoingVerification {
|
||||
params := []pegomock.Param{}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasSelectors", params, verifier.timeout)
|
||||
return &SwitchableCruder_HasSelectors_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type SwitchableCruder_HasSelectors_OngoingVerification struct {
|
||||
mock *MockSwitchableCruder
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *SwitchableCruder_HasSelectors_OngoingVerification) GetCapturedArguments() {
|
||||
}
|
||||
|
||||
func (c *SwitchableCruder_HasSelectors_OngoingVerification) GetAllCapturedArguments() {
|
||||
}
|
||||
|
||||
func (verifier *VerifierSwitchableCruder) List(_param0 string) *SwitchableCruder_List_OngoingVerification {
|
||||
params := []pegomock.Param{_param0}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "List", params, verifier.timeout)
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@ type (
|
|||
keyboard(evt *tcell.EventKey) *tcell.EventKey
|
||||
}
|
||||
|
||||
actionsFn func(keyActions)
|
||||
|
||||
resourceViewer interface {
|
||||
igniter
|
||||
|
||||
setEnterFn(enterFn)
|
||||
setColorerFn(colorerFn)
|
||||
setDecorateFn(decorateFn)
|
||||
setExtraActionsFn(actionsFn)
|
||||
}
|
||||
|
||||
appView struct {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ func (v *containerView) shellIn(path, co string) {
|
|||
}
|
||||
args = append(args, "--", "sh")
|
||||
log.Debug().Msgf("Shell args %v", args)
|
||||
runK(v.app, args...)
|
||||
runK(true, v.app, args...)
|
||||
}
|
||||
|
||||
func (v *containerView) extraActions(aa keyActions) {
|
||||
|
|
@ -103,7 +103,9 @@ func (v *containerView) extraActions(aa keyActions) {
|
|||
aa[KeyS] = newKeyAction("Shell", v.shellCmd, true)
|
||||
aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false)
|
||||
aa[KeyP] = newKeyAction("Previous", v.backCmd, false)
|
||||
aa[tcell.KeyEnter] = newKeyAction("Enter", v.logsCmd, false)
|
||||
aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false)
|
||||
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true)
|
||||
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true)
|
||||
}
|
||||
|
||||
func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type deployView struct {
|
||||
*resourceView
|
||||
}
|
||||
|
||||
func newDeployView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := deployView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.switchPage("deploy")
|
||||
}
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *deployView) extraActions(aa keyActions) {
|
||||
aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true)
|
||||
aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true)
|
||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||
}
|
||||
|
||||
func (v *deployView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t := v.getTV()
|
||||
t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc
|
||||
t.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *deployView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
ns, n := namespaced(v.selectedItem)
|
||||
d := k8s.NewDeployment(v.app.conn())
|
||||
dep, err := d.Get(ns, n)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching Deployment %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
dp := dep.(*v1.Deployment)
|
||||
|
||||
sel, err := metav1.LabelSelectorAsSelector(dp.Spec.Selector)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Converting selector for Deployment %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
showPods(v.app, "", "Deployment", v.selectedItem, sel.String(), "", v.backCmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *deployView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
extv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type daemonSetView struct {
|
||||
*resourceView
|
||||
}
|
||||
|
||||
func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := daemonSetView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.switchPage("ds")
|
||||
}
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *daemonSetView) extraActions(aa keyActions) {
|
||||
aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true)
|
||||
aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true)
|
||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||
}
|
||||
|
||||
func (v *daemonSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t := v.getTV()
|
||||
t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc
|
||||
t.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *daemonSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
ns, n := namespaced(v.selectedItem)
|
||||
d := k8s.NewDaemonSet(v.app.conn())
|
||||
dset, err := d.Get(ns, n)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching DeaemonSet %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
ds := dset.(*extv1beta1.DaemonSet)
|
||||
|
||||
sel, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Converting selector for DaemonSet %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
showPods(v.app, "", "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *daemonSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func runK(app *appView, args ...string) bool {
|
||||
func runK(clear bool, app *appView, args ...string) bool {
|
||||
bin, err := exec.LookPath("kubectl")
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to find kubeclt command in path %v", err)
|
||||
|
|
@ -24,21 +24,23 @@ func runK(app *appView, args ...string) bool {
|
|||
last := len(args) - 1
|
||||
if args[last] == "sh" {
|
||||
args[last] = "bash"
|
||||
if err := execute(bin, args...); err != nil {
|
||||
if err := execute(clear, bin, args...); err != nil {
|
||||
args[last] = "sh"
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := execute(bin, args...); err != nil {
|
||||
if err := execute(clear, bin, args...); err != nil {
|
||||
log.Error().Msgf("Command exited: %T %v %v", err, err, args)
|
||||
app.flash(flashErr, "Command exited:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func execute(bin string, args ...string) error {
|
||||
func execute(clear bool, bin string, args ...string) error {
|
||||
if clear {
|
||||
clearScreen()
|
||||
}
|
||||
log.Debug().Msgf("Running command > %s %s", bin, strings.Join(args, " "))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
|
@ -64,5 +66,6 @@ func execute(bin string, args ...string) error {
|
|||
}
|
||||
|
||||
func clearScreen() {
|
||||
log.Debug().Msg("Clearing screen...")
|
||||
fmt.Print("\033[H\033[2J")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ func (v *helpView) init(_ context.Context, _ string) {
|
|||
|
||||
views := []helpItem{
|
||||
{"?", "Help"},
|
||||
{"a", "Aliases view"},
|
||||
{"Ctrl-a", "Aliases view"},
|
||||
}
|
||||
fmt.Fprintf(v, "️️\n😱 [aqua::b]%s\n", "Help")
|
||||
for _, h := range views {
|
||||
|
|
@ -106,7 +106,7 @@ func (v *helpView) init(_ context.Context, _ string) {
|
|||
}
|
||||
|
||||
func (v *helpView) printHelp(key, desc string) {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [white::]%s\n", key, desc)
|
||||
fmt.Fprintf(v, "[dodgerblue::b]%9s [white::]%s\n", key, desc)
|
||||
}
|
||||
|
||||
func (v *helpView) hints() hints {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type jobView struct {
|
||||
|
|
@ -31,6 +34,9 @@ func newJobView(t string, app *appView, list resource.List) resourceViewer {
|
|||
|
||||
// Protocol...
|
||||
|
||||
func (v *jobView) setExtraActionsFn(f actionsFn) {
|
||||
}
|
||||
|
||||
func (v *jobView) appView() *appView {
|
||||
return v.app
|
||||
}
|
||||
|
|
@ -99,4 +105,37 @@ func (v *jobView) showLogs(path, co, view string, parent loggable, prev bool) {
|
|||
func (v *jobView) extraActions(aa keyActions) {
|
||||
aa[KeyL] = newKeyAction("Logs", v.logsCmd, true)
|
||||
aa[KeyShiftL] = newKeyAction("Previous Logs", v.prevLogsCmd, true)
|
||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||
}
|
||||
|
||||
func (v *jobView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
ns, n := namespaced(v.selectedItem)
|
||||
j := k8s.NewJob(v.app.conn())
|
||||
job, err := j.Get(ns, n)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching Job %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
jo := job.(*batchv1.Job)
|
||||
|
||||
sel, err := metav1.LabelSelectorAsSelector(jo.Spec.Selector)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Converting selector for Job %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
showPods(v.app, "", "Job", v.selectedItem, sel.String(), "", v.backCmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *jobView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
|
@ -22,6 +25,7 @@ func newNodeView(t string, app *appView, list resource.List) resourceViewer {
|
|||
func (v *nodeView) extraActions(aa keyActions) {
|
||||
aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(7, false), true)
|
||||
aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(8, false), true)
|
||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||
}
|
||||
|
||||
func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
|
|
@ -33,3 +37,34 @@ func (v *nodeView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcel
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *nodeView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
showPods(v.app, "", "Node", v.selectedItem, "", "spec.nodeName="+v.selectedItem, v.backCmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *nodeView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func showPods(app *appView, ns, res, selected, labelSel, fieldSel string, b actionHandler) {
|
||||
mx := k8s.NewMetricsServer(app.conn())
|
||||
list := resource.NewPodList(app.conn(), mx, ns)
|
||||
|
||||
list.SetLabelSelector(labelSel)
|
||||
list.SetFieldSelector(fieldSel)
|
||||
|
||||
title := fmt.Sprintf("%s:%s Pods", res, selected)
|
||||
pv := newPodView(title, app, list)
|
||||
pv.setExtraActionsFn(func(aa keyActions) {
|
||||
aa[tcell.KeyEsc] = newKeyAction("Back", b, true)
|
||||
})
|
||||
app.inject(pv)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ func (v *podView) shellIn(path, co string) {
|
|||
}
|
||||
args = append(args, "--", "sh")
|
||||
log.Debug().Msgf("Shell args %v", args)
|
||||
runK(v.app, args...)
|
||||
runK(true, v.app, args...)
|
||||
}
|
||||
|
||||
func (v *podView) extraActions(aa keyActions) {
|
||||
|
|
|
|||
|
|
@ -153,14 +153,14 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
|||
"ds": {
|
||||
title: "DaemonSets",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
viewFn: newDaemonSetView,
|
||||
listFn: resource.NewDaemonSetList,
|
||||
colorerFn: dpColorer,
|
||||
},
|
||||
"dp": {
|
||||
title: "Deployments",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
viewFn: newDeployView,
|
||||
listFn: resource.NewDeploymentList,
|
||||
colorerFn: dpColorer,
|
||||
},
|
||||
|
|
@ -255,7 +255,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
|||
"rs": {
|
||||
title: "ReplicaSets",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
viewFn: newReplicaSetView,
|
||||
listFn: resource.NewReplicaSetList,
|
||||
colorerFn: rsColorer,
|
||||
},
|
||||
|
|
@ -275,7 +275,7 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
|
|||
"sts": {
|
||||
title: "StatefulSets",
|
||||
api: "apps",
|
||||
viewFn: newResourceView,
|
||||
viewFn: newStatefulSetView,
|
||||
listFn: resource.NewStatefulSetList,
|
||||
colorerFn: stsColorer,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ type (
|
|||
selectedFn func() string
|
||||
decorateFn decorateFn
|
||||
colorerFn colorerFn
|
||||
actions keyActions
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie
|
|||
v := resourceView{
|
||||
app: app,
|
||||
title: title,
|
||||
actions: make(keyActions),
|
||||
list: list,
|
||||
selectedNS: list.GetNamespace(),
|
||||
Pages: tview.NewPages(),
|
||||
|
|
@ -103,6 +105,10 @@ func (v *resourceView) init(ctx context.Context, ns string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (v *resourceView) setExtraActionsFn(f actionsFn) {
|
||||
f(v.actions)
|
||||
}
|
||||
|
||||
func (v *resourceView) getTitle() string {
|
||||
return v.title
|
||||
}
|
||||
|
|
@ -251,7 +257,7 @@ func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
args = append(args, "-n", ns)
|
||||
args = append(args, "--context", v.app.config.K9s.CurrentContext)
|
||||
args = append(args, po)
|
||||
runK(v.app, args...)
|
||||
runK(true, v.app, args...)
|
||||
return evt
|
||||
}
|
||||
|
||||
|
|
@ -369,8 +375,7 @@ func (v *resourceView) refreshActions() {
|
|||
}
|
||||
|
||||
var nn []interface{}
|
||||
aa := make(keyActions)
|
||||
if k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") {
|
||||
if !v.list.HasSelectors() && k8s.CanIAccess(v.app.conn().Config(), log.Logger, "", "list", "namespaces", "namespace.v1") {
|
||||
var err error
|
||||
nn, err = k8s.NewNamespace(v.app.conn()).List(resource.AllNamespaces)
|
||||
if err != nil {
|
||||
|
|
@ -387,35 +392,35 @@ func (v *resourceView) refreshActions() {
|
|||
if v.list.Access(resource.NamespaceAccess) {
|
||||
v.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
||||
for i, n := range v.app.config.FavNamespaces() {
|
||||
aa[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true)
|
||||
v.actions[tcell.Key(numKeys[i])] = newKeyAction(n, v.switchNamespaceCmd, true)
|
||||
v.namespaces[i] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aa[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false)
|
||||
v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false)
|
||||
|
||||
aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false)
|
||||
aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false)
|
||||
aa[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
|
||||
v.actions[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false)
|
||||
v.actions[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false)
|
||||
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
|
||||
|
||||
if v.list.Access(resource.EditAccess) {
|
||||
aa[KeyE] = newKeyAction("Edit", v.editCmd, true)
|
||||
v.actions[KeyE] = newKeyAction("Edit", v.editCmd, true)
|
||||
}
|
||||
if v.list.Access(resource.DeleteAccess) {
|
||||
aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true)
|
||||
v.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true)
|
||||
}
|
||||
if v.list.Access(resource.ViewAccess) {
|
||||
aa[KeyY] = newKeyAction("YAML", v.viewCmd, true)
|
||||
v.actions[KeyY] = newKeyAction("YAML", v.viewCmd, true)
|
||||
}
|
||||
if v.list.Access(resource.DescribeAccess) {
|
||||
aa[KeyD] = newKeyAction("Describe", v.describeCmd, true)
|
||||
v.actions[KeyD] = newKeyAction("Describe", v.describeCmd, true)
|
||||
}
|
||||
|
||||
if v.extraActionsFn != nil {
|
||||
v.extraActionsFn(aa)
|
||||
v.extraActionsFn(v.actions)
|
||||
}
|
||||
t := v.getTV()
|
||||
t.setActions(aa)
|
||||
t.setActions(v.actions)
|
||||
v.app.setHints(t.hints())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/kubectl"
|
||||
)
|
||||
|
||||
type replicaSetView struct {
|
||||
*resourceView
|
||||
}
|
||||
|
||||
func newReplicaSetView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := replicaSetView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.switchPage("rs")
|
||||
}
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *replicaSetView) extraActions(aa keyActions) {
|
||||
aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(2, false), true)
|
||||
aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(3, false), true)
|
||||
aa[tcell.KeyCtrlB] = newKeyAction("Rollback", v.rollbackCmd, true)
|
||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||
}
|
||||
|
||||
func (v *replicaSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t := v.getTV()
|
||||
t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc
|
||||
t.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *replicaSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
ns, n := namespaced(v.selectedItem)
|
||||
rset := k8s.NewReplicaSet(v.app.conn())
|
||||
r, err := rset.Get(ns, n)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching ReplicaSet %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
rs := r.(*v1.ReplicaSet)
|
||||
|
||||
sel, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Converting selector for ReplicaSet %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
showPods(v.app, "", "ReplicaSet", v.selectedItem, sel.String(), "", v.backCmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *replicaSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *replicaSetView) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
confirm := v.GetPrimitive("confirm").(*tview.Modal)
|
||||
confirm.SetText(fmt.Sprintf("Rollback %s %s?", v.list.GetName(), v.selectedItem))
|
||||
confirm.SetDoneFunc(func(_ int, button string) {
|
||||
if button == "OK" {
|
||||
v.app.flash(flashInfo, fmt.Sprintf("Rolling back %s %s", v.list.GetName(), v.selectedItem))
|
||||
if !rollback(v.app, v.selectedItem) {
|
||||
v.app.flash(flashErr, "Rollback failed!")
|
||||
} else {
|
||||
v.refresh()
|
||||
}
|
||||
}
|
||||
v.switchPage(v.list.GetName())
|
||||
})
|
||||
v.SwitchToPage("confirm")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rollback(app *appView, selectedItem string) bool {
|
||||
ns, n := namespaced(selectedItem)
|
||||
rset := k8s.NewReplicaSet(app.conn())
|
||||
r, err := rset.Get(ns, n)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching ReplicaSet %s", selectedItem)
|
||||
app.flash(flashErr, err.Error())
|
||||
return false
|
||||
}
|
||||
rs := r.(*v1.ReplicaSet)
|
||||
|
||||
var ctrlName, ctrlKind, ctrlAPI string
|
||||
for _, ref := range rs.ObjectMeta.OwnerReferences {
|
||||
if ref.Controller != nil && *ref.Controller {
|
||||
ctrlAPI, ctrlKind, ctrlName = ref.APIVersion, ref.Kind, ref.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if ctrlName == "" || ctrlKind == "" || ctrlAPI == "" {
|
||||
app.flash(flashErr, "Unable to find controller for ReplicaSet %s", selectedItem)
|
||||
return false
|
||||
}
|
||||
|
||||
revision := rs.ObjectMeta.Annotations["deployment.kubernetes.io/revision"]
|
||||
if rs.Status.Replicas != 0 {
|
||||
app.flash(flashWarn, "Can not rollback the current replica!")
|
||||
return false
|
||||
}
|
||||
|
||||
dpr := k8s.NewDeployment(app.conn())
|
||||
dep, err := dpr.Get(ns, ctrlName)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching Deployment %s", selectedItem)
|
||||
app.flash(flashErr, err.Error())
|
||||
return false
|
||||
}
|
||||
dp := dep.(*appsv1.Deployment)
|
||||
|
||||
vers, err := strconv.Atoi(revision)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Revision conversion failed")
|
||||
return false
|
||||
}
|
||||
|
||||
tokens := strings.Split(ctrlAPI, "/")
|
||||
group := ctrlAPI
|
||||
if len(tokens) == 2 {
|
||||
group = tokens[0]
|
||||
}
|
||||
rb, err := kubectl.RollbackerFor(schema.GroupKind{group, ctrlKind}, app.conn().DialOrDie())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("No rollbacker")
|
||||
return false
|
||||
}
|
||||
|
||||
res, err := rb.Rollback(dp, map[string]string{}, int64(vers), false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Rollback failed")
|
||||
return false
|
||||
}
|
||||
log.Debug().Msgf("Version %s %s", revision, res)
|
||||
app.flash(flashInfo, fmt.Sprintf("Version %s %s", revision, res))
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type statefulSetView struct {
|
||||
*resourceView
|
||||
}
|
||||
|
||||
func newStatefulSetView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := statefulSetView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.switchPage("sts")
|
||||
}
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *statefulSetView) extraActions(aa keyActions) {
|
||||
aa[KeyShiftD] = newKeyAction("Sort Desired", v.sortColCmd(1, false), true)
|
||||
aa[KeyShiftC] = newKeyAction("Sort Current", v.sortColCmd(2, false), true)
|
||||
aa[tcell.KeyEnter] = newKeyAction("View Pods", v.showPodsCmd, true)
|
||||
}
|
||||
|
||||
func (v *statefulSetView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t := v.getTV()
|
||||
t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc
|
||||
t.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *statefulSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.rowSelected() {
|
||||
return evt
|
||||
}
|
||||
|
||||
ns, n := namespaced(v.selectedItem)
|
||||
d := k8s.NewStatefulSet(v.app.conn())
|
||||
s, err := d.Get(ns, n)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching StatefulSet %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
sts := s.(*v1.StatefulSet)
|
||||
|
||||
sel, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Converting selector for StatefulSet %s", v.selectedItem)
|
||||
v.app.flash(flashErr, err.Error())
|
||||
return evt
|
||||
}
|
||||
showPods(v.app, "", "StatefulSet", v.selectedItem, sel.String(), "", v.backCmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *statefulSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.app.inject(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -83,6 +83,9 @@ func (v *subjectView) init(c context.Context, _ string) {
|
|||
v.app.SetFocus(v)
|
||||
}
|
||||
|
||||
func (v *subjectView) setExtraActionsFn(f actionsFn) {
|
||||
}
|
||||
|
||||
func (v *subjectView) setColorerFn(f colorerFn) {}
|
||||
func (v *subjectView) setEnterFn(f enterFn) {}
|
||||
func (v *subjectView) setDecorateFn(f decorateFn) {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue