Merge pull request #3986 from deitch/dockerfile-in-build-yml

Dockerfile in build yml and CLI; tag templates
This commit is contained in:
Avi Deitcher 2024-02-21 12:21:17 -08:00 committed by GitHub
commit 72be49c81c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 150 additions and 13 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

@ -22,6 +22,7 @@ func pkgCmd() *cobra.Command {
hashPath string hashPath string
dirty bool dirty bool
devMode bool devMode bool
tag string
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -36,6 +37,7 @@ func pkgCmd() *cobra.Command {
HashPath: hashPath, HashPath: hashPath,
Dirty: dirty, Dirty: dirty,
Dev: devMode, Dev: devMode,
Tag: tag,
} }
if cmd.Flags().Changed("disable-cache") && cmd.Flags().Changed("enable-cache") { if cmd.Flags().Changed("disable-cache") && cmd.Flags().Changed("enable-cache") {
return errors.New("cannot set but disable-cache and enable-cache") return errors.New("cannot set but disable-cache and enable-cache")
@ -85,6 +87,7 @@ func pkgCmd() *cobra.Command {
cmd.PersistentFlags().StringVar(&argOrg, "org", piBase.Org, "Override the hub org") cmd.PersistentFlags().StringVar(&argOrg, "org", piBase.Org, "Override the hub org")
cmd.PersistentFlags().StringVar(&buildYML, "build-yml", "build.yml", "Override the name of the yml file") cmd.PersistentFlags().StringVar(&buildYML, "build-yml", "build.yml", "Override the name of the yml file")
cmd.PersistentFlags().StringVar(&hash, "hash", "", "Override the image hash (default is to query git for the package's tree-sh)") cmd.PersistentFlags().StringVar(&hash, "hash", "", "Override the image hash (default is to query git for the package's tree-sh)")
cmd.PersistentFlags().StringVar(&tag, "tag", "{{.Hash}}", "Override the tag using fixed strings and/or text templates. Acceptable are .Hash for the hash")
cmd.PersistentFlags().StringVar(&hashCommit, "hash-commit", "HEAD", "Override the git commit to use for the hash") cmd.PersistentFlags().StringVar(&hashCommit, "hash-commit", "HEAD", "Override the git commit to use for the hash")
cmd.PersistentFlags().StringVar(&hashPath, "hash-path", "", "Override the directory to use for the image hash, must be a parent of the package dir (default is to use the package dir)") cmd.PersistentFlags().StringVar(&hashPath, "hash-path", "", "Override the directory to use for the image hash, must be a parent of the package dir (default is to use the package dir)")
cmd.PersistentFlags().BoolVar(&dirty, "force-dirty", false, "Force the pkg(s) to be considered dirty") cmd.PersistentFlags().BoolVar(&dirty, "force-dirty", false, "Force the pkg(s) to be considered dirty")

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")
} }
@ -534,6 +534,7 @@ func TestBuild(t *testing.T) {
} }
opts = append(opts, WithBuildPlatforms(targets...)) opts = append(opts, WithBuildPlatforms(targets...))
} }
tt.p.dockerfile = "testdata/Dockerfile"
err := tt.p.Build(opts...) err := tt.p.Build(opts...)
switch { switch {
case (tt.err == "" && err != nil) || (tt.err != "" && err == nil) || (tt.err != "" && err != nil && !strings.HasPrefix(err.Error(), tt.err)): case (tt.err == "" && err != nil) || (tt.err != "" && err == nil) || (tt.err != "" && err != nil && !strings.HasPrefix(err.Error(), tt.err)):

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

@ -1,12 +1,14 @@
package pkglib package pkglib
import ( import (
"bytes"
"crypto/sha1" "crypto/sha1"
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"text/template"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -18,6 +20,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"` // ??
@ -49,8 +52,10 @@ type PkglibConfig struct {
HashPath string HashPath string
Dirty bool Dirty bool
Dev bool Dev bool
Tag string // Tag is a text/template string, defaults to {{.Hash}}
} }
// NewPkInfo returns a new pkgInfo with default values
func NewPkgInfo() pkgInfo { func NewPkgInfo() pkgInfo {
return pkgInfo{ return pkgInfo{
Org: "linuxkit", Org: "linuxkit",
@ -58,6 +63,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,7 +90,9 @@ type Pkg struct {
// Internal state // Internal state
path string path string
dockerfile string
hash string hash string
tag string
dirty bool dirty bool
commitHash string commitHash string
git *git git *git
@ -250,6 +258,16 @@ func NewFromConfig(cfg PkglibConfig, args ...string) ([]Pkg, error) {
} }
} }
// calculate the tag to use based on the template and the pkgHash
tmpl, err := template.New("tag").Parse(cfg.Tag)
if err != nil {
return nil, fmt.Errorf("invalid tag template: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, map[string]string{"Hash": pkgHash}); err != nil {
return nil, fmt.Errorf("failed to execute tag template: %v", err)
}
tag := buf.String()
pkgs = append(pkgs, Pkg{ pkgs = append(pkgs, Pkg{
image: pi.Image, image: pi.Image,
org: pi.Org, org: pi.Org,
@ -265,7 +283,9 @@ 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,
tag: tag,
}) })
} }
return pkgs, nil return pkgs, nil
@ -290,7 +310,7 @@ func (p Pkg) ReleaseTag(release string) (string, error) {
// Tag returns the tag to use for the package // Tag returns the tag to use for the package
func (p Pkg) Tag() string { func (p Pkg) Tag() string {
t := p.hash t := p.tag
if t == "" { if t == "" {
t = "latest" t = "latest"
} }

View File

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