Add OverrideChanges and OverrideConfig to CommitOptions
Add an OverrideChanges and an OverrideConfig field to CommitOptions, both of which can be used to make last-minute edits to the configuration of an image that we're committing. Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
parent
07482ae885
commit
3a61cc0996
2
Makefile
2
Makefile
|
@ -39,7 +39,7 @@ LIBSECCOMP_COMMIT := release-2.3
|
|||
|
||||
EXTRA_LDFLAGS ?=
|
||||
BUILDAH_LDFLAGS := $(GO_LDFLAGS) '-X main.GitCommit=$(GIT_COMMIT) -X main.buildInfo=$(SOURCE_DATE_EPOCH) -X main.cniVersion=$(CNI_COMMIT) $(EXTRA_LDFLAGS)'
|
||||
SOURCES=*.go imagebuildah/*.go bind/*.go chroot/*.go copier/*.go define/*.go docker/*.go internal/mkcw/*.go internal/mkcw/types/*.go internal/parse/*.go internal/source/*.go internal/util/*.go manifests/*.go pkg/chrootuser/*.go pkg/cli/*.go pkg/completion/*.go pkg/formats/*.go pkg/overlay/*.go pkg/parse/*.go pkg/rusage/*.go pkg/sshagent/*.go pkg/umask/*.go pkg/util/*.go util/*.go
|
||||
SOURCES=*.go imagebuildah/*.go bind/*.go chroot/*.go copier/*.go define/*.go docker/*.go internal/config/*.go internal/mkcw/*.go internal/mkcw/types/*.go internal/parse/*.go internal/source/*.go internal/util/*.go manifests/*.go pkg/chrootuser/*.go pkg/cli/*.go pkg/completion/*.go pkg/formats/*.go pkg/overlay/*.go pkg/parse/*.go pkg/rusage/*.go pkg/sshagent/*.go pkg/umask/*.go pkg/util/*.go util/*.go
|
||||
|
||||
LINTFLAGS ?=
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/containers/buildah/util"
|
||||
"github.com/containers/common/pkg/auth"
|
||||
"github.com/containers/common/pkg/completion"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/shortnames"
|
||||
storageTransport "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
|
@ -26,6 +28,8 @@ type commitInputOptions struct {
|
|||
omitHistory bool
|
||||
blobCache string
|
||||
certDir string
|
||||
changes []string
|
||||
configFile string
|
||||
creds string
|
||||
cwOptions string
|
||||
disableCompression bool
|
||||
|
@ -84,8 +88,11 @@ func commitListFlagSet(cmd *cobra.Command, opts *commitInputOptions) {
|
|||
flags.IntSliceVar(&opts.encryptLayers, "encrypt-layer", nil, "layers to encrypt, 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified")
|
||||
_ = cmd.RegisterFlagCompletionFunc("encryption-key", completion.AutocompleteNone)
|
||||
|
||||
flags.StringArrayVarP(&opts.changes, "change", "c", nil, "apply containerfile `instruction`s to the committed image")
|
||||
flags.StringVar(&opts.certDir, "cert-dir", "", "use certificates at the specified path to access the registry")
|
||||
_ = cmd.RegisterFlagCompletionFunc("cert-dir", completion.AutocompleteDefault)
|
||||
flags.StringVar(&opts.configFile, "config", "", "apply configuration JSON `file` to the committed image")
|
||||
_ = cmd.RegisterFlagCompletionFunc("config", completion.AutocompleteDefault)
|
||||
flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing the registry")
|
||||
_ = cmd.RegisterFlagCompletionFunc("creds", completion.AutocompleteNone)
|
||||
flags.StringVar(&opts.cwOptions, "cw", "", "confidential workload `options`")
|
||||
|
@ -204,6 +211,18 @@ func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error
|
|||
return fmt.Errorf("unable to obtain encryption config: %w", err)
|
||||
}
|
||||
|
||||
var overrideConfig *manifest.Schema2Config
|
||||
if c.Flag("config").Changed {
|
||||
configBytes, err := os.ReadFile(iopts.configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading configuration blob from file: %w", err)
|
||||
}
|
||||
overrideConfig = &manifest.Schema2Config{}
|
||||
if err := json.Unmarshal(configBytes, &overrideConfig); err != nil {
|
||||
return fmt.Errorf("parsing configuration blob from %q: %w", iopts.configFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
options := buildah.CommitOptions{
|
||||
PreferredManifestType: format,
|
||||
Manifest: iopts.manifest,
|
||||
|
@ -218,6 +237,8 @@ func commitCmd(c *cobra.Command, args []string, iopts commitInputOptions) error
|
|||
OciEncryptConfig: encConfig,
|
||||
OciEncryptLayers: encLayers,
|
||||
UnsetEnvs: iopts.unsetenvs,
|
||||
OverrideChanges: iopts.changes,
|
||||
OverrideConfig: overrideConfig,
|
||||
}
|
||||
exclusiveFlags := 0
|
||||
if c.Flag("reference-time").Changed {
|
||||
|
|
|
@ -305,7 +305,7 @@ func updateConfig(builder *buildah.Builder, c *cobra.Command, iopts configResult
|
|||
conditionallyAddHistory(builder, c, "/bin/sh -c #(nop) LABEL %s", strings.Join(iopts.label, " "))
|
||||
}
|
||||
// unset labels if any
|
||||
for _, key := range iopts.unsetLabels {
|
||||
for _, key := range iopts.unsetLabels {
|
||||
builder.UnsetLabel(key)
|
||||
}
|
||||
if c.Flag("workingdir").Changed {
|
||||
|
|
|
@ -110,6 +110,14 @@ type CommitOptions struct {
|
|||
// UnsetEnvs is a list of environments to not add to final image.
|
||||
// Deprecated: use UnsetEnv() before committing instead.
|
||||
UnsetEnvs []string
|
||||
// OverrideConfig is an optional Schema2Config which can override parts
|
||||
// of the working container's configuration for the image that is being
|
||||
// committed.
|
||||
OverrideConfig *manifest.Schema2Config
|
||||
// OverrideChanges is a slice of Dockerfile-style instructions to make
|
||||
// to the configuration of the image that is being committed, after
|
||||
// OverrideConfig is applied.
|
||||
OverrideChanges []string
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
@ -33,6 +33,18 @@ environment variable. `export REGISTRY_AUTH_FILE=path`
|
|||
Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry.
|
||||
The default certificates directory is _/etc/containers/certs.d_.
|
||||
|
||||
**--change**, **-c** *"INSTRUCTION"*
|
||||
|
||||
Apply the change to the committed image that would have been made if it had
|
||||
been built using a Containerfile which included the specified instruction.
|
||||
This option can be specified multiple times.
|
||||
|
||||
**--config** *filename*
|
||||
|
||||
Read a JSON-encoded version of an image configuration object from the specified
|
||||
file, and merge the values from it with the configuration of the image being
|
||||
committed.
|
||||
|
||||
**--creds** *creds*
|
||||
|
||||
The [username[:password]] to use to authenticate with the registry if required.
|
||||
|
|
11
image.go
11
image.go
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/containers/buildah/copier"
|
||||
"github.com/containers/buildah/define"
|
||||
"github.com/containers/buildah/docker"
|
||||
"github.com/containers/buildah/internal/config"
|
||||
"github.com/containers/buildah/internal/mkcw"
|
||||
"github.com/containers/buildah/internal/tmpdir"
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
|
@ -79,6 +80,8 @@ type containerImageRef struct {
|
|||
blobDirectory string
|
||||
preEmptyLayers []v1.History
|
||||
postEmptyLayers []v1.History
|
||||
overrideChanges []string
|
||||
overrideConfig *manifest.Schema2Config
|
||||
}
|
||||
|
||||
type blobLayerInfo struct {
|
||||
|
@ -298,6 +301,12 @@ func (i *containerImageRef) createConfigsAndManifests() (v1.Image, v1.Manifest,
|
|||
dimage.History = []docker.V2S2History{}
|
||||
}
|
||||
|
||||
// If we were supplied with a configuration, copy fields from it to
|
||||
// matching fields in both formats.
|
||||
if err := config.Override(dimage.Config, &oimage.Config, i.overrideChanges, i.overrideConfig); err != nil {
|
||||
return v1.Image{}, v1.Manifest{}, docker.V2Image{}, docker.V2S2Manifest{}, fmt.Errorf("applying changes: %w", err)
|
||||
}
|
||||
|
||||
// If we're producing a confidential workload, override the command and
|
||||
// assorted other settings that aren't expected to work correctly.
|
||||
if i.confidentialWorkload.Convert {
|
||||
|
@ -924,6 +933,8 @@ func (b *Builder) makeContainerImageRef(options CommitOptions) (*containerImageR
|
|||
blobDirectory: options.BlobDirectory,
|
||||
preEmptyLayers: b.PrependedEmptyLayers,
|
||||
postEmptyLayers: b.AppendedEmptyLayers,
|
||||
overrideChanges: options.OverrideChanges,
|
||||
overrideConfig: options.OverrideConfig,
|
||||
}
|
||||
return ref, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/containers/image/v5/manifest"
|
||||
dockerclient "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
// Schema2ConfigFromGoDockerclientConfig converts a go-dockerclient Config
|
||||
// structure to a manifest Schema2Config.
|
||||
func Schema2ConfigFromGoDockerclientConfig(config *dockerclient.Config) *manifest.Schema2Config {
|
||||
overrideExposedPorts := make(map[manifest.Schema2Port]struct{})
|
||||
for port := range config.ExposedPorts {
|
||||
overrideExposedPorts[manifest.Schema2Port(port)] = struct{}{}
|
||||
}
|
||||
var overrideHealthCheck *manifest.Schema2HealthConfig
|
||||
if config.Healthcheck != nil {
|
||||
overrideHealthCheck = &manifest.Schema2HealthConfig{
|
||||
Test: config.Healthcheck.Test,
|
||||
StartPeriod: config.Healthcheck.StartPeriod,
|
||||
Interval: config.Healthcheck.Interval,
|
||||
Timeout: config.Healthcheck.Timeout,
|
||||
Retries: config.Healthcheck.Retries,
|
||||
}
|
||||
}
|
||||
labels := make(map[string]string)
|
||||
for k, v := range config.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
volumes := make(map[string]struct{})
|
||||
for v := range config.Volumes {
|
||||
volumes[v] = struct{}{}
|
||||
}
|
||||
s2config := &manifest.Schema2Config{
|
||||
Hostname: config.Hostname,
|
||||
Domainname: config.Domainname,
|
||||
User: config.User,
|
||||
AttachStdin: config.AttachStdin,
|
||||
AttachStdout: config.AttachStdout,
|
||||
AttachStderr: config.AttachStderr,
|
||||
ExposedPorts: overrideExposedPorts,
|
||||
Tty: config.Tty,
|
||||
OpenStdin: config.OpenStdin,
|
||||
StdinOnce: config.StdinOnce,
|
||||
Env: append([]string{}, config.Env...),
|
||||
Cmd: append([]string{}, config.Cmd...),
|
||||
Healthcheck: overrideHealthCheck,
|
||||
ArgsEscaped: config.ArgsEscaped,
|
||||
Image: config.Image,
|
||||
Volumes: volumes,
|
||||
WorkingDir: config.WorkingDir,
|
||||
Entrypoint: append([]string{}, config.Entrypoint...),
|
||||
NetworkDisabled: config.NetworkDisabled,
|
||||
MacAddress: config.MacAddress,
|
||||
OnBuild: append([]string{}, config.OnBuild...),
|
||||
Labels: labels,
|
||||
StopSignal: config.StopSignal,
|
||||
Shell: config.Shell,
|
||||
}
|
||||
if config.StopTimeout != 0 {
|
||||
s2config.StopTimeout = &config.StopTimeout
|
||||
}
|
||||
return s2config
|
||||
}
|
||||
|
||||
// GoDockerclientConfigFromSchema2Config converts a manifest Schema2Config
|
||||
// to a go-dockerclient config structure.
|
||||
func GoDockerclientConfigFromSchema2Config(s2config *manifest.Schema2Config) *dockerclient.Config {
|
||||
overrideExposedPorts := make(map[dockerclient.Port]struct{})
|
||||
for port := range s2config.ExposedPorts {
|
||||
overrideExposedPorts[dockerclient.Port(port)] = struct{}{}
|
||||
}
|
||||
var healthCheck *dockerclient.HealthConfig
|
||||
if s2config.Healthcheck != nil {
|
||||
healthCheck = &dockerclient.HealthConfig{
|
||||
Test: s2config.Healthcheck.Test,
|
||||
StartPeriod: s2config.Healthcheck.StartPeriod,
|
||||
Interval: s2config.Healthcheck.Interval,
|
||||
Timeout: s2config.Healthcheck.Timeout,
|
||||
Retries: s2config.Healthcheck.Retries,
|
||||
}
|
||||
}
|
||||
labels := make(map[string]string)
|
||||
for k, v := range s2config.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
volumes := make(map[string]struct{})
|
||||
for v := range s2config.Volumes {
|
||||
volumes[v] = struct{}{}
|
||||
}
|
||||
config := &dockerclient.Config{
|
||||
Hostname: s2config.Hostname,
|
||||
Domainname: s2config.Domainname,
|
||||
User: s2config.User,
|
||||
AttachStdin: s2config.AttachStdin,
|
||||
AttachStdout: s2config.AttachStdout,
|
||||
AttachStderr: s2config.AttachStderr,
|
||||
PortSpecs: nil,
|
||||
ExposedPorts: overrideExposedPorts,
|
||||
Tty: s2config.Tty,
|
||||
OpenStdin: s2config.OpenStdin,
|
||||
StdinOnce: s2config.StdinOnce,
|
||||
Env: append([]string{}, s2config.Env...),
|
||||
Cmd: append([]string{}, s2config.Cmd...),
|
||||
Healthcheck: healthCheck,
|
||||
ArgsEscaped: s2config.ArgsEscaped,
|
||||
Image: s2config.Image,
|
||||
Volumes: volumes,
|
||||
WorkingDir: s2config.WorkingDir,
|
||||
Entrypoint: append([]string{}, s2config.Entrypoint...),
|
||||
NetworkDisabled: s2config.NetworkDisabled,
|
||||
MacAddress: s2config.MacAddress,
|
||||
OnBuild: append([]string{}, s2config.OnBuild...),
|
||||
Labels: labels,
|
||||
StopSignal: s2config.StopSignal,
|
||||
Shell: s2config.Shell,
|
||||
}
|
||||
if s2config.StopTimeout != nil {
|
||||
config.StopTimeout = *s2config.StopTimeout
|
||||
}
|
||||
return config
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/buildah/util"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
dockerclient "github.com/fsouza/go-dockerclient"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fillAllFields recursively fills in 1 or "1" for every field in the passed-in
|
||||
// structure, and that slices and maps have at least one value in them.
|
||||
func fillAllFields[pStruct any](t *testing.T, st pStruct) {
|
||||
v := reflect.ValueOf(st)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v = reflect.Indirect(v)
|
||||
}
|
||||
fillAllValueFields(t, v)
|
||||
}
|
||||
|
||||
func fillAllValueFields(t *testing.T, v reflect.Value) {
|
||||
fields := reflect.VisibleFields(v.Type())
|
||||
for _, field := range fields {
|
||||
if field.Anonymous {
|
||||
// all right, fine, keep your secrets
|
||||
continue
|
||||
}
|
||||
f := v.FieldByName(field.Name)
|
||||
var keyType, elemType reflect.Type
|
||||
if field.Type.Kind() == reflect.Map {
|
||||
keyType = field.Type.Key()
|
||||
}
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
elemType = field.Type.Elem()
|
||||
}
|
||||
fillValue(t, f, field.Name, field.Type.Kind(), keyType, elemType)
|
||||
}
|
||||
}
|
||||
|
||||
func fillValue(t *testing.T, value reflect.Value, name string, kind reflect.Kind, keyType, elemType reflect.Type) {
|
||||
switch kind {
|
||||
case reflect.Invalid,
|
||||
reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, reflect.UnsafePointer,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.Complex64, reflect.Complex128:
|
||||
require.NotEqualf(t, kind, kind, "unhandled %s field %s: tests require updating", kind, name)
|
||||
case reflect.Bool:
|
||||
value.SetBool(true)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
value.SetInt(1)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
value.SetUint(1)
|
||||
case reflect.Map:
|
||||
if value.IsNil() {
|
||||
value.Set(reflect.MakeMap(value.Type()))
|
||||
}
|
||||
keyPtr := reflect.New(keyType)
|
||||
key := reflect.Indirect(keyPtr)
|
||||
fillValue(t, key, name, keyType.Kind(), nil, nil)
|
||||
elemPtr := reflect.New(elemType)
|
||||
elem := reflect.Indirect(elemPtr)
|
||||
fillValue(t, elem, name, elemType.Kind(), nil, nil)
|
||||
value.SetMapIndex(key, reflect.Indirect(elem))
|
||||
case reflect.Slice:
|
||||
vPtr := reflect.New(elemType)
|
||||
v := reflect.Indirect(vPtr)
|
||||
fillValue(t, v, name, elemType.Kind(), nil, nil)
|
||||
value.Set(reflect.Append(reflect.MakeSlice(value.Type(), 0, 1), v))
|
||||
case reflect.String:
|
||||
value.SetString("1")
|
||||
case reflect.Struct:
|
||||
fillAllValueFields(t, value)
|
||||
case reflect.Pointer:
|
||||
p := reflect.New(elemType)
|
||||
fillValue(t, reflect.Indirect(p), name, elemType.Kind(), nil, nil)
|
||||
value.Set(p)
|
||||
}
|
||||
}
|
||||
|
||||
// checkAllFields recursively checks that every field not listed in allowZeroed
|
||||
// is not set to its zero value, that every slice is not empty, and that every
|
||||
// map has at least one entry. It makes an additional exception for structs
|
||||
// which have no defined fields.
|
||||
func checkAllFields[pStruct any](t *testing.T, st pStruct, allowZeroed []string) {
|
||||
v := reflect.ValueOf(st)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v = reflect.Indirect(v)
|
||||
}
|
||||
checkAllValueFields(t, v, "", allowZeroed)
|
||||
}
|
||||
|
||||
func checkAllValueFields(t *testing.T, v reflect.Value, name string, allowedToBeZero []string) {
|
||||
fields := reflect.VisibleFields(v.Type())
|
||||
for _, field := range fields {
|
||||
if field.Anonymous {
|
||||
// all right, fine, keep your secrets
|
||||
continue
|
||||
}
|
||||
fieldName := field.Name
|
||||
if name != "" {
|
||||
fieldName = name + "." + field.Name
|
||||
}
|
||||
if util.StringInSlice(fieldName, allowedToBeZero) {
|
||||
continue
|
||||
}
|
||||
f := v.FieldByName(field.Name)
|
||||
var elemType reflect.Type
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Array, reflect.Chan, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
elemType = field.Type.Elem()
|
||||
}
|
||||
checkValue(t, f, fieldName, field.Type.Kind(), elemType, allowedToBeZero)
|
||||
}
|
||||
}
|
||||
|
||||
func checkValue(t *testing.T, value reflect.Value, name string, kind reflect.Kind, elemType reflect.Type, allowedToBeZero []string) {
|
||||
if kind != reflect.Invalid {
|
||||
switch kind {
|
||||
case reflect.Map:
|
||||
assert.Falsef(t, value.IsZero(), "map field %s not set when it was not already expected to be left unpopulated by conversion", name)
|
||||
keys := value.MapKeys()
|
||||
for i := 0; i < len(keys); i++ {
|
||||
v := value.MapIndex(keys[i])
|
||||
checkValue(t, v, name+"{"+keys[i].String()+"}", elemType.Kind(), nil, allowedToBeZero)
|
||||
}
|
||||
case reflect.Slice:
|
||||
assert.Falsef(t, value.IsZero(), "slice field %s not set when it was not already expected to be left unpopulated by conversion", name)
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
v := value.Index(i)
|
||||
checkValue(t, v, name+"["+strconv.Itoa(i)+"]", elemType.Kind(), nil, allowedToBeZero)
|
||||
}
|
||||
case reflect.Struct:
|
||||
if fields := reflect.VisibleFields(value.Type()); len(fields) != 0 {
|
||||
// structs which are defined with no fields are okay
|
||||
assert.Falsef(t, value.IsZero(), "slice field %s not set when it was not already expected to be left unpopulated by conversion", name)
|
||||
}
|
||||
checkAllValueFields(t, value, name, allowedToBeZero)
|
||||
case reflect.Pointer:
|
||||
assert.Falsef(t, value.IsZero(), "pointer field %s not set when it was not already expected to be left unpopulated by conversion", name)
|
||||
checkValue(t, reflect.Indirect(value), name, elemType.Kind(), nil, allowedToBeZero)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoDockerclientConfigFromSchema2Config(t *testing.T) {
|
||||
var input manifest.Schema2Config
|
||||
fillAllFields(t, &input)
|
||||
output := GoDockerclientConfigFromSchema2Config(&input)
|
||||
// make exceptions for fields in "output" which have no corresponding field in "input"
|
||||
notInSchema2Config := []string{"CPUSet", "CPUShares", "DNS", "Memory", "KernelMemory", "MemorySwap", "MemoryReservation", "Mounts", "PortSpecs", "PublishService", "SecurityOpts", "VolumeDriver", "VolumesFrom"}
|
||||
checkAllFields(t, output, notInSchema2Config)
|
||||
}
|
||||
|
||||
func TestSchema2ConfigFromGoDockerclientConfig(t *testing.T) {
|
||||
var input dockerclient.Config
|
||||
fillAllFields(t, &input)
|
||||
output := Schema2ConfigFromGoDockerclientConfig(&input)
|
||||
// make exceptions for fields in "output" which have no corresponding field in "input"
|
||||
notInDockerConfig := []string{}
|
||||
checkAllFields(t, output, notInDockerConfig)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
dockerclient "github.com/fsouza/go-dockerclient"
|
||||
"github.com/openshift/imagebuilder"
|
||||
)
|
||||
|
||||
// configOnlyExecutor implements the Executor interface that an
|
||||
// imagebuilder.Builder expects to be able to call to do some heavy lifting,
|
||||
// but it just refuses to do the work of ADD, COPY, or RUN. It also doesn't
|
||||
// care if the working directory exists in a container, because it's really
|
||||
// only concerned with letting the Builder's RunConfig get updated by changes
|
||||
// from a Dockerfile. Try anything more than that and it'll return an error.
|
||||
type configOnlyExecutor struct{}
|
||||
|
||||
func (g *configOnlyExecutor) Preserve(path string) error {
|
||||
return errors.New("ADD/COPY/RUN not supported as changes")
|
||||
}
|
||||
|
||||
func (g *configOnlyExecutor) EnsureContainerPath(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *configOnlyExecutor) EnsureContainerPathAs(path, user string, mode *os.FileMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *configOnlyExecutor) Copy(excludes []string, copies ...imagebuilder.Copy) error {
|
||||
if len(copies) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.New("ADD/COPY not supported as changes")
|
||||
}
|
||||
|
||||
func (g *configOnlyExecutor) Run(run imagebuilder.Run, config dockerclient.Config) error {
|
||||
return errors.New("RUN not supported as changes")
|
||||
}
|
||||
|
||||
func (g *configOnlyExecutor) UnrecognizedInstruction(step *imagebuilder.Step) error {
|
||||
return fmt.Errorf("did not understand change instruction %q", step.Original)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package config
|
||||
|
||||
import "github.com/openshift/imagebuilder"
|
||||
|
||||
var _ imagebuilder.Executor = &configOnlyExecutor{}
|
|
@ -0,0 +1,181 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/buildah/docker"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/openshift/imagebuilder"
|
||||
)
|
||||
|
||||
// firstStringElseSecondString takes two strings, and returns the first
|
||||
// string if it isn't empty, else the second string
|
||||
func firstStringElseSecondString(first, second string) string {
|
||||
if first != "" {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// firstSliceElseSecondSlice takes two string slices, and returns the first
|
||||
// slice of strings if it has contents, else the second slice
|
||||
func firstSliceElseSecondSlice(first, second []string) []string {
|
||||
if len(first) > 0 {
|
||||
return append([]string{}, first...)
|
||||
}
|
||||
return append([]string{}, second...)
|
||||
}
|
||||
|
||||
// firstSlicePairElseSecondSlicePair takes two pairs of string slices, and
|
||||
// returns the first pair of slices if either has contents, else the second
|
||||
// pair
|
||||
func firstSlicePairElseSecondSlicePair(firstA, firstB, secondA, secondB []string) ([]string, []string) {
|
||||
if len(firstA) > 0 || len(firstB) > 0 {
|
||||
return append([]string{}, firstA...), append([]string{}, firstB...)
|
||||
}
|
||||
return append([]string{}, secondA...), append([]string{}, secondB...)
|
||||
}
|
||||
|
||||
// mergeEnv combines variables from a and b into a single environment slice. if
|
||||
// a and b both provide values for the same variable, the value from b is
|
||||
// preferred
|
||||
func mergeEnv(a, b []string) []string {
|
||||
index := make(map[string]int)
|
||||
results := make([]string, 0, len(a)+len(b))
|
||||
for _, kv := range append(append([]string{}, a...), b...) {
|
||||
k, _, specifiesValue := strings.Cut(kv, "=")
|
||||
if !specifiesValue {
|
||||
if value, ok := os.LookupEnv(kv); ok {
|
||||
kv = kv + "=" + value
|
||||
} else {
|
||||
kv = kv + "="
|
||||
}
|
||||
}
|
||||
if i, seen := index[k]; seen {
|
||||
results[i] = kv
|
||||
} else {
|
||||
index[k] = len(results)
|
||||
results = append(results, kv)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Override takes a buildah docker config and an OCI ImageConfig, and applies a
|
||||
// mixture of a slice of Dockerfile-style instructions and fields from a config
|
||||
// blob to them both
|
||||
func Override(dconfig *docker.Config, oconfig *v1.ImageConfig, overrideChanges []string, overrideConfig *manifest.Schema2Config) error {
|
||||
if len(overrideChanges) > 0 {
|
||||
if overrideConfig == nil {
|
||||
overrideConfig = &manifest.Schema2Config{}
|
||||
}
|
||||
// Parse the set of changes as we would a Dockerfile.
|
||||
changes := strings.Join(overrideChanges, "\n")
|
||||
parsed, err := imagebuilder.ParseDockerfile(strings.NewReader(changes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing change set %+v: %w", changes, err)
|
||||
}
|
||||
// Create a dummy builder object to process configuration-related
|
||||
// instructions.
|
||||
subBuilder := imagebuilder.NewBuilder(nil)
|
||||
// Convert the incoming data into an initial RunConfig.
|
||||
subBuilder.RunConfig = *GoDockerclientConfigFromSchema2Config(overrideConfig)
|
||||
// Process the change instructions one by one.
|
||||
for _, node := range parsed.Children {
|
||||
var step imagebuilder.Step
|
||||
if err := step.Resolve(node); err != nil {
|
||||
return fmt.Errorf("resolving change %q: %w", node.Original, err)
|
||||
}
|
||||
if err := subBuilder.Run(&step, &configOnlyExecutor{}, true); err != nil {
|
||||
return fmt.Errorf("processing change %q: %w", node.Original, err)
|
||||
}
|
||||
}
|
||||
// Pull settings out of the dummy builder's RunConfig.
|
||||
overrideConfig = Schema2ConfigFromGoDockerclientConfig(&subBuilder.RunConfig)
|
||||
}
|
||||
if overrideConfig != nil {
|
||||
// Apply changes from a possibly-provided possibly-changed config struct.
|
||||
dconfig.Hostname = firstStringElseSecondString(overrideConfig.Hostname, dconfig.Hostname)
|
||||
dconfig.Domainname = firstStringElseSecondString(overrideConfig.Domainname, dconfig.Domainname)
|
||||
dconfig.User = firstStringElseSecondString(overrideConfig.User, dconfig.User)
|
||||
oconfig.User = firstStringElseSecondString(overrideConfig.User, oconfig.User)
|
||||
dconfig.AttachStdin = overrideConfig.AttachStdin
|
||||
dconfig.AttachStdout = overrideConfig.AttachStdout
|
||||
dconfig.AttachStderr = overrideConfig.AttachStderr
|
||||
if len(overrideConfig.ExposedPorts) > 0 {
|
||||
dexposedPorts := make(map[docker.Port]struct{})
|
||||
oexposedPorts := make(map[string]struct{})
|
||||
for port := range dconfig.ExposedPorts {
|
||||
dexposedPorts[port] = struct{}{}
|
||||
}
|
||||
for port := range overrideConfig.ExposedPorts {
|
||||
dexposedPorts[docker.Port(port)] = struct{}{}
|
||||
}
|
||||
for port := range oconfig.ExposedPorts {
|
||||
oexposedPorts[port] = struct{}{}
|
||||
}
|
||||
for port := range overrideConfig.ExposedPorts {
|
||||
oexposedPorts[string(port)] = struct{}{}
|
||||
}
|
||||
dconfig.ExposedPorts = dexposedPorts
|
||||
oconfig.ExposedPorts = oexposedPorts
|
||||
}
|
||||
dconfig.Tty = overrideConfig.Tty
|
||||
dconfig.OpenStdin = overrideConfig.OpenStdin
|
||||
dconfig.StdinOnce = overrideConfig.StdinOnce
|
||||
if len(overrideConfig.Env) > 0 {
|
||||
dconfig.Env = mergeEnv(dconfig.Env, overrideConfig.Env)
|
||||
oconfig.Env = mergeEnv(oconfig.Env, overrideConfig.Env)
|
||||
}
|
||||
dconfig.Entrypoint, dconfig.Cmd = firstSlicePairElseSecondSlicePair(overrideConfig.Entrypoint, overrideConfig.Cmd, dconfig.Entrypoint, dconfig.Cmd)
|
||||
oconfig.Entrypoint, oconfig.Cmd = firstSlicePairElseSecondSlicePair(overrideConfig.Entrypoint, overrideConfig.Cmd, oconfig.Entrypoint, oconfig.Cmd)
|
||||
if overrideConfig.Healthcheck != nil {
|
||||
dconfig.Healthcheck = &docker.HealthConfig{
|
||||
Test: append([]string{}, overrideConfig.Healthcheck.Test...),
|
||||
Interval: overrideConfig.Healthcheck.Interval,
|
||||
Timeout: overrideConfig.Healthcheck.Timeout,
|
||||
StartPeriod: overrideConfig.Healthcheck.StartPeriod,
|
||||
Retries: overrideConfig.Healthcheck.Retries,
|
||||
}
|
||||
}
|
||||
dconfig.ArgsEscaped = overrideConfig.ArgsEscaped
|
||||
dconfig.Image = firstStringElseSecondString(overrideConfig.Image, dconfig.Image)
|
||||
if len(overrideConfig.Volumes) > 0 {
|
||||
if dconfig.Volumes == nil {
|
||||
dconfig.Volumes = make(map[string]struct{})
|
||||
}
|
||||
if oconfig.Volumes == nil {
|
||||
oconfig.Volumes = make(map[string]struct{})
|
||||
}
|
||||
for volume := range overrideConfig.Volumes {
|
||||
dconfig.Volumes[volume] = struct{}{}
|
||||
oconfig.Volumes[volume] = struct{}{}
|
||||
}
|
||||
}
|
||||
dconfig.WorkingDir = firstStringElseSecondString(overrideConfig.WorkingDir, dconfig.WorkingDir)
|
||||
oconfig.WorkingDir = firstStringElseSecondString(overrideConfig.WorkingDir, oconfig.WorkingDir)
|
||||
dconfig.NetworkDisabled = overrideConfig.NetworkDisabled
|
||||
dconfig.MacAddress = overrideConfig.MacAddress
|
||||
dconfig.OnBuild = overrideConfig.OnBuild
|
||||
if len(overrideConfig.Labels) > 0 {
|
||||
if dconfig.Labels == nil {
|
||||
dconfig.Labels = make(map[string]string)
|
||||
}
|
||||
if oconfig.Labels == nil {
|
||||
oconfig.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range overrideConfig.Labels {
|
||||
dconfig.Labels[k] = v
|
||||
oconfig.Labels[k] = v
|
||||
}
|
||||
}
|
||||
dconfig.StopSignal = overrideConfig.StopSignal
|
||||
oconfig.StopSignal = overrideConfig.StopSignal
|
||||
dconfig.StopTimeout = overrideConfig.StopTimeout
|
||||
dconfig.Shell = firstSliceElseSecondSlice(overrideConfig.Shell, dconfig.Shell)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
@ -25,12 +26,15 @@ import (
|
|||
"github.com/containers/buildah/copier"
|
||||
"github.com/containers/buildah/define"
|
||||
"github.com/containers/buildah/imagebuildah"
|
||||
"github.com/containers/buildah/internal/config"
|
||||
"github.com/containers/image/v5/docker/daemon"
|
||||
"github.com/containers/image/v5/image"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/compression"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
istorage "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
|
@ -119,34 +123,37 @@ func TestMain(m *testing.M) {
|
|||
flag.StringVar(&imagebuilderDir, "imagebuilder-dir", imagebuilderDir, "location to save imagebuilder build results")
|
||||
flag.StringVar(&buildahDir, "buildah-dir", buildahDir, "location to save buildah build results")
|
||||
flag.Parse()
|
||||
var tempdir string
|
||||
if buildahDir == "" || dockerDir == "" || imagebuilderDir == "" {
|
||||
if tempdir == "" {
|
||||
if tempdir, err = os.MkdirTemp("", "conformance"); err != nil {
|
||||
logrus.Fatalf("creating temporary directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if buildahDir == "" {
|
||||
buildahDir = filepath.Join(tempdir, "buildah")
|
||||
}
|
||||
if dockerDir == "" {
|
||||
dockerDir = filepath.Join(tempdir, "docker")
|
||||
}
|
||||
if imagebuilderDir == "" {
|
||||
imagebuilderDir = filepath.Join(tempdir, "imagebuilder")
|
||||
}
|
||||
level, err := logrus.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
logrus.Fatalf("error parsing log level %q: %v", logLevel, err)
|
||||
}
|
||||
logrus.SetLevel(level)
|
||||
os.Exit(m.Run())
|
||||
result := m.Run()
|
||||
if err = os.RemoveAll(tempdir); err != nil {
|
||||
logrus.Errorf("removing temporary directory %q: %v", tempdir, err)
|
||||
}
|
||||
os.Exit(result)
|
||||
}
|
||||
|
||||
func TestConformance(t *testing.T) {
|
||||
var tempdir string
|
||||
if buildahDir == "" {
|
||||
if tempdir == "" {
|
||||
tempdir = t.TempDir()
|
||||
}
|
||||
buildahDir = filepath.Join(tempdir, "buildah")
|
||||
}
|
||||
if dockerDir == "" {
|
||||
if tempdir == "" {
|
||||
tempdir = t.TempDir()
|
||||
}
|
||||
dockerDir = filepath.Join(tempdir, "docker")
|
||||
}
|
||||
if imagebuilderDir == "" {
|
||||
if tempdir == "" {
|
||||
tempdir = t.TempDir()
|
||||
}
|
||||
imagebuilderDir = filepath.Join(tempdir, "imagebuilder")
|
||||
}
|
||||
dateStamp := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
for i := range internalTestCases {
|
||||
t.Run(internalTestCases[i].name, func(t *testing.T) {
|
||||
|
@ -3075,3 +3082,506 @@ var internalTestCases = []testCase{
|
|||
dockerUseBuildKit: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestCommit(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
baseImage string
|
||||
changes, derivedChanges []string
|
||||
config, derivedConfig *docker.Config
|
||||
}{
|
||||
{
|
||||
description: "defaults",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
},
|
||||
{
|
||||
description: "empty change",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{""},
|
||||
},
|
||||
{
|
||||
description: "empty config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{},
|
||||
},
|
||||
{
|
||||
description: "cmd just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"CMD /bin/imaginarySh"},
|
||||
},
|
||||
{
|
||||
description: "cmd just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
Cmd: []string{"/usr/bin/imaginarySh"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "cmd conflict",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"CMD /bin/imaginarySh"},
|
||||
config: &docker.Config{
|
||||
Cmd: []string{"/usr/bin/imaginarySh"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "entrypoint just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"ENTRYPOINT /bin/imaginarySh"},
|
||||
},
|
||||
{
|
||||
description: "entrypoint just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
Entrypoint: []string{"/usr/bin/imaginarySh"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "entrypoint conflict",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"ENTRYPOINT /bin/imaginarySh"},
|
||||
config: &docker.Config{
|
||||
Entrypoint: []string{"/usr/bin/imaginarySh"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "environment just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"ENV A=1", "ENV C=2"},
|
||||
},
|
||||
{
|
||||
description: "environment just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
Env: []string{"A=B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "environment with conflict union",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"ENV A=1", "ENV C=2"},
|
||||
config: &docker.Config{
|
||||
Env: []string{"A=B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "expose just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"EXPOSE 12345"},
|
||||
},
|
||||
{
|
||||
description: "expose just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
ExposedPorts: map[docker.Port]struct{}{"23456": struct{}{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "expose union",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"EXPOSE 12345"},
|
||||
config: &docker.Config{
|
||||
ExposedPorts: map[docker.Port]struct{}{"23456": struct{}{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "healthcheck just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{`HEALTHCHECK --interval=1s --timeout=1s --start-period=1s --retries=1 CMD ["/bin/false"]`},
|
||||
},
|
||||
{
|
||||
description: "healthcheck just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
Healthcheck: &docker.HealthConfig{
|
||||
Test: []string{"/bin/true"},
|
||||
Interval: 2 * time.Second,
|
||||
Timeout: 2 * time.Second,
|
||||
StartPeriod: 2 * time.Second,
|
||||
Retries: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "healthcheck conflict",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{`HEALTHCHECK --interval=1s --timeout=1s --start-period=1s --retries=1 CMD ["/bin/false"]`},
|
||||
config: &docker.Config{
|
||||
Healthcheck: &docker.HealthConfig{
|
||||
Test: []string{"/bin/true"},
|
||||
Interval: 2 * time.Second,
|
||||
Timeout: 2 * time.Second,
|
||||
StartPeriod: 2 * time.Second,
|
||||
Retries: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "label just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"LABEL A=1 C=2"},
|
||||
},
|
||||
{
|
||||
description: "label just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
Labels: map[string]string{"A": "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "label with conflict union",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"LABEL A=1 C=2"},
|
||||
config: &docker.Config{
|
||||
Labels: map[string]string{"A": "B"},
|
||||
},
|
||||
},
|
||||
// n.b. dockerd didn't like a MAINTAINER change, so no test for it, and it's not in a config blob
|
||||
{
|
||||
description: "onbuild just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"ONBUILD USER alice", "ONBUILD LABEL A=1"},
|
||||
derivedChanges: []string{"LABEL C=3"},
|
||||
},
|
||||
{
|
||||
description: "onbuild just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
OnBuild: []string{"USER bob", `CMD ["/bin/smash"]`, "LABEL B=2"},
|
||||
},
|
||||
derivedChanges: []string{"LABEL C=3"},
|
||||
},
|
||||
{
|
||||
description: "onbuild conflict",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"ONBUILD USER alice", "ONBUILD LABEL A=1"},
|
||||
config: &docker.Config{
|
||||
OnBuild: []string{"USER bob", `CMD ["/bin/smash"]`, "LABEL B=2"},
|
||||
},
|
||||
derivedChanges: []string{"LABEL C=3"},
|
||||
},
|
||||
// n.b. dockerd didn't like a SHELL change, so no test for it or a conflict with a config blob
|
||||
{
|
||||
description: "shell just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
Shell: []string{"/usr/bin/imaginarySh"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "stop signal conflict",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"STOPSIGNAL SIGTERM"},
|
||||
config: &docker.Config{
|
||||
StopSignal: "SIGKILL",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "stop timeout=0",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
StopTimeout: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "stop timeout=15",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
StopTimeout: 15,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "stop timeout=15, then 0",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
StopTimeout: 15,
|
||||
},
|
||||
derivedConfig: &docker.Config{
|
||||
StopTimeout: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "stop timeout=0, then 15",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
StopTimeout: 0,
|
||||
},
|
||||
derivedConfig: &docker.Config{
|
||||
StopTimeout: 15,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "user just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"USER 1001:1001"},
|
||||
},
|
||||
{
|
||||
description: "user just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
User: "1000:1000",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "user with conflict",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"USER 1001:1001"},
|
||||
config: &docker.Config{
|
||||
User: "1000:1000",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "volume just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"VOLUME /a-volume"},
|
||||
},
|
||||
{
|
||||
description: "volume just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
Volumes: map[string]struct{}{"/b-volume": struct{}{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "volume union",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"VOLUME /a-volume"},
|
||||
config: &docker.Config{
|
||||
Volumes: map[string]struct{}{"/b-volume": struct{}{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "workdir just changes",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"WORKDIR /yeah"},
|
||||
},
|
||||
{
|
||||
description: "workdir just config",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
config: &docker.Config{
|
||||
WorkingDir: "/naw",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "workdir with conflict",
|
||||
baseImage: "docker.io/library/busybox",
|
||||
changes: []string{"WORKDIR /yeah"},
|
||||
config: &docker.Config{
|
||||
WorkingDir: "/naw",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var tempdir string
|
||||
buildahDir := buildahDir
|
||||
if buildahDir == "" {
|
||||
if tempdir == "" {
|
||||
tempdir = t.TempDir()
|
||||
}
|
||||
buildahDir = filepath.Join(tempdir, "buildah")
|
||||
}
|
||||
dockerDir := dockerDir
|
||||
if dockerDir == "" {
|
||||
if tempdir == "" {
|
||||
tempdir = t.TempDir()
|
||||
}
|
||||
dockerDir = filepath.Join(tempdir, "docker")
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
// connect to dockerd using go-dockerclient
|
||||
client, err := docker.NewClientFromEnv()
|
||||
require.NoErrorf(t, err, "unable to initialize docker client")
|
||||
var dockerVersion []string
|
||||
if version, err := client.Version(); err == nil {
|
||||
if version != nil {
|
||||
for _, s := range *version {
|
||||
dockerVersion = append(dockerVersion, s)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
require.NoErrorf(t, err, "unable to connect to docker daemon")
|
||||
}
|
||||
|
||||
// find a new place to store buildah builds
|
||||
tempdir = t.TempDir()
|
||||
|
||||
// create subdirectories to use for buildah storage
|
||||
rootDir := filepath.Join(tempdir, "root")
|
||||
runrootDir := filepath.Join(tempdir, "runroot")
|
||||
|
||||
// initialize storage for buildah
|
||||
options := storage.StoreOptions{
|
||||
GraphDriverName: os.Getenv("STORAGE_DRIVER"),
|
||||
GraphRoot: rootDir,
|
||||
RunRoot: runrootDir,
|
||||
RootlessStoragePath: rootDir,
|
||||
}
|
||||
store, err := storage.GetStore(options)
|
||||
require.NoErrorf(t, err, "error creating buildah storage at %q", rootDir)
|
||||
defer func() {
|
||||
if store != nil {
|
||||
_, err := store.Shutdown(true)
|
||||
require.NoErrorf(t, err, "error shutting down storage for buildah")
|
||||
}
|
||||
}()
|
||||
|
||||
// walk through test cases
|
||||
for testIndex, testCase := range testCases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
test := testCases[testIndex]
|
||||
|
||||
// create the test container, then commit it, using the docker client
|
||||
baseImage := test.baseImage
|
||||
repository, tag := docker.ParseRepositoryTag(baseImage)
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
baseImage = repository + ":" + tag
|
||||
if _, err := client.InspectImage(test.baseImage); err != nil && errors.Is(err, docker.ErrNoSuchImage) {
|
||||
// oh, we need to pull the base image
|
||||
err = client.PullImage(docker.PullImageOptions{
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
}, docker.AuthConfiguration{})
|
||||
require.NoErrorf(t, err, "pulling base image")
|
||||
}
|
||||
container, err := client.CreateContainer(docker.CreateContainerOptions{
|
||||
Context: ctx,
|
||||
Config: &docker.Config{
|
||||
Image: baseImage,
|
||||
},
|
||||
})
|
||||
require.NoErrorf(t, err, "creating the working container with docker")
|
||||
if err == nil {
|
||||
defer func(containerName string) {
|
||||
err := client.RemoveContainer(docker.RemoveContainerOptions{
|
||||
ID: containerName,
|
||||
Force: true,
|
||||
})
|
||||
assert.Nil(t, err, "error deleting working docker container %q", containerName)
|
||||
}(container.ID)
|
||||
}
|
||||
dockerImageName := "committed:" + strconv.Itoa(testIndex)
|
||||
dockerImage, err := client.CommitContainer(docker.CommitContainerOptions{
|
||||
Container: container.ID,
|
||||
Changes: test.changes,
|
||||
Run: test.config,
|
||||
Repository: dockerImageName,
|
||||
})
|
||||
assert.NoErrorf(t, err, "committing the working container with docker")
|
||||
if err == nil {
|
||||
defer func(dockerImageName string) {
|
||||
err := client.RemoveImageExtended(dockerImageName, docker.RemoveImageOptions{
|
||||
Context: ctx,
|
||||
Force: true,
|
||||
})
|
||||
assert.Nil(t, err, "error deleting newly-built docker image %q", dockerImage.ID)
|
||||
}(dockerImageName)
|
||||
}
|
||||
dockerRef, err := alltransports.ParseImageName("docker-daemon:" + dockerImageName)
|
||||
assert.NoErrorf(t, err, "parsing name of newly-committed docker image")
|
||||
|
||||
if len(test.derivedChanges) > 0 || test.derivedConfig != nil {
|
||||
container, err := client.CreateContainer(docker.CreateContainerOptions{
|
||||
Context: ctx,
|
||||
Config: &docker.Config{
|
||||
Image: dockerImage.ID,
|
||||
},
|
||||
})
|
||||
require.NoErrorf(t, err, "creating the derived container with docker")
|
||||
if err == nil {
|
||||
defer func(containerName string) {
|
||||
err := client.RemoveContainer(docker.RemoveContainerOptions{
|
||||
ID: containerName,
|
||||
Force: true,
|
||||
})
|
||||
assert.Nil(t, err, "error deleting derived docker container %q", containerName)
|
||||
}(container.ID)
|
||||
}
|
||||
derivedImageName := "derived:" + strconv.Itoa(testIndex)
|
||||
derivedImage, err := client.CommitContainer(docker.CommitContainerOptions{
|
||||
Container: container.ID,
|
||||
Changes: test.derivedChanges,
|
||||
Run: test.derivedConfig,
|
||||
Repository: derivedImageName,
|
||||
})
|
||||
assert.NoErrorf(t, err, "committing the derived container with docker")
|
||||
defer func(derivedImageName string) {
|
||||
err := client.RemoveImageExtended(derivedImageName, docker.RemoveImageOptions{
|
||||
Context: ctx,
|
||||
Force: true,
|
||||
})
|
||||
assert.Nil(t, err, "error deleting newly-derived docker image %q", derivedImage.ID)
|
||||
}(derivedImageName)
|
||||
dockerRef, err = alltransports.ParseImageName("docker-daemon:" + derivedImageName)
|
||||
assert.NoErrorf(t, err, "parsing name of newly-derived docker image")
|
||||
}
|
||||
|
||||
// create the test container, then commit it, using the buildah API
|
||||
builder, err := buildah.NewBuilder(ctx, store, buildah.BuilderOptions{
|
||||
FromImage: baseImage,
|
||||
})
|
||||
require.NoErrorf(t, err, "creating the working container with buildah")
|
||||
defer func(builder *buildah.Builder) {
|
||||
err := builder.Delete()
|
||||
assert.NoErrorf(t, err, "removing the working container")
|
||||
}(builder)
|
||||
var overrideConfig *manifest.Schema2Config
|
||||
if test.config != nil {
|
||||
overrideConfig = config.Schema2ConfigFromGoDockerclientConfig(test.config)
|
||||
}
|
||||
buildahID, _, _, err := builder.Commit(ctx, nil, buildah.CommitOptions{
|
||||
PreferredManifestType: manifest.DockerV2Schema2MediaType,
|
||||
OverrideChanges: test.changes,
|
||||
OverrideConfig: overrideConfig,
|
||||
})
|
||||
assert.NoErrorf(t, err, "committing buildah image")
|
||||
buildahRef, err := is.Transport.NewStoreReference(store, nil, buildahID)
|
||||
assert.NoErrorf(t, err, "parsing name of newly-built buildah image")
|
||||
|
||||
if len(test.derivedChanges) > 0 || test.derivedConfig != nil {
|
||||
derivedBuilder, err := buildah.NewBuilder(ctx, store, buildah.BuilderOptions{
|
||||
FromImage: buildahID,
|
||||
})
|
||||
defer func(builder *buildah.Builder) {
|
||||
err := builder.Delete()
|
||||
assert.NoErrorf(t, err, "removing the derived container")
|
||||
}(derivedBuilder)
|
||||
require.NoErrorf(t, err, "creating the derived container with buildah")
|
||||
var overrideConfig *manifest.Schema2Config
|
||||
if test.derivedConfig != nil {
|
||||
overrideConfig = config.Schema2ConfigFromGoDockerclientConfig(test.derivedConfig)
|
||||
}
|
||||
derivedID, _, _, err := builder.Commit(ctx, nil, buildah.CommitOptions{
|
||||
PreferredManifestType: manifest.DockerV2Schema2MediaType,
|
||||
OverrideChanges: test.derivedChanges,
|
||||
OverrideConfig: overrideConfig,
|
||||
})
|
||||
assert.NoErrorf(t, err, "committing derived buildah image")
|
||||
buildahRef, err = is.Transport.NewStoreReference(store, nil, derivedID)
|
||||
assert.NoErrorf(t, err, "parsing name of newly-derived buildah image")
|
||||
}
|
||||
|
||||
// scan the images
|
||||
saveReport(ctx, t, dockerRef, filepath.Join(dockerDir, t.Name()), []byte{}, []byte{}, dockerVersion)
|
||||
saveReport(ctx, t, buildahRef, filepath.Join(buildahDir, t.Name()), []byte{}, []byte{}, dockerVersion)
|
||||
// compare the scans
|
||||
_, originalDockerConfig, ociDockerConfig, fsDocker := readReport(t, filepath.Join(dockerDir, t.Name()))
|
||||
_, originalBuildahConfig, ociBuildahConfig, fsBuildah := readReport(t, filepath.Join(buildahDir, t.Name()))
|
||||
miss, left, diff, same := compareJSON(originalDockerConfig, originalBuildahConfig, originalSkip)
|
||||
if !same {
|
||||
assert.Failf(t, "Image configurations differ as committed in Docker format", configCompareResult(miss, left, diff, "buildah"))
|
||||
}
|
||||
miss, left, diff, same = compareJSON(ociDockerConfig, ociBuildahConfig, ociSkip)
|
||||
if !same {
|
||||
assert.Failf(t, "Image configurations differ when converted to OCI format", configCompareResult(miss, left, diff, "buildah"))
|
||||
}
|
||||
miss, left, diff, same = compareJSON(fsDocker, fsBuildah, fsSkip)
|
||||
if !same {
|
||||
assert.Failf(t, "Filesystem contents differ", fsCompareResult(miss, left, diff, "buildah"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue