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 }