test(get deployed): Add pkg/action level tests

Add pkg/action level tests related to `helm get deployed` command.

Related to #12722

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>
pull/12875/head
Bhargav Ravuri 11 months ago
parent 9810277c39
commit b314998314
No known key found for this signature in database
GPG Key ID: F2D6B63B5A17E057

@ -115,7 +115,7 @@ func (g *GetDeployed) processResourceRecord(manifest *yaml.RNode, mapper meta.RE
// Build resource list required for Helm kube client
filter, err := g.cfg.KubeClient.Build(bytes.NewBufferString(manifestStr), false)
if err != nil {
return nil, fmt.Errorf("failed to build resource list: %v", err)
return nil, fmt.Errorf("failed to build resource list: %w", err)
}
// Fetch the resources from the Kubernetes cluster based on the resource list built above
@ -124,12 +124,12 @@ func (g *GetDeployed) processResourceRecord(manifest *yaml.RNode, mapper meta.RE
// the current record.
list, err := g.cfg.KubeClient.Get(filter, false)
if err != nil {
return nil, fmt.Errorf("failed to get the resource from cluster: %v", err)
return nil, fmt.Errorf("failed to get the resource from cluster: %w", err)
}
var (
resourceObj runtime.Object
metaObj metav1.Object
objMeta metav1.Object
)
// Extract the resource object and its metadata from the list of resources. Note: Though Get() returns a list of
@ -139,12 +139,12 @@ func (g *GetDeployed) processResourceRecord(manifest *yaml.RNode, mapper meta.RE
var ok bool
for _, objects := range list {
for _, obj := range objects {
metaObj, ok = obj.(metav1.Object)
objMeta, ok = obj.(metav1.Object)
if !ok {
return fmt.Errorf("object does not implement metav1.Object interface")
}
if metaObj.GetName() != manifest.GetName() {
if objMeta.GetName() != manifest.GetName() {
continue
}
@ -170,9 +170,9 @@ func (g *GetDeployed) processResourceRecord(manifest *yaml.RNode, mapper meta.RE
return &ResourceElement{
Resource: resourceMapping.Resource.Resource,
Name: manifest.GetName(),
Namespace: metaObj.GetNamespace(),
Namespace: objMeta.GetNamespace(),
APIVersion: manifest.GetApiVersion(),
CreationTimestamp: metaObj.GetCreationTimestamp(),
CreationTimestamp: objMeta.GetCreationTimestamp(),
}, nil
}

@ -0,0 +1,557 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"fmt"
"io"
"testing"
texttemplate "text/template"
"time"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metatable "k8s.io/apimachinery/pkg/api/meta/table"
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
const (
manifestTemplate = `---
# Source: templates/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Namespace }}
creationTimestamp: {{ .CreationTimestamp }}
---
# Source: templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nami
namespace: {{ .Namespace }}
creationTimestamp: {{ .CreationTimestamp }}
data:
attack: "Gomu Gomu no King Kong Gun!"
---
# Source: templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: zoro
namespace: {{ .Namespace }}
creationTimestamp: {{ .CreationTimestamp }}
spec:
type: ClusterIP
selector:
app: one-piece
ports:
- protocol: TCP
port: 80
targetPort: 80
---
# Source: templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: luffy
namespace: {{ .Namespace }}
creationTimestamp: {{ .CreationTimestamp }}
spec:
replicas: 2
selector:
matchLabels:
app: one-piece
template:
metadata:
labels:
app: one-piece
spec:
containers:
- name: luffy-arsenal
image: "nginx:1.21.6"
ports:
- containerPort: 80
env:
- name: ATTACK
valueFrom:
configMapKeyRef:
name: luffy
key: attack
`
tableOutputTemplate = `NAMESPACE NAME API_VERSION AGE
namespaces/{{ .Namespace }} v1 {{ .Age }}
{{ .Namespace }} configmaps/nami v1 {{ .Age }}
{{ .Namespace }} services/zoro v1 {{ .Age }}
{{ .Namespace }} deployments/luffy apps/v1 {{ .Age }}
`
jsonOutputTemplate = `[{` +
`"name":"{{ .Namespace }}",` +
`"namespace":"",` +
`"apiVersion":"v1",` +
`"resource":"namespaces",` +
`"creationTimestamp":"{{ .CreationTimestamp }}"},{"name":"nami",` +
`"namespace":"{{ .Namespace }}",` +
`"apiVersion":"v1",` +
`"resource":"configmaps",` +
`"creationTimestamp":"{{ .CreationTimestamp }}"},{"name":"zoro",` +
`"namespace":"{{ .Namespace }}",` +
`"apiVersion":"v1",` +
`"resource":"services",` +
`"creationTimestamp":"{{ .CreationTimestamp }}"},{"name":"luffy",` +
`"namespace":"{{ .Namespace }}",` +
`"apiVersion":"apps/v1",` +
`"resource":"deployments",` +
`"creationTimestamp":"{{ .CreationTimestamp }}"` +
`}]
`
yamlOutputTemplate = `- apiVersion: v1
creationTimestamp: "{{ .CreationTimestamp }}"
name: {{ .Namespace }}
namespace: ""
resource: namespaces
- apiVersion: v1
creationTimestamp: "{{ .CreationTimestamp }}"
name: nami
namespace: {{ .Namespace }}
resource: configmaps
- apiVersion: v1
creationTimestamp: "{{ .CreationTimestamp }}"
name: zoro
namespace: {{ .Namespace }}
resource: services
- apiVersion: apps/v1
creationTimestamp: "{{ .CreationTimestamp }}"
name: luffy
namespace: {{ .Namespace }}
resource: deployments
`
)
type getDeployedOutputData struct {
Namespace string
CreationTimestamp string
Age string
}
func TestGetDeployed(t *testing.T) {
var (
is = assert.New(t)
chartName = `one-piece`
namespace = `thousand-sunny`
exactTimestamp = time.Date(2024, time.October, 28, 0, 4, 30, 0, time.FixedZone("IST", 19800)).UTC()
relativeTimestamp = time.Now().Add(-2 * time.Minute).UTC()
)
type (
testFunc struct {
writeTable bool
writeJSON bool
writeYAML bool
}
testCase struct {
name string
creationTimestamp time.Time
testFunc testFunc
}
)
tests := []testCase{
{
name: "With Exact Creation Time",
creationTimestamp: exactTimestamp,
testFunc: testFunc{
writeTable: false,
writeJSON: true,
writeYAML: true,
},
},
{
name: "With Relative Creation Time",
creationTimestamp: relativeTimestamp,
testFunc: testFunc{
writeTable: true,
writeJSON: false,
writeYAML: false,
},
},
}
scheme := runtime.NewScheme()
is.NoError(corev1.AddToScheme(scheme))
is.NoError(appsv1.AddToScheme(scheme))
restMapper := testrestmapper.TestOnlyStaticRESTMapper(scheme)
configFlags := genericclioptions.NewTestConfigFlags().
WithRESTMapper(restMapper)
formatResourceList := func(creationTimestamp time.Time) []ResourceElement {
creationTimestampStr := creationTimestamp.Format(time.RFC3339)
manifest, err := parseGetDeployedTestTemplate(namespace, creationTimestampStr, "", manifestTemplate)
is.NoError(err)
config := actionConfigFixture(t)
config.KubeClient = &kubefake.PrintingKubeClient{
Out: io.Discard,
Options: &kubefake.Options{
GetReturnResourceMap: true,
BuildReturnResourceList: true,
},
}
config.RESTClientGetter = configFlags
client := NewGetDeployed(config)
releases := []*release.Release{
{
Name: chartName,
Info: &release.Info{
LastDeployed: helmtime.Unix(creationTimestamp.Unix(), 0),
Status: release.StatusDeployed,
},
Manifest: manifest.String(),
Namespace: namespace,
},
}
for _, rel := range releases {
err = client.cfg.Releases.Create(rel)
is.NoError(err)
}
resourceList, err := client.Run(chartName)
is.NoError(err)
return resourceList
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
is := assert.New(t)
resourceList := formatResourceList(tc.creationTimestamp)
is.NotEmpty(resourceList)
writer := NewResourceListWriter(resourceList, false)
creationTimestampStr := tc.creationTimestamp.Format(time.RFC3339)
creationTimestampAgeStr := metatable.ConvertToHumanReadableDateType(metav1.NewTime(tc.creationTimestamp))
var (
out bytes.Buffer
expectedOut fmt.Stringer
err error
)
if tc.testFunc.writeTable {
t.Run("Write Table", func(t *testing.T) {
is := assert.New(t)
out.Truncate(0)
expectedOut, err = parseGetDeployedTestTemplate(
namespace,
"", // Creation timestamp is not used in table output, but creation timestamp's age
creationTimestampAgeStr,
tableOutputTemplate,
)
is.NoError(err)
err = writer.WriteTable(&out)
is.NoError(err)
is.Equal(expectedOut.String(), out.String())
})
}
if tc.testFunc.writeJSON {
t.Run("Write JSON", func(t *testing.T) {
is := assert.New(t)
out.Truncate(0)
expectedOut, err = parseGetDeployedTestTemplate(
namespace,
creationTimestampStr,
"", // Creation timestamp's age is not used in JSON output, but the creation timestamp itself
jsonOutputTemplate,
)
is.NoError(err)
err = writer.WriteJSON(&out)
is.NoError(err)
is.Equal(expectedOut.String(), out.String())
})
}
if tc.testFunc.writeYAML {
t.Run("Write YAML", func(t *testing.T) {
is := assert.New(t)
out.Truncate(0)
expectedOut, err = parseGetDeployedTestTemplate(
namespace,
creationTimestampStr,
"", // Creation timestamp's age is not used in YAML output, but the creation timestamp itself
yamlOutputTemplate,
)
is.NoError(err)
err = writer.WriteYAML(&out)
is.NoError(err)
is.Equal(expectedOut.String(), out.String())
})
}
})
}
}
func parseGetDeployedTestTemplate(namespace, creationTimestamp, age, template string) (fmt.Stringer, error) {
outputParser, err := texttemplate.New("template").Parse(template)
if err != nil {
return nil, err
}
var out bytes.Buffer
err = outputParser.Execute(&out, getDeployedOutputData{
Namespace: namespace,
CreationTimestamp: creationTimestamp,
Age: age,
})
if err != nil {
return nil, err
}
return &out, nil
}
func TestGetDeployed_ErrorKubeClientNotReachable(t *testing.T) {
is := assert.New(t)
chartName := `one-piece`
config := actionConfigFixture(t)
config.KubeClient = &kubefake.PrintingKubeClient{
Out: io.Discard,
Options: &kubefake.Options{
IsReachableReturnsError: true,
},
}
client := NewGetDeployed(config)
resourceList, err := client.Run(chartName)
is.Nil(resourceList)
is.Error(err)
is.ErrorIs(err, kubefake.ErrPrintingKubeClientNotReachable)
}
func TestGetDeployed_ErrorReleaseNotFound(t *testing.T) {
is := assert.New(t)
chartName := `one-piece`
config := actionConfigFixture(t)
config.KubeClient = &kubefake.PrintingKubeClient{
Out: io.Discard,
Options: &kubefake.Options{
IsReachableReturnsError: false,
},
}
client := NewGetDeployed(config)
resourceList, err := client.Run(chartName)
is.Nil(resourceList)
is.Error(err)
is.Contains(err.Error(), "release: not found")
}
func TestGetDeployed_RESTMapperNotFound(t *testing.T) {
var (
is = assert.New(t)
chartName = `one-piece`
)
configFlags := genericclioptions.NewTestConfigFlags().
WithRESTMapper(nil)
config := actionConfigFixture(t)
config.KubeClient = &kubefake.PrintingKubeClient{
Out: io.Discard,
Options: &kubefake.Options{
GetReturnResourceMap: true,
BuildReturnResourceList: true,
},
}
config.RESTClientGetter = configFlags
client := NewGetDeployed(config)
err := client.cfg.Releases.Create(&release.Release{
Name: chartName,
Info: &release.Info{},
})
is.NoError(err)
resourceList, err := client.Run(chartName)
is.Nil(resourceList)
is.Error(err)
is.Contains(err.Error(), "failed to extract the REST mapper: no restmapper")
}
func TestGetDeployed_ResourceListBuildFailure(t *testing.T) {
var (
is = assert.New(t)
chartName = `one-piece`
namespace = `thousand-sunny`
exactTimestamp = time.Date(2024, time.October, 28, 0, 4, 30, 0, time.FixedZone("IST", 19800)).UTC()
)
scheme := runtime.NewScheme()
is.NoError(corev1.AddToScheme(scheme))
is.NoError(appsv1.AddToScheme(scheme))
restMapper := testrestmapper.TestOnlyStaticRESTMapper(scheme)
configFlags := genericclioptions.NewTestConfigFlags().
WithRESTMapper(restMapper)
creationTimestampStr := exactTimestamp.Format(time.RFC3339)
manifest, err := parseGetDeployedTestTemplate(namespace, creationTimestampStr, "", manifestTemplate)
is.NoError(err)
config := actionConfigFixture(t)
config.KubeClient = &kubefake.PrintingKubeClient{
Out: io.Discard,
Options: &kubefake.Options{
BuildReturnError: true,
},
}
config.RESTClientGetter = configFlags
client := NewGetDeployed(config)
err = client.cfg.Releases.Create(&release.Release{
Name: chartName,
Info: &release.Info{
LastDeployed: helmtime.Unix(exactTimestamp.Unix(), 0),
Status: release.StatusDeployed,
},
Manifest: manifest.String(),
Namespace: namespace,
})
is.NoError(err)
resourceList, err := client.Run(chartName)
is.Nil(resourceList)
is.Error(err)
is.ErrorIs(err, kubefake.ErrPrintingKubeClientBuildFailure)
}
func TestGetDeployed_GetResourceFailure(t *testing.T) {
var (
is = assert.New(t)
chartName = `one-piece`
namespace = `thousand-sunny`
exactTimestamp = time.Date(2024, time.October, 28, 0, 4, 30, 0, time.FixedZone("IST", 19800)).UTC()
)
scheme := runtime.NewScheme()
is.NoError(corev1.AddToScheme(scheme))
is.NoError(appsv1.AddToScheme(scheme))
restMapper := testrestmapper.TestOnlyStaticRESTMapper(scheme)
configFlags := genericclioptions.NewTestConfigFlags().
WithRESTMapper(restMapper)
creationTimestampStr := exactTimestamp.Format(time.RFC3339)
manifest, err := parseGetDeployedTestTemplate(namespace, creationTimestampStr, "", manifestTemplate)
is.NoError(err)
config := actionConfigFixture(t)
config.KubeClient = &kubefake.PrintingKubeClient{
Out: io.Discard,
Options: &kubefake.Options{
GetReturnError: true,
},
}
config.RESTClientGetter = configFlags
client := NewGetDeployed(config)
err = client.cfg.Releases.Create(&release.Release{
Name: chartName,
Info: &release.Info{
LastDeployed: helmtime.Unix(exactTimestamp.Unix(), 0),
Status: release.StatusDeployed,
},
Manifest: manifest.String(),
Namespace: namespace,
})
is.NoError(err)
resourceList, err := client.Run(chartName)
is.Nil(resourceList)
is.Error(err)
is.ErrorIs(err, kubefake.ErrPrintingKubeClientGetFailure)
}
func TestGetDeployed_MissingGVK(t *testing.T) {
var (
is = assert.New(t)
chartName = `one-piece`
namespace = `thousand-sunny`
exactTimestamp = time.Date(2024, time.October, 28, 0, 4, 30, 0, time.FixedZone("IST", 19800)).UTC()
)
scheme := runtime.NewScheme()
is.NoError(corev1.AddToScheme(scheme))
restMapper := testrestmapper.TestOnlyStaticRESTMapper(scheme)
configFlags := genericclioptions.NewTestConfigFlags().
WithRESTMapper(restMapper)
creationTimestampStr := exactTimestamp.Format(time.RFC3339)
manifest, err := parseGetDeployedTestTemplate(namespace, creationTimestampStr, "", manifestTemplate)
is.NoError(err)
config := actionConfigFixture(t)
config.KubeClient = &kubefake.PrintingKubeClient{
Out: io.Discard,
Options: &kubefake.Options{
GetReturnResourceMap: true,
BuildReturnResourceList: true,
},
}
config.RESTClientGetter = configFlags
client := NewGetDeployed(config)
err = client.cfg.Releases.Create(&release.Release{
Name: chartName,
Info: &release.Info{
LastDeployed: helmtime.Unix(exactTimestamp.Unix(), 0),
Status: release.StatusDeployed,
},
Manifest: manifest.String(),
Namespace: namespace,
})
is.NoError(err)
resourceList, err := client.Run(chartName)
is.Nil(resourceList)
is.Error(err)
is.Contains(err.Error(), "no matches for kind \"Deployment\" in version \"apps/v1\"")
}

@ -17,6 +17,7 @@ limitations under the License.
package fake
import (
"errors"
"fmt"
"io"
"strings"
@ -39,7 +40,10 @@ import (
// Options to control the fake behavior of PrintingKubeClient
type Options struct {
GetReturnResourceMap bool
GetReturnError bool
BuildReturnResourceList bool
BuildReturnError bool
IsReachableReturnsError bool
}
// PrintingKubeClient implements KubeClient, but simply prints the reader to
@ -49,8 +53,18 @@ type PrintingKubeClient struct {
Options *Options
}
var (
ErrPrintingKubeClientNotReachable = errors.New("kubernetes cluster not reachable")
ErrPrintingKubeClientBuildFailure = errors.New("failed to build resource list")
ErrPrintingKubeClientGetFailure = errors.New("failed to get resource")
)
// IsReachable checks if the cluster is reachable
func (p *PrintingKubeClient) IsReachable() error {
if p.Options != nil && p.Options.IsReachableReturnsError {
return ErrPrintingKubeClientNotReachable
}
return nil
}
@ -69,18 +83,24 @@ func (p *PrintingKubeClient) Get(resources kube.ResourceList, _ bool) (map[strin
return nil, err
}
if p.Options == nil || !p.Options.GetReturnResourceMap {
return make(map[string][]runtime.Object), nil
}
if p.Options != nil {
if p.Options.GetReturnError {
return nil, ErrPrintingKubeClientGetFailure
}
if p.Options.GetReturnResourceMap {
result := make(map[string][]runtime.Object)
for _, r := range resources {
result[r.Name] = []runtime.Object{
r.Object,
}
}
result := make(map[string][]runtime.Object)
for _, r := range resources {
result[r.Name] = []runtime.Object{
r.Object,
return result, nil
}
}
return result, nil
return make(map[string][]runtime.Object), nil
}
func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error {
@ -129,21 +149,27 @@ func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ bool) (*kub
// Build implements KubeClient Build.
func (p *PrintingKubeClient) Build(in io.Reader, _ bool) (kube.ResourceList, error) {
if p.Options == nil || !p.Options.BuildReturnResourceList {
return []*resource.Info{}, nil
}
if p.Options != nil {
if p.Options.BuildReturnError {
return nil, ErrPrintingKubeClientBuildFailure
}
manifest, err := (&kio.ByteReader{Reader: in}).Read()
if err != nil {
return nil, err
}
if p.Options.BuildReturnResourceList {
manifest, err := (&kio.ByteReader{Reader: in}).Read()
if err != nil {
return nil, err
}
resources, err := parseResources(manifest)
if err != nil {
return nil, err
resources, err := parseResources(manifest)
if err != nil {
return nil, err
}
return resources, nil
}
}
return resources, nil
return []*resource.Info{}, nil
}
// BuildTable implements KubeClient BuildTable.

Loading…
Cancel
Save