github.com/banzaicloud/operator-tools@v0.28.10/pkg/helm/render.go (about)

     1  // Copyright © 2020 Banzai Cloud
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package helm
    16  
    17  import (
    18  	"bytes"
    19  	"net/http"
    20  	"os"
    21  	"path"
    22  	"strings"
    23  
    24  	"emperror.dev/errors"
    25  	"github.com/banzaicloud/operator-tools/pkg/resources"
    26  	"github.com/ghodss/yaml"
    27  	"helm.sh/helm/v3/pkg/chart"
    28  	"helm.sh/helm/v3/pkg/releaseutil"
    29  
    30  	"helm.sh/helm/v3/pkg/chart/loader"
    31  	"helm.sh/helm/v3/pkg/chartutil"
    32  	"helm.sh/helm/v3/pkg/engine"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  )
    35  
    36  const legacyRequirementsFileName = "requirements.yaml"
    37  
    38  type ReleaseOptions struct {
    39  	Name         string
    40  	Namespace    string
    41  	Revision     int
    42  	IsUpgrade    bool
    43  	IsInstall    bool
    44  	Scheme       *runtime.Scheme
    45  	Capabilities chartutil.Capabilities
    46  }
    47  
    48  func GetDefaultValues(fs http.FileSystem) ([]byte, error) {
    49  	file, err := fs.Open(chartutil.ValuesfileName)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	defer file.Close()
    54  
    55  	buf := new(bytes.Buffer)
    56  	_, err = buf.ReadFrom(file)
    57  	if err != nil {
    58  		return nil, errors.WrapIf(err, "could not read default values")
    59  	}
    60  
    61  	return buf.Bytes(), nil
    62  }
    63  
    64  func Render(fs http.FileSystem, values map[string]interface{}, releaseOptions ReleaseOptions, chartName string) ([]runtime.Object, error) {
    65  	files, err := GetFiles(fs)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	// Create chart and render templates
    71  	chrt, err := loader.LoadFiles(files)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	renderOpts := chartutil.ReleaseOptions{
    77  		Name:      releaseOptions.Name,
    78  		IsInstall: true,
    79  		IsUpgrade: false,
    80  		Namespace: releaseOptions.Namespace,
    81  	}
    82  
    83  	if err := chartutil.ProcessDependencies(chrt, values); err != nil {
    84  		return nil, err
    85  	}
    86  	renderedValues, err := chartutil.ToRenderValues(chrt, values, renderOpts, &releaseOptions.Capabilities)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	renderedTemplates, err := engine.Render(chrt, renderedValues)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	crds := make(map[string]*chart.File)
    96  	for _, crd := range chrt.CRDObjects() {
    97  		crds[crd.Filename] = crd.File
    98  	}
    99  
   100  	typedParser := resources.NewObjectParser(releaseOptions.Scheme)
   101  
   102  	parser := func(json []byte) (runtime.Object, error) {
   103  		return typedParser.ParseYAMLToK8sObject(json, resources.ReplaceAPIVersionYAMLModifier("autoscaling/v2beta1", "autoscaling/v1"))
   104  	}
   105  
   106  	// Merge templates and inject
   107  	var objects []runtime.Object
   108  	for _, tmpl := range files {
   109  		if !strings.HasSuffix(tmpl.Name, "yaml") && !strings.HasSuffix(tmpl.Name, "yml") && !strings.HasSuffix(tmpl.Name, "tpl") {
   110  			continue
   111  		}
   112  		t := path.Join(chartName, tmpl.Name)
   113  		if renderedTemplate, ok := renderedTemplates[t]; ok {
   114  			objects, err = parseAndAppendObjects(parser, objects, renderedTemplate, t)
   115  			if err != nil {
   116  				return nil, err
   117  			}
   118  		} else if crd, ok := crds[t]; ok {
   119  			objects, err = parseAndAppendObjects(parser, objects, string(crd.Data), t)
   120  			if err != nil {
   121  				return nil, err
   122  			}
   123  		}
   124  	}
   125  
   126  	return objects, nil
   127  }
   128  
   129  func parseAndAppendObjects(parser func([]byte) (runtime.Object, error), objects []runtime.Object, renderedTemplate, path string) ([]runtime.Object, error) {
   130  	renderedTemplate = strings.TrimSpace(renderedTemplate)
   131  	if renderedTemplate == "" {
   132  		return objects, nil
   133  	}
   134  
   135  	manifests := releaseutil.SplitManifests(renderedTemplate)
   136  	for _, manifest := range manifests {
   137  		yamlDoc := strings.TrimSpace(manifest)
   138  		if yamlDoc == "" {
   139  			continue
   140  		}
   141  
   142  		// convert yaml to json
   143  		json, err := yaml.YAMLToJSON([]byte(yamlDoc))
   144  		if err != nil {
   145  			return nil, errors.WrapIfWithDetails(err, "unable to convert yaml to json", map[string]interface{}{"templatePath": path})
   146  		}
   147  
   148  		if string(json) == "null" {
   149  			continue
   150  		}
   151  
   152  		o, err := parser(json)
   153  		if err != nil {
   154  			return nil, err
   155  		}
   156  
   157  		objects = append(objects, o)
   158  	}
   159  	return objects, nil
   160  }
   161  
   162  func GetFiles(fs http.FileSystem) ([]*loader.BufferedFile, error) {
   163  	files := []*loader.BufferedFile{
   164  		{
   165  			Name: chartutil.ChartfileName,
   166  		},
   167  		{
   168  			// Without requirements.yaml legacy charts's subdependencies will be processed but cannot be disabled
   169  			// See https://github.com/helm/helm/blob/e2442699fa4703456b16884990c5218c16ed16fc/pkg/chart/loader/load.go#L105
   170  			Name: legacyRequirementsFileName,
   171  		},
   172  	}
   173  
   174  	// if the Helm chart templates use some resource files (like dashboards), those should be put under resources
   175  	for _, dirName := range []string{"resources", "crds", chartutil.TemplatesDir, chartutil.ChartsDir} {
   176  		dir, err := fs.Open(dirName)
   177  		if err != nil {
   178  			if !os.IsNotExist(err) {
   179  				return nil, err
   180  			}
   181  		} else {
   182  			// Recursively get all the files from the dir and it's subfolders
   183  			files, err = getFilesFromDir(fs, dir, files, dirName)
   184  			if err != nil {
   185  				return nil, err
   186  			}
   187  		}
   188  	}
   189  
   190  	filteredFiles := []*loader.BufferedFile{}
   191  	for _, f := range files {
   192  		data, err := readIntoBytes(fs, f.Name)
   193  		if err != nil {
   194  			if strings.HasSuffix(f.Name, legacyRequirementsFileName) {
   195  				continue
   196  			}
   197  			return nil, err
   198  		}
   199  
   200  		f.Data = data
   201  		filteredFiles = append(filteredFiles, f)
   202  	}
   203  
   204  	return filteredFiles, nil
   205  }
   206  
   207  func getFilesFromDir(fs http.FileSystem, dir http.File, files []*loader.BufferedFile, dirName string) ([]*loader.BufferedFile, error) {
   208  	dirFiles, err := dir.Readdir(-1)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	for _, file := range dirFiles {
   214  		filename := file.Name()
   215  		if strings.HasSuffix(filename, "yaml") || strings.HasSuffix(filename, "yml") || strings.HasSuffix(filename, "tpl") || strings.HasSuffix(filename, "json") {
   216  			files = append(files, &loader.BufferedFile{
   217  				Name: dirName + "/" + filename,
   218  			})
   219  		} else if file.IsDir() {
   220  			dir, err := fs.Open(dirName + "/" + filename)
   221  			if err != nil {
   222  				return nil, err
   223  			}
   224  
   225  			files, err = getFilesFromDir(fs, dir, files, dirName+"/"+filename)
   226  			if err != nil {
   227  				return nil, err
   228  			}
   229  		}
   230  	}
   231  	return files, nil
   232  }
   233  
   234  func readIntoBytes(fs http.FileSystem, filename string) ([]byte, error) {
   235  	file, err := fs.Open(filename)
   236  	if err != nil {
   237  		return nil, errors.WrapIf(err, "could not open file")
   238  	}
   239  	defer file.Close()
   240  
   241  	buf := new(bytes.Buffer)
   242  	_, err = buf.ReadFrom(file)
   243  	if err != nil {
   244  		return nil, errors.WrapIf(err, "could not read file")
   245  	}
   246  
   247  	return buf.Bytes(), nil
   248  }