mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 13:27:16 +00:00
Image config labels are copied onto the container by both the CRI plugin (BuildLabels) and the client's WithImageConfigLabels option used by `ctr run`. Labels in the containerd.io/* namespace are interpreted by containerd itself and labels in the io.cri-containerd* namespace are interpreted by the CRI plugin. An image config is not a trusted source for labels in either namespace. Skip labels in both reserved namespaces when copying labels from an image config to a container, and warn about each label skipped: an image that tries to set them may be attempting to alter containerd behavior. Oversized image labels are already skipped this way by the CRI plugin. Labels set explicitly by clients, for example via `ctr run --label` or in the CRI request, are unaffected. Verified with the CRI plugin and with `ctr run` against an image whose config carries labels like these: the labels are no longer present on the created container and a warning is logged for each. Assisted-by: Claude Code Signed-off-by: Ben Cressey <ben@cressey.org> Signed-off-by: Samuel Karp <samuelkarp@google.com>
287 lines
7.4 KiB
Go
287 lines
7.4 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package util
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
crilabels "github.com/containerd/containerd/v2/internal/cri/labels"
|
|
"github.com/containerd/errdefs/pkg/errgrpc"
|
|
"github.com/containerd/ttrpc"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
|
)
|
|
|
|
func TestPassThroughAnnotationsFilter(t *testing.T) {
|
|
for _, test := range []struct {
|
|
desc string
|
|
podAnnotations map[string]string
|
|
runtimePodAnnotations []string
|
|
passthroughAnnotations map[string]string
|
|
}{
|
|
{
|
|
desc: "should support direct match",
|
|
podAnnotations: map[string]string{"c": "d", "d": "e"},
|
|
runtimePodAnnotations: []string{"c"},
|
|
passthroughAnnotations: map[string]string{"c": "d"},
|
|
},
|
|
{
|
|
desc: "should support wildcard match",
|
|
podAnnotations: map[string]string{
|
|
"t.f": "j",
|
|
"z.g": "o",
|
|
"z": "o",
|
|
"y.ca": "b",
|
|
"y": "b",
|
|
},
|
|
runtimePodAnnotations: []string{"*.f", "z*g", "y.c*"},
|
|
passthroughAnnotations: map[string]string{
|
|
"t.f": "j",
|
|
"z.g": "o",
|
|
"y.ca": "b",
|
|
},
|
|
},
|
|
{
|
|
desc: "should support wildcard match all",
|
|
podAnnotations: map[string]string{
|
|
"t.f": "j",
|
|
"z.g": "o",
|
|
"z": "o",
|
|
"y.ca": "b",
|
|
"y": "b",
|
|
},
|
|
runtimePodAnnotations: []string{"*"},
|
|
passthroughAnnotations: map[string]string{
|
|
"t.f": "j",
|
|
"z.g": "o",
|
|
"z": "o",
|
|
"y.ca": "b",
|
|
"y": "b",
|
|
},
|
|
},
|
|
{
|
|
desc: "should support match including path separator",
|
|
podAnnotations: map[string]string{
|
|
"matchend.com/end": "1",
|
|
"matchend.com/end1": "2",
|
|
"matchend.com/1end": "3",
|
|
"matchmid.com/mid": "4",
|
|
"matchmid.com/mi1d": "5",
|
|
"matchmid.com/mid1": "6",
|
|
"matchhead.com/head": "7",
|
|
"matchhead.com/1head": "8",
|
|
"matchhead.com/head1": "9",
|
|
"matchall.com/abc": "10",
|
|
"matchall.com/def": "11",
|
|
"end/matchend": "12",
|
|
"end1/matchend": "13",
|
|
"1end/matchend": "14",
|
|
"mid/matchmid": "15",
|
|
"mi1d/matchmid": "16",
|
|
"mid1/matchmid": "17",
|
|
"head/matchhead": "18",
|
|
"1head/matchhead": "19",
|
|
"head1/matchhead": "20",
|
|
"abc/matchall": "21",
|
|
"def/matchall": "22",
|
|
"match1/match2": "23",
|
|
"nomatch/nomatch": "24",
|
|
},
|
|
runtimePodAnnotations: []string{
|
|
"matchend.com/end*",
|
|
"matchmid.com/mi*d",
|
|
"matchhead.com/*head",
|
|
"matchall.com/*",
|
|
"end*/matchend",
|
|
"mi*d/matchmid",
|
|
"*head/matchhead",
|
|
"*/matchall",
|
|
"match*/match*",
|
|
},
|
|
passthroughAnnotations: map[string]string{
|
|
"matchend.com/end": "1",
|
|
"matchend.com/end1": "2",
|
|
"matchmid.com/mid": "4",
|
|
"matchmid.com/mi1d": "5",
|
|
"matchhead.com/head": "7",
|
|
"matchhead.com/1head": "8",
|
|
"matchall.com/abc": "10",
|
|
"matchall.com/def": "11",
|
|
"end/matchend": "12",
|
|
"end1/matchend": "13",
|
|
"mid/matchmid": "15",
|
|
"mi1d/matchmid": "16",
|
|
"head/matchhead": "18",
|
|
"1head/matchhead": "19",
|
|
"abc/matchall": "21",
|
|
"def/matchall": "22",
|
|
"match1/match2": "23",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
passthroughAnnotations := GetPassthroughAnnotations(test.podAnnotations, test.runtimePodAnnotations)
|
|
assert.Equal(t, test.passthroughAnnotations, passthroughAnnotations)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildLabels(t *testing.T) {
|
|
imageConfigLabels := map[string]string{
|
|
"a": "z",
|
|
"d": "y",
|
|
"long-label": strings.Repeat("example", 10000),
|
|
"containerd.io/restart.policy": "always",
|
|
"io.cri-containerd.image": "managed",
|
|
}
|
|
configLabels := map[string]string{
|
|
"a": "b",
|
|
"c": "d",
|
|
// reserved namespaces are only filtered for image config labels, not
|
|
// for labels from the CRI request
|
|
"containerd.io/restart.status": "stopped",
|
|
}
|
|
newLabels := BuildLabels(configLabels, imageConfigLabels, crilabels.ContainerKindSandbox)
|
|
assert.Len(t, newLabels, 5)
|
|
assert.Equal(t, "b", newLabels["a"])
|
|
assert.Equal(t, "d", newLabels["c"])
|
|
assert.Equal(t, "y", newLabels["d"])
|
|
assert.Equal(t, "stopped", newLabels["containerd.io/restart.status"])
|
|
assert.Equal(t, crilabels.ContainerKindSandbox, newLabels[crilabels.ContainerKindLabel])
|
|
assert.NotContains(t, newLabels, "long-label")
|
|
assert.NotContains(t, newLabels, "containerd.io/restart.policy")
|
|
assert.NotContains(t, newLabels, "io.cri-containerd.image")
|
|
|
|
newLabels["a"] = "e"
|
|
assert.Empty(t, configLabels[crilabels.ContainerKindLabel], "should not add new labels into original label")
|
|
assert.Equal(t, "b", configLabels["a"], "change in new labels should not affect original label")
|
|
}
|
|
|
|
func TestGenerateUserString(t *testing.T) {
|
|
type testcase struct {
|
|
// the name of the test case
|
|
name string
|
|
|
|
u string
|
|
uid, gid *runtime.Int64Value
|
|
|
|
result string
|
|
expectedError bool
|
|
}
|
|
testcases := []testcase{
|
|
{
|
|
name: "Empty",
|
|
result: "",
|
|
},
|
|
{
|
|
name: "Username Only",
|
|
u: "testuser",
|
|
result: "testuser",
|
|
},
|
|
{
|
|
name: "Username, UID",
|
|
u: "testuser",
|
|
uid: &runtime.Int64Value{Value: 1},
|
|
result: "testuser",
|
|
},
|
|
{
|
|
name: "Username, UID, GID",
|
|
u: "testuser",
|
|
uid: &runtime.Int64Value{Value: 1},
|
|
gid: &runtime.Int64Value{Value: 10},
|
|
result: "testuser:10",
|
|
},
|
|
{
|
|
name: "Username, GID",
|
|
u: "testuser",
|
|
gid: &runtime.Int64Value{Value: 10},
|
|
result: "testuser:10",
|
|
},
|
|
{
|
|
name: "UID only",
|
|
uid: &runtime.Int64Value{Value: 1},
|
|
result: "1",
|
|
},
|
|
{
|
|
name: "UID, GID",
|
|
uid: &runtime.Int64Value{Value: 1},
|
|
gid: &runtime.Int64Value{Value: 10},
|
|
result: "1:10",
|
|
},
|
|
{
|
|
name: "GID only",
|
|
gid: &runtime.Int64Value{Value: 10},
|
|
result: "",
|
|
expectedError: true,
|
|
},
|
|
}
|
|
for _, tc := range testcases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r, err := GenerateUserString(tc.u, tc.uid, tc.gid)
|
|
if tc.expectedError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
assert.Equal(t, tc.result, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsShimTTRPCClosed(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "nil error",
|
|
err: nil,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ttrpc closed error text",
|
|
err: fmt.Errorf("ttrpc: closed"),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ttrpc closed error",
|
|
err: ttrpc.ErrClosed,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "wrapped ttrpc closed error",
|
|
err: fmt.Errorf("some context: %w", ttrpc.ErrClosed),
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "non-ttrpc error",
|
|
err: fmt.Errorf("some other error"),
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.expected, IsShimTTRPCClosed(errgrpc.ToNative(tt.err)))
|
|
})
|
|
}
|
|
}
|