Skip to content

Commit 0c29c90

Browse files
authored
Merge pull request #63 from chmeliik/dfparse-with-envs
image build: add --envs option
2 parents 1d3c39e + bd67891 commit 0c29c90

File tree

6 files changed

+212
-23
lines changed

6 files changed

+212
-23
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/containerd/platforms v1.0.0-rc.2
99
github.com/containers/image/v5 v5.36.2
1010
github.com/keilerkonzept/dockerfile-json v1.2.2
11+
github.com/moby/buildkit v0.19.0
1112
github.com/onsi/gomega v1.38.0
1213
github.com/opencontainers/go-digest v1.0.0
1314
github.com/opencontainers/image-spec v1.1.1
@@ -25,7 +26,6 @@ require (
2526
github.com/google/go-cmp v0.7.0 // indirect
2627
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2728
github.com/kr/pretty v0.1.0 // indirect
28-
github.com/moby/buildkit v0.19.0 // indirect
2929
github.com/moby/docker-image-spec v1.3.1 // indirect
3030
github.com/onsi/ginkgo/v2 v2.25.1 // indirect
3131
github.com/pkg/errors v0.9.1 // indirect

integration_tests/build_test.go

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type BuildParams struct {
3333
WorkdirMount string
3434
BuildArgs []string
3535
BuildArgsFile string
36+
Envs []string
3637
ContainerfileJsonOutput string
3738
ExtraArgs []string
3839
}
@@ -158,6 +159,10 @@ func runBuildWithOutput(container *TestRunnerContainer, buildParams BuildParams)
158159
if buildParams.BuildArgsFile != "" {
159160
args = append(args, "--build-args-file", buildParams.BuildArgsFile)
160161
}
162+
if len(buildParams.Envs) > 0 {
163+
args = append(args, "--envs")
164+
args = append(args, buildParams.Envs...)
165+
}
161166
if buildParams.ContainerfileJsonOutput != "" {
162167
args = append(args, "--containerfile-json-output", buildParams.ContainerfileJsonOutput)
163168
}
@@ -201,25 +206,37 @@ func writeContainerfile(contextDir, content string) {
201206
Expect(err).ToNot(HaveOccurred())
202207
}
203208

204-
func getImageLabels(container *TestRunnerContainer, imageRef string) map[string]string {
209+
type containerImageMeta struct {
210+
labels map[string]string
211+
envs map[string]string
212+
}
213+
214+
func getImageMeta(container *TestRunnerContainer, imageRef string) containerImageMeta {
205215
stdout, _, err := container.ExecuteCommandWithOutput("buildah", "inspect", imageRef)
206216
Expect(err).ToNot(HaveOccurred())
207217

208218
var inspect struct {
209219
OCIv1 struct {
210220
Config struct {
211221
Labels map[string]string
222+
Env []string
212223
} `json:"config"`
213224
}
214225
}
215226

216227
err = json.Unmarshal([]byte(stdout), &inspect)
217228
Expect(err).ToNot(HaveOccurred())
218229

219-
return inspect.OCIv1.Config.Labels
230+
envs := make(map[string]string, len(inspect.OCIv1.Config.Env))
231+
for _, env := range inspect.OCIv1.Config.Env {
232+
key, value, _ := strings.Cut(env, "=")
233+
envs[key] = value
234+
}
235+
236+
return containerImageMeta{labels: inspect.OCIv1.Config.Labels, envs: envs}
220237
}
221238

222-
func getContainerfileLabels(container *TestRunnerContainer, containerfileJsonPath string) map[string]string {
239+
func getContainerfileMeta(container *TestRunnerContainer, containerfileJsonPath string) containerImageMeta {
223240
containerfileJSON, err := container.GetFileContent(containerfileJsonPath)
224241
Expect(err).ToNot(HaveOccurred())
225242

@@ -232,8 +249,12 @@ func getContainerfileLabels(container *TestRunnerContainer, containerfileJsonPat
232249
Commands []struct {
233250
Name string
234251
Labels []struct {
235-
Key string
236-
Value string
252+
Key string
253+
Value string
254+
}
255+
Env []struct {
256+
Key string
257+
Value string
237258
}
238259
}
239260
}
@@ -243,14 +264,18 @@ func getContainerfileLabels(container *TestRunnerContainer, containerfileJsonPat
243264
Expect(err).ToNot(HaveOccurred())
244265

245266
labels := make(map[string]string)
267+
envs := make(map[string]string)
268+
246269
for _, cmd := range containerfile.Stages[0].Commands {
247-
if strings.ToLower(cmd.Name) == "label" {
248-
for _, label := range cmd.Labels {
249-
labels[label.Key] = label.Value
250-
}
270+
for _, label := range cmd.Labels {
271+
labels[label.Key] = label.Value
272+
}
273+
for _, env := range cmd.Env {
274+
envs[env.Key] = env.Value
251275
}
252276
}
253-
return labels
277+
278+
return containerImageMeta{labels: labels, envs: envs}
254279
}
255280

256281
func formatAsKeyValuePairs(m map[string]string) []string {
@@ -562,12 +587,15 @@ LABEL test.label="build-args-test"
562587
"undefined-buildarg=",
563588
}
564589

590+
imageMeta := getImageMeta(container, outputRef)
591+
containerfileMeta := getContainerfileMeta(container, containerfileJsonPath)
592+
565593
// Verify image labels
566-
imageLabels := formatAsKeyValuePairs(getImageLabels(container, outputRef))
594+
imageLabels := formatAsKeyValuePairs(imageMeta.labels)
567595
Expect(imageLabels).To(ContainElements(expectedLabels))
568596

569597
// Verify the parsed Containerfile has the same label values
570-
containerfileLabels := formatAsKeyValuePairs(getContainerfileLabels(container, containerfileJsonPath))
598+
containerfileLabels := formatAsKeyValuePairs(containerfileMeta.labels)
571599
Expect(containerfileLabels).To(ContainElements(expectedLabels))
572600
})
573601

@@ -614,8 +642,8 @@ LABEL test.label="platform-build-args-test"
614642
Expect(err).ToNot(HaveOccurred())
615643

616644
// Verify platform values match between the parsed Containerfile and the actual image
617-
imageLabels := getImageLabels(container, outputRef)
618-
containerfileLabels := getContainerfileLabels(container, containerfileJsonPath)
645+
imageLabels := getImageMeta(container, outputRef).labels
646+
containerfileLabels := getContainerfileMeta(container, containerfileJsonPath).labels
619647

620648
labelsToCheck := []string{
621649
"TARGETPLATFORM",
@@ -698,4 +726,75 @@ LABEL test.label="platform-build-args-test"
698726
]
699727
}`))
700728
})
729+
730+
t.Run("WithEnvs", func(t *testing.T) {
731+
contextDir := setupTestContext(t)
732+
733+
writeContainerfile(contextDir, `
734+
FROM scratch
735+
736+
LABEL foo=$FOO
737+
LABEL bar=$BAR
738+
739+
LABEL test.label="envs-test"
740+
`)
741+
742+
outputRef := "localhost/test-image-envs:" + GenerateUniqueTag(t)
743+
// Also verify that envs are handled properly for Containerfile parsing
744+
containerfileJsonPath := "/workspace/parsed-containerfile.json"
745+
746+
buildParams := BuildParams{
747+
Context: contextDir,
748+
OutputRef: outputRef,
749+
Push: false,
750+
Envs: []string{
751+
"FOO=foo-value",
752+
"BAR=bar-value",
753+
// Corner cases to verify that dockerfile-json and buildah handle them the same way
754+
// Should be an env var without a name (causes an error when starting the container)
755+
"=noname",
756+
// Should be an empty string
757+
"NOVALUE=",
758+
// Shouldn't be set at all
759+
"NOSUCHENV",
760+
},
761+
ContainerfileJsonOutput: containerfileJsonPath,
762+
}
763+
764+
container := setupBuildContainerWithCleanup(t, buildParams, nil)
765+
766+
err := runBuild(container, buildParams)
767+
Expect(err).ToNot(HaveOccurred())
768+
769+
expectedEnvs := []string{
770+
"FOO=foo-value",
771+
"BAR=bar-value",
772+
"=noname",
773+
"NOVALUE=",
774+
}
775+
expectedLabels := []string{
776+
"foo=foo-value",
777+
"bar=bar-value",
778+
}
779+
780+
imageMeta := getImageMeta(container, outputRef)
781+
containerfileMeta := getContainerfileMeta(container, containerfileJsonPath)
782+
783+
// Verify envs
784+
imageEnvs := formatAsKeyValuePairs(imageMeta.envs)
785+
Expect(imageEnvs).To(ContainElements(expectedEnvs))
786+
787+
containerfileEnvs := formatAsKeyValuePairs(containerfileMeta.envs)
788+
Expect(containerfileEnvs).To(ContainElements(expectedEnvs))
789+
790+
Expect(imageMeta.envs).ToNot(HaveKey("NOSUCHENV"))
791+
Expect(containerfileMeta.envs).ToNot(HaveKey("NOSUCHENV"))
792+
793+
// Verify labels
794+
imageLabels := formatAsKeyValuePairs(imageMeta.labels)
795+
Expect(imageLabels).To(ContainElements(expectedLabels))
796+
797+
containerfileLabels := formatAsKeyValuePairs(containerfileMeta.labels)
798+
Expect(containerfileLabels).To(ContainElements(expectedLabels))
799+
})
701800
}

pkg/cliwrappers/buildah.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type BuildahBuildArgs struct {
4545
Volumes []BuildahVolume
4646
BuildArgs []string
4747
BuildArgsFile string
48+
Envs []string
4849
ExtraArgs []string
4950
}
5051

@@ -159,6 +160,10 @@ func (b *BuildahCli) Build(args *BuildahBuildArgs) error {
159160
buildahArgs = append(buildahArgs, "--build-arg-file="+args.BuildArgsFile)
160161
}
161162

163+
for _, env := range args.Envs {
164+
buildahArgs = append(buildahArgs, "--env="+env)
165+
}
166+
162167
// Append extra arguments before the context directory
163168
buildahArgs = append(buildahArgs, args.ExtraArgs...)
164169
// Context directory must be the last argument

pkg/cliwrappers/buildah_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,29 @@ func TestBuildahCli_Build(t *testing.T) {
156156
g.Expect(capturedArgs).To(ContainElement("--build-arg-file=/path/to/build-args-file"))
157157
})
158158

159+
t.Run("should turn Envs into --env params", func(t *testing.T) {
160+
buildahCli, executor := setupBuildahCli()
161+
var capturedArgs []string
162+
executor.executeWithOutput = func(command string, args ...string) (string, string, int, error) {
163+
g.Expect(command).To(Equal("buildah"))
164+
capturedArgs = args
165+
return "", "", 0, nil
166+
}
167+
168+
buildArgs := &cliwrappers.BuildahBuildArgs{
169+
Containerfile: containerfile,
170+
ContextDir: contextDir,
171+
OutputRef: outputRef,
172+
Envs: []string{"FOO=bar", "BAZ=qux"},
173+
}
174+
175+
err := buildahCli.Build(buildArgs)
176+
g.Expect(err).ToNot(HaveOccurred())
177+
178+
g.Expect(capturedArgs).To(ContainElement("--env=FOO=bar"))
179+
g.Expect(capturedArgs).To(ContainElement("--env=BAZ=qux"))
180+
})
181+
159182
t.Run("should append extra args before context directory", func(t *testing.T) {
160183
buildahCli, executor := setupBuildahCli()
161184
var capturedArgs []string

pkg/commands/build.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ var BuildParamsConfig = map[string]common.Parameter{
8181
TypeKind: reflect.String,
8282
Usage: "Path to a file with build arguments, see https://www.mankier.com/1/buildah-build#--build-arg-file",
8383
},
84+
"envs": {
85+
Name: "envs",
86+
ShortName: "",
87+
EnvVarName: "KBC_BUILD_ENVS",
88+
TypeKind: reflect.Slice,
89+
Usage: "Environment variables to pass to the build using buildah's --env option.",
90+
},
8491
"containerfile-json-output": {
8592
Name: "containerfile-json-output",
8693
ShortName: "",
@@ -99,6 +106,7 @@ type BuildParams struct {
99106
WorkdirMount string `paramName:"workdir-mount"`
100107
BuildArgs []string `paramName:"build-args"`
101108
BuildArgsFile string `paramName:"build-args-file"`
109+
Envs []string `paramName:"envs"`
102110
ContainerfileJsonOutput string `paramName:"containerfile-json-output"`
103111
ExtraArgs []string // Additional arguments to pass to buildah build
104112
}
@@ -223,6 +231,9 @@ func (c *Build) logParams() {
223231
if c.Params.BuildArgsFile != "" {
224232
l.Logger.Infof("[param] BuildArgsFile: %s", c.Params.BuildArgsFile)
225233
}
234+
if len(c.Params.Envs) > 0 {
235+
l.Logger.Infof("[param] Envs: %v", c.Params.Envs)
236+
}
226237
if c.Params.ContainerfileJsonOutput != "" {
227238
l.Logger.Infof("[param] ContainerfileJsonOutput: %s", c.Params.ContainerfileJsonOutput)
228239
}
@@ -417,11 +428,14 @@ func (c *Build) parseContainerfile() (*dockerfile.Dockerfile, error) {
417428
return nil, fmt.Errorf("failed to parse %s: %w", c.containerfilePath, err)
418429
}
419430

431+
envs := processKeyValueEnvs(c.Params.Envs)
432+
420433
argExp, err := c.createBuildArgExpander()
421434
if err != nil {
422435
return nil, fmt.Errorf("failed to process build args: %w", err)
423436
}
424437

438+
containerfile.InjectEnv(envs)
425439
containerfile.Expand(argExp)
426440
return containerfile, nil
427441
}
@@ -453,14 +467,8 @@ func (c *Build) createBuildArgExpander() (dockerfile.SingleWordExpander, error)
453467
}
454468

455469
// CLI --build-args take precedence over everything else
456-
for _, arg := range c.Params.BuildArgs {
457-
key, value, hasValue := strings.Cut(arg, "=")
458-
if hasValue {
459-
args[key] = value
460-
} else if valueFromEnv, ok := os.LookupEnv(key); ok {
461-
args[key] = valueFromEnv
462-
}
463-
}
470+
cliArgs := processKeyValueEnvs(c.Params.BuildArgs)
471+
maps.Copy(args, cliArgs)
464472

465473
// Return the kind of "expander" function expected by the dockerfile-json API
466474
// (takes the name of a build arg, returns the value or error for undefined build args)
@@ -473,6 +481,21 @@ func (c *Build) createBuildArgExpander() (dockerfile.SingleWordExpander, error)
473481
return argExp, nil
474482
}
475483

484+
// Parse an array of key[=value] args. If '=' is missing, look up the value in
485+
// environment variables. This is how buildah handles --build-arg and --env values.
486+
func processKeyValueEnvs(args []string) map[string]string {
487+
values := make(map[string]string)
488+
for _, arg := range args {
489+
key, value, hasValue := strings.Cut(arg, "=")
490+
if hasValue {
491+
values[key] = value
492+
} else if valueFromEnv, ok := os.LookupEnv(key); ok {
493+
values[key] = valueFromEnv
494+
}
495+
}
496+
return values
497+
}
498+
476499
func (c *Build) buildImage() error {
477500
l.Logger.Info("Building container image...")
478501

@@ -492,6 +515,7 @@ func (c *Build) buildImage() error {
492515
Secrets: c.buildahSecrets,
493516
BuildArgs: c.Params.BuildArgs,
494517
BuildArgsFile: c.Params.BuildArgsFile,
518+
Envs: c.Params.Envs,
495519
ExtraArgs: c.Params.ExtraArgs,
496520
}
497521
if c.Params.WorkdirMount != "" {

0 commit comments

Comments
 (0)