test(get deployed): Add cmd/helm level tests

Add cmd/helm 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 7b46794469
commit 9810277c39
No known key found for this signature in database
GPG Key ID: F2D6B63B5A17E057

@ -0,0 +1,209 @@
/*
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 main
import (
"bytes"
"fmt"
"testing"
"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"
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
"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
`
func TestGetDeployed(t *testing.T) {
const (
namespace = `thousand-sunny`
releaseName = `one-piece`
)
var (
is = assert.New(t)
manifest bytes.Buffer
relativeCreationTimestamp = time.Now().Add(-2 * time.Minute)
relativeCreationTimestampStr = relativeCreationTimestamp.Format(time.RFC3339)
exactCreationTimestamp = time.Date(2024, time.October, 28, 0, 4, 30, 0, time.FixedZone("IST", 19800))
exactCreationTimestampStr = exactCreationTimestamp.Format(time.RFC3339)
scheme = runtime.NewScheme()
)
manifestTemplateParser, err := template.New("manifestTemplate").Parse(manifestTemplate)
is.NoError(err)
is.NoError(corev1.AddToScheme(scheme))
is.NoError(appsv1.AddToScheme(scheme))
restMapper := testrestmapper.TestOnlyStaticRESTMapper(scheme)
configFlags := genericclioptions.NewTestConfigFlags().
WithRESTMapper(restMapper)
prepareReleaseFunc := func(name, namespace, timestamp string, manifest bytes.Buffer, info *release.Info) []*release.Release {
err = manifestTemplateParser.Execute(&manifest, struct {
Namespace string
CreationTimestamp string
}{
Namespace: namespace,
CreationTimestamp: timestamp,
})
is.NoError(err)
return []*release.Release{{
Name: name,
Namespace: namespace,
Info: info,
Manifest: manifest.String(),
}}
}
tests := []cmdTestCase{
{
name: "get deployed with release",
cmd: fmt.Sprintf("get deployed %s --namespace %s", releaseName, namespace),
golden: "output/get-deployed.txt",
rels: prepareReleaseFunc(
releaseName,
namespace,
relativeCreationTimestampStr,
manifest,
&release.Info{
LastDeployed: helmtime.Unix(relativeCreationTimestamp.Unix(), 0).UTC(),
Status: release.StatusDeployed,
},
),
restClientGetter: configFlags,
kubeClientOpts: &kubefake.Options{
GetReturnResourceMap: true,
BuildReturnResourceList: true,
},
},
{
name: "get deployed with release in json format",
cmd: fmt.Sprintf("get deployed %s --namespace %s --output json", releaseName, namespace),
golden: "output/get-deployed.json",
rels: prepareReleaseFunc(
releaseName,
namespace,
exactCreationTimestampStr,
manifest,
&release.Info{
LastDeployed: helmtime.Unix(exactCreationTimestamp.Unix(), 0).UTC(),
Status: release.StatusDeployed,
},
),
restClientGetter: configFlags,
kubeClientOpts: &kubefake.Options{
GetReturnResourceMap: true,
BuildReturnResourceList: true,
},
},
{
name: "get deployed with release in yaml format",
cmd: fmt.Sprintf("get deployed %s --namespace %s --output yaml", releaseName, namespace),
golden: "output/get-deployed.yaml",
rels: prepareReleaseFunc(
releaseName,
namespace,
exactCreationTimestampStr,
manifest,
&release.Info{
LastDeployed: helmtime.Unix(exactCreationTimestamp.Unix(), 0).UTC(),
Status: release.StatusDeployed,
},
),
restClientGetter: configFlags,
kubeClientOpts: &kubefake.Options{
GetReturnResourceMap: true,
BuildReturnResourceList: true,
},
},
}
runTestCmd(t, tests)
}

@ -0,0 +1 @@
[{"name":"thousand-sunny","namespace":"","apiVersion":"v1","resource":"namespaces","creationTimestamp":"2024-10-27T18:34:30Z"},{"name":"nami","namespace":"thousand-sunny","apiVersion":"v1","resource":"configmaps","creationTimestamp":"2024-10-27T18:34:30Z"},{"name":"zoro","namespace":"thousand-sunny","apiVersion":"v1","resource":"services","creationTimestamp":"2024-10-27T18:34:30Z"},{"name":"luffy","namespace":"thousand-sunny","apiVersion":"apps/v1","resource":"deployments","creationTimestamp":"2024-10-27T18:34:30Z"}]

@ -0,0 +1,5 @@
NAMESPACE NAME API_VERSION AGE
namespaces/thousand-sunny v1 2m
thousand-sunny configmaps/nami v1 2m
thousand-sunny services/zoro v1 2m
thousand-sunny deployments/luffy apps/v1 2m

@ -0,0 +1,20 @@
- apiVersion: v1
creationTimestamp: "2024-10-27T18:34:30Z"
name: thousand-sunny
namespace: ""
resource: namespaces
- apiVersion: v1
creationTimestamp: "2024-10-27T18:34:30Z"
name: nami
namespace: thousand-sunny
resource: configmaps
- apiVersion: v1
creationTimestamp: "2024-10-27T18:34:30Z"
name: zoro
namespace: thousand-sunny
resource: services
- apiVersion: apps/v1
creationTimestamp: "2024-10-27T18:34:30Z"
name: luffy
namespace: thousand-sunny
resource: deployments

@ -17,14 +17,21 @@ limitations under the License.
package fake
import (
"fmt"
"io"
"strings"
"sync"
"time"
v1 "k8s.io/api/core/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
"helm.sh/helm/v3/pkg/kube"
)
@ -61,7 +68,19 @@ func (p *PrintingKubeClient) Get(resources kube.ResourceList, _ bool) (map[strin
if err != nil {
return nil, err
}
return make(map[string][]runtime.Object), nil
if p.Options == nil || !p.Options.GetReturnResourceMap {
return make(map[string][]runtime.Object), nil
}
result := make(map[string][]runtime.Object)
for _, r := range resources {
result[r.Name] = []runtime.Object{
r.Object,
}
}
return result, nil
}
func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error {
@ -109,8 +128,22 @@ func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ bool) (*kub
}
// Build implements KubeClient Build.
func (p *PrintingKubeClient) Build(_ io.Reader, _ bool) (kube.ResourceList, error) {
return []*resource.Info{}, nil
func (p *PrintingKubeClient) Build(in io.Reader, _ bool) (kube.ResourceList, error) {
if p.Options == nil || !p.Options.BuildReturnResourceList {
return []*resource.Info{}, nil
}
manifest, err := (&kio.ByteReader{Reader: in}).Read()
if err != nil {
return nil, err
}
resources, err := parseResources(manifest)
if err != nil {
return nil, err
}
return resources, nil
}
// BuildTable implements KubeClient BuildTable.
@ -119,8 +152,8 @@ func (p *PrintingKubeClient) BuildTable(_ io.Reader, _ bool) (kube.ResourceList,
}
// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase.
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (v1.PodPhase, error) {
return v1.PodSucceeded, nil
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (corev1.PodPhase, error) {
return corev1.PodSucceeded, nil
}
// DeleteWithPropagationPolicy implements KubeClient delete.
@ -141,3 +174,111 @@ func bufferize(resources kube.ResourceList) io.Reader {
}
return strings.NewReader(builder.String())
}
// parseResources parses Kubernetes manifest YAML as resources suitable for Helm
func parseResources(manifest []*yaml.RNode) ([]*resource.Info, error) {
// Create a scheme
scheme := runtime.NewScheme()
// Define serializer options
serializer := json.NewSerializerWithOptions(
json.DefaultMetaFactory,
scheme,
scheme, json.SerializerOptions{
Yaml: true,
},
)
var objects []*resource.Info
for _, node := range manifest {
// Get the GVK of the rNode
gvk, err := getGVKForNode(node)
if err != nil {
return nil, fmt.Errorf("failed to get the GVK of rNode: %v", err)
}
// Add the GVK to scheme
err = addSchemeForGVK(scheme, gvk)
if err != nil {
return nil, fmt.Errorf("failed to add GVK %q to scheme: %v", gvk, err)
}
// Convert the rNode to JSON bytes
jsonData, err := node.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("error marshaling RNode to JSON: %w", err)
}
// Decode the JSON data into a Kubernetes runtime.Object
obj, _, err := serializer.Decode(jsonData, nil, nil)
if err != nil {
return nil, fmt.Errorf("error decoding JSON to runtime.Object: %w", err)
}
objects = append(objects, &resource.Info{Object: obj})
}
return objects, nil
}
// getGVKForNode returns GVK from an resource YAML node
func getGVKForNode(node *yaml.RNode) (schema.GroupVersionKind, error) {
// Retrieve the apiVersion field from the RNode
apiVersionNode, err := node.Pipe(yaml.Lookup(`apiVersion`))
if err != nil || apiVersionNode == nil {
return schema.GroupVersionKind{}, fmt.Errorf("apiVersion not found in RNode: %v", err)
}
// Retrieve the kind field from the RNode
kindNode, err := node.Pipe(yaml.Lookup(`kind`))
if err != nil || kindNode == nil {
return schema.GroupVersionKind{}, fmt.Errorf("kind not found in RNode: %v", err)
}
// Extract values
apiVersion := apiVersionNode.YNode().Value
kind := kindNode.YNode().Value
// Parse the apiVersion to get GroupVersion
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return schema.GroupVersionKind{}, fmt.Errorf("error parsing apiVersion: %v", err)
}
return gv.WithKind(kind), nil
}
// Mutex to protect concurrent access to the scheme
var schemeMutex sync.Mutex
// Registry to hold AddToScheme functions for each API group.
// Add more GroupVersion to AddToScheme func mappings if required by tests.
var addToSchemeRegistry = map[schema.GroupVersion]func(*runtime.Scheme) error{
corev1.SchemeGroupVersion: corev1.AddToScheme,
appsv1.SchemeGroupVersion: appsv1.AddToScheme,
}
// addSchemeForGVK dynamically adds GroupVersion to scheme
func addSchemeForGVK(scheme *runtime.Scheme, gvk schema.GroupVersionKind) error {
schemeMutex.Lock()
defer schemeMutex.Unlock()
// Exit early if GroupVersion is already registered
gv := gvk.GroupVersion()
if scheme.IsVersionRegistered(gv) {
return nil
}
// Look up the function corresponding to current GroupVersion
addToSchemeFunc, exists := addToSchemeRegistry[gv]
if !exists {
return fmt.Errorf("no AddToScheme function registered for %s", gv)
}
// Register the GroupVersion in the scheme
if err := addToSchemeFunc(scheme); err != nil {
return fmt.Errorf("failed to add scheme for %s: %w", gv, err)
}
return nil
}

Loading…
Cancel
Save