stack: Do not use templates for env files

This commit removes the templating logic for writing .env files.
Instead it simply writes a key-value directory directly to the destined
file.
This commit is contained in:
Tom 2023-07-14 14:04:38 +02:00
parent 46b16e5700
commit 588cb7ebaa
22 changed files with 180 additions and 121 deletions

142
pkg/compose/env.go Normal file
View file

@ -0,0 +1,142 @@
package compose
import (
"fmt"
"io"
"strings"
"github.com/tkw1536/pkglib/collection"
)
const (
EnvFileHeader = "# THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT. \n\n"
EnvEqualChar = '=' // assignment
EnvReplaceChar = '#' // replacement for invalid characters
EnvEscapeChar = '\\' // escaping
EnvQuoteChar = '"' // quoting
)
type errInvalidName string
func (ei errInvalidName) Error() string {
return fmt.Sprintf("invalid variable name: %q", string(ei))
}
// WriteEnvFile writes a .env file to io.Writer.
// Variables are written in consistent order.
//
// Variable names may only contain ascii letters, numbers or the character "_".
// Invalid variable names are an error.
//
// Variables values are escaped using EscapeEnvValue.
//
// count contains the number of bytes written to writer.
// In case of an error, partial content may already have been written to writer, as indicated by count.
func WriteEnvFile(writer io.Writer, env map[string]string) (count int, err error) {
var n int
// write the header to the file
n, err = fmt.Fprint(writer, EnvFileHeader)
count += n
if err != nil {
return
}
collection.IterateSorted(env, func(key, value string) {
// if we already had an error, break
if err != nil {
return
}
// if we don't have a valid name, break
if !isValidVariable(key) {
err = errInvalidName(key)
return
}
// write write key = EscapeEnvValue(value) followed by a new line
n, err = fmt.Fprintf(writer, "%s%s%s\n", key, string(EnvEqualChar), EscapeEnvValue(value))
count += n
if err != nil {
return
}
})
return
}
// isValidVariable checks if name is a valid variable name.
func isValidVariable(name string) bool {
for _, r := range name {
if !(r == '_' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) {
return false
}
}
return true
}
// Escape escapes the given value to be written to an environment variable.
// If the value does not need escaping, it may return it unchanged.
//
// EscapeEnvValue allows ASCII characters from ' ' to '~' (inclusive) as well as '\t', '\r', '\n'.
// Other characters are automatically replaced by EnvReplaceChar.
func EscapeEnvValue(value string) (escaped string) {
// first check if we need to escape at all.
var changed bool
for _, r := range value {
if !isValidEnvChar(r) || r == '\n' || r == '\r' || r == '\t' || r == '$' || r == EnvEscapeChar || r == EnvQuoteChar {
changed = true
}
}
if !changed {
return value
}
// make a new builder and make space for the original value
var builder strings.Builder
builder.Grow(len(value) + 2)
// begin the quoting
builder.WriteRune(EnvQuoteChar)
// iterate over it
for _, r := range value {
// if the character is invalid, it is replaced with an '_'
if !isValidEnvChar(r) {
builder.WriteRune(EnvReplaceChar)
continue
}
switch r {
// custom escape for '\n', '\r', '\t'
case '\n':
builder.WriteRune(EnvEscapeChar)
builder.WriteRune('n')
case '\r':
builder.WriteRune(EnvEscapeChar)
builder.WriteRune('r')
case '\t':
builder.WriteRune(EnvEscapeChar)
builder.WriteRune('t')
// standard escape for special characters
case '$', EnvEscapeChar, EnvQuoteChar:
builder.WriteRune(EnvEscapeChar)
fallthrough
// that's it
default:
builder.WriteRune(r)
}
}
// close the quote
builder.WriteRune(EnvQuoteChar)
return builder.String()
}
// isValidEnvChar checks if the rune r is allowed in environment variables.
func isValidEnvChar(r rune) bool {
return r == '\n' || r == '\r' || r == '\t' || (r >= ' ' && r <= '~')
}

View file

@ -10,7 +10,6 @@ import (
"github.com/tkw1536/pkglib/fsx/umaskfree"
)
var errExpectedFileButGotDirectory = errors.New("expected a file, but got a directory")
var errExpectedDirectoryButGotFile = errors.New("expected a directory, but got a file")
// InstallDir installs the directory at src within fsys to dst.

View file

@ -5,10 +5,8 @@ import (
"bufio"
"fmt"
"io"
"io/fs"
"strings"
"github.com/tkw1536/pkglib/fsx/umaskfree"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
@ -196,38 +194,3 @@ parseloop:
return nil
}
// InstallTemplate unpacks the resource located at src in fsys, then processes it as a template, and eventually writes it to dst.
// Any existing file is truncated and overwritten.
//
// See [WriteTemplate] for possible errors.
func InstallTemplate(dst string, context map[string]string, src string, fsys fs.FS) error {
// open the srcFile
srcFile, err := fsys.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
// stat it
srcInfo, err := srcFile.Stat()
if err != nil {
return err
}
// check if it is a directory
if srcInfo.IsDir() {
return errExpectedFileButGotDirectory
}
// open the destination file
file, err := umaskfree.Create(dst, srcInfo.Mode())
if err != nil {
return err
}
defer file.Close()
// write the file!
return WriteTemplate(file, context, srcFile)
}