diff --git a/forge/cli/blueprint.cue b/forge/cli/blueprint.cue index 84e1f892..648faf4d 100644 --- a/forge/cli/blueprint.cue +++ b/forge/cli/blueprint.cue @@ -22,5 +22,6 @@ project: { "darwin/arm64", ] } + test: retries: 3 } } diff --git a/forge/cli/cmd/cmds/run.go b/forge/cli/cmd/cmds/run.go index 5adaf4b3..bbaa8860 100644 --- a/forge/cli/cmd/cmds/run.go +++ b/forge/cli/cmd/cmds/run.go @@ -1,11 +1,9 @@ package cmds import ( - "fmt" "log/slog" - "strings" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthly" + "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthfile" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/executor" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/secrets" ) @@ -21,40 +19,33 @@ type RunCmd struct { } func (c *RunCmd) Run(logger *slog.Logger) error { - if !strings.Contains(c.Path, "+") { - return fmt.Errorf("invalid Earthfile+Target pair: %s", c.Path) + ref, err := earthfile.ParseEarthfileRef(c.Path) + if err != nil { + return err } - earthfileDir := strings.Split(c.Path, "+")[0] - target := strings.Split(c.Path, "+")[1] - - project, err := loadProject(earthfileDir, logger) + project, err := loadProject(ref.Path, logger) if err != nil { return err } + logger.Info("Executing Earthly target", "project", project.Path, "target", ref.Target) localExec := executor.NewLocalExecutor( logger, executor.WithRedirect(), ) - - opts := generateOpts(target, c, &project.Blueprint) - earthlyExec := earthly.NewEarthlyExecutor( - earthfileDir, - target, + result, err := project.RunTarget( + ref.Target, + c.CI, + c.Local, localExec, secrets.NewDefaultSecretStore(), - logger, - opts..., + generateOpts(c)..., ) - - logger.Info("Executing Earthly target", "earthfile", earthfileDir, "target", target) - result, err := earthlyExec.Run() if err != nil { return err } printJson(result, c.Pretty) - return nil } diff --git a/forge/cli/cmd/cmds/scan.go b/forge/cli/cmd/cmds/scan.go index 406f074f..ccd419d2 100644 --- a/forge/cli/cmd/cmds/scan.go +++ b/forge/cli/cmd/cmds/scan.go @@ -26,7 +26,7 @@ type ScanCmd struct { func (c *ScanCmd) Run(logger *slog.Logger) error { walker := walker.NewDefaultFSWalker(logger) - loader := project.NewDefaultProjectLoader(loadRuntimes(logger), logger) + loader := project.NewDefaultProjectLoader(project.GetDefaultRuntimes(logger), logger) var rootPath string if c.Absolute { diff --git a/forge/cli/cmd/cmds/util.go b/forge/cli/cmd/cmds/util.go index 38b1a87e..6ff1a09a 100644 --- a/forge/cli/cmd/cmds/util.go +++ b/forge/cli/cmd/cmds/util.go @@ -5,7 +5,6 @@ import ( "fmt" "log/slog" - "github.com/input-output-hk/catalyst-forge/blueprint/schema" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthly" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/project" ) @@ -22,50 +21,11 @@ func enumerate(data map[string][]string) []string { return result } -// generateOpts generates the options for the Earthly executor based on the configuration file and flags. -func generateOpts(target string, flags *RunCmd, config *schema.Blueprint) []earthly.EarthlyExecutorOption { +// generateOpts generates the options for the Earthly executor based on command +// flags. +func generateOpts(flags *RunCmd) []earthly.EarthlyExecutorOption { var opts []earthly.EarthlyExecutorOption - if config != nil { - if _, ok := config.Project.CI.Targets[target]; ok { - targetConfig := config.Project.CI.Targets[target] - - if len(targetConfig.Args) > 0 { - var args []string - for k, v := range targetConfig.Args { - args = append(args, fmt.Sprintf("--%s", k), v) - } - - opts = append(opts, earthly.WithTargetArgs(args...)) - } - - // We only run multiple platforms in CI mode to avoid issues with local builds. - if targetConfig.Platforms != nil && flags.CI { - opts = append(opts, earthly.WithPlatforms(targetConfig.Platforms...)) - } - - if targetConfig.Privileged != nil && *targetConfig.Privileged { - opts = append(opts, earthly.WithPrivileged()) - } - - if targetConfig.Retries != nil { - opts = append(opts, earthly.WithRetries(*targetConfig.Retries)) - } - - if len(targetConfig.Secrets) > 0 { - opts = append(opts, earthly.WithSecrets(targetConfig.Secrets)) - } - } - - if config.Global.CI.Providers.Earthly.Satellite != nil && !flags.Local { - opts = append(opts, earthly.WithSatellite(*config.Global.CI.Providers.Earthly.Satellite)) - } - - if len(config.Global.CI.Secrets) > 0 { - opts = append(opts, earthly.WithSecrets(config.Global.CI.Secrets)) - } - } - if flags != nil { if flags.Artifact != "" { opts = append(opts, earthly.WithArtifact(flags.Artifact)) @@ -80,7 +40,7 @@ func generateOpts(target string, flags *RunCmd, config *schema.Blueprint) []eart opts = append(opts, earthly.WithPlatforms(flags.Platform...)) } - if flags.TargetArgs != nil && len(flags.TargetArgs) > 0 && flags.TargetArgs[0] != "" { + if len(flags.TargetArgs) > 0 && flags.TargetArgs[0] != "" { opts = append(opts, earthly.WithTargetArgs(flags.TargetArgs...)) } } @@ -90,17 +50,10 @@ func generateOpts(target string, flags *RunCmd, config *schema.Blueprint) []eart // loadProject loads the project from the given root path. func loadProject(rootPath string, logger *slog.Logger) (project.Project, error) { - loader := project.NewDefaultProjectLoader(loadRuntimes(logger), logger) + loader := project.NewDefaultProjectLoader(project.GetDefaultRuntimes(logger), logger) return loader.Load(rootPath) } -// loadRuntimes loads the all runtime data collectors. -func loadRuntimes(logger *slog.Logger) []project.RuntimeData { - return []project.RuntimeData{ - project.NewGitRuntime(logger), - } -} - // printJson prints the given data as a JSON string. func printJson(data interface{}, pretty bool) { var out []byte diff --git a/forge/cli/pkg/earthfile/earthfile.go b/forge/cli/pkg/earthfile/earthfile.go index fa97e6bb..24fd19cd 100644 --- a/forge/cli/pkg/earthfile/earthfile.go +++ b/forge/cli/pkg/earthfile/earthfile.go @@ -2,6 +2,8 @@ package earthfile import ( "context" + "fmt" + "strings" "github.com/earthly/earthly/ast" "github.com/earthly/earthly/ast/spec" @@ -13,6 +15,12 @@ type Earthfile struct { spec spec.Earthfile } +// EarthfileRef represents a reference to an Earthfile and a target. +type EarthfileRef struct { + Path string + Target string +} + // Targets returns the names of the targets in the Earthfile. func (e Earthfile) Targets() []string { var targetNames []string @@ -52,6 +60,20 @@ func ParseEarthfile(ctx context.Context, earthfile walker.FileSeeker) (Earthfile }, nil } +// ParseEarthfileRef parses an Earthfile+Target pair. +func ParseEarthfileRef(ref string) (EarthfileRef, error) { + parts := strings.Split(ref, "+") + + if len(parts) != 2 { + return EarthfileRef{}, fmt.Errorf("invalid Earthfile+Target pair: %s", ref) + } + + return EarthfileRef{ + Path: parts[0], + Target: parts[1], + }, nil +} + // namedReader is a FileSeeker that also provides the name of the file. // This is used to interopt with the Earthly AST parser. type namedReader struct { diff --git a/forge/cli/pkg/project/loader.go b/forge/cli/pkg/project/loader.go index b8cc8e98..5b144f07 100644 --- a/forge/cli/pkg/project/loader.go +++ b/forge/cli/pkg/project/loader.go @@ -119,6 +119,7 @@ func (p *DefaultProjectLoader) Load(projectPath string) (Project, error) { Path: projectPath, Repo: repo, RepoRoot: gitRoot, + logger: p.logger, rawBlueprint: rbp, }, nil } diff --git a/forge/cli/pkg/project/project.go b/forge/cli/pkg/project/project.go index a627129d..713df954 100644 --- a/forge/cli/pkg/project/project.go +++ b/forge/cli/pkg/project/project.go @@ -2,6 +2,7 @@ package project import ( "fmt" + "log/slog" "path/filepath" "strings" @@ -9,6 +10,9 @@ import ( "github.com/input-output-hk/catalyst-forge/blueprint/pkg/blueprint" "github.com/input-output-hk/catalyst-forge/blueprint/schema" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthfile" + "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthly" + "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/executor" + "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/secrets" ) type TagInfo struct { @@ -25,14 +29,10 @@ type Project struct { Repo *gg.Repository RepoRoot string Tags TagInfo + logger *slog.Logger rawBlueprint blueprint.RawBlueprint } -// Raw returns the raw blueprint. -func (p *Project) Raw() blueprint.RawBlueprint { - return p.rawBlueprint -} - // GetRelativePath returns the relative path of the project from the repo root. func (p *Project) GetRelativePath() (string, error) { var projectPath, repoRoot string @@ -67,3 +67,70 @@ func (p *Project) GetRelativePath() (string, error) { return relPath, nil } + +// Raw returns the raw blueprint. +func (p *Project) Raw() blueprint.RawBlueprint { + return p.rawBlueprint +} + +func (p *Project) RunTarget( + target string, + ci bool, + local bool, + exec executor.Executor, + store secrets.SecretStore, + opts ...earthly.EarthlyExecutorOption, +) (map[string]earthly.EarthlyExecutionResult, error) { + return earthly.NewEarthlyExecutor( + p.Path, + target, + exec, + store, + p.logger, + append(p.generateOpts(target, ci, local), opts...)..., + ).Run() +} + +func (p *Project) generateOpts(target string, ci, local bool) []earthly.EarthlyExecutorOption { + var opts []earthly.EarthlyExecutorOption + + if _, ok := p.Blueprint.Project.CI.Targets[target]; ok { + targetConfig := p.Blueprint.Project.CI.Targets[target] + + if len(targetConfig.Args) > 0 { + var args []string + for k, v := range targetConfig.Args { + args = append(args, fmt.Sprintf("--%s", k), v) + } + + opts = append(opts, earthly.WithTargetArgs(args...)) + } + + // We only run multiple platforms in CI mode to avoid issues with local builds. + if targetConfig.Platforms != nil && ci { + opts = append(opts, earthly.WithPlatforms(targetConfig.Platforms...)) + } + + if targetConfig.Privileged != nil && *targetConfig.Privileged { + opts = append(opts, earthly.WithPrivileged()) + } + + if targetConfig.Retries != nil { + opts = append(opts, earthly.WithRetries(*targetConfig.Retries)) + } + + if len(targetConfig.Secrets) > 0 { + opts = append(opts, earthly.WithSecrets(targetConfig.Secrets)) + } + } + + if p.Blueprint.Global.CI.Providers.Earthly.Satellite != nil && !local { + opts = append(opts, earthly.WithSatellite(*p.Blueprint.Global.CI.Providers.Earthly.Satellite)) + } + + if len(p.Blueprint.Global.CI.Secrets) > 0 { + opts = append(opts, earthly.WithSecrets(p.Blueprint.Global.CI.Secrets)) + } + + return opts +} diff --git a/forge/cli/pkg/project/runtime.go b/forge/cli/pkg/project/runtime.go index 37dd963e..9194de1d 100644 --- a/forge/cli/pkg/project/runtime.go +++ b/forge/cli/pkg/project/runtime.go @@ -69,8 +69,16 @@ func (g *GitRuntime) Load(project *Project) map[string]string { return data } +// NewGitRuntime creates a new GitRuntime. func NewGitRuntime(logger *slog.Logger) *GitRuntime { return &GitRuntime{ logger: logger, } } + +// GetDefaultRuntimes returns the default runtime data loaders. +func GetDefaultRuntimes(logger *slog.Logger) []RuntimeData { + return []RuntimeData{ + NewGitRuntime(logger), + } +}