bergerx/kubectl-status

View on GitHub
pkg/plugin/render_engine.go

Summary

Maintainability
A
40 mins
Test Coverage
package plugin

import (
    "embed"
    "fmt"
    "os"
    "path/filepath"
    "text/template"

    "github.com/go-sprout/sprout"
    "k8s.io/apimachinery/pkg/api/meta"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/cli-runtime/pkg/genericiooptions"
    "k8s.io/cli-runtime/pkg/resource"
    "k8s.io/klog/v2"
    "k8s.io/kubectl/pkg/cmd/util"

    "github.com/bergerx/kubectl-status/pkg/input"
)

//go:embed templates
var templatesFS embed.FS

// renderEngine provides methods to build kubernetes api queries from provided cli options.
// Also holds the parsed templates.
type renderEngine struct {
    ioStreams genericiooptions.IOStreams
    repo      input.ResourceRepo
    template.Template
}

func newRenderEngine(f util.Factory, streams genericiooptions.IOStreams) (*renderEngine, error) {
    klog.V(5).InfoS("Creating new render engine instance...", "f", f)
    tmpl, err := getTemplate()
    repo := input.NewResourceRepo(f)
    if err != nil {
        klog.V(3).ErrorS(err, "Error parsing templates")
        return nil, err
    }
    return &renderEngine{
        streams,
        repo,
        *tmpl,
    }, nil
}

// We don't overlay templates dynamically, we use them all in all cases, this may be inefficient and changing this
// could be beneficial in the future. But we parse them all once and re-use again for all template executions.
func getTemplate() (*template.Template, error) {
    klog.V(5).InfoS("Creating new template instance...")
    tmpl := template.
        New("templates").
        Funcs(sprout.TxtFuncMap()).
        Funcs(funcMap())
    return parseTemplates(tmpl)
}

func parseTemplates(tmpl *template.Template) (*template.Template, error) {
    klog.V(5).InfoS("parsing templates from the embedded template fs ...")
    parsedTemplates, err := tmpl.ParseFS(templatesFS, "templates/*.tmpl")
    if err != nil {
        klog.V(3).ErrorS(err, "Error parsing some templates")
        return nil, err
    }
    homeDir, err := os.UserHomeDir()
    if err != nil {
        klog.V(3).ErrorS(err, "error getting user home dir, ignoring")
    }
    templatesDir := filepath.Join(homeDir, ".kubectl-status", "templates")
    parsedTemplatesWithLocalTemplates, err := parsedTemplates.ParseGlob(filepath.Join(templatesDir, "*.tmpl"))
    if err != nil {
        klog.V(1).ErrorS(err, "Error parsing user provided templates, ignoring user provided templates")
    } else {
        parsedTemplates = parsedTemplatesWithLocalTemplates
    }
    klog.V(5).InfoS("Finished parsing all embedded template fs files.")
    return parsedTemplates, nil
}

// Use kind name if such template exists in templates, else returnDefaultResource
func findTemplateName(tmpl template.Template, kind string) string {
    if tmpl.Lookup(kind) == nil {
        return "DefaultResource"
    }
    return kind
}

// This is a modified copy of resource.Builder's mappingFor method.
func (e *renderEngine) mappingFor(resourceOrKindArg string) (*meta.RESTMapping, error) {
    fullySpecifiedGVR, groupResource := schema.ParseResourceArg(resourceOrKindArg)
    gvk := schema.GroupVersionKind{}
    restMapper, err := e.repo.ToRESTMapper()
    if err != nil {
        return nil, err
    }

    if fullySpecifiedGVR != nil {
        gvk, _ = restMapper.KindFor(*fullySpecifiedGVR)
    }
    if gvk.Empty() {
        gvk, _ = restMapper.KindFor(groupResource.WithVersion(""))
    }
    if !gvk.Empty() {
        return restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
    }

    fullySpecifiedGVK, groupKind := schema.ParseKindArg(resourceOrKindArg)
    if fullySpecifiedGVK == nil {
        gvk := groupKind.WithVersion("")
        fullySpecifiedGVK = &gvk
    }

    if !fullySpecifiedGVK.Empty() {
        if mapping, err := restMapper.RESTMapping(fullySpecifiedGVK.GroupKind(), fullySpecifiedGVK.Version); err == nil {
            return mapping, nil
        }
    }

    mapping, err := restMapper.RESTMapping(groupKind, gvk.Version)
    if err != nil {
        // if we error out here, it is because we could not match a resource or a kind
        // for the given argument. To maintain consistency with previous behavior,
        // announce that a resource type could not be found.
        // if the error is _not_ a *meta.NoKindMatchError, then we had trouble doing discovery,
        // so we should return the original error since it may help a user diagnose what is actually wrong
        if meta.IsNoMatchError(err) {
            return nil, fmt.Errorf("the server doesn't have a resource type %q", groupResource.Resource)
        }
        return nil, err
    }

    return mapping, nil
}

func (e renderEngine) getResourceQueryInfos(namespace string, args []string) ([]*resource.Info, error) {
    klog.V(5).InfoS("getResourceQueryInfos", "namespace", namespace, "args", args)
    return e.repo.ResourceInfos(namespace, args, "")
}