Unverified Commit fe4ffcfa authored by Matt Farina's avatar Matt Farina
Browse files

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: default avatarMatt Farina <matt@mattfarina.com>
(cherry picked from commit e8396c92)
No related merge requests found
Showing with 162 additions and 58 deletions
+162 -58
......@@ -23,6 +23,7 @@ import (
goerrors "errors"
"fmt"
"io"
"io/ioutil"
"log"
"sort"
"strings"
......@@ -42,6 +43,8 @@ import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
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/labels"
"k8s.io/apimachinery/pkg/runtime"
......@@ -51,13 +54,14 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
cachetools "k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/validation"
"k8s.io/kubernetes/pkg/apis/core"
"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)
}
// 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.
//
// 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().
// Schema(c.validator()). // No schema validation
Stream(reader, "").
Latest().
Flatten().
Do().Infos()
return scrubValidationError(err)
......@@ -199,7 +238,7 @@ func resourceInfoToObject(info *resource.Info, c *Client) runtime.Object {
return internalObj
}
func sortByKey(objs map[string](map[string]runtime.Object)) []string {
func sortByKey(objs map[string][]runtime.Object) []string {
var keys []string
// Create a simple slice, so we can sort it
for key := range objs {
......@@ -210,24 +249,79 @@ func sortByKey(objs map[string](map[string]runtime.Object)) []string {
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.
//
// Namespace will set the namespace.
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
// 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{}
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 {
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{}
err = perform(infos, func(info *resource.Info) error {
err = perform(tinfos, func(info *resource.Info) error {
mux.Lock()
defer mux.Unlock()
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.
gvk := info.ResourceMapping().GroupVersionKind
vk := gvk.Version + "/" + gvk.Kind
gk[vk] = gvk.GroupKind()
// 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 individual resource later
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
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
objPods, err = c.getSelectRelationPod(info, objPods)
objs, err = c.getSelectRelationPod(info, objs)
if err != nil {
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
}
//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
// 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
// track of tab widths.
buf := new(bytes.Buffer)
printFlags := get.NewHumanPrintFlags()
// Sort alphabetically by version/kind keys
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 {
return "", err
}
typePrinter, _ := printFlags.ToPrinter("")
vk := objs[t]
var sortedResources []string
for resource := range objs[t] {
sortedResources = append(sortedResources, resource)
// The request made for tables returns each Kubernetes object as its
// own table. The normal sorting provided by kubectl and cli-runtime
// 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
// is sorted, we print each resource using the k8s printer
vk := objs[t]
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)
for _, resource := range vk {
if err := printer.PrintObj(resource, buf); err != nil {
c.Log("failed to print object type %s: %v", t, err)
return "", err
}
}
......@@ -985,11 +1092,11 @@ func isPodComplete(event watch.Event) (bool, error) {
return false, nil
}
//get a kubernetes resources' relation pods
// get a kubernetes resources' relation 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 {
return objPods, nil
return objs, nil
}
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)
selector, ok := getSelectorFromObject(versioned)
if !ok {
return objPods, nil
return objs, nil
}
client, _ := c.KubernetesClientSet()
pods, err := client.CoreV1().Pods(info.Namespace).List(metav1.ListOptions{
LabelSelector: labels.Set(selector).AsSelector().String(),
})
// 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
// can be used with a table printer.
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 {
return objPods, err
return objs, err
}
for _, pod := range pods.Items {
vk := "v1/Pod"
if !isFoundPod(objPods[vk], pod) {
objPods[vk] = append(objPods[vk], pod)
}
for _, info := range infos {
vk := "v1/Pod(related)"
objs[vk] = append(objs[vk], info.Object)
}
return objPods, nil
}
func isFoundPod(podItem []v1.Pod, pod v1.Pod) bool {
for _, value := range podItem {
if (value.Namespace == pod.Namespace) && (value.Name == pod.Name) {
return true
}
}
return false
return objs, nil
}
func asVersionedOrUnstructured(info *resource.Info) runtime.Object {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment