refactor: make startup more resilient
Some checks failed
continuous-integration/drone/push Build is failing
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:
parent
3de7016a50
commit
364a0e9bab
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
})...)
|
||||
|
|
|
@ -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))),
|
||||
)
|
||||
})...,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
15
internal/ioutils/no_op_write_closer.go
Normal file
15
internal/ioutils/no_op_write_closer.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
18
internal/vault/err_encryption.go
Normal file
18
internal/vault/err_encryption.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in a new issue