Restores display of object status when displaying helm status

Changes to the Kubernetes API server and kubectl libraries caused
the status to no longer display when helm status was run for a
release. This change restores the status display.

Generation of the tables for display was moved server
side. A request for the data as a table is made and a kubectl
printer for tables can display this data. Kubectl uses this setup and
the structure here closely resembles kubectl. kubectl is still
able to display objects as tables from prior to server side
printing but only prints limited information.

Note, an extra request is made because table responses cannot be
easily transformed into Go objects for Kubernetes types to work
with. There is one request to get the resources for display in
a table and a second request to get the resources to lookup the
related pods. The related pods are now requested as a table as
well for display purposes.

This is likely part of the larger trend to move features like
this server side so that more libraries in more languages can
get to the feature.

Closes #6896

Signed-off-by: Matt Farina <matt@mattfarina.com>
pull/7785/head
Matt Farina 6 years ago
parent b7d2947d10
commit e8396c9283
No known key found for this signature in database
GPG Key ID: 9436E80BFBA46909

@ -23,6 +23,7 @@ import (
goerrors "errors" goerrors "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"sort" "sort"
"strings" "strings"
@ -42,6 +43,8 @@ import (
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -51,13 +54,14 @@ import (
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
cachetools "k8s.io/client-go/tools/cache" cachetools "k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch" watchtools "k8s.io/client-go/tools/watch"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/validation" "k8s.io/kubectl/pkg/validation"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/kubectl/cmd/get" "k8s.io/kubernetes/pkg/kubectl/cmd/get"
) )
@ -158,6 +162,40 @@ func (c *Client) BuildUnstructured(namespace string, reader io.Reader) (Result,
return result, scrubValidationError(err) return result, scrubValidationError(err)
} }
// BuildUnstructuredTable reads Kubernetes objects and returns unstructured infos
// as a Table. This is meant for viewing resources and displaying them in a table.
// This is similar to BuildUnstructured but transforms the request for table
// display.
func (c *Client) BuildUnstructuredTable(namespace string, reader io.Reader) (Result, error) {
var result Result
result, err := c.NewBuilder().
Unstructured().
ContinueOnError().
NamespaceParam(namespace).
DefaultNamespace().
Stream(reader, "").
Flatten().
TransformRequests(transformRequests).
Do().Infos()
return result, scrubValidationError(err)
}
// This is used to retrieve a table view of the data. A table view is how kubectl
// retrieves the information Helm displays as resources in status. Note, table
// data is returned as a Table type that does not conform to the runtime.Object
// interface but is that type. So, you can't transform it into Go objects easily.
func transformRequests(req *rest.Request) {
// The request headers are for both the v1 and v1beta1 versions of the table
// as Kubernetes 1.14 and older used the beta version.
req.SetHeader("Accept", strings.Join([]string{
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName),
"application/json",
}, ","))
}
// Validate reads Kubernetes manifests and validates the content. // Validate reads Kubernetes manifests and validates the content.
// //
// This function does not actually do schema validation of manifests. Adding // This function does not actually do schema validation of manifests. Adding
@ -170,6 +208,7 @@ func (c *Client) Validate(namespace string, reader io.Reader) error {
DefaultNamespace(). DefaultNamespace().
// Schema(c.validator()). // No schema validation // Schema(c.validator()). // No schema validation
Stream(reader, ""). Stream(reader, "").
Latest().
Flatten(). Flatten().
Do().Infos() Do().Infos()
return scrubValidationError(err) return scrubValidationError(err)
@ -199,7 +238,7 @@ func resourceInfoToObject(info *resource.Info, c *Client) runtime.Object {
return internalObj return internalObj
} }
func sortByKey(objs map[string](map[string]runtime.Object)) []string { func sortByKey(objs map[string][]runtime.Object) []string {
var keys []string var keys []string
// Create a simple slice, so we can sort it // Create a simple slice, so we can sort it
for key := range objs { for key := range objs {
@ -210,24 +249,79 @@ func sortByKey(objs map[string](map[string]runtime.Object)) []string {
return keys return keys
} }
// We have slices of tables that need to be sorted by name. In this case the
// self link is used so the sorting will include namespace and name.
func sortTableSlice(objs []runtime.Object) []runtime.Object {
// If there are 0 or 1 objects to sort there is nothing to sort so
// the list can be returned
if len(objs) < 2 {
return objs
}
ntbl := &metav1.Table{}
unstr, ok := objs[0].(*unstructured.Unstructured)
if !ok {
return objs
}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, ntbl); err != nil {
return objs
}
// Sort the list of objects
var newObjs []runtime.Object
namesCache := make(map[string]runtime.Object, len(objs))
var names []string
for _, obj := range objs {
unstr, ok := obj.(*unstructured.Unstructured)
if !ok {
return objs
}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstr.Object, ntbl); err != nil {
return objs
}
namesCache[ntbl.GetSelfLink()] = obj
names = append(names, ntbl.GetSelfLink())
}
sort.Strings(names)
for _, name := range names {
newObjs = append(newObjs, namesCache[name])
}
return newObjs
}
// Get gets Kubernetes resources as pretty-printed string. // Get gets Kubernetes resources as pretty-printed string.
// //
// Namespace will set the namespace. // Namespace will set the namespace.
func (c *Client) Get(namespace string, reader io.Reader) (string, error) { func (c *Client) Get(namespace string, reader io.Reader) (string, error) {
// Since we don't know what order the objects come in, let's group them by the types and then sort them, so // Since we don't know what order the objects come in, let's group them by the types and then sort them, so
// that when we print them, they come out looking good (headers apply to subgroups, etc.). // that when we print them, they come out looking good (headers apply to subgroups, etc.).
objs := make(map[string](map[string]runtime.Object)) objs := make(map[string][]runtime.Object)
gk := make(map[string]schema.GroupKind)
mux := &sync.Mutex{} mux := &sync.Mutex{}
infos, err := c.BuildUnstructured(namespace, reader) // The contents of the reader are used two times. The bytes are coppied out
// for use in future readers.
b, err := ioutil.ReadAll(reader)
if err != nil { if err != nil {
return "", err return "", err
} }
var objPods = make(map[string][]v1.Pod) // Get the table display for the objects associated with the release. This
// is done in table format so that it can be displayed in the status in
// the same way kubectl displays the resource information.
// Note, the response returns unstructured data instead of typed objects.
// These cannot be easily (i.e., via the go packages) transformed into
// Go types.
tinfos, err := c.BuildUnstructuredTable(namespace, bytes.NewBuffer(b))
if err != nil {
return "", err
}
missing := []string{} missing := []string{}
err = perform(infos, func(info *resource.Info) error { err = perform(tinfos, func(info *resource.Info) error {
mux.Lock() mux.Lock()
defer mux.Unlock() defer mux.Unlock()
c.Log("Doing get for %s: %q", info.Mapping.GroupVersionKind.Kind, info.Name) c.Log("Doing get for %s: %q", info.Mapping.GroupVersionKind.Kind, info.Name)
@ -241,18 +335,36 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) {
// versions per cluster, but this certainly won't hurt anything, so let's be safe. // versions per cluster, but this certainly won't hurt anything, so let's be safe.
gvk := info.ResourceMapping().GroupVersionKind gvk := info.ResourceMapping().GroupVersionKind
vk := gvk.Version + "/" + gvk.Kind vk := gvk.Version + "/" + gvk.Kind
gk[vk] = gvk.GroupKind()
// Initialize map. The main map groups resources based on version/kind // Initialize map. The main map groups resources based on version/kind
// The second level is a simple 'Name' to 'Object', that will help sort // The second level is a simple 'Name' to 'Object', that will help sort
// the individual resource later // the individual resource later
if objs[vk] == nil { if objs[vk] == nil {
objs[vk] = make(map[string]runtime.Object) objs[vk] = []runtime.Object{}
} }
// Map between the resource name to the underlying info object // Map between the resource name to the underlying info object
objs[vk][info.Name] = resourceInfoToObject(info, c) objs[vk] = append(objs[vk], resourceInfoToObject(info, c))
return nil
})
if err != nil {
return "", err
}
// This section finds related resources (e.g., pods). Before looking up pods
// the resources the pods are made from need to be looked up in a manner
// that can be turned into Go types and worked with.
infos, err := c.BuildUnstructured(namespace, bytes.NewBuffer(b))
if err != nil {
return "", err
}
err = perform(infos, func(info *resource.Info) error {
mux.Lock()
defer mux.Unlock()
//Get the relation pods //Get the relation pods
objPods, err = c.getSelectRelationPod(info, objPods) objs, err = c.getSelectRelationPod(info, objs)
if err != nil { if err != nil {
c.Log("Warning: get the relation pod is failed, err:%s", err.Error()) c.Log("Warning: get the relation pod is failed, err:%s", err.Error())
} }
@ -263,25 +375,11 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) {
return "", err return "", err
} }
//here, we will add the objPods to the objs
for key, podItems := range objPods {
for i := range podItems {
pod := &core.Pod{}
scheme.Scheme.Convert(&podItems[i], pod, nil)
if objs[key+"(related)"] == nil {
objs[key+"(related)"] = make(map[string]runtime.Object)
}
objs[key+"(related)"][pod.ObjectMeta.Name] = runtime.Object(pod)
}
}
// Ok, now we have all the objects grouped by types (say, by v1/Pod, v1/Service, etc.), so // Ok, now we have all the objects grouped by types (say, by v1/Pod, v1/Service, etc.), so
// spin through them and print them. Printer is cool since it prints the header only when // spin through them and print them. Printer is cool since it prints the header only when
// an object type changes, so we can just rely on that. Problem is it doesn't seem to keep // an object type changes, so we can just rely on that. Problem is it doesn't seem to keep
// track of tab widths. // track of tab widths.
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
printFlags := get.NewHumanPrintFlags()
// Sort alphabetically by version/kind keys // Sort alphabetically by version/kind keys
vkKeys := sortByKey(objs) vkKeys := sortByKey(objs)
@ -290,20 +388,29 @@ func (c *Client) Get(namespace string, reader io.Reader) (string, error) {
if _, err = fmt.Fprintf(buf, "==> %s\n", t); err != nil { if _, err = fmt.Fprintf(buf, "==> %s\n", t); err != nil {
return "", err return "", err
} }
typePrinter, _ := printFlags.ToPrinter("") vk := objs[t]
var sortedResources []string // The request made for tables returns each Kubernetes object as its
for resource := range objs[t] { // own table. The normal sorting provided by kubectl and cli-runtime
sortedResources = append(sortedResources, resource) // does not handle this case. Here we sort within each of our own
// grouping.
vk = sortTableSlice(vk)
// The printer flag setup follows a simalar setup to kubectl
printFlags := get.NewHumanPrintFlags()
if lgk, ok := gk[t]; ok {
printFlags.SetKind(lgk)
} }
sort.Strings(sortedResources) printer, _ := printFlags.ToPrinter("")
printer, err = printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(printer, nil)
if err != nil {
return "", err
}
printer = &get.TablePrinter{Delegate: printer}
// Now that each individual resource within the specific version/kind for _, resource := range vk {
// is sorted, we print each resource using the k8s printer if err := printer.PrintObj(resource, buf); err != nil {
vk := objs[t] c.Log("failed to print object type %s: %v", t, err)
for _, resourceName := range sortedResources {
if err := typePrinter.PrintObj(vk[resourceName], buf); err != nil {
c.Log("failed to print object type %s, object: %q :\n %v", t, resourceName, err)
return "", err return "", err
} }
} }
@ -985,11 +1092,11 @@ func isPodComplete(event watch.Event) (bool, error) {
return false, nil return false, nil
} }
//get a kubernetes resources' relation pods // get a kubernetes resources' relation pods
// kubernetes resource used select labels to relate pods // kubernetes resource used select labels to relate pods
func (c *Client) getSelectRelationPod(info *resource.Info, objPods map[string][]v1.Pod) (map[string][]v1.Pod, error) { func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]runtime.Object) (map[string][]runtime.Object, error) {
if info == nil { if info == nil {
return objPods, nil return objs, nil
} }
c.Log("get relation pod of object: %s/%s/%s", info.Namespace, info.Mapping.GroupVersionKind.Kind, info.Name) c.Log("get relation pod of object: %s/%s/%s", info.Namespace, info.Mapping.GroupVersionKind.Kind, info.Name)
@ -997,34 +1104,31 @@ func (c *Client) getSelectRelationPod(info *resource.Info, objPods map[string][]
versioned := asVersionedOrUnstructured(info) versioned := asVersionedOrUnstructured(info)
selector, ok := getSelectorFromObject(versioned) selector, ok := getSelectorFromObject(versioned)
if !ok { if !ok {
return objPods, nil return objs, nil
} }
client, _ := c.KubernetesClientSet() // The related pods are looked up in Table format so that their display can
// be printed in a manner similar to kubectl when it get pods. The response
pods, err := client.CoreV1().Pods(info.Namespace).List(metav1.ListOptions{ // can be used with a table printer.
LabelSelector: labels.Set(selector).AsSelector().String(), infos, err := c.NewBuilder().
}) Unstructured().
ContinueOnError().
NamespaceParam(info.Namespace).
DefaultNamespace().
ResourceTypes("pods").
LabelSelector(labels.Set(selector).AsSelector().String()).
TransformRequests(transformRequests).
Do().Infos()
if err != nil { if err != nil {
return objPods, err return objs, err
} }
for _, pod := range pods.Items { for _, info := range infos {
vk := "v1/Pod" vk := "v1/Pod(related)"
if !isFoundPod(objPods[vk], pod) { objs[vk] = append(objs[vk], info.Object)
objPods[vk] = append(objPods[vk], pod)
}
} }
return objPods, nil
}
func isFoundPod(podItem []v1.Pod, pod v1.Pod) bool { return objs, nil
for _, value := range podItem {
if (value.Namespace == pod.Namespace) && (value.Name == pod.Name) {
return true
}
}
return false
} }
func asVersionedOrUnstructured(info *resource.Info) runtime.Object { func asVersionedOrUnstructured(info *resource.Info) runtime.Object {

Loading…
Cancel
Save