Skip to content

Commit 70e0db3

Browse files
feat(wanda): add Artifact struct to spec for extraction
Add support for defining artifacts to extract from built container images. The Artifact struct specifies: - src: path inside container (supports variable expansion) - dst: destination path on host (relative to artifacts dir) - optional: if true, extraction failure warns instead of failing build Topic: wanda-artifact-spec Labels: draft Signed-off-by: andrew <andrew@anyscale.com>
1 parent 6165f02 commit 70e0db3

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

wanda/spec.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ import (
88
"gopkg.in/yaml.v3"
99
)
1010

11+
// Artifact defines a file or directory to extract from the built image.
12+
type Artifact struct {
13+
// Src is the path inside the container to extract.
14+
// Can be a file or directory. Supports variable expansion.
15+
Src string `yaml:"src"`
16+
17+
// Dst is the destination path on the host filesystem.
18+
// If it ends with "/", src is copied into the directory.
19+
// Otherwise, src is copied as the file/directory named dst.
20+
// Relative paths are relative to ArtifactsDir.
21+
Dst string `yaml:"dst"`
22+
23+
// Optional marks this artifact as best-effort.
24+
// If true, extraction failure will be logged but won't fail the build.
25+
Optional bool `yaml:"optional,omitempty"`
26+
}
27+
1128
// Spec is a specification for a container image.
1229
type Spec struct {
1330
Name string `yaml:"name,omitempty"`
@@ -28,6 +45,9 @@ type Spec struct {
2845

2946
// DisableCaching disables use of caching.
3047
DisableCaching bool `yaml:"disable_caching,omitempty"`
48+
49+
// Artifacts defines files and directories to extract from the built image.
50+
Artifacts []*Artifact `yaml:"artifacts,omitempty"`
3151
}
3252

3353
func parseSpecFile(f string) (*Spec, error) {
@@ -116,6 +136,21 @@ func stringsExpandVar(slice []string, lookup lookupFunc) []string {
116136
return result
117137
}
118138

139+
func artifactsExpandVar(artifacts []*Artifact, lookup lookupFunc) []*Artifact {
140+
if len(artifacts) == 0 {
141+
return nil
142+
}
143+
result := make([]*Artifact, len(artifacts))
144+
for i, a := range artifacts {
145+
result[i] = &Artifact{
146+
Src: expandVar(a.Src, lookup),
147+
Dst: expandVar(a.Dst, lookup),
148+
Optional: a.Optional,
149+
}
150+
}
151+
return result
152+
}
153+
119154
func (s *Spec) expandVar(lookup lookupFunc) *Spec {
120155
result := new(Spec)
121156

@@ -127,6 +162,7 @@ func (s *Spec) expandVar(lookup lookupFunc) *Spec {
127162
result.BuildArgs = stringsExpandVar(s.BuildArgs, lookup)
128163
result.BuildHintArgs = stringsExpandVar(s.BuildHintArgs, lookup)
129164
result.DisableCaching = s.DisableCaching
165+
result.Artifacts = artifactsExpandVar(s.Artifacts, lookup)
130166

131167
return result
132168
}

wanda/spec_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,120 @@ func TestParseSpecFile(t *testing.T) {
178178
t.Errorf("got %+v, want %+v", got, want)
179179
}
180180
}
181+
182+
func TestParseSpecFileWithArtifacts(t *testing.T) {
183+
tmpDir := t.TempDir()
184+
185+
specFile := filepath.Join(tmpDir, "spec.yaml")
186+
spec := strings.Join([]string{
187+
"name: wheel-builder",
188+
"froms: [python:3.11]",
189+
"dockerfile: Dockerfile",
190+
"artifacts:",
191+
" - src: /build/dist/*.whl",
192+
" dst: ./wheels/",
193+
" - src: /build/docs/",
194+
" dst: ./docs-output/",
195+
" - src: /app/bin/myapp",
196+
" dst: ./bin/myapp",
197+
}, "\n") + "\n"
198+
199+
if err := os.WriteFile(specFile, []byte(spec), 0644); err != nil {
200+
t.Fatalf("write spec file: %v", err)
201+
}
202+
203+
got, err := parseSpecFile(specFile)
204+
if err != nil {
205+
t.Fatalf("parse spec file: %v", err)
206+
}
207+
208+
want := &Spec{
209+
Name: "wheel-builder",
210+
Froms: []string{"python:3.11"},
211+
Dockerfile: "Dockerfile",
212+
Artifacts: []*Artifact{
213+
{Src: "/build/dist/*.whl", Dst: "./wheels/"},
214+
{Src: "/build/docs/", Dst: "./docs-output/"},
215+
{Src: "/app/bin/myapp", Dst: "./bin/myapp"},
216+
},
217+
}
218+
219+
if !reflect.DeepEqual(got, want) {
220+
t.Errorf("got %+v, want %+v", got, want)
221+
}
222+
}
223+
224+
func TestSpecExpandWithArtifacts(t *testing.T) {
225+
spec := &Spec{
226+
Name: "wheel-builder",
227+
Froms: []string{"python:$PYTHON_VERSION"},
228+
Dockerfile: "Dockerfile",
229+
Artifacts: []*Artifact{
230+
{Src: "/build/$PROJECT/dist/*.whl", Dst: "$OUTPUT_DIR/wheels/"},
231+
{Src: "/build/docs/", Dst: "./docs/"},
232+
},
233+
}
234+
235+
envs := map[string]string{
236+
"PYTHON_VERSION": "3.11",
237+
"PROJECT": "myproject",
238+
"OUTPUT_DIR": "/tmp/artifacts",
239+
}
240+
241+
expanded := spec.expandVar(func(k string) (string, bool) {
242+
v, ok := envs[k]
243+
return v, ok
244+
})
245+
246+
want := &Spec{
247+
Name: "wheel-builder",
248+
Froms: []string{"python:3.11"},
249+
Dockerfile: "Dockerfile",
250+
Artifacts: []*Artifact{
251+
{Src: "/build/myproject/dist/*.whl", Dst: "/tmp/artifacts/wheels/"},
252+
{Src: "/build/docs/", Dst: "./docs/"},
253+
},
254+
}
255+
256+
if !reflect.DeepEqual(expanded, want) {
257+
t.Errorf("got %+v, want %+v", expanded, want)
258+
}
259+
}
260+
261+
func TestSpecMarshalLoopbackWithArtifacts(t *testing.T) {
262+
spec := &Spec{
263+
Name: "test",
264+
Froms: []string{"ubuntu:22.04"},
265+
Dockerfile: "Dockerfile",
266+
Artifacts: []*Artifact{
267+
{Src: "/app/output.bin", Dst: "./output/"},
268+
{Src: "/app/data.txt", Dst: "./data/"},
269+
},
270+
}
271+
272+
bs, err := yaml.Marshal(spec)
273+
if err != nil {
274+
t.Fatalf("marshal: %v", err)
275+
}
276+
277+
loopback := new(Spec)
278+
if err := yaml.Unmarshal(bs, loopback); err != nil {
279+
t.Fatalf("unmarshal: %v", err)
280+
}
281+
282+
if spec.Name != loopback.Name {
283+
t.Errorf("Name: got %q, want %q", loopback.Name, spec.Name)
284+
}
285+
if spec.Dockerfile != loopback.Dockerfile {
286+
t.Errorf("Dockerfile: got %q, want %q", loopback.Dockerfile, spec.Dockerfile)
287+
}
288+
if len(spec.Artifacts) != len(loopback.Artifacts) {
289+
t.Fatalf("Artifacts length: got %d, want %d", len(loopback.Artifacts), len(spec.Artifacts))
290+
}
291+
for i, a := range spec.Artifacts {
292+
lb := loopback.Artifacts[i]
293+
if a.Src != lb.Src || a.Dst != lb.Dst {
294+
t.Errorf("Artifacts[%d]: got %+v, want %+v", i, lb, a)
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)