Skip to content

Commit

Permalink
Merge pull request #11720 from mattfarina/fix-11712
Browse files Browse the repository at this point in the history
Fix improper use of Table request/response to k8s API
  • Loading branch information
mattfarina committed Jan 13, 2023
2 parents b2d5235 + 36e18fa commit 76157c6
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 41 deletions.
7 changes: 7 additions & 0 deletions cmd/helm/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {

// When the output format is a table the resources should be fetched
// and displayed as a table. When YAML or JSON the resources will be
// returned. This mirrors the handling in kubectl.
if outfmt == output.Table {
client.ShowResourcesTable = true
}
rel, err := client.Run(args[0])
if err != nil {
return err
Expand Down
26 changes: 21 additions & 5 deletions pkg/action/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package action

import (
"bytes"
"errors"

"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release"
Expand All @@ -36,9 +37,13 @@ type Status struct {
// TODO Helm 4: Remove this flag and output the description by default.
ShowDescription bool

// If true, display resources of release to output format
// ShowResources sets if the resources should be retrieved with the status.
// TODO Helm 4: Remove this flag and output the resources by default.
ShowResources bool

// ShowResourcesTable is used with ShowResources. When true this will cause
// the resulting objects to be retrieved as a kind=table.
ShowResourcesTable bool
}

// NewStatus creates a new Status object with the given configuration.
Expand All @@ -63,10 +68,21 @@ func (s *Status) Run(name string) (*release.Release, error) {
return nil, err
}

resources, _ := s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false)

if kubeClient, ok := s.cfg.KubeClient.(kube.InterfaceResources); ok {
resp, err := kubeClient.Get(resources, bytes.NewBufferString(rel.Manifest))
var resources kube.ResourceList
if s.ShowResourcesTable {
resources, err = kubeClient.BuildTable(bytes.NewBufferString(rel.Manifest), false)
if err != nil {
return nil, err
}
} else {
resources, err = s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false)
if err != nil {
return nil, err
}
}

resp, err := kubeClient.Get(resources, true)
if err != nil {
return nil, err
}
Expand All @@ -75,5 +91,5 @@ func (s *Status) Run(name string) (*release.Release, error) {

return rel, nil
}
return nil, err
return nil, errors.New("unable to get kubeClient with interface InterfaceResources")
}
113 changes: 80 additions & 33 deletions pkg/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ func transformRequests(req *rest.Request) {
req.Param("includeObject", "Object")
}

func (c *Client) Get(resources ResourceList, reader io.Reader) (map[string][]runtime.Object, error) {
// Get retrieves the resource objects supplied. If related is set to true the
// related pods are fetched as well. If the passed in resources are a table kind
// the related resources will also be fetched as kind=table.
func (c *Client) Get(resources ResourceList, related bool) (map[string][]runtime.Object, error) {
buf := new(bytes.Buffer)
objs := make(map[string][]runtime.Object)

Expand All @@ -167,9 +170,20 @@ func (c *Client) Get(resources ResourceList, reader io.Reader) (map[string][]run
} else {
objs[vk] = append(objs[vk], obj)

objs, err = c.getSelectRelationPod(info, objs, &podSelectors)
if err != nil {
c.Log("Warning: get the relation pod is failed, err:%s", err.Error())
// Only fetch related pods if they are requested
if related {
// Discover if the existing object is a table. If it is, request
// the pods as Tables. Otherwise request them normally.
objGVK := obj.GetObjectKind().GroupVersionKind()
var isTable bool
if objGVK.Kind == "Table" {
isTable = true
}

objs, err = c.getSelectRelationPod(info, objs, isTable, &podSelectors)
if err != nil {
c.Log("Warning: get the relation pod is failed, err:%s", err.Error())
}
}
}

Expand All @@ -182,7 +196,7 @@ func (c *Client) Get(resources ResourceList, reader io.Reader) (map[string][]run
return objs, nil
}

func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]runtime.Object, podSelectors *[]map[string]string) (map[string][]runtime.Object, error) {
func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]runtime.Object, table bool, podSelectors *[]map[string]string) (map[string][]runtime.Object, error) {
if info == nil {
return objs, nil
}
Expand All @@ -201,17 +215,33 @@ func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]run

*podSelectors = append(*podSelectors, selector)

infos, err := c.Factory.NewBuilder().
Unstructured().
ContinueOnError().
NamespaceParam(info.Namespace).
DefaultNamespace().
ResourceTypes("pods").
LabelSelector(labels.Set(selector).AsSelector().String()).
TransformRequests(transformRequests).
Do().Infos()
if err != nil {
return objs, err
var infos []*resource.Info
var err error
if table {
infos, err = c.Factory.NewBuilder().
Unstructured().
ContinueOnError().
NamespaceParam(info.Namespace).
DefaultNamespace().
ResourceTypes("pods").
LabelSelector(labels.Set(selector).AsSelector().String()).
TransformRequests(transformRequests).
Do().Infos()
if err != nil {
return objs, err
}
} else {
infos, err = c.Factory.NewBuilder().
Unstructured().
ContinueOnError().
NamespaceParam(info.Namespace).
DefaultNamespace().
ResourceTypes("pods").
LabelSelector(labels.Set(selector).AsSelector().String()).
Do().Infos()
if err != nil {
return objs, err
}
}
vk := "v1/Pod(related)"

Expand Down Expand Up @@ -317,21 +347,38 @@ func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) {
if err != nil {
return nil, err
}
var result ResourceList
result, err := c.newBuilder().
Unstructured().
Schema(schema).
Stream(reader, "").
Do().Infos()
return result, scrubValidationError(err)
}

// BuildTable validates for Kubernetes objects and returns unstructured infos.
// The returned kind is a Table.
func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, error) {
validationDirective := metav1.FieldValidationIgnore
if validate {
result, err = c.newBuilder().
Unstructured().
Schema(schema).
Stream(reader, "").
Do().Infos()
} else {
result, err = c.newBuilder().
Unstructured().
Schema(schema).
Stream(reader, "").
TransformRequests(transformRequests).
Do().Infos()
validationDirective = metav1.FieldValidationStrict
}

dynamicClient, err := c.Factory.DynamicClient()
if err != nil {
return nil, err
}

verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
schema, err := c.Factory.Validator(validationDirective, verifier)
if err != nil {
return nil, err
}
result, err := c.newBuilder().
Unstructured().
Schema(schema).
Stream(reader, "").
TransformRequests(transformRequests).
Do().Infos()
return result, scrubValidationError(err)
}

Expand Down Expand Up @@ -472,10 +519,10 @@ func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
// For most kinds, it checks to see if the resource is marked as Added or Modified
// by the Kubernetes event stream. For some kinds, it does more:
//
// - Jobs: A job is marked "Ready" when it has successfully completed. This is
// ascertained by watching the Status fields in a job's output.
// - Pods: A pod is marked "Ready" when it has successfully completed. This is
// ascertained by watching the status.phase field in a pod's output.
// - Jobs: A job is marked "Ready" when it has successfully completed. This is
// ascertained by watching the Status fields in a job's output.
// - Pods: A pod is marked "Ready" when it has successfully completed. This is
// ascertained by watching the status.phase field in a pod's output.
//
// Handling for other kinds will be added as necessary.
func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration) error {
Expand Down
39 changes: 39 additions & 0 deletions pkg/kube/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,45 @@ func TestBuild(t *testing.T) {
}
}

func TestBuildTable(t *testing.T) {
tests := []struct {
name string
namespace string
reader io.Reader
count int
err bool
}{
{
name: "Valid input",
namespace: "test",
reader: strings.NewReader(guestbookManifest),
count: 6,
}, {
name: "Valid input, deploying resources into different namespaces",
namespace: "test",
reader: strings.NewReader(namespacedGuestbookManifest),
count: 1,
},
}

c := newTestClient(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test for an invalid manifest
infos, err := c.BuildTable(tt.reader, false)
if err != nil && !tt.err {
t.Errorf("Got error message when no error should have occurred: %v", err)
} else if err != nil && strings.Contains(err.Error(), "--validate=false") {
t.Error("error message was not scrubbed")
}

if len(infos) != tt.count {
t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
}
})
}
}

func TestPerform(t *testing.T) {
tests := []struct {
name string
Expand Down
19 changes: 19 additions & 0 deletions pkg/kube/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"

"helm.sh/helm/v3/pkg/kube"
Expand All @@ -33,11 +34,13 @@ import (
type FailingKubeClient struct {
PrintingKubeClient
CreateError error
GetError error
WaitError error
DeleteError error
WatchUntilReadyError error
UpdateError error
BuildError error
BuildTableError error
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
WaitDuration time.Duration
Expand All @@ -51,6 +54,14 @@ func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, e
return f.PrintingKubeClient.Create(resources)
}

// Get returns the configured error if set or prints
func (f *FailingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) {
if f.GetError != nil {
return nil, f.GetError
}
return f.PrintingKubeClient.Get(resources, related)
}

// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints.
func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error {
time.Sleep(f.WaitDuration)
Expand Down Expand Up @@ -108,6 +119,14 @@ func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error
return f.PrintingKubeClient.Build(r, false)
}

// BuildTable returns the configured error if set or prints
func (f *FailingKubeClient) BuildTable(r io.Reader, _ bool) (kube.ResourceList, error) {
if f.BuildTableError != nil {
return []*resource.Info{}, f.BuildTableError
}
return f.PrintingKubeClient.BuildTable(r, false)
}

// WaitAndGetCompletedPodPhase returns the configured error if set or prints
func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duration) (v1.PodPhase, error) {
if f.WaitAndGetCompletedPodPhaseError != nil {
Expand Down
7 changes: 6 additions & 1 deletion pkg/kube/fake/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result,
return &kube.Result{Created: resources}, nil
}

func (p *PrintingKubeClient) Get(resources kube.ResourceList, reader io.Reader) (map[string][]runtime.Object, error) {
func (p *PrintingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, err
Expand Down Expand Up @@ -105,6 +105,11 @@ func (p *PrintingKubeClient) Build(_ io.Reader, _ bool) (kube.ResourceList, erro
return []*resource.Info{}, nil
}

// BuildTable implements KubeClient BuildTable.
func (p *PrintingKubeClient) BuildTable(_ io.Reader, _ bool) (kube.ResourceList, error) {
return []*resource.Info{}, nil
}

// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase.
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (v1.PodPhase, error) {
return v1.PodSucceeded, nil
Expand Down
18 changes: 16 additions & 2 deletions pkg/kube/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,22 @@ type InterfaceExt interface {
//
// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface.
type InterfaceResources interface {
// Get details of deployed resources in ResourceList to be printed.
Get(resources ResourceList, reader io.Reader) (map[string][]runtime.Object, error)
// Get details of deployed resources.
// The first argument is a list of resources to get. The second argument
// specifies if related pods should be fetched. For example, the pods being
// managed by a deployment.
Get(resources ResourceList, related bool) (map[string][]runtime.Object, error)

// BuildTable creates a resource list from a Reader. This differs from
// Interface.Build() in that a table kind is returned. A table is useful
// if you want to use a printer to display the information.
//
// Reader must contain a YAML stream (one or more YAML documents separated
// by "\n---\n")
//
// Validates against OpenAPI schema if validate is true.
// TODO Helm 4: Integrate into Build with an argument
BuildTable(reader io.Reader, validate bool) (ResourceList, error)
}

var _ Interface = (*Client)(nil)
Expand Down

0 comments on commit 76157c6

Please sign in to comment.