refactor: make startup more resilient
Some checks failed
continuous-integration/drone/push Build is failing

- add in-memory services for vault and state store
- improve vault security by erasing passphrase on shutdown
- properly handle when project directory wasn't detected
- enable to run many commands even though there's no active project
This commit is contained in:
Peter 2023-09-25 15:32:44 +02:00
parent 3de7016a50
commit 364a0e9bab
No known key found for this signature in database
19 changed files with 321 additions and 194 deletions

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
@ -45,7 +46,10 @@ import (
"github.com/spf13/cobra"
)
var cwd string
var (
cwd string
ErrStateStoreNotInitialized = errors.New("state DB not initialized")
)
func init() {
if wd, err := os.Getwd(); err != nil {
@ -79,15 +83,8 @@ func NewApp(ctx context.Context) *App {
loggingCfg: logging.NewConfig(),
}
app.buildrCfg = lazy.NewLazyCtx(ctx, app.prepareBuildrConfig)
app.services.vault = lazy.NewLazy(app.appCfg.InitVault)
app.services.stateDB = lazy.NewLazyCtx(ctx, app.appCfg.InitState)
app.services.stateStore = lazy.NewLazy(app.prepareStateStore)
app.services.cache = lazy.NewLazy(app.prepareCache)
app.services.repo = lazy.NewLazyCtx(ctx, app.prepareModulesRepo)
app.rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
return app.Init()
return app.Init(cmd.Context())
}
app.rootCmd.PersistentPostRunE = func(*cobra.Command, []string) error {
@ -112,7 +109,7 @@ func NewApp(ctx context.Context) *App {
//nolint:contextcheck // there's no context to pass here
app.rootCmd.AddCommand(
ModulesCommand(app, app, manApp, app),
ModulesCommand(app, manApp, app),
PluginsCommand(NewPluginApp(app, app.pluginMgr, app)),
VaultCommand(NewVaultApp(app, app)),
ServerCommand(NewServerApp(app)),
@ -131,12 +128,11 @@ var _ ModuleCommander = (*App)(nil)
type appServices struct {
registry *modules.TypeRegistry
vault *lazy.Lazy[*vault.Vault]
vault *vault.Vault
dockerClient *lazy.Lazy[*client.Client]
ignorer *ignore.Ignorer
stateStore *lazy.Lazy[state.Store]
cache *lazy.Lazy[state.Cache]
stateDB *lazy.Lazy[*state.DB]
cache state.Cache
stateDB *state.DB
diagsWriter hcl2.DiagnosticWriter
repo *lazy.Lazy[*modules.Repository]
}
@ -155,23 +151,22 @@ type App struct {
pluginMgr *plugins.Manager
loggingCfg logging.Config
appCfg AppConfig
toClose []io.Closer
}
func (a *App) SetVault(v *vault.Vault) {
a.services.vault.Set(v)
a.services.vault = v
}
func (a *App) Vault() (*vault.Vault, error) {
return a.services.vault.Get()
return a.services.vault, nil
}
func (a *App) PluginsRepo() (state.Plugins, error) {
s, err := a.services.stateDB.Get()
if err != nil {
return nil, err
if a.services.stateDB == nil {
return nil, ErrStateStoreNotInitialized
}
return s.Plugins, nil
return a.services.stateDB.Plugins, nil
}
func (a *App) TypeRegistry() *modules.TypeRegistry {
@ -187,30 +182,69 @@ func (a *App) RunWithArgs(ctx context.Context, args ...string) error {
return a.rootCmd.ExecuteContext(ctx)
}
func (a *App) Init() (err error) {
slog.SetDefault(a.loggingCfg.Logger())
func (a *App) Init(ctx context.Context) (err error) {
logger, closer, err := a.loggingCfg.Logger()
if err != nil {
return err
}
a.toClose = append(a.toClose, closer)
slog.SetDefault(logger)
if a.recorder, err = a.appCfg.Profiling.Setup(); err != nil {
return err
}
if err := a.appCfg.InitPaths(cwd); err != nil {
return err
if !errors.Is(err, ErrFailedToFindDirectory) {
return err
}
}
if a.services.ignorer, err = a.appCfg.Ignorer(); err != nil {
return err
}
if a.services.vault, err = a.appCfg.InitVault(); err != nil {
return err
}
if a.services.stateDB, err = a.appCfg.InitState(ctx); err != nil {
return err
}
a.services.cache = state.NewStateCache(a.appCfg.Cache.TTL, a.services.stateDB.State)
a.pluginMgr.Init(a.services.stateDB.Plugins, a.appCfg.Cache.Directory)
if err := a.pluginMgr.Register(ctx, a.services.registry); err != nil {
return err
}
// init these in the background for better startup performance
a.vcs = lazy.NewFuture[any](a.appCfg.ParseVCSInfo)
a.repoDetails = lazy.NewFuture[repo.Repo](a.prepareRepoMetaInfo)
a.buildrCfg = lazy.NewLazyCtx(ctx, a.prepareBuildrConfig)
a.services.repo = lazy.NewLazyCtx(ctx, a.prepareModulesRepo)
return nil
}
func (a *App) Shutdown() error {
return errors.Join(a.persistVaultState(), a.recorder.Close(), a.services.dockerClient.Close())
shutdownErrors := make([]error, 0)
if err := a.persistVaultState(); err != nil {
shutdownErrors = append(shutdownErrors, err)
}
a.toClose = append(a.toClose, a.recorder, a.services.dockerClient)
for _, c := range a.toClose {
if err := c.Close(); err != nil {
shutdownErrors = append(shutdownErrors, c.Close())
}
}
return errors.Join(shutdownErrors...)
}
func (a *App) AppConfig() AppConfig {
@ -258,8 +292,8 @@ func (a *App) RunModule(ctx context.Context, cat modules.Category, name string)
}
factory := execution.NewTaskFactory(
execution.WithProvider(local.Provider(a.services.stateStore.MustGet())),
execution.WithProvider(container.Provider(orchestratorLazy, modulesRepo, a.services.stateStore.MustGet())),
execution.WithProvider(local.Provider(a.services.stateDB.State)),
execution.WithProvider(container.Provider(orchestratorLazy, modulesRepo, a.services.stateDB.State)),
)
plan, err := execution.NewPlanFor(cat, name, modulesRepo, factory)
@ -274,8 +308,8 @@ func (a *App) RunModule(ctx context.Context, cat modules.Category, name string)
return plan.Execute(ctx, execution.Spec{
ProjectRoot: a.appCfg.ProjectRoot,
CacheDirectory: a.appCfg.Cache.Directory,
BinariesDirectory: buildrCfg.BinariesDirectory,
CacheDirectory: buildrCfg.CacheDirectory,
OutDirectory: buildrCfg.OutDirectory,
LogsDirectory: buildrCfg.LogsDirectory,
LogToStdErr: buildrCfg.LogToStderr,
@ -355,8 +389,8 @@ func (a *App) prepareRepoMetaInfo() (repo.Repo, error) {
}
func (a *App) persistVaultState() error {
instance, err := a.services.vault.Get()
if err != nil || instance == nil {
instance := a.services.vault
if !instance.IsInitialized() || instance.IsInMemory() {
return nil
}
@ -374,24 +408,13 @@ func (a *App) persistVaultState() error {
return encoder.Encode(instance)
}
func (a *App) prepareStateStore() (state.Store, error) {
db, err := a.services.stateDB.Get()
if err != nil {
return nil, err
}
return db.State, nil
}
func (a *App) prepareCache() (state.Cache, error) {
db, err := a.services.stateDB.Get()
if err != nil {
return nil, err
}
return state.NewStateCache(a.appCfg.Cache.TTL, db.State), nil
}
func (a *App) prepareBuildrConfig(ctx context.Context) (*config.Buildr, error) {
parser := hcl.NewParser(slog.Default(), a.appCfg.BuildRFS())
buildrFS, err := a.appCfg.BuildRFS()
if err != nil {
return nil, err
}
parser := hcl.NewParser(slog.Default(), buildrFS)
slog.Debug("Reading files", "buildr_directory", a.appCfg.BuildRDirectory)
if err := parser.ReadFiles(ctx); err != nil {
@ -408,30 +431,19 @@ func (a *App) prepareBuildrConfig(ctx context.Context) (*config.Buildr, error) {
},
}
vaultInstance, err := a.services.vault.Get()
if err != nil {
return nil, err
}
a.parsingState.currentEvalCtx = hcl.BasicContext(vaultInstance)
a.parsingState.currentEvalCtx = hcl.BasicContext(a.services.vault)
if err := parser.Parse(a.parsingState.currentEvalCtx, &buildrCfg); err != nil {
return nil, err
}
a.services.diagsWriter = parser.DiagsWriter()
if err := buildrCfg.SetupDirectories(a.appCfg.BuildRDirectory(), a.appCfg.ProjectRoot, a.appCfg.Execution.CleanDirectories); err != nil {
return nil, err
}
stateDB, err := a.services.stateDB.Get()
buildrDir, err := a.appCfg.BuildRDirectory()
if err != nil {
return nil, err
}
a.pluginMgr.Init(stateDB.Plugins, buildrCfg.CacheDirectory)
if err := a.pluginMgr.Register(ctx, a.services.registry); err != nil {
if err := buildrCfg.SetupDirectories(buildrDir, a.appCfg.ProjectRoot, a.appCfg.Execution.CleanDirectories); err != nil {
return nil, err
}
@ -441,18 +453,13 @@ func (a *App) prepareBuildrConfig(ctx context.Context) (*config.Buildr, error) {
}
func (a *App) prepareModulesRepo(ctx context.Context) (*modules.Repository, error) {
vaultInstance, err := a.services.vault.Get()
if err != nil {
return nil, err
}
// call this early to ensure the parsing state is initialized
buildrCfg, err := a.buildrCfg.Get()
if err != nil {
return nil, err
}
evalCtx := hcl.FullContext(ctx, a.parsingState.currentEvalCtx, vaultInstance, a.services.cache.MustGet())
evalCtx := hcl.FullContext(ctx, a.parsingState.currentEvalCtx, a.services.vault, a.services.cache)
var rawSpec hcl.ModulesSpec
if diags := gohcl.DecodeBody(a.parsingState.parsingRemainder, evalCtx, &rawSpec); diags.HasErrors() {

View file

@ -11,7 +11,6 @@ import (
func ModulesArgsProviderFor(
registryAcc TypeRegistryAccessor,
buildrConfigAccessor BuildrConfigAccessor,
cat modules.Category,
) KnownModulesArgProvider {
return KnownModulesArgProviderFunc(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@ -19,10 +18,6 @@ func ModulesArgsProviderFor(
return nil, cobra.ShellCompDirectiveNoFileComp
}
if _, err := buildrConfigAccessor.BuildrConfig(); err != nil {
return nil, cobra.ShellCompDirectiveError
}
knownModules := registryAcc.TypeRegistry().Inventory()[cat]
filtered := make([]string, 0, len(knownModules))
toComplete = strings.ToLower(toComplete)

View file

@ -24,14 +24,16 @@ import (
)
const (
defaultCacheTTL = 15 * time.Minute
buildrDirectoryName = ".buildr"
buildrProjectRootEnv = "BUILDR_PROJECT_ROOT"
defaultCacheTTL = 15 * time.Minute
buildrDirectoryName = ".buildr"
buildrProjectRootEnv = "BUILDR_PROJECT_ROOT"
defaultDirectoryPermissions = 0o755
)
var (
ErrFailedToFindDirectory = errors.New("failed to find directory")
ErrNoProjectDir = errors.New("no project directory detected")
ErrNoVCSRootDetected = errors.New("no VCS root detected")
ErrFailedToFindDirectory = errors.New("failed to find directory")
)
type AppConfig struct {
@ -45,8 +47,11 @@ type AppConfig struct {
VCSType vcs.Type
State struct{ FilePath string }
Profiling profiling.Config
Cache struct{ TTL time.Duration }
Execution struct {
Cache struct {
Directory string
TTL time.Duration
}
Execution struct {
LogToStderr bool
CleanDirectories bool
}
@ -88,21 +93,39 @@ func (c *AppConfig) ParseVCSInfo() (vcsDetails any, err error) {
}
}
func (c *AppConfig) BuildRFS() fs.FS {
return os.DirFS(c.BuildRDirectory())
func (c *AppConfig) BuildRFS() (fs.FS, error) {
if dir, err := c.BuildRDirectory(); err != nil {
return nil, err
} else {
return os.DirFS(dir), nil
}
}
func (c *AppConfig) BuildRDirectory() string {
return filepath.Join(c.ProjectRoot, buildrDirectoryName)
func (c *AppConfig) BuildRDirectory() (string, error) {
if c.ProjectRoot == "" {
return "", ErrNoProjectDir
}
return filepath.Join(c.ProjectRoot, buildrDirectoryName), nil
}
func (c *AppConfig) Ignorer() (*ignore.Ignorer, error) {
if c.ProjectRoot == "" {
return ignore.NewIgnorer(cwd)
}
return ignore.NewIgnorer(c.ProjectRoot, c.Vault.FilePath)
}
func (c *AppConfig) InitPaths(from string) (err error) {
var projectRoot string
if ucd, err := os.UserCacheDir(); err != nil {
return err
} else {
c.Cache.Directory = filepath.Join(ucd, "buildr")
if err := os.MkdirAll(c.Cache.Directory, defaultDirectoryPermissions); err != nil {
return err
}
}
var projectRoot string
if root, ok := os.LookupEnv(buildrProjectRootEnv); ok {
projectRoot = root
} else if root, err = c.findDirectory(from, buildrDirectoryName); err != nil {
@ -126,13 +149,12 @@ func (c *AppConfig) InitPaths(from string) (err error) {
return nil
}
//nolint:nilnil // if state is empty we don't care
func (c *AppConfig) InitState(ctx context.Context) (*state.DB, error) {
if stateFilePath := c.State.FilePath; stateFilePath != "" {
if stateFilePath := c.State.FilePath; c.ProjectRoot != "" && stateFilePath != "" {
return state.NewDB(ctx, c.State.FilePath)
}
return nil, nil
return state.NewInMemDB(ctx)
}
func (c *AppConfig) InitVault() (*vault.Vault, error) {
@ -142,17 +164,26 @@ func (c *AppConfig) InitVault() (*vault.Vault, error) {
vaultOpts = append(vaultOpts, vault.WithPassphrase(pw))
} else if path := c.Vault.PassphraseFile; path != "" {
vaultOpts = append(vaultOpts, vault.WithPassphraseFile(path))
} else {
return vault.NewInMemVault()
}
if filePath := c.Vault.FilePath; filePath != "" {
inst, err := vault.NewVault(vaultOpts...)
if err != nil {
return nil, err
}
if filePath := c.Vault.FilePath; c.ProjectRoot != "" && filePath != "" {
if !filepath.IsAbs(c.Vault.FilePath) {
filePath = filepath.Join(c.ProjectRoot, filePath)
}
vaultOpts = append(vaultOpts, vault.WithLoadFromPath(filePath))
if err := inst.LoadFromFile(filePath); err != nil {
return nil, err
}
}
return vault.NewVault(vaultOpts...)
return inst, nil
}
func (c *AppConfig) Flags() *flag.FlagSet {

View file

@ -74,7 +74,6 @@ func (m *ManConfig) Pager(ctx context.Context, title string) (Pager, error) {
func ModuleManCommand(
category modules.Category,
cmder ManCommander,
buildrConfigAccessor BuildrConfigAccessor,
argsProvider KnownModulesArgProvider,
manCfg *ManConfig,
) *cobra.Command {
@ -86,9 +85,6 @@ func ModuleManCommand(
Args: cobra.ExactArgs(1),
ValidArgsFunction: argsProvider.ValidModulesArgs,
RunE: func(cmd *cobra.Command, args []string) (err error) {
if _, err = buildrConfigAccessor.BuildrConfig(); err != nil {
return err
}
p, err := manCfg.Pager(cmd.Context(), fmt.Sprintf("Manual - %s/%s", modules.CategoryName(category), args[0]))
if err != nil {
return err
@ -102,7 +98,6 @@ func ModuleManCommand(
func ManCmd(
cmder ManCommander,
registryAcc TypeRegistryAccessor,
buildrConfigAccessor BuildrConfigAccessor,
) *cobra.Command {
var manCfg ManConfig
manCmd := &cobra.Command{
@ -123,8 +118,7 @@ func ManCmd(
return ModuleManCommand(
c,
cmder,
buildrConfigAccessor,
ModulesArgsProviderFor(registryAcc, buildrConfigAccessor, c),
ModulesArgsProviderFor(registryAcc, c),
&manCfg,
)
})...)

View file

@ -12,7 +12,6 @@ import (
func ModulesCommand(
registryAcc TypeRegistryAccessor,
buildrConfigAccessor BuildrConfigAccessor,
manCmder ManCommander,
moduleCmder BootstrapModuleCommander,
) *cobra.Command {
@ -25,15 +24,14 @@ func ModulesCommand(
}
cmd.AddCommand(
ModulesListCommand(registryAcc, buildrConfigAccessor, os.Stdout),
ManCmd(manCmder, registryAcc, buildrConfigAccessor),
ModulesListCommand(registryAcc, os.Stdout),
ManCmd(manCmder, registryAcc),
NewCmd(
slices.Map(modules.Categories(), func(c modules.Category) *cobra.Command {
return BootstrapModuleCmd(
c,
moduleCmder,
buildrConfigAccessor,
ModulesArgsProviderFor(registryAcc, buildrConfigAccessor, c),
ModulesArgsProviderFor(registryAcc, c),
WithShort(fmt.Sprintf("Bootstrap %s module", modules.CategoryName(c))),
)
})...,

View file

@ -9,7 +9,6 @@ import (
func ModulesListCommand(
registryAcc TypeRegistryAccessor,
buildrConfigaccessor BuildrConfigAccessor,
out io.Writer,
) *cobra.Command {
return &cobra.Command{
@ -20,10 +19,6 @@ func ModulesListCommand(
SilenceErrors: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if _, err := buildrConfigaccessor.BuildrConfig(); err != nil {
return err
}
table := tablewriter.NewWriter(out)
table.SetHeader([]string{"Category", "Module"})
table.SetBorder(false)

View file

@ -20,7 +20,6 @@ func NewCmd(subCommands ...*cobra.Command) *cobra.Command {
func BootstrapModuleCmd(
category modules.Category,
cmder BootstrapModuleCommander,
buildrConfigAccessor BuildrConfigAccessor,
argsProvider KnownModulesArgProvider,
opts ...ModuleCommandOption,
) *cobra.Command {
@ -32,9 +31,6 @@ func BootstrapModuleCmd(
ValidArgsFunction: argsProvider.ValidModulesArgs,
Args: cobra.RangeArgs(1, argsWithModuleName),
RunE: func(cmd *cobra.Command, args []string) error {
if _, err := buildrConfigAccessor.BuildrConfig(); err != nil {
return err
}
var (
typeName = args[0]
moduleName string

View file

@ -15,10 +15,9 @@ type PluginReference struct {
}
type Buildr struct {
BinariesDirectory string `hcl:"bin_dir,optional"`
OutDirectory string `hcl:"out_dir,optional"`
LogsDirectory string `hcl:"logs_dir,optional"`
CacheDirectory string
BinariesDirectory string `hcl:"bin_dir,optional"`
OutDirectory string `hcl:"out_dir,optional"`
LogsDirectory string `hcl:"logs_dir,optional"`
Plugins []PluginReference `hcl:"plugin,block"`
LogToStderr bool `hcl:"log_to_stdout,optional"`
}
@ -37,16 +36,6 @@ func (c Buildr) PluginURLs() (pluginUrls []*url.URL, err error) {
}
func (c *Buildr) SetupDirectories(buildRDir, projectRoot string, cleanDirectories bool) error {
if ucd, err := os.UserCacheDir(); err != nil {
return err
} else {
c.CacheDirectory = filepath.Join(ucd, "buildr")
}
if err := createCleanDir(c.CacheDirectory, false); err != nil {
return err
}
if c.BinariesDirectory == "" {
c.BinariesDirectory = filepath.Join(buildRDir, "bin")
} else if !filepath.IsAbs(c.BinariesDirectory) {

View file

@ -0,0 +1,15 @@
package ioutils
import "io"
func NoOpWriteCloser(writer io.Writer) io.WriteCloser {
return &noOpWriteCloser{writer}
}
type noOpWriteCloser struct {
io.Writer
}
func (*noOpWriteCloser) Close() error {
return nil
}

View file

@ -2,18 +2,53 @@ package logging
import (
"flag"
"io"
"log/slog"
"os"
"strings"
"code.icb4dc0.de/buildr/buildr/internal/ioutils"
)
var _ flag.Value = (*LogOutput)(nil)
type LogOutput string
func (o *LogOutput) Set(s string) error {
*o = LogOutput(s)
return nil
}
func (o *LogOutput) String() string {
return string(*o)
}
func (o *LogOutput) Open() (io.WriteCloser, error) {
switch strings.ToLower(o.String()) {
case "", "stderr":
return ioutils.NoOpWriteCloser(os.Stderr), nil
case "stdout":
return ioutils.NoOpWriteCloser(os.Stdout), nil
default:
return os.Create(string(*o))
}
}
const (
LogOutputStderr LogOutput = "stderr"
LogOutputStdout LogOutput = "stdout"
)
func NewConfig() Config {
return Config{
LogLevel: new(slog.LevelVar),
LogLevel: new(slog.LevelVar),
LogOutput: LogOutputStderr,
}
}
type Config struct {
LogLevel *slog.LevelVar
LogOutput LogOutput
AddSource bool
}
@ -32,14 +67,25 @@ func (c *Config) Flags() *flag.FlagSet {
"Enable to get detailed information where the log was produced",
)
flagSet.Var(
&c.LogOutput,
"log.output",
"Configure where the log output should be written to [stderr, stdout, any file path]",
)
return flagSet
}
func (c *Config) Logger() *slog.Logger {
func (c *Config) Logger() (*slog.Logger, io.Closer, error) {
slogOptions := slog.HandlerOptions{
AddSource: c.AddSource,
Level: c.LogLevel.Level(),
}
return slog.New(slog.NewTextHandler(os.Stderr, &slogOptions))
out, err := c.LogOutput.Open()
if err != nil {
return nil, nil, err
}
return slog.New(slog.NewTextHandler(out, &slogOptions)), out, nil
}

View file

@ -21,7 +21,7 @@ type AesGcmEncryption struct {
keyDeriver KeyDeriver
}
func (a AesGcmEncryption) Encrypt(plainText []byte, passphrase string) (cipherText, salt, nonce []byte, err error) {
func (a AesGcmEncryption) Encrypt(plainText []byte, passphrase []byte) (cipherText, salt, nonce []byte, err error) {
key, salt := a.keyDeriver.DeriveKey(passphrase, nil)
block, err := aes.NewCipher(key)
if err != nil {
@ -41,7 +41,7 @@ func (a AesGcmEncryption) Encrypt(plainText []byte, passphrase string) (cipherTe
return cipherText, salt, nonce, nil
}
func (a AesGcmEncryption) Decrypt(cipherText []byte, passphrase string, salt, nonce []byte) (plainText []byte, err error) {
func (a AesGcmEncryption) Decrypt(cipherText []byte, passphrase []byte, salt, nonce []byte) (plainText []byte, err error) {
key, _ := a.keyDeriver.DeriveKey(passphrase, salt)
block, err := aes.NewCipher(key)

View file

@ -16,19 +16,19 @@ type Encryption interface {
}
type Encrypter interface {
Encrypt(plainText []byte, passphrase string) (cipherText, salt, nonce []byte, err error)
Encrypt(plainText []byte, passphrase []byte) (cipherText, salt, nonce []byte, err error)
}
type Decrypter interface {
Decrypt(cipherText []byte, passphrase string, salt, nonce []byte) (plainText []byte, err error)
Decrypt(cipherText []byte, passphrase []byte, salt, nonce []byte) (plainText []byte, err error)
}
type KeyDeriver interface {
DeriveKey(passphrase string, existingSalt []byte) (key, salt []byte)
DeriveKey(passphrase []byte, existingSalt []byte) (key, salt []byte)
}
type KeyDeriverFunc func(passphrase string, existingSalt []byte) (key, salt []byte)
type KeyDeriverFunc func(passphrase []byte, existingSalt []byte) (key, salt []byte)
func (f KeyDeriverFunc) DeriveKey(passphrase string, existingSalt []byte) (key, salt []byte) {
func (f KeyDeriverFunc) DeriveKey(passphrase []byte, existingSalt []byte) (key, salt []byte) {
return f(passphrase, existingSalt)
}

View file

@ -0,0 +1,18 @@
package vault
import "errors"
var (
ErrNoEncryption = errors.New("no encryption")
_ Encryption = (*errEncryption)(nil)
)
type errEncryption struct{}
func (errEncryption) Encrypt([]byte, []byte) (cipherText, salt, nonce []byte, err error) {
return nil, nil, nil, ErrNoEncryption
}
func (errEncryption) Decrypt([]byte, []byte, []byte, []byte) (plainText []byte, err error) {
return nil, ErrNoEncryption
}

View file

@ -1,48 +1,24 @@
package vault
import (
"encoding/json"
"os"
)
func WithPassphrase(passphrase string) InitOption {
return initOptionFunc(func(vault *Vault) error {
vault.passphrase = passphrase
vault.passphrase = []byte(passphrase)
return nil
})
}
func WithPassphraseFile(path string) InitOption {
return initOptionFunc(func(vault *Vault) error {
passphrase, err := os.ReadFile(path)
passphraseBytes, err := os.ReadFile(path)
if err != nil {
return err
}
vault.passphrase = string(passphrase)
return nil
})
}
func WithLoadFromPath(path string) InitOption {
return initOptionFunc(func(vault *Vault) error {
vaultFile, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer func() {
_ = vaultFile.Close()
}()
dec := json.NewDecoder(vaultFile)
if err := dec.Decode(vault); err != nil {
return err
}
vault.passphrase = passphraseBytes
return nil
})
}

View file

@ -13,7 +13,7 @@ func Pbkdf2Deriver() KeyDeriver {
iterations = 1000
keyLength = 32
)
return KeyDeriverFunc(func(passphrase string, existingSalt []byte) (key []byte, salt []byte) {
return KeyDeriverFunc(func(passphrase []byte, existingSalt []byte) (key []byte, salt []byte) {
salt = make([]byte, saltLength)
if existingSalt == nil {
// http://www.ietf.org/rfc/rfc2898.txt
@ -26,6 +26,6 @@ func Pbkdf2Deriver() KeyDeriver {
_, _ = rand.Read(salt[len(existingSalt):])
}
return pbkdf2.Key([]byte(passphrase), salt, iterations, keyLength, sha256.New), salt
return pbkdf2.Key(passphrase, salt, iterations, keyLength, sha256.New), salt
})
}

View file

@ -1,10 +1,12 @@
package vault
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
@ -59,10 +61,24 @@ func (e Entry) MarshalText() string {
return val
}
func (e Entry) Decrypt(decrypter Decrypter, passphrase string) ([]byte, error) {
func (e Entry) Decrypt(decrypter Decrypter, passphrase []byte) ([]byte, error) {
return decrypter.Decrypt(e.Value, passphrase, e.Salt, e.Nonce)
}
func NewInMemVault() (*Vault, error) {
passphrase := make([]byte, 32)
n, err := rand.Read(passphrase)
if err != nil {
return nil, err
}
return &Vault{
encryption: NewAesGcmEncryption(Pbkdf2Deriver()),
inMem: true,
entries: make(map[string]Entry),
passphrase: passphrase[:n],
}, nil
}
func NewVault(opts ...InitOption) (*Vault, error) {
v := &Vault{
encryption: NewAesGcmEncryption(Pbkdf2Deriver()),
@ -83,12 +99,33 @@ func NewVault(opts ...InitOption) (*Vault, error) {
}
type Vault struct {
inMem bool
encryption Encryption
entries map[string]Entry
passphrase string
passphrase []byte
lock sync.RWMutex
}
func (v *Vault) IsInitialized() bool {
if v == nil {
return false
}
v.lock.Lock()
defer v.lock.Unlock()
_, isErrEncryption := v.encryption.(errEncryption)
return v.passphrase != nil && !isErrEncryption
}
func (v *Vault) IsInMemory() bool {
v.lock.RLock()
defer v.lock.RUnlock()
return v.inMem
}
func (v *Vault) Keys() []string {
v.lock.RLock()
defer v.lock.RUnlock()
@ -124,28 +161,34 @@ func (v *Vault) RemoveValue(entryKey string) {
delete(v.entries, entryKey)
}
func (v *Vault) doGetValue(entryKey string) ([]byte, error) {
entry, ok := v.entries[entryKey]
if !ok {
return nil, nil
}
return entry.Decrypt(v.encryption, v.passphrase)
}
func (v *Vault) doSetValue(entryKey string, plainText []byte) error {
cipher, salt, nonce, err := v.encryption.Encrypt(plainText, v.passphrase)
func (v *Vault) LoadFromFile(path string) (err error) {
f, err := os.Open(path)
if err != nil {
return err
}
v.entries[entryKey] = Entry{
Value: cipher,
Salt: salt,
Nonce: nonce,
defer func() {
err = errors.Join(err, f.Close())
}()
return v.LoadFrom(f)
}
func (v *Vault) LoadFrom(reader io.Reader) error {
decoder := json.NewDecoder(reader)
return decoder.Decode(v)
}
func (v *Vault) Dispose() {
v.lock.Lock()
defer v.lock.Unlock()
for i := range v.passphrase {
v.passphrase[i] = 0
}
return nil
clear(v.entries)
v.encryption = errEncryption{}
}
func (v *Vault) UnmarshalJSON(data []byte) error {
@ -185,3 +228,27 @@ func (v *Vault) MarshalJSON() ([]byte, error) {
return json.Marshal(toSerialize)
}
func (v *Vault) doGetValue(entryKey string) ([]byte, error) {
entry, ok := v.entries[entryKey]
if !ok {
return nil, nil
}
return entry.Decrypt(v.encryption, v.passphrase)
}
func (v *Vault) doSetValue(entryKey string, plainText []byte) error {
cipher, salt, nonce, err := v.encryption.Encrypt(plainText, v.passphrase)
if err != nil {
return err
}
v.entries[entryKey] = Entry{
Value: cipher,
Salt: salt,
Nonce: nonce,
}
return nil
}

View file

@ -2,15 +2,6 @@ package archive
import (
"errors"
"io"
)
var ErrEmptyArchiveName = errors.New("archive name may not be empty")
type noOpWritecloser struct {
io.Writer
}
func (*noOpWritecloser) Close() error {
return nil
}

View file

@ -12,6 +12,7 @@ import (
"code.icb4dc0.de/buildr/buildr/internal/archive"
"code.icb4dc0.de/buildr/buildr/internal/ignore"
"code.icb4dc0.de/buildr/buildr/internal/ioutils"
"code.icb4dc0.de/buildr/buildr/modules"
)
@ -25,7 +26,7 @@ type CompressionType string
func (t CompressionType) Writer(outer io.WriteCloser) (io.WriteCloser, error) {
switch t {
case CompressionNone:
return &noOpWritecloser{Writer: outer}, nil
return ioutils.NoOpWriteCloser(outer), nil
case CompressionGzip:
return pgzip.NewWriter(outer), nil
default:

View file

@ -9,14 +9,22 @@ import (
"entgo.io/ent/dialect"
)
func NewInMemDB(ctx context.Context) (*DB, error) {
return newDB(ctx, "file:buildr?mode=memory&cache=shared&_pragma=foreign_keys(1)")
}
func NewDB(ctx context.Context, stateFilePath string) (*DB, error) {
client, err := ent.Open(dialect.SQLite, fmt.Sprintf("file:%s?_fk=1&_pragma=foreign_keys(1)", stateFilePath))
return newDB(ctx, fmt.Sprintf("file:%s?_fk=1&_pragma=foreign_keys(1)", stateFilePath))
}
func newDB(ctx context.Context, connectionString string) (*DB, error) {
client, err := ent.Open(dialect.SQLite, connectionString)
if err != nil {
return nil, fmt.Errorf("failed to open SQLite database: %w at %s", err, stateFilePath)
return nil, fmt.Errorf("failed to open SQLite database: %w at %s", err, connectionString)
}
if err := client.Schema.Create(ctx); err != nil {
return nil, fmt.Errorf("failed to create schema: %w at %s", err, stateFilePath)
return nil, fmt.Errorf("failed to create schema: %w at %s", err, connectionString)
}
db := &DB{