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:
parent
46b16e5700
commit
588cb7ebaa
22 changed files with 180 additions and 121 deletions
|
|
@ -1 +0,0 @@
|
||||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
|
||||||
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
|
||||||
HTTPS_ENABLED=${HTTPS_ENABLED}
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
|
||||||
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
|
||||||
CERT_EMAIL=${CERT_EMAIL}
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
SLUG=${SLUG}
|
|
||||||
HOST_RULE=${HOST_RULE}
|
|
||||||
|
|
||||||
DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
|
|
||||||
HTTPS_ENABLED=${HTTPS_ENABLED}
|
|
||||||
|
|
@ -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
142
pkg/compose/env.go
Normal 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 <= '~')
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue