514 lines
13 KiB
Go
514 lines
13 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Authors of K9s
|
|
|
|
package dao
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/derailed/k9s/internal/client"
|
|
"github.com/rs/zerolog/log"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
)
|
|
|
|
const (
|
|
crdCat = "crd"
|
|
k9sCat = "k9s"
|
|
helmCat = "helm"
|
|
)
|
|
|
|
// MetaAccess tracks resources metadata.
|
|
var MetaAccess = NewMeta()
|
|
|
|
var stdGroups = []string{
|
|
"admissionregistration.k8s.io/v1",
|
|
"admissionregistration.k8s.io/v1beta1",
|
|
"apiextensions.k8s.io/v1",
|
|
"apiextensions.k8s.io/v1beta1",
|
|
"apiregistration.k8s.io/v1",
|
|
"apiregistration.k8s.io/v1beta1",
|
|
"apps/v1",
|
|
"authentication.k8s.io/v1",
|
|
"authentication.k8s.io/v1beta1",
|
|
"authorization.k8s.io/v1",
|
|
"authorization.k8s.io/v1beta1",
|
|
"autoscaling/v1",
|
|
"autoscaling/v2beta1",
|
|
"autoscaling/v2beta2",
|
|
"batch/v1",
|
|
"batch/v1beta1",
|
|
"certificates.k8s.io/v1",
|
|
"certificates.k8s.io/v1beta1",
|
|
"coordination.k8s.io/v1",
|
|
"coordination.k8s.io/v1beta1",
|
|
"discovery.k8s.io/v1beta1",
|
|
"dynatrace.com/v1alpha1",
|
|
"events.k8s.io/v1",
|
|
"extensions/v1beta1",
|
|
"flowcontrol.apiserver.k8s.io/v1beta1",
|
|
"metrics.k8s.io/v1beta1",
|
|
"networking.k8s.io/v1",
|
|
"networking.k8s.io/v1beta1",
|
|
"node.k8s.io/v1",
|
|
"node.k8s.io/v1beta1",
|
|
"policy/v1beta1",
|
|
"rbac.authorization.k8s.io/v1",
|
|
"rbac.authorization.k8s.io/v1beta1",
|
|
"scheduling.k8s.io/v1",
|
|
"scheduling.k8s.io/v1beta1",
|
|
"storage.k8s.io/v1",
|
|
"storage.k8s.io/v1beta1",
|
|
"v1",
|
|
}
|
|
|
|
// Meta represents available resource metas.
|
|
type Meta struct {
|
|
resMetas ResourceMetas
|
|
mx sync.RWMutex
|
|
}
|
|
|
|
// NewMeta returns a resource meta.
|
|
func NewMeta() *Meta {
|
|
return &Meta{resMetas: make(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 client.GVR) (Accessor, error) {
|
|
m := Accessors{
|
|
client.NewGVR("contexts"): &Context{},
|
|
client.NewGVR("containers"): &Container{},
|
|
client.NewGVR("scans"): &ImageScan{},
|
|
client.NewGVR("screendumps"): &ScreenDump{},
|
|
client.NewGVR("benchmarks"): &Benchmark{},
|
|
client.NewGVR("portforwards"): &PortForward{},
|
|
client.NewGVR("v1/services"): &Service{},
|
|
client.NewGVR("v1/pods"): &Pod{},
|
|
client.NewGVR("v1/nodes"): &Node{},
|
|
client.NewGVR("apps/v1/deployments"): &Deployment{},
|
|
client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},
|
|
client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
|
|
client.NewGVR("apps/v1/replicasets"): &ReplicaSet{},
|
|
client.NewGVR("batch/v1/cronjobs"): &CronJob{},
|
|
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
|
|
client.NewGVR("batch/v1/jobs"): &Job{},
|
|
client.NewGVR("v1/namespaces"): &Namespace{},
|
|
client.NewGVR("popeye"): &Popeye{},
|
|
client.NewGVR("helm"): &HelmChart{},
|
|
client.NewGVR("dir"): &Dir{},
|
|
}
|
|
|
|
r, ok := m[gvr]
|
|
if !ok {
|
|
r = new(Generic)
|
|
log.Debug().Msgf("No DAO registry entry for %q. Using generics!", gvr)
|
|
}
|
|
r.Init(f, gvr)
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// RegisterMeta registers a new resource meta object.
|
|
func (m *Meta) RegisterMeta(gvr string, res metav1.APIResource) {
|
|
m.mx.Lock()
|
|
defer m.mx.Unlock()
|
|
|
|
m.resMetas[client.NewGVR(gvr)] = res
|
|
}
|
|
|
|
// AllGVRs returns all cluster resources.
|
|
func (m *Meta) AllGVRs() client.GVRs {
|
|
m.mx.RLock()
|
|
defer m.mx.RUnlock()
|
|
|
|
kk := make(client.GVRs, 0, len(m.resMetas))
|
|
for k := range m.resMetas {
|
|
kk = append(kk, k)
|
|
}
|
|
sort.Sort(kk)
|
|
|
|
return kk
|
|
}
|
|
|
|
// IsCRD checks if resource represents a CRD
|
|
func IsCRD(r metav1.APIResource) bool {
|
|
for _, c := range r.Categories {
|
|
if c == crdCat {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MetaFor returns a resource metadata for a given gvr.
|
|
func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) {
|
|
m.mx.RLock()
|
|
defer m.mx.RUnlock()
|
|
|
|
meta, ok := m.resMetas[gvr]
|
|
if !ok {
|
|
return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
|
|
}
|
|
return meta, nil
|
|
}
|
|
|
|
// IsK8sMeta checks for non resource meta.
|
|
func IsK8sMeta(m metav1.APIResource) bool {
|
|
for _, c := range m.Categories {
|
|
if c == k9sCat || c == helmCat {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsK9sMeta checks for non resource meta.
|
|
func IsK9sMeta(m metav1.APIResource) bool {
|
|
for _, c := range m.Categories {
|
|
if c == k9sCat {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// LoadResources hydrates server preferred+CRDs resource metadata.
|
|
func (m *Meta) LoadResources(f Factory) error {
|
|
m.mx.Lock()
|
|
defer m.mx.Unlock()
|
|
|
|
m.resMetas = make(ResourceMetas, 100)
|
|
if err := loadPreferred(f, m.resMetas); err != nil {
|
|
return err
|
|
}
|
|
loadNonResource(m.resMetas)
|
|
loadCRDs(f, m.resMetas)
|
|
|
|
return nil
|
|
}
|
|
|
|
// BOZO!! Need countermeasures for direct commands!
|
|
func loadNonResource(m ResourceMetas) {
|
|
loadK9s(m)
|
|
loadRBAC(m)
|
|
loadHelm(m)
|
|
}
|
|
|
|
func loadK9s(m ResourceMetas) {
|
|
m[client.NewGVR("pulses")] = metav1.APIResource{
|
|
Name: "pulses",
|
|
Kind: "Pulse",
|
|
SingularName: "pulses",
|
|
ShortNames: []string{"hz", "pu"},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("dir")] = metav1.APIResource{
|
|
Name: "dir",
|
|
Kind: "Dir",
|
|
SingularName: "dir",
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("xrays")] = metav1.APIResource{
|
|
Name: "xray",
|
|
Kind: "XRays",
|
|
SingularName: "xray",
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("references")] = metav1.APIResource{
|
|
Name: "references",
|
|
Kind: "References",
|
|
SingularName: "reference",
|
|
Verbs: []string{},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("aliases")] = metav1.APIResource{
|
|
Name: "aliases",
|
|
Kind: "Aliases",
|
|
SingularName: "alias",
|
|
Verbs: []string{},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("popeye")] = metav1.APIResource{
|
|
Name: "popeye",
|
|
Kind: "Popeye",
|
|
SingularName: "popeye",
|
|
Namespaced: true,
|
|
Verbs: []string{},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("sanitizer")] = metav1.APIResource{
|
|
Name: "sanitizer",
|
|
Kind: "Sanitizer",
|
|
SingularName: "sanitizer",
|
|
Verbs: []string{},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("contexts")] = metav1.APIResource{
|
|
Name: "contexts",
|
|
Kind: "Contexts",
|
|
SingularName: "context",
|
|
ShortNames: []string{"ctx"},
|
|
Verbs: []string{},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("screendumps")] = metav1.APIResource{
|
|
Name: "screendumps",
|
|
Kind: "ScreenDumps",
|
|
SingularName: "screendump",
|
|
ShortNames: []string{"sd"},
|
|
Verbs: []string{"delete"},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("benchmarks")] = metav1.APIResource{
|
|
Name: "benchmarks",
|
|
Kind: "Benchmarks",
|
|
SingularName: "benchmark",
|
|
ShortNames: []string{"be"},
|
|
Verbs: []string{"delete"},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("portforwards")] = metav1.APIResource{
|
|
Name: "portforwards",
|
|
Namespaced: true,
|
|
Kind: "PortForwards",
|
|
SingularName: "portforward",
|
|
ShortNames: []string{"pf"},
|
|
Verbs: []string{"delete"},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("containers")] = metav1.APIResource{
|
|
Name: "containers",
|
|
Kind: "Containers",
|
|
SingularName: "container",
|
|
Verbs: []string{},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("scans")] = metav1.APIResource{
|
|
Name: "scans",
|
|
Kind: "Scans",
|
|
SingularName: "scan",
|
|
Verbs: []string{},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
}
|
|
|
|
func loadHelm(m ResourceMetas) {
|
|
m[client.NewGVR("helm")] = metav1.APIResource{
|
|
Name: "chart",
|
|
Kind: "Chart",
|
|
Namespaced: true,
|
|
Verbs: []string{"delete"},
|
|
Categories: []string{helmCat},
|
|
}
|
|
m[client.NewGVR("helm-history")] = metav1.APIResource{
|
|
Name: "history",
|
|
Kind: "History",
|
|
Namespaced: true,
|
|
Verbs: []string{"delete"},
|
|
Categories: []string{k9sCat},
|
|
}
|
|
}
|
|
|
|
func loadRBAC(m ResourceMetas) {
|
|
m[client.NewGVR("rbac")] = metav1.APIResource{
|
|
Name: "rbacs",
|
|
Kind: "Rules",
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("policy")] = metav1.APIResource{
|
|
Name: "policies",
|
|
Kind: "Rules",
|
|
Namespaced: true,
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("users")] = metav1.APIResource{
|
|
Name: "users",
|
|
Kind: "User",
|
|
Categories: []string{k9sCat},
|
|
}
|
|
m[client.NewGVR("groups")] = metav1.APIResource{
|
|
Name: "groups",
|
|
Kind: "Group",
|
|
Categories: []string{k9sCat},
|
|
}
|
|
}
|
|
|
|
func loadPreferred(f Factory, m ResourceMetas) error {
|
|
if f.Client() == nil || !f.Client().ConnectionOK() {
|
|
log.Error().Msgf("Load cluster resources - No API server connection")
|
|
return nil
|
|
}
|
|
|
|
dial, err := f.Client().CachedDiscovery()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rr, err := dial.ServerPreferredResources()
|
|
if err != nil {
|
|
log.Debug().Err(err).Msgf("Failed to load preferred resources")
|
|
}
|
|
for _, r := range rr {
|
|
for _, res := range r.APIResources {
|
|
gvr := client.FromGVAndR(r.GroupVersion, res.Name)
|
|
if isDeprecated(gvr) {
|
|
continue
|
|
}
|
|
res.Group, res.Version = gvr.G(), gvr.V()
|
|
if res.SingularName == "" {
|
|
res.SingularName = strings.ToLower(res.Kind)
|
|
}
|
|
if !isStandardGroup(res.Group) {
|
|
res.Categories = append(res.Categories, crdCat)
|
|
}
|
|
m[gvr] = res
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isStandardGroup(r string) bool {
|
|
for _, res := range stdGroups {
|
|
if strings.Index(res, r) == 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var deprecatedGVRs = map[client.GVR]struct{}{
|
|
client.NewGVR("extensions/v1beta1/ingresses"): {},
|
|
}
|
|
|
|
func isDeprecated(gvr client.GVR) bool {
|
|
_, ok := deprecatedGVRs[gvr]
|
|
return ok
|
|
}
|
|
|
|
func loadCRDs(f Factory, m ResourceMetas) {
|
|
if f.Client() == nil || !f.Client().ConnectionOK() {
|
|
return
|
|
}
|
|
const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
|
|
oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
|
|
if err != nil {
|
|
log.Warn().Err(err).Msgf("Fail CRDs load")
|
|
return
|
|
}
|
|
|
|
for _, o := range oo {
|
|
meta, errs := extractMeta(o)
|
|
if len(errs) > 0 {
|
|
log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs))
|
|
continue
|
|
}
|
|
meta.Categories = append(meta.Categories, crdCat)
|
|
gvr := client.NewGVRFromMeta(meta)
|
|
m[gvr] = meta
|
|
}
|
|
}
|
|
|
|
func extractMeta(o runtime.Object) (metav1.APIResource, []error) {
|
|
var (
|
|
m metav1.APIResource
|
|
errs []error
|
|
)
|
|
|
|
crd, ok := o.(*unstructured.Unstructured)
|
|
if !ok {
|
|
return m, append(errs, fmt.Errorf("expected unstructured, but got %T", o))
|
|
}
|
|
|
|
var spec map[string]interface{}
|
|
spec, errs = extractMap(crd.Object, "spec", errs)
|
|
|
|
var meta map[string]interface{}
|
|
meta, errs = extractMap(crd.Object, "metadata", errs)
|
|
m.Name, errs = extractStr(meta, "name", errs)
|
|
|
|
m.Group, errs = extractStr(spec, "group", errs)
|
|
versions, errs := extractSlice(spec, "versions", errs)
|
|
if len(versions) > 0 {
|
|
m.Version = versions[0]
|
|
}
|
|
|
|
var scope string
|
|
scope, errs = extractStr(spec, "scope", errs)
|
|
|
|
m.Namespaced = isNamespaced(scope)
|
|
|
|
var names map[string]interface{}
|
|
names, errs = extractMap(spec, "names", errs)
|
|
m.Kind, errs = extractStr(names, "kind", errs)
|
|
m.SingularName, errs = extractStr(names, "singular", errs)
|
|
m.Name, errs = extractStr(names, "plural", errs)
|
|
m.ShortNames, errs = extractSlice(names, "shortNames", errs)
|
|
|
|
return m, errs
|
|
}
|
|
|
|
func isNamespaced(scope string) bool {
|
|
return scope == "Namespaced"
|
|
}
|
|
|
|
func extractSlice(m map[string]interface{}, n string, errs []error) ([]string, []error) {
|
|
if m[n] == nil {
|
|
return nil, errs
|
|
}
|
|
|
|
s, ok := m[n].([]string)
|
|
if ok {
|
|
return s, errs
|
|
}
|
|
|
|
ii, ok := m[n].([]interface{})
|
|
if !ok {
|
|
return s, append(errs, fmt.Errorf("failed to extract slice %s -- %#v", n, m))
|
|
}
|
|
|
|
ss := make([]string, 0, len(ii))
|
|
for _, name := range ii {
|
|
switch o := name.(type) {
|
|
case string:
|
|
ss = append(ss, o)
|
|
case map[string]interface{}:
|
|
s, ok := o["name"].(string)
|
|
if ok {
|
|
ss = append(ss, s)
|
|
} else {
|
|
errs = append(errs, fmt.Errorf("unable to find key %q in map", n))
|
|
}
|
|
default:
|
|
errs = append(errs, fmt.Errorf("unknown field type %t for key %q", o, n))
|
|
}
|
|
}
|
|
|
|
return ss, errs
|
|
}
|
|
|
|
func extractStr(m map[string]interface{}, n string, errs []error) (string, []error) {
|
|
s, ok := m[n].(string)
|
|
if !ok {
|
|
return s, append(errs, fmt.Errorf("failed to extract string %s", n))
|
|
}
|
|
return s, errs
|
|
}
|
|
|
|
func extractMap(m map[string]interface{}, n string, errs []error) (map[string]interface{}, []error) {
|
|
v, ok := m[n].(map[string]interface{})
|
|
if !ok {
|
|
return v, append(errs, fmt.Errorf("failed to extract field %s", n))
|
|
}
|
|
return v, errs
|
|
}
|