Fix Healthcheck API
[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         "k8s.io/helm/pkg/strvals"
24         "os"
25         "path/filepath"
26         "regexp"
27         "sort"
28         "strconv"
29         "strings"
30
31         utils "github.com/onap/multicloud-k8s/src/k8splugin/internal"
32
33         "github.com/ghodss/yaml"
34         pkgerrors "github.com/pkg/errors"
35         "k8s.io/apimachinery/pkg/runtime/schema"
36         "k8s.io/apimachinery/pkg/runtime/serializer/json"
37         "k8s.io/apimachinery/pkg/util/validation"
38         k8syaml "k8s.io/apimachinery/pkg/util/yaml"
39         "k8s.io/helm/pkg/chartutil"
40         "k8s.io/helm/pkg/hooks"
41         "k8s.io/helm/pkg/manifest"
42         "k8s.io/helm/pkg/proto/hapi/chart"
43         protorelease "k8s.io/helm/pkg/proto/hapi/release"
44         "k8s.io/helm/pkg/releaseutil"
45         "k8s.io/helm/pkg/renderutil"
46         "k8s.io/helm/pkg/tiller"
47         "k8s.io/helm/pkg/timeconv"
48 )
49
50 // Template is the interface for all helm templating commands
51 // Any backend implementation will implement this interface and will
52 // access the functionality via this.
53 // FIXME Template is not referenced anywhere
54 type Template interface {
55         GenerateKubernetesArtifacts(
56                 chartPath string,
57                 valueFiles []string,
58                 values []string) (map[string][]string, error)
59 }
60
61 // TemplateClient implements the Template interface
62 // It will also be used to maintain any localized state
63 type TemplateClient struct {
64         emptyRegex    *regexp.Regexp
65         kubeVersion   string
66         kubeNameSpace string
67         releaseName   string
68 }
69
70 // NewTemplateClient returns a new instance of TemplateClient
71 func NewTemplateClient(k8sversion, namespace, releasename string) *TemplateClient {
72         return &TemplateClient{
73                 // emptyRegex defines template content that could be considered empty yaml-wise
74                 emptyRegex: regexp.MustCompile(`(?m)\A(^(\s*#.*|\s*)$\n?)*\z`),
75                 // defaultKubeVersion is the default value of --kube-version flag
76                 kubeVersion:   k8sversion,
77                 kubeNameSpace: namespace,
78                 releaseName:   releasename,
79         }
80 }
81
82 // Define hooks that are honored by k8splugin
83 var honoredEvents = map[string]protorelease.Hook_Event{
84         hooks.ReleaseTestSuccess: protorelease.Hook_RELEASE_TEST_SUCCESS,
85         hooks.ReleaseTestFailure: protorelease.Hook_RELEASE_TEST_FAILURE,
86 }
87
88 // Combines valueFiles and values into a single values stream.
89 // values takes precedence over valueFiles
90 func (h *TemplateClient) processValues(valueFiles []string, values []string) ([]byte, error) {
91         base := map[string]interface{}{}
92
93         //Values files that are used for overriding the chart
94         for _, filePath := range valueFiles {
95                 currentMap := map[string]interface{}{}
96
97                 var bytes []byte
98                 var err error
99                 if strings.TrimSpace(filePath) == "-" {
100                         bytes, err = ioutil.ReadAll(os.Stdin)
101                 } else {
102                         bytes, err = ioutil.ReadFile(filePath)
103                 }
104
105                 if err != nil {
106                         return []byte{}, err
107                 }
108
109                 if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
110                         return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
111                 }
112                 // Merge with the previous map
113                 base = h.mergeValues(base, currentMap)
114         }
115
116         //User specified value. Similar to ones provided by -x
117         for _, value := range values {
118                 if err := strvals.ParseInto(value, base); err != nil {
119                         return []byte{}, fmt.Errorf("failed parsing --set data: %s", err)
120                 }
121         }
122
123         return yaml.Marshal(base)
124 }
125
126 func (h *TemplateClient) mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
127         for k, v := range src {
128                 // If the key doesn't exist already, then just set the key to that value
129                 if _, exists := dest[k]; !exists {
130                         dest[k] = v
131                         continue
132                 }
133                 nextMap, ok := v.(map[string]interface{})
134                 // If it isn't another map, overwrite the value
135                 if !ok {
136                         dest[k] = v
137                         continue
138                 }
139                 // Edge case: If the key exists in the destination, but isn't a map
140                 destMap, isMap := dest[k].(map[string]interface{})
141                 // If the source map has a map for this key, prefer it
142                 if !isMap {
143                         dest[k] = v
144                         continue
145                 }
146                 // If we got to this point, it is a map in both, so merge them
147                 dest[k] = h.mergeValues(destMap, nextMap)
148         }
149         return dest
150 }
151
152 // Checks whether resource is a hook and if it is, returns hook struct
153 //Logic is based on private method
154 //file *manifestFile) sort(result *result) error
155 //of helm/pkg/tiller package
156 func isHook(path, resource string) (*protorelease.Hook, error) {
157
158         var entry releaseutil.SimpleHead
159         err := yaml.Unmarshal([]byte(resource), &entry)
160         if err != nil {
161                 return nil, pkgerrors.Wrap(err, "Loading resource to YAML")
162         }
163         //If resource has no metadata it can't be a hook
164         if entry.Metadata == nil ||
165                 entry.Metadata.Annotations == nil ||
166                 len(entry.Metadata.Annotations) == 0 {
167                 return nil, nil
168         }
169         //Determine hook weight
170         hookWeight, err := strconv.Atoi(entry.Metadata.Annotations[hooks.HookWeightAnno])
171         if err != nil {
172                 hookWeight = 0
173         }
174         //Prepare hook obj
175         resultHook := &protorelease.Hook{
176                 Name:           entry.Metadata.Name,
177                 Kind:           entry.Kind,
178                 Path:           path,
179                 Manifest:       resource,
180                 Events:         []protorelease.Hook_Event{},
181                 Weight:         int32(hookWeight),
182                 DeletePolicies: []protorelease.Hook_DeletePolicy{},
183         }
184         //Determine hook's events
185         hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno]
186         if !ok {
187                 return resultHook, nil
188         }
189         for _, hookType := range strings.Split(hookTypes, ",") {
190                 hookType = strings.ToLower(strings.TrimSpace(hookType))
191                 e, ok := honoredEvents[hookType]
192                 if ok {
193                         resultHook.Events = append(resultHook.Events, e)
194                 }
195         }
196         return resultHook, nil
197 }
198
199 // GenerateKubernetesArtifacts a mapping of type to fully evaluated helm template
200 func (h *TemplateClient) GenerateKubernetesArtifacts(inputPath string, valueFiles []string,
201         values []string) ([]KubernetesResourceTemplate, []*protorelease.Hook, error) {
202
203         var outputDir, chartPath, namespace, releaseName string
204         var retData []KubernetesResourceTemplate
205         var hookList []*protorelease.Hook
206
207         releaseName = h.releaseName
208         namespace = h.kubeNameSpace
209
210         // verify chart path exists
211         if _, err := os.Stat(inputPath); err == nil {
212                 if chartPath, err = filepath.Abs(inputPath); err != nil {
213                         return retData, hookList, err
214                 }
215         } else {
216                 return retData, hookList, err
217         }
218
219         //Create a temp directory in the system temp folder
220         outputDir, err := ioutil.TempDir("", "helm-tmpl-")
221         if err != nil {
222                 return retData, hookList, pkgerrors.Wrap(err, "Got error creating temp dir")
223         }
224
225         if namespace == "" {
226                 namespace = "default"
227         }
228
229         // get combined values and create config
230         rawVals, err := h.processValues(valueFiles, values)
231         if err != nil {
232                 return retData, hookList, err
233         }
234         config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}}
235
236         if msgs := validation.IsDNS1123Label(releaseName); releaseName != "" && len(msgs) > 0 {
237                 return retData, hookList, fmt.Errorf("release name %s is not a valid DNS label: %s", releaseName, strings.Join(msgs, ";"))
238         }
239
240         // Check chart requirements to make sure all dependencies are present in /charts
241         c, err := chartutil.Load(chartPath)
242         if err != nil {
243                 return retData, hookList, pkgerrors.Errorf("Got error: %s", err.Error())
244         }
245
246         renderOpts := renderutil.Options{
247                 ReleaseOptions: chartutil.ReleaseOptions{
248                         Name:      releaseName,
249                         IsInstall: true,
250                         IsUpgrade: false,
251                         Time:      timeconv.Now(),
252                         Namespace: namespace,
253                 },
254                 KubeVersion: h.kubeVersion,
255         }
256
257         renderedTemplates, err := renderutil.Render(c, config, renderOpts)
258         if err != nil {
259                 return retData, hookList, err
260         }
261
262         newRenderedTemplates := make(map[string]string)
263
264         //Some manifests can contain multiple yaml documents
265         //This step is splitting them up into multiple files
266         //Each file contains only a single k8s kind
267         for k, v := range renderedTemplates {
268                 //Splits into manifest-0, manifest-1 etc
269                 if filepath.Base(k) == "NOTES.txt" {
270                         continue
271                 }
272                 rmap := releaseutil.SplitManifests(v)
273
274                 // Iterating over map can yield different order at times
275                 // so first we'll sort keys
276                 sortedKeys := make([]string, len(rmap))
277                 for k1, _ := range rmap {
278                         sortedKeys = append(sortedKeys, k1)
279                 }
280                 // This makes empty files have the lowest indices
281                 sort.Strings(sortedKeys)
282
283                 for k1, v1 := range sortedKeys {
284                         key := fmt.Sprintf("%s-%d", k, k1)
285                         newRenderedTemplates[key] = rmap[v1]
286                 }
287         }
288
289         listManifests := manifest.SplitManifests(newRenderedTemplates)
290         var manifestsToRender []manifest.Manifest
291         //render all manifests in the chart
292         manifestsToRender = listManifests
293         for _, m := range tiller.SortByKind(manifestsToRender) {
294                 data := m.Content
295                 b := filepath.Base(m.Name)
296                 if b == "NOTES.txt" {
297                         continue
298                 }
299                 if strings.HasPrefix(b, "_") {
300                         continue
301                 }
302
303                 // blank template after execution
304                 if h.emptyRegex.MatchString(data) {
305                         continue
306                 }
307
308                 mfilePath := filepath.Join(outputDir, m.Name)
309                 utils.EnsureDirectory(mfilePath)
310                 err = ioutil.WriteFile(mfilePath, []byte(data), 0666)
311                 if err != nil {
312                         return retData, hookList, err
313                 }
314
315                 hook, _ := isHook(mfilePath, data)
316                 // if hook is not nil, then append it to hooks list and continue
317                 // if it's not, disregard error
318                 if hook != nil {
319                         hookList = append(hookList, hook)
320                         continue
321                 }
322
323                 gvk, err := getGroupVersionKind(data)
324                 if err != nil {
325                         return retData, hookList, err
326                 }
327
328                 kres := KubernetesResourceTemplate{
329                         GVK:      gvk,
330                         FilePath: mfilePath,
331                 }
332                 retData = append(retData, kres)
333         }
334         return retData, hookList, nil
335 }
336
337 func getGroupVersionKind(data string) (schema.GroupVersionKind, error) {
338         out, err := k8syaml.ToJSON([]byte(data))
339         if err != nil {
340                 return schema.GroupVersionKind{}, pkgerrors.Wrap(err, "Converting yaml to json:\n"+data)
341         }
342
343         simpleMeta := json.SimpleMetaFactory{}
344         gvk, err := simpleMeta.Interpret(out)
345         if err != nil {
346                 return schema.GroupVersionKind{}, pkgerrors.Wrap(err, "Parsing apiversion and kind")
347         }
348
349         return *gvk, nil
350 }