Skip to content

Commit a80853b

Browse files
committed
Add fixes
Signed-off-by: Anmol Virdi <anmol.virdi@chainguard.dev>
1 parent 03cd6ab commit a80853b

5 files changed

Lines changed: 198 additions & 26 deletions

File tree

docs/BUILD-PROCESS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ needs:
3333
- CAP_SYS_ADMIN
3434
```
3535

36-
Capabilities declared by build pipelines are added to the build runner. Capabilities declared by test pipelines are scoped to that test's runner under `melange test`, so a capability one subpackage's test needs is not granted to sibling tests or to the build runner. Names are checked while the pipeline is compiled, so a misspelled `CAP_*` fails the build rather than the container.
36+
Capabilities declared by build pipelines are added to the build runner. Capabilities declared by test pipelines are scoped to that test's runner under `melange test`, so a capability one subpackage's test needs is not granted to sibling tests or to the build runner. Compilation records them under the test's `capabilities`, so they survive into a compiled configuration and are still applied when testing it. Names are checked while the pipeline is compiled, so a misspelled `CAP_*` fails the build rather than the container.
3737

3838
## Where does Melange build?
3939

pkg/build/compile.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,14 @@ func (b *Build) Compile(ctx context.Context, opts ...CompileOption) error {
195195
// Sort and remove duplicates.
196196
te.Packages = slices.Compact(slices.Sorted(slices.Values(te.Packages)))
197197

198-
// Capabilities from test pipelines are intentionally not applied here:
199-
// `melange build` never runs the test pipelines, so granting them to the
200-
// build runner would only over-privilege it. Test.Compile scopes them to
201-
// each test's runner under `melange test`.
198+
// Capabilities gathered from the test pipelines are recorded on the test
199+
// rather than on b.Configuration.Capabilities: `melange build` never runs
200+
// the test pipelines, so granting them to the build runner would only
201+
// over-privilege it. Recording them here keeps the requirement in the
202+
// compiled configuration, so `melange test` on a compiled manifest (or on
203+
// the .melange.yaml embedded in the APK) still gets them, mirroring how
204+
// needs.packages is folded into test.environment.
205+
addCapabilities(&cfg.Subpackages[i].Test.Capabilities, tc.Capabilities)
202206
}
203207

204208
ic := &b.Configuration.Environment.Contents
@@ -223,6 +227,10 @@ func (b *Build) Compile(ctx context.Context, opts ...CompileOption) error {
223227

224228
// Sort and remove duplicates.
225229
te.Packages = slices.Compact(slices.Sorted(slices.Values(te.Packages)))
230+
231+
// As above: scoped to the test's runner, not the build runner, but kept in
232+
// the compiled configuration so it survives a compile/test round trip.
233+
addCapabilities(&b.Configuration.Test.Capabilities, tc.Capabilities)
226234
}
227235

228236
return nil

pkg/build/compile_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,87 @@ func TestCompileCapabilities(t *testing.T) {
259259
}
260260
})
261261

262+
// The same manifest a user writes, taken through ParseConfiguration rather
263+
// than built as a Go literal: inline capabilities have to survive parsing to
264+
// reach the runners and to be validated.
265+
t.Run("inline capabilities from a parsed manifest", func(t *testing.T) {
266+
write := func(t *testing.T, cap string) string {
267+
t.Helper()
268+
fp := filepath.Join(t.TempDir(), "melange.yaml")
269+
if err := os.WriteFile(fp, []byte(`
270+
package:
271+
name: caps
272+
version: 0.0.1
273+
epoch: 0
274+
description: inline capabilities
275+
276+
pipeline:
277+
- needs:
278+
capabilities:
279+
add:
280+
- `+cap+`
281+
runs: "true"
282+
283+
test:
284+
pipeline:
285+
- needs:
286+
capabilities:
287+
add:
288+
- `+cap+`
289+
runs: "true"
290+
`), 0o644); err != nil {
291+
t.Fatal(err)
292+
}
293+
return fp
294+
}
295+
296+
ctx := context.Background()
297+
298+
cfg, err := config.ParseConfiguration(ctx, write(t, "CAP_SYS_ADMIN"))
299+
if err != nil {
300+
t.Fatalf("failed to parse configuration: %v", err)
301+
}
302+
303+
build := &Build{Configuration: cfg}
304+
if err := build.Compile(ctx); err != nil {
305+
t.Fatalf("unexpected error: %v", err)
306+
}
307+
if got, want := build.Configuration.Capabilities.Add, []string{"CAP_SYS_ADMIN"}; !slices.Equal(got, want) {
308+
t.Errorf("build capabilities: want %v, got %v", want, got)
309+
}
310+
// Recorded on the test so a compiled configuration still declares what
311+
// `melange test` needs, without widening the build runner.
312+
if got, want := build.Configuration.Test.Capabilities.Add, []string{"CAP_SYS_ADMIN"}; !slices.Equal(got, want) {
313+
t.Errorf("compiled test capabilities: want %v, got %v", want, got)
314+
}
315+
316+
testCfg, err := config.ParseConfiguration(ctx, write(t, "CAP_SYS_ADMIN"))
317+
if err != nil {
318+
t.Fatalf("failed to parse configuration: %v", err)
319+
}
320+
321+
test := &Test{Package: "caps", Configuration: *testCfg}
322+
if err := test.Compile(ctx); err != nil {
323+
t.Fatalf("unexpected error: %v", err)
324+
}
325+
if got, want := test.Configuration.Test.Capabilities.Add, []string{"CAP_SYS_ADMIN"}; !slices.Equal(got, want) {
326+
t.Errorf("test capabilities: want %v, got %v", want, got)
327+
}
328+
329+
// A misspelled name in the inline form fails the build, as documented.
330+
badCfg, err := config.ParseConfiguration(ctx, write(t, "CAP_SYS_ADMN"))
331+
if err != nil {
332+
t.Fatalf("failed to parse configuration: %v", err)
333+
}
334+
err = (&Build{Configuration: badCfg}).Compile(ctx)
335+
if err == nil {
336+
t.Fatal("expected an error for an unknown capability, got none")
337+
}
338+
if !strings.Contains(err.Error(), "CAP_SYS_ADMN") {
339+
t.Errorf("error should name the offending capability, got: %v", err)
340+
}
341+
})
342+
262343
// A misspelled capability is rejected while compiling, rather than by the
263344
// runner once the container is created.
264345
t.Run("unknown capability fails compile", func(t *testing.T) {

pkg/config/config.go

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,13 +1405,16 @@ func replaceAll(r *strings.Replacer, in []string) []string {
14051405
return out
14061406
}
14071407

1408+
// replaceNeeds copies the input and overrides only the fields substitution
1409+
// applies to, so a field added to Needs later is carried through rather than
1410+
// silently dropped.
14081411
func replaceNeeds(r *strings.Replacer, in *Needs) *Needs {
14091412
if in == nil {
14101413
return nil
14111414
}
1412-
return &Needs{
1413-
Packages: replaceAll(r, in.Packages),
1414-
}
1415+
out := *in
1416+
out.Packages = replaceAll(r, in.Packages)
1417+
return &out
14151418
}
14161419

14171420
func replaceMap(r *strings.Replacer, in map[string]string) map[string]string {
@@ -1463,21 +1466,20 @@ func replaceImageConfig(r *strings.Replacer, in apko_types.ImageConfiguration) a
14631466
}
14641467
}
14651468

1469+
// replacePipeline copies the input and overrides only the fields substitution
1470+
// applies to, so a field added to Pipeline later is carried through rather than
1471+
// silently dropped.
14661472
func replacePipeline(r *strings.Replacer, in Pipeline) Pipeline {
1467-
return Pipeline{
1468-
Name: r.Replace(in.Name),
1469-
Uses: in.Uses,
1470-
With: replaceMap(r, in.With),
1471-
Runs: r.Replace(in.Runs),
1472-
Pipeline: replacePipelines(r, in.Pipeline),
1473-
Inputs: in.Inputs,
1474-
Needs: replaceNeeds(r, in.Needs),
1475-
Label: in.Label,
1476-
If: r.Replace(in.If),
1477-
Assertions: in.Assertions,
1478-
WorkDir: r.Replace(in.WorkDir),
1479-
Environment: replaceMap(r, in.Environment),
1480-
}
1473+
out := in
1474+
out.Name = r.Replace(in.Name)
1475+
out.With = replaceMap(r, in.With)
1476+
out.Runs = r.Replace(in.Runs)
1477+
out.Pipeline = replacePipelines(r, in.Pipeline)
1478+
out.Needs = replaceNeeds(r, in.Needs)
1479+
out.If = r.Replace(in.If)
1480+
out.WorkDir = r.Replace(in.WorkDir)
1481+
out.Environment = replaceMap(r, in.Environment)
1482+
return out
14811483
}
14821484

14831485
func replacePipelines(r *strings.Replacer, in []Pipeline) []Pipeline {
@@ -1492,14 +1494,17 @@ func replacePipelines(r *strings.Replacer, in []Pipeline) []Pipeline {
14921494
return out
14931495
}
14941496

1497+
// replaceTest copies the input and overrides only the fields substitution
1498+
// applies to, so a field added to Test later is carried through rather than
1499+
// silently dropped.
14951500
func replaceTest(r *strings.Replacer, in *Test) *Test {
14961501
if in == nil {
14971502
return nil
14981503
}
1499-
return &Test{
1500-
Environment: replaceImageConfig(r, in.Environment),
1501-
Pipeline: replacePipelines(r, in.Pipeline),
1502-
}
1504+
out := *in
1505+
out.Environment = replaceImageConfig(r, in.Environment)
1506+
out.Pipeline = replacePipelines(r, in.Pipeline)
1507+
return &out
15031508
}
15041509

15051510
func replaceUpdate(r *strings.Replacer, in Update) Update {

pkg/config/config_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,84 @@ test:
185185
require.Equal(t, "/usr/local/FOO", cfg.Test.Environment.Environment["LD_LIBRARY_PATH"])
186186
}
187187

188+
// ParseConfiguration rebuilds pipelines, tests and subpackages field by field,
189+
// so capabilities written inline in a manifest have to survive that rebuild the
190+
// same way needs.packages does.
191+
func Test_capabilitiesSurviveParsing(t *testing.T) {
192+
ctx := slogtest.Context(t)
193+
194+
fp := filepath.Join(t.TempDir(), "melange.yaml")
195+
if err := os.WriteFile(fp, []byte(`
196+
package:
197+
name: caps-parsing
198+
version: 0.0.1
199+
epoch: 0
200+
description: capabilities survive parsing
201+
202+
capabilities:
203+
add:
204+
- CAP_NET_ADMIN
205+
206+
pipeline:
207+
- needs:
208+
packages:
209+
- wget
210+
capabilities:
211+
add:
212+
- CAP_SYS_ADMIN
213+
runs: echo hi
214+
215+
test:
216+
capabilities:
217+
add:
218+
- CAP_SYS_PTRACE
219+
pipeline:
220+
- needs:
221+
capabilities:
222+
add:
223+
- CAP_SYS_CHROOT
224+
runs: echo test
225+
226+
subpackages:
227+
- name: caps-parsing-sub
228+
pipeline:
229+
- needs:
230+
capabilities:
231+
add:
232+
- CAP_MKNOD
233+
runs: echo sub
234+
test:
235+
capabilities:
236+
add:
237+
- CAP_SYS_NICE
238+
pipeline:
239+
- needs:
240+
capabilities:
241+
add:
242+
- CAP_SYS_TIME
243+
runs: echo sub test
244+
`), 0o644); err != nil {
245+
t.Fatal(err)
246+
}
247+
248+
cfg, err := ParseConfiguration(ctx, fp)
249+
if err != nil {
250+
t.Fatalf("failed to parse configuration: %s", err)
251+
}
252+
253+
require.Equal(t, []string{"CAP_NET_ADMIN"}, cfg.Capabilities.Add)
254+
require.Equal(t, []string{"wget"}, cfg.Pipeline[0].Needs.Packages)
255+
require.Equal(t, []string{"CAP_SYS_ADMIN"}, cfg.Pipeline[0].Needs.Capabilities.Add)
256+
257+
require.Equal(t, []string{"CAP_SYS_PTRACE"}, cfg.Test.Capabilities.Add)
258+
require.Equal(t, []string{"CAP_SYS_CHROOT"}, cfg.Test.Pipeline[0].Needs.Capabilities.Add)
259+
260+
sp := cfg.Subpackages[0]
261+
require.Equal(t, []string{"CAP_MKNOD"}, sp.Pipeline[0].Needs.Capabilities.Add)
262+
require.Equal(t, []string{"CAP_SYS_NICE"}, sp.Test.Capabilities.Add)
263+
require.Equal(t, []string{"CAP_SYS_TIME"}, sp.Test.Pipeline[0].Needs.Capabilities.Add)
264+
}
265+
188266
func Test_updateBlockVarSubstitution(t *testing.T) {
189267
ctx := slogtest.Context(t)
190268

0 commit comments

Comments
 (0)