refactor(cli): restructure initialization logic and get rid of global state
This commit is contained in:
parent
c60fc4b347
commit
fd2aebaa3b
|
@ -88,7 +88,6 @@ func (t *Tar) addDir(srcPath, destPath string) error {
|
|||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add dir %s recursively: %w", srcPath, err)
|
||||
}
|
||||
|
|
82
internal/cmd/api.go
Normal file
82
internal/cmd/api.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/rpc"
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type InitLevel int
|
||||
|
||||
const (
|
||||
InitLevelNone InitLevel = iota
|
||||
InitLevelBasic
|
||||
InitLevelBuildRConfig
|
||||
InitLevelParseConfig
|
||||
)
|
||||
|
||||
type LevelInitializer interface {
|
||||
InitAt(lvl InitLevel) error
|
||||
}
|
||||
|
||||
type VaultInitConfig struct {
|
||||
PassphraseLength uint `mapstructure:"vault-pw-length"`
|
||||
}
|
||||
|
||||
func (c *VaultInitConfig) Flags() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("vault-init", flag.ExitOnError)
|
||||
|
||||
fs.UintVar(
|
||||
&c.PassphraseLength,
|
||||
"vault.pw-length",
|
||||
32,
|
||||
"Length of the passphrase to generate",
|
||||
)
|
||||
|
||||
return fs
|
||||
}
|
||||
|
||||
type AppConfigAccessor interface {
|
||||
AppConfig() AppConfig
|
||||
}
|
||||
|
||||
type VaultCommander interface {
|
||||
Init(cfg VaultInitConfig) error
|
||||
List(writer io.Writer) error
|
||||
Get(key string, writer io.Writer) error
|
||||
Set(key string, value []byte) error
|
||||
Remove(key string) error
|
||||
}
|
||||
|
||||
type ServerCommander interface {
|
||||
ServeAPI(ctx context.Context, cfg *rpc.GrpcConfig) error
|
||||
}
|
||||
|
||||
type ModuleCommander interface {
|
||||
RunModule(ctx context.Context, cat modules.Category, name string) error
|
||||
}
|
||||
|
||||
type ModuleArgsProvider interface {
|
||||
ValidArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
}
|
||||
|
||||
type ModuleArgsProviderFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
|
||||
func (f ModuleArgsProviderFunc) ValidArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return f(cmd, args, toComplete)
|
||||
}
|
||||
|
||||
type AppInitializer interface {
|
||||
Init(ctx context.Context) error
|
||||
}
|
||||
|
||||
type AppInitializerFunc func(ctx context.Context) error
|
||||
|
||||
func (f AppInitializerFunc) Init(ctx context.Context) error {
|
||||
return f(ctx)
|
||||
}
|
406
internal/cmd/app.go
Normal file
406
internal/cmd/app.go
Normal file
|
@ -0,0 +1,406 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/containers"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/errs"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/container"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/local"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ignore"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ioutils"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/logging"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/parsing"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/services"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/build"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/buildr"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/packaging"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/task"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/tool"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var cwd string
|
||||
|
||||
func init() {
|
||||
if wd, err := os.Getwd(); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
cwd = wd
|
||||
}
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
app := &App{
|
||||
rootCmd: &cobra.Command{
|
||||
Use: "buildr",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
},
|
||||
loggingCfg: logging.NewConfig(),
|
||||
}
|
||||
|
||||
app.rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
||||
return app.Init(cmd.Context())
|
||||
}
|
||||
|
||||
app.rootCmd.PersistentPostRunE = func(*cobra.Command, []string) error {
|
||||
return app.Shutdown()
|
||||
}
|
||||
|
||||
app.initializers = map[InitLevel]AppInitializer{
|
||||
InitLevelBasic: AppInitializerFunc(app.initBasic),
|
||||
InitLevelBuildRConfig: AppInitializerFunc(app.initBuildRConfig),
|
||||
InitLevelParseConfig: AppInitializerFunc(app.initParseConfigs),
|
||||
}
|
||||
|
||||
app.rootCmd.AddCommand(
|
||||
ModuleCommand(
|
||||
modules.CategoryTool,
|
||||
app,
|
||||
app.ArgProviderFor(modules.CategoryTool),
|
||||
WithShort("Run a tool by its name"),
|
||||
),
|
||||
ModuleCommand(
|
||||
modules.CategoryTask,
|
||||
app,
|
||||
app.ArgProviderFor(modules.CategoryTask),
|
||||
WithShort("Run a task by its name"),
|
||||
),
|
||||
ModuleCommand(
|
||||
modules.CategoryBuild,
|
||||
app,
|
||||
app.ArgProviderFor(modules.CategoryBuild),
|
||||
WithShort("Run a build by its name"),
|
||||
),
|
||||
ModuleCommand(
|
||||
modules.CategoryPackage,
|
||||
app,
|
||||
app.ArgProviderFor(modules.CategoryPackage),
|
||||
WithShort("Create a package by its name"),
|
||||
),
|
||||
VaultCommand(NewVaultApp(app, app.Collection, app)),
|
||||
ServerCommand(NewServerApp(app.Collection)),
|
||||
VersionCommand(),
|
||||
)
|
||||
|
||||
app.rootCmd.PersistentFlags().AddGoFlagSet(app.appCfg.Flags())
|
||||
app.rootCmd.PersistentFlags().AddGoFlagSet(app.loggingCfg.Flags())
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
var (
|
||||
_ LevelInitializer = (*App)(nil)
|
||||
_ ModuleCommander = (*App)(nil)
|
||||
)
|
||||
|
||||
type App struct {
|
||||
*services.Collection
|
||||
loggingCfg logging.Config
|
||||
appCfg AppConfig
|
||||
rootCmd *cobra.Command
|
||||
profile *pprof.Profile
|
||||
initializers map[InitLevel]AppInitializer
|
||||
buildrInstance *buildr.Buildr
|
||||
repo *modules.Repository
|
||||
parsingState struct {
|
||||
parsingRemainder hcl.Body
|
||||
currentEvalCtx *hcl.EvalContext
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Run(ctx context.Context) error {
|
||||
return a.rootCmd.ExecuteContext(ctx)
|
||||
}
|
||||
|
||||
func (a *App) RunWithArgs(ctx context.Context, args ...string) error {
|
||||
a.rootCmd.SetArgs(args)
|
||||
return a.rootCmd.ExecuteContext(ctx)
|
||||
}
|
||||
|
||||
func (a *App) InitAt(lvl InitLevel) error {
|
||||
for cl := InitLevelNone; cl <= lvl; cl++ {
|
||||
if init, ok := a.initializers[cl]; ok {
|
||||
if err := init.Init(a.rootCmd.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) SetInitializer(lvl InitLevel, init AppInitializer) {
|
||||
a.initializers[lvl] = init
|
||||
}
|
||||
|
||||
func (a *App) Init(ctx context.Context) (err error) {
|
||||
slog.SetDefault(a.loggingCfg.Logger())
|
||||
|
||||
if profilingCfg := a.appCfg.Profiling; profilingCfg.IsConfigured() {
|
||||
a.profile = pprof.Lookup(profilingCfg.ProfileName)
|
||||
}
|
||||
|
||||
registry := modules.NewRegistry(
|
||||
build.Registration,
|
||||
tool.Registration,
|
||||
task.Registration,
|
||||
packaging.Registration,
|
||||
)
|
||||
|
||||
if err = a.appCfg.InitPaths(cwd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ignorer *ignore.Ignorer
|
||||
if ignorer, err = a.appCfg.Ignorer(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Collection, err = services.NewCollection(
|
||||
services.WithTypeRegistry(registry),
|
||||
services.WithIgnorer(ignorer),
|
||||
services.WithDockerClientFromEnv(ctx),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Shutdown() error {
|
||||
return errors.Join(a.persistVaultState(), a.writeProfile(), a.Collection.Close())
|
||||
}
|
||||
|
||||
func (a *App) AppConfig() AppConfig {
|
||||
return a.appCfg
|
||||
}
|
||||
|
||||
func (a *App) initBasic(ctx context.Context) (err error) {
|
||||
var v *vault.Vault
|
||||
|
||||
if v, err = a.appCfg.InitVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.Collection.With(services.WithVault(v))
|
||||
}
|
||||
|
||||
func (a *App) RunModule(ctx context.Context, cat modules.Category, name string) error {
|
||||
if err := a.InitAt(InitLevelParseConfig); err != nil {
|
||||
}
|
||||
orchestrator, err := containers.NewOrchestrator(ctx, a.DockerClient(), a.Ignorer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
factory := execution.NewTaskFactory(
|
||||
execution.WithProvider(local.Provider()),
|
||||
execution.WithProvider(container.Provider(orchestrator, a.repo)),
|
||||
)
|
||||
|
||||
plan, err := execution.NewPlanFor(cat, name, a.repo, factory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return plan.Execute(ctx, *a.buildrInstance)
|
||||
}
|
||||
|
||||
func (a *App) ArgProviderFor(cat modules.Category) ModuleArgsProvider {
|
||||
return ModuleArgsProviderFunc(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if err := a.basicParseConfig(); err != nil {
|
||||
slog.Warn("Failed to parse config", slog.String("err", err.Error()))
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
tasks := maps.Keys(a.repo.ModulesByCategory(cat))
|
||||
|
||||
filtered := make([]string, 0, len(tasks))
|
||||
|
||||
for i := range tasks {
|
||||
if strings.HasPrefix(tasks[i], toComplete) {
|
||||
filtered = append(filtered, tasks[i])
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
// basicParseConfig parses the config files without Vault and API clients.
|
||||
// It's primary purpose is to be able to list completions
|
||||
func (a *App) basicParseConfig() (err error) {
|
||||
evalCtx := parsing.MockContext()
|
||||
|
||||
completeSpec := struct {
|
||||
BuildrCfg parsing.BuildrConfig `hcl:"buildr,block"`
|
||||
ModulesSpec parsing.ModulesSpec `hcl:",remain"`
|
||||
}{}
|
||||
|
||||
parser := parsing.NewParser(slog.Default(), a.appCfg.BuildRFS())
|
||||
if err = parser.ReadFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = parser.Parse(evalCtx, &completeSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.buildrInstance, err = completeSpec.BuildrCfg.InitBuildr(a.appCfg.BuildRDirectory, a.appCfg.RepoRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.repo, err = completeSpec.ModulesSpec.Repository(evalCtx, *a.buildrInstance, a.TypeRegistry()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) initBuildRConfig(ctx context.Context) (err error) {
|
||||
parser := parsing.NewParser(slog.Default(), a.appCfg.BuildRFS())
|
||||
slog.Debug("Reading files", "buildr_directory", a.appCfg.BuildRDirectory)
|
||||
|
||||
if err = parser.ReadFiles(); err != nil {
|
||||
slog.Error("Failed to read files", err)
|
||||
return err
|
||||
}
|
||||
|
||||
buildrCfg := struct {
|
||||
parsing.BuildrConfig `hcl:"buildr,block"`
|
||||
Remainder hcl.Body `hcl:",remain"`
|
||||
}{
|
||||
BuildrConfig: parsing.BuildrConfig{
|
||||
LogToStderr: a.appCfg.Execution.LogToStderr,
|
||||
},
|
||||
}
|
||||
|
||||
a.parsingState.currentEvalCtx = parsing.BasicContext(a.Vault())
|
||||
if err = parser.Parse(a.parsingState.currentEvalCtx, &buildrCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.buildrInstance, err = buildrCfg.InitBuildr(a.appCfg.BuildRDirectory, a.appCfg.RepoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = copyCurrentBinaryToBinariesDir(a.buildrInstance.Config.BinariesDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.parsingState.parsingRemainder = buildrCfg.Remainder
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) initParseConfigs(ctx context.Context) (err error) {
|
||||
evalCtx := parsing.FullContext(ctx, a.parsingState.currentEvalCtx, a.Collection)
|
||||
var rawSpec parsing.ModulesSpec
|
||||
|
||||
if diags := gohcl.DecodeBody(a.parsingState.parsingRemainder, evalCtx, &rawSpec); diags.HasErrors() {
|
||||
logging.Diagnostics(diags, slog.Default())
|
||||
return errs.ErrAlreadyLogged
|
||||
}
|
||||
|
||||
if a.repo, err = rawSpec.Repository(evalCtx, *a.buildrInstance, a.TypeRegistry()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) writeProfile() (err error) {
|
||||
profilingCfg := a.appCfg.Profiling
|
||||
if !profilingCfg.IsConfigured() || a.profile == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Create(profilingCfg.OutFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, f.Close())
|
||||
}()
|
||||
|
||||
return a.profile.WriteTo(f, 0)
|
||||
}
|
||||
|
||||
func (a *App) persistVaultState() error {
|
||||
if a.Vault() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
outFile, err := os.Create(a.appCfg.Vault.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, outFile.Close())
|
||||
}()
|
||||
|
||||
encoder := json.NewEncoder(outFile)
|
||||
|
||||
return encoder.Encode(a.Vault())
|
||||
}
|
||||
|
||||
func copyCurrentBinaryToBinariesDir(binariesDir string) (err error) {
|
||||
expectedBuildrBinPath := filepath.Join(binariesDir, fmt.Sprintf("buildr_%s_%s", runtime.GOOS, runtime.GOARCH))
|
||||
|
||||
if _, err := os.Stat(expectedBuildrBinPath); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentBinaryPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentBinary, err := os.Open(currentBinaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, currentBinary.Close())
|
||||
}()
|
||||
|
||||
outFile, err := os.OpenFile(expectedBuildrBinPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, outFile.Close())
|
||||
}()
|
||||
|
||||
_, err = ioutils.CopyWithPooledBuffer(outFile, currentBinary)
|
||||
|
||||
return err
|
||||
}
|
12
internal/cmd/app_pprof.go
Normal file
12
internal/cmd/app_pprof.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"runtime/pprof"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/profiling"
|
||||
)
|
||||
|
||||
type PprofApp struct {
|
||||
profilingCfg *profiling.Config
|
||||
profile *pprof.Profile
|
||||
}
|
42
internal/cmd/app_server.go
Normal file
42
internal/cmd/app_server.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/rpc"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/services"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var _ ServerCommander = (*ServerApp)(nil)
|
||||
|
||||
func NewServerApp(accessor services.TypeRegistryAccessor) *ServerApp {
|
||||
return &ServerApp{
|
||||
accessor: accessor,
|
||||
}
|
||||
}
|
||||
|
||||
type ServerApp struct {
|
||||
accessor services.TypeRegistryAccessor
|
||||
}
|
||||
|
||||
func (s *ServerApp) ServeAPI(ctx context.Context, cfg *rpc.GrpcConfig) error {
|
||||
logger := slog.Default()
|
||||
|
||||
logger.Info("Starting gRPC server", slog.Group("grpc", slog.String("addr", cfg.Host.Address)))
|
||||
|
||||
grpcServer := rpc.NewServer(logger, s.accessor)
|
||||
|
||||
go func() {
|
||||
if err := grpcServer.Start(cfg.Host.Address); err != nil {
|
||||
slog.Error("Error occurred while serving gRPC API", slog.String("err", err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
grpcServer.Close()
|
||||
|
||||
return nil
|
||||
}
|
145
internal/cmd/app_vault.go
Normal file
145
internal/cmd/app_vault.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/services"
|
||||
"code.icb4dc0.de/prskr/go-pwgen"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var _ VaultCommander = (*VaultApp)(nil)
|
||||
|
||||
type VaultAppServiceAccess interface {
|
||||
services.VaultAccessor
|
||||
services.CollectionModifier
|
||||
}
|
||||
|
||||
func NewVaultApp(
|
||||
initializer LevelInitializer,
|
||||
serviceAccess VaultAppServiceAccess,
|
||||
configAccessor AppConfigAccessor,
|
||||
) *VaultApp {
|
||||
return &VaultApp{
|
||||
initializer: initializer,
|
||||
serviceAccess: serviceAccess,
|
||||
configAccessor: configAccessor,
|
||||
}
|
||||
}
|
||||
|
||||
type VaultApp struct {
|
||||
initializer LevelInitializer
|
||||
serviceAccess VaultAppServiceAccess
|
||||
configAccessor AppConfigAccessor
|
||||
}
|
||||
|
||||
func (v VaultApp) Init(initCfg VaultInitConfig) error {
|
||||
if err := v.initializer.InitAt(InitLevelBasic); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vault := v.serviceAccess.Vault()
|
||||
if vault != nil {
|
||||
slog.Default().Info("vault already initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := v.configAccessor.AppConfig()
|
||||
|
||||
if cfg.Vault.PassphraseFile == "" {
|
||||
return errors.New("vault.passphrase-file may not be empty")
|
||||
}
|
||||
|
||||
key, err := pwgen.Generate(pwgen.WithLength(initCfg.PassphraseLength))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating password: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cfg.Vault.PassphraseFile, []byte(key), 0o600); err != nil {
|
||||
return fmt.Errorf("failed to write passphrase to file: %w", err)
|
||||
}
|
||||
|
||||
if vault, err := cfg.InitVault(); err != nil {
|
||||
return fmt.Errorf("failed to prepare vault: %w", err)
|
||||
} else if err = v.serviceAccess.With(services.WithVault(vault)); err != nil {
|
||||
return fmt.Errorf("failed to update service collection: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v VaultApp) List(writer io.Writer) error {
|
||||
if err := v.initializer.InitAt(InitLevelBasic); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vaultInstance := v.serviceAccess.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(writer)
|
||||
|
||||
for _, k := range vaultInstance.Keys() {
|
||||
if _, err := fmt.Fprintln(writer, "\t%s\n", k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v VaultApp) Get(key string, writer io.Writer) error {
|
||||
if err := v.initializer.InitAt(InitLevelBasic); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vaultInstance := v.serviceAccess.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
|
||||
val, err := vaultInstance.GetValue(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(writer)
|
||||
if _, err := fmt.Fprintln(writer, val); err != nil {
|
||||
return fmt.Errorf("failed to print value: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v VaultApp) Set(key string, value []byte) error {
|
||||
if err := v.initializer.InitAt(InitLevelBasic); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vaultInstance := v.serviceAccess.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
|
||||
return vaultInstance.SetValue(key, value)
|
||||
}
|
||||
|
||||
func (v VaultApp) Remove(key string) error {
|
||||
if err := v.initializer.InitAt(InitLevelBasic); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vaultInstance := v.serviceAccess.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
|
||||
vaultInstance.RemoveValue(key)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
)
|
||||
|
||||
func validArgsFor(moduleType modules.ModuleCategory) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
tasks := maps.Keys(repo.ModulesByType(moduleType))
|
||||
|
||||
filtered := make([]string, 0, len(tasks))
|
||||
|
||||
for i := range tasks {
|
||||
if strings.HasPrefix(tasks[i], toComplete) {
|
||||
filtered = append(filtered, tasks[i])
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/containers"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/container"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/local"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
)
|
||||
|
||||
var buildCmd = &cobra.Command{
|
||||
Use: "build",
|
||||
RunE: runBuild,
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
ValidArgsFunction: validArgsFor(modules.ModuleCategoryBuild),
|
||||
}
|
||||
|
||||
func runBuild(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("no tool specified")
|
||||
}
|
||||
|
||||
orchestrator, err := containers.NewOrchestrator(cmd.Context(), svc.DockerClient(), svc.Ignorer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
factory := execution.NewTaskFactory(
|
||||
execution.WithProvider(local.Provider()),
|
||||
execution.WithProvider(container.Provider(orchestrator, repo)),
|
||||
)
|
||||
plan, err := execution.NewPlanFor(modules.ModuleCategoryBuild, args[0], repo, factory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return plan.Execute(cmd.Context(), repo.Buildr)
|
||||
}
|
|
@ -1,27 +1,153 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"errors"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/config"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ignore"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/profiling"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/vcs"
|
||||
)
|
||||
|
||||
func LoadConfig[T any](flags *pflag.FlagSet) (*T, error) {
|
||||
var cfg T
|
||||
var ErrRepoRootNotFound = errors.New("failed to detect repo root")
|
||||
|
||||
vipr := viper.New()
|
||||
vipr.SetEnvPrefix("buildr")
|
||||
vipr.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
|
||||
vipr.AutomaticEnv()
|
||||
|
||||
if err := vipr.BindPFlags(flags); err != nil {
|
||||
return nil, err
|
||||
type AppConfig struct {
|
||||
Profiling profiling.Config
|
||||
VCSType vcs.Type
|
||||
BuildRDirectory string
|
||||
RepoRoot string
|
||||
Vault struct {
|
||||
FilePath string
|
||||
Passphrase string
|
||||
PassphraseFile string
|
||||
}
|
||||
|
||||
if err := vipr.Unmarshal(&cfg); err != nil {
|
||||
return nil, err
|
||||
Execution struct {
|
||||
LogToStderr bool
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (c *AppConfig) RepoFS() fs.FS {
|
||||
return os.DirFS(c.RepoRoot)
|
||||
}
|
||||
|
||||
func (c *AppConfig) BuildRFS() fs.FS {
|
||||
return os.DirFS(c.BuildRDirectory)
|
||||
}
|
||||
|
||||
func (c *AppConfig) Ignorer() (*ignore.Ignorer, error) {
|
||||
return ignore.NewIgnorer(c.RepoRoot, c.Vault.FilePath)
|
||||
}
|
||||
|
||||
func (c *AppConfig) InitPaths(from string) (err error) {
|
||||
if c.RepoRoot == "" {
|
||||
if c.RepoRoot, err = c.findRepoRoot(from); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(c.BuildRDirectory) {
|
||||
c.BuildRDirectory = filepath.Join(c.RepoRoot, c.BuildRDirectory)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AppConfig) InitVault() (*vault.Vault, error) {
|
||||
var vaultOpts []vault.InitOption
|
||||
|
||||
if pw := c.Vault.Passphrase; pw != "" {
|
||||
vaultOpts = append(vaultOpts, vault.WithPassphrase(pw))
|
||||
} else if path := c.Vault.PassphraseFile; path != "" {
|
||||
vaultOpts = append(vaultOpts, vault.WithPassphraseFile(path))
|
||||
}
|
||||
|
||||
if filePath := c.Vault.FilePath; filePath != "" {
|
||||
if !filepath.IsAbs(c.Vault.FilePath) {
|
||||
filePath = filepath.Join(c.RepoRoot, filePath)
|
||||
}
|
||||
|
||||
vaultOpts = append(vaultOpts, vault.WithLoadFromPath(filePath))
|
||||
}
|
||||
|
||||
return vault.NewVault(vaultOpts...)
|
||||
}
|
||||
|
||||
func (c *AppConfig) Flags() *flag.FlagSet {
|
||||
flags := flag.NewFlagSet("buildr", flag.ExitOnError)
|
||||
|
||||
c.VCSType = config.EnvOr("BUILDR_VCS_TYPE", vcs.ParseType, vcs.TypeGit)
|
||||
flags.StringVar(
|
||||
&c.BuildRDirectory,
|
||||
"buildr.directory",
|
||||
config.StringEnvOr("BUILDR_DIRECTORY", ".buildr"),
|
||||
"Directory where to look for BuildR files - if relative path will be relative to repository root (depending on the VCS directory)",
|
||||
)
|
||||
|
||||
flags.StringVar(
|
||||
&c.RepoRoot,
|
||||
"buildr.repo-root",
|
||||
config.StringEnvOr("BUILDR_REPO_ROOT", ""),
|
||||
"Repository root directory - normally auto-detected based on VCS directory",
|
||||
)
|
||||
|
||||
flags.StringVar(
|
||||
&c.Vault.FilePath,
|
||||
"vault.file",
|
||||
config.StringEnvOr("BUILDR_VAULT_FILE", filepath.Join(".buildr", "vault.json")),
|
||||
"Relative file path to vault file",
|
||||
)
|
||||
|
||||
flags.StringVar(
|
||||
&c.Vault.Passphrase,
|
||||
"vault.passphrase",
|
||||
config.StringEnvOr("BUILDR_VAULT_PASSPHRASE", ""),
|
||||
"Password for vault - has precedence over vault-passphrase-file",
|
||||
)
|
||||
|
||||
flags.StringVar(
|
||||
&c.Vault.PassphraseFile,
|
||||
"vault.passphrase-file",
|
||||
config.StringEnvOr("BUILDR_VAULT_PASSPHRASE_FILE", filepath.Join(".buildr", ".vaultpw")),
|
||||
"File containing the vault passphrase",
|
||||
)
|
||||
|
||||
flags.BoolVar(
|
||||
&c.Execution.LogToStderr,
|
||||
"execution.log-to-stderr",
|
||||
config.EnvOr("BUILDR_EXECUTION_LOG_TO_STDERR", strconv.ParseBool, false),
|
||||
"If output of commands should be piped to stdout",
|
||||
)
|
||||
|
||||
flags.Var(
|
||||
&c.VCSType,
|
||||
"vcs.type",
|
||||
"The type of VCS to use",
|
||||
)
|
||||
|
||||
c.Profiling.AddFlags(flags)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func (c *AppConfig) findRepoRoot(dir string) (string, error) {
|
||||
if dir == "" {
|
||||
return "", ErrRepoRootNotFound
|
||||
}
|
||||
|
||||
info, err := os.Stat(filepath.Join(dir, c.VCSType.Directory()))
|
||||
if err != nil {
|
||||
return c.findRepoRoot(filepath.Dir(dir))
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
return c.findRepoRoot(filepath.Dir(dir))
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var loggingConfig = struct {
|
||||
LogLevel *slog.LevelVar
|
||||
AddSource bool
|
||||
}{
|
||||
LogLevel: new(slog.LevelVar),
|
||||
}
|
||||
|
||||
func prepareLoggingFlags() *flag.FlagSet {
|
||||
flagSet := flag.NewFlagSet("logging", flag.PanicOnError)
|
||||
flagSet.TextVar(
|
||||
loggingConfig.LogLevel,
|
||||
"log-level",
|
||||
loggingConfig.LogLevel,
|
||||
"set log level",
|
||||
)
|
||||
flagSet.BoolVar(
|
||||
&loggingConfig.AddSource,
|
||||
"log-add-source",
|
||||
false,
|
||||
"Enable to get detailed information where the log was produced",
|
||||
)
|
||||
|
||||
return flagSet
|
||||
}
|
38
internal/cmd/module.go
Normal file
38
internal/cmd/module.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ModuleCommandOption func(cmd *cobra.Command)
|
||||
|
||||
func WithShort(short string) ModuleCommandOption {
|
||||
return func(cmd *cobra.Command) {
|
||||
cmd.Short = short
|
||||
}
|
||||
}
|
||||
|
||||
func ModuleCommand(
|
||||
category modules.Category,
|
||||
cmder ModuleCommander,
|
||||
argsProvider ModuleArgsProvider,
|
||||
opts ...ModuleCommandOption,
|
||||
) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: category.String(),
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
ValidArgsFunction: argsProvider.ValidArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmder.RunModule(cmd.Context(), category, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
for i := range opts {
|
||||
opts[i](cmd)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/containers"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/container"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/local"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
)
|
||||
|
||||
var packageCmd = &cobra.Command{
|
||||
Use: "package",
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: runPackage,
|
||||
ValidArgsFunction: validArgsFor(modules.ModuleCategoryPackage),
|
||||
}
|
||||
|
||||
func runPackage(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("no package specified")
|
||||
}
|
||||
orchestrator, err := containers.NewOrchestrator(cmd.Context(), svc.DockerClient(), svc.Ignorer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
factory := execution.NewTaskFactory(
|
||||
execution.WithProvider(local.Provider()),
|
||||
execution.WithProvider(container.Provider(orchestrator, repo)),
|
||||
)
|
||||
plan, err := execution.NewPlanFor(modules.ModuleCategoryPackage, args[0], repo, factory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return plan.Execute(cmd.Context(), repo.Buildr)
|
||||
}
|
|
@ -1,297 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/modules/vcs"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/errs"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ignore"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ioutils"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/logging"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/profiling"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/parsing"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/services"
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/build"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/packaging"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/task"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/tool"
|
||||
)
|
||||
|
||||
var (
|
||||
vcsType = vcs.TypeGit
|
||||
rawSpec parsing.RawSpec
|
||||
repo *modules.Repository
|
||||
svc *services.Collection
|
||||
buildrSpec parsing.BuildrConfig
|
||||
svcConfig *services.Config
|
||||
profilingCfg *profiling.Config
|
||||
profile *pprof.Profile
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "buildr",
|
||||
PersistentPreRunE: initBuildR,
|
||||
PersistentPostRunE: shutdownBuildr,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
skipInitCommands = []*cobra.Command{
|
||||
serveApiCmd,
|
||||
versionCmd,
|
||||
initVaultCmd,
|
||||
listVaultCmd,
|
||||
getVaultCmd,
|
||||
setVaultCmd,
|
||||
rmVaultCmd,
|
||||
}
|
||||
|
||||
registry = modules.NewRegistry(
|
||||
build.Registration,
|
||||
tool.Registration,
|
||||
task.Registration,
|
||||
packaging.Registration,
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Server CMD
|
||||
serveApiCmd.Flags().String(
|
||||
"grpc-serve-address",
|
||||
":3000",
|
||||
"Address on which the gRPC server will listen",
|
||||
)
|
||||
|
||||
serverCmd.AddCommand(serveApiCmd)
|
||||
|
||||
// Vault CMD
|
||||
initVaultCmd.Flags().Int32("vault-pw-length", 32, "Length of the passphrase to generate")
|
||||
vaultCmd.AddCommand(initVaultCmd, listVaultCmd, getVaultCmd, setVaultCmd, rmVaultCmd)
|
||||
|
||||
// Root CMD
|
||||
rootCmd.AddCommand(toolCmd, taskCmd, buildCmd, packageCmd, vaultCmd, serverCmd, versionCmd)
|
||||
rootCmd.PersistentFlags().String("buildR-dir", ".buildr", "Directory where to look for BuildR files - if relative path will be relavtive to repository root (where the next .git directory resides")
|
||||
rootCmd.PersistentFlags().Var(&vcsType, "vcs-type", "VCS to use, currently only Git is supported")
|
||||
rootCmd.PersistentFlags().String("repo-root", "", "Repository root directory - normally auto-detected based on .git directory")
|
||||
rootCmd.PersistentFlags().String("vault-file", filepath.Join(".buildr", "vault.json"), "Relative file path to vault file")
|
||||
rootCmd.PersistentFlags().String("vault-passphrase", "", "Password for vault - has precedence over vault-passphrase-file")
|
||||
rootCmd.PersistentFlags().String("vault-passphrase-file", filepath.Join(".buildr", ".vaultpw"), "File containing the vault passphrase")
|
||||
rootCmd.PersistentFlags().String("pprof-out-file", "", "If set a pprof profile will be written to this file")
|
||||
rootCmd.PersistentFlags().String("pprof-profile-name", "goroutine", "Pprof profile to create, possible values: [goroutine, heap, allocs, threadcreate, block, mutex]")
|
||||
rootCmd.PersistentFlags().BoolVar(&buildrSpec.LogToStderr, "log-to-stderr", false, "If output of commands should be piped to stdout")
|
||||
rootCmd.PersistentFlags().AddGoFlagSet(prepareLoggingFlags())
|
||||
}
|
||||
|
||||
func Execute(ctx context.Context) error {
|
||||
return rootCmd.ExecuteContext(ctx)
|
||||
}
|
||||
|
||||
func initBuildR(cmd *cobra.Command, _ []string) (err error) {
|
||||
slogOptions := slog.HandlerOptions{
|
||||
AddSource: loggingConfig.AddSource,
|
||||
Level: loggingConfig.LogLevel.Level(),
|
||||
}
|
||||
|
||||
slog.SetDefault(slog.New(slogOptions.NewTextHandler(os.Stderr)))
|
||||
|
||||
profilingCfg, err = LoadConfig[profiling.Config](cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if profilingCfg.IsConfigured() {
|
||||
profile = pprof.Lookup(profilingCfg.ProfileName)
|
||||
}
|
||||
|
||||
svcConfig, err = LoadConfig[services.Config](cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := svcConfig.Init(vcsType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fallbackIgnorer, err := ignore.NewIgnorer(svcConfig.BuildR.RepoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v, err := svcConfig.PrepareVault(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
svc, err = services.NewCollection(
|
||||
services.WithVault(v),
|
||||
services.WithTypeRegistry(registry),
|
||||
services.WithIgnorer(fallbackIgnorer),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if skipInit(cmd) {
|
||||
slog.Debug("Skipping initialization", slog.String("command_name", cmd.Name()))
|
||||
return nil
|
||||
}
|
||||
|
||||
buildrDirFs := os.DirFS(svcConfig.BuildR.BuildrDirectory)
|
||||
parser := parsing.NewParser(slog.Default(), buildrDirFs)
|
||||
|
||||
slog.Debug("Reading files", "buildr_directory", svcConfig.BuildR.BuildrDirectory)
|
||||
|
||||
if err := parser.ReadFiles(); err != nil {
|
||||
slog.Error("Failed to read files", err)
|
||||
return err
|
||||
}
|
||||
|
||||
buildrCfg := struct {
|
||||
Config parsing.BuildrConfig `hcl:"buildr,block"`
|
||||
Remainder hcl.Body `hcl:",remain"`
|
||||
}{
|
||||
Config: buildrSpec,
|
||||
}
|
||||
|
||||
evalCtx := parsing.BasicContext(svc.Vault())
|
||||
if err := parser.Parse(evalCtx, &buildrCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buildrInstance, err := buildrCfg.Config.Buildr(svcConfig.BuildR.BuildrDirectory, svcConfig.BuildR.RepoRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := copyCurrentBinaryToBinariesDir(buildrInstance.Config.BinariesDirectory); err != nil {
|
||||
return fmt.Errorf("failed to copy current binary to bin directory: %w", err)
|
||||
}
|
||||
|
||||
err = svc.With(
|
||||
services.WithGitHubTokenClient(cmd.Context(), buildrInstance.GitHub.APIToken),
|
||||
services.WithIgnorerFromBuildr(buildrInstance, svcConfig.Vault.FilePath),
|
||||
services.WithDockerClientFromEnv(cmd.Context()),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evalCtx = parsing.FullContext(cmd.Context(), evalCtx, svc)
|
||||
|
||||
if diags := gohcl.DecodeBody(buildrCfg.Remainder, evalCtx, &rawSpec); diags.HasErrors() {
|
||||
logging.Diagnostics(diags, slog.Default())
|
||||
return errs.ErrAlreadyLogged
|
||||
}
|
||||
|
||||
if r, err := rawSpec.Repository(evalCtx, *buildrInstance, registry); err != nil {
|
||||
return err
|
||||
} else {
|
||||
repo = r
|
||||
}
|
||||
|
||||
slog.Debug("Completed initialization")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyCurrentBinaryToBinariesDir(binariesDir string) (err error) {
|
||||
expectedBuildrBinPath := filepath.Join(binariesDir, fmt.Sprintf("buildr_%s_%s", runtime.GOOS, runtime.GOARCH))
|
||||
|
||||
if _, err := os.Stat(expectedBuildrBinPath); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentBinaryPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentBinary, err := os.Open(currentBinaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, currentBinary.Close())
|
||||
}()
|
||||
|
||||
outFile, err := os.OpenFile(expectedBuildrBinPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, outFile.Close())
|
||||
}()
|
||||
|
||||
_, err = ioutils.CopyWithPooledBuffer(outFile, currentBinary)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func shutdownBuildr(_ *cobra.Command, _ []string) (err error) {
|
||||
return errors.Join(persistVaultState(), writeProfile(), svc.Close())
|
||||
}
|
||||
|
||||
func writeProfile() (err error) {
|
||||
if profilingCfg == nil || !profilingCfg.IsConfigured() {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Create(profilingCfg.OutFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, f.Close())
|
||||
}()
|
||||
|
||||
return profile.WriteTo(f, 0)
|
||||
}
|
||||
|
||||
func persistVaultState() error {
|
||||
if svcConfig == nil || svc.Vault() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
outFile, err := os.Create(svcConfig.Vault.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = errors.Join(err, outFile.Close())
|
||||
}()
|
||||
|
||||
encoder := json.NewEncoder(outFile)
|
||||
|
||||
return encoder.Encode(svc.Vault())
|
||||
}
|
||||
|
||||
func skipInit(cmd *cobra.Command) bool {
|
||||
for i := range skipInitCommands {
|
||||
if skipInitCommands[i] == cmd {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Use == "completion" || cmd.Parent().Use == "completion" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -3,46 +3,30 @@ package cmd
|
|||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/internal/rpc"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var (
|
||||
serverCmd = &cobra.Command{
|
||||
func ServerCommand(cmder ServerCommander) *cobra.Command {
|
||||
cfg := new(rpc.GrpcConfig)
|
||||
|
||||
serverCmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
serveApiCmd = &cobra.Command{
|
||||
serveApiCmd := &cobra.Command{
|
||||
Use: "api",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: runServeAPI,
|
||||
}
|
||||
)
|
||||
|
||||
func runServeAPI(cmd *cobra.Command, _ []string) error {
|
||||
logger := slog.Default()
|
||||
|
||||
grpcCfg, err := LoadConfig[rpc.GrpcConfig](cmd.Flags())
|
||||
if err != nil {
|
||||
return err
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmder.ServeAPI(cmd.Context(), cfg)
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info("Starting gRPC server", slog.Group("grpc", slog.String("addr", grpcCfg.Host.Address)))
|
||||
serveApiCmd.Flags().AddGoFlagSet(cfg.Flags())
|
||||
|
||||
grpcServer := rpc.NewServer(logger, svc)
|
||||
serverCmd.AddCommand(serveApiCmd)
|
||||
|
||||
go func() {
|
||||
if err := grpcServer.Start(grpcCfg.Host.Address); err != nil {
|
||||
slog.Error("Error occurred while serving gRPC API", slog.String("err", err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
<-cmd.Context().Done()
|
||||
|
||||
grpcServer.Close()
|
||||
|
||||
return nil
|
||||
return serverCmd
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/containers"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/container"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/local"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
)
|
||||
|
||||
var taskCmd = &cobra.Command{
|
||||
Use: "task",
|
||||
RunE: runTask,
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
ValidArgsFunction: validArgsFor(modules.ModuleCategoryTask),
|
||||
}
|
||||
|
||||
func runTask(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("no tool specified")
|
||||
}
|
||||
|
||||
orchestrator, err := containers.NewOrchestrator(cmd.Context(), svc.DockerClient(), svc.Ignorer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
factory := execution.NewTaskFactory(
|
||||
execution.WithProvider(local.Provider()),
|
||||
execution.WithProvider(container.Provider(orchestrator, repo)),
|
||||
)
|
||||
plan, err := execution.NewPlanFor(modules.ModuleCategoryTask, args[0], repo, factory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return plan.Execute(cmd.Context(), repo.Buildr)
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/containers"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/container"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/execution/local"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
)
|
||||
|
||||
var toolCmd = &cobra.Command{
|
||||
Use: "tool",
|
||||
RunE: runTool,
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
ValidArgsFunction: validArgsFor(modules.ModuleCategoryTool),
|
||||
}
|
||||
|
||||
func runTool(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return fmt.Errorf("no tool specified")
|
||||
}
|
||||
|
||||
orchestrator, err := containers.NewOrchestrator(cmd.Context(), svc.DockerClient(), svc.Ignorer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
factory := execution.NewTaskFactory(
|
||||
execution.WithProvider(local.Provider()),
|
||||
execution.WithProvider(container.Provider(orchestrator, repo)),
|
||||
)
|
||||
|
||||
plan, err := execution.NewPlanFor(modules.ModuleCategoryTool, args[0], repo, factory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return plan.Execute(cmd.Context(), repo.Buildr)
|
||||
}
|
|
@ -2,28 +2,26 @@ package cmd
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/services"
|
||||
"code.icb4dc0.de/prskr/go-pwgen"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/exp/slog"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrVaultNotInitiated = errors.New("vault is not initiated set either vault-passphrase or vault-passphrase-file")
|
||||
var ErrVaultNotInitiated = errors.New("vault is not initiated set either vault-passphrase or vault-passphrase-file")
|
||||
|
||||
vaultCmd = &cobra.Command{
|
||||
func VaultCommand(cmder VaultCommander) *cobra.Command {
|
||||
var initCfg VaultInitConfig
|
||||
|
||||
vaultCmd := &cobra.Command{
|
||||
Use: "vault",
|
||||
Short: "Interact with the buildr vault",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
initVaultCmd = &cobra.Command{
|
||||
initVaultCmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize vault - create an empty vault and a key file",
|
||||
Long: `Creates an empty vault file and bootstraps a random passphrase
|
||||
|
@ -31,145 +29,68 @@ which will be written either to the configured --vault-passphrase-file or to the
|
|||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: runInitVault,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return cmder.Init(initCfg)
|
||||
},
|
||||
}
|
||||
|
||||
getVaultCmd = &cobra.Command{
|
||||
getVaultCmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get value from vault",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runGetVault,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return cmder.Get(args[0], os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
listVaultCmd = &cobra.Command{
|
||||
listVaultCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all vault entries - no decrypted values",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: runListVault,
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
return cmder.List(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
setVaultCmd = &cobra.Command{
|
||||
setVaultCmd := &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Set a vault value",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: runSetVault,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 1 {
|
||||
inBuf := bytes.NewBuffer(nil)
|
||||
if _, err := io.Copy(inBuf, os.Stdin); err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
|
||||
return cmder.Set(args[0], inBuf.Bytes())
|
||||
}
|
||||
|
||||
return cmder.Set(args[0], []byte(args[1]))
|
||||
},
|
||||
}
|
||||
|
||||
rmVaultCmd = &cobra.Command{
|
||||
rmVaultCmd := &cobra.Command{
|
||||
Use: "rm",
|
||||
Short: "Remove value from vault",
|
||||
Aliases: []string{"del"},
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runRemoveVaultEntry,
|
||||
}
|
||||
)
|
||||
|
||||
func runInitVault(cmd *cobra.Command, _ []string) error {
|
||||
if svc.Vault() != nil {
|
||||
slog.Default().Info("vault already initialized")
|
||||
return nil
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return cmder.Remove(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig[services.Config](cmd.Flags())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading config: %w", err)
|
||||
}
|
||||
initVaultCmd.Flags().AddGoFlagSet(initCfg.Flags())
|
||||
|
||||
if cfg.Vault.PassphraseFile == "" {
|
||||
return errors.New("vault.passphrase-file may not be empty")
|
||||
}
|
||||
vaultCmd.AddCommand(initVaultCmd, listVaultCmd, getVaultCmd, setVaultCmd, rmVaultCmd)
|
||||
|
||||
initCfg, err := LoadConfig[InitConfig](cmd.Flags())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading init config: %w", err)
|
||||
}
|
||||
|
||||
key, err := pwgen.Generate(pwgen.WithLength(initCfg.PassphraseLength))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating password: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cfg.Vault.PassphraseFile, []byte(key), 0o600); err != nil {
|
||||
return fmt.Errorf("failed to write passphrase to file: %w", err)
|
||||
}
|
||||
|
||||
if v, err := cfg.PrepareVault(); err != nil {
|
||||
return fmt.Errorf("failed to prepare vault: %w", err)
|
||||
} else if err = svc.With(services.WithVault(v)); err != nil {
|
||||
return fmt.Errorf("failed to update service collection: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runListVault(*cobra.Command, []string) error {
|
||||
vaultInstance := svc.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
for _, k := range vaultInstance.Keys() {
|
||||
fmt.Printf("\t%s\n", k)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runGetVault(_ *cobra.Command, args []string) error {
|
||||
vaultInstance := svc.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
|
||||
val, err := vaultInstance.GetValue(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
_, _ = os.Stdout.Write(val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRemoveVaultEntry(_ *cobra.Command, args []string) error {
|
||||
vaultInstance := svc.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
|
||||
vaultInstance.RemoveValue(args[0])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSetVault(_ *cobra.Command, args []string) error {
|
||||
vaultInstance := svc.Vault()
|
||||
if vaultInstance == nil {
|
||||
return ErrVaultNotInitiated
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
inBuf := bytes.NewBuffer(nil)
|
||||
if _, err := io.Copy(inBuf, os.Stdin); err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
|
||||
return vaultInstance.SetValue(args[0], inBuf.Bytes())
|
||||
}
|
||||
|
||||
return vaultInstance.SetValue(args[0], []byte(args[1]))
|
||||
}
|
||||
|
||||
type InitConfig struct {
|
||||
PassphraseLength uint `mapstructure:"vault-pw-length"`
|
||||
return vaultCmd
|
||||
}
|
||||
|
|
|
@ -7,20 +7,19 @@ import (
|
|||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var (
|
||||
CurrentVersion = "development"
|
||||
var CurrentVersion = "development"
|
||||
|
||||
versionCmd = &cobra.Command{
|
||||
func VersionCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Prints the version of the CLI",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Run: runPrintVersion,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
slog.Info("BuildR version",
|
||||
slog.String("current_version", CurrentVersion),
|
||||
slog.String("go_version", runtime.Version()),
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func runPrintVersion(*cobra.Command, []string) {
|
||||
slog.Info("BuildR version",
|
||||
slog.String("current_version", CurrentVersion),
|
||||
slog.String("go_version", runtime.Version()),
|
||||
)
|
||||
}
|
||||
|
|
25
internal/config/env.go
Normal file
25
internal/config/env.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
func StringEnvOr(key string, def string) string {
|
||||
val, set := os.LookupEnv(key)
|
||||
if !set {
|
||||
return def
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func EnvOr[T any](key string, parser func(string) (T, error), def T) T {
|
||||
val, set := os.LookupEnv(key)
|
||||
if !set {
|
||||
return def
|
||||
}
|
||||
|
||||
if parsed, err := parser(val); err != nil {
|
||||
return def
|
||||
} else {
|
||||
return parsed
|
||||
}
|
||||
}
|
|
@ -29,7 +29,6 @@ type ContainerProber struct {
|
|||
}
|
||||
|
||||
func (p *ContainerProber) WaitReady(ctx context.Context) error {
|
||||
|
||||
waitCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
monitorErr := p.inspectMonitor(ctx)
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func NewPlanFor(
|
||||
moduleType modules.ModuleCategory,
|
||||
moduleType modules.Category,
|
||||
moduleName string,
|
||||
repo *modules.Repository,
|
||||
factory *TaskFactory,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package logging
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type TaskOutputSink interface {
|
||||
StdOut() io.Writer
|
||||
|
|
46
internal/logging/setup.go
Normal file
46
internal/logging/setup.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
LogLevel: new(slog.LevelVar),
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
LogLevel *slog.LevelVar
|
||||
AddSource bool
|
||||
}
|
||||
|
||||
func (c *Config) Flags() *flag.FlagSet {
|
||||
flagSet := flag.NewFlagSet("logging", flag.PanicOnError)
|
||||
flagSet.TextVar(
|
||||
c.LogLevel,
|
||||
"log.level",
|
||||
c.LogLevel,
|
||||
"set log level",
|
||||
)
|
||||
flagSet.BoolVar(
|
||||
&c.AddSource,
|
||||
"log.add-source",
|
||||
false,
|
||||
"Enable to get detailed information where the log was produced",
|
||||
)
|
||||
|
||||
return flagSet
|
||||
}
|
||||
|
||||
func (c *Config) Logger() *slog.Logger {
|
||||
slogOptions := slog.HandlerOptions{
|
||||
AddSource: c.AddSource,
|
||||
Level: c.LogLevel.Level(),
|
||||
}
|
||||
|
||||
return slog.New(slogOptions.NewTextHandler(os.Stderr))
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
package parsing
|
||||
|
||||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/internal/semver"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/vcs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/semver"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/vcs"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/modules/buildr"
|
||||
)
|
||||
|
||||
|
@ -24,7 +25,7 @@ type BuildrConfig struct {
|
|||
GitHub GitHubConfig `hcl:"github,block"`
|
||||
}
|
||||
|
||||
func (c BuildrConfig) Buildr(buildRDir, repoRoot string) (b *buildr.Buildr, err error) {
|
||||
func (c BuildrConfig) InitBuildr(buildRDir, repoRoot string) (b *buildr.Buildr, err error) {
|
||||
b = &buildr.Buildr{
|
||||
Repo: buildr.Repo{
|
||||
Root: repoRoot,
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/zclconf/go-cty/cty/function"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/services"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/helpers/env"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/helpers/github"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/helpers/hclhelpers"
|
||||
|
@ -18,7 +17,7 @@ import (
|
|||
vaultHelpers "code.icb4dc0.de/buildr/buildr/modules/helpers/vault"
|
||||
)
|
||||
|
||||
func BasicContext(vaultInstance *vault.Vault) *hcl.EvalContext {
|
||||
func MockContext() *hcl.EvalContext {
|
||||
evalctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"env": envVars(),
|
||||
|
@ -29,7 +28,24 @@ func BasicContext(vaultInstance *vault.Vault) *hcl.EvalContext {
|
|||
env.RegisterInContext(evalctx)
|
||||
string_helpers.RegisterInContext(evalctx)
|
||||
hclhelpers.RegisterInContext(evalctx)
|
||||
vaultHelpers.RegisterInContext(evalctx, vaultInstance)
|
||||
vaultHelpers.RegisterInContext(evalctx, vaultHelpers.MockGetter("<mocked>"))
|
||||
github.RegisterInContext(context.Background(), evalctx, nil)
|
||||
|
||||
return evalctx
|
||||
}
|
||||
|
||||
func BasicContext(vaultGetter vaultHelpers.VaultGetter) *hcl.EvalContext {
|
||||
evalctx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"env": envVars(),
|
||||
},
|
||||
Functions: make(map[string]function.Function),
|
||||
}
|
||||
|
||||
env.RegisterInContext(evalctx)
|
||||
string_helpers.RegisterInContext(evalctx)
|
||||
hclhelpers.RegisterInContext(evalctx)
|
||||
vaultHelpers.RegisterInContext(evalctx, vaultGetter)
|
||||
|
||||
return evalctx
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
"code.icb4dc0.de/buildr/buildr/modules/buildr"
|
||||
)
|
||||
|
||||
func prepareBlockForParsing(block GenericBlock, modType modules.ModuleCategory, evalCtx *hcl.EvalContext, buildrInstance buildr.Buildr) (blockPreparationResult, error) {
|
||||
func prepareBlockForParsing(block GenericBlock, modType modules.Category, evalCtx *hcl.EvalContext, buildrInstance buildr.Buildr) (blockPreparationResult, error) {
|
||||
syntaxBlock, ok := block.BlockBody.(*hclsyntax.Body)
|
||||
if !ok {
|
||||
return blockPreparationResult{
|
||||
|
@ -106,7 +106,7 @@ func prepareBlockForParsing(block GenericBlock, modType modules.ModuleCategory,
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func parsingSpecOf(modType modules.ModuleCategory, block GenericBlock, additionalVariables map[string]cty.Value) blockParsingSpec {
|
||||
func parsingSpecOf(modType modules.Category, block GenericBlock, additionalVariables map[string]cty.Value) blockParsingSpec {
|
||||
if additionalVariables == nil {
|
||||
additionalVariables = make(map[string]cty.Value)
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ func parsingSpecOf(modType modules.ModuleCategory, block GenericBlock, additiona
|
|||
}
|
||||
|
||||
type blockParsingSpec struct {
|
||||
Type modules.ModuleCategory
|
||||
Type modules.Category
|
||||
Block GenericBlock
|
||||
AdditionalParsingVariables map[string]cty.Value
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ func parseSyntaxBodyToObject(body *hclsyntax.Body, evalCtx *hcl.EvalContext, con
|
|||
return attrs, nil
|
||||
}
|
||||
|
||||
func initBlock(body *hclsyntax.Body, moduleType modules.ModuleCategory, moduleName, outDir string) *hclsyntax.Body {
|
||||
func initBlock(body *hclsyntax.Body, moduleType modules.Category, moduleName, outDir string) *hclsyntax.Body {
|
||||
body.Attributes["id"] = &hclsyntax.Attribute{
|
||||
Name: "id",
|
||||
Expr: &hclsyntax.LiteralValueExpr{
|
||||
|
|
|
@ -76,7 +76,7 @@ func mapStruct(t reflect.Type, val reflect.Value, cfg MappingConfig) (cty.Value,
|
|||
|
||||
for i := 0; i < numField; i++ {
|
||||
field := t.Field(i)
|
||||
if field.IsExported() {
|
||||
if field.IsExported() && !isNil(val.Field(i)) {
|
||||
if mapped, err := mapToVal(val.Field(i).Interface(), cfg); err != nil {
|
||||
return cty.Value{}, err
|
||||
} else if field.Anonymous && mapped.CanIterateElements() {
|
||||
|
@ -129,6 +129,15 @@ func mapMap(t reflect.Type, val reflect.Value, cfg MappingConfig) (cty.Value, er
|
|||
return cty.MapVal(out), nil
|
||||
}
|
||||
|
||||
func isNil(val reflect.Value) bool {
|
||||
switch val.Kind() {
|
||||
case reflect.Pointer, reflect.Interface, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice:
|
||||
return val.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func mapType(t reflect.Type) cty.Type {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
|
|
|
@ -2,12 +2,13 @@ package parsing
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"golang.org/x/exp/slog"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/errs"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/logging"
|
||||
|
@ -23,11 +24,11 @@ func NewParser(logger *slog.Logger, fs fs.FS) *Parser {
|
|||
type Parser struct {
|
||||
logger *slog.Logger
|
||||
fs fs.FS
|
||||
body hcl.Body
|
||||
Body hcl.Body
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(ctx *hcl.EvalContext, val any) error {
|
||||
diags := gohcl.DecodeBody(p.body, ctx, val)
|
||||
diags := gohcl.DecodeBody(p.Body, ctx, val)
|
||||
if diags.HasErrors() {
|
||||
logging.Diagnostics(diags, p.logger)
|
||||
return errs.ErrAlreadyLogged
|
||||
|
@ -64,7 +65,7 @@ func (p *Parser) ReadFiles() error {
|
|||
return err
|
||||
}
|
||||
|
||||
p.body = hcl.MergeFiles(files)
|
||||
p.Body = hcl.MergeFiles(files)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"code.icb4dc0.de/buildr/buildr/modules/buildr"
|
||||
)
|
||||
|
||||
type RawSpec struct {
|
||||
type ModulesSpec struct {
|
||||
Locals []LocalsBlock `hcl:"locals,block"`
|
||||
Tools GenericBlocks `hcl:"tool,block"`
|
||||
Tasks GenericBlocks `hcl:"task,block"`
|
||||
|
@ -16,12 +16,10 @@ type RawSpec struct {
|
|||
Packages GenericBlocks `hcl:"package,block"`
|
||||
}
|
||||
|
||||
func (s RawSpec) Repository(evalCtx *hcl.EvalContext, b buildr.Buildr, registry *modules.TypeRegistry) (repo *modules.Repository, err error) {
|
||||
repo = &modules.Repository{
|
||||
Buildr: b,
|
||||
}
|
||||
func (s ModulesSpec) Repository(evalCtx *hcl.EvalContext, b buildr.Buildr, registry *modules.TypeRegistry) (repo *modules.Repository, err error) {
|
||||
repo = &modules.Repository{}
|
||||
|
||||
if evalCtx.Variables["buildr"], err = mapToVal(repo.Buildr, MappingConfig{}); err != nil {
|
||||
if evalCtx.Variables["buildr"], err = mapToVal(b, MappingConfig{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -31,25 +29,25 @@ func (s RawSpec) Repository(evalCtx *hcl.EvalContext, b buildr.Buildr, registry
|
|||
|
||||
parsingSpecs := make([]blockParsingSpec, 0, len(s.Tools)+len(s.Tasks)+len(s.Builds))
|
||||
|
||||
if specs, err := s.buildParsingInventory(modules.ModuleCategoryTool, s.Tools, evalCtx, b); err != nil {
|
||||
if specs, err := s.buildParsingInventory(modules.CategoryTool, s.Tools, evalCtx, b); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
parsingSpecs = append(parsingSpecs, specs...)
|
||||
}
|
||||
|
||||
if specs, err := s.buildParsingInventory(modules.ModuleCategoryTask, s.Tasks, evalCtx, b); err != nil {
|
||||
if specs, err := s.buildParsingInventory(modules.CategoryTask, s.Tasks, evalCtx, b); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
parsingSpecs = append(parsingSpecs, specs...)
|
||||
}
|
||||
|
||||
if specs, err := s.buildParsingInventory(modules.ModuleCategoryBuild, s.Builds, evalCtx, b); err != nil {
|
||||
if specs, err := s.buildParsingInventory(modules.CategoryBuild, s.Builds, evalCtx, b); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
parsingSpecs = append(parsingSpecs, specs...)
|
||||
}
|
||||
|
||||
if specs, err := s.buildParsingInventory(modules.ModuleCategoryPackage, s.Packages, evalCtx, b); err != nil {
|
||||
if specs, err := s.buildParsingInventory(modules.CategoryPackage, s.Packages, evalCtx, b); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
parsingSpecs = append(parsingSpecs, specs...)
|
||||
|
@ -64,8 +62,8 @@ func (s RawSpec) Repository(evalCtx *hcl.EvalContext, b buildr.Buildr, registry
|
|||
return repo, nil
|
||||
}
|
||||
|
||||
func (s RawSpec) buildParsingInventory(
|
||||
modType modules.ModuleCategory,
|
||||
func (s ModulesSpec) buildParsingInventory(
|
||||
modType modules.Category,
|
||||
blocks []GenericBlock,
|
||||
evalCtx *hcl.EvalContext,
|
||||
buildrInstance buildr.Buildr,
|
||||
|
@ -94,7 +92,7 @@ func (s RawSpec) buildParsingInventory(
|
|||
return blockGroupParsingSpecs, nil
|
||||
}
|
||||
|
||||
func (s RawSpec) parseBlocksToModules(specs []blockParsingSpec, registry *modules.TypeRegistry, evalCtx *hcl.EvalContext) ([]modules.ModuleWithMeta, error) {
|
||||
func (s ModulesSpec) parseBlocksToModules(specs []blockParsingSpec, registry *modules.TypeRegistry, evalCtx *hcl.EvalContext) ([]modules.ModuleWithMeta, error) {
|
||||
parsedModules := make([]modules.ModuleWithMeta, 0, len(specs))
|
||||
|
||||
for i := range specs {
|
||||
|
|
|
@ -1,10 +1,32 @@
|
|||
package profiling
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
OutFile string `mapstructure:"pprof-out-file"`
|
||||
ProfileName string `mapstructure:"pprof-profile-name"`
|
||||
}
|
||||
|
||||
func (c Config) IsConfigured() bool {
|
||||
func (c *Config) IsConfigured() bool {
|
||||
return c.ProfileName != "" && c.OutFile != ""
|
||||
}
|
||||
|
||||
func (c *Config) AddFlags(fs *flag.FlagSet) {
|
||||
fs.StringVar(
|
||||
&c.OutFile,
|
||||
"pprof.out-file",
|
||||
config.StringEnvOr("BUILDR_PPROF_OUT_FILE", ""),
|
||||
"Output file for PPROF profiling data.",
|
||||
)
|
||||
|
||||
fs.StringVar(
|
||||
&c.ProfileName,
|
||||
"pprof.profile-name",
|
||||
config.StringEnvOr("BUILDR_PPROF_PROFILE_NAME", ""),
|
||||
"Name of the PPROF profiling data.",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
package rpc
|
||||
|
||||
import "flag"
|
||||
|
||||
type GrpcConfig struct {
|
||||
Host struct {
|
||||
Address string `mapstructure:"grpc-serve-address"`
|
||||
} `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func (c *GrpcConfig) Flags() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("grpc", flag.ExitOnError)
|
||||
|
||||
fs.StringVar(
|
||||
&c.Host.Address,
|
||||
"grpc-serve-address",
|
||||
":3000",
|
||||
"Address on which the gRPC server will listen",
|
||||
)
|
||||
|
||||
return fs
|
||||
}
|
||||
|
|
|
@ -21,24 +21,24 @@ import (
|
|||
|
||||
func NewServer(
|
||||
logger *slog.Logger,
|
||||
svc *services.Collection,
|
||||
typeRegistryAccess services.TypeRegistryAccessor,
|
||||
) *BuildrAPI {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
|
||||
return &BuildrAPI{
|
||||
logger: logger,
|
||||
svc: svc,
|
||||
logger: logger,
|
||||
typeRegistryAccess: typeRegistryAccess,
|
||||
}
|
||||
}
|
||||
|
||||
type BuildrAPI struct {
|
||||
lock sync.Mutex
|
||||
logger *slog.Logger
|
||||
svc *services.Collection
|
||||
server *grpc.Server
|
||||
serverRunning chan struct{}
|
||||
lock sync.Mutex
|
||||
logger *slog.Logger
|
||||
typeRegistryAccess services.TypeRegistryAccessor
|
||||
server *grpc.Server
|
||||
serverRunning chan struct{}
|
||||
}
|
||||
|
||||
func (a *BuildrAPI) Start(address string) (err error) {
|
||||
|
@ -66,7 +66,7 @@ func (a *BuildrAPI) Start(address string) (err error) {
|
|||
|
||||
v1Health.RegisterHealthServer(a.server, v1.NewHealthServer(a.logger))
|
||||
|
||||
rpcv1.RegisterExecutorServiceServer(a.server, v1.NewExecutorServiceServer(a.svc.TypeRegistry()))
|
||||
rpcv1.RegisterExecutorServiceServer(a.server, v1.NewExecutorServiceServer(a.typeRegistryAccess.TypeRegistry()))
|
||||
|
||||
reflection.Register(a.server)
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ func (e *ExecutorServiceServer) ExecuteTask(server rpcv1.ExecutorService_Execute
|
|||
return status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
mod, err := e.registry.CreateFromJSON(modules.ModuleCategory(t.GetReference().GetModuleType()), t.GetReference().GetModuleName(), t.GetRawTask())
|
||||
mod, err := e.registry.CreateFromJSON(modules.Category(t.GetReference().GetModuleType()), t.GetReference().GetModuleName(), t.GetRawTask())
|
||||
if err != nil {
|
||||
logger.Error("Failed to unmarshal module from JSON", slog.String("err", err.Error()))
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
|
|
|
@ -37,11 +37,13 @@ func (g GrpcExecutorHandler) Handle(_ context.Context, record slog.Record) error
|
|||
Attributes: make([]*rpcv1.TaskLog_LogAttribute, 0, record.NumAttrs()),
|
||||
}
|
||||
|
||||
record.Attrs(func(attr slog.Attr) {
|
||||
record.Attrs(func(attr slog.Attr) bool {
|
||||
taskLog.Attributes = append(taskLog.Attributes, &rpcv1.TaskLog_LogAttribute{
|
||||
Key: attr.Key,
|
||||
Value: attr.Value.String(),
|
||||
})
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
resp := rpcv1.ExecuteTaskResponse{
|
||||
|
|
35
internal/services/api.go
Normal file
35
internal/services/api.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ignore"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
"github.com/docker/docker/client"
|
||||
gh "github.com/google/go-github/v50/github"
|
||||
)
|
||||
|
||||
type (
|
||||
CollectionModifier interface {
|
||||
With(opts ...CollectionOption) error
|
||||
}
|
||||
|
||||
VaultAccessor interface {
|
||||
Vault() *vault.Vault
|
||||
}
|
||||
|
||||
TypeRegistryAccessor interface {
|
||||
TypeRegistry() *modules.TypeRegistry
|
||||
}
|
||||
|
||||
IgnoreAccessor interface {
|
||||
Ignorer() *ignore.Ignorer
|
||||
}
|
||||
|
||||
DockerClientAccessor interface {
|
||||
DockerClient() *client.Client
|
||||
}
|
||||
|
||||
GitHubClientAccessor interface {
|
||||
GitHubClient() *gh.Client
|
||||
}
|
||||
)
|
|
@ -10,7 +10,6 @@ import (
|
|||
"code.icb4dc0.de/buildr/buildr/internal/ignore"
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
"code.icb4dc0.de/buildr/buildr/modules/buildr"
|
||||
)
|
||||
|
||||
type CollectionOption interface {
|
||||
|
@ -44,17 +43,6 @@ func WithIgnorer(ignorer *ignore.Ignorer) CollectionOption {
|
|||
})
|
||||
}
|
||||
|
||||
func WithIgnorerFromBuildr(b *buildr.Buildr, additionalPatterns ...string) CollectionOption {
|
||||
return collectionOptionFunc(func(svc *Collection) error {
|
||||
if ig, err := ignore.NewRootIgnorer(b, additionalPatterns...); err != nil {
|
||||
return err
|
||||
} else {
|
||||
svc.ignorer = ig
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func WithDockerClientFromEnv(ctx context.Context) CollectionOption {
|
||||
return collectionOptionFunc(func(svc *Collection) error {
|
||||
cli, err := client.NewClientWithOpts(
|
||||
|
|
|
@ -1,17 +1,5 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/modules/vcs"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
)
|
||||
|
||||
var ErrRepoRootNotFound = errors.New("failed to detect repo root")
|
||||
|
||||
type Config struct {
|
||||
BuildR struct {
|
||||
RepoRoot string `mapstructure:"repo-root"`
|
||||
|
@ -23,92 +11,3 @@ type Config struct {
|
|||
PassphraseFile string `mapstructure:"vault-passphrase-file"`
|
||||
} `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func (cfg *Config) Init(vcsType vcs.Type) error {
|
||||
if cfg.BuildR.RepoRoot == "" {
|
||||
if root, err := repoRootFromCurrentWorkingDir(vcsType); err != nil {
|
||||
return err
|
||||
} else {
|
||||
cfg.BuildR.RepoRoot = root
|
||||
}
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(cfg.BuildR.BuildrDirectory) {
|
||||
cfg.BuildR.BuildrDirectory = filepath.Join(cfg.BuildR.RepoRoot, cfg.BuildR.BuildrDirectory)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) PrepareVault() (v *vault.Vault, err error) {
|
||||
if passphrase := cfg.Vault.Passphrase; passphrase != "" {
|
||||
v = vault.NewVault(passphrase, vault.NewAesGcmEncryption(vault.Pbkdf2Deriver()))
|
||||
}
|
||||
|
||||
if filePath := cfg.Vault.PassphraseFile; filePath != "" {
|
||||
if !filepath.IsAbs(filePath) {
|
||||
filePath = filepath.Join(cfg.BuildR.RepoRoot, filePath)
|
||||
}
|
||||
if passphrase, err := os.ReadFile(filePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
v = vault.NewVault(string(passphrase), vault.NewAesGcmEncryption(vault.Pbkdf2Deriver()))
|
||||
}
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(cfg.Vault.FilePath) {
|
||||
cfg.Vault.FilePath = filepath.Join(cfg.BuildR.RepoRoot, cfg.Vault.FilePath)
|
||||
}
|
||||
|
||||
vaultFile, err := os.Open(cfg.Vault.FilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return v, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = vaultFile.Close()
|
||||
}()
|
||||
|
||||
dec := json.NewDecoder(vaultFile)
|
||||
if err = dec.Decode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func repoRootFromCurrentWorkingDir(vcsType vcs.Type) (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return findRepoRoot(cwd, vcsType)
|
||||
}
|
||||
|
||||
func findRepoRoot(dir string, vcsType vcs.Type) (string, error) {
|
||||
if dir == "" {
|
||||
return "", ErrRepoRootNotFound
|
||||
}
|
||||
|
||||
info, err := os.Stat(filepath.Join(dir, vcsType.Directory()))
|
||||
if err != nil {
|
||||
return findRepoRoot(filepath.Dir(dir), vcsType)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
return findRepoRoot(filepath.Dir(dir), vcsType)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
package vault
|
||||
|
||||
type InitOption interface {
|
||||
applyToVault(v *Vault) error
|
||||
}
|
||||
|
||||
type initOptionFunc func(*Vault) error
|
||||
|
||||
func (f initOptionFunc) applyToVault(v *Vault) error {
|
||||
return f(v)
|
||||
}
|
||||
|
||||
type Encryption interface {
|
||||
Encrypter
|
||||
Decrypter
|
||||
|
|
55
internal/vault/options.go
Normal file
55
internal/vault/options.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
func WithPassphrase(passphrase string) InitOption {
|
||||
return initOptionFunc(func(vault *Vault) error {
|
||||
vault.passphrase = passphrase
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func WithPassphraseFile(path string) InitOption {
|
||||
return initOptionFunc(func(vault *Vault) error {
|
||||
passphrase, 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
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func WithEncryption(encryption Encryption) InitOption {
|
||||
return initOptionFunc(func(vault *Vault) error {
|
||||
vault.encryption = encryption
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
package vault_test
|
||||
|
||||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
"code.icb4dc0.de/prskr/go-pwgen"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
"code.icb4dc0.de/prskr/go-pwgen"
|
||||
)
|
||||
|
||||
func TestPbkdf2Deriver(t *testing.T) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
@ -59,12 +60,22 @@ func (e Entry) Decrypt(decrypter Decrypter, passphrase string) ([]byte, error) {
|
|||
return decrypter.Decrypt(e.Value, passphrase, e.Salt, e.Nonce)
|
||||
}
|
||||
|
||||
func NewVault(passphrase string, encrypter Encryption) *Vault {
|
||||
return &Vault{
|
||||
encryption: encrypter,
|
||||
passphrase: passphrase,
|
||||
func NewVault(opts ...InitOption) (*Vault, error) {
|
||||
v := &Vault{
|
||||
encryption: NewAesGcmEncryption(Pbkdf2Deriver()),
|
||||
entries: make(map[string]Entry),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt.applyToVault(v); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type Vault struct {
|
||||
|
|
4
main.go
4
main.go
|
@ -23,7 +23,9 @@ func main() {
|
|||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
|
||||
defer cancel()
|
||||
|
||||
if err := cmd.Execute(ctx); err != nil {
|
||||
app := cmd.NewApp()
|
||||
|
||||
if err := app.Run(ctx); err != nil {
|
||||
if !errors.Is(err, errs.ErrAlreadyLogged) {
|
||||
slog.Error("Error occurred", slog.Any("error", err))
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ type ModuleWithMeta interface {
|
|||
|
||||
type Module interface {
|
||||
Execute(ctx ExecutionContext) error
|
||||
Category() ModuleCategory
|
||||
Category() Category
|
||||
Type() string
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@ package modules
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"reflect"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
)
|
||||
|
@ -126,7 +127,7 @@ func (m *Metadata[T]) Execute(ctx ExecutionContext) error {
|
|||
return m.Module.Execute(ctx)
|
||||
}
|
||||
|
||||
func (m *Metadata[T]) Category() ModuleCategory {
|
||||
func (m *Metadata[T]) Category() Category {
|
||||
return m.Module.Category()
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,8 @@ func (g GoBuild) Type() string {
|
|||
return "go_build"
|
||||
}
|
||||
|
||||
func (g GoBuild) Category() modules.ModuleCategory {
|
||||
return modules.ModuleCategoryBuild
|
||||
func (g GoBuild) Category() modules.Category {
|
||||
return modules.CategoryBuild
|
||||
}
|
||||
|
||||
func (g GoBuild) Execute(ctx modules.ExecutionContext) (err error) {
|
||||
|
|
|
@ -32,7 +32,7 @@ func GetLatestReleaseTag(ctx context.Context, cli *github.Client) function.Funct
|
|||
repo := args[1].AsString()
|
||||
|
||||
if cli == nil {
|
||||
cli = github.NewClient(nil)
|
||||
return cty.StringVal("<mocked>"), nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, DefaultAPITimeout)
|
||||
|
|
|
@ -2,10 +2,18 @@ package vault
|
|||
|
||||
import (
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
)
|
||||
|
||||
func RegisterInContext(evalCtx *hcl.EvalContext, vault *vault.Vault) {
|
||||
evalCtx.Functions["from_vault"] = FromVault(vault)
|
||||
type VaultGetter interface {
|
||||
GetValue(entryKey string) ([]byte, error)
|
||||
}
|
||||
|
||||
type MockGetter []byte
|
||||
|
||||
func (m MockGetter) GetValue(string) ([]byte, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func RegisterInContext(evalCtx *hcl.EvalContext, getter VaultGetter) {
|
||||
evalCtx.Functions["from_vault"] = FromVault(getter)
|
||||
}
|
||||
|
|
|
@ -5,11 +5,9 @@ import (
|
|||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/vault"
|
||||
)
|
||||
|
||||
func FromVault(vaultInstance *vault.Vault) function.Function {
|
||||
func FromVault(getter VaultGetter) function.Function {
|
||||
return function.New(&function.Spec{
|
||||
Description: "Get value from vault",
|
||||
Params: []function.Parameter{
|
||||
|
@ -21,10 +19,10 @@ func FromVault(vaultInstance *vault.Vault) function.Function {
|
|||
},
|
||||
Type: function.StaticReturnType(cty.String),
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
if vaultInstance == nil {
|
||||
if getter == nil {
|
||||
return cty.StringVal(""), errors.New("vault is not initialized - cannot read value")
|
||||
}
|
||||
val, err := vaultInstance.GetValue(args[0].AsString())
|
||||
val, err := getter.GetValue(args[0].AsString())
|
||||
if err != nil {
|
||||
return cty.Value{}, err
|
||||
}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
package archive
|
||||
|
||||
import (
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ioutils"
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"golang.org/x/exp/slog"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"code.icb4dc0.de/buildr/buildr/internal/ioutils"
|
||||
"code.icb4dc0.de/buildr/buildr/modules"
|
||||
|
||||
"github.com/klauspost/compress/zip"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
var _ modules.Module = (*ZipArchive)(nil)
|
||||
|
@ -20,8 +22,8 @@ type ZipArchive struct {
|
|||
WrapInDir string `hcl:"wrap_in_directory,optional"`
|
||||
}
|
||||
|
||||
func (z ZipArchive) Category() modules.ModuleCategory {
|
||||
return modules.ModuleCategoryPackage
|
||||
func (z ZipArchive) Category() modules.Category {
|
||||
return modules.CategoryPackage
|
||||
}
|
||||
|
||||
func (z ZipArchive) Type() string {
|
||||
|
|
|
@ -48,8 +48,8 @@ func (o ContainerImage) Type() string {
|
|||
return "container_image"
|
||||
}
|
||||
|
||||
func (o ContainerImage) Category() modules.ModuleCategory {
|
||||
return modules.ModuleCategoryPackage
|
||||
func (o ContainerImage) Category() modules.Category {
|
||||
return modules.CategoryPackage
|
||||
}
|
||||
|
||||
func (o ContainerImage) Execute(ctx modules.ExecutionContext) error {
|
||||
|
|
|
@ -38,7 +38,7 @@ type TypeRegistry struct {
|
|||
registrations map[moduleSpec]Factory
|
||||
}
|
||||
|
||||
func (r *TypeRegistry) CreateFromJSON(moduleCategory ModuleCategory, moduleType string, raw []byte) (Module, error) {
|
||||
func (r *TypeRegistry) CreateFromJSON(moduleCategory Category, moduleType string, raw []byte) (Module, error) {
|
||||
s := specOf(moduleCategory, moduleType)
|
||||
if m, ok := r.registrations[s]; ok {
|
||||
module := m.Create()
|
||||
|
@ -53,7 +53,7 @@ func (r *TypeRegistry) CreateFromJSON(moduleCategory ModuleCategory, moduleType
|
|||
}
|
||||
|
||||
func (r *TypeRegistry) CreateFromHCL(
|
||||
moduleCategory ModuleCategory,
|
||||
moduleCategory Category,
|
||||
moduleType string,
|
||||
body hcl.Body,
|
||||
hclCtx *hcl.EvalContext,
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
package modules
|
||||
|
||||
import "code.icb4dc0.de/buildr/buildr/modules/buildr"
|
||||
|
||||
type Repository struct {
|
||||
Buildr buildr.Buildr
|
||||
|
||||
modulesById map[string]ModuleWithMeta
|
||||
moduleByName map[moduleSpec]ModuleWithMeta
|
||||
}
|
||||
|
@ -26,15 +22,15 @@ func (s *Repository) RegisterModules(modules ...ModuleWithMeta) {
|
|||
}
|
||||
|
||||
func (s *Repository) Tools() map[string]ModuleWithMeta {
|
||||
return s.ModulesByType(ModuleCategoryTool)
|
||||
return s.ModulesByCategory(CategoryTool)
|
||||
}
|
||||
|
||||
func (s *Repository) Tasks() map[string]ModuleWithMeta {
|
||||
return s.ModulesByType(ModuleCategoryTask)
|
||||
return s.ModulesByCategory(CategoryTask)
|
||||
}
|
||||
|
||||
func (s *Repository) Builds() map[string]ModuleWithMeta {
|
||||
return s.ModulesByType(ModuleCategoryBuild)
|
||||
return s.ModulesByCategory(CategoryBuild)
|
||||
}
|
||||
|
||||
func (s *Repository) ModuleById(id string) ModuleWithMeta {
|
||||
|
@ -46,7 +42,7 @@ func (s *Repository) ModuleById(id string) ModuleWithMeta {
|
|||
return module
|
||||
}
|
||||
|
||||
func (s *Repository) Module(moduleType ModuleCategory, moduleName string) ModuleWithMeta {
|
||||
func (s *Repository) Module(moduleType Category, moduleName string) ModuleWithMeta {
|
||||
module, ok := s.moduleByName[specOf(moduleType, moduleName)]
|
||||
if !ok {
|
||||
return nil
|
||||
|
@ -55,7 +51,7 @@ func (s *Repository) Module(moduleType ModuleCategory, moduleName string) Module
|
|||
return module
|
||||
}
|
||||
|
||||
func (s *Repository) ModulesByType(moduleType ModuleCategory) map[string]ModuleWithMeta {
|
||||
func (s *Repository) ModulesByCategory(moduleType Category) map[string]ModuleWithMeta {
|
||||
out := make(map[string]ModuleWithMeta, len(s.moduleByName))
|
||||
for spec, module := range s.moduleByName {
|
||||
if spec.TypeName == moduleType {
|
||||
|
@ -66,7 +62,7 @@ func (s *Repository) ModulesByType(moduleType ModuleCategory) map[string]ModuleW
|
|||
return out
|
||||
}
|
||||
|
||||
func specOf(typeName ModuleCategory, moduleName string) moduleSpec {
|
||||
func specOf(typeName Category, moduleName string) moduleSpec {
|
||||
return moduleSpec{
|
||||
TypeName: typeName,
|
||||
ModuleName: moduleName,
|
||||
|
@ -74,6 +70,6 @@ func specOf(typeName ModuleCategory, moduleName string) moduleSpec {
|
|||
}
|
||||
|
||||
type moduleSpec struct {
|
||||
TypeName ModuleCategory
|
||||
TypeName Category
|
||||
ModuleName string
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ func (s ScriptTask) Type() string {
|
|||
return "script"
|
||||
}
|
||||
|
||||
func (s ScriptTask) Category() modules.ModuleCategory {
|
||||
return modules.ModuleCategoryTask
|
||||
func (s ScriptTask) Category() modules.Category {
|
||||
return modules.CategoryTask
|
||||
}
|
||||
|
||||
func (s ScriptTask) Execute(ctx modules.ExecutionContext) (err error) {
|
||||
|
|
|
@ -61,8 +61,8 @@ func (g GoTool) Dependencies() []string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (g GoTool) Category() modules.ModuleCategory {
|
||||
return modules.ModuleCategoryTool
|
||||
func (g GoTool) Category() modules.Category {
|
||||
return modules.CategoryTool
|
||||
}
|
||||
|
||||
func (g GoTool) Execute(ctx modules.ExecutionContext) (err error) {
|
||||
|
|
|
@ -2,19 +2,19 @@ package modules
|
|||
|
||||
import "fmt"
|
||||
|
||||
type ModuleCategory string
|
||||
type Category string
|
||||
|
||||
func (t ModuleCategory) String() string {
|
||||
func (t Category) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
func (t ModuleCategory) GroupName() string {
|
||||
func (t Category) GroupName() string {
|
||||
return fmt.Sprintf("%ss", t)
|
||||
}
|
||||
|
||||
const (
|
||||
ModuleCategoryTool ModuleCategory = "tool"
|
||||
ModuleCategoryTask ModuleCategory = "task"
|
||||
ModuleCategoryBuild ModuleCategory = "build"
|
||||
ModuleCategoryPackage ModuleCategory = "package"
|
||||
CategoryTool Category = "tool"
|
||||
CategoryTask Category = "task"
|
||||
CategoryBuild Category = "build"
|
||||
CategoryPackage Category = "package"
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ package vcs
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
package vcs
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/spf13/pflag"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
_ pflag.Value = (*Type)(nil)
|
||||
vcsTypes = map[string]Type{
|
||||
_ flag.Value = (*Type)(nil)
|
||||
_ encoding.TextUnmarshaler = (*Type)(nil)
|
||||
vcsTypes = map[string]Type{
|
||||
"git": TypeGit,
|
||||
}
|
||||
vcsDir = map[Type]string{
|
||||
|
@ -20,8 +22,28 @@ const (
|
|||
TypeGit Type = "git"
|
||||
)
|
||||
|
||||
func ParseType(s string) (Type, error) {
|
||||
var t Type
|
||||
if err := t.UnmarshalText([]byte(s)); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
type Type string
|
||||
|
||||
func (t *Type) UnmarshalText(text []byte) error {
|
||||
txt := string(text)
|
||||
if knownType, ok := vcsTypes[txt]; !ok {
|
||||
return fmt.Errorf("unknown VCS type %q", t)
|
||||
} else {
|
||||
*t = knownType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Type) String() string {
|
||||
s := string(*t)
|
||||
return s
|
||||
|
|
Loading…
Reference in a new issue