3c25ac8cbe0125633040d9d3b043f42c431bf4e1
[multicloud/k8s.git] / src / k8splugin / internal / helm / helm.go
1 /*
2  * Copyright 2018 Intel Corporation, Inc
3  * Copyright © 2021 Samsung Electronics
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package helm
19
20 import (
21         "fmt"
22         "io/ioutil"
23         "os"
24         "path/filepath"
25         "regexp"
26         "strings"
27
28         utils "github.com/onap/multicloud-k8s/src/k8splugin/internal"
29
30         pkgerrors "github.com/pkg/errors"
31         "helm.sh/helm/v3/pkg/action"
32         "helm.sh/helm/v3/pkg/chart/loader"
33         "helm.sh/helm/v3/pkg/cli"
34         helmOptions "helm.sh/helm/v3/pkg/cli/values"
35         "helm.sh/helm/v3/pkg/getter"
36         "helm.sh/helm/v3/pkg/releaseutil"
37         "k8s.io/apimachinery/pkg/runtime/schema"
38         "k8s.io/apimachinery/pkg/runtime/serializer/json"
39         "k8s.io/apimachinery/pkg/util/validation"
40         k8syaml "k8s.io/apimachinery/pkg/util/yaml"
41 )
42
43 // Template is the interface for all helm templating commands
44 // Any backend implementation will implement this interface and will
45 // access the functionality via this.
46 // FIXME Template is not referenced anywhere
47 type Template interface {
48         GenerateKubernetesArtifacts(
49                 chartPath string,
50                 valueFiles []string,
51                 values []string) ([]KubernetesResourceTemplate, []*Hook, error)
52 }
53
54 // TemplateClient implements the Template interface
55 // It will also be used to maintain any localized state
56 type TemplateClient struct {
57         emptyRegex    *regexp.Regexp
58         kubeVersion   string
59         kubeNameSpace string
60         releaseName   string
61 }
62
63 // NewTemplateClient returns a new instance of TemplateClient
64 func NewTemplateClient(k8sversion, namespace, releasename string) *TemplateClient {
65         return &TemplateClient{
66                 // emptyRegex defines template content that could be considered empty yaml-wise
67                 emptyRegex: regexp.MustCompile(`(?m)\A(^(\s*#.*|\s*)$\n?)*\z`),
68                 // defaultKubeVersion is the default value of --kube-version flag
69                 kubeVersion:   k8sversion,
70                 kubeNameSpace: namespace,
71                 releaseName:   releasename,
72         }
73 }
74
75 // Combines valueFiles and values into a single values stream.
76 // values takes precedence over valueFiles
77 func (h *TemplateClient) processValues(valueFiles []string, values []string) (map[string]interface{}, error) {
78         settings := cli.New()
79         providers := getter.All(settings)
80         options := helmOptions.Options{
81                 ValueFiles: valueFiles,
82                 Values:     values,
83         }
84         base, err := options.MergeValues(providers)
85         if err != nil {
86                 return nil, err
87         }
88
89         return base, nil
90 }
91
92 // GenerateKubernetesArtifacts a mapping of type to fully evaluated helm template
93 func (h *TemplateClient) GenerateKubernetesArtifacts(inputPath string, valueFiles []string,
94         values []string) ([]KubernetesResourceTemplate, []*Hook, error) {
95
96         var outputDir, chartPath, namespace, releaseName string
97         var retData []KubernetesResourceTemplate
98         var hookList []*Hook
99
100         releaseName = h.releaseName
101         namespace = h.kubeNameSpace
102
103         // verify chart path exists
104         if _, err := os.Stat(inputPath); err == nil {
105                 if chartPath, err = filepath.Abs(inputPath); err != nil {
106                         return retData, hookList, err
107                 }
108         } else {
109                 return retData, hookList, err
110         }
111
112         //Create a temp directory in the system temp folder
113         outputDir, err := ioutil.TempDir("", "helm-tmpl-")
114         if err != nil {
115                 return retData, hookList, pkgerrors.Wrap(err, "Got error creating temp dir")
116         }
117
118         if namespace == "" {
119                 namespace = "default"
120         }
121
122         // get combined values and create config
123         rawVals, err := h.processValues(valueFiles, values)
124         if err != nil {
125                 return retData, hookList, err
126         }
127
128         if msgs := validation.IsDNS1123Label(releaseName); releaseName != "" && len(msgs) > 0 {
129                 return retData, hookList, fmt.Errorf("release name %s is not a valid DNS label: %s", releaseName, strings.Join(msgs, ";"))
130         }
131
132         // Initialize the install client
133         client := action.NewInstall(&action.Configuration{})
134         client.DryRun = true
135         client.ClientOnly = true
136         client.ReleaseName = releaseName
137         client.IncludeCRDs = true
138         client.DisableHooks = true //to ensure no duplicates in case of defined pre/post install hooks
139
140         // Check chart dependencies to make sure all are present in /charts
141         chartRequested, err := loader.Load(chartPath)
142         if err != nil {
143                 return retData, hookList, err
144         }
145
146         if chartRequested.Metadata.Type != "" && chartRequested.Metadata.Type != "application" {
147                 return retData, hookList, fmt.Errorf(
148                         "chart %q has an unsupported type and is not installable: %q",
149                         chartRequested.Metadata.Name,
150                         chartRequested.Metadata.Type,
151                 )
152         }
153
154         client.Namespace = namespace
155         release, err := client.Run(chartRequested, rawVals)
156         if err != nil {
157                 return retData, hookList, err
158         }
159         // SplitManifests returns integer-sortable so that manifests get output
160         // in the same order as the input by `BySplitManifestsOrder`.
161         rmap := releaseutil.SplitManifests(release.Manifest)
162         // We won't get any meaningful hooks from here
163         _, m, err := releaseutil.SortManifests(rmap, nil, releaseutil.InstallOrder)
164         if err != nil {
165                 return retData, hookList, err
166         }
167         for _, k := range m {
168                 data := k.Content
169                 b := filepath.Base(k.Name)
170                 if b == "NOTES.txt" {
171                         continue
172                 }
173                 if strings.HasPrefix(b, "_") {
174                         continue
175                 }
176                 // blank template after execution
177                 if h.emptyRegex.MatchString(data) {
178                         continue
179                 }
180                 mfilePath := filepath.Join(outputDir, k.Name)
181                 utils.EnsureDirectory(mfilePath)
182                 err = ioutil.WriteFile(mfilePath, []byte(k.Content), 0600)
183                 if err != nil {
184                         return retData, hookList, err
185                 }
186                 gvk, err := getGroupVersionKind(data)
187                 if err != nil {
188                         return retData, hookList, err
189                 }
190                 kres := KubernetesResourceTemplate{
191                         GVK:      gvk,
192                         FilePath: mfilePath,
193                 }
194                 retData = append(retData, kres)
195         }
196         for _, h := range release.Hooks {
197                 hFilePath := filepath.Join(outputDir, h.Name)
198                 utils.EnsureDirectory(hFilePath)
199                 err = ioutil.WriteFile(hFilePath, []byte(h.Manifest), 0600)
200                 if err != nil {
201                         return retData, hookList, err
202                 }
203                 gvk, err := getGroupVersionKind(h.Manifest)
204                 if err != nil {
205                         return retData, hookList, err
206                 }
207                 hookList = append(hookList, &Hook{*h, KubernetesResourceTemplate{gvk, hFilePath}})
208         }
209         return retData, hookList, nil
210 }
211
212 func getGroupVersionKind(data string) (schema.GroupVersionKind, error) {
213         out, err := k8syaml.ToJSON([]byte(data))
214         if err != nil {
215                 return schema.GroupVersionKind{}, pkgerrors.Wrap(err, "Converting yaml to json:\n"+data)
216         }
217
218         simpleMeta := json.SimpleMetaFactory{}
219         gvk, err := simpleMeta.Interpret(out)
220         if err != nil {
221                 return schema.GroupVersionKind{}, pkgerrors.Wrap(err, "Parsing apiversion and kind")
222         }
223
224         return *gvk, nil
225 }