Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for build arguments to build command #449

Merged
merged 4 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"os"
"path"
"strings"

Expand Down Expand Up @@ -46,7 +47,22 @@ Otherwise, dib will create a new tag based on the previous tag.`,
opts := dib.BuildOpts{}
hydrateOptsFromViper(&opts)

if err := doBuild(opts); err != nil {
buildArgs := map[string]string{}
for _, arg := range opts.BuildArg {
key, val, hasVal := strings.Cut(arg, "=")
if hasVal {
buildArgs[key] = os.ExpandEnv(val)
} else {
// check if the env is set in the local environment and use that value if it is
if val, present := os.LookupEnv(key); present {
buildArgs[key] = os.ExpandEnv(val)
} else {
delete(buildArgs, key)
}
}
}

if err := doBuild(opts, buildArgs); err != nil {
logger.Fatalf("Build failed: %v", err)
}

Expand Down Expand Up @@ -80,9 +96,11 @@ func init() {
fmt.Sprintf("Build Backend used to run image builds. Supported backends: %v", supportedBackends))
buildCmd.Flags().Int("rate-limit", 1,
"Concurrent number of builds that can run simultaneously")
buildCmd.Flags().StringArray("build-arg", []string{},
"`argument=value` to supply to the builder")
}

func doBuild(opts dib.BuildOpts) error {
func doBuild(opts dib.BuildOpts, buildArgs map[string]string) error {
if opts.Backend == types.BackendKaniko && opts.LocalOnly {
logger.Warnf("Using Backend \"kaniko\" with the --local-only flag is partially supported.")
}
Expand All @@ -98,7 +116,7 @@ func doBuild(opts dib.BuildOpts) error {
logger.Infof("Building images in directory \"%s\"", buildPath)

logger.Debugf("Generate DAG")
graph, err := dib.GenerateDAG(buildPath, opts.RegistryURL, opts.HashListFilePath)
graph, err := dib.GenerateDAG(buildPath, opts.RegistryURL, opts.HashListFilePath, buildArgs)
if err != nil {
return fmt.Errorf("cannot generate DAG: %w", err)
}
Expand Down Expand Up @@ -135,7 +153,7 @@ func doBuild(opts dib.BuildOpts) error {
return fmt.Errorf("invalid backend \"%s\": not supported", opts.Backend)
}

res := dibBuilder.RebuildGraph(builder, ratelimit.NewChannelRateLimiter(opts.RateLimit))
res := dibBuilder.RebuildGraph(builder, ratelimit.NewChannelRateLimiter(opts.RateLimit), buildArgs)

res.Print()
if err := report.Generate(res, dibBuilder.Graph); err != nil {
Expand Down
21 changes: 20 additions & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package main

import (
"fmt"
"os"
"path"
"strings"

"github.com/radiofrance/dib/internal/logger"
"github.com/radiofrance/dib/pkg/dib"
Expand Down Expand Up @@ -32,6 +34,8 @@ func init() {
listCmd.Flags().StringP("output", "o", "", ""+
"Output format (console|go-template-file)\n"+
"You can provide a custom format using go-template: like this: \"-o go-template-file=...\".")
listCmd.Flags().StringArray("build-arg", []string{},
"`argument=value` to supply to the builder")
}

func doList(opts dib.ListOpts) error {
Expand All @@ -45,8 +49,23 @@ func doList(opts dib.ListOpts) error {
return err
}

buildArgs := map[string]string{}
for _, arg := range opts.BuildArg {
key, val, hasVal := strings.Cut(arg, "=")
if hasVal {
buildArgs[key] = os.ExpandEnv(val)
} else {
// check if the env is set in the local environment and use that value if it is
if val, present := os.LookupEnv(key); present {
buildArgs[key] = os.ExpandEnv(val)
} else {
delete(buildArgs, key)
}
}
}

buildPath := path.Join(workingDir, opts.BuildPath)
graph, err := dib.GenerateDAG(buildPath, opts.RegistryURL, opts.HashListFilePath)
graph, err := dib.GenerateDAG(buildPath, opts.RegistryURL, opts.HashListFilePath, buildArgs)
if err != nil {
return fmt.Errorf("cannot generate DAG: %w", err)
}
Expand Down
8 changes: 7 additions & 1 deletion docs/examples/config/reference.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
# Log level: "trace", "debug", "info", "warning", "error", "fatal", "panic". Defaults to "info".
# Log level: "debug", "info", "warning", "error", "fatal". Defaults to "info".
log_level: info

# URL of the registry where the images should be stored.
Expand All @@ -18,6 +18,12 @@ placeholder_tag: latest
# when using the Kubernetes executor as build pods are scheduled across multiple nodes.
rate_limit: 1

# Use build arguments to set build-time variables. The format is a list of strings. Env vars are expanded.
build_arg:
antoinegelloz marked this conversation as resolved.
Show resolved Hide resolved
- FOO1="bar1"
- FOO2=$BAR
- FOO3=${BAR}

# Path to the directory where the reports are generated. The directory will be created if it doesn't exist.
reports_dir: reports

Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Roadmap
DIB is still a work in progress, but we plan to release a stable version (v1.0.0) after we have added the
following features:

- **Per-image configuration:** Some images may require additional build args, or have their own tagging scheme. Being
- **Per-image configuration:** Some images may require their own tagging scheme. Being
able to configure those settings for each image is necessary.


Expand Down
2 changes: 1 addition & 1 deletion internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ func SetLevel(level *string) {

switch *level {
case "debug":
Infof("debug mode enabled")
log.Level = LogLevelDebug
case "info":
log.Level = LogLevelInfo
Expand All @@ -71,6 +70,7 @@ func SetLevel(level *string) {
}

logger.Store(log)
Infof("Log level set to %s", log.Level)
}

// LogLevelStyle returns the style of the prefix for each log level.
Expand Down
3 changes: 2 additions & 1 deletion pkg/dag/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func Test_Print(t *testing.T) {
" tag: \"3.17\"\n" +
" digest: 9ed4aefc74f6792b5a804d1d146fe4b4a2299147b0f50eaf2b08435d7b38c27e\n" +
" labels:\n" +
" dib.extra-tags: \"3.17\"\n"
" dib.extra-tags: \"3.17\"\n" +
" args: {}\n"
assert.Equal(t, expected, image.Print())
}
14 changes: 11 additions & 3 deletions pkg/dib/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ type BuildOpts struct {
Trivy trivy.Config `mapstructure:"trivy"`
Kaniko kaniko.Config `mapstructure:"kaniko"`
RateLimit int `mapstructure:"rate_limit"`
BuildArg []string `mapstructure:"build_arg"`
}

// RebuildGraph iterates over the graph to rebuild all the images that are marked to be rebuilt.
func (p *Builder) RebuildGraph(
builder types.ImageBuilder,
rateLimiter ratelimit.RateLimiter,
buildArgs map[string]string,
) *report.Report {
buildOpts, err := yaml.Marshal(&p.BuildOpts)
if err != nil {
Expand All @@ -63,6 +65,7 @@ func (p *Builder) RebuildGraph(
res.GetBuildReportDir(),
res.GetJunitReportDir(),
res.GetTrivyReportDir(),
buildArgs,
)

for buildReport := range buildReportsChan {
Expand All @@ -77,6 +80,7 @@ func (p *Builder) rebuildGraph(
builder types.ImageBuilder,
rateLimiter ratelimit.RateLimiter,
buildReportDir, junitReportDir, trivyReportDir string,
buildArgs map[string]string,
) {
p.Graph.
Filter(
Expand All @@ -100,7 +104,7 @@ func (p *Builder) rebuildGraph(
if img.NeedsRebuild {
meta := LoadCommonMetadata(&exec.ShellExecutor{})
if err := buildNode(node, builder, rateLimiter, meta,
p.PlaceholderTag, p.LocalOnly, buildReportDir,
p.PlaceholderTag, p.LocalOnly, buildReportDir, buildArgs,
); err != nil {
img.RebuildFailed = true
buildReportsChan <- buildReport.WithError(err)
Expand Down Expand Up @@ -139,6 +143,7 @@ func buildNode(
placeholderTag string,
localOnly bool,
buildReportDir string,
buildArgs map[string]string,
) error {
rateLimiter.Acquire()
defer rateLimiter.Release()
Expand All @@ -151,11 +156,13 @@ func buildNode(
for _, parent := range node.Parents() {
tagsToReplace[parent.Image.DockerRef(placeholderTag)] = parent.Image.CurrentRef()
}
if err := dockerfile.ReplaceTags(*img.Dockerfile, tagsToReplace); err != nil {
if err := dockerfile.ReplaceInFile(
path.Join(img.Dockerfile.ContextPath, img.Dockerfile.Filename), tagsToReplace); err != nil {
return fmt.Errorf("failed to replace tag in dockerfile %s: %w", img.Dockerfile.ContextPath, err)
}
defer func() {
if err := dockerfile.ResetTags(*img.Dockerfile, tagsToReplace); err != nil {
if err := dockerfile.ResetFile(
path.Join(img.Dockerfile.ContextPath, img.Dockerfile.Filename), tagsToReplace); err != nil {
logger.Warnf("failed to reset tag in dockerfile %s: %v", img.Dockerfile.ContextPath, err)
}
}()
Expand All @@ -178,6 +185,7 @@ func buildNode(
Labels: meta.WithImage(img).ToLabels(),
Push: !localOnly,
LogOutput: fileOutput,
BuildArgs: buildArgs,
}

logger.Infof("Building \"%s\" in context \"%s\"", img.CurrentRef(), img.Dockerfile.ContextPath)
Expand Down
2 changes: 1 addition & 1 deletion pkg/dib/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func TestRebuildGraph(t *testing.T) {
},
}

res := dibBuilder.RebuildGraph(builder, mock.RateLimiter{})
res := dibBuilder.RebuildGraph(builder, mock.RateLimiter{}, map[string]string{})

assert.Len(t, res.BuildReports, len(test.expBuildReports))
for i, buildReport := range res.BuildReports {
Expand Down
30 changes: 27 additions & 3 deletions pkg/dib/generate_dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const (

// GenerateDAG discovers and parses all Dockerfiles at a given path,
// and generates the DAG representing the relationships between images.
func GenerateDAG(buildPath string, registryPrefix string, customHashListPath string) (*dag.DAG, error) {
func GenerateDAG(buildPath, registryPrefix, customHashListPath string, buildArgs map[string]string) (*dag.DAG, error) {
var allFiles []string
cache := make(map[string]*dag.Node)
allParents := make(map[string][]dockerfile.ImageRef)
Expand All @@ -38,6 +38,7 @@ func GenerateDAG(buildPath string, registryPrefix string, customHashListPath str
if !info.IsDir() {
allFiles = append(allFiles, filePath)
}

if dockerfile.IsDockerfile(filePath) {
dckfile, err := dockerfile.ParseDockerfile(filePath)
if err != nil {
Expand Down Expand Up @@ -115,14 +116,14 @@ func GenerateDAG(buildPath string, registryPrefix string, customHashListPath str
}
}

if err := generateHashes(graph, allFiles, customHashListPath); err != nil {
if err := generateHashes(graph, allFiles, customHashListPath, buildArgs); err != nil {
return nil, err
}

return graph, nil
}

func generateHashes(graph *dag.DAG, allFiles []string, customHashListPath string) error {
func generateHashes(graph *dag.DAG, allFiles []string, customHashListPath string, buildArgs map[string]string) error {
customHumanizedHashList, err := loadCustomHumanizedHashList(customHashListPath)
if err != nil {
return err
Expand Down Expand Up @@ -189,6 +190,29 @@ func generateHashes(graph *dag.DAG, allFiles []string, customHashListPath string
humanizedKeywords = customHumanizedHashList
}

filename := path.Join(node.Image.Dockerfile.ContextPath, node.Image.Dockerfile.Filename)

argInstructionsToReplace := make(map[string]string)
for key, newArg := range buildArgs {
prevArgInstruction, ok := node.Image.Dockerfile.Args[key]
if ok {
argInstructionsToReplace[prevArgInstruction] = fmt.Sprintf("ARG %s=%s", key, newArg)
logger.Debugf("Overriding ARG instruction %q in %q [%q -> %q]",
key, filename, prevArgInstruction, fmt.Sprintf("ARG %s=%s", key, newArg))
}
}

if err := dockerfile.ReplaceInFile(
filename, argInstructionsToReplace); err != nil {
return fmt.Errorf("failed to replace ARG instructions in file %s: %w", filename, err)
}
defer func() {
if err := dockerfile.ResetFile(
filename, argInstructionsToReplace); err != nil {
logger.Warnf("failed to reset ARG instructions in file %q: %v", filename, err)
}
}()

hash, err := hashFiles(node.Image.Dockerfile.ContextPath, nodeFiles[node], parentHashes, humanizedKeywords)
if err != nil {
return fmt.Errorf("could not hash files for node %s: %w", node.Image.Name, err)
Expand Down
40 changes: 31 additions & 9 deletions pkg/dib/generate_dag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const fixtureDir = "../../test/fixtures/docker"
func TestGenerateDAG(t *testing.T) {
t.Run("basic tests", func(t *testing.T) {
graph, err := dib.GenerateDAG(fixtureDir,
"eu.gcr.io/my-test-repository", "")
"eu.gcr.io/my-test-repository", "", map[string]string{})
require.NoError(t, err)

nodes := flattenNodes(graph)
Expand All @@ -40,7 +40,7 @@ func TestGenerateDAG(t *testing.T) {
tmpDir := copyFixtures(t)

graph0, err := dib.GenerateDAG(tmpDir,
"eu.gcr.io/my-test-repository", "")
"eu.gcr.io/my-test-repository", "", map[string]string{})
require.NoError(t, err)

nodes0 := flattenNodes(graph0)
Expand All @@ -56,7 +56,7 @@ func TestGenerateDAG(t *testing.T) {

// Then ONLY the hash of the child node bullseye/multistage should have changed
graph1, err := dib.GenerateDAG(tmpDir,
"eu.gcr.io/my-test-repository", "")
"eu.gcr.io/my-test-repository", "", map[string]string{})
require.NoError(t, err)

nodes1 := flattenNodes(graph1)
Expand All @@ -73,7 +73,7 @@ func TestGenerateDAG(t *testing.T) {
tmpDir := copyFixtures(t)

graph0, err := dib.GenerateDAG(tmpDir,
"eu.gcr.io/my-test-repository", "")
"eu.gcr.io/my-test-repository", "", map[string]string{})
require.NoError(t, err)

nodes0 := flattenNodes(graph0)
Expand All @@ -89,7 +89,7 @@ func TestGenerateDAG(t *testing.T) {

// Then ONLY the hash of the child node bullseye/multistage should have changed
graph1, err := dib.GenerateDAG(tmpDir,
"eu.gcr.io/my-test-repository", "")
"eu.gcr.io/my-test-repository", "", map[string]string{})
require.NoError(t, err)

nodes1 := flattenNodes(graph1)
Expand All @@ -104,12 +104,13 @@ func TestGenerateDAG(t *testing.T) {

t.Run("using custom hash list should change only hashes of nodes with custom label", func(t *testing.T) {
graph0, err := dib.GenerateDAG(fixtureDir,
"eu.gcr.io/my-test-repository", "")
"eu.gcr.io/my-test-repository", "", map[string]string{})
require.NoError(t, err)

graph1, err := dib.GenerateDAG(fixtureDir,
"eu.gcr.io/my-test-repository",
"../../test/fixtures/dib/valid_wordlist.txt")
"../../test/fixtures/dib/valid_wordlist.txt",
map[string]string{})
require.NoError(t, err)

nodes0 := flattenNodes(graph0)
Expand All @@ -120,8 +121,29 @@ func TestGenerateDAG(t *testing.T) {
subNode1 := nodes1["sub-image"]

assert.Equal(t, rootNode1.Image.Hash, rootNode0.Image.Hash)
assert.Equal(t, "berlin-undress-hydrogen-april", subNode0.Image.Hash)
assert.Equal(t, "archeops-glaceon-chinchou-aipom", subNode1.Image.Hash)
assert.Equal(t, "violet-minnesota-alabama-alpha", subNode0.Image.Hash)
assert.Equal(t, "golduck-dialga-abra-aegislash", subNode1.Image.Hash)
})

t.Run("using arg used in root node should change all hashes", func(t *testing.T) {
graph0, err := dib.GenerateDAG(fixtureDir,
"eu.gcr.io/my-test-repository", "",
map[string]string{})
require.NoError(t, err)

graph1, err := dib.GenerateDAG(fixtureDir,
"eu.gcr.io/my-test-repository", "",
map[string]string{
"HELLO": "world",
})
require.NoError(t, err)

nodes0 := flattenNodes(graph0)
rootNode0 := nodes0["bullseye"]
nodes1 := flattenNodes(graph1)
rootNode1 := nodes1["bullseye"]

assert.NotEqual(t, rootNode1.Image.Hash, rootNode0.Image.Hash)
})
}

Expand Down
Loading
Loading