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

View file

@ -1 +0,0 @@
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}

View file

@ -2,13 +2,14 @@ package binder
import ( import (
"bytes" "bytes"
"embed"
"io" "io"
"path/filepath" "path/filepath"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/tkw1536/pkglib/yamlx" "github.com/tkw1536/pkglib/yamlx"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
_ "embed"
) )
type Binder struct { type Binder struct {
@ -66,13 +67,8 @@ func (binder *Binder) buildYML() ([]byte, error) {
return yaml.Marshal(dockerCompose) return yaml.Marshal(dockerCompose)
} }
//go:embed binder.env
var resources embed.FS
func (binder *Binder) Stack() component.StackWithResources { func (binder *Binder) Stack() component.StackWithResources {
return component.MakeStack(binder, component.StackWithResources{ return component.MakeStack(binder, component.StackWithResources{
Resources: resources,
EnvPath: "binder.env",
ReadComposeFile: func() (io.Reader, error) { ReadComposeFile: func() (io.Reader, error) {
data, err := binder.buildYML() data, err := binder.buildYML()
if err != nil { if err != nil {

View file

@ -1,11 +0,0 @@
HOST_RULE=${HOST_RULE}
CONFIG_PATH=${CONFIG_PATH}
DEPLOY_ROOT=${DEPLOY_ROOT}
SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
SELF_RESOLVER_BLOCK_FILE=${SELF_RESOLVER_BLOCK_FILE}
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
HTTPS_ENABLED=${HTTPS_ENABLED}
CUSTOM_ASSETS_PATH=${CUSTOM_ASSETS_PATH}

View file

@ -15,14 +15,13 @@ func (control Server) Path() string {
return filepath.Join(control.Still.Config.Paths.Root, "core", "dis") return filepath.Join(control.Still.Config.Paths.Root, "core", "dis")
} }
//go:embed all:server server.env //go:embed all:server
var resources embed.FS var resources embed.FS
func (server *Server) Stack() component.StackWithResources { func (server *Server) Stack() component.StackWithResources {
return component.MakeStack(server, component.StackWithResources{ return component.MakeStack(server, component.StackWithResources{
Resources: resources, Resources: resources,
ContextPath: "server", ContextPath: "server",
EnvPath: "server.env",
EnvContext: map[string]string{ EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": server.Config.Docker.Network(), "DOCKER_NETWORK_NAME": server.Config.Docker.Network(),

View file

@ -1 +0,0 @@
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}

View file

@ -29,7 +29,6 @@ func (*Solr) Context(parent component.InstallationContext) component.Installatio
} }
//go:embed all:solr //go:embed all:solr
//go:embed solr.env
var resources embed.FS var resources embed.FS
func (solr *Solr) Stack() component.StackWithResources { func (solr *Solr) Stack() component.StackWithResources {
@ -37,7 +36,6 @@ func (solr *Solr) Stack() component.StackWithResources {
Resources: resources, Resources: resources,
ContextPath: "solr", ContextPath: "solr",
EnvPath: "solr.env",
EnvContext: map[string]string{ EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": solr.Config.Docker.Network(), "DOCKER_NETWORK_NAME": solr.Config.Docker.Network(),
}, },

View file

@ -1,2 +0,0 @@
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
HTTPS_ENABLED=${HTTPS_ENABLED}

View file

@ -37,7 +37,6 @@ func (*SQL) Context(parent component.InstallationContext) component.Installation
} }
//go:embed all:sql //go:embed all:sql
//go:embed sql.env
var resources embed.FS var resources embed.FS
func (sql *SQL) Stack() component.StackWithResources { func (sql *SQL) Stack() component.StackWithResources {
@ -45,7 +44,6 @@ func (sql *SQL) Stack() component.StackWithResources {
Resources: resources, Resources: resources,
ContextPath: "sql", ContextPath: "sql",
EnvPath: "sql.env",
EnvContext: map[string]string{ EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": sql.Config.Docker.Network(), "DOCKER_NETWORK_NAME": sql.Config.Docker.Network(),
"HTTPS_ENABLED": sql.Config.HTTP.HTTPSEnabledEnv(), "HTTPS_ENABLED": sql.Config.HTTP.HTTPSEnabledEnv(),

View file

@ -1,8 +0,0 @@
HOST_RULE=${HOST_RULE}
CONFIG_PATH=${CONFIG_PATH}
DEPLOY_ROOT=${DEPLOY_ROOT}
SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
SELF_RESOLVER_BLOCK_FILE=${SELF_RESOLVER_BLOCK_FILE}
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}

View file

@ -12,14 +12,13 @@ func (ssh *SSH2) Path() string {
return filepath.Join(ssh.Still.Config.Paths.Root, "core", "ssh2") return filepath.Join(ssh.Still.Config.Paths.Root, "core", "ssh2")
} }
//go:embed all:ssh2 ssh2.env //go:embed all:ssh2
var resources embed.FS var resources embed.FS
func (ssh *SSH2) Stack() component.StackWithResources { func (ssh *SSH2) Stack() component.StackWithResources {
return component.MakeStack(ssh, component.StackWithResources{ return component.MakeStack(ssh, component.StackWithResources{
Resources: resources, Resources: resources,
ContextPath: "ssh2", ContextPath: "ssh2",
EnvPath: "ssh2.env",
EnvContext: map[string]string{ EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": ssh.Config.Docker.Network(), "DOCKER_NETWORK_NAME": ssh.Config.Docker.Network(),

View file

@ -177,9 +177,11 @@ type StackWithResources struct {
Resources fs.FS Resources fs.FS
ContextPath string // the 'docker compose' stack context. Can, but does not have to, contain 'docker-compose.yml' ContextPath string // the 'docker compose' stack context. Can, but does not have to, contain 'docker-compose.yml'
// TODO: Make this nicer to replace variables
ReadComposeFile func() (io.Reader, error) // read the 'docker-compose.yml' (if not contained in context) ReadComposeFile func() (io.Reader, error) // read the 'docker-compose.yml' (if not contained in context)
EnvPath string // the '.env' template, will be installed using [unpack.InstallTemplate]. // EnvPath string // the '.env' template, will be installed using [unpack.InstallTemplate]. If empty, use new syntax
EnvContext map[string]string // context when instantiating the '.env' template EnvContext map[string]string // context when instantiating the '.env' template
CopyContextFiles []string // Files to copy from the installation context CopyContextFiles []string // Files to copy from the installation context
@ -253,14 +255,10 @@ func (is StackWithResources) Install(ctx context.Context, progress io.Writer, co
// configure .env // configure .env
envDest := filepath.Join(is.Dir, ".env") envDest := filepath.Join(is.Dir, ".env")
if is.EnvPath != "" && is.EnvContext != nil { if is.EnvContext != nil {
fmt.Fprintf(progress, "[config] %s\n", envDest) fmt.Fprintf(progress, "[config] %s\n", envDest)
if err := unpack.InstallTemplate(
envDest, if err := writeEnvFile(envDest, is.TouchFilesPerm, is.EnvContext); err != nil {
is.EnvContext,
is.EnvPath,
is.Resources,
); err != nil {
return err return err
} }
} }
@ -319,3 +317,22 @@ func (is StackWithResources) Install(ctx context.Context, progress io.Writer, co
return nil return nil
} }
// writeEnvFile writes an environment file
func writeEnvFile(path string, perm fs.FileMode, variables map[string]string) error {
// create the environment file
file, err := umaskfree.Create(path, perm)
if err != nil {
return err
}
defer file.Close()
// write the file!
_, err = compose.WriteEnvFile(file, variables)
if err != nil {
return err
}
// and return nil
return nil
}

View file

@ -1 +0,0 @@
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}

View file

@ -32,7 +32,6 @@ func (Triplestore) Context(parent component.InstallationContext) component.Insta
} }
//go:embed all:triplestore //go:embed all:triplestore
//go:embed triplestore.env
var resources embed.FS var resources embed.FS
func (ts *Triplestore) Stack() component.StackWithResources { func (ts *Triplestore) Stack() component.StackWithResources {
@ -42,7 +41,6 @@ func (ts *Triplestore) Stack() component.StackWithResources {
CopyContextFiles: []string{"graphdb.zip"}, // TODO: Move into constant? CopyContextFiles: []string{"graphdb.zip"}, // TODO: Move into constant?
EnvPath: "triplestore.env",
EnvContext: map[string]string{ EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": ts.Config.Docker.Network(), "DOCKER_NETWORK_NAME": ts.Config.Docker.Network(),
}, },

View file

@ -1,2 +0,0 @@
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
CERT_EMAIL=${CERT_EMAIL}

View file

@ -2,11 +2,12 @@ package web
import ( import (
"bytes" "bytes"
"embed"
"io" "io"
"path/filepath" "path/filepath"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
_ "embed"
) )
// Web implements the ingress gateway for the distillery. // Web implements the ingress gateway for the distillery.
@ -28,9 +29,6 @@ func (*Web) Context(parent component.InstallationContext) component.Installation
return parent return parent
} }
//go:embed web.env
var webEnv embed.FS
//go:embed docker-compose-http.yml //go:embed docker-compose-http.yml
var dockerComposeHTTP []byte var dockerComposeHTTP []byte
@ -39,8 +37,6 @@ var dockerComposeHTTPS []byte
func (web *Web) Stack() component.StackWithResources { func (web *Web) Stack() component.StackWithResources {
var stack component.StackWithResources var stack component.StackWithResources
stack.Resources = webEnv
stack.EnvPath = "web.env"
stack.EnvContext = map[string]string{ stack.EnvContext = map[string]string{
"DOCKER_NETWORK_NAME": web.Config.Docker.Network(), "DOCKER_NETWORK_NAME": web.Config.Docker.Network(),

View file

@ -1,12 +0,0 @@
DATA_PATH=${DATA_PATH}
RUNTIME_DIR=${RUNTIME_DIR}
SLUG=${SLUG}
WISSKI_HOSTNAME=${HOSTNAME}
HOST_RULE=${HOST_RULE}
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
HTTPS_ENABLED=${HTTPS_ENABLED}
BARREL_BASE_IMAGE=${BARREL_BASE_IMAGE}
OPCACHE_MODE=${OPCACHE_MODE}

View file

@ -7,7 +7,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
) )
//go:embed all:barrel barrel.env //go:embed all:barrel
var barrelResources embed.FS var barrelResources embed.FS
// Barrel returns a stack representing the running WissKI Instance // Barrel returns a stack representing the running WissKI Instance
@ -19,14 +19,13 @@ func (barrel *Barrel) Stack() component.StackWithResources {
Resources: barrelResources, Resources: barrelResources,
ContextPath: filepath.Join("barrel"), ContextPath: filepath.Join("barrel"),
EnvPath: filepath.Join("barrel.env"),
EnvContext: map[string]string{ EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": barrel.Malt.Config.Docker.Network(), "DOCKER_NETWORK_NAME": barrel.Malt.Config.Docker.Network(),
"SLUG": barrel.Slug, "SLUG": barrel.Slug,
"HOST_RULE": barrel.HostRule(), "HOST_RULE": barrel.HostRule(),
"HOSTNAME": barrel.Hostname(), "WISSKI_HOSTNAME": barrel.Hostname(),
"HTTPS_ENABLED": barrel.Malt.Config.HTTP.HTTPSEnabledEnv(), "HTTPS_ENABLED": barrel.Malt.Config.HTTP.HTTPSEnabledEnv(),
"DATA_PATH": filepath.Join(barrel.FilesystemBase, "data"), "DATA_PATH": filepath.Join(barrel.FilesystemBase, "data"),

View file

@ -1,5 +0,0 @@
SLUG=${SLUG}
HOST_RULE=${HOST_RULE}
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
HTTPS_ENABLED=${HTTPS_ENABLED}

View file

@ -14,7 +14,7 @@ type Reserve struct {
ingredient.Base ingredient.Base
} }
//go:embed all:reserve reserve.env //go:embed all:reserve
var reserveResources embed.FS var reserveResources embed.FS
// Stack returns a stack representing the reserve instance // Stack returns a stack representing the reserve instance
@ -26,14 +26,12 @@ func (reserve *Reserve) Stack() component.StackWithResources {
Resources: reserveResources, Resources: reserveResources,
ContextPath: filepath.Join("reserve"), ContextPath: filepath.Join("reserve"),
EnvPath: filepath.Join("reserve.env"),
EnvContext: map[string]string{ EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": reserve.Malt.Config.Docker.Network(), "DOCKER_NETWORK_NAME": reserve.Malt.Config.Docker.Network(),
"SLUG": reserve.Slug, "SLUG": reserve.Slug,
"HOST_RULE": reserve.HostRule(), "HOST_RULE": reserve.HostRule(),
"HOSTNAME": reserve.Hostname(),
"HTTPS_ENABLED": reserve.Malt.Config.HTTP.HTTPSEnabledEnv(), "HTTPS_ENABLED": reserve.Malt.Config.HTTP.HTTPSEnabledEnv(),
}, },
} }

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" "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") var errExpectedDirectoryButGotFile = errors.New("expected a directory, but got a file")
// InstallDir installs the directory at src within fsys to dst. // InstallDir installs the directory at src within fsys to dst.

View file

@ -5,10 +5,8 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"io/fs"
"strings" "strings"
"github.com/tkw1536/pkglib/fsx/umaskfree"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -196,38 +194,3 @@ parseloop:
return nil 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)
}