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  }