Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3a026aa

Browse files
committedFeb 26, 2024
In-memory OCI artifact pull
CRI-O now pulls OCI artifacts directly in-memory after the discussions in containers/image#2306 and kubernetes/website#45121. CRI-O also enforces the config media type `application/vnd.cncf.seccomp-profile.config.v1+json` for seccomp profiles. Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
1 parent 6ff7c53 commit 3a026aa

File tree

5 files changed

+96
-109
lines changed

5 files changed

+96
-109
lines changed
 

‎internal/config/ociartifact/ociartifact.go

+57-28
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ package ociartifact
33
import (
44
"context"
55
"fmt"
6+
"io"
67

78
"github.com/containers/common/libimage"
8-
"github.com/containers/common/pkg/config"
9+
"github.com/containers/image/v5/docker"
10+
"github.com/containers/image/v5/manifest"
11+
"github.com/containers/image/v5/pkg/blobinfocache"
912
"github.com/containers/image/v5/types"
10-
"github.com/containers/storage"
1113

1214
"github.com/cri-o/cri-o/internal/log"
1315
)
1416

1517
// Impl is the main implementation interface of this package.
1618
type Impl interface {
17-
Pull(context.Context, *types.SystemContext, string) (*Artifact, error)
19+
Pull(context.Context, *types.SystemContext, string, string) (*Artifact, error)
1820
}
1921

2022
// New returns a new OCI artifact implementation.
@@ -24,54 +26,81 @@ func New() Impl {
2426

2527
// Artifact can be used to manage OCI artifacts.
2628
type Artifact struct {
27-
// MountPath is the local path containing the artifact data.
28-
MountPath string
29-
30-
// Cleanup has to be called if the artifact is not used any more.
31-
Cleanup func()
29+
// Data is the actual artifact content.
30+
Data []byte
3231
}
3332

3433
// defaultImpl is the default implementation for the OCI artifact handling.
3534
type defaultImpl struct{}
3635

37-
// Pull downloads and mounts the artifact content by using the provided ref.
38-
func (*defaultImpl) Pull(ctx context.Context, sys *types.SystemContext, ref string) (*Artifact, error) {
39-
log.Infof(ctx, "Pulling OCI artifact from ref: %s", ref)
36+
const (
37+
maxArtifactSize = 1024 * 1024 // 1 MiB
38+
)
39+
40+
// Pull downloads and mounts the artifact content by using the provided image name.
41+
func (*defaultImpl) Pull(
42+
ctx context.Context,
43+
sys *types.SystemContext,
44+
img string,
45+
enforceConfigMediaType string,
46+
) (*Artifact, error) {
47+
log.Infof(ctx, "Pulling OCI artifact from ref: %s", img)
4048

41-
storeOpts, err := storage.DefaultStoreOptions(false, 0)
49+
name, err := libimage.NormalizeName(img)
4250
if err != nil {
43-
return nil, fmt.Errorf("get default storage options: %w", err)
51+
return nil, fmt.Errorf("parse image name: %w", err)
4452
}
4553

46-
store, err := storage.GetStore(storeOpts)
54+
ref, err := docker.NewReference(name)
4755
if err != nil {
48-
return nil, fmt.Errorf("get container storage: %w", err)
56+
return nil, fmt.Errorf("create docker reference: %w", err)
4957
}
5058

51-
runtime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: sys})
59+
src, err := ref.NewImageSource(ctx, sys)
5260
if err != nil {
53-
return nil, fmt.Errorf("create libimage runtime: %w", err)
61+
return nil, fmt.Errorf("build image source: %w", err)
5462
}
5563

56-
images, err := runtime.Pull(ctx, ref, config.PullPolicyAlways, &libimage.PullOptions{})
64+
manifestBytes, mimeType, err := src.GetManifest(ctx, nil)
5765
if err != nil {
58-
return nil, fmt.Errorf("pull OCI artifact: %w", err)
66+
return nil, fmt.Errorf("get manifest: %w", err)
5967
}
60-
image := images[0]
6168

62-
mountPath, err := image.Mount(ctx, nil, "")
69+
parsedManifest, err := manifest.FromBlob(manifestBytes, mimeType)
6370
if err != nil {
64-
return nil, fmt.Errorf("mount OCI artifact: %w", err)
71+
return nil, fmt.Errorf("parse manifest: %w", err)
72+
}
73+
if enforceConfigMediaType != "" && parsedManifest.ConfigInfo().MediaType != enforceConfigMediaType {
74+
return nil, fmt.Errorf(
75+
"wrong config media type %q, requires %q",
76+
parsedManifest.ConfigInfo().MediaType, enforceConfigMediaType,
77+
)
78+
}
79+
80+
layers := parsedManifest.LayerInfos()
81+
if len(layers) < 1 {
82+
return nil, fmt.Errorf("artifact needs at least one layer")
6583
}
84+
layer := layers[0]
6685

67-
cleanup := func() {
68-
if err := image.Unmount(true); err != nil {
69-
log.Warnf(ctx, "Unable to unmount OCI artifact path %s: %v", mountPath, err)
70-
}
86+
bic := blobinfocache.DefaultCache(sys)
87+
rc, size, err := src.GetBlob(ctx, layer.BlobInfo, bic)
88+
if err != nil {
89+
return nil, fmt.Errorf("get layer blob: %w", err)
90+
}
91+
defer rc.Close()
92+
if size < 0 {
93+
return nil, fmt.Errorf("unknown layer blob size")
94+
} else if size > maxArtifactSize {
95+
return nil, fmt.Errorf("layer exceeds max size of %d bytes", maxArtifactSize)
96+
}
97+
98+
layerBytes, err := io.ReadAll(io.LimitReader(rc, maxArtifactSize))
99+
if err != nil {
100+
return nil, fmt.Errorf("read from blob: %w", err)
71101
}
72102

73103
return &Artifact{
74-
MountPath: mountPath,
75-
Cleanup: cleanup,
104+
Data: layerBytes,
76105
}, nil
77106
}

‎internal/config/seccomp/seccompociartifact/seccompociartifact.go

+11-38
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ package seccompociartifact
33
import (
44
"context"
55
"fmt"
6-
"io/fs"
7-
"os"
8-
"path/filepath"
96

107
"github.com/containers/image/v5/types"
118

@@ -27,9 +24,14 @@ func New() *SeccompOCIArtifact {
2724
}
2825
}
2926

30-
// SeccompProfilePodAnnotation is the annotation used for matching a whole pod
31-
// rather than a specific container.
32-
const SeccompProfilePodAnnotation = annotations.SeccompProfileAnnotation + "/POD"
27+
const (
28+
// SeccompProfilePodAnnotation is the annotation used for matching a whole pod
29+
// rather than a specific container.
30+
SeccompProfilePodAnnotation = annotations.SeccompProfileAnnotation + "/POD"
31+
32+
// requiredConfigMediaType is the config media type for OCI artifact seccomp profiles.
33+
requiredConfigMediaType = "application/vnd.cncf.seccomp-profile.config.v1+json"
34+
)
3335

3436
// TryPull tries to pull the OCI artifact seccomp profile while evaluating
3537
// the provided annotations.
@@ -64,40 +66,11 @@ func (s *SeccompOCIArtifact) TryPull(
6466
return nil, nil
6567
}
6668

67-
artifact, err := s.ociArtifactImpl.Pull(ctx, sys, profileRef)
69+
artifact, err := s.ociArtifactImpl.Pull(ctx, sys, profileRef, requiredConfigMediaType)
6870
if err != nil {
6971
return nil, fmt.Errorf("pull OCI artifact: %w", err)
7072
}
71-
defer artifact.Cleanup()
72-
73-
const jsonExt = ".json"
74-
seccompProfilePath := ""
75-
if err := filepath.Walk(artifact.MountPath,
76-
func(p string, info os.FileInfo, err error) error {
77-
if err != nil {
78-
return err
79-
}
80-
81-
if info.IsDir() ||
82-
info.Mode()&os.ModeSymlink == os.ModeSymlink ||
83-
filepath.Ext(info.Name()) != jsonExt {
84-
return nil
85-
}
86-
87-
seccompProfilePath = p
88-
89-
// TODO(sgrunert): allow merging profiles, not just choosing the first one
90-
return fs.SkipAll
91-
}); err != nil {
92-
return nil, fmt.Errorf("walk %s: %w", artifact.MountPath, err)
93-
}
94-
95-
log.Infof(ctx, "Trying to read profile from: %s", seccompProfilePath)
96-
profileContent, err := os.ReadFile(seccompProfilePath)
97-
if err != nil {
98-
return nil, fmt.Errorf("read %s from file store: %w", seccompProfilePath, err)
99-
}
10073

101-
log.Infof(ctx, "Retrieved OCI artifact seccomp profile of len: %d", len(profileContent))
102-
return profileContent, nil
74+
log.Infof(ctx, "Retrieved OCI artifact seccomp profile of len: %d", len(artifact.Data))
75+
return artifact.Data, nil
10376
}

‎internal/config/seccomp/seccompociartifact/seccompociartifact_test.go

+7-35
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"context"
55
"errors"
66
"io"
7-
"os"
8-
"path/filepath"
97

108
"github.com/golang/mock/gomock"
119
. "github.com/onsi/ginkgo/v2"
@@ -21,10 +19,7 @@ import (
2119
// The actual test suite
2220
var _ = t.Describe("SeccompOCIArtifact", func() {
2321
t.Describe("TryPull", func() {
24-
const (
25-
testProfileContent = "{}"
26-
testSeccompJSONFile = "seccomp.json"
27-
)
22+
const testProfileContent = "{}"
2823

2924
var (
3025
sut *seccompociartifact.SeccompOCIArtifact
@@ -44,13 +39,8 @@ var _ = t.Describe("SeccompOCIArtifact", func() {
4439
ociArtifactImplMock = ociartifactmock.NewMockImpl(mockCtrl)
4540
sut.SetOCIArtifactImpl(ociArtifactImplMock)
4641

47-
tempDir, err := os.MkdirTemp("", "seccompociartifact-test-*")
48-
Expect(err).NotTo(HaveOccurred())
49-
Expect(os.WriteFile(filepath.Join(tempDir, testSeccompJSONFile), []byte(testProfileContent), 0o644)).NotTo(HaveOccurred())
50-
5142
testArtifact = &ociartifact.Artifact{
52-
MountPath: tempDir,
53-
Cleanup: func() { os.RemoveAll(tempDir) },
43+
Data: []byte(testProfileContent),
5444
}
5545
})
5646

@@ -71,7 +61,7 @@ var _ = t.Describe("SeccompOCIArtifact", func() {
7161
It("should match image specific annotation for whole pod", func() {
7262
// Given
7363
gomock.InOrder(
74-
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
64+
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
7565
)
7666

7767
// When
@@ -88,7 +78,7 @@ var _ = t.Describe("SeccompOCIArtifact", func() {
8878
It("should match image specific annotation for container", func() {
8979
// Given
9080
gomock.InOrder(
91-
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
81+
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
9282
)
9383

9484
// When
@@ -105,7 +95,7 @@ var _ = t.Describe("SeccompOCIArtifact", func() {
10595
It("should match pod specific annotation", func() {
10696
// Given
10797
gomock.InOrder(
108-
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
98+
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
10999
)
110100

111101
// When
@@ -122,7 +112,7 @@ var _ = t.Describe("SeccompOCIArtifact", func() {
122112
It("should match container specific annotation", func() {
123113
// Given
124114
gomock.InOrder(
125-
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
115+
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
126116
)
127117

128118
// When
@@ -152,26 +142,8 @@ var _ = t.Describe("SeccompOCIArtifact", func() {
152142
It("should fail if artifact pull fails", func() {
153143
// Given
154144
gomock.InOrder(
155-
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errTest),
156-
)
157-
158-
// When
159-
res, err := sut.TryPull(context.Background(), nil, "", nil,
160-
map[string]string{
161-
seccompociartifact.SeccompProfilePodAnnotation: "test",
162-
})
163-
164-
// Then
165-
Expect(err).To(HaveOccurred())
166-
Expect(res).To(BeNil())
167-
})
168-
169-
It("should fail if seccomp.json is not in artifact", func() {
170-
// Given
171-
gomock.InOrder(
172-
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any()).Return(testArtifact, nil),
145+
ociArtifactImplMock.EXPECT().Pull(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errTest),
173146
)
174-
Expect(os.RemoveAll(filepath.Join(testArtifact.MountPath, testSeccompJSONFile))).NotTo(HaveOccurred())
175147

176148
// When
177149
res, err := sut.TryPull(context.Background(), nil, "", nil,

‎test/mocks/ociartifact/ociartifact.go

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/seccomp_oci_artifacts.bats

+17-4
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ function teardown() {
1616
cleanup_test
1717
}
1818

19-
ARTIFACT_IMAGE_WITH_ANNOTATION=quay.io/crio/nginx-seccomp:generic
20-
ARTIFACT_IMAGE_WITH_POD_ANNOTATION=quay.io/crio/nginx-seccomp:pod
21-
ARTIFACT_IMAGE_WITH_CONTAINER_ANNOTATION=quay.io/crio/nginx-seccomp:container
22-
ARTIFACT_IMAGE=quay.io/crio/seccomp:v1
19+
ARTIFACT_IMAGE_WITH_ANNOTATION=quay.io/crio/nginx-seccomp:v2
20+
ARTIFACT_IMAGE_WITH_POD_ANNOTATION=$ARTIFACT_IMAGE_WITH_ANNOTATION-pod
21+
ARTIFACT_IMAGE_WITH_CONTAINER_ANNOTATION=$ARTIFACT_IMAGE_WITH_ANNOTATION-container
22+
ARTIFACT_IMAGE=quay.io/crio/seccomp:v2
2323
CONTAINER_NAME=container1
2424
ANNOTATION=seccomp-profile.kubernetes.cri-o.io
2525
POD_ANNOTATION=seccomp-profile.kubernetes.cri-o.io/POD
@@ -196,3 +196,16 @@ TEST_SYSCALL=OCI_ARTIFACT_TEST
196196
grep -vq "Retrieved OCI artifact seccomp profile" "$CRIO_LOG"
197197
crictl inspect "$CTR" | jq -e '.info.runtimeSpec.linux.seccomp == null'
198198
}
199+
200+
@test "seccomp OCI artifact with missing artifact" {
201+
# Run with enabled feature set
202+
create_runtime_with_allowed_annotation seccomp $ANNOTATION
203+
start_crio
204+
205+
jq '.annotations += { "'$POD_ANNOTATION'": "wrong" }' \
206+
"$TESTDATA"/sandbox_config.json > "$TESTDIR"/sandbox.json
207+
208+
! crictl run "$TESTDATA/container_config.json" "$TESTDIR/sandbox.json"
209+
210+
grep -q "try to pull OCI artifact seccomp profile" "$CRIO_LOG"
211+
}

0 commit comments

Comments
 (0)
Please sign in to comment.