embed: Begin refactor to use clearer paths
This commit is contained in:
parent
e75dc29de1
commit
e1ee569629
16 changed files with 431 additions and 181 deletions
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/hostname"
|
"github.com/FAU-CDI/wisski-distillery/internal/hostname"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/password"
|
"github.com/FAU-CDI/wisski-distillery/internal/password"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/unpack"
|
||||||
"github.com/tkw1536/goprogram/exit"
|
"github.com/tkw1536/goprogram/exit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -123,18 +124,23 @@ func (bs bootstrap) Run(context wisski_distillery.Context) error {
|
||||||
return errBootstrapWriteConfig.WithMessageF(err)
|
return errBootstrapWriteConfig.WithMessageF(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := embed.InstallTemplate(envPath, filepath.Join("resources", "templates", "bootstrap", "env"), map[string]string{
|
if err := unpack.InstallTemplate(
|
||||||
"DEPLOY_ROOT": root,
|
envPath,
|
||||||
"DEFAULT_DOMAIN": domain,
|
map[string]string{
|
||||||
"SELF_OVERRIDES_FILE": overridesPath,
|
"DEPLOY_ROOT": root,
|
||||||
"AUTHORIZED_KEYS_FILE": authorizedKeysFile,
|
"DEFAULT_DOMAIN": domain,
|
||||||
|
"SELF_OVERRIDES_FILE": overridesPath,
|
||||||
|
"AUTHORIZED_KEYS_FILE": authorizedKeysFile,
|
||||||
|
|
||||||
"GRAPHDB_ADMIN_USER": "admin",
|
"GRAPHDB_ADMIN_USER": "admin",
|
||||||
"GRAPHDB_ADMIN_PASSWORD": password[:64],
|
"GRAPHDB_ADMIN_PASSWORD": password[:64],
|
||||||
|
|
||||||
"MYSQL_ADMIN_USER": "admin",
|
"MYSQL_ADMIN_USER": "admin",
|
||||||
"MYSQL_ADMIN_PASSWORD": password[64:],
|
"MYSQL_ADMIN_PASSWORD": password[64:],
|
||||||
}); err != nil {
|
},
|
||||||
|
filepath.Join("resources", "templates", "bootstrap", "env"),
|
||||||
|
embed.ResourceEmbed,
|
||||||
|
); err != nil {
|
||||||
return errBootstrapWriteConfig.WithMessageF(err)
|
return errBootstrapWriteConfig.WithMessageF(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,12 +152,18 @@ func (bs bootstrap) Run(context wisski_distillery.Context) error {
|
||||||
if err := logging.LogOperation(func() error {
|
if err := logging.LogOperation(func() error {
|
||||||
|
|
||||||
context.Println(overridesPath)
|
context.Println(overridesPath)
|
||||||
if err := embed.InstallTemplate(overridesPath, filepath.Join("resources", "templates", "bootstrap", "overrides.json"), map[string]string{}); err != nil {
|
if err := unpack.InstallFile(
|
||||||
|
overridesPath,
|
||||||
|
fsx.OpenFS(filepath.Join("resources", "templates", "bootstrap", "overrides.json"), embed.ResourceEmbed),
|
||||||
|
); err != nil {
|
||||||
return errBootstrapCreateFile.WithMessageF(err)
|
return errBootstrapCreateFile.WithMessageF(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Println(authorizedKeysFile)
|
context.Println(authorizedKeysFile)
|
||||||
if err := embed.InstallTemplate(authorizedKeysFile, filepath.Join("resources", "templates", "bootstrap", "global_authorized_keys"), map[string]string{}); err != nil {
|
if err := unpack.InstallFile(
|
||||||
|
authorizedKeysFile,
|
||||||
|
fsx.OpenFS(filepath.Join("resources", "templates", "bootstrap", "global_authorized_keys"), embed.ResourceEmbed),
|
||||||
|
); err != nil {
|
||||||
return errBootstrapCreateFile.WithMessageF(err)
|
return errBootstrapCreateFile.WithMessageF(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resourceEmbed contains all the resources required by the WissKI-Distillery package.
|
// ResourceEmbed contains all the resources required by the WissKI-Distillery package.
|
||||||
//go:embed all:resources
|
//go:embed all:resources
|
||||||
var resourceEmbed embed.FS
|
var ResourceEmbed embed.FS
|
||||||
|
|
||||||
// InstallResource install a resource src into dest.
|
// InstallResource install a resource src into dest.
|
||||||
// When it encounters a directory, recursively installs the directory is called.
|
// When it encounters a directory, recursively installs the directory is called.
|
||||||
|
|
@ -22,7 +22,7 @@ var resourceEmbed embed.FS
|
||||||
// If src points to a file, dst must either be an existing file, or not exist.
|
// If src points to a file, dst must either be an existing file, or not exist.
|
||||||
// If src points to a directory, dst must either be an existing directory, or not exist.
|
// If src points to a directory, dst must either be an existing directory, or not exist.
|
||||||
func InstallResource(dst, src string, onInstallFile func(dst, src string)) error {
|
func InstallResource(dst, src string, onInstallFile func(dst, src string)) error {
|
||||||
return installFile(dst, resourceEmbed, src, onInstallFile)
|
return installFile(dst, ResourceEmbed, src, onInstallFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
var errExpectedFileButGotDirectory = errors.New("Expected a file, but got a directory")
|
var errExpectedFileButGotDirectory = errors.New("Expected a file, but got a directory")
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
package embed
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/unpack"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InstallTemplates open the resource src, and installs it into dst.
|
|
||||||
// the template resource must fit into memory.
|
|
||||||
//
|
|
||||||
// For each variable ${THING} inside dest, a key 'THING' must exist in context.
|
|
||||||
// Extra or missing template keys are an error.
|
|
||||||
func InstallTemplate(dst, src string, context map[string]string) error {
|
|
||||||
// open the source file!
|
|
||||||
srcFile, err := resourceEmbed.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Error opening source file %s", src)
|
|
||||||
}
|
|
||||||
defer srcFile.Close()
|
|
||||||
|
|
||||||
// write the template
|
|
||||||
bytes, srcMode, err := unpack.UnpackTemplate(context, srcFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine if we need to create the destination file, or if it already exists
|
|
||||||
dstStat, dstErr := os.Stat(dst)
|
|
||||||
switch {
|
|
||||||
case os.IsNotExist(dstErr):
|
|
||||||
case dstErr != nil:
|
|
||||||
return errors.Wrapf(dstErr, "Error calling stat on destination %s", dst)
|
|
||||||
case dstStat.IsDir():
|
|
||||||
return errors.Wrapf(errExpectedFileButGotDirectory, "Error processing destination %s", dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
// open and write the destination file
|
|
||||||
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcMode)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "Unable to open file %s", dst)
|
|
||||||
}
|
|
||||||
_, err = dstFile.Write(bytes)
|
|
||||||
return errors.Wrapf(err, "Unable to write destination %s", dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadTemplate is like InstallTemplate, except that it writes template into a byte slice and returns it.
|
|
||||||
func ReadTemplate(src string, context map[string]string) ([]byte, error) {
|
|
||||||
// open the source file!
|
|
||||||
srcFile, err := resourceEmbed.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "Error opening source file %s", src)
|
|
||||||
}
|
|
||||||
defer srcFile.Close()
|
|
||||||
|
|
||||||
// and return it
|
|
||||||
bytes, _, err := unpack.UnpackTemplate(context, srcFile)
|
|
||||||
return bytes, err
|
|
||||||
}
|
|
||||||
11
env/component.go
vendored
11
env/component.go
vendored
|
|
@ -3,6 +3,7 @@ package env
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/embed"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -37,8 +38,14 @@ func (dis *Distillery) makeComponentStack(component Component, stack stack.Insta
|
||||||
stack.Dir = dis.getComponentPath(component)
|
stack.Dir = dis.getComponentPath(component)
|
||||||
|
|
||||||
name := component.Name()
|
name := component.Name()
|
||||||
stack.ContextResource = filepath.Join("resources", "compose", name)
|
|
||||||
stack.EnvFileResource = filepath.Join("resources", "templates", "docker-env", name)
|
// TODO: This writes out resources.
|
||||||
|
// Should migrate this directly!
|
||||||
|
if stack.Resources == nil {
|
||||||
|
stack.Resources = embed.ResourceEmbed
|
||||||
|
stack.ContextPath = filepath.Join("resources", "compose", name)
|
||||||
|
stack.EnvPath = filepath.Join("resources", "templates", "docker-env", name)
|
||||||
|
}
|
||||||
|
|
||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
env/component_dis.go
vendored
2
env/component_dis.go
vendored
|
|
@ -21,7 +21,7 @@ func (DisComponent) Name() string {
|
||||||
|
|
||||||
func (dis DisComponent) Stack() stack.Installable {
|
func (dis DisComponent) Stack() stack.Installable {
|
||||||
return dis.dis.makeComponentStack(dis, stack.Installable{
|
return dis.dis.makeComponentStack(dis, stack.Installable{
|
||||||
EnvFileContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"VIRTUAL_HOST": dis.dis.DefaultVirtualHost(),
|
"VIRTUAL_HOST": dis.dis.DefaultVirtualHost(),
|
||||||
"LETSENCRYPT_HOST": dis.dis.DefaultLetsencryptHost(),
|
"LETSENCRYPT_HOST": dis.dis.DefaultLetsencryptHost(),
|
||||||
"LETSENCRYPT_EMAIL": dis.dis.Config.CertbotEmail,
|
"LETSENCRYPT_EMAIL": dis.dis.Config.CertbotEmail,
|
||||||
|
|
|
||||||
3
env/component_resolver.go
vendored
3
env/component_resolver.go
vendored
|
|
@ -35,7 +35,7 @@ func (ResolverComponent) Name() string {
|
||||||
|
|
||||||
func (resolver ResolverComponent) Stack() stack.Installable {
|
func (resolver ResolverComponent) Stack() stack.Installable {
|
||||||
return resolver.dis.makeComponentStack(resolver, stack.Installable{
|
return resolver.dis.makeComponentStack(resolver, stack.Installable{
|
||||||
EnvFileContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"VIRTUAL_HOST": resolver.dis.DefaultVirtualHost(),
|
"VIRTUAL_HOST": resolver.dis.DefaultVirtualHost(),
|
||||||
"LETSENCRYPT_HOST": resolver.dis.DefaultLetsencryptHost(),
|
"LETSENCRYPT_HOST": resolver.dis.DefaultLetsencryptHost(),
|
||||||
"LETSENCRYPT_EMAIL": resolver.dis.Config.CertbotEmail,
|
"LETSENCRYPT_EMAIL": resolver.dis.Config.CertbotEmail,
|
||||||
|
|
@ -47,6 +47,7 @@ func (resolver ResolverComponent) Stack() stack.Installable {
|
||||||
"SELF_OVERRIDES_FILE": resolver.dis.Config.SelfOverridesFile,
|
"SELF_OVERRIDES_FILE": resolver.dis.Config.SelfOverridesFile,
|
||||||
"RESOLVER_CONFIG": resolver.ConfigPath(),
|
"RESOLVER_CONFIG": resolver.ConfigPath(),
|
||||||
},
|
},
|
||||||
|
TouchFiles: []string{resolver.ConfigName},
|
||||||
CopyContextFiles: []string{core.Executable},
|
CopyContextFiles: []string{core.Executable},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
env/component_self.go
vendored
2
env/component_self.go
vendored
|
|
@ -27,7 +27,7 @@ func (sc SelfComponent) Stack() stack.Installable {
|
||||||
}
|
}
|
||||||
|
|
||||||
return sc.dis.makeComponentStack(sc, stack.Installable{
|
return sc.dis.makeComponentStack(sc, stack.Installable{
|
||||||
EnvFileContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"VIRTUAL_HOST": sc.dis.DefaultVirtualHost(),
|
"VIRTUAL_HOST": sc.dis.DefaultVirtualHost(),
|
||||||
"LETSENCRYPT_HOST": sc.dis.DefaultLetsencryptHost(),
|
"LETSENCRYPT_HOST": sc.dis.DefaultLetsencryptHost(),
|
||||||
"LETSENCRYPT_EMAIL": sc.dis.Config.CertbotEmail,
|
"LETSENCRYPT_EMAIL": sc.dis.Config.CertbotEmail,
|
||||||
|
|
|
||||||
14
env/component_triplestore.go
vendored
14
env/component_triplestore.go
vendored
|
|
@ -13,8 +13,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/embed"
|
"github.com/FAU-CDI/wisski-distillery/embed"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/unpack"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/wait"
|
"github.com/FAU-CDI/wisski-distillery/internal/wait"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/tkw1536/goprogram/exit"
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
|
@ -154,10 +156,14 @@ func (ts TriplestoreComponent) Provision(name, domain, user, password string) er
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare the create repo request
|
// prepare the create repo request
|
||||||
createRepo, err := embed.ReadTemplate(filepath.Join("resources", "templates", "repository", "graphdb-repo.ttl"), map[string]string{
|
// TODO: Move this into a seperate file
|
||||||
"GRAPHDB_REPO": name,
|
createRepo, _, err := unpack.UnpackTemplate(
|
||||||
"INSTANCE_DOMAIN": domain,
|
map[string]string{
|
||||||
})
|
"GRAPHDB_REPO": name,
|
||||||
|
"INSTANCE_DOMAIN": domain,
|
||||||
|
},
|
||||||
|
fsx.OpenFS(filepath.Join("resources", "templates", "repository", "graphdb-repo.ttl"), embed.ResourceEmbed),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
env/component_web.go
vendored
2
env/component_web.go
vendored
|
|
@ -18,7 +18,7 @@ func (WebComponent) Name() string {
|
||||||
|
|
||||||
func (web WebComponent) Stack() stack.Installable {
|
func (web WebComponent) Stack() stack.Installable {
|
||||||
return web.dis.makeComponentStack(web, stack.Installable{
|
return web.dis.makeComponentStack(web, stack.Installable{
|
||||||
EnvFileContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DEFAULT_HOST": web.dis.Config.DefaultDomain,
|
"DEFAULT_HOST": web.dis.Config.DefaultDomain,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
12
env/instances.go
vendored
12
env/instances.go
vendored
|
|
@ -233,10 +233,10 @@ func (instance Instance) Stack() stack.Installable {
|
||||||
Stack: stack.Stack{
|
Stack: stack.Stack{
|
||||||
Dir: instance.FilesystemBase,
|
Dir: instance.FilesystemBase,
|
||||||
},
|
},
|
||||||
ContextResource: filepath.Join("resources", "compose", "barrel"),
|
ContextPath: filepath.Join("resources", "compose", "barrel"),
|
||||||
|
|
||||||
EnvFileResource: filepath.Join("resources", "templates", "docker-env", "barrel"),
|
EnvPath: filepath.Join("resources", "templates", "docker-env", "barrel"),
|
||||||
EnvFileContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DATA_PATH": filepath.Join(instance.FilesystemBase, "data"),
|
"DATA_PATH": filepath.Join(instance.FilesystemBase, "data"),
|
||||||
|
|
||||||
"SLUG": instance.Slug,
|
"SLUG": instance.Slug,
|
||||||
|
|
@ -263,10 +263,10 @@ func (instance Instance) ReserveStack() stack.Installable {
|
||||||
Stack: stack.Stack{
|
Stack: stack.Stack{
|
||||||
Dir: instance.FilesystemBase,
|
Dir: instance.FilesystemBase,
|
||||||
},
|
},
|
||||||
ContextResource: filepath.Join("resources", "compose", "reserve"),
|
ContextPath: filepath.Join("resources", "compose", "reserve"),
|
||||||
|
|
||||||
EnvFileResource: filepath.Join("resources", "templates", "docker-env", "reserve"),
|
EnvPath: filepath.Join("resources", "templates", "docker-env", "reserve"),
|
||||||
EnvFileContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"VIRTUAL_HOST": instance.Domain(),
|
"VIRTUAL_HOST": instance.Domain(),
|
||||||
|
|
||||||
"LETSENCRYPT_HOST": instance.dis.IfHttps(instance.Domain()),
|
"LETSENCRYPT_HOST": instance.dis.IfHttps(instance.Domain()),
|
||||||
|
|
|
||||||
31
internal/fsx/open.go
Normal file
31
internal/fsx/open.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package fsx
|
||||||
|
|
||||||
|
import "io/fs"
|
||||||
|
|
||||||
|
// OpenFS opens the named file in filesystem.
|
||||||
|
// If opening the file results in an error, returns [ErrFile].
|
||||||
|
func OpenFS(name string, fsys fs.FS) fs.File {
|
||||||
|
file, err := fsys.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return ErrFile{Err: err}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrFile implements a no-op [fs.File].
|
||||||
|
//
|
||||||
|
// Every operation will return an underlying error
|
||||||
|
type ErrFile struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrFile) Stat() (fs.FileInfo, error) {
|
||||||
|
return nil, err.Err
|
||||||
|
}
|
||||||
|
func (err ErrFile) Read([]byte) (int, error) {
|
||||||
|
return 0, err.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrFile) Close() error {
|
||||||
|
return err.Err
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/FAU-CDI/wisski-distillery/embed"
|
"github.com/FAU-CDI/wisski-distillery/embed"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/unpack"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/tkw1536/goprogram/stream"
|
"github.com/tkw1536/goprogram/stream"
|
||||||
)
|
)
|
||||||
|
|
@ -16,10 +17,14 @@ import (
|
||||||
type Installable struct {
|
type Installable struct {
|
||||||
Stack
|
Stack
|
||||||
|
|
||||||
ContextResource string // Path to the resource containing 'docker compose' context
|
// Installable enabled installing several resources from a (potentially embedded) filesystem.
|
||||||
|
//
|
||||||
EnvFileResource string // Path to the resource containing dynamically generated env file
|
// The Resources holds these, with appropriate resources specified below.
|
||||||
EnvFileContext map[string]string // Context of variables to replace in the env file
|
// These all refer to paths within the Resource filesystem.
|
||||||
|
Resources fs.FS
|
||||||
|
ContextPath string // the 'docker compose' stack context, containing e.g. 'docker-compose.yml'.
|
||||||
|
EnvPath string // the '.env' template, will be installed using [unpack.InstallTemplate].
|
||||||
|
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
|
||||||
|
|
||||||
|
|
@ -40,7 +45,7 @@ func (is Installable) Install(io stream.IOStream, context InstallationContext) e
|
||||||
// setup the base files
|
// setup the base files
|
||||||
if err := embed.InstallResource(
|
if err := embed.InstallResource(
|
||||||
is.Dir,
|
is.Dir,
|
||||||
is.ContextResource,
|
is.ContextPath,
|
||||||
func(dst, src string) {
|
func(dst, src string) {
|
||||||
io.Printf("[install] %s\n", dst)
|
io.Printf("[install] %s\n", dst)
|
||||||
},
|
},
|
||||||
|
|
@ -50,12 +55,13 @@ func (is Installable) Install(io stream.IOStream, context InstallationContext) e
|
||||||
|
|
||||||
// configure .env
|
// configure .env
|
||||||
envDest := filepath.Join(is.Dir, ".env")
|
envDest := filepath.Join(is.Dir, ".env")
|
||||||
if is.EnvFileResource != "" && is.EnvFileContext != nil {
|
if is.EnvPath != "" && is.EnvContext != nil {
|
||||||
io.Printf("[config] %s\n", envDest)
|
io.Printf("[config] %s\n", envDest)
|
||||||
if err := embed.InstallTemplate(
|
if err := unpack.InstallTemplate(
|
||||||
envDest,
|
envDest,
|
||||||
is.EnvFileResource,
|
is.EnvContext,
|
||||||
is.EnvFileContext,
|
is.EnvPath,
|
||||||
|
is.Resources,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
internal/unpack/dir.go
Normal file
82
internal/unpack/dir.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package unpack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstallDir installs the directory at src within fsys to dst.
|
||||||
|
//
|
||||||
|
// onInstallFile is called for each file or directory being installed.
|
||||||
|
//
|
||||||
|
// If the destination path does not exist, it is created using [os.MakeDirs]
|
||||||
|
// The directory is installed recursively.
|
||||||
|
func InstallDir(dst string, src string, fsys fs.FS, onInstallFile func(dst, src string)) error {
|
||||||
|
// open the source file
|
||||||
|
srcFile, err := fsys.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// stat it!
|
||||||
|
srcInfo, err := srcFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure it's a file!
|
||||||
|
if !srcInfo.IsDir() {
|
||||||
|
return errExpectedDirectoryButGotFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the hook (if any)
|
||||||
|
if onInstallFile != nil {
|
||||||
|
onInstallFile(dst, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the installation of the directory.
|
||||||
|
// the type cast should be safe.
|
||||||
|
return installDir(dst, srcInfo, srcFile.(fs.ReadDirFile), src, fsys, onInstallFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func installDir(dst string, srcInfo fs.FileInfo, srcFile fs.ReadDirFile, src string, fsys fs.FS, onInstallFile func(dst, src string)) error {
|
||||||
|
// create the destination
|
||||||
|
dstStat, dstErr := os.Stat(dst)
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(dstErr):
|
||||||
|
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
|
||||||
|
return errors.Wrapf(err, "Error creating destination directory %s", dst)
|
||||||
|
}
|
||||||
|
case dstErr != nil:
|
||||||
|
return errors.Wrapf(dstErr, "Error calling stat on destination %s", dst)
|
||||||
|
case !dstStat.IsDir():
|
||||||
|
return errors.Wrapf(errExpectedDirectoryButGotFile, "Error opening destination %s", dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(twiesing): We don't use fs.Walk here.
|
||||||
|
// If we did, we'd have to reconstruct relative paths.
|
||||||
|
// That would be very ugly!
|
||||||
|
|
||||||
|
// read the directory
|
||||||
|
entries, err := srcFile.ReadDir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Error reading source directory %s", srcFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over all the children
|
||||||
|
for _, entry := range entries {
|
||||||
|
if err := InstallResource(
|
||||||
|
filepath.Join(dst, entry.Name()),
|
||||||
|
filepath.Join(src, entry.Name()),
|
||||||
|
fsys,
|
||||||
|
onInstallFile,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
76
internal/unpack/file.go
Normal file
76
internal/unpack/file.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package unpack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstallFile installs the file from src into dst.
|
||||||
|
//
|
||||||
|
// If the destination path does not exist, it is created.
|
||||||
|
func InstallFile(dst string, src fs.File) error {
|
||||||
|
// stat it!
|
||||||
|
srcInfo, err := src.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is a directory, something went wrong!
|
||||||
|
if srcInfo.IsDir() {
|
||||||
|
return errExpectedFileButGotDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
// and store it there!
|
||||||
|
return installFile(dst, srcInfo, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func installFile(dst string, srcInfo fs.FileInfo, src fs.File) error {
|
||||||
|
// create the file using the right mode!
|
||||||
|
file, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// copy over the content!
|
||||||
|
_, err = io.Copy(file, src)
|
||||||
|
return errors.Wrapf(err, "Error writing to destination %s", dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// write the file!
|
||||||
|
return WriteTemplate(file, context, srcFile)
|
||||||
|
}
|
||||||
|
|
@ -3,119 +3,140 @@ package unpack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errExpectedFileButGotDirectory = errors.New("Expected a file, but got a directory")
|
// ts represents state of the template parser
|
||||||
|
type ts int
|
||||||
// UnpackTemplate unpacks the given file template and template
|
|
||||||
func UnpackTemplate(context map[string]string, src fs.File) ([]byte, fs.FileMode, error) {
|
|
||||||
// stat the source file to install
|
|
||||||
srcStat, srcErr := src.Stat()
|
|
||||||
if srcErr != nil {
|
|
||||||
return nil, 0, errors.Wrapf(srcErr, "Error calling stat on source")
|
|
||||||
}
|
|
||||||
|
|
||||||
// should not be a directory
|
|
||||||
if srcStat.IsDir() {
|
|
||||||
return nil, 0, errors.Wrapf(errExpectedFileButGotDirectory, "Error calling stat on source %s", srcStat.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// read all the bytes into a buffer
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
WriteTemplate(&buffer, context, src)
|
|
||||||
return buffer.Bytes(), srcStat.Mode(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type templateMode int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
templateModeNormal templateMode = iota // normal mode
|
tsGobble ts = iota // gobble into dst
|
||||||
templateModeDollar // saw '$'
|
tsSawDollar // saw a '$'
|
||||||
templateModeOpen // saw '${'
|
tsGobbleVar // gobble into var
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MissingTemplateKeyError indicates [WriteTemplate] found missing keys in the context
|
||||||
|
type MissingTemplateKeyError struct {
|
||||||
|
Keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mtke MissingTemplateKeyError) Error() string {
|
||||||
|
return fmt.Sprintf("missing template keys from context: %v", mtke.Keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnusuedTemplateKeyError indicates [WriteTemplate] found unusued keys in the context
|
||||||
|
type UnusuedTemplateKeyError struct {
|
||||||
|
Keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (utke UnusuedTemplateKeyError) Error() string {
|
||||||
|
return fmt.Sprintf("unused template keys from context: %v", utke.Keys)
|
||||||
|
}
|
||||||
|
|
||||||
// WriteTemplate writes the template defined by src with the given context into reader.
|
// WriteTemplate writes the template defined by src with the given context into reader.
|
||||||
//
|
//
|
||||||
// To run the template, variables of the form ${NAME} are replaced with their corresponding value from the context.
|
// To run the template, variables of the form ${NAME} are replaced with their corresponding value from the context.
|
||||||
//
|
//
|
||||||
// Extra or missing variables from the context are an error.
|
// If an underlying read or write fails, it is returned as is.
|
||||||
|
// Missing template keys return a [MissingTemplateKeyError], but are replaced with the empty string.
|
||||||
|
// Unused template keys return a [UnusuedTemplateKeyError], but are replaced with the empty string.
|
||||||
|
//
|
||||||
|
// Reader / Writer errors are always returned first; next missing template keys, and finally unused template keys.
|
||||||
func WriteTemplate(dst io.Writer, context map[string]string, src io.Reader) error {
|
func WriteTemplate(dst io.Writer, context map[string]string, src io.Reader) error {
|
||||||
// keep track of context keys that have not been used
|
|
||||||
unuusedContext := make(map[string]struct{}, len(context))
|
// We keep track of contect keys that have not been used.
|
||||||
|
//
|
||||||
|
// We first fill the map with all the keys from the context.
|
||||||
|
// Then when we use a key, we delete it from the map.
|
||||||
|
// If there are any keys left at the end of the replacement, that is an error.
|
||||||
|
unusedKeys := make(map[string]struct{}, len(context))
|
||||||
for key := range context {
|
for key := range context {
|
||||||
unuusedContext[key] = struct{}{}
|
unusedKeys[key] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(src) // a new reader
|
// When we encounter a missing key, put it into this map.
|
||||||
var missingKeyErr error // error for missing keys
|
// This is so that we can build an error message below.
|
||||||
var builder strings.Builder // holding variable names
|
missingKeys := make(map[string]struct{}, 0)
|
||||||
mode := templateModeNormal // the current mode of the reader
|
|
||||||
|
// We use a new bufio reader to read data from the input.
|
||||||
|
// This is a cheap trick to get a ReadRune() method.
|
||||||
|
reader := bufio.NewReader(src)
|
||||||
|
|
||||||
|
//
|
||||||
|
// MAIN PARSING LOOP
|
||||||
|
//
|
||||||
|
|
||||||
|
// start out in gobble mode!
|
||||||
|
mode := tsGobble
|
||||||
|
|
||||||
|
// keep track of variable names
|
||||||
|
var varB strings.Builder
|
||||||
|
|
||||||
parseloop:
|
parseloop:
|
||||||
for {
|
for {
|
||||||
r, _, err := reader.ReadRune()
|
r, _, err := reader.ReadRune()
|
||||||
switch {
|
switch {
|
||||||
case err == io.EOF:
|
case err == io.EOF:
|
||||||
/* finished the source, see below */
|
// finished parsing the source
|
||||||
break parseloop
|
break parseloop
|
||||||
case err != nil:
|
case err != nil:
|
||||||
/* something went wrong */
|
// the reader broke
|
||||||
return err
|
return err
|
||||||
|
|
||||||
case mode == templateModeNormal && r == '$':
|
case mode == tsGobble && r == '$':
|
||||||
// saw a '$' in normal mode
|
// saw a '$' in gobble mode
|
||||||
// => switch to the dollar mode
|
mode = tsSawDollar
|
||||||
mode = templateModeDollar
|
case mode == tsGobble:
|
||||||
case mode == templateModeNormal:
|
// normal gobbleing
|
||||||
// saw anything else
|
// => pass it through
|
||||||
// => just pass it through
|
|
||||||
if _, err := dst.Write([]byte{byte(r)}); err != nil {
|
if _, err := dst.Write([]byte{byte(r)}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case mode == templateModeDollar && r == '{':
|
case mode == tsSawDollar && r == '{':
|
||||||
// saw '{', following the '$'
|
// saw '{', following the '$'
|
||||||
// => read everything else into the buffer
|
// => read everything else into the buffer
|
||||||
mode = templateModeOpen
|
mode = tsGobbleVar
|
||||||
case mode == templateModeDollar && r == '$':
|
case mode == tsSawDollar && r == '$':
|
||||||
// saw a '$' following the '$'
|
// saw a '$' following the '$'
|
||||||
// => write the first '$', and handle the case $${stuff}
|
// => write the first '$', and handle the case $${stuff}
|
||||||
if _, err := dst.Write([]byte("$")); err != nil {
|
if _, err := dst.Write([]byte("$")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case mode == templateModeDollar:
|
case mode == tsSawDollar:
|
||||||
// saw anything else following the '$'
|
// saw anything else following the '$'
|
||||||
// => write both back and switch back to normal mode
|
// => write both back and switch back to gobble mode
|
||||||
if _, err := dst.Write([]byte{byte('$'), byte(r)}); err != nil {
|
if _, err := dst.Write([]byte{byte('$'), byte(r)}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mode = templateModeNormal
|
mode = tsGobble
|
||||||
|
|
||||||
case mode == templateModeOpen && r != '}':
|
case mode == tsGobbleVar && r != '}':
|
||||||
// saw anything except for closing bracket
|
// saw anything except for closing bracket
|
||||||
// => keep it in the buffer
|
// => keep it in the buffer
|
||||||
if _, err := builder.WriteRune(r); err != nil {
|
if _, err := varB.WriteRune(r); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case mode == templateModeOpen:
|
case mode == tsGobbleVar:
|
||||||
// saw a closing '}' inside the open mode
|
// saw a closing '}' inside tsGobbleVar mode
|
||||||
// => use the variable
|
// => use the variable
|
||||||
|
|
||||||
name := builder.String()
|
name := varB.String()
|
||||||
delete(unuusedContext, name) // mark the variable as used
|
|
||||||
|
|
||||||
// get the variable from the context
|
// get the variable from the context
|
||||||
value, ok := context[name]
|
value, ok := context[name]
|
||||||
if missingKeyErr != nil && !ok {
|
|
||||||
missingKeyErr = errors.Errorf("key %s missing in context", name)
|
delete(unusedKeys, name) // mark the variable as used!
|
||||||
|
if !ok {
|
||||||
|
// store unusued variables
|
||||||
|
missingKeys[name] = struct{}{}
|
||||||
|
value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the replacement into the string
|
// write the replacement into the string
|
||||||
|
|
@ -124,48 +145,51 @@ parseloop:
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset the builder and go back into normal mode
|
// reset the builder and go back into normal mode
|
||||||
builder.Reset()
|
varB.Reset()
|
||||||
mode = templateModeNormal
|
mode = tsGobble
|
||||||
|
|
||||||
default:
|
|
||||||
panic("never reached")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup at end of input
|
//
|
||||||
|
// CLEANUP UNUSUED INPUT
|
||||||
|
//
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case templateModeNormal:
|
case tsSawDollar:
|
||||||
// => everything is fine
|
|
||||||
case templateModeDollar:
|
|
||||||
// we had a '$', but no '{'
|
// we had a '$', but no '{'
|
||||||
// => write the trailing '$' into dest
|
// => write the trailing '$' into dest
|
||||||
if _, err := dst.Write([]byte("$")); err != nil {
|
if _, err := dst.Write([]byte("$")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case templateModeOpen:
|
case tsGobbleVar:
|
||||||
// we had a "${", followed by somthing unclosed
|
// we had a "${", followed by somthing unclosed
|
||||||
// => write everything back into the dst
|
// => write everything back into the dst
|
||||||
if _, err := dst.Write([]byte("${")); err != nil {
|
if _, err := dst.Write([]byte("${")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := io.WriteString(dst, builder.String()); err != nil {
|
if _, err := io.WriteString(dst, varB.String()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
panic("never reached")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if there was a missing key!
|
// Check if there were missing template keys.
|
||||||
if missingKeyErr != nil {
|
// If so, we sort them and return an appropriate error.
|
||||||
return missingKeyErr
|
if len(missingKeys) != 0 {
|
||||||
}
|
keys := maps.Keys(unusedKeys)
|
||||||
|
|
||||||
// check if there was an unused key!
|
|
||||||
if len(unuusedContext) != 0 {
|
|
||||||
keys := maps.Keys(unuusedContext)
|
|
||||||
slices.Sort(keys)
|
slices.Sort(keys)
|
||||||
return errors.Errorf("additional keys %s in context", strings.Join(keys, ","))
|
return MissingTemplateKeyError{
|
||||||
|
Keys: keys,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there were unused template keys.
|
||||||
|
// If so, we sort them and return an appropriate error.
|
||||||
|
if len(unusedKeys) != 0 {
|
||||||
|
keys := maps.Keys(unusedKeys)
|
||||||
|
slices.Sort(keys)
|
||||||
|
return UnusuedTemplateKeyError{
|
||||||
|
Keys: keys,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
65
internal/unpack/unpack.go
Normal file
65
internal/unpack/unpack.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Package unpack unpacks files and templates to a target directory
|
||||||
|
package unpack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errExpectedFileButGotDirectory = errors.New("expected a file, but got a directory")
|
||||||
|
var errExpectedDirectoryButGotFile = errors.New("expected a directory, but got a file")
|
||||||
|
|
||||||
|
// InstallResource installs the resource at src within fsys to dst.
|
||||||
|
//
|
||||||
|
// OnInstallFile is called for each source and destination file.
|
||||||
|
// OnInstallFile may be nil.
|
||||||
|
//
|
||||||
|
// See [InstallDir] or [InstallFile].
|
||||||
|
func InstallResource(dst string, src string, fsys fs.FS, onInstallFile func(dst, src string)) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the hook (if any)
|
||||||
|
if onInstallFile != nil {
|
||||||
|
onInstallFile(dst, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a directory, so the cast is safe!
|
||||||
|
if srcInfo.IsDir() {
|
||||||
|
return installDir(dst, srcInfo, srcFile.(fs.ReadDirFile), src, fsys, onInstallFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a regular file!
|
||||||
|
return installFile(dst, srcInfo, srcFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpackTemplate unpacks the given file template and template.
|
||||||
|
// See [WriteTemplate] for possible errors.
|
||||||
|
func UnpackTemplate(context map[string]string, src fs.File) ([]byte, fs.FileMode, error) {
|
||||||
|
// stat the source file to install
|
||||||
|
srcStat, srcErr := src.Stat()
|
||||||
|
if srcErr != nil {
|
||||||
|
return nil, 0, srcErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not be a directory
|
||||||
|
if srcStat.IsDir() {
|
||||||
|
return nil, 0, errExpectedFileButGotDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
// read all the bytes into a buffer
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err := WriteTemplate(&buffer, context, src)
|
||||||
|
return buffer.Bytes(), srcStat.Mode(), err
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue