Skip to content

Commit 4c089d6

Browse files
committed
fix: support gzip-compressed OCI artifacts (application/x-tgz).
This enables KLM to handle OCI artifacts compressed with gzip, which are generated by modulectl 2.x + OCM CLI workflow.
1 parent 2afd005 commit 4c089d6

File tree

5 files changed

+147
-6
lines changed

5 files changed

+147
-6
lines changed

internal/manifest/img/parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func getOCIRef(
110110
accessSpec *localblob.AccessSpec,
111111
) (*OCI, error) {
112112
layerRef := OCI{}
113-
if accessSpec.MediaType == mime.MIME_TAR {
113+
if accessSpec.MediaType == mime.MIME_TAR || accessSpec.MediaType == mime.MIME_TGZ {
114114
layerRef.Type = string(v1beta2.OciDirType)
115115
} else {
116116
layerRef.Type = string(v1beta2.OciRefType)

internal/manifest/img/parse_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ func TestParse(t *testing.T) {
3535
Type: "oci-dir",
3636
},
3737
},
38+
}, {
39+
"should parse raw-manifest layer from mediaType: application/x-tgz",
40+
"v1beta2_template_operator_tgz_format.yaml",
41+
"1.0.0-tgz-format",
42+
img.Layer{
43+
LayerName: "raw-manifest",
44+
LayerRepresentation: &img.OCI{
45+
Repo: "europe-west3-docker.pkg.dev/sap-kyma-jellyfish-dev/template-operator/component-descriptors",
46+
Name: testutils.DefaultFQDN,
47+
Ref: "sha256:d2cc278224a71384b04963a83e784da311a268a2b3fa8732bc31e70ca0c5bc52",
48+
Type: "oci-dir",
49+
},
50+
},
3851
}, {
3952
"should parse raw-manifest layer from mediaType: application/octet-stream",
4053
"v1beta2_template_operator_current_ocm.yaml",

internal/manifest/img/pathextractor.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package img
22

33
import (
44
"archive/tar"
5+
"compress/gzip"
56
"context"
67
"errors"
78
"fmt"
@@ -135,7 +136,17 @@ func (p PathExtractor) ExtractLayer(tarPath string) (string, error) {
135136
}
136137
defer tarFile.Close()
137138

138-
tarReader := tar.NewReader(tarFile)
139+
var tarReader *tar.Reader
140+
gzipReader, err := gzip.NewReader(tarFile)
141+
if err == nil {
142+
defer gzipReader.Close()
143+
tarReader = tar.NewReader(gzipReader)
144+
} else {
145+
if _, err := tarFile.Seek(0, 0); err != nil {
146+
return "", fmt.Errorf("failed to seek file: %w", err)
147+
}
148+
tarReader = tar.NewReader(tarFile)
149+
}
139150
for {
140151
header, err := tarReader.Next()
141152
if errors.Is(err, io.EOF) {

internal/manifest/img/pathextractor_test.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package img_test
33
import (
44
"archive/tar"
55
"bytes"
6+
"compress/gzip"
67
"fmt"
78
"os"
89
"path/filepath"
@@ -19,7 +20,19 @@ import (
1920
)
2021

2122
func TestPathExtractor_ExtractLayer(t *testing.T) {
22-
content, tarFilePath := generateDummyTarFile(t)
23+
t.Run("should extract uncompressed tar file", func(t *testing.T) {
24+
content, tarFilePath := generateDummyTarFile(t, false)
25+
testExtractLayer(t, content, tarFilePath)
26+
})
27+
28+
t.Run("should extract gzip-compressed tar file", func(t *testing.T) {
29+
content, tarFilePath := generateDummyTarFile(t, true)
30+
testExtractLayer(t, content, tarFilePath)
31+
})
32+
}
33+
34+
func testExtractLayer(t *testing.T, content []byte, tarFilePath string) {
35+
t.Helper()
2336
pathExtractor := img.NewPathExtractor()
2437
numGoroutines := 5
2538
resultCh := make(chan string, numGoroutines)
@@ -115,9 +128,11 @@ func TestPathExtractor_FetchLayerToFile(t *testing.T) {
115128
}
116129
}
117130

118-
func generateDummyTarFile(t *testing.T) ([]byte, string) {
131+
func generateDummyTarFile(t *testing.T, compress bool) ([]byte, string) {
119132
t.Helper()
120133
var buf bytes.Buffer
134+
135+
// Create tar writer
121136
tarWriter := tar.NewWriter(&buf)
122137

123138
content := []byte("file-content")
@@ -135,8 +150,28 @@ func generateDummyTarFile(t *testing.T) ([]byte, string) {
135150

136151
err = tarWriter.Close()
137152
require.NoError(t, err)
138-
tarFilePath := filepath.Join(os.TempDir(), "test.tar")
139-
err = os.WriteFile(tarFilePath, buf.Bytes(), 0o600)
153+
154+
// Get tar bytes
155+
tarBytes := buf.Bytes()
156+
157+
// Optionally compress with gzip
158+
if compress {
159+
var gzipBuf bytes.Buffer
160+
gzipWriter := gzip.NewWriter(&gzipBuf)
161+
_, err = gzipWriter.Write(tarBytes)
162+
require.NoError(t, err)
163+
err = gzipWriter.Close()
164+
require.NoError(t, err)
165+
tarBytes = gzipBuf.Bytes()
166+
}
167+
168+
// Write to file
169+
ext := ".tar"
170+
if compress {
171+
ext = ".tar.gz"
172+
}
173+
tarFilePath := filepath.Join(os.TempDir(), "test"+ext)
174+
err = os.WriteFile(tarFilePath, tarBytes, 0o600)
140175
require.NoError(t, err)
141176
return content, tarFilePath
142177
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
apiVersion: operator.kyma-project.io/v1beta2
2+
kind: ModuleTemplate
3+
metadata:
4+
name: template-operator-regular
5+
namespace: kcp-system
6+
annotations:
7+
"operator.kyma-project.io/is-cluster-scoped": "false"
8+
"operator.kyma-project.io/module-version": "1.0.0-tgz-format"
9+
spec:
10+
channel: regular
11+
mandatory: false
12+
data:
13+
apiVersion: operator.kyma-project.io/v1alpha1
14+
kind: Sample
15+
metadata:
16+
name: sample-yaml
17+
spec:
18+
initKey: initValue
19+
resourceFilePath: "./module-data/yaml"
20+
descriptor:
21+
component:
22+
componentReferences: [ ]
23+
creationTime: "2024-07-09T12:22:30Z"
24+
name: kyma-project.io/module/template-operator
25+
provider: kyma-project.io
26+
repositoryContexts:
27+
- baseUrl: europe-west3-docker.pkg.dev
28+
componentNameMapping: urlPath
29+
subPath: sap-kyma-jellyfish-dev/template-operator
30+
type: OCIRegistry
31+
resources:
32+
- access:
33+
imageReference: europe-docker.pkg.dev/kyma-project/prod/template-operator:1.0.0
34+
type: ociArtifact
35+
digest:
36+
hashAlgorithm: SHA-256
37+
normalisationAlgorithm: ociArtifactDigest/v1
38+
value: 03a194e1dca2421755cec5ec1e946de744407e6e1ca3b671f715fee939e8d1fb
39+
name: module-image
40+
relation: external
41+
type: ociArtifact
42+
version: 1.0.0
43+
- access:
44+
localReference: sha256:d2cc278224a71384b04963a83e784da311a268a2b3fa8732bc31e70ca0c5bc52
45+
mediaType: application/x-tgz
46+
type: localBlob
47+
digest:
48+
hashAlgorithm: SHA-256
49+
normalisationAlgorithm: genericBlobDigest/v1
50+
value: d2cc278224a71384b04963a83e784da311a268a2b3fa8732bc31e70ca0c5bc52
51+
name: raw-manifest
52+
relation: local
53+
type: directory
54+
version: 1.0.0
55+
- access:
56+
localReference: sha256:9230471fa6a62ff7b1549e8d0e9ccb545896fabadf82d2ec4503fc798d2bcd8a
57+
mediaType: application/x-tar
58+
type: localBlob
59+
digest:
60+
hashAlgorithm: SHA-256
61+
normalisationAlgorithm: genericBlobDigest/v1
62+
value: 9230471fa6a62ff7b1549e8d0e9ccb545896fabadf82d2ec4503fc798d2bcd8a
63+
name: default-cr
64+
relation: local
65+
type: directory
66+
version: 1.0.0
67+
- access:
68+
localReference: sha256:b46281580f6377bf10672b5a8f156d183d47c0ec3bcda8b807bd8c5d520884bd
69+
mediaType: application/octet-stream
70+
type: localBlob
71+
digest:
72+
hashAlgorithm: SHA-256
73+
normalisationAlgorithm: genericBlobDigest/v1
74+
value: b46281580f6377bf10672b5a8f156d183d47c0ec3bcda8b807bd8c5d520884bd
75+
name: associated-resources
76+
relation: local
77+
type: plainText
78+
version: 1.0.0
79+
sources: [ ]
80+
version: 1.0.0-tgz-format
81+
meta:
82+
schemaVersion: v2

0 commit comments

Comments
 (0)