Skip to content

Commit b98ae8b

Browse files
tinovyatkinclaude
andcommitted
feat: add --source-policy-file flag to nerdctl build
Add support for BuildKit source policies via `nerdctl build --source-policy-file`. This enables reproducible and policy-driven builds (pin base images to digests, deny/allow sources, enforce HTTP checksums) without modifying Dockerfiles. The implementation: - Adds --source-policy-file flag that passes through to buildctl - Supports EXPERIMENTAL_BUILDKIT_SOURCE_POLICY env var for Docker Buildx compatibility - Flag takes precedence over env var when both are set This is a minimal passthrough to BuildKit - nerdctl does not validate the policy file; BuildKit handles all validation and error messages. See: https://github.com/moby/buildkit/blob/master/docs/build-repro.md Signed-off-by: Konstantin Vyatkin <tino@vtkn.io> Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a630881 commit b98ae8b

5 files changed

Lines changed: 72 additions & 0 deletions

File tree

cmd/nerdctl/builder/builder_build.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container
8181

8282
cmd.Flags().String("iidfile", "", "Write the image ID to the file")
8383
cmd.Flags().StringArray("label", nil, "Set metadata for an image")
84+
cmd.Flags().String("source-policy-file", "", "BuildKit source policy file (see https://github.com/moby/buildkit/blob/master/docs/build-repro.md)")
8485

8586
return cmd
8687
}
@@ -209,6 +210,10 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
209210
if err != nil {
210211
return types.BuilderBuildOptions{}, err
211212
}
213+
sourcePolicyFile, err := cmd.Flags().GetString("source-policy-file")
214+
if err != nil {
215+
return types.BuilderBuildOptions{}, err
216+
}
212217

213218
usernsRemap, err := cmd.Flags().GetString("userns-remap")
214219
if err != nil {
@@ -246,6 +251,7 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
246251
NetworkMode: network,
247252
ExtendedBuildContext: extendedBuildCtx,
248253
ExtraHosts: extraHosts,
254+
SourcePolicyFile: sourcePolicyFile,
249255
}, nil
250256
}
251257

docs/command-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,8 @@ Flags:
780780
- :whale: `--network=(default|host|none)`: Set the networking mode for the RUN instructions during build.(compatible with `buildctl build`)
781781
- :whale: `--build-context`: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp)
782782
- :whale: `--add-host`: Add a custom host-to-IP mapping (format: `host:ip`)
783+
- :nerd_face: `--source-policy-file`: BuildKit source policy JSON file for reproducible builds. See [BuildKit build-repro docs](https://github.com/moby/buildkit/blob/master/docs/build-repro.md).
784+
For compatibility with Docker Buildx, the `EXPERIMENTAL_BUILDKIT_SOURCE_POLICY` environment variable is also supported. Example no-op policy: `{"rules":[]}`
783785

784786
Unimplemented `docker build` flags: `--squash`
785787

pkg/api/types/builder_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ type BuilderBuildOptions struct {
7373
Pull *bool
7474
// ExtraHosts is a set of custom host-to-IP mappings.
7575
ExtraHosts []string
76+
// SourcePolicyFile is the path to a BuildKit source policy file.
77+
// Passed through to buildctl as --source-policy-file.
78+
SourcePolicyFile string
7679
}
7780

7881
// BuilderPruneOptions specifies options for `nerdctl builder prune`.

pkg/cmd/builder/build.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,16 @@ func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotte
194194
return nil
195195
}
196196

197+
// GetEffectiveSourcePolicyFile returns the effective source policy file path.
198+
// If optionValue is set, it takes precedence. Otherwise, the EXPERIMENTAL_BUILDKIT_SOURCE_POLICY
199+
// environment variable is used for Docker Buildx compatibility.
200+
func GetEffectiveSourcePolicyFile(optionValue string) string {
201+
if optionValue != "" {
202+
return optionValue
203+
}
204+
return os.Getenv("EXPERIMENTAL_BUILDKIT_SOURCE_POLICY")
205+
}
206+
197207
func generateBuildctlArgs(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) (buildCtlBinary string,
198208
buildctlArgs []string, needsLoading bool, metaFile string, tags []string, cleanup func(), err error) {
199209

@@ -472,6 +482,11 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
472482
buildctlArgs = append(buildctlArgs, "--opt=add-hosts="+strings.Join(extraHosts, ","))
473483
}
474484

485+
// Source policy file: use explicit option if set, otherwise fallback to env var for Buildx compatibility
486+
if sourcePolicyFile := GetEffectiveSourcePolicyFile(options.SourcePolicyFile); sourcePolicyFile != "" {
487+
buildctlArgs = append(buildctlArgs, "--source-policy-file="+sourcePolicyFile)
488+
}
489+
475490
return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil
476491
}
477492

pkg/cmd/builder/build_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,49 @@ func TestParseBuildctlArgsForOCILayout(t *testing.T) {
238238
})
239239
}
240240
}
241+
242+
func TestGetEffectiveSourcePolicyFile(t *testing.T) {
243+
// Cannot use t.Parallel() since subtests modify environment variables
244+
245+
tests := []struct {
246+
name string
247+
optionValue string
248+
envValue string
249+
expected string
250+
}{
251+
{
252+
name: "option value takes precedence over env var",
253+
optionValue: "/path/from/flag.json",
254+
envValue: "/path/from/env.json",
255+
expected: "/path/from/flag.json",
256+
},
257+
{
258+
name: "env var is used when option is empty",
259+
optionValue: "",
260+
envValue: "/path/from/env.json",
261+
expected: "/path/from/env.json",
262+
},
263+
{
264+
name: "empty when both are unset",
265+
optionValue: "",
266+
envValue: "",
267+
expected: "",
268+
},
269+
{
270+
name: "option value used when env var is empty",
271+
optionValue: "/path/from/flag.json",
272+
envValue: "",
273+
expected: "/path/from/flag.json",
274+
},
275+
}
276+
277+
for _, tc := range tests {
278+
t.Run(tc.name, func(t *testing.T) {
279+
// Set up the environment variable for this test
280+
t.Setenv("EXPERIMENTAL_BUILDKIT_SOURCE_POLICY", tc.envValue)
281+
282+
result := GetEffectiveSourcePolicyFile(tc.optionValue)
283+
assert.Equal(t, result, tc.expected)
284+
})
285+
}
286+
}

0 commit comments

Comments
 (0)