github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/template/template.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package template provides functions for templating yaml files. 5 package template 6 7 import ( 8 "bufio" 9 "encoding/base64" 10 "fmt" 11 "os" 12 "regexp" 13 "strings" 14 15 "github.com/Racer159/jackal/src/types" 16 17 "github.com/Racer159/jackal/src/config" 18 "github.com/Racer159/jackal/src/pkg/message" 19 "github.com/Racer159/jackal/src/pkg/utils" 20 "github.com/defenseunicorns/pkg/helpers" 21 ) 22 23 // TextTemplate represents a value to be templated into a text file. 24 type TextTemplate struct { 25 Sensitive bool 26 AutoIndent bool 27 Type types.VariableType 28 Value string 29 } 30 31 // Values contains the values to be used in the template. 32 type Values struct { 33 config *types.PackagerConfig 34 htpasswd string 35 } 36 37 // Generate returns a Values struct with the values to be used in the template. 38 func Generate(cfg *types.PackagerConfig) (*Values, error) { 39 message.Debug("template.Generate()") 40 var generated Values 41 42 if cfg == nil { 43 return nil, fmt.Errorf("config is nil") 44 } 45 46 generated.config = cfg 47 48 if cfg.State == nil { 49 return &generated, nil 50 } 51 52 regInfo := cfg.State.RegistryInfo 53 54 // Only calculate this for internal registries to allow longer external passwords 55 if regInfo.InternalRegistry { 56 pushUser, err := utils.GetHtpasswdString(regInfo.PushUsername, regInfo.PushPassword) 57 if err != nil { 58 return nil, fmt.Errorf("error generating htpasswd string: %w", err) 59 } 60 61 pullUser, err := utils.GetHtpasswdString(regInfo.PullUsername, regInfo.PullPassword) 62 if err != nil { 63 return nil, fmt.Errorf("error generating htpasswd string: %w", err) 64 } 65 66 generated.htpasswd = fmt.Sprintf("%s\\n%s", pushUser, pullUser) 67 } 68 69 return &generated, nil 70 } 71 72 // Ready returns true if the Values struct is ready to be used in the template. 73 func (values *Values) Ready() bool { 74 return values.config.State != nil 75 } 76 77 // SetState sets the state 78 func (values *Values) SetState(state *types.JackalState) { 79 values.config.State = state 80 } 81 82 // GetVariables returns the variables to be used in the template. 83 func (values *Values) GetVariables(component types.JackalComponent) (templateMap map[string]*TextTemplate, deprecations map[string]string) { 84 templateMap = make(map[string]*TextTemplate) 85 86 depMarkerOld := "DATA_INJECTON_MARKER" 87 depMarkerNew := "DATA_INJECTION_MARKER" 88 deprecations = map[string]string{ 89 fmt.Sprintf("###JACKAL_%s###", depMarkerOld): fmt.Sprintf("###JACKAL_%s###", depMarkerNew), 90 } 91 92 if values.config.State != nil { 93 regInfo := values.config.State.RegistryInfo 94 gitInfo := values.config.State.GitServer 95 96 builtinMap := map[string]string{ 97 "STORAGE_CLASS": values.config.State.StorageClass, 98 99 // Registry info 100 "REGISTRY": regInfo.Address, 101 "NODEPORT": fmt.Sprintf("%d", regInfo.NodePort), 102 "REGISTRY_AUTH_PUSH": regInfo.PushPassword, 103 "REGISTRY_AUTH_PULL": regInfo.PullPassword, 104 105 // Git server info 106 "GIT_PUSH": gitInfo.PushUsername, 107 "GIT_AUTH_PUSH": gitInfo.PushPassword, 108 "GIT_PULL": gitInfo.PullUsername, 109 "GIT_AUTH_PULL": gitInfo.PullPassword, 110 } 111 112 // Include the data injection marker template if the component has data injections 113 if len(component.DataInjections) > 0 { 114 // Preserve existing misspelling for backwards compatibility 115 builtinMap[depMarkerOld] = config.GetDataInjectionMarker() 116 builtinMap[depMarkerNew] = config.GetDataInjectionMarker() 117 } 118 119 // Don't template component-specific variables for every component 120 switch component.Name { 121 case "jackal-agent": 122 agentTLS := values.config.State.AgentTLS 123 builtinMap["AGENT_CRT"] = base64.StdEncoding.EncodeToString(agentTLS.Cert) 124 builtinMap["AGENT_KEY"] = base64.StdEncoding.EncodeToString(agentTLS.Key) 125 builtinMap["AGENT_CA"] = base64.StdEncoding.EncodeToString(agentTLS.CA) 126 127 case "jackal-seed-registry", "jackal-registry": 128 builtinMap["SEED_REGISTRY"] = fmt.Sprintf("%s:%s", helpers.IPV4Localhost, config.JackalSeedPort) 129 builtinMap["HTPASSWD"] = values.htpasswd 130 builtinMap["REGISTRY_SECRET"] = regInfo.Secret 131 132 case "logging": 133 builtinMap["LOGGING_AUTH"] = values.config.State.LoggingSecret 134 } 135 136 // Iterate over any custom variables and add them to the mappings for templating 137 for key, value := range builtinMap { 138 // Builtin keys are always uppercase in the format ###JACKAL_KEY### 139 templateMap[strings.ToUpper(fmt.Sprintf("###JACKAL_%s###", key))] = &TextTemplate{ 140 Value: value, 141 } 142 143 if key == "LOGGING_AUTH" || key == "REGISTRY_SECRET" || key == "HTPASSWD" || 144 key == "AGENT_CA" || key == "AGENT_KEY" || key == "AGENT_CRT" || key == "GIT_AUTH_PULL" || 145 key == "GIT_AUTH_PUSH" || key == "REGISTRY_AUTH_PULL" || key == "REGISTRY_AUTH_PUSH" { 146 // Sanitize any builtin templates that are sensitive 147 templateMap[strings.ToUpper(fmt.Sprintf("###JACKAL_%s###", key))].Sensitive = true 148 } 149 } 150 } 151 152 for key, variable := range values.config.SetVariableMap { 153 // Variable keys are always uppercase in the format ###JACKAL_VAR_KEY### 154 templateMap[strings.ToUpper(fmt.Sprintf("###JACKAL_VAR_%s###", key))] = &TextTemplate{ 155 Value: variable.Value, 156 Sensitive: variable.Sensitive, 157 AutoIndent: variable.AutoIndent, 158 Type: variable.Type, 159 } 160 } 161 162 for _, constant := range values.config.Pkg.Constants { 163 // Constant keys are always uppercase in the format ###JACKAL_CONST_KEY### 164 templateMap[strings.ToUpper(fmt.Sprintf("###JACKAL_CONST_%s###", constant.Name))] = &TextTemplate{ 165 Value: constant.Value, 166 AutoIndent: constant.AutoIndent, 167 } 168 } 169 170 debugPrintTemplateMap(templateMap) 171 message.Debugf("deprecations = %#v", deprecations) 172 173 return templateMap, deprecations 174 } 175 176 // Apply renders the template and writes the result to the given path. 177 func (values *Values) Apply(component types.JackalComponent, path string, ignoreReady bool) error { 178 // If Apply() is called before all values are loaded, fail unless ignoreReady is true 179 if !values.Ready() && !ignoreReady { 180 return fmt.Errorf("template.Apply() called before template.Generate()") 181 } 182 183 templateMap, deprecations := values.GetVariables(component) 184 err := ReplaceTextTemplate(path, templateMap, deprecations, "###JACKAL_[A-Z0-9_]+###") 185 186 return err 187 } 188 189 // ReplaceTextTemplate loads a file from a given path, replaces text in it and writes it back in place. 190 func ReplaceTextTemplate(path string, mappings map[string]*TextTemplate, deprecations map[string]string, templateRegex string) error { 191 textFile, err := os.Open(path) 192 if err != nil { 193 return err 194 } 195 196 // This regex takes a line and parses the text before and after a discovered template: https://regex101.com/r/ilUxAz/1 197 regexTemplateLine := regexp.MustCompile(fmt.Sprintf("(?P<preTemplate>.*?)(?P<template>%s)(?P<postTemplate>.*)", templateRegex)) 198 199 fileScanner := bufio.NewScanner(textFile) 200 201 // Set the buffer to 1 MiB to handle long lines (i.e. base64 text in a secret) 202 // 1 MiB is around the documented maximum size for secrets and configmaps 203 const maxCapacity = 1024 * 1024 204 buf := make([]byte, maxCapacity) 205 fileScanner.Buffer(buf, maxCapacity) 206 207 // Set the scanner to split on new lines 208 fileScanner.Split(bufio.ScanLines) 209 210 text := "" 211 212 for fileScanner.Scan() { 213 line := fileScanner.Text() 214 215 for { 216 matches := regexTemplateLine.FindStringSubmatch(line) 217 218 // No template left on this line so move on 219 if len(matches) == 0 { 220 text += fmt.Sprintln(line) 221 break 222 } 223 224 preTemplate := matches[regexTemplateLine.SubexpIndex("preTemplate")] 225 templateKey := matches[regexTemplateLine.SubexpIndex("template")] 226 227 _, present := deprecations[templateKey] 228 if present { 229 message.Warnf("This Jackal Package uses a deprecated variable: '%s' changed to '%s'. Please notify your package creator for an update.", templateKey, deprecations[templateKey]) 230 } 231 232 template := mappings[templateKey] 233 234 // Check if the template is nil (present), use the original templateKey if not (so that it is not replaced). 235 value := templateKey 236 if template != nil { 237 value = template.Value 238 239 // Check if the value is a file type and load the value contents from the file 240 if template.Type == types.FileVariableType && value != "" { 241 if isText, err := helpers.IsTextFile(value); err != nil || !isText { 242 message.Warnf("Refusing to load a non-text file for templating %s", templateKey) 243 line = matches[regexTemplateLine.SubexpIndex("postTemplate")] 244 continue 245 } 246 247 contents, err := os.ReadFile(value) 248 if err != nil { 249 message.Warnf("Unable to read file for templating - skipping: %s", err.Error()) 250 line = matches[regexTemplateLine.SubexpIndex("postTemplate")] 251 continue 252 } 253 254 value = string(contents) 255 } 256 257 // Check if the value is autoIndented and add the correct spacing 258 if template.AutoIndent { 259 indent := fmt.Sprintf("\n%s", strings.Repeat(" ", len(preTemplate))) 260 value = strings.ReplaceAll(value, "\n", indent) 261 } 262 } 263 264 // Add the processed text and continue processing the line 265 text += fmt.Sprintf("%s%s", preTemplate, value) 266 line = matches[regexTemplateLine.SubexpIndex("postTemplate")] 267 } 268 } 269 270 textFile.Close() 271 272 return os.WriteFile(path, []byte(text), helpers.ReadWriteUser) 273 274 } 275 276 func debugPrintTemplateMap(templateMap map[string]*TextTemplate) { 277 debugText := "templateMap = { " 278 279 for key, template := range templateMap { 280 if template.Sensitive { 281 debugText += fmt.Sprintf("\"%s\": \"**sanitized**\", ", key) 282 } else { 283 debugText += fmt.Sprintf("\"%s\": \"%s\", ", key, template.Value) 284 } 285 } 286 287 debugText += " }" 288 289 message.Debug(debugText) 290 }