Skip to content

Commit cdea920

Browse files
authored
feat: allows earthly target names to be regular expressions (#143)
1 parent 3635623 commit cdea920

File tree

10 files changed

+389
-141
lines changed

10 files changed

+389
-141
lines changed

cli/cmd/cmds/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (c *RunCmd) Run(ctx run.RunContext) error {
2626
}
2727

2828
ctx.Logger.Info("Executing Earthly target", "project", project.Path, "target", ref.Target)
29-
runner := run.NewDefaultProjectRunner(ctx, &project)
29+
runner := earthly.NewDefaultProjectRunner(ctx, &project)
3030
if err := runner.RunTarget(
3131
ref.Target,
3232
generateOpts(c, ctx)...,

cli/pkg/run/mocks/runner.go renamed to cli/pkg/earthly/mocks/runner.go

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/pkg/earthly/project.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package earthly
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"regexp"
7+
8+
"cuelang.org/go/cue"
9+
"cuelang.org/go/cue/cuecontext"
10+
"github.com/input-output-hk/catalyst-forge/cli/pkg/executor"
11+
"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
12+
"github.com/input-output-hk/catalyst-forge/lib/project/project"
13+
"github.com/input-output-hk/catalyst-forge/lib/project/secrets"
14+
"github.com/input-output-hk/catalyst-forge/lib/schema"
15+
sp "github.com/input-output-hk/catalyst-forge/lib/schema/blueprint/project"
16+
)
17+
18+
var (
19+
ErrNoMatchingTargets = fmt.Errorf("no matching targets found")
20+
)
21+
22+
//go:generate go run github.com/matryer/moq@latest -pkg mocks -out mocks/runner.go . ProjectRunner
23+
24+
// ProjectRunner is an interface for running Earthly targets for a project.
25+
type ProjectRunner interface {
26+
RunTarget(target string, opts ...EarthlyExecutorOption) error
27+
}
28+
29+
// DefaultProjectRunner is the default implementation of the ProjectRunner interface.
30+
type DefaultProjectRunner struct {
31+
ctx run.RunContext
32+
exectuor executor.Executor
33+
logger *slog.Logger
34+
project *project.Project
35+
store secrets.SecretStore
36+
}
37+
38+
// RunTarget runs the given Earthly target.
39+
func (p *DefaultProjectRunner) RunTarget(
40+
target string,
41+
opts ...EarthlyExecutorOption,
42+
) error {
43+
popts, err := p.generateOpts(target)
44+
if err != nil {
45+
return err
46+
}
47+
48+
return NewEarthlyExecutor(
49+
p.project.Path,
50+
target,
51+
p.exectuor,
52+
p.store,
53+
p.logger,
54+
append(popts, opts...)...,
55+
).Run()
56+
}
57+
58+
// generateOpts generates the options for the Earthly executor.
59+
func (p *DefaultProjectRunner) generateOpts(target string) ([]EarthlyExecutorOption, error) {
60+
var opts []EarthlyExecutorOption
61+
62+
if schema.HasProjectCiDefined(p.project.Blueprint) {
63+
targetConfig, err := p.unifyTargets(p.project.Blueprint.Project.Ci.Targets, target)
64+
if err != nil && err != ErrNoMatchingTargets {
65+
return nil, err
66+
} else if err != ErrNoMatchingTargets {
67+
if len(targetConfig.Args) > 0 {
68+
var args []string
69+
for k, v := range targetConfig.Args {
70+
args = append(args, fmt.Sprintf("--%s", k), v)
71+
}
72+
73+
opts = append(opts, WithTargetArgs(args...))
74+
}
75+
76+
// We only run multiple platforms in CI mode to avoid issues with local builds.
77+
if targetConfig.Platforms != nil && p.ctx.CI {
78+
opts = append(opts, WithPlatforms(targetConfig.Platforms...))
79+
}
80+
81+
if targetConfig.Privileged {
82+
opts = append(opts, WithPrivileged())
83+
}
84+
85+
if targetConfig.Retries > 0 {
86+
opts = append(opts, WithRetries(int(targetConfig.Retries)))
87+
}
88+
89+
if len(targetConfig.Secrets) > 0 {
90+
opts = append(opts, WithSecrets(targetConfig.Secrets))
91+
}
92+
}
93+
}
94+
95+
if schema.HasEarthlyProviderDefined(p.project.Blueprint) {
96+
if p.project.Blueprint.Global.Ci.Providers.Earthly.Satellite != "" && !p.ctx.Local {
97+
opts = append(opts, WithSatellite(p.project.Blueprint.Global.Ci.Providers.Earthly.Satellite))
98+
}
99+
}
100+
101+
if schema.HasGlobalCIDefined(p.project.Blueprint) {
102+
if len(p.project.Blueprint.Global.Ci.Secrets) > 0 {
103+
opts = append(opts, WithSecrets(p.project.Blueprint.Global.Ci.Secrets))
104+
}
105+
}
106+
107+
return opts, nil
108+
}
109+
110+
// unifyTargets unifies the targets that match the given name.
111+
func (p *DefaultProjectRunner) unifyTargets(
112+
Targets map[string]sp.Target,
113+
name string,
114+
) (sp.Target, error) {
115+
var targets []string
116+
for target := range Targets {
117+
filter, err := regexp.Compile(target)
118+
if err != nil {
119+
return sp.Target{}, fmt.Errorf("failed to compile target name '%s' to regex: %w", name, err)
120+
}
121+
122+
if filter.MatchString(name) {
123+
targets = append(targets, target)
124+
}
125+
}
126+
127+
if len(targets) == 0 {
128+
return sp.Target{}, ErrNoMatchingTargets
129+
}
130+
131+
var rt cue.Value
132+
ctx := cuecontext.New()
133+
for _, target := range targets {
134+
rt = rt.Unify(ctx.Encode(Targets[target]))
135+
}
136+
137+
if rt.Err() != nil {
138+
return sp.Target{}, fmt.Errorf("failed to unify targets: %w", rt.Err())
139+
}
140+
141+
var target sp.Target
142+
if err := rt.Decode(&target); err != nil {
143+
return sp.Target{}, fmt.Errorf("failed to decode unified targets: %w", err)
144+
}
145+
146+
return target, nil
147+
}
148+
149+
// NewDefaultProjectRunner creates a new DefaultProjectRunner instance.
150+
func NewDefaultProjectRunner(
151+
ctx run.RunContext,
152+
project *project.Project,
153+
) DefaultProjectRunner {
154+
e := executor.NewLocalExecutor(
155+
ctx.Logger,
156+
executor.WithRedirect(),
157+
)
158+
159+
return DefaultProjectRunner{
160+
ctx: ctx,
161+
exectuor: e,
162+
logger: ctx.Logger,
163+
project: project,
164+
store: ctx.SecretStore,
165+
}
166+
}
167+
168+
// NewCustomDefaultProjectRunner creates a new DefaultProjectRunner instance with custom dependencies.
169+
func NewCustomDefaultProjectRunner(
170+
ctx run.RunContext,
171+
exec executor.Executor,
172+
logger *slog.Logger,
173+
project *project.Project,
174+
store secrets.SecretStore,
175+
) DefaultProjectRunner {
176+
return DefaultProjectRunner{
177+
ctx: ctx,
178+
exectuor: exec,
179+
logger: logger,
180+
project: project,
181+
store: store,
182+
}
183+
}

0 commit comments

Comments
 (0)