diff options
Diffstat (limited to 'src/k8splugin/internal/helm')
-rw-r--r-- | src/k8splugin/internal/helm/helm.go | 253 | ||||
-rw-r--r-- | src/k8splugin/internal/helm/helm_test.go | 67 | ||||
-rw-r--r-- | src/k8splugin/internal/helm/types.go | 22 |
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, ¤tMap); 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}) +} |