From 1f60346da61383f18b7277037439711aef38a0fe Mon Sep 17 00:00:00 2001 From: Ritu Sood Date: Tue, 23 Feb 2021 20:18:26 -0800 Subject: Migrate to use Helm v3 libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Konrad Bańka Change-Id: I091b75d69841dde56ad2c294cca2d5a0291ffa8f --- src/k8splugin/internal/helm/helm.go | 253 +++++++++--------------------------- 1 file changed, 64 insertions(+), 189 deletions(-) (limited to 'src/k8splugin/internal/helm/helm.go') 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 } -- cgit 1.2.3-korg