526 lines
11 KiB
Go
526 lines
11 KiB
Go
package views
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/derailed/k9s/internal/k8s"
|
|
"github.com/derailed/k9s/internal/resource"
|
|
"github.com/derailed/k9s/internal/ui"
|
|
"github.com/rs/zerolog/log"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
type (
|
|
viewFn func(ns string, app *appView, list resource.List) resourceViewer
|
|
listFn func(c resource.Connection, ns string) resource.List
|
|
enterFn func(app *appView, ns, resource, selection string)
|
|
decorateFn func(resource.TableData) resource.TableData
|
|
|
|
crdCmd struct {
|
|
api string
|
|
version string
|
|
plural string
|
|
singular string
|
|
}
|
|
|
|
resCmd struct {
|
|
crdCmd
|
|
|
|
kind string
|
|
viewFn viewFn
|
|
listFn listFn
|
|
enterFn enterFn
|
|
colorerFn ui.ColorerFunc
|
|
decorateFn decorateFn
|
|
}
|
|
|
|
AliasConfig struct {
|
|
Aliases map[string]string `yaml:"aliases"`
|
|
}
|
|
)
|
|
|
|
var DefaultAliasConfig = AliasConfig{
|
|
Aliases: map[string]string{
|
|
"Deployment": "dp",
|
|
"Secret": "sec",
|
|
"Jobs": "jo",
|
|
|
|
"ClusterRoles": "cr",
|
|
"ClusterRoleBindings": "crb",
|
|
"RoleBindings": "rb",
|
|
"Roles": "ro",
|
|
|
|
"NetworkPolicies": "np",
|
|
|
|
"Contexts": "ctx",
|
|
"Users": "usr",
|
|
"Groups": "grp",
|
|
"PortForward": "pf",
|
|
"Benchmark": "be",
|
|
"ScreenDumps": "sd",
|
|
},
|
|
}
|
|
|
|
func aliasCmds(c k8s.Connection, m map[string]*resCmd) {
|
|
resourceViews(c, m)
|
|
if c != nil {
|
|
allCRDs(c, m)
|
|
}
|
|
}
|
|
|
|
func allCRDs(c k8s.Connection, m map[string]*resCmd) {
|
|
crds, _ := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces).
|
|
Resource().
|
|
List(resource.AllNamespaces)
|
|
|
|
for _, crd := range crds {
|
|
ff := crd.ExtFields()
|
|
|
|
grp := k8s.APIGroup{
|
|
GKV: k8s.GKV{
|
|
Group: ff["group"].(string),
|
|
Kind: ff["kind"].(string),
|
|
Version: ff["version"].(string),
|
|
},
|
|
}
|
|
|
|
res := resCmd{
|
|
kind: grp.Kind,
|
|
crdCmd: crdCmd{
|
|
api: grp.Group,
|
|
version: grp.Version,
|
|
},
|
|
}
|
|
if p, ok := ff["plural"].(string); ok {
|
|
res.plural = p
|
|
m[p] = &res
|
|
}
|
|
|
|
if s, ok := ff["singular"].(string); ok {
|
|
res.singular = s
|
|
m[s] = &res
|
|
}
|
|
|
|
if aa, ok := ff["aliases"].([]interface{}); ok {
|
|
for _, a := range aa {
|
|
m[a.(string)] = &res
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func showRBAC(app *appView, ns, resource, selection string) {
|
|
kind := clusterRole
|
|
if resource == "role" {
|
|
kind = role
|
|
}
|
|
app.inject(newRBACView(app, ns, selection, kind))
|
|
}
|
|
|
|
func showCRD(app *appView, ns, resource, selection string) {
|
|
log.Debug().Msgf("Launching CRD %q -- %q -- %q", ns, resource, selection)
|
|
tokens := strings.Split(selection, ".")
|
|
app.gotoResource(tokens[0], true)
|
|
|
|
}
|
|
|
|
func showClusterRole(app *appView, ns, resource, selection string) {
|
|
crb, err := app.Conn().DialOrDie().RbacV1().ClusterRoleBindings().Get(selection, metav1.GetOptions{})
|
|
if err != nil {
|
|
app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", selection)
|
|
return
|
|
}
|
|
app.inject(newRBACView(app, ns, crb.RoleRef.Name, clusterRole))
|
|
}
|
|
|
|
func showRole(app *appView, _, resource, selection string) {
|
|
ns, n := namespaced(selection)
|
|
rb, err := app.Conn().DialOrDie().RbacV1().RoleBindings(ns).Get(n, metav1.GetOptions{})
|
|
if err != nil {
|
|
app.Flash().Errf("Unable to retrieve rolebindings for %s", selection)
|
|
return
|
|
}
|
|
app.inject(newRBACView(app, ns, fqn(ns, rb.RoleRef.Name), role))
|
|
}
|
|
|
|
func showSAPolicy(app *appView, _, _, selection string) {
|
|
_, n := namespaced(selection)
|
|
app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n))
|
|
}
|
|
|
|
func collect(commandList ...[]*resCmd) (accum []*resCmd) {
|
|
for _, commands := range commandList {
|
|
accum = append(accum, commands...)
|
|
}
|
|
return
|
|
}
|
|
|
|
func resourceViews(c k8s.Connection, cmdMap map[string]*resCmd) {
|
|
commands := collect(
|
|
primRes(), coreRes(), stateRes(), rbacRes(), apiExtRes(),
|
|
batchRes(), appsRes(), extRes(), v1beta1Res(), custRes(),
|
|
)
|
|
|
|
if c != nil {
|
|
commands = append(commands, hpaRes(c)...)
|
|
}
|
|
|
|
for _, rsc := range commands {
|
|
cmdMap[strings.ToLower(rsc.kind)] = rsc
|
|
}
|
|
|
|
// Add default aliases
|
|
// TODO: read aliases from a config file.
|
|
for rsc, alias := range DefaultAliasConfig.Aliases {
|
|
if cmd, ok := cmdMap[strings.ToLower(rsc)]; ok {
|
|
addAlias(cmdMap, cmd, alias)
|
|
}
|
|
}
|
|
|
|
if c != nil {
|
|
discoverAliasesFromServer(c, cmdMap)
|
|
}
|
|
}
|
|
|
|
func addAlias(cmdMap map[string]*resCmd, cmd *resCmd, alias string) {
|
|
if alias == "" {
|
|
return
|
|
}
|
|
|
|
alias = strings.ToLower(alias)
|
|
if _, ok := cmdMap[alias]; !ok {
|
|
cmdMap[alias] = cmd
|
|
}
|
|
}
|
|
|
|
func addAPIResourceAliases(cmds map[string]*resCmd, resource metav1.APIResource) {
|
|
if strings.Contains(resource.Name, "/") {
|
|
// Ignore resources that has slash, e.g.,
|
|
// deployment/status, namespace/finalizers and etc.
|
|
return
|
|
}
|
|
|
|
if cmd, ok := cmds[strings.ToLower(resource.Kind)]; ok {
|
|
addAlias(cmds, cmd, resource.Name)
|
|
addAlias(cmds, cmd, resource.SingularName)
|
|
for _, sn := range resource.ShortNames {
|
|
addAlias(cmds, cmd, sn)
|
|
}
|
|
}
|
|
}
|
|
|
|
func discoverAliasesFromServer(con k8s.Connection, cmds map[string]*resCmd) {
|
|
_, resourceLists, _ := con.DialOrDie().Discovery().ServerGroupsAndResources()
|
|
for _, resourceList := range resourceLists {
|
|
for _, resource := range resourceList.APIResources {
|
|
addAPIResourceAliases(cmds, resource)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stateRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "ConfigMap",
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewConfigMapList,
|
|
},
|
|
{
|
|
kind: "PersistentVolume",
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewPersistentVolumeList,
|
|
colorerFn: pvColorer,
|
|
},
|
|
{
|
|
kind: "PersistentVolumeClaim",
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewPersistentVolumeClaimList,
|
|
colorerFn: pvcColorer,
|
|
},
|
|
{
|
|
kind: "Secret",
|
|
viewFn: newSecretView,
|
|
listFn: resource.NewSecretList,
|
|
},
|
|
{
|
|
kind: "StorageClass",
|
|
crdCmd: crdCmd{
|
|
api: "storage.k8s.io",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewStorageClassList,
|
|
},
|
|
}
|
|
}
|
|
|
|
func primRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "Node",
|
|
viewFn: newNodeView,
|
|
listFn: resource.NewNodeList,
|
|
colorerFn: nsColorer,
|
|
},
|
|
{
|
|
kind: "Namespace",
|
|
viewFn: newNamespaceView,
|
|
listFn: resource.NewNamespaceList,
|
|
colorerFn: nsColorer,
|
|
},
|
|
{
|
|
kind: "Pod",
|
|
viewFn: newPodView,
|
|
listFn: resource.NewPodList,
|
|
colorerFn: podColorer,
|
|
},
|
|
{
|
|
kind: "ServiceAccount",
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewServiceAccountList,
|
|
enterFn: showSAPolicy,
|
|
},
|
|
{
|
|
kind: "Service",
|
|
viewFn: newSvcView,
|
|
listFn: resource.NewServiceList,
|
|
},
|
|
}
|
|
}
|
|
|
|
func coreRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "Contexts",
|
|
viewFn: newContextView,
|
|
listFn: resource.NewContextList,
|
|
colorerFn: ctxColorer,
|
|
},
|
|
{
|
|
kind: "DaemonSet",
|
|
viewFn: newDaemonSetView,
|
|
listFn: resource.NewDaemonSetList,
|
|
colorerFn: dpColorer,
|
|
},
|
|
{
|
|
kind: "EndPoints",
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewEndpointsList,
|
|
},
|
|
{
|
|
kind: "Event",
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewEventList,
|
|
colorerFn: evColorer,
|
|
},
|
|
{
|
|
kind: "ReplicationController",
|
|
viewFn: newScalableResourceView,
|
|
listFn: resource.NewReplicationControllerList,
|
|
colorerFn: rsColorer,
|
|
},
|
|
}
|
|
}
|
|
|
|
func custRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "Users",
|
|
viewFn: newSubjectView,
|
|
},
|
|
{
|
|
kind: "Groups",
|
|
viewFn: newSubjectView,
|
|
},
|
|
{
|
|
kind: "PortForward",
|
|
viewFn: newForwardView,
|
|
},
|
|
{
|
|
kind: "Benchmark",
|
|
viewFn: newBenchView,
|
|
},
|
|
{
|
|
kind: "ScreenDumps",
|
|
viewFn: newDumpView,
|
|
},
|
|
}
|
|
|
|
}
|
|
|
|
func rbacRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "ClusterRole",
|
|
crdCmd: crdCmd{
|
|
api: "rbac.authorization.k8s.io",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewClusterRoleList,
|
|
enterFn: showRBAC,
|
|
},
|
|
{
|
|
kind: "ClusterRoleBinding",
|
|
crdCmd: crdCmd{
|
|
api: "rbac.authorization.k8s.io",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewClusterRoleBindingList,
|
|
enterFn: showClusterRole,
|
|
},
|
|
{
|
|
kind: "RoleBinding",
|
|
crdCmd: crdCmd{
|
|
api: "rbac.authorization.k8s.io",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewRoleBindingList,
|
|
enterFn: showRole,
|
|
},
|
|
{
|
|
kind: "Role",
|
|
crdCmd: crdCmd{
|
|
api: "rbac.authorization.k8s.io",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewRoleList,
|
|
enterFn: showRBAC,
|
|
},
|
|
}
|
|
}
|
|
|
|
func apiExtRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "CustomResourceDefinition",
|
|
crdCmd: crdCmd{
|
|
api: "apiextensions.k8s.io",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewCustomResourceDefinitionList,
|
|
enterFn: showCRD,
|
|
},
|
|
{
|
|
kind: "NetworkPolicy",
|
|
crdCmd: crdCmd{
|
|
api: "apiextensions.k8s.io",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewNetworkPolicyList,
|
|
},
|
|
}
|
|
}
|
|
|
|
func batchRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "CronJob",
|
|
crdCmd: crdCmd{
|
|
api: "batch",
|
|
},
|
|
viewFn: newCronJobView,
|
|
listFn: resource.NewCronJobList,
|
|
},
|
|
{
|
|
kind: "Job",
|
|
crdCmd: crdCmd{
|
|
api: "batch",
|
|
},
|
|
viewFn: newJobView,
|
|
listFn: resource.NewJobList,
|
|
},
|
|
}
|
|
}
|
|
|
|
func appsRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "Deployment",
|
|
crdCmd: crdCmd{
|
|
api: "apps",
|
|
},
|
|
viewFn: newDeployView,
|
|
listFn: resource.NewDeploymentList,
|
|
colorerFn: dpColorer,
|
|
},
|
|
{
|
|
kind: "ReplicaSet",
|
|
crdCmd: crdCmd{
|
|
api: "apps",
|
|
},
|
|
viewFn: newReplicaSetView,
|
|
listFn: resource.NewReplicaSetList,
|
|
colorerFn: rsColorer,
|
|
},
|
|
{
|
|
kind: "StatefulSet",
|
|
crdCmd: crdCmd{
|
|
api: "apps",
|
|
},
|
|
viewFn: newStatefulSetView,
|
|
listFn: resource.NewStatefulSetList,
|
|
colorerFn: stsColorer,
|
|
},
|
|
}
|
|
|
|
}
|
|
|
|
func extRes() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "Ingress",
|
|
crdCmd: crdCmd{
|
|
api: "extensions",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewIngressList,
|
|
},
|
|
}
|
|
}
|
|
|
|
func v1beta1Res() []*resCmd {
|
|
return []*resCmd{
|
|
{
|
|
kind: "PodDisruptionBudget",
|
|
crdCmd: crdCmd{
|
|
api: "v1.beta1",
|
|
},
|
|
viewFn: newResourceView,
|
|
listFn: resource.NewPDBList,
|
|
colorerFn: pdbColorer,
|
|
},
|
|
}
|
|
}
|
|
|
|
func hpaRes(c k8s.Connection) []*resCmd {
|
|
rev, ok, err := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Checking HPA")
|
|
return nil
|
|
}
|
|
if !ok {
|
|
log.Error().Msg("HPA are not supported on this cluster")
|
|
return nil
|
|
}
|
|
|
|
hpa := &resCmd{
|
|
kind: "HorizontalPodAutoscaler",
|
|
crdCmd: crdCmd{
|
|
api: "autoscaling",
|
|
},
|
|
viewFn: newResourceView,
|
|
}
|
|
|
|
switch rev {
|
|
case "v1":
|
|
hpa.listFn = resource.NewHorizontalPodAutoscalerV1List
|
|
case "v2beta1":
|
|
hpa.listFn = resource.NewHorizontalPodAutoscalerV2Beta1List
|
|
case "v2beta2":
|
|
hpa.listFn = resource.NewHorizontalPodAutoscalerList
|
|
default:
|
|
log.Panic().Msgf("K9s unsupported HPA version. Exiting!")
|
|
}
|
|
|
|
return []*resCmd{hpa}
|
|
}
|