Repository: FairwindsOps/pluto
Pull request #18 (2020-03-27 21:07:25)

Refactor helm package to be able to test the kubernetes-related functions

codecov[bot]: # [Codecov](https://codecov.io/gh/FairwindsOps/pluto/pull/18?src=pr&el=h1) Report
> Merging [#18](https://codecov.io/gh/FairwindsOps/pluto/pull/18?src=pr&el=desc) into [master](https://codecov.io/gh/FairwindsOps/pluto/commit/7f7aa75b1b32e0aaa2baeff54380ffa45ac7d926&el=desc) will **increase** coverage by `7.37%`.
> The diff coverage is `69.69%`.

[![Impacted file tree graph](https://codecov.io/gh/FairwindsOps/pluto/pull/18/graphs/tree.svg?width=650&height=150&src=pr&token=A23F79JTNA)](https://codecov.io/gh/FairwindsOps/pluto/pull/18?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #18      +/-   ##
==========================================
+ Coverage   64.55%   71.92%   +7.37%     
==========================================
  Files           5        5              
  Lines         268      285      +17     
==========================================
+ Hits          173      205      +32     
+ Misses         77       60      -17     
- Partials       18       20       +2     
```


| [Impacted Files](https://codecov.io/gh/FairwindsOps/pluto/pull/18?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/helm/helm.go](https://codecov.io/gh/FairwindsOps/pluto/pull/18/diff?src=pr&el=tree#diff-cGtnL2hlbG0vaGVsbS5nbw==) | `62.96% <69.69%> (+23.40%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/FairwindsOps/pluto/pull/18?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/FairwindsOps/pluto/pull/18?src=pr&el=footer). Last update [7f7aa75...4bc429a](https://codecov.io/gh/FairwindsOps/pluto/pull/18?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/go.mod b/go.mod
index 009250a..56d1520 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/stretchr/testify v1.4.0
 	gopkg.in/yaml.v2 v2.2.8
 	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
+	k8s.io/api v0.17.4
 	k8s.io/apimachinery v0.17.4
 	k8s.io/client-go v0.17.4
 	k8s.io/klog v1.0.0
diff --git a/go.sum b/go.sum
index 79b34b3..64afdc4 100644
--- a/go.sum
+++ b/go.sum
@@ -71,6 +71,7 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
 github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
@@ -469,6 +470,7 @@ k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUc
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
+k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go
index d26626c..cd9b61c 100644
--- a/pkg/helm/helm.go
+++ b/pkg/helm/helm.go
@@ -24,6 +24,7 @@ import (
 	"regexp"
 	"strconv"
 
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/klog"
 
@@ -35,6 +36,7 @@ type Helm struct {
 	CurrentReleases []*Release
 	Outputs         []*api.Output
 	Version         string
+	Kube            *kube
 }
 
 // Release is a single version of a chart release
@@ -47,18 +49,19 @@ type Release struct {
 func NewHelm(version string) *Helm {
 	return &Helm{
 		Version: version,
+		Kube:    getConfigInstance(),
 	}
 }
 
 // FindVersions is the primary method in the package. It ties together all the functionality
 func (h *Helm) FindVersions() error {
 	var err error
-	var k = getConfigInstance()
+
 	switch h.Version {
 	case "2":
-		err = h.getManifestsVersionTwo(k)
+		err = h.getManifestsVersionTwo()
 	case "3":
-		err = h.getManifestsVersionThree(k)
+		err = h.getManifestsVersionThree()
 	default:
 		err = fmt.Errorf("helm version either not specified or incorrect (use 2 or 3)")
 	}
@@ -67,28 +70,24 @@ func (h *Helm) FindVersions() error {
 }
 
 // getManifestsVersionTwo retrieves helm 2 manifests from ConfigMaps
-func (h *Helm) getManifestsVersionTwo(k *kube) error {
+func (h *Helm) getManifestsVersionTwo() error {
+	if h.Version != "2" {
+		return fmt.Errorf("helm 2 function called without helm 2 version set")
+	}
 	return fmt.Errorf("helm 2 check not implemented")
 }
 
 // getManifestsVersionThree retrieves helm 3 manifests from Secrets
-func (h *Helm) getManifestsVersionThree(k *kube) error {
-	secrets, err := k.Client.CoreV1().Secrets("").List(metav1.ListOptions{})
+func (h *Helm) getManifestsVersionThree() error {
+
+	secrets, err := h.getHelmSecrets()
 	if err != nil {
 		return err
 	}
-	var releases []*Release
-	for _, secret := range secrets.Items {
-		if secret.Type != "helm.sh/release.v1" {
-			continue
-		}
-		thisRelease, err := decodeReleaseSecret(string(secret.Data["release"]))
-		if err != nil {
-			klog.Error("Failed parsing secret release data for helm 3: ", err)
-			return err
-		}
-		thisRelease.Name = secret.Name
-		releases = append(releases, thisRelease)
+
+	releases, err := h.getReleases(secrets)
+	if err != nil {
+		return err
 	}
 	err = h.setCurrentReleases(releases)
 	if err != nil {
@@ -104,6 +103,38 @@ func (h *Helm) getManifestsVersionThree(k *kube) error {
 	return nil
 }
 
+func (h *Helm) getReleases(secrets []*v1.Secret) ([]*Release, error) {
+	var releases []*Release
+	for _, secret := range secrets {
+		thisRelease, err := decodeReleaseSecret(string(secret.Data["release"]))
+		if err != nil {
+			return nil, err
+		}
+		thisRelease.Name = secret.Name
+		releases = append(releases, thisRelease)
+	}
+	return releases, nil
+}
+
+func (h *Helm) getHelmSecrets() ([]*v1.Secret, error) {
+	if h.Version != "3" {
+		return nil, fmt.Errorf("helm 3 function called without helm 3 version set")
+	}
+	secrets, err := h.Kube.Client.CoreV1().Secrets("").List(metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+	helmSecrets := []*v1.Secret{}
+	for _, secret := range secrets.Items {
+		if secret.Type != "helm.sh/release.v1" {
+			klog.V(8).Infof("skipping secret %s because it is not a helm release", secret.ObjectMeta.Name)
+			continue
+		}
+		helmSecrets = append(helmSecrets, &secret)
+	}
+	return helmSecrets, nil
+}
+
 // setCurrentReleases parses all the releases we found and makes sure we only return the most recent
 // which should be the one deployed currently
 func (h *Helm) setCurrentReleases(allReleases []*Release) error {


Test patch:
diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go
index 74b478b..af44f91 100644
--- a/pkg/helm/helm_test.go
+++ b/pkg/helm/helm_test.go
@@ -15,9 +15,13 @@
 package helm
 
 import (
+	"flag"
 	"testing"
 
+	"github.com/fairwindsops/pluto/pkg/api"
 	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 var (
@@ -45,31 +49,21 @@ var (
 	decodedRelease              = &Release{
 		Manifest: "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: utilities\n  labels:\n    app: utilities\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: utilities\n  template:\n    metadata:\n      labels:\n        app: utilities\n    spec:\n      containers:\n      - name: utilities\n        image: quay.io/sudermanjr/utilities:latest\n        # Just spin & wait forever\n        command: [ \"/bin/bash\", \"-c\", \"--\" ]\n        args: [ \"while true; do sleep 30; done;\" ]\n        securityContext:\n          readOnlyRootFilesystem: true\n          allowPrivilegeEscalation: false\n          runAsNonRoot: true\n          runAsUser: 10324\n          capabilities:\n            drop:\n              - ALL\n        resources:\n          requests:\n            cpu: 30m\n            memory: 64Mi\n          limits:\n            cpu: 100m\n            memory: 128Mi",
 	}
-)
-
-func TestNewHelm(t *testing.T) {
-	tests := []struct {
-		name    string
-		version string
-		want    *Helm
-	}{
-		{
-			name:    "version two",
-			version: "2",
-			want:    &Helm{Version: "2"},
+	kubeSecret = &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "utilities",
+			Namespace: "test-namespace",
 		},
-		{
-			// This is a bit silly, but it does test the return of the function.
-			name:    "version three",
-			version: "3",
-			want:    &Helm{Version: "3"},
+		Data: map[string][]byte{
+			"release": []byte(gzippedReleaseDataString),
 		},
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := NewHelm(tt.version)
-			assert.Equal(t, tt.want, got)
-		})
+)
+
+func newMockHelm(version string) *Helm {
+	return &Helm{
+		Version: version,
+		Kube:    getMockConfigInstance(),
 	}
 }
 
@@ -157,3 +151,206 @@ func TestDecodeReleaseSecret(t *testing.T) {
 		})
 	}
 }
+
+func Test_checkForAPIVersion(t *testing.T) {
+	tests := []struct {
+		name     string
+		manifest []byte
+		want     []*api.Output
+		wantErr  bool
+	}{
+		{
+			name:     "empty",
+			manifest: []byte{},
+			want:     []*api.Output{{}},
+			wantErr:  true,
+		},
+		{
+			name:     "got version",
+			manifest: []byte("apiVersion: apps/v1\nkind: Deployment"),
+			want:     []*api.Output{{APIVersion: &api.Version{Name: "apps/v1", Kind: "Deployment", Deprecated: false}}},
+			wantErr:  false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := checkForAPIVersion(tt.manifest)
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
+			assert.NoError(t, err)
+			assert.EqualValues(t, tt.want, got)
+
+		})
+	}
+}
+
+func TestHelm_getManifestsVersionTwo(t *testing.T) {
+	tests := []struct {
+		name        string
+		helmVersion string
+		wantErr     bool
+		errMessage  string
+	}{
+		{
+			name:        "basic",
+			helmVersion: "2",
+			wantErr:     true,
+			errMessage:  "helm 2 check not implemented",
+		},
+		{
+			name:        "three",
+			helmVersion: "3",
+			wantErr:     true,
+			errMessage:  "helm 2 function called without helm 2 version set",
+		},
+	}
+	for _, tt := range tests {
+		h := newMockHelm(tt.helmVersion)
+		t.Run(tt.name, func(t *testing.T) {
+			err := h.getManifestsVersionTwo()
+			if tt.wantErr {
+				assert.EqualError(t, err, tt.errMessage)
+				return
+			}
+			assert.NoError(t, err)
+
+		})
+	}
+}
+
+func TestHelm_getHelmSecrets(t *testing.T) {
+	err := flag.Set("test.v", "false")
+	if err != nil {
+		t.Fatal(err)
+	}
+	kubeSecretNonHelm := v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "non-helm-secret",
+			Namespace: "test-namespace",
+		},
+		Type: "generic",
+	}
+	kubeSecretHelm := v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "helm-secret",
+			Namespace: "test-namespace",
+		},
+		Type: "helm.sh/release.v1",
+	}
+	tests := []struct {
+		name        string
+		helmVersion string
+		want        []*v1.Secret
+		mockSecrets []*v1.Secret
+		wantErr     bool
+	}{
+		{
+			name:        "basic",
+			helmVersion: "2",
+			want:        []*v1.Secret{},
+			wantErr:     true,
+		},
+		{
+			name:        "empty",
+			helmVersion: "3",
+			want:        []*v1.Secret{&kubeSecretHelm},
+			mockSecrets: []*v1.Secret{&kubeSecretNonHelm, &kubeSecretHelm},
+			wantErr:     false,
+		},
+	}
+	for _, tt := range tests {
+
+		// Initalize the mock
+		h := newMockHelm(tt.helmVersion)
+		if len(tt.want) > 0 {
+			for _, secret := range tt.mockSecrets {
+				_, err := h.Kube.Client.CoreV1().Secrets("test-namespace").Create(secret)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
+		}
+
+		// Run the test
+		t.Run(tt.name, func(t *testing.T) {
+			secrets, err := h.getHelmSecrets()
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
+			assert.NoError(t, err)
+			assert.EqualValues(t, tt.want, secrets)
+		})
+	}
+}
+
+func TestHelm_getReleases(t *testing.T) {
+	tests := []struct {
+		name    string
+		secrets []*v1.Secret
+		want    []*Release
+		wantErr bool
+	}{
+		{
+			name:    "functional",
+			secrets: []*v1.Secret{kubeSecret},
+			want:    []*Release{&Release{Name: "utilities", Manifest: decodedRelease.Manifest}},
+			wantErr: false,
+		},
+		{
+			name:    "empty",
+			secrets: []*v1.Secret{},
+			want:    nil,
+			wantErr: false,
+		},
+		{
+			name: "error",
+			secrets: []*v1.Secret{&v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "bad",
+				},
+				Data: map[string][]byte{
+					"release": []byte("this is not good data"),
+				},
+			}},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		h := newMockHelm("3")
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := h.getReleases(tt.secrets)
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
+			assert.NoError(t, err)
+			assert.EqualValues(t, tt.want, got)
+		})
+	}
+}
+
+func TestHelm_FindVersions(t *testing.T) {
+	tests := []struct {
+		name        string
+		helmVersion string
+		wantErr     bool
+		errMessage  string
+	}{
+		// Only adding this one test case since the others generally cross into other functions.
+		{"one - err", "1", true, "helm version either not specified or incorrect (use 2 or 3)"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			h := newMockHelm(tt.helmVersion)
+			err := h.FindVersions()
+			if tt.wantErr {
+				assert.EqualError(t, err, tt.errMessage)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/helm/kube_test.go b/pkg/helm/kube_test.go
new file mode 100644
index 0000000..15e042d
--- /dev/null
+++ b/pkg/helm/kube_test.go
@@ -0,0 +1,12 @@
+package helm
+
+import (
+	testclient "k8s.io/client-go/kubernetes/fake"
+)
+
+func getMockConfigInstance() *kube {
+	kubeClient = &kube{
+		Client: testclient.NewSimpleClientset(),
+	}
+	return kubeClient
+}