Skip to content

Commit 35530d6

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 7475e00 commit 35530d6

File tree

8 files changed

+113
-6
lines changed

8 files changed

+113
-6
lines changed

wanda/forge.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,51 @@ 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+
wandaSpecsFile := config.WandaSpecsFile
27+
if wandaSpecsFile == "" {
28+
wandaSpecsFile = filepath.Join(config.WorkDir, ".wandaspecs")
29+
}
30+
31+
graph, err := buildDepGraph(specFile, os.LookupEnv, config.NamePrefix, wandaSpecsFile)
2432
if err != nil {
25-
return fmt.Errorf("parse spec file: %w", err)
33+
return fmt.Errorf("build dep graph: %w", err)
2634
}
2735

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

3140
forge, err := NewForge(config)
3241
if err != nil {
3342
return fmt.Errorf("make forge: %w", err)
3443
}
35-
return forge.Build(spec)
44+
45+
// In RayCI mode, only build the root (deps built by prior pipeline steps).
46+
order := graph.Order
47+
if config.RayCI {
48+
order = []string{graph.Root}
49+
}
50+
51+
for _, name := range order {
52+
rs := graph.Specs[name]
53+
54+
log.Printf("building %s (from %s)", name, rs.Path)
55+
56+
if err := forge.Build(rs.Spec); err != nil {
57+
return fmt.Errorf("build %s: %w", name, err)
58+
}
59+
}
60+
61+
return nil
3662
}
3763

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

wanda/forge_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,71 @@ 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+
}
465+
466+
if err := Build("testdata/dep-top.wanda.yaml", config); err != nil {
467+
t.Fatalf("build with deps: %v", err)
468+
}
469+
470+
// Verify dep-top was built and can be read
471+
ref, err := name.ParseReference("cr.ray.io/rayproject/dep-top")
472+
if err != nil {
473+
t.Fatalf("parse reference: %v", err)
474+
}
475+
476+
img, err := daemon.Image(ref)
477+
if err != nil {
478+
t.Fatalf("read dep-top image: %v", err)
479+
}
480+
481+
layers, err := img.Layers()
482+
if err != nil {
483+
t.Fatalf("read layers: %v", err)
484+
}
485+
486+
// Should have 3 layers: dep-base, dep-middle, dep-top
487+
if got, want := len(layers), 3; got != want {
488+
t.Errorf("got %d layers, want %d", got, want)
489+
}
490+
}
491+
492+
func TestBuild_NoDeps(t *testing.T) {
493+
// Test backward compatibility: a spec with no deps should work
494+
config := &ForgeConfig{
495+
WorkDir: "testdata",
496+
NamePrefix: "cr.ray.io/rayproject/",
497+
}
498+
499+
if err := Build("testdata/hello-test.wanda.yaml", config); err != nil {
500+
t.Fatalf("build with deps: %v", err)
501+
}
502+
503+
ref, err := name.ParseReference("cr.ray.io/rayproject/hello-test")
504+
if err != nil {
505+
t.Fatalf("parse reference: %v", err)
506+
}
507+
508+
img, err := daemon.Image(ref)
509+
if err != nil {
510+
t.Fatalf("read hello image: %v", err)
511+
}
512+
513+
layers, err := img.Layers()
514+
if err != nil {
515+
t.Fatalf("read layers: %v", err)
516+
}
517+
518+
if got, want := len(layers), 1; got != want {
519+
t.Errorf("got %d layers, want %d", got, want)
520+
}
521+
}
522+
458523
func TestForgeLocal_withNamePrefix(t *testing.T) {
459524
if runtime.GOOS != "linux" {
460525
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name: dep-middle
2+
froms: ["cr.ray.io/rayproject/dep-base"]
3+
dockerfile: Dockerfile.dep-middle
4+
srcs:
5+
- world.txt

wanda/testdata/dep-top.wanda.yaml

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

0 commit comments

Comments
 (0)