add support for specifying dockerfile in build process

Signed-off-by: Avi Deitcher <avi@deitcher.net>
This commit is contained in:
Avi Deitcher 2024-02-21 19:53:50 +02:00
parent 2cff5681b5
commit 0c31697e10
18 changed files with 130 additions and 12 deletions

View File

@ -50,6 +50,7 @@ A package source consists of a directory containing at least two files:
- `image` _(string)_: *(mandatory)* The name of the image to build - `image` _(string)_: *(mandatory)* The name of the image to build
- `org` _(string)_: The hub/registry organisation to which this package belongs - `org` _(string)_: The hub/registry organisation to which this package belongs
- `dockerfile` _(string)_: The dockerfile to use to build this package, must be in this directory or below (default: `Dockerfile`)
- `arches` _(list of string)_: The architectures which this package should be built for (valid entries are `GOARCH` names) - `arches` _(list of string)_: The architectures which this package should be built for (valid entries are `GOARCH` names)
- `extra-sources` _(list of strings)_: Additional sources for the package outside the package directory. The format is `src:dst`, where `src` can be relative to the package directory and `dst` is the destination in the build context. This is useful for sharing files, such as vendored go code, between packages. - `extra-sources` _(list of strings)_: Additional sources for the package outside the package directory. The format is `src:dst`, where `src` can be relative to the package directory and `dst` is the destination in the build context. This is useful for sharing files, such as vendored go code, between packages.
- `gitrepo` _(string)_: The git repository where the package source is kept. - `gitrepo` _(string)_: The git repository where the package source is kept.

View File

@ -45,6 +45,7 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
manifest bool manifest bool
cacheDir = flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir} cacheDir = flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir}
sbomScanner string sbomScanner string
dockerfile string
) )
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
@ -92,6 +93,7 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
if sbomScanner != "false" { if sbomScanner != "false" {
opts = append(opts, pkglib.WithBuildSbomScanner(sbomScanner)) opts = append(opts, pkglib.WithBuildSbomScanner(sbomScanner))
} }
opts = append(opts, pkglib.WithDockerfile(dockerfile))
// skipPlatformsMap contains platforms that should be skipped // skipPlatformsMap contains platforms that should be skipped
skipPlatformsMap := make(map[string]bool) skipPlatformsMap := make(map[string]bool)
@ -128,12 +130,12 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
// look for builders env var // look for builders env var
buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap) buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap)
if err != nil { if err != nil {
return fmt.Errorf("error in environment variable %s: %w\n", buildersEnvVar, err) return fmt.Errorf("error in environment variable %s: %w", buildersEnvVar, err)
} }
// any CLI options override env var // any CLI options override env var
buildersMap, err = buildPlatformBuildersMap(builders, buildersMap) buildersMap, err = buildPlatformBuildersMap(builders, buildersMap)
if err != nil { if err != nil {
return fmt.Errorf("error in --builders flag: %w\n", err) return fmt.Errorf("error in --builders flag: %w", err)
} }
opts = append(opts, pkglib.WithBuildBuilders(buildersMap)) opts = append(opts, pkglib.WithBuildBuilders(buildersMap))
opts = append(opts, pkglib.WithBuildBuilderImage(builderImage)) opts = append(opts, pkglib.WithBuildBuilderImage(builderImage))
@ -202,6 +204,7 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
cmd.Flags().BoolVar(&nobuild, "nobuild", false, "Skip building the image before pushing, conflicts with -force") cmd.Flags().BoolVar(&nobuild, "nobuild", false, "Skip building the image before pushing, conflicts with -force")
cmd.Flags().BoolVar(&manifest, "manifest", true, "Create and push multi-arch manifest") cmd.Flags().BoolVar(&manifest, "manifest", true, "Create and push multi-arch manifest")
cmd.Flags().StringVar(&sbomScanner, "sbom-scanner", "", "SBOM scanner to use, must match the buildkit spec; set to blank to use the buildkit default; set to 'false' for no scanning") cmd.Flags().StringVar(&sbomScanner, "sbom-scanner", "", "SBOM scanner to use, must match the buildkit spec; set to blank to use the buildkit default; set to 'false' for no scanning")
cmd.Flags().StringVar(&dockerfile, "dockerfile", "", "Dockerfile to use for building the image, must be in this directory or below, overrides what is in build.yml")
return cmd return cmd
} }

View File

@ -43,6 +43,7 @@ type buildOpts struct {
builderRestart bool builderRestart bool
sbomScan bool sbomScan bool
sbomScannerImage string sbomScannerImage string
dockerfile string
} }
// BuildOpt allows callers to specify options to Build // BuildOpt allows callers to specify options to Build
@ -186,6 +187,14 @@ func WithBuildSbomScanner(scanner string) BuildOpt {
} }
} }
// WithDockerfile which dockerfile to use when building the package
func WithDockerfile(dockerfile string) BuildOpt {
return func(bo *buildOpts) error {
bo.dockerfile = dockerfile
return nil
}
}
// Build builds the package // Build builds the package
func (p Pkg) Build(bos ...BuildOpt) error { func (p Pkg) Build(bos ...BuildOpt) error {
var bo buildOpts var bo buildOpts
@ -211,6 +220,22 @@ func (p Pkg) Build(bos ...BuildOpt) error {
return err return err
} }
// validate the Dockerfile before bothing to move ahead, because this func call is public, so someone could
// pass something to it as a library call. We also check in the build function, to avoid multiple loops each with an error.
// if the dockerfile override was not set in the build options, i.e. it is empty, use the one from the package,
// which never should be empty. We set it onto the buildOpts, because that is what we use to pass it around to lower-level
// funcs.
if bo.dockerfile == "" {
bo.dockerfile = p.dockerfile
}
if strings.Contains(bo.dockerfile, "..") {
return fmt.Errorf("cannot expand beyond root of context for dockerfile %s", bo.dockerfile)
}
if _, err := os.Stat(filepath.Join(p.path, bo.dockerfile)); err != nil {
return fmt.Errorf("dockerfile %s does not exist or cannot be read in context %s", bo.dockerfile, p.path)
}
// did we have the build cache dir provided? // did we have the build cache dir provided?
if bo.cacheDir == "" { if bo.cacheDir == "" {
return errors.New("must provide linuxkit build cache directory") return errors.New("must provide linuxkit build cache directory")
@ -593,7 +618,8 @@ func (p Pkg) buildArch(ctx context.Context, d dockerRunner, c lktspec.CacheProvi
if bo.ignoreCache { if bo.ignoreCache {
passCache = nil passCache = nil
} }
if err := d.build(ctx, tagArch, p.path, builderName, builderImage, platform, restart, passCache, buildCtx.Reader(), stdout, bo.sbomScan, bo.sbomScannerImage, imageBuildOpts); err != nil {
if err := d.build(ctx, tagArch, p.path, bo.dockerfile, builderName, builderImage, platform, restart, passCache, buildCtx.Reader(), stdout, bo.sbomScan, bo.sbomScannerImage, imageBuildOpts); err != nil {
stdoutCloser() stdoutCloser()
if strings.Contains(err.Error(), "executor failed running [/dev/.buildkit_qemu_emulator") { if strings.Contains(err.Error(), "executor failed running [/dev/.buildkit_qemu_emulator") {
return nil, fmt.Errorf("buildkit was unable to emulate %s. check binfmt has been set up and works for this platform: %v", platform, err) return nil, fmt.Errorf("buildkit was unable to emulate %s. check binfmt has been set up and works for this platform: %v", platform, err)

View File

@ -58,7 +58,7 @@ func (d *dockerMocker) contextSupportCheck() error {
func (d *dockerMocker) builder(_ context.Context, _, _, _ string, _ bool) (*buildkitClient.Client, error) { func (d *dockerMocker) builder(_ context.Context, _, _, _ string, _ bool) (*buildkitClient.Client, error) {
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")
} }
func (d *dockerMocker) build(ctx context.Context, tag, pkg, dockerContext, builderImage, platform string, builderRestart bool, c lktspec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage string, imageBuildOpts dockertypes.ImageBuildOptions) error { func (d *dockerMocker) build(ctx context.Context, tag, pkg, dockerfile, dockerContext, builderImage, platform string, builderRestart bool, c lktspec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage string, imageBuildOpts dockertypes.ImageBuildOptions) error {
if !d.enableBuild { if !d.enableBuild {
return errors.New("build disabled") return errors.New("build disabled")
} }

View File

@ -53,7 +53,7 @@ const (
type dockerRunner interface { type dockerRunner interface {
tag(ref, tag string) error tag(ref, tag string) error
build(ctx context.Context, tag, pkg, dockerContext, builderImage, platform string, restart bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage string, imageBuildOpts types.ImageBuildOptions) error build(ctx context.Context, tag, pkg, dockerfile, dockerContext, builderImage, platform string, restart bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage string, imageBuildOpts types.ImageBuildOptions) error
save(tgt string, refs ...string) error save(tgt string, refs ...string) error
load(src io.Reader) error load(src io.Reader) error
pull(img string) (bool, error) pull(img string) (bool, error)
@ -402,7 +402,7 @@ func (dr *dockerRunnerImpl) tag(ref, tag string) error {
return dr.command(nil, nil, nil, "image", "tag", ref, tag) return dr.command(nil, nil, nil, "image", "tag", ref, tag)
} }
func (dr *dockerRunnerImpl) build(ctx context.Context, tag, pkg, dockerContext, builderImage, platform string, restart bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage string, imageBuildOpts types.ImageBuildOptions) error { func (dr *dockerRunnerImpl) build(ctx context.Context, tag, pkg, dockerfile, dockerContext, builderImage, platform string, restart bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage string, imageBuildOpts types.ImageBuildOptions) error {
// ensure we have a builder // ensure we have a builder
client, err := dr.builder(ctx, dockerContext, builderImage, platform, restart) client, err := dr.builder(ctx, dockerContext, builderImage, platform, restart)
if err != nil { if err != nil {
@ -473,26 +473,26 @@ func (dr *dockerRunnerImpl) build(ctx context.Context, tag, pkg, dockerContext,
solveOpts.Session = append(solveOpts.Session, up) solveOpts.Session = append(solveOpts.Session, up)
} else { } else {
solveOpts.LocalDirs = map[string]string{ solveOpts.LocalDirs = map[string]string{
builder.DefaultLocalNameDockerfile: pkg, builder.DefaultLocalNameDockerfile: path.Join(pkg, dockerfile),
builder.DefaultLocalNameContext: pkg, builder.DefaultLocalNameContext: pkg,
} }
} }
// go through the dockerfile to see if we have any provided images cached // go through the dockerfile to see if we have any provided images cached
if c != nil { if c != nil {
dockerfile := path.Join(pkg, "Dockerfile") dockerfileRef := path.Join(pkg, dockerfile)
f, err := os.Open(dockerfile) f, err := os.Open(dockerfileRef)
if err != nil { if err != nil {
return fmt.Errorf("error opening dockerfile %s: %v", dockerfile, err) return fmt.Errorf("error opening dockerfile %s: %v", dockerfileRef, err)
} }
defer f.Close() defer f.Close()
ast, err := parser.Parse(f) ast, err := parser.Parse(f)
if err != nil { if err != nil {
return fmt.Errorf("error parsing dockerfile from bytes into AST %s: %v", dockerfile, err) return fmt.Errorf("error parsing dockerfile from bytes into AST %s: %v", dockerfileRef, err)
} }
stages, metaArgs, err := instructions.Parse(ast.AST) stages, metaArgs, err := instructions.Parse(ast.AST)
if err != nil { if err != nil {
return fmt.Errorf("error parsing dockerfile from AST into stages %s: %v", dockerfile, err) return fmt.Errorf("error parsing dockerfile from AST into stages %s: %v", dockerfileRef, err)
} }
// fill optMetaArgs with args found while parsing Dockerfile // fill optMetaArgs with args found while parsing Dockerfile

View File

@ -18,6 +18,7 @@ import (
type pkgInfo struct { type pkgInfo struct {
Image string `yaml:"image"` Image string `yaml:"image"`
Org string `yaml:"org"` Org string `yaml:"org"`
Dockerfile string `yaml:"dockerfile"`
Arches []string `yaml:"arches"` Arches []string `yaml:"arches"`
ExtraSources []string `yaml:"extra-sources"` ExtraSources []string `yaml:"extra-sources"`
GitRepo string `yaml:"gitrepo"` // ?? GitRepo string `yaml:"gitrepo"` // ??
@ -51,6 +52,7 @@ type PkglibConfig struct {
Dev bool Dev bool
} }
// NewPkInfo returns a new pkgInfo with default values
func NewPkgInfo() pkgInfo { func NewPkgInfo() pkgInfo {
return pkgInfo{ return pkgInfo{
Org: "linuxkit", Org: "linuxkit",
@ -58,6 +60,7 @@ func NewPkgInfo() pkgInfo {
GitRepo: "https://github.com/linuxkit/linuxkit", GitRepo: "https://github.com/linuxkit/linuxkit",
Network: false, Network: false,
DisableCache: false, DisableCache: false,
Dockerfile: "Dockerfile",
} }
} }
@ -84,6 +87,7 @@ type Pkg struct {
// Internal state // Internal state
path string path string
dockerfile string
hash string hash string
dirty bool dirty bool
commitHash string commitHash string
@ -265,6 +269,7 @@ func NewFromConfig(cfg PkglibConfig, args ...string) ([]Pkg, error) {
dockerDepends: dockerDepends, dockerDepends: dockerDepends,
dirty: dirty, dirty: dirty,
path: pkgPath, path: pkgPath,
dockerfile: pi.Dockerfile,
git: git, git: git,
}) })
} }

View File

@ -0,0 +1 @@
FROM alpine:3.19

View File

@ -0,0 +1,3 @@
org: linuxkit
image: missing-in-yml
dockerfile: Dockerfilenotexist

View File

@ -0,0 +1,21 @@
#!/bin/sh
# SUMMARY: Check that tar output format build is reproducible
# LABELS:
set -e
# Source libraries. Uncomment if needed/defined
#. "${RT_LIB}"
. "${RT_PROJECT_ROOT}/_lib/lib.sh"
set +e
linuxkit pkg build --force .
command_status=$?
set -e
if [ $command_status -eq 0 ]; then
echo "Command should have failed"
exit 1
fi
exit 0

View File

@ -0,0 +1 @@
FROM alpine:3.19

View File

@ -0,0 +1,3 @@
org: linuxkit
image: missing-in-yml
dockerfile: Dockerfilenotexist

View File

@ -0,0 +1,13 @@
#!/bin/sh
# SUMMARY: Check that tar output format build is reproducible
# LABELS:
set -e
# Source libraries. Uncomment if needed/defined
#. "${RT_LIB}"
. "${RT_PROJECT_ROOT}/_lib/lib.sh"
linuxkit pkg build --force --dockerfile Dockerfile .
exit 0

View File

@ -0,0 +1 @@
FROM alpine:3.19

View File

@ -0,0 +1,3 @@
org: linuxkit
image: missing-in-yml
dockerfile: Dockerfile

View File

@ -0,0 +1,21 @@
#!/bin/sh
# SUMMARY: Check that tar output format build is reproducible
# LABELS:
set -e
# Source libraries. Uncomment if needed/defined
#. "${RT_LIB}"
. "${RT_PROJECT_ROOT}/_lib/lib.sh"
set +e
linuxkit pkg build --force --dockerfile nosuchfile .
command_status=$?
set -e
if [ $command_status -eq 0 ]; then
echo "Command should have failed"
exit 1
fi
exit 0

View File

@ -0,0 +1 @@
FROM alpine:3.19

View File

@ -0,0 +1,2 @@
org: linuxkit
image: missing-in-yml

View File

@ -0,0 +1,13 @@
#!/bin/sh
# SUMMARY: Check that tar output format build is reproducible
# LABELS:
set -e
# Source libraries. Uncomment if needed/defined
#. "${RT_LIB}"
. "${RT_PROJECT_ROOT}/_lib/lib.sh"
linuxkit pkg build --force .
exit 0