Skip to content

Commit b0597a4

Browse files
feat(wanda): Build dependencies in topological order
Extend Build() to automatically build all dependencies before building the root spec. In local mode, specs are discovered by scanning the repo for *.wanda.yaml files and built in topological order. For a slight optimization, in RayCI mode, only the root spec is built (deps built by prior pipeline steps). Topic: wanda-build-deps Relative: wanda-deps Signed-off-by: andrew <andrew@anyscale.com>
1 parent 9bd8415 commit b0597a4

File tree

8 files changed

+111
-6
lines changed

8 files changed

+111
-6
lines changed

wanda/forge.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,46 @@ import (
1414
"github.com/google/go-containerregistry/pkg/v1/remote"
1515
)
1616

17-
// Build builds a container image from the given specification file.
17+
// Build builds a container image from the given specification file, and builds
18+
// all its dependencies in topological order.
19+
// In RayCI mode, dependencies are assumed built by prior pipeline steps; only
20+
// the root is built.
1821
func Build(specFile string, config *ForgeConfig) error {
1922
if config == nil {
2023
config = &ForgeConfig{}
2124
}
2225

23-
spec, err := parseSpecFile(specFile)
26+
graph, err := buildDepGraph(specFile, os.LookupEnv, config.NamePrefix, config.SpecDirs)
2427
if err != nil {
25-
return fmt.Errorf("parse spec file: %w", err)
28+
return fmt.Errorf("build dep graph: %w", err)
2629
}
2730

28-
// Expand env variable.
29-
spec = spec.expandVar(os.LookupEnv)
31+
if err := graph.validateDeps(); err != nil {
32+
return fmt.Errorf("validate deps: %w", err)
33+
}
3034

3135
forge, err := NewForge(config)
3236
if err != nil {
3337
return fmt.Errorf("make forge: %w", err)
3438
}
35-
return forge.Build(spec)
39+
40+
// In RayCI mode, only build the root (deps built by prior pipeline steps).
41+
order := graph.Order
42+
if config.RayCI {
43+
order = []string{graph.Root}
44+
}
45+
46+
for _, name := range order {
47+
rs := graph.Specs[name]
48+
49+
log.Printf("building %s (from %s)", name, rs.Path)
50+
51+
if err := forge.Build(rs.Spec); err != nil {
52+
return fmt.Errorf("build %s: %w", name, err)
53+
}
54+
}
55+
56+
return nil
3657
}
3758

3859
// Forge is a forge to build container images.

wanda/forge_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,72 @@ func TestForgeWithRemoteWorkRepo(t *testing.T) {
455455
}
456456
}
457457

458+
func TestBuild_WithDeps(t *testing.T) {
459+
// Test: dep-top -> dep-middle -> dep-base
460+
// Build should build in order: dep-base, dep-middle, dep-top
461+
config := &ForgeConfig{
462+
WorkDir: "testdata",
463+
NamePrefix: "cr.ray.io/rayproject/",
464+
SpecDirs: []string{"wanda/testdata"},
465+
}
466+
467+
if err := Build("testdata/dep-top.wanda.yaml", config); err != nil {
468+
t.Fatalf("build with deps: %v", err)
469+
}
470+
471+
// Verify dep-top was built and can be read
472+
ref, err := name.ParseReference("cr.ray.io/rayproject/dep-top")
473+
if err != nil {
474+
t.Fatalf("parse reference: %v", err)
475+
}
476+
477+
img, err := daemon.Image(ref)
478+
if err != nil {
479+
t.Fatalf("read dep-top image: %v", err)
480+
}
481+
482+
layers, err := img.Layers()
483+
if err != nil {
484+
t.Fatalf("read layers: %v", err)
485+
}
486+
487+
// Should have 3 layers: dep-base, dep-middle, dep-top
488+
if got, want := len(layers), 3; got != want {
489+
t.Errorf("got %d layers, want %d", got, want)
490+
}
491+
}
492+
493+
func TestBuild_NoDeps(t *testing.T) {
494+
// Test backward compatibility: a spec with no deps should work
495+
config := &ForgeConfig{
496+
WorkDir: "testdata",
497+
NamePrefix: "cr.ray.io/rayproject/",
498+
}
499+
500+
if err := Build("testdata/hello-test.wanda.yaml", config); err != nil {
501+
t.Fatalf("build with deps: %v", err)
502+
}
503+
504+
ref, err := name.ParseReference("cr.ray.io/rayproject/hello-test")
505+
if err != nil {
506+
t.Fatalf("parse reference: %v", err)
507+
}
508+
509+
img, err := daemon.Image(ref)
510+
if err != nil {
511+
t.Fatalf("read hello image: %v", err)
512+
}
513+
514+
layers, err := img.Layers()
515+
if err != nil {
516+
t.Fatalf("read layers: %v", err)
517+
}
518+
519+
if got, want := len(layers), 1; got != want {
520+
t.Errorf("got %d layers, want %d", got, want)
521+
}
522+
}
523+
458524
func TestForgeLocal_withNamePrefix(t *testing.T) {
459525
if runtime.GOOS != "linux" {
460526
t.Skip("skipping test on non-linux")

wanda/testdata/Dockerfile.dep-base

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM scratch
2+
COPY Dockerfile.dep-base /opt/
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM cr.ray.io/rayproject/dep-base
2+
COPY world.txt /opt/

wanda/testdata/Dockerfile.dep-top

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM cr.ray.io/rayproject/dep-middle
2+
COPY Dockerfile.dep-top /opt/

wanda/testdata/dep-base.wanda.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name: dep-base
2+
dockerfile: Dockerfile.dep-base
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
name: dep-middle
3+
froms: ["cr.ray.io/rayproject/dep-base"]
4+
dockerfile: Dockerfile.dep-middle
5+
srcs:
6+
- world.txt

wanda/testdata/dep-top.wanda.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
name: dep-top
3+
froms: ["cr.ray.io/rayproject/dep-middle"]
4+
dockerfile: Dockerfile.dep-top

0 commit comments

Comments
 (0)