feat: ensure images layers correspond with the image media type (#2719)
Ensure zstd compression only gets applied to oci images. When adding a layer to an image ensure that they are compatable if not convert them. Create function to convert mediatypes between oci and docker types.
This commit is contained in:
parent
572904c1c4
commit
14b2ea5528
|
|
@ -501,29 +501,16 @@ func (s *stageBuilder) saveSnapshotToLayer(tarPath string) (v1.Layer, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var layerOpts []tarball.LayerOption
|
layerOpts := s.getLayerOptionFromOpts()
|
||||||
|
imageMediaType, err := s.image.MediaType()
|
||||||
if s.opts.CompressedCaching == true {
|
if err != nil {
|
||||||
layerOpts = append(layerOpts, tarball.WithCompressedCaching)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Only appending MediaType for OCI images as the default is docker
|
||||||
if s.opts.CompressionLevel > 0 {
|
if extractMediaTypeVendor(imageMediaType) == types.OCIVendorPrefix {
|
||||||
layerOpts = append(layerOpts, tarball.WithCompressionLevel(s.opts.CompressionLevel))
|
if s.opts.Compression == config.ZStd {
|
||||||
}
|
layerOpts = append(layerOpts, tarball.WithCompression("zstd"), tarball.WithMediaType(types.OCILayerZStd))
|
||||||
|
} else {
|
||||||
switch s.opts.Compression {
|
|
||||||
case config.ZStd:
|
|
||||||
layerOpts = append(layerOpts, tarball.WithCompression("zstd"), tarball.WithMediaType(types.OCILayerZStd))
|
|
||||||
|
|
||||||
case config.GZip:
|
|
||||||
|
|
||||||
// layer already gzipped by default
|
|
||||||
default:
|
|
||||||
mt, err := s.image.MediaType()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if strings.Contains(string(mt), types.OCIVendorPrefix) {
|
|
||||||
layerOpts = append(layerOpts, tarball.WithMediaType(types.OCILayer))
|
layerOpts = append(layerOpts, tarball.WithMediaType(types.OCILayer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -535,8 +522,101 @@ func (s *stageBuilder) saveSnapshotToLayer(tarPath string) (v1.Layer, error) {
|
||||||
|
|
||||||
return layer, nil
|
return layer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stageBuilder) getLayerOptionFromOpts() []tarball.LayerOption {
|
||||||
|
var layerOpts []tarball.LayerOption
|
||||||
|
|
||||||
|
if s.opts.CompressedCaching {
|
||||||
|
layerOpts = append(layerOpts, tarball.WithCompressedCaching)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.opts.CompressionLevel > 0 {
|
||||||
|
layerOpts = append(layerOpts, tarball.WithCompressionLevel(s.opts.CompressionLevel))
|
||||||
|
}
|
||||||
|
return layerOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMediaTypeVendor(mt types.MediaType) string {
|
||||||
|
if strings.Contains(string(mt), types.OCIVendorPrefix) {
|
||||||
|
return types.OCIVendorPrefix
|
||||||
|
}
|
||||||
|
return types.DockerVendorPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/opencontainers/image-spec/blob/main/media-types.md#compatibility-matrix
|
||||||
|
func convertMediaType(mt types.MediaType) types.MediaType {
|
||||||
|
switch mt {
|
||||||
|
case types.DockerManifestSchema1, types.DockerManifestSchema2:
|
||||||
|
return types.OCIManifestSchema1
|
||||||
|
case types.DockerManifestList:
|
||||||
|
return types.OCIImageIndex
|
||||||
|
case types.DockerLayer:
|
||||||
|
return types.OCILayer
|
||||||
|
case types.DockerConfigJSON:
|
||||||
|
return types.OCIConfigJSON
|
||||||
|
case types.DockerForeignLayer:
|
||||||
|
return types.OCIUncompressedRestrictedLayer
|
||||||
|
case types.DockerUncompressedLayer:
|
||||||
|
return types.OCIUncompressedLayer
|
||||||
|
case types.OCIImageIndex:
|
||||||
|
return types.DockerManifestList
|
||||||
|
case types.OCIManifestSchema1:
|
||||||
|
return types.DockerManifestSchema2
|
||||||
|
case types.OCIConfigJSON:
|
||||||
|
return types.DockerConfigJSON
|
||||||
|
case types.OCILayer, types.OCILayerZStd:
|
||||||
|
return types.DockerLayer
|
||||||
|
case types.OCIRestrictedLayer:
|
||||||
|
return types.DockerForeignLayer
|
||||||
|
case types.OCIUncompressedLayer:
|
||||||
|
return types.DockerUncompressedLayer
|
||||||
|
case types.OCIContentDescriptor, types.OCIUncompressedRestrictedLayer, types.DockerManifestSchema1Signed, types.DockerPluginConfig:
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stageBuilder) convertLayerMediaType(layer v1.Layer) (v1.Layer, error) {
|
||||||
|
layerMediaType, err := layer.MediaType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
imageMediaType, err := s.image.MediaType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if extractMediaTypeVendor(layerMediaType) != extractMediaTypeVendor(imageMediaType) {
|
||||||
|
layerOpts := s.getLayerOptionFromOpts()
|
||||||
|
targetMediaType := convertMediaType(layerMediaType)
|
||||||
|
|
||||||
|
if extractMediaTypeVendor(imageMediaType) == types.OCIVendorPrefix {
|
||||||
|
if s.opts.Compression == config.ZStd {
|
||||||
|
targetMediaType = types.OCILayerZStd
|
||||||
|
layerOpts = append(layerOpts, tarball.WithCompression("zstd"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layerOpts = append(layerOpts, tarball.WithMediaType(targetMediaType))
|
||||||
|
|
||||||
|
if targetMediaType != "" {
|
||||||
|
return tarball.LayerFromOpener(layer.Uncompressed, layerOpts...)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"layer with media type %v cannot be converted to a media type that matches %v",
|
||||||
|
layerMediaType,
|
||||||
|
imageMediaType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return layer, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stageBuilder) saveLayerToImage(layer v1.Layer, createdBy string) error {
|
func (s *stageBuilder) saveLayerToImage(layer v1.Layer, createdBy string) error {
|
||||||
var err error
|
var err error
|
||||||
|
layer, err = s.convertLayerMediaType(layer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
s.image, err = mutate.Append(s.image,
|
s.image, err = mutate.Append(s.image,
|
||||||
mutate.Addendum{
|
mutate.Addendum{
|
||||||
Layer: layer,
|
Layer: layer,
|
||||||
|
|
|
||||||
|
|
@ -1704,14 +1704,6 @@ func Test_ResolveCrossStageInstructions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ociFakeImage struct {
|
|
||||||
*fakeImage
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f ociFakeImage) MediaType() (types.MediaType, error) {
|
|
||||||
return types.OCIManifestSchema1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_stageBuilder_saveSnapshotToLayer(t *testing.T) {
|
func Test_stageBuilder_saveSnapshotToLayer(t *testing.T) {
|
||||||
dir, files := tempDirAndFile(t)
|
dir, files := tempDirAndFile(t)
|
||||||
type fields struct {
|
type fields struct {
|
||||||
|
|
@ -1788,7 +1780,7 @@ func Test_stageBuilder_saveSnapshotToLayer(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "oci image, zstd compression",
|
name: "oci image, zstd compression",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
image: fakeImage{},
|
image: ociFakeImage{},
|
||||||
opts: &config.KanikoOptions{
|
opts: &config.KanikoOptions{
|
||||||
ForceBuildMetadata: true,
|
ForceBuildMetadata: true,
|
||||||
Compression: config.ZStd,
|
Compression: config.ZStd,
|
||||||
|
|
@ -1847,3 +1839,144 @@ func Test_stageBuilder_saveSnapshotToLayer(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_stageBuilder_convertLayerMediaType(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
stage config.KanikoStage
|
||||||
|
image v1.Image
|
||||||
|
cf *v1.ConfigFile
|
||||||
|
baseImageDigest string
|
||||||
|
finalCacheKey string
|
||||||
|
opts *config.KanikoOptions
|
||||||
|
fileContext util.FileContext
|
||||||
|
cmds []commands.DockerCommand
|
||||||
|
args *dockerfile.BuildArgs
|
||||||
|
crossStageDeps map[int][]string
|
||||||
|
digestToCacheKey map[string]string
|
||||||
|
stageIdxToDigest map[string]string
|
||||||
|
snapshotter snapShotter
|
||||||
|
layerCache cache.LayerCache
|
||||||
|
pushLayerToCache cachePusher
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
layer v1.Layer
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
expectedMediaType types.MediaType
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "docker image w/ docker layer",
|
||||||
|
fields: fields{
|
||||||
|
image: fakeImage{},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
layer: fakeLayer{
|
||||||
|
mediaType: types.DockerLayer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMediaType: types.DockerLayer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oci image w/ oci layer",
|
||||||
|
fields: fields{
|
||||||
|
image: ociFakeImage{},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
layer: fakeLayer{
|
||||||
|
mediaType: types.OCILayer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMediaType: types.OCILayer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oci image w/ convertable docker layer",
|
||||||
|
fields: fields{
|
||||||
|
image: ociFakeImage{},
|
||||||
|
opts: &config.KanikoOptions{},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
layer: fakeLayer{
|
||||||
|
mediaType: types.DockerLayer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMediaType: types.OCILayer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oci image w/ convertable docker layer and zstd compression",
|
||||||
|
fields: fields{
|
||||||
|
image: ociFakeImage{},
|
||||||
|
opts: &config.KanikoOptions{
|
||||||
|
Compression: config.ZStd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
layer: fakeLayer{
|
||||||
|
mediaType: types.DockerLayer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMediaType: types.OCILayerZStd,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "docker image and oci zstd layer",
|
||||||
|
fields: fields{
|
||||||
|
image: dockerFakeImage{},
|
||||||
|
opts: &config.KanikoOptions{},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
layer: fakeLayer{
|
||||||
|
mediaType: types.OCILayerZStd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMediaType: types.DockerLayer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "docker image w/ uncovertable oci image",
|
||||||
|
fields: fields{
|
||||||
|
image: dockerFakeImage{},
|
||||||
|
opts: &config.KanikoOptions{},
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
layer: fakeLayer{
|
||||||
|
mediaType: types.OCIUncompressedRestrictedLayer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &stageBuilder{
|
||||||
|
stage: tt.fields.stage,
|
||||||
|
image: tt.fields.image,
|
||||||
|
cf: tt.fields.cf,
|
||||||
|
baseImageDigest: tt.fields.baseImageDigest,
|
||||||
|
finalCacheKey: tt.fields.finalCacheKey,
|
||||||
|
opts: tt.fields.opts,
|
||||||
|
fileContext: tt.fields.fileContext,
|
||||||
|
cmds: tt.fields.cmds,
|
||||||
|
args: tt.fields.args,
|
||||||
|
crossStageDeps: tt.fields.crossStageDeps,
|
||||||
|
digestToCacheKey: tt.fields.digestToCacheKey,
|
||||||
|
stageIdxToDigest: tt.fields.stageIdxToDigest,
|
||||||
|
snapshotter: tt.fields.snapshotter,
|
||||||
|
layerCache: tt.fields.layerCache,
|
||||||
|
pushLayerToCache: tt.fields.pushLayerToCache,
|
||||||
|
}
|
||||||
|
got, err := s.convertLayerMediaType(tt.args.layer)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("stageBuilder.convertLayerMediaType() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
mt, _ := got.MediaType()
|
||||||
|
if mt != tt.expectedMediaType {
|
||||||
|
t.Errorf("stageBuilder.convertLayerMediaType() = %v, want %v", mt, tt.expectedMediaType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ func (f *fakeLayerCache) RetrieveLayer(key string) (v1.Image, error) {
|
||||||
|
|
||||||
type fakeLayer struct {
|
type fakeLayer struct {
|
||||||
TarContent []byte
|
TarContent []byte
|
||||||
|
mediaType types.MediaType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fakeLayer) Digest() (v1.Hash, error) {
|
func (f fakeLayer) Digest() (v1.Hash, error) {
|
||||||
|
|
@ -163,7 +164,7 @@ func (f fakeLayer) Size() (int64, error) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
func (f fakeLayer) MediaType() (types.MediaType, error) {
|
func (f fakeLayer) MediaType() (types.MediaType, error) {
|
||||||
return "", nil
|
return f.mediaType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeImage struct {
|
type fakeImage struct {
|
||||||
|
|
@ -203,3 +204,19 @@ func (f fakeImage) LayerByDigest(v1.Hash) (v1.Layer, error) {
|
||||||
func (f fakeImage) LayerByDiffID(v1.Hash) (v1.Layer, error) {
|
func (f fakeImage) LayerByDiffID(v1.Hash) (v1.Layer, error) {
|
||||||
return fakeLayer{}, nil
|
return fakeLayer{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ociFakeImage struct {
|
||||||
|
*fakeImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f ociFakeImage) MediaType() (types.MediaType, error) {
|
||||||
|
return types.OCIManifestSchema1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerFakeImage struct {
|
||||||
|
*fakeImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f dockerFakeImage) MediaType() (types.MediaType, error) {
|
||||||
|
return types.DockerManifestSchema2, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue