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