Repository: kinvolk/lokomotive (go)

Issue:
lokoctl's pre-update checks rolls back to older Kubernetes version unnecessarily
I installed a cluster with version `v0.6.1` and Kubernetes version being `v1.19.4`.

```console
$ helm history kubernetes -n kube-system
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hummer/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hummer/.kube/config
WARNING: "kubernetes-charts.storage.googleapis.com" is deprecated for "stable" and will be deleted Nov. 13, 2020.
WARNING: You should switch to "https://charts.helm.sh/stable" via:
WARNING: helm repo add "stable" "https://charts.helm.sh/stable" --force-update
REVISION        UPDATED                         STATUS          CHART                   APP VERSION     DESCRIPTION     
1               Tue Mar 23 12:49:59 2021        deployed        kubernetes-0.1.2        v1.19.4         Install complete
```

```console
$ kubectl version -ojson | jq -r '.serverVersion.gitVersion'
v1.19.4
```

Then updated the cluster using new binary versioned `v0.7.0` using command `lokoctl cluster apply` and kubernetes version is now `v1.20.4`.


```console
$ helm history kubernetes -n kube-system
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hummer/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hummer/.kube/config
WARNING: "kubernetes-charts.storage.googleapis.com" is deprecated for "stable" and will be deleted Nov. 13, 2020.
WARNING: You should switch to "https://charts.helm.sh/stable" via:
WARNING: helm repo add "stable" "https://charts.helm.sh/stable" --force-update
REVISION        UPDATED                         STATUS          CHART                   APP VERSION     DESCRIPTION     
1               Tue Mar 23 12:49:59 2021        superseded      kubernetes-0.1.2        v1.19.4         Install complete
2               Tue Mar 23 18:28:24 2021        superseded      kubernetes-0.1.2        v1.19.4         Rollback to 1   
3               Tue Mar 23 18:32:23 2021        superseded      kubernetes-0.1.2        v1.19.4         Rollback to 1   
4               Tue Mar 23 18:36:21 2021        deployed        kubernetes-0.1.3        v1.20.4         Upgrade complete
```

```console
$ kubectl version -ojson | jq -r '.serverVersion.gitVersion'
v1.20.4
```

Now I run the commnd `lokoctl cluster apply` again lokoctl during its pre-update check lokoctl has rolled back the cluster to `1.19.4` while I am using `v0.7.0` lokoctl binary. This is not desirable.

```console
$ helm history kubernetes -n kube-system
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/hummer/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/hummer/.kube/config
WARNING: "kubernetes-charts.storage.googleapis.com" is deprecated for "stable" and will be deleted Nov. 13, 2020.
WARNING: You should switch to "https://charts.helm.sh/stable" via:
WARNING: helm repo add "stable" "https://charts.helm.sh/stable" --force-update
REVISION        UPDATED                         STATUS                  CHART                   APP VERSION     DESCRIPTION     
1               Tue Mar 23 12:49:59 2021        superseded              kubernetes-0.1.2        v1.19.4         Install complete
2               Tue Mar 23 18:28:24 2021        superseded              kubernetes-0.1.2        v1.19.4         Rollback to 1   
3               Tue Mar 23 18:32:23 2021        superseded              kubernetes-0.1.2        v1.19.4         Rollback to 1   
4               Tue Mar 23 18:36:21 2021        deployed                kubernetes-0.1.3        v1.20.4         Upgrade complete
5               Tue Mar 23 19:04:59 2021        pending-rollback        kubernetes-0.1.2        v1.19.4         Rollback to 1
```

```console
$ kubectl version -ojson | jq -r '.serverVersion.gitVersion'
v1.19.4
```


Pull request description:
pkg/helm: add wrapper to get the Helm history we expect
We expect to get the history in descending order by version and limited
to 1 version to get the last version so we can ensure the user has the
last version of the Helm chart.

However, it turns out the Helm API doesn't honor the Max config parameter and
returns the Helm history in a non-deterministic order.

This adds a wrapper function that sorts what the Helm API returns in the
way we expect and uses that.

Fixes #1442 

Patch:
diff --git a/cli/cmd/cluster/cluster.go b/cli/cmd/cluster/cluster.go
index f25ab37c..fea2c9eb 100644
--- a/cli/cmd/cluster/cluster.go
+++ b/cli/cmd/cluster/cluster.go
@@ -32,6 +32,7 @@ import (
 	"github.com/kinvolk/lokomotive/pkg/backend/local"
 	"github.com/kinvolk/lokomotive/pkg/components/util"
 	"github.com/kinvolk/lokomotive/pkg/config"
+	"github.com/kinvolk/lokomotive/pkg/helm"
 	"github.com/kinvolk/lokomotive/pkg/platform"
 	"github.com/kinvolk/lokomotive/pkg/terraform"
 )
@@ -309,9 +310,9 @@ func (c controlplaneUpdater) ensureComponent(component, namespace string) error
 	}
 
 	histClient := action.NewHistory(actionConfig)
-	histClient.Max = 1
+	histMax := 1
 
-	history, err := histClient.Run(component)
+	history, err := helm.GetHistory(histClient, component, histMax)
 	if err != nil && err != driver.ErrReleaseNotFound {
 		return fmt.Errorf("checking for chart history: %w", err)
 	}
diff --git a/pkg/helm/history.go b/pkg/helm/history.go
new file mode 100644
index 00000000..3449f578
--- /dev/null
+++ b/pkg/helm/history.go
@@ -0,0 +1,54 @@
+// Copyright 2021 The Lokomotive 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 helm
+
+import (
+	"sort"
+
+	"helm.sh/helm/v3/pkg/release"
+)
+
+type releaseByVersionDesc []*release.Release
+
+func (rs releaseByVersionDesc) Len() int           { return len(rs) }
+func (rs releaseByVersionDesc) Less(i, j int) bool { return rs[i].Version < rs[j].Version }
+func (rs releaseByVersionDesc) Swap(i, j int)      { rs[i], rs[j] = rs[j], rs[i] }
+
+// HistoryClient allows mocking for tests.
+type HistoryClient interface {
+	Run(name string) ([]*release.Release, error)
+}
+
+// GetHistory returns at most max elements of the Helm history in descending
+// version order, that is, the first element returned is the newest version of
+// the release.
+//
+// See
+// https://github.com/helm/helm/blob/041ce5a2c17a58be0fcd5f5e16fb3e7e95fea622/cmd/helm/history.go#L115-L135.
+// When helm exposes this in the API we can get rid of this.
+func GetHistory(client HistoryClient, name string, max int) ([]*release.Release, error) {
+	history, err := client.Run(name)
+	if err != nil {
+		return nil, err
+	}
+
+	sort.Sort(sort.Reverse(releaseByVersionDesc(history)))
+
+	if max > len(history) {
+		max = len(history)
+	}
+
+	return history[:max], nil
+}


Test patch:
diff --git a/pkg/helm/history_test.go b/pkg/helm/history_test.go
new file mode 100644
index 00000000..4c913efd
--- /dev/null
+++ b/pkg/helm/history_test.go
@@ -0,0 +1,124 @@
+// Copyright 2021 The Lokomotive 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 helm_test
+
+import (
+	"testing"
+
+	"github.com/kinvolk/lokomotive/pkg/helm"
+
+	"github.com/google/go-cmp/cmp"
+	"helm.sh/helm/v3/pkg/release"
+)
+
+type mockHistory []*release.Release
+
+func newMockHistory(rs []*release.Release) mockHistory {
+	return rs
+}
+
+func (m mockHistory) Run(name string) ([]*release.Release, error) {
+	return m, nil
+}
+
+func TestGetHistory(t *testing.T) { //nolint:funlen
+	h := newMockHistory([]*release.Release{
+		{
+			Name:    "testRelease",
+			Version: 6,
+		},
+		{
+			Name:    "testRelease",
+			Version: 2,
+		},
+		{
+			Name:    "testRelease",
+			Version: 4,
+		},
+		{
+			Name:    "testRelease",
+			Version: 3,
+		},
+		{
+			Name:    "testRelease",
+			Version: 5,
+		},
+	})
+
+	type testCase struct {
+		name     string
+		max      int
+		expected []*release.Release
+	}
+
+	cases := []testCase{
+		{
+			name: "returns_at_most_given_max_elements",
+			max:  2,
+			expected: []*release.Release{
+				{
+					Name:    "testRelease",
+					Version: 6,
+				},
+				{
+					Name:    "testRelease",
+					Version: 5,
+				},
+			},
+		},
+		{
+			name: "returns_releases_with_descending_order_by_version",
+			max:  10,
+			expected: []*release.Release{
+				{
+					Name:    "testRelease",
+					Version: 6,
+				},
+				{
+					Name:    "testRelease",
+					Version: 5,
+				},
+				{
+					Name:    "testRelease",
+					Version: 4,
+				},
+				{
+					Name:    "testRelease",
+					Version: 3,
+				},
+				{
+					Name:    "testRelease",
+					Version: 2,
+				},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			got, err := helm.GetHistory(h, "testRelease", tc.max)
+			if err != nil {
+				t.Fatalf("Getting history: %v", err)
+			}
+
+			if diff := cmp.Diff(got, tc.expected); diff != "" {
+				t.Fatalf("Unexpected history (-want +got)\n%s", diff)
+			}
+		})
+	}
+}