214 lines
5.0 KiB
Go
214 lines
5.0 KiB
Go
package dao
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/derailed/k9s/internal/watch"
|
|
"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"
|
|
)
|
|
|
|
// MetaViewers represents a collection of meta viewers.
|
|
type ResourceMetas map[GVR]metav1.APIResource
|
|
|
|
var resMetas ResourceMetas
|
|
|
|
func AccessorFor(f Factory, gvr GVR) (Accessor, error) {
|
|
m := map[GVR]Accessor{
|
|
"contexts": &Context{},
|
|
"screendumps": &ScreenDump{},
|
|
"apps/v1/deployments": &Deployment{},
|
|
"apps/v1/daemonsets": &DaemonSet{},
|
|
"extensions/v1beta1/daemonsets": &DaemonSet{},
|
|
"apps/v1/statefulsets": &StatefulSet{},
|
|
}
|
|
|
|
r, ok := m[gvr]
|
|
if !ok {
|
|
r = &Resource{}
|
|
log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr)
|
|
}
|
|
r.Init(f, gvr)
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func AllGVRs() []GVR {
|
|
kk := make(GVRs, 0, len(resMetas))
|
|
for k := range resMetas {
|
|
kk = append(kk, k)
|
|
}
|
|
sort.Sort(kk)
|
|
|
|
return kk
|
|
}
|
|
|
|
// MetaFor returns a resource metadata for a given gvr.
|
|
func MetaFor(gvr GVR) (metav1.APIResource, error) {
|
|
m, ok := resMetas[gvr]
|
|
if !ok {
|
|
return metav1.APIResource{}, fmt.Errorf("no resource meta defined for %q", gvr)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// Load hydrates server preferred+CRDs resource metadata.
|
|
func Load(f *watch.Factory) error {
|
|
resMetas = make(ResourceMetas, 100)
|
|
if err := loadPreferred(f, resMetas); err != nil {
|
|
return err
|
|
}
|
|
if err := loadNonResource(resMetas); err != nil {
|
|
return err
|
|
}
|
|
|
|
return loadCRDs(f, resMetas)
|
|
}
|
|
|
|
func loadNonResource(m ResourceMetas) error {
|
|
m["contexts"] = metav1.APIResource{
|
|
Name: "contexts",
|
|
SingularName: "context",
|
|
Namespaced: false,
|
|
Kind: "Context",
|
|
ShortNames: []string{"ctx"},
|
|
Verbs: []string{},
|
|
Categories: []string{"K9s"},
|
|
}
|
|
m["screendumps"] = metav1.APIResource{
|
|
Name: "screendumps",
|
|
SingularName: "screendump",
|
|
Namespaced: false,
|
|
Kind: "ScreenDump",
|
|
ShortNames: []string{"sd"},
|
|
Verbs: []string{"delete"},
|
|
Categories: []string{"K9s"},
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func loadPreferred(f *watch.Factory, m ResourceMetas) error {
|
|
discovery, err := f.Client().CachedDiscovery()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rr, err := discovery.ServerPreferredResources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, r := range rr {
|
|
for _, res := range r.APIResources {
|
|
gvr := FromGVAndR(r.GroupVersion, res.Name)
|
|
res.Group, res.Version = gvr.ToG(), gvr.ToV()
|
|
m[gvr] = res
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func loadCRDs(f *watch.Factory, m ResourceMetas) error {
|
|
oo, err := f.List("", "apiextensions.k8s.io/v1beta1/customresourcedefinitions", labels.Everything())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.WaitForCacheSync()
|
|
|
|
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
|
|
}
|
|
gvr := NewGVR(meta.Group, meta.Version, meta.Name)
|
|
m[gvr] = meta
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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 CustomResourceDefinition, 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)
|
|
m.Version, errs = extractStr(spec, "version", errs)
|
|
|
|
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, len(ii))
|
|
for i, name := range ii {
|
|
ss[i], ok = name.(string)
|
|
if !ok {
|
|
return s, append(errs, fmt.Errorf("expecting string shortnames"))
|
|
}
|
|
}
|
|
return s, 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
|
|
}
|