refactor(cli): restructure initialization logic and get rid of global state

This commit is contained in:
Peter 2023-05-01 10:15:53 +02:00
parent c60fc4b347
commit fd2aebaa3b
No known key found for this signature in database
58 changed files with 1305 additions and 925 deletions

View file

@ -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
View 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
View 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
View 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
}

View 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
View 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
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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))
}

View file

@ -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
View 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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
View 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
}
}

View file

@ -29,7 +29,6 @@ type ContainerProber struct {
}
func (p *ContainerProber) WaitReady(ctx context.Context) error {
waitCtx, cancel := context.WithCancel(ctx)
monitorErr := p.inspectMonitor(ctx)

View file

@ -10,7 +10,7 @@ import (
)
func NewPlanFor(
moduleType modules.ModuleCategory,
moduleType modules.Category,
moduleName string,
repo *modules.Repository,
factory *TaskFactory,

View file

@ -1,6 +1,8 @@
package logging
import "io"
import (
"io"
)
type TaskOutputSink interface {
StdOut() io.Writer

46
internal/logging/setup.go Normal file
View 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))
}

View file

@ -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,

View file

@ -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
}

View file

@ -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
}

View file

@ -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{

View file

@ -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:

View file

@ -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
}

View file

@ -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 {

View file

@ -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.",
)
}

View file

@ -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
}

View file

@ -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)

View file

@ -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())

View file

@ -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
View 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
}
)

View file

@ -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(

View file

@ -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)
}

View file

@ -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
View 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
})
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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))
}

View file

@ -33,7 +33,7 @@ type ModuleWithMeta interface {
type Module interface {
Execute(ctx ExecutionContext) error
Category() ModuleCategory
Category() Category
Type() string
}

View file

@ -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()
}

View file

@ -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) {

View file

@ -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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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"
)

View file

@ -2,6 +2,7 @@ package vcs
import (
"fmt"
"github.com/go-git/go-git/v5"
)

View file

@ -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