-
Notifications
You must be signed in to change notification settings - Fork 2
jinja support in caterpillar #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,13 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package config | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||||||||||||||
| "regexp" | ||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||
| "text/template" | ||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/itchyny/gojq" | ||||||||||||||||||||||||||||||||||||||||||||||
| "gopkg.in/yaml.v3" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -17,6 +21,11 @@ const ( | |||||||||||||||||||||||||||||||||||||||||||||
| placeholderRegex = "([a-zA-Z0-9_]+?)" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||||||||||||||||||
| // Jinja-style include directive: {% include 'path/to/file' %} | ||||||||||||||||||||||||||||||||||||||||||||||
| includeRegex = regexp.MustCompile(`\{\%\s*include\s+['"]([^'"]+)['"]\s*\%\}`) | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Generic String type that can evaluate both macro and context templates | ||||||||||||||||||||||||||||||||||||||||||||||
| type String string | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -58,15 +67,23 @@ func Load(configFile string, obj interface{}) error { | |||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Preprocess Jinja-style includes | ||||||||||||||||||||||||||||||||||||||||||||||
| configDir := filepath.Dir(configFile) | ||||||||||||||||||||||||||||||||||||||||||||||
| processedContent, err := processIncludes(string(content), configDir, make(map[string]bool)) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to process includes: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // inject secrets | ||||||||||||||||||||||||||||||||||||||||||||||
| configTemplate := template.New(templateName).Funcs(template.FuncMap{ | ||||||||||||||||||||||||||||||||||||||||||||||
| "env": getEnvironmentVariable, // returns environment variable | ||||||||||||||||||||||||||||||||||||||||||||||
| "macro": setMacroPlaceholder, // set placeholder string for macro replacement | ||||||||||||||||||||||||||||||||||||||||||||||
| "secret": getSecret, // we use this template function to inject secrets from parameter store | ||||||||||||||||||||||||||||||||||||||||||||||
| "context": setContextPlaceholder, // set placeholder string for context replacement | ||||||||||||||||||||||||||||||||||||||||||||||
| "ds": getDateString, // returns date string (YYYY-MM-DD) from env var or current date | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| parsedTemplate, err := configTemplate.Parse(string(content)) | ||||||||||||||||||||||||||||||||||||||||||||||
| parsedTemplate, err := configTemplate.Parse(processedContent) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -84,3 +101,80 @@ func Load(configFile string, obj interface{}) error { | |||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // processIncludes processes Jinja-style {% include 'path' %} directives | ||||||||||||||||||||||||||||||||||||||||||||||
| // It recursively includes files and prevents circular includes | ||||||||||||||||||||||||||||||||||||||||||||||
| func processIncludes(content string, baseDir string, visited map[string]bool) (string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Find all include directives | ||||||||||||||||||||||||||||||||||||||||||||||
| matches := includeRegex.FindAllStringSubmatch(content, -1) | ||||||||||||||||||||||||||||||||||||||||||||||
| if len(matches) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||
| return content, nil | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| result := content | ||||||||||||||||||||||||||||||||||||||||||||||
| for _, match := range matches { | ||||||||||||||||||||||||||||||||||||||||||||||
| if len(match) != 2 { | ||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| includePath := match[0] // Full match: {% include 'path' %} | ||||||||||||||||||||||||||||||||||||||||||||||
| filePath := match[1] // Captured group: path | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Resolve the file path relative to baseDir | ||||||||||||||||||||||||||||||||||||||||||||||
| // If the path starts with /, it's absolute, otherwise relative to baseDir | ||||||||||||||||||||||||||||||||||||||||||||||
| var resolvedPath string | ||||||||||||||||||||||||||||||||||||||||||||||
| if filepath.IsAbs(filePath) { | ||||||||||||||||||||||||||||||||||||||||||||||
| resolvedPath = filePath | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| resolvedPath = filepath.Join(baseDir, filePath) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Normalize the path to handle .. and . correctly | ||||||||||||||||||||||||||||||||||||||||||||||
| resolvedPath = filepath.Clean(resolvedPath) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Check for circular includes | ||||||||||||||||||||||||||||||||||||||||||||||
| absPath, err := filepath.Abs(resolvedPath) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("failed to resolve absolute path for %s: %w", filePath, err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if visited[absPath] { | ||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("circular include detected: %s", absPath) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Read the included file | ||||||||||||||||||||||||||||||||||||||||||||||
| includedContent, err := os.ReadFile(resolvedPath) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("failed to read included file %s: %w", filePath, err) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+136
to
+148
|
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Recursively process includes in the included file | ||||||||||||||||||||||||||||||||||||||||||||||
| visited[absPath] = true | ||||||||||||||||||||||||||||||||||||||||||||||
| processedContent, err := processIncludes(string(includedContent), filepath.Dir(resolvedPath), visited) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return "", err | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| delete(visited, absPath) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Replace the include directive with the file content | ||||||||||||||||||||||||||||||||||||||||||||||
| result = strings.ReplaceAll(result, includePath, processedContent) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+159
to
+161
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Replace the include directive with the file content | |
| result = strings.ReplaceAll(result, includePath, processedContent) | |
| } | |
| // Replace only the first occurrence of the include directive with the file content | |
| if idx := strings.Index(result, includePath); idx != -1 { | |
| result = result[:idx] + processedContent + result[idx+len(includePath):] | |
| } |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getDateString function always returns nil error but is declared to return an error. Since this function never fails (both environment variable reads and time.Now().Format() cannot fail), the error return type is unnecessary and inconsistent with similar functions like getEnvironmentVariable which also returns (string, error) but could be simplified.
Consider either:
- Removing the error return type:
func getDateString() string - Validating the date format from environment variables and returning an error if invalid
If you keep the error return, you should validate that DS/EXECUTION_DATE values are in the expected YYYY-MM-DD format.
| func getDateString() (string, error) { | |
| // Check for DS environment variable first (common in data pipelines) | |
| if ds := os.Getenv("DS"); ds != "" { | |
| return ds, nil | |
| } | |
| // Check for EXECUTION_DATE (alternative common name) | |
| if execDate := os.Getenv("EXECUTION_DATE"); execDate != "" { | |
| return execDate, nil | |
| } | |
| // Default to current date | |
| return time.Now().Format("2006-01-02"), nil | |
| func getDateString() string { | |
| // Check for DS environment variable first (common in data pipelines) | |
| if ds := os.Getenv("DS"); ds != "" { | |
| return ds | |
| } | |
| // Check for EXECUTION_DATE (alternative common name) | |
| if execDate := os.Getenv("EXECUTION_DATE"); execDate != "" { | |
| return execDate | |
| } | |
| // Default to current date | |
| return time.Now().Format("2006-01-02") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern
\{\%\s*include\s+['"]([^'"]+)['"]\s*\%\}doesn't properly match Jinja-style include directives that may have mixed quotes. The pattern[^'"]will match until it finds either a single or double quote, which could lead to incorrect parsing if quotes are mixed or nested.For example, the pattern would incorrectly parse:
{% include "file's_name.yaml" %}(stops at the apostrophe in "file's"){% include 'file"name.yaml' %}(stops at the internal quote)Consider using separate patterns for single and double quotes:
Then handle both capture groups in the matching logic.