2 * Copyright 2018 Intel Corporation, Inc
3 * Copyright © 2021 Samsung Electronics
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
23 "k8s.io/helm/pkg/strvals"
31 utils "github.com/onap/multicloud-k8s/src/k8splugin/internal"
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"
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(
58 values []string) (map[string][]string, error)
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
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,
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,
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{}{}
93 //Values files that are used for overriding the chart
94 for _, filePath := range valueFiles {
95 currentMap := map[string]interface{}{}
99 if strings.TrimSpace(filePath) == "-" {
100 bytes, err = ioutil.ReadAll(os.Stdin)
102 bytes, err = ioutil.ReadFile(filePath)
109 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil {
110 return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
112 // Merge with the previous map
113 base = h.mergeValues(base, currentMap)
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)
123 return yaml.Marshal(base)
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 {
133 nextMap, ok := v.(map[string]interface{})
134 // If it isn't another map, overwrite the value
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
146 // If we got to this point, it is a map in both, so merge them
147 dest[k] = h.mergeValues(destMap, nextMap)
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) {
158 var entry releaseutil.SimpleHead
159 err := yaml.Unmarshal([]byte(resource), &entry)
161 return nil, pkgerrors.Wrap(err, "Loading resource to YAML")
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 {
169 //Determine hook weight
170 hookWeight, err := strconv.Atoi(entry.Metadata.Annotations[hooks.HookWeightAnno])
175 resultHook := &protorelease.Hook{
176 Name: entry.Metadata.Name,
180 Events: []protorelease.Hook_Event{},
181 Weight: int32(hookWeight),
182 DeletePolicies: []protorelease.Hook_DeletePolicy{},
184 //Determine hook's events
185 hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno]
187 return resultHook, nil
189 for _, hookType := range strings.Split(hookTypes, ",") {
190 hookType = strings.ToLower(strings.TrimSpace(hookType))
191 e, ok := honoredEvents[hookType]
193 resultHook.Events = append(resultHook.Events, e)
196 return resultHook, nil
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) {
203 var outputDir, chartPath, namespace, releaseName string
204 var retData []KubernetesResourceTemplate
205 var hookList []*protorelease.Hook
207 releaseName = h.releaseName
208 namespace = h.kubeNameSpace
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
216 return retData, hookList, err
219 //Create a temp directory in the system temp folder
220 outputDir, err := ioutil.TempDir("", "helm-tmpl-")
222 return retData, hookList, pkgerrors.Wrap(err, "Got error creating temp dir")
226 namespace = "default"
229 // get combined values and create config
230 rawVals, err := h.processValues(valueFiles, values)
232 return retData, hookList, err
234 config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}}
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, ";"))
240 // Check chart requirements to make sure all dependencies are present in /charts
241 c, err := chartutil.Load(chartPath)
243 return retData, hookList, pkgerrors.Errorf("Got error: %s", err.Error())
246 renderOpts := renderutil.Options{
247 ReleaseOptions: chartutil.ReleaseOptions{
251 Time: timeconv.Now(),
252 Namespace: namespace,
254 KubeVersion: h.kubeVersion,
257 renderedTemplates, err := renderutil.Render(c, config, renderOpts)
259 return retData, hookList, err
262 newRenderedTemplates := make(map[string]string)
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" {
272 rmap := releaseutil.SplitManifests(v)
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)
280 // This makes empty files have the lowest indices
281 sort.Strings(sortedKeys)
283 for k1, v1 := range sortedKeys {
284 key := fmt.Sprintf("%s-%d", k, k1)
285 newRenderedTemplates[key] = rmap[v1]
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) {
295 b := filepath.Base(m.Name)
296 if b == "NOTES.txt" {
299 if strings.HasPrefix(b, "_") {
303 // blank template after execution
304 if h.emptyRegex.MatchString(data) {
308 mfilePath := filepath.Join(outputDir, m.Name)
309 utils.EnsureDirectory(mfilePath)
310 err = ioutil.WriteFile(mfilePath, []byte(data), 0666)
312 return retData, hookList, err
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
319 hookList = append(hookList, hook)
323 gvk, err := getGroupVersionKind(data)
325 return retData, hookList, err
328 kres := KubernetesResourceTemplate{
332 retData = append(retData, kres)
334 return retData, hookList, nil
337 func getGroupVersionKind(data string) (schema.GroupVersionKind, error) {
338 out, err := k8syaml.ToJSON([]byte(data))
340 return schema.GroupVersionKind{}, pkgerrors.Wrap(err, "Converting yaml to json:\n"+data)
343 simpleMeta := json.SimpleMetaFactory{}
344 gvk, err := simpleMeta.Interpret(out)
346 return schema.GroupVersionKind{}, pkgerrors.Wrap(err, "Parsing apiversion and kind")