caddyfile: Preprocess env vars in {$THIS} format (#2963)

* transform a caddyfile with environment variables

* support adapt time and runtime variables in the caddyfile

* caddyfile: Pre-process environment variables before parsing

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
This commit is contained in:
Mark Sargent 2020-01-10 05:40:16 +13:00 committed by Matt Holt
parent 3828a3aaac
commit 7c419d5349
3 changed files with 124 additions and 120 deletions

View file

@ -15,6 +15,7 @@
package caddyfile
import (
"bytes"
"io"
"log"
"os"
@ -28,8 +29,12 @@ import (
// Directives that do not appear in validDirectives will cause
// an error. If you do not want to check for valid directives,
// pass in nil instead.
func Parse(filename string, input io.Reader) ([]ServerBlock, error) {
tokens, err := allTokens(filename, input)
//
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
// will be replaced before parsing begins.
func Parse(filename string, input []byte) ([]ServerBlock, error) {
input = replaceEnvVars(input)
tokens, err := allTokens(filename, bytes.NewReader(input))
if err != nil {
return nil, err
}
@ -37,6 +42,41 @@ func Parse(filename string, input io.Reader) ([]ServerBlock, error) {
return p.parseAll()
}
// replaceEnvVars replaces all occurrences of environment variables.
func replaceEnvVars(input []byte) []byte {
var offset int
for {
begin := bytes.Index(input[offset:], spanOpen)
if begin < 0 {
break
}
begin += offset // make beginning relative to input, not offset
end := bytes.Index(input[begin+len(spanOpen):], spanClose)
if end < 0 {
break
}
end += begin + len(spanOpen) // make end relative to input, not begin
// get the name; if there is no name, skip it
envVarName := input[begin+len(spanOpen) : end]
if len(envVarName) == 0 {
offset = end + len(spanClose)
continue
}
// get the value of the environment variable
envVarValue := []byte(os.Getenv(string(envVarName)))
// splice in the value
input = append(input[:begin],
append(envVarValue, input[end+len(spanClose):]...)...)
// continue at the end of the replacement
offset = begin + len(envVarValue)
}
return input
}
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
@ -128,7 +168,7 @@ func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := replaceEnvVars(p.Val())
tkn := p.Val()
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
@ -245,7 +285,7 @@ func (p *parser) doImport() error {
if !p.NextArg() {
return p.ArgErr()
}
importPattern := replaceEnvVars(p.Val())
importPattern := p.Val()
if importPattern == "" {
return p.Err("Import requires a non-empty filepath")
}
@ -353,8 +393,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
// evaluate any env vars in directive token
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
// a segment is a list of tokens associated with this directive
var segment Segment
@ -379,7 +417,7 @@ func (p *parser) directive() error {
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
segment = append(segment, p.Token())
}
@ -414,36 +452,6 @@ func (p *parser) closeCurlyBrace() error {
return nil
}
// replaceEnvVars replaces environment variables that appear in the token
// and understands both the $UNIX and %WINDOWS% syntaxes.
func replaceEnvVars(s string) string {
s = replaceEnvReferences(s, "{%", "%}")
s = replaceEnvReferences(s, "{$", "}")
return s
}
// replaceEnvReferences performs the actual replacement of env variables
// in s, given the placeholder start and placeholder end strings.
func replaceEnvReferences(s, refStart, refEnd string) string {
index := strings.Index(s, refStart)
for index != -1 {
endIndex := strings.Index(s[index:], refEnd)
if endIndex == -1 {
break
}
endIndex += index
if endIndex > index+len(refStart) {
ref := s[index : endIndex+len(refEnd)]
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
} else {
return s
}
index = strings.Index(s, refStart)
}
return s
}
func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
@ -514,3 +522,7 @@ func (s Segment) Directive() string {
}
return ""
}
// spanOpen and spanClose are used to bound spans that
// contain the name of an environment variable.
var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}