Skip to content

fix: Helm - only default to release namespace for namespaced resources #373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions pkg/collector/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package collector

import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"

"github.com/ghodss/yaml"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/releaseutil"
)

func parseManifests(manifest string, defaultNamespace string) ([]map[string]interface{}, error) {
func parseManifests(manifest string, defaultNamespace string, discoveryClient discovery.DiscoveryInterface) ([]map[string]interface{}, error) {
var results []map[string]interface{}

manifests := releaseutil.SplitManifests(manifest)
Expand All @@ -20,22 +24,35 @@ func parseManifests(manifest string, defaultNamespace string) ([]map[string]inte
return nil, err
}

fixNamespace(&manifest, defaultNamespace)
fixNamespace(&unstructured.Unstructured{Object: manifest}, defaultNamespace, discoveryClient)

results = append(results, manifest)
}

return results, nil
}

func fixNamespace(manifest *map[string]interface{}, defaultNamespace string) {
func fixNamespace(resource *unstructured.Unstructured, defaultNamespace string, discoveryClient discovery.DiscoveryInterface) {
// Default to the release namespace if the manifest doesn't have the namespace set
if meta, ok := (*manifest)["metadata"]; ok {
switch v := meta.(type) {
case map[string]interface{}:
if val, ok := v["namespace"]; !ok || val == nil {
v["namespace"] = defaultNamespace
}
if resource.GetNamespace() == "" && isResourceNamespaced(discoveryClient, resource.GroupVersionKind()) {
resource.SetNamespace(defaultNamespace)
}
}

func isResourceNamespaced(discoveryClient discovery.DiscoveryInterface, gvk schema.GroupVersionKind) bool {
rs, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
// It seems discovery client fails with error if resource is not found
// this can happen, but is not fatal, we should notify user but continue
if err != nil {
log.Warn().Msgf("failed to discover supported resources for %s: %v", gvk.GroupVersion(), err)
return false
}

for _, r := range rs.APIResources {
if r.Kind == gvk.Kind {
return r.Namespaced
}
}

return false
}
2 changes: 1 addition & 1 deletion pkg/collector/helm2.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (c *HelmV2Collector) Get() ([]map[string]interface{}, error) {
var results []map[string]interface{}

for _, r := range releases {
if manifests, err := parseManifests(r.Manifest, r.Namespace); err != nil {
if manifests, err := parseManifests(r.Manifest, r.Namespace, c.discoveryClient); err != nil {
log.Warn().Msgf("failed to parse release %s/%s: %v", r.Namespace, r.Name, err)
} else {
results = append(results, manifests...)
Expand Down
2 changes: 1 addition & 1 deletion pkg/collector/helm3.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (c *HelmV3Collector) Get() ([]map[string]interface{}, error) {
var results []map[string]interface{}

for _, r := range releases {
if manifests, err := parseManifests(r.Manifest, r.Namespace); err != nil {
if manifests, err := parseManifests(r.Manifest, r.Namespace, c.discoveryClient); err != nil {
log.Warn().Msgf("failed to parse release %s/%s: %v", r.Namespace, r.Name, err)
} else {
results = append(results, manifests...)
Expand Down
53 changes: 48 additions & 5 deletions pkg/collector/helm_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package collector

import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"reflect"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/fake"
)

var FAKE_API_RESOURCES = &metav1.APIResourceList{
GroupVersion: "v1",
APIResources: []metav1.APIResource{
{Name: "pods", Namespaced: true, Kind: "Pod"},
{Name: "services", Namespaced: true, Kind: "Service"},
{Name: "namespaces", Namespaced: false, Kind: "Namespace"},
},
}

func TestSplitManifests(t *testing.T) {
testCases := []struct {
name string
Expand All @@ -24,7 +38,8 @@ func TestSplitManifests(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

manifests, err := parseManifests(tc.input, "default")
fcs := fake.NewSimpleClientset()
manifests, err := parseManifests(tc.input, "default", fcs.Discovery())

if err != nil {
t.Fatalf("Expected to successfully parse manifests: %v", err)
Expand All @@ -45,23 +60,25 @@ func TestFixNamespace(t *testing.T) {
expected string // kinds of objects
}{
{"present",
map[string]interface{}{"metadata": map[string]interface{}{"namespace": "some-namespace"}},
map[string]interface{}{"apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{"namespace": "some-namespace"}},
"some-namespace",
},
{"missing",
map[string]interface{}{"metadata": map[string]interface{}{}},
map[string]interface{}{"apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{}},
"default-namespace",
},
{"nil",
map[string]interface{}{"metadata": map[string]interface{}{"namespace": nil}},
map[string]interface{}{"apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{"namespace": nil}},
"default-namespace",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

fixNamespace(&tc.input, tc.expected)
fcs := fake.NewSimpleClientset()
fcs.Resources = []*metav1.APIResourceList{FAKE_API_RESOURCES}
fixNamespace(&unstructured.Unstructured{tc.input}, tc.expected, fcs.Discovery())

meta, ok := tc.input["metadata"]
if !ok {
Expand All @@ -85,3 +102,29 @@ func TestFixNamespace(t *testing.T) {
})
}
}

func Test_isResourceNamespaced(t *testing.T) {
tests := []struct {
name string
gvk string
want bool
}{
{"true-pod", "Pod.v1.", true},
{"false-namespace", "Namespace.v1.", false},
{"false-non-existent", "XXX.v1.", false},
}

fcs := fake.NewSimpleClientset()
fcs.Resources = []*metav1.APIResourceList{FAKE_API_RESOURCES}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gvk, _ := schema.ParseKindArg(tt.gvk)

got := isResourceNamespaced(fcs.Discovery(), *gvk)
if got != tt.want {
t.Errorf("isResourceNamespaced() got = %v, want %v", got, tt.want)
}
})
}
}