Skip to content

Commit

Permalink
feat(forge/cli): adds support for running targets against multiple pl…
Browse files Browse the repository at this point in the history
…atforms
  • Loading branch information
jmgilman committed Sep 6, 2024
1 parent 159b41c commit 6bbf892
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 74 deletions.
4 changes: 4 additions & 0 deletions blueprint/schema/_embed/schema.cue
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ version: "1.0"
[string]: string
} @go(Args,map[string]string)

// Platforms contains the platforms to run the target against.
// +optional
platforms?: [...string] @go(Platforms,[]string)

// Privileged determines if the target should run in privileged mode.
// +optional
privileged?: null | bool @go(Privileged,*bool)
Expand Down
4 changes: 4 additions & 0 deletions blueprint/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ type Target struct {
// +optional
Args map[string]string `json:"args"`

// Platforms contains the platforms to run the target against.
// +optional
Platforms []string `json:"platforms"`

// Privileged determines if the target should run in privileged mode.
// +optional
Privileged *bool `json:"privileged"`
Expand Down
4 changes: 4 additions & 0 deletions blueprint/schema/schema_go_gen.cue
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ package schema
// +optional
args?: {[string]: string} @go(Args,map[string]string)

// Platforms contains the platforms to run the target against.
// +optional
platforms?: [...string] @go(Platforms,[]string)

// Privileged determines if the target should run in privileged mode.
// +optional
privileged?: null | bool @go(Privileged,*bool)
Expand Down
12 changes: 6 additions & 6 deletions forge/cli/cmd/cmds/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import (
)

type RunCmd struct {
Artifact string `short:"a" help:"Dump all produced artifacts to the given path."`
CI bool `help:"Run the target in CI mode."`
Local bool `short:"l" help:"Forces the target to run locally (ignores satellite)."`
Path string `arg:"" help:"The path to the target to execute (i.e., ./dir1+test)."`
Platform string `short:"p" help:"Run the target with the given platform."`
Pretty bool `help:"Pretty print JSON output."`
Artifact string `short:"a" help:"Dump all produced artifacts to the given path."`
CI bool `help:"Run the target in CI mode."`
Local bool `short:"l" help:"Forces the target to run locally (ignores satellite)."`
Path string `arg:"" help:"The path to the target to execute (i.e., ./dir1+test)."`
Platform []string `short:"p" help:"Run the target with the given platform."`
Pretty bool `help:"Pretty print JSON output."`
}

func (c *RunCmd) Run(logger *slog.Logger) error {
Expand Down
10 changes: 8 additions & 2 deletions forge/cli/cmd/cmds/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ func generateOpts(target string, flags *RunCmd, config *schema.Blueprint) []eart
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())
}
Expand Down Expand Up @@ -66,8 +71,9 @@ func generateOpts(target string, flags *RunCmd, config *schema.Blueprint) []eart
opts = append(opts, earthly.WithCI())
}

if flags.Platform != "" {
opts = append(opts, earthly.WithPlatform(flags.Platform))
// Users can explicitly set the platforms to use without being in CI mode.
if flags.Platform != nil {
opts = append(opts, earthly.WithPlatforms(flags.Platform...))
}
}

Expand Down
6 changes: 4 additions & 2 deletions forge/cli/cmd/testdata/run/1.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
exec forge run ./dir1+test
exec forge run --platform test ./dir1+test
cmp stdout golden.txt

-- golden.txt --
earthly
--platform
test
./dir1+test
Image ./dir1+test output as test

{"artifacts":{},"images":{"./dir1+test":"test"}}
{"test":{"artifacts":{},"images":{"./dir1+test":"test"}}}
-- earthly_stdout.txt --
Image ./dir1+test output as test
-- dir1/Earthfile --
Expand Down
6 changes: 4 additions & 2 deletions forge/cli/cmd/testdata/run/2.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
exec forge run --artifact output ./dir1+test
exec forge run --platform test --artifact output ./dir1+test
cmp stdout golden.txt

-- golden.txt --
earthly
--platform
test
--artifact
./dir1+test/*
output/
Image ./dir1+test output as test
Artifact ./dir1+test output as test

{"artifacts":{"./dir1+test":"test"},"images":{"./dir1+test":"test"}}
{"test":{"artifacts":{"./dir1+test":"test"},"images":{"./dir1+test":"test"}}}
-- earthly_stdout.txt --
Image ./dir1+test output as test
Artifact ./dir1+test output as test
Expand Down
14 changes: 11 additions & 3 deletions forge/cli/cmd/testdata/run/3.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
exec forge run --ci --platform test ./dir1+test
exec forge run --ci --platform test --platform test1 ./dir1+test
cmp stdout golden.txt

-- golden.txt --
earthly
--ci
--platform
test
--ci
./dir1+test
Image ./dir1+test output as test
Artifact ./dir1+test output as test

earthly
--platform
test1
--ci
./dir1+test
Image ./dir1+test output as test
Artifact ./dir1+test output as test

{"artifacts":{"./dir1+test":"test"},"images":{"./dir1+test":"test"}}
{"test":{"artifacts":{"./dir1+test":"test"},"images":{"./dir1+test":"test"}},"test1":{"artifacts":{"./dir1+test":"test"},"images":{"./dir1+test":"test"}}}
-- earthly_stdout.txt --
Image ./dir1+test output as test
Artifact ./dir1+test output as test
Expand Down
16 changes: 14 additions & 2 deletions forge/cli/cmd/testdata/run/4.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
exec forge run ./dir1+test
exec forge run --ci ./dir1+test
cmp stdout golden.txt

-- golden.txt --
earthly
--platform
test
--allow-privileged
--ci
./dir1+test
Image ./dir1+test output as test

{"artifacts":{},"images":{"./dir1+test":"test"}}
earthly
--platform
test1
--allow-privileged
--ci
./dir1+test
Image ./dir1+test output as test

{"test":{"artifacts":{},"images":{"./dir1+test":"test"}},"test1":{"artifacts":{},"images":{"./dir1+test":"test"}}}
-- earthly_stdout.txt --
Image ./dir1+test output as test
-- dir1/blueprint.cue --
Expand All @@ -17,6 +28,7 @@ project: {
ci: {
targets: {
test: {
platforms: ["test", "test1"]
privileged: true
}
}
Expand Down
6 changes: 4 additions & 2 deletions forge/cli/cmd/testdata/run/5.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
exec forge run ./dir1+test
exec forge run --platform test ./dir1+test
cmp stdout golden.txt

-- dir1/Secretfile --
{"secret_key": "secret_value"}
-- golden.txt --
earthly
--platform
test
--allow-privileged
./dir1+test
EARTHLY_SECRETS=secret_id=secret_value
Image ./dir1+test output as test

{"artifacts":{},"images":{"./dir1+test":"test"}}
{"test":{"artifacts":{},"images":{"./dir1+test":"test"}}}
-- earthly_stdout.txt --
Image ./dir1+test output as test
-- dir1/blueprint.cue --
Expand Down
54 changes: 38 additions & 16 deletions forge/cli/pkg/earthly/earthly.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log/slog"
"os"
"regexp"
"runtime"
"strconv"
"strings"

Expand All @@ -26,9 +27,10 @@ type EarthlySecret struct {
// earthlyExecutorOptions contains the configuration options for an
// EarthlyExecutor.
type earthlyExecutorOptions struct {
artifact string
ci bool
retries int
artifact string
ci bool
platforms []string
retries int
}

// EarthlyExecutor is an Executor that runs Earthly targets.
Expand All @@ -52,7 +54,7 @@ type EarthlyExecutionResult struct {

// Run executes the Earthly target and returns the resulting images and
// artifacts.
func (e EarthlyExecutor) Run() (EarthlyExecutionResult, error) {
func (e EarthlyExecutor) Run() (map[string]EarthlyExecutionResult, error) {
var (
err error
secrets []EarthlySecret
Expand All @@ -61,7 +63,7 @@ func (e EarthlyExecutor) Run() (EarthlyExecutionResult, error) {
if e.secrets != nil {
secrets, err = e.buildSecrets()
if err != nil {
return EarthlyExecutionResult{}, err
return nil, err
}

var secretString []string
Expand All @@ -75,30 +77,45 @@ func (e EarthlyExecutor) Run() (EarthlyExecutionResult, error) {
}
}

var output []byte
arguments := e.buildArguments()
for i := 0; i < e.opts.retries+1; i++ {
e.logger.Info("Executing Earthly", "attempt", i, "retries", e.opts.retries, "arguments", arguments)
output, err = e.executor.Execute("earthly", arguments)
if err == nil {
break
if e.opts.platforms == nil {
e.opts.platforms = []string{getNativePlatform()}
}

results := make(map[string]EarthlyExecutionResult)
for _, platform := range e.opts.platforms {
var output []byte

for i := 0; i < e.opts.retries+1; i++ {
arguments := e.buildArguments(platform)

e.logger.Info("Executing Earthly", "attempt", i, "retries", e.opts.retries, "arguments", arguments, "platform", platform)
output, err = e.executor.Execute("earthly", arguments)
if err == nil {
break
}

e.logger.Error("Failed to run Earthly", "error", err)
}

e.logger.Error("Failed to run Earthly", "error", err)
results[platform] = parseResult(string(output))
}

if err != nil {
e.logger.Error("Failed to run Earthly", "error", err)
return EarthlyExecutionResult{}, fmt.Errorf("failed to run Earthly: %w", err)
return nil, fmt.Errorf("failed to run Earthly: %w", err)
}

return parseResult(string(output)), nil
return results, nil
}

// buildArguments constructs the arguments to pass to the Earthly target.
func (e *EarthlyExecutor) buildArguments() []string {
func (e *EarthlyExecutor) buildArguments(platform string) []string {
var earthlyArgs []string

if platform != getNativePlatform() {
earthlyArgs = append(earthlyArgs, "--platform", platform)
}

earthlyArgs = append(earthlyArgs, e.earthlyArgs...)

if e.opts.artifact != "" {
Expand Down Expand Up @@ -186,6 +203,11 @@ func NewEarthlyExecutor(
return e
}

// getNativePlatform returns the native platform of the current machine.
func getNativePlatform() string {
return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
}

// parseResult parses the output of an Earthly execution and returns the
// resulting images and artifacts.
func parseResult(output string) EarthlyExecutionResult {
Expand Down
Loading

0 comments on commit 6bbf892

Please sign in to comment.