aboutsummaryrefslogtreecommitdiffstats
path: root/src/k8splugin/internal/helm
diff options
context:
space:
mode:
authorRitu Sood <ritu.sood@intel.com>2021-02-23 20:18:26 -0800
committerKonrad Bańka <k.banka@samsung.com>2021-04-12 09:52:04 +0200
commit1f60346da61383f18b7277037439711aef38a0fe (patch)
tree620201bbf61283c8db54da8f15d6340bbb813988 /src/k8splugin/internal/helm
parent120019529489b5cbcf82d77eec228283fb12d43a (diff)
Migrate to use Helm v3 libraries
Moving to Helm v3. Updated unit tests. Reworked Healthcheck Execution to align with v3 design. Helm v3 requires newer version for K8s libraries. Moved to use version 0.19.4. Issue-ID: MULTICLOUD-1295 Signed-off-by: Ritu Sood <ritu.sood@intel.com> Signed-off-by: Konrad Bańka <k.banka@samsung.com> Change-Id: I091b75d69841dde56ad2c294cca2d5a0291ffa8f
Diffstat (limited to 'src/k8splugin/internal/helm')
-rw-r--r--src/k8splugin/internal/helm/helm.go253
-rw-r--r--src/k8splugin/internal/helm/helm_test.go67
-rw-r--r--src/k8splugin/internal/helm/types.go22
3 files changed, 133 insertions, 209 deletions
diff --git a/src/k8splugin/internal/helm/helm.go b/src/k8splugin/internal/helm/helm.go
index 31047eb6..3c25ac8c 100644
--- a/src/k8splugin/internal/helm/helm.go
+++ b/src/k8splugin/internal/helm/helm.go
@@ -20,31 +20,24 @@ package helm
import (
"fmt"
"io/ioutil"
- "k8s.io/helm/pkg/strvals"
"os"
"path/filepath"
"regexp"
- "sort"
- "strconv"
"strings"
utils "github.com/onap/multicloud-k8s/src/k8splugin/internal"
- "github.com/ghodss/yaml"
pkgerrors "github.com/pkg/errors"
+ "helm.sh/helm/v3/pkg/action"
+ "helm.sh/helm/v3/pkg/chart/loader"
+ "helm.sh/helm/v3/pkg/cli"
+ helmOptions "helm.sh/helm/v3/pkg/cli/values"
+ "helm.sh/helm/v3/pkg/getter"
+ "helm.sh/helm/v3/pkg/releaseutil"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/validation"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
- "k8s.io/helm/pkg/chartutil"
- "k8s.io/helm/pkg/hooks"
- "k8s.io/helm/pkg/manifest"
- "k8s.io/helm/pkg/proto/hapi/chart"
- protorelease "k8s.io/helm/pkg/proto/hapi/release"
- "k8s.io/helm/pkg/releaseutil"
- "k8s.io/helm/pkg/renderutil"
- "k8s.io/helm/pkg/tiller"
- "k8s.io/helm/pkg/timeconv"
)
// Template is the interface for all helm templating commands
@@ -55,7 +48,7 @@ type Template interface {
GenerateKubernetesArtifacts(
chartPath string,
valueFiles []string,
- values []string) (map[string][]string, error)
+ values []string) ([]KubernetesResourceTemplate, []*Hook, error)
}
// TemplateClient implements the Template interface
@@ -79,130 +72,30 @@ func NewTemplateClient(k8sversion, namespace, releasename string) *TemplateClien
}
}
-// Define hooks that are honored by k8splugin
-var honoredEvents = map[string]protorelease.Hook_Event{
- hooks.ReleaseTestSuccess: protorelease.Hook_RELEASE_TEST_SUCCESS,
- hooks.ReleaseTestFailure: protorelease.Hook_RELEASE_TEST_FAILURE,
-}
-
// Combines valueFiles and values into a single values stream.
// values takes precedence over valueFiles
-func (h *TemplateClient) processValues(valueFiles []string, values []string) ([]byte, error) {
- base := map[string]interface{}{}
-
- //Values files that are used for overriding the chart
- for _, filePath := range valueFiles {
- currentMap := map[string]interface{}{}
-
- var bytes []byte
- var err error
- if strings.TrimSpace(filePath) == "-" {
- bytes, err = ioutil.ReadAll(os.Stdin)
- } else {
- bytes, err = ioutil.ReadFile(filePath)
- }
-
- if err != nil {
- return []byte{}, err
- }
-
- if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
- return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
- }
- // Merge with the previous map
- base = h.mergeValues(base, currentMap)
- }
-
- //User specified value. Similar to ones provided by -x
- for _, value := range values {
- if err := strvals.ParseInto(value, base); err != nil {
- return []byte{}, fmt.Errorf("failed parsing --set data: %s", err)
- }
- }
-
- return yaml.Marshal(base)
-}
-
-func (h *TemplateClient) mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
- for k, v := range src {
- // If the key doesn't exist already, then just set the key to that value
- if _, exists := dest[k]; !exists {
- dest[k] = v
- continue
- }
- nextMap, ok := v.(map[string]interface{})
- // If it isn't another map, overwrite the value
- if !ok {
- dest[k] = v
- continue
- }
- // Edge case: If the key exists in the destination, but isn't a map
- destMap, isMap := dest[k].(map[string]interface{})
- // If the source map has a map for this key, prefer it
- if !isMap {
- dest[k] = v
- continue
- }
- // If we got to this point, it is a map in both, so merge them
- dest[k] = h.mergeValues(destMap, nextMap)
- }
- return dest
-}
-
-// Checks whether resource is a hook and if it is, returns hook struct
-//Logic is based on private method
-//file *manifestFile) sort(result *result) error
-//of helm/pkg/tiller package
-func isHook(path, resource string) (*protorelease.Hook, error) {
-
- var entry releaseutil.SimpleHead
- err := yaml.Unmarshal([]byte(resource), &entry)
+func (h *TemplateClient) processValues(valueFiles []string, values []string) (map[string]interface{}, error) {
+ settings := cli.New()
+ providers := getter.All(settings)
+ options := helmOptions.Options{
+ ValueFiles: valueFiles,
+ Values: values,
+ }
+ base, err := options.MergeValues(providers)
if err != nil {
- return nil, pkgerrors.Wrap(err, "Loading resource to YAML")
- }
- //If resource has no metadata it can't be a hook
- if entry.Metadata == nil ||
- entry.Metadata.Annotations == nil ||
- len(entry.Metadata.Annotations) == 0 {
- return nil, nil
- }
- //Determine hook weight
- hookWeight, err := strconv.Atoi(entry.Metadata.Annotations[hooks.HookWeightAnno])
- if err != nil {
- hookWeight = 0
- }
- //Prepare hook obj
- resultHook := &protorelease.Hook{
- Name: entry.Metadata.Name,
- Kind: entry.Kind,
- Path: path,
- Manifest: resource,
- Events: []protorelease.Hook_Event{},
- Weight: int32(hookWeight),
- DeletePolicies: []protorelease.Hook_DeletePolicy{},
+ return nil, err
}
- //Determine hook's events
- hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno]
- if !ok {
- return resultHook, nil
- }
- for _, hookType := range strings.Split(hookTypes, ",") {
- hookType = strings.ToLower(strings.TrimSpace(hookType))
- e, ok := honoredEvents[hookType]
- if ok {
- resultHook.Events = append(resultHook.Events, e)
- }
- }
- return resultHook, nil
+
+ return base, nil
}
// GenerateKubernetesArtifacts a mapping of type to fully evaluated helm template
func (h *TemplateClient) GenerateKubernetesArtifacts(inputPath string, valueFiles []string,
- values []string) ([]KubernetesResourceTemplate, []*protorelease.Hook, error) {
+ values []string) ([]KubernetesResourceTemplate, []*Hook, error) {
var outputDir, chartPath, namespace, releaseName string
var retData []KubernetesResourceTemplate
- var hookList []*protorelease.Hook
+ var hookList []*Hook
releaseName = h.releaseName
namespace = h.kubeNameSpace
@@ -231,106 +124,88 @@ func (h *TemplateClient) GenerateKubernetesArtifacts(inputPath string, valueFile
if err != nil {
return retData, hookList, err
}
- config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}}
if msgs := validation.IsDNS1123Label(releaseName); releaseName != "" && len(msgs) > 0 {
return retData, hookList, fmt.Errorf("release name %s is not a valid DNS label: %s", releaseName, strings.Join(msgs, ";"))
}
- // Check chart requirements to make sure all dependencies are present in /charts
- c, err := chartutil.Load(chartPath)
+ // Initialize the install client
+ client := action.NewInstall(&action.Configuration{})
+ client.DryRun = true
+ client.ClientOnly = true
+ client.ReleaseName = releaseName
+ client.IncludeCRDs = true
+ client.DisableHooks = true //to ensure no duplicates in case of defined pre/post install hooks
+
+ // Check chart dependencies to make sure all are present in /charts
+ chartRequested, err := loader.Load(chartPath)
if err != nil {
- return retData, hookList, pkgerrors.Errorf("Got error: %s", err.Error())
+ return retData, hookList, err
}
- renderOpts := renderutil.Options{
- ReleaseOptions: chartutil.ReleaseOptions{
- Name: releaseName,
- IsInstall: true,
- IsUpgrade: false,
- Time: timeconv.Now(),
- Namespace: namespace,
- },
- KubeVersion: h.kubeVersion,
+ if chartRequested.Metadata.Type != "" && chartRequested.Metadata.Type != "application" {
+ return retData, hookList, fmt.Errorf(
+ "chart %q has an unsupported type and is not installable: %q",
+ chartRequested.Metadata.Name,
+ chartRequested.Metadata.Type,
+ )
}
- renderedTemplates, err := renderutil.Render(c, config, renderOpts)
+ client.Namespace = namespace
+ release, err := client.Run(chartRequested, rawVals)
if err != nil {
return retData, hookList, err
}
-
- newRenderedTemplates := make(map[string]string)
-
- //Some manifests can contain multiple yaml documents
- //This step is splitting them up into multiple files
- //Each file contains only a single k8s kind
- for k, v := range renderedTemplates {
- //Splits into manifest-0, manifest-1 etc
- if filepath.Base(k) == "NOTES.txt" {
- continue
- }
- rmap := releaseutil.SplitManifests(v)
-
- // Iterating over map can yield different order at times
- // so first we'll sort keys
- sortedKeys := make([]string, len(rmap))
- for k1, _ := range rmap {
- sortedKeys = append(sortedKeys, k1)
- }
- // This makes empty files have the lowest indices
- sort.Strings(sortedKeys)
-
- for k1, v1 := range sortedKeys {
- key := fmt.Sprintf("%s-%d", k, k1)
- newRenderedTemplates[key] = rmap[v1]
- }
+ // SplitManifests returns integer-sortable so that manifests get output
+ // in the same order as the input by `BySplitManifestsOrder`.
+ rmap := releaseutil.SplitManifests(release.Manifest)
+ // We won't get any meaningful hooks from here
+ _, m, err := releaseutil.SortManifests(rmap, nil, releaseutil.InstallOrder)
+ if err != nil {
+ return retData, hookList, err
}
-
- listManifests := manifest.SplitManifests(newRenderedTemplates)
- var manifestsToRender []manifest.Manifest
- //render all manifests in the chart
- manifestsToRender = listManifests
- for _, m := range tiller.SortByKind(manifestsToRender) {
- data := m.Content
- b := filepath.Base(m.Name)
+ for _, k := range m {
+ data := k.Content
+ b := filepath.Base(k.Name)
if b == "NOTES.txt" {
continue
}
if strings.HasPrefix(b, "_") {
continue
}
-
// blank template after execution
if h.emptyRegex.MatchString(data) {
continue
}
-
- mfilePath := filepath.Join(outputDir, m.Name)
+ mfilePath := filepath.Join(outputDir, k.Name)
utils.EnsureDirectory(mfilePath)
- err = ioutil.WriteFile(mfilePath, []byte(data), 0666)
+ err = ioutil.WriteFile(mfilePath, []byte(k.Content), 0600)
if err != nil {
return retData, hookList, err
}
-
- hook, _ := isHook(mfilePath, data)
- // if hook is not nil, then append it to hooks list and continue
- // if it's not, disregard error
- if hook != nil {
- hookList = append(hookList, hook)
- continue
- }
-
gvk, err := getGroupVersionKind(data)
if err != nil {
return retData, hookList, err
}
-
kres := KubernetesResourceTemplate{
GVK: gvk,
FilePath: mfilePath,
}
retData = append(retData, kres)
}
+ for _, h := range release.Hooks {
+ hFilePath := filepath.Join(outputDir, h.Name)
+ utils.EnsureDirectory(hFilePath)
+ err = ioutil.WriteFile(hFilePath, []byte(h.Manifest), 0600)
+ if err != nil {
+ return retData, hookList, err
+ }
+ gvk, err := getGroupVersionKind(h.Manifest)
+ if err != nil {
+ return retData, hookList, err
+ }
+ hookList = append(hookList, &Hook{*h, KubernetesResourceTemplate{gvk, hFilePath}})
+ }
return retData, hookList, nil
}
diff --git a/src/k8splugin/internal/helm/helm_test.go b/src/k8splugin/internal/helm/helm_test.go
index 358577ea..29d446fa 100644
--- a/src/k8splugin/internal/helm/helm_test.go
+++ b/src/k8splugin/internal/helm/helm_test.go
@@ -20,6 +20,7 @@ package helm
import (
"crypto/sha256"
"fmt"
+ "gopkg.in/yaml.v2"
"io/ioutil"
"path/filepath"
"strings"
@@ -45,7 +46,7 @@ func TestProcessValues(t *testing.T) {
filepath.Join(profileDir, "override_values.yaml"),
},
//Hash of a combined values.yaml file that is expected
- expectedHash: "c18a70f426933de3c051c996dc34fd537d0131b2d13a2112a2ecff674db6c2f9",
+ expectedHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
expectedError: "",
},
{
@@ -58,7 +59,7 @@ func TestProcessValues(t *testing.T) {
"service.externalPort=82",
},
//Hash of a combined values.yaml file that is expected
- expectedHash: "028a3521fc9f8777ea7e67a6de0c51f2c875b88ca91734999657f0ca924ddb7a",
+ expectedHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
expectedError: "",
},
{
@@ -73,7 +74,7 @@ func TestProcessValues(t *testing.T) {
"service.externalPort=82",
},
//Hash of a combined values.yaml file that is expected
- expectedHash: "516fab4ab7b76ba2ff35a97c2a79b74302543f532857b945f2fe25e717e755be",
+ expectedHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
expectedError: "",
},
{
@@ -83,7 +84,7 @@ func TestProcessValues(t *testing.T) {
"servers[0].port=80",
},
expectedError: "",
- expectedHash: "50d9401b003f65c1ccfd1c5155106fff88c8201ab8b7d66bd6ffa4fe2883bead",
+ expectedHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
}
@@ -102,11 +103,11 @@ func TestProcessValues(t *testing.T) {
}
} else {
//Compute the hash of returned data and compare
- h.Write(out)
gotHash := fmt.Sprintf("%x", h.Sum(nil))
h.Reset()
if gotHash != testCase.expectedHash {
- t.Fatalf("Got unexpected hash '%s' of values.yaml:\n%s", gotHash, out)
+ mout, _ := yaml.Marshal(&out)
+ t.Fatalf("Got unexpected hash '%s' of values.yaml:\n%v", gotHash, string(mout))
}
}
})
@@ -133,9 +134,9 @@ func TestGenerateKubernetesArtifacts(t *testing.T) {
values: []string{},
//sha256 hash of the evaluated templates in each chart
expectedHashMap: map[string]string{
- "testchart2/templates/service.yaml": "fdd6a2b6795486f0dd1d8c44379afb5ffe4072c09f9cf6594738e8ded4dd872d",
- "subcharta/templates/service.yaml": "570389588fffdb7193ab265888d781f3d751f3a40362533344f9aa7bb93a8bb0",
- "subchartb/templates/service.yaml": "5654e03d922e8ec49649b4bbda9dfc9e643b3b7c9c18b602cc7e26fd36a39c2a",
+ "manifest-0": "fcc1083ace82b633e3a0a687d50f532c07e1212b7a42b2c178b65e5768fffcfe",
+ "manifest-2": "eefeac6ff5430a16a32ae3974857cbe5ff516a1a68566e5edcddd410d60397c0",
+ "manifest-1": "b88aa963ee3afb9676e9930519d7caa103df1251da48a9351ab4ac0c5730d2af",
},
expectedError: "",
},
@@ -150,9 +151,9 @@ func TestGenerateKubernetesArtifacts(t *testing.T) {
},
//sha256 hash of the evaluated templates in each chart
expectedHashMap: map[string]string{
- "testchart2/templates/service.yaml": "2bb96e791ecb6a3404bc5de3f6c4182aed881630269e2aa6766df38b0f852724",
- "subcharta/templates/service.yaml": "570389588fffdb7193ab265888d781f3d751f3a40362533344f9aa7bb93a8bb0",
- "subchartb/templates/service.yaml": "5654e03d922e8ec49649b4bbda9dfc9e643b3b7c9c18b602cc7e26fd36a39c2a",
+ "manifest-0": "fcc1083ace82b633e3a0a687d50f532c07e1212b7a42b2c178b65e5768fffcfe",
+ "manifest-2": "03ae530e49071d005be78f581b7c06c59119f91f572b28c0c0c06ced8e37bf6e",
+ "manifest-1": "b88aa963ee3afb9676e9930519d7caa103df1251da48a9351ab4ac0c5730d2af",
},
expectedError: "",
},
@@ -164,8 +165,8 @@ func TestGenerateKubernetesArtifacts(t *testing.T) {
"goingEmpty=false",
},
expectedHashMap: map[string]string{
- "testchart3/templates/multi.yaml-2": "e24cbbefac2c2f700880b8fd041838f2dd48bbc1e099e7c1d2485ae7feb3da0d",
- "testchart3/templates/multi.yaml-3": "592a8e5b2c35b8469aa45703a835bc00657bfe36b51eb08427a46e7d22fb1525",
+ "manifest-0": "666e8d114981a4b5d13fb799be060aa57e0e48904bba4a410f87a2e827a57ddb",
+ "manifest-2": "6a5af22538c273b9d4a3156e3b6bb538c655041eae31e93db21a9e178f73ecf0",
},
expectedError: "",
},
@@ -177,9 +178,9 @@ func TestGenerateKubernetesArtifacts(t *testing.T) {
"goingEmpty=true",
},
expectedHashMap: map[string]string{
- "testchart3/templates/multi.yaml-3": "e24cbbefac2c2f700880b8fd041838f2dd48bbc1e099e7c1d2485ae7feb3da0d",
- "testchart3/templates/multi.yaml-4": "0bea01e65148584609ede5000c024241ba1c35b440b32ec0a4f7013015715bfe",
- "testchart3/templates/multi.yaml-5": "6a5af22538c273b9d4a3156e3b6bb538c655041eae31e93db21a9e178f73ecf0",
+ "manifest-0": "666e8d114981a4b5d13fb799be060aa57e0e48904bba4a410f87a2e827a57ddb",
+ "manifest-1": "8613e7e7cc0186516b13be37ec7fc321ff89e3abaed0a841773a4eba2d77ce2a",
+ "manifest-2": "3543ae9563fe62ce4a7446d72e1cd23140d8cc5495f0221430d70e94845c1408",
},
expectedError: "",
},
@@ -190,7 +191,8 @@ func TestGenerateKubernetesArtifacts(t *testing.T) {
values: []string{},
expectedError: "",
expectedHashMap: map[string]string{
- "mockv3/templates/deployment.yaml": "259a027a4957e7428eb1d2e774fa1afaa62449521853f8b2916887040bae2ca4",
+ "manifest-0": "94975ff704b9cc00a7988fe7fc865665495655ec2584d3e9de2f7e5294c7eb0d",
+ "dummy-test": "b50bb5f818fe0be332f09401104ae9cea59442e2dabe1a16b4ce21b753177a80",
},
},
}
@@ -200,7 +202,7 @@ func TestGenerateKubernetesArtifacts(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.label, func(t *testing.T) {
tc := NewTemplateClient("1.12.3", "testnamespace", "testreleasename")
- out, _, err := tc.GenerateKubernetesArtifacts(testCase.chartPath, testCase.valueFiles,
+ out, hooks, err := tc.GenerateKubernetesArtifacts(testCase.chartPath, testCase.valueFiles,
testCase.values)
if err != nil {
if testCase.expectedError == "" {
@@ -209,13 +211,38 @@ func TestGenerateKubernetesArtifacts(t *testing.T) {
if strings.Contains(err.Error(), testCase.expectedError) == false {
t.Fatalf("Got unexpected error message %s", err)
}
+ } else if len(testCase.expectedHashMap) != len(out)+len(hooks) {
+ t.Fatalf("Mismatch of expected files (%d) and returned resources (%d)",
+ len(testCase.expectedHashMap), len(out)+len(hooks))
} else {
//Compute the hash of returned data and compare
for _, v := range out {
f := v.FilePath
data, err := ioutil.ReadFile(f)
if err != nil {
- t.Errorf("Unable to read file %s", v)
+ t.Fatalf("Unable to read file %s", v)
+ }
+ h.Write(data)
+ gotHash := fmt.Sprintf("%x", h.Sum(nil))
+ h.Reset()
+
+ //Find the right hash from expectedHashMap
+ expectedHash := ""
+ for k1, v1 := range testCase.expectedHashMap {
+ if strings.Contains(f, k1) == true {
+ expectedHash = v1
+ break
+ }
+ }
+ if gotHash != expectedHash {
+ t.Fatalf("Got unexpected hash for %s: '%s'; expected: '%s'", f, gotHash, expectedHash)
+ }
+ }
+ for _, v := range hooks {
+ f := v.KRT.FilePath
+ data, err := ioutil.ReadFile(f)
+ if err != nil {
+ t.Fatalf("Unable to read file %+v", v)
}
h.Write(data)
gotHash := fmt.Sprintf("%x", h.Sum(nil))
diff --git a/src/k8splugin/internal/helm/types.go b/src/k8splugin/internal/helm/types.go
index 2c8badb8..9e066bbc 100644
--- a/src/k8splugin/internal/helm/types.go
+++ b/src/k8splugin/internal/helm/types.go
@@ -1,5 +1,6 @@
/*
* Copyright 2018 Intel Corporation, Inc
+ * Copyright © 2021 Samsung Electronics
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +18,9 @@
package helm
import (
+ "encoding/json"
+
+ "helm.sh/helm/v3/pkg/release"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@@ -39,3 +43,21 @@ type KubernetesResource struct {
// Name of resource in Kubernetes
Name string
}
+
+// Hook is internal container for Helm Hook Definition
+type Hook struct {
+ Hook release.Hook
+ KRT KubernetesResourceTemplate
+}
+
+// Custom Marshal implementation to satisfy external interface
+func (h Hook) MarshalJSON() ([]byte, error) {
+ return json.Marshal(&struct {
+ Name string `json:"name"`
+ Kind string `json:"kind"`
+ Path string `json:"kind"`
+ Manifest string `json:"kind"`
+ Events []release.HookEvent `json:"events"`
+ }{h.Hook.Name, h.Hook.Kind, h.Hook.Path,
+ h.Hook.Manifest, h.Hook.Events})
+}