Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure images layers correspond with the image media type #2719

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 95 additions & 15 deletions pkg/executor/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,42 +501,122 @@ func (s *stageBuilder) saveSnapshotToLayer(tarPath string) (v1.Layer, error) {
return nil, nil
}

layerOpts := s.getLayerOptionFromOpts()
imageMediaType, err := s.image.MediaType()
if err != nil {
return nil, err
}
// Only appending MediaType for OCI images as the default is docker
if extractMediaTypeVendor(imageMediaType) == types.OCIVendorPrefix {
if s.opts.Compression == config.ZStd {
layerOpts = append(layerOpts, tarball.WithCompression("zstd"), tarball.WithMediaType(types.OCILayerZStd))
} else {
layerOpts = append(layerOpts, tarball.WithMediaType(types.OCILayer))
}
}

layer, err := tarball.LayerFromFile(tarPath, layerOpts...)
if err != nil {
return nil, err
}

return layer, nil
}

func (s *stageBuilder) getLayerOptionFromOpts() []tarball.LayerOption {
var layerOpts []tarball.LayerOption

if s.opts.CompressedCaching == true {
if s.opts.CompressedCaching {
layerOpts = append(layerOpts, tarball.WithCompressedCaching)
}

if s.opts.CompressionLevel > 0 {
layerOpts = append(layerOpts, tarball.WithCompressionLevel(s.opts.CompressionLevel))
}
return layerOpts
}

switch s.opts.Compression {
case config.ZStd:
layerOpts = append(layerOpts, tarball.WithCompression("zstd"), tarball.WithMediaType(types.OCILayerZStd))

case config.GZip:
func extractMediaTypeVendor(mt types.MediaType) string {
if strings.Contains(string(mt), types.OCIVendorPrefix) {
return types.OCIVendorPrefix
}
return types.DockerVendorPrefix
}

// layer already gzipped by default
// 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:
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))
}
return ""
}
}

layer, err := tarball.LayerFromFile(tarPath, layerOpts...)
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 {
var err error
layer, err = s.convertLayerMediaType(layer)
if err != nil {
return err
}
s.image, err = mutate.Append(s.image,
mutate.Addendum{
Layer: layer,
Expand Down
151 changes: 142 additions & 9 deletions pkg/executor/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
dir, files := tempDirAndFile(t)
type fields struct {
Expand Down Expand Up @@ -1788,7 +1780,7 @@ func Test_stageBuilder_saveSnapshotToLayer(t *testing.T) {
{
name: "oci image, zstd compression",
fields: fields{
image: fakeImage{},
image: ociFakeImage{},
opts: &config.KanikoOptions{
ForceBuildMetadata: true,
Compression: config.ZStd,
Expand Down Expand Up @@ -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)
}
}
})
}
}
19 changes: 18 additions & 1 deletion pkg/executor/fakes.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (f *fakeLayerCache) RetrieveLayer(key string) (v1.Image, error) {

type fakeLayer struct {
TarContent []byte
mediaType types.MediaType
}

func (f fakeLayer) Digest() (v1.Hash, error) {
Expand All @@ -163,7 +164,7 @@ func (f fakeLayer) Size() (int64, error) {
return 0, nil
}
func (f fakeLayer) MediaType() (types.MediaType, error) {
return "", nil
return f.mediaType, nil
}

type fakeImage struct {
Expand Down Expand Up @@ -203,3 +204,19 @@ func (f fakeImage) LayerByDigest(v1.Hash) (v1.Layer, error) {
func (f fakeImage) LayerByDiffID(v1.Hash) (v1.Layer, error) {
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