diff --git a/README.md b/README.md index 5b3b18867..224890bac 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ expect - see [Known Issues](#known-issues). - [Flag `--cleanup`](#flag---cleanup) - [Flag `--compression`](#flag---compression) - [Flag `--compression-level`](#flag---compression-level) + - [Flag `--image-format`](#flag---image-format) - [Flag `--compressed-caching`](#flag---compressed-caching) - [Flag `--context-sub-path`](#flag---context-sub-path) - [Flag `--credential-helpers`](#flag---credential-helpers) @@ -742,6 +743,10 @@ Use this flag to select the compression algorithm `[gzip, zstd]`. Defaults to `g Use this flag to select the compression level. Defaults to `-1` (no compression) +#### Flag `--image-format` + +Use this flag to select the output image media type `[docker, oci]`. `docker` writes a Docker schema2 manifest, `oci` writes an OCI image manifest. When unset, kaniko inherits the format of the base image. + #### Flag `--compressed-caching` Set this to false in order to prevent tar compression for cached layers. This diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index 46b4696e2..53c3ea12b 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -150,6 +150,15 @@ var RootCmd = &cobra.Command{ if !opts.NoPush && len(opts.Destinations) == 0 { return errors.New("you must provide --destination, or use --no-push") } + if opts.ImageFormat == config.ImageFormatDocker && opts.Compression == config.ZStd { + return errors.New("--compression=zstd cannot be used with --image-format=docker, the Docker schema2 format has no zstd layer media type, use --image-format=oci") + } + if opts.TarPath != "" && opts.ImageFormat == config.ImageFormatOCI { + return errors.New("--tar-path writes a Docker schema2 tarball and cannot be used with --image-format=oci, use --oci-layout-path to write an OCI image") + } + if opts.TarPath != "" && opts.Compression == config.ZStd { + return errors.New("--compression=zstd cannot be used with --tar-path, the Docker schema2 tarball has no zstd layer media type, use --oci-layout-path for zstd layers") + } if err := cacheFlagsValid(); err != nil { return fmt.Errorf("cache flags invalid: %w", err) } @@ -284,6 +293,7 @@ func AddKanikoOptionsFlags(cmd *cobra.Command, opts *config.KanikoOptions) { cmd.Flags().StringVarP(&opts.ImageNameTagDigestFile, "image-name-tag-with-digest-file", "", "", "Specify a file to save the image name w/ image tag w/ digest of the built image to.") cmd.Flags().StringVarP(&opts.OCILayoutPath, "oci-layout-path", "", "", "Path to save the OCI image layout of the built image.") cmd.Flags().VarP(&opts.Compression, "compression", "", "Compression algorithm (gzip, zstd)") + cmd.Flags().VarP(&opts.ImageFormat, "image-format", "", "Output image media type (docker, oci). Defaults to inheriting the format of the base image.") cmd.Flags().IntVarP(&opts.CompressionLevel, "compression-level", "", -1, "Compression level") cmd.Flags().BoolVarP(&opts.Cache, "cache", "", false, "Use cache when building image") cmd.Flags().BoolVarP(&opts.CompressedCaching, "compressed-caching", "", true, "Compress the cached layers. Decreases build time, but increases memory usage.") diff --git a/integration/dockerfiles/Dockerfile_test_issue_mz849 b/integration/dockerfiles/Dockerfile_test_issue_mz849 new file mode 100644 index 000000000..2d827d0f4 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_issue_mz849 @@ -0,0 +1,4 @@ +# mz849: --image-format=docker forces Docker schema2 output +# and overrides the OCI scratch base. +FROM scratch +COPY context/foo /foo diff --git a/integration/images.go b/integration/images.go index d562468ee..171f4409c 100644 --- a/integration/images.go +++ b/integration/images.go @@ -125,6 +125,7 @@ var additionalDockerFlagsMap = map[string][]string{ "Dockerfile_test_issue_mz661": {"--secret=id=kaniko,src=context/foo"}, // provenance forces ociv1 on buildkit but for these images we emit dockerv2 in kaniko "Dockerfile_test_cross_compile": {"--platform=linux/" + crossCompileArch}, + "Dockerfile_test_issue_mz849": {"--provenance=false"}, "Dockerfile_test_mv_add": {"--provenance=false"}, "Dockerfile_test_snapshotter_ignorelist": {"--provenance=false"}, "Dockerfile_test_whitelist": {"--provenance=false"}, @@ -170,6 +171,7 @@ var executorImages = map[string]string{ // Arguments to build Dockerfiles with when building with kaniko var additionalKanikoFlagsMap = map[string][]string{ "Dockerfile_test_issue_519": {"--target=final_stage,nosquash1,nosquash2"}, + "Dockerfile_test_issue_mz849": {"--image-format=docker"}, "Dockerfile_test_multistage_args_issue_1911": {"--target=base-custom2,nosquash1,nosquash2,nosquash3"}, "Dockerfile_test_cmd": {"--target=final,nosquash"}, "Dockerfile_test_issue_mz247": {"--target=final,nosquash"}, diff --git a/pkg/config/options.go b/pkg/config/options.go index 2245f5f43..c8be052b6 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -77,6 +77,7 @@ type KanikoOptions struct { ImageNameTagDigestFile string OCILayoutPath string Compression Compression + ImageFormat ImageFormat CompressionLevel int ImageFSExtractRetry int SingleSnapshot bool @@ -182,6 +183,34 @@ func (c *Compression) Type() string { return "compression" } +// ImageFormat is the output image media type +// unset means inherit from the base image (legacy behaviour) +type ImageFormat string + +const ( + ImageFormatInherit ImageFormat = "" + ImageFormatDocker ImageFormat = "docker" + ImageFormatOCI ImageFormat = "oci" +) + +func (f *ImageFormat) String() string { + return string(*f) +} + +func (f *ImageFormat) Set(v string) error { + switch v { + case "docker", "oci": + *f = ImageFormat(v) + return nil + default: + return errors.New(`must be either "docker" or "oci"`) + } +} + +func (f *ImageFormat) Type() string { + return "imageformat" +} + // WarmerOptions are options that are set by command line arguments to the cache warmer. type WarmerOptions struct { CacheOptions diff --git a/pkg/executor/build.go b/pkg/executor/build.go index e428535b4..d4bffe2d8 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -101,6 +101,10 @@ func newStageBuilder(sourceImage v1.Image, args *dockerfile.BuildArgs, opts *con if !stage.Push { _opts.Labels = []string{} } + sourceImage, err := applyImageFormat(sourceImage, opts.ImageFormat) + if err != nil { + return nil, err + } imageConfig, err := initializeConfig(sourceImage, &_opts) if err != nil { return nil, err @@ -716,6 +720,8 @@ func saveSnapshotToLayer(tarPath string, imageMediaType types.MediaType, opts *c } else { layerOpts = append(layerOpts, tarball.WithMediaType(types.OCILayer)) } + } else if opts.Compression == config.ZStd { + logrus.Warn("ignoring --compression=zstd, the Docker schema2 output format has no zstd layer media type, use --image-format=oci for zstd layers") } layer, err := tarball.LayerFromFile(tarPath, layerOpts...) @@ -746,6 +752,17 @@ func extractMediaTypeVendor(mt types.MediaType) string { return types.DockerVendorPrefix } +func applyImageFormat(image v1.Image, format config.ImageFormat) (v1.Image, error) { + switch format { + case config.ImageFormatOCI: + return image_util.WithMediaType(image, types.OCIManifestSchema1) + case config.ImageFormatDocker: + return image_util.WithMediaType(image, types.DockerManifestSchema2) + default: + return image, nil + } +} + // https://github.com/opencontainers/image-spec/blob/main/media-types.md#compatibility-matrix func convertMediaType(mt types.MediaType) types.MediaType { switch mt { diff --git a/pkg/image/transform.go b/pkg/image/transform.go index ea1724696..9bba5debe 100644 --- a/pkg/image/transform.go +++ b/pkg/image/transform.go @@ -18,11 +18,14 @@ package image import ( "encoding/json" + "fmt" + "strings" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" "github.com/osscontainertools/kaniko/pkg/util" ) @@ -132,3 +135,81 @@ func (n *noAnnotationsImage) RawManifest() ([]byte, error) { func (n *noAnnotationsImage) Digest() (v1.Hash, error) { return partial.Digest(n) } + +func WithMediaType(img v1.Image, manifestMT types.MediaType) (v1.Image, error) { + vendor := mediaTypeVendor(manifestMT) + configMT := types.DockerConfigJSON + if vendor == types.OCIVendorPrefix { + configMT = types.OCIConfigJSON + } + relabeled, err := relabelLayers(img, vendor) + if err != nil { + return nil, err + } + return mutate.ConfigMediaType(mutate.MediaType(relabeled, manifestMT), configMT), nil +} + +func relabelLayers(img v1.Image, vendor string) (v1.Image, error) { + man, err := img.Manifest() + if err != nil { + return nil, err + } + relabeled := man.DeepCopy() + for i := range relabeled.Layers { + mt, err := relabelLayerMediaType(relabeled.Layers[i].MediaType, vendor) + if err != nil { + return nil, err + } + relabeled.Layers[i].MediaType = mt + } + return &layerRelabeledImage{Image: img, manifest: relabeled}, nil +} + +type layerRelabeledImage struct { + v1.Image + manifest *v1.Manifest +} + +func (i *layerRelabeledImage) Manifest() (*v1.Manifest, error) { + return i.manifest.DeepCopy(), nil +} + +func (i *layerRelabeledImage) RawManifest() ([]byte, error) { + return json.Marshal(i.manifest) +} + +func mediaTypeVendor(mt types.MediaType) string { + if strings.Contains(string(mt), types.OCIVendorPrefix) { + return types.OCIVendorPrefix + } + return types.DockerVendorPrefix +} + +func relabelLayerMediaType(mt types.MediaType, vendor string) (types.MediaType, error) { + if mediaTypeVendor(mt) == vendor { + return mt, nil + } + switch vendor { + case types.OCIVendorPrefix: + switch mt { + case types.DockerLayer: + return types.OCILayer, nil + case types.DockerUncompressedLayer: + return types.OCIUncompressedLayer, nil + case types.DockerForeignLayer: + return types.OCIRestrictedLayer, nil + } + case types.DockerVendorPrefix: + switch mt { + case types.OCILayer: + return types.DockerLayer, nil + case types.OCIUncompressedLayer: + return types.DockerUncompressedLayer, nil + case types.OCIRestrictedLayer: + return types.DockerForeignLayer, nil + case types.OCILayerZStd: + return "", fmt.Errorf("cannot relabel zstd layer %q to docker schema2, which has no zstd media type, use --image-format=oci", mt) + } + } + return "", fmt.Errorf("cannot relabel layer media type %q to %s", mt, vendor) +}