mirror of
https://github.com/containerd/containerd.git
synced 2026-10-28 09:35:10 +08: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>
347 lines
11 KiB
Go
347 lines
11 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 client
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
|
|
"github.com/containerd/containerd/v2/core/containers"
|
|
"github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/core/snapshots"
|
|
"github.com/containerd/containerd/v2/pkg/labels"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
"github.com/containerd/containerd/v2/pkg/oci"
|
|
"github.com/containerd/errdefs"
|
|
"github.com/containerd/log"
|
|
"github.com/containerd/typeurl/v2"
|
|
"github.com/opencontainers/image-spec/identity"
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
// DeleteOpts allows the caller to set options for the deletion of a container
|
|
type DeleteOpts func(ctx context.Context, client *Client, c containers.Container) error
|
|
|
|
// NewContainerOpts allows the caller to set additional options when creating a container
|
|
type NewContainerOpts func(ctx context.Context, client *Client, c *containers.Container) error
|
|
|
|
// UpdateContainerOpts allows the caller to set additional options when updating a container
|
|
type UpdateContainerOpts func(ctx context.Context, client *Client, c *containers.Container) error
|
|
|
|
// InfoOpts controls how container metadata is fetched and returned
|
|
type InfoOpts func(*InfoConfig)
|
|
|
|
// InfoConfig specifies how container metadata is fetched
|
|
type InfoConfig struct {
|
|
// Refresh will to a fetch of the latest container metadata
|
|
Refresh bool
|
|
}
|
|
|
|
// WithRuntime allows a user to specify the runtime name and additional options that should
|
|
// be used to create tasks for the container
|
|
func WithRuntime(name string, options any) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
var (
|
|
opts typeurl.Any
|
|
err error
|
|
)
|
|
if options != nil {
|
|
opts, err = typeurl.MarshalAny(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c.Runtime = containers.RuntimeInfo{
|
|
Name: name,
|
|
Options: opts,
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithSandbox joins the container to a container group (aka sandbox) from the given ID
|
|
// Note: shim runtime must support sandboxes environments.
|
|
func WithSandbox(sandboxID string) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
c.SandboxID = sandboxID
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithImage sets the provided image as the base for the container
|
|
func WithImage(i Image) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
c.Image = i.Name()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithImageName allows setting the image name as the base for the container
|
|
func WithImageName(n string) NewContainerOpts {
|
|
return func(ctx context.Context, _ *Client, c *containers.Container) error {
|
|
c.Image = n
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithContainerLabels sets the provided labels to the container.
|
|
// The existing labels are cleared.
|
|
// Use WithAdditionalContainerLabels to preserve the existing labels.
|
|
func WithContainerLabels(labels map[string]string) NewContainerOpts {
|
|
return func(_ context.Context, _ *Client, c *containers.Container) error {
|
|
c.Labels = labels
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithImageConfigLabels sets the image config labels on the container.
|
|
// The existing labels are cleared as this is expected to be the first
|
|
// operation in setting up a container's labels. Use WithAdditionalContainerLabels
|
|
// to add/overwrite the existing image config labels.
|
|
//
|
|
// Image config labels in the namespaces reserved for containerd
|
|
// (containerd.io/) and the CRI plugin (io.cri-containerd) are not copied
|
|
// to the container.
|
|
func WithImageConfigLabels(image Image) NewContainerOpts {
|
|
return func(ctx context.Context, _ *Client, c *containers.Container) error {
|
|
ic, err := image.Config(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !images.IsConfigType(ic.MediaType) {
|
|
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
|
}
|
|
|
|
var (
|
|
ociimage v1.Image
|
|
config v1.ImageConfig
|
|
)
|
|
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = json.Unmarshal(p, &ociimage); err != nil {
|
|
return err
|
|
}
|
|
config = ociimage.Config
|
|
|
|
c.Labels = config.Labels
|
|
// 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, so they are not copied from untrusted image configs.
|
|
maps.DeleteFunc(c.Labels, func(k, _ string) bool {
|
|
if labels.IsReserved(k) {
|
|
log.G(ctx).Warnf("skipping image label %q: the label namespace is reserved for containerd; possible malicious image attempting to alter containerd behavior", k)
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithAdditionalContainerLabels adds the provided labels to the container
|
|
// The existing labels are preserved as long as they do not conflict with the added labels.
|
|
func WithAdditionalContainerLabels(labels map[string]string) NewContainerOpts {
|
|
return func(_ context.Context, _ *Client, c *containers.Container) error {
|
|
if c.Labels == nil {
|
|
c.Labels = labels
|
|
return nil
|
|
}
|
|
maps.Copy(c.Labels, labels)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithImageStopSignal sets a well-known containerd label (StopSignalLabel)
|
|
// on the container for storing the stop signal specified in the OCI image
|
|
// config
|
|
func WithImageStopSignal(image Image, defaultSignal string) NewContainerOpts {
|
|
return func(ctx context.Context, _ *Client, c *containers.Container) error {
|
|
if c.Labels == nil {
|
|
c.Labels = make(map[string]string)
|
|
}
|
|
stopSignal, err := GetOCIStopSignal(ctx, image, defaultSignal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Labels[StopSignalLabel] = stopSignal
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithSnapshotter sets the provided snapshotter for use by the container
|
|
//
|
|
// This option must appear before other snapshotter options to have an effect.
|
|
func WithSnapshotter(name string) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
c.Snapshotter = name
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithSnapshot uses an existing root filesystem for the container
|
|
func WithSnapshot(id string) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
// check that the snapshot exists, if not, fail on creation
|
|
var err error
|
|
c.Snapshotter, err = client.resolveSnapshotterName(ctx, c.Snapshotter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s, err := client.getSnapshotter(ctx, c.Snapshotter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.Mounts(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
c.SnapshotKey = id
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithSnapshotCleanup deletes the rootfs snapshot allocated for the container
|
|
func WithSnapshotCleanup(ctx context.Context, client *Client, c containers.Container) error {
|
|
if c.SnapshotKey != "" {
|
|
if c.Snapshotter == "" {
|
|
return fmt.Errorf("container.Snapshotter must be set to cleanup rootfs snapshot: %w", errdefs.ErrInvalidArgument)
|
|
}
|
|
s, err := client.getSnapshotter(ctx, c.Snapshotter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.Remove(ctx, c.SnapshotKey); err != nil && !errdefs.IsNotFound(err) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WithNewSnapshot allocates a new snapshot to be used by the container as the
|
|
// root filesystem in read-write mode
|
|
func WithNewSnapshot(id string, i Image, opts ...snapshots.Opt) NewContainerOpts {
|
|
return withNewSnapshot(id, i, false, opts...)
|
|
}
|
|
|
|
// WithNewSnapshotView allocates a new snapshot to be used by the container as the
|
|
// root filesystem in read-only mode
|
|
func WithNewSnapshotView(id string, i Image, opts ...snapshots.Opt) NewContainerOpts {
|
|
return withNewSnapshot(id, i, true, opts...)
|
|
}
|
|
|
|
func withNewSnapshot(id string, i Image, readonly bool, opts ...snapshots.Opt) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
diffIDs, err := i.RootFS(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parent := identity.ChainID(diffIDs).String()
|
|
c.Snapshotter, err = client.resolveSnapshotterName(ctx, c.Snapshotter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s, err := client.getSnapshotter(ctx, c.Snapshotter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parent, err = resolveSnapshotOptions(ctx, client, c.Snapshotter, s, parent, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if readonly {
|
|
_, err = s.View(ctx, id, parent, opts...)
|
|
} else {
|
|
_, err = s.Prepare(ctx, id, parent, opts...)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.SnapshotKey = id
|
|
c.Image = i.Name()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithContainerExtension appends extension data to the container object.
|
|
// Use this to decorate the container object with additional data for the client
|
|
// integration.
|
|
//
|
|
// Make sure to register the type of `extension` in the typeurl package via
|
|
// `typeurl.Register` or container creation may fail.
|
|
func WithContainerExtension(name string, extension any) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
if name == "" {
|
|
return fmt.Errorf("extension key must not be zero-length: %w", errdefs.ErrInvalidArgument)
|
|
}
|
|
|
|
ext, err := typeurl.MarshalAny(extension)
|
|
if err != nil {
|
|
if errors.Is(err, typeurl.ErrNotFound) {
|
|
return fmt.Errorf("extension %q is not registered with the typeurl package, see `typeurl.Register`: %w", name, err)
|
|
}
|
|
return fmt.Errorf("error marshalling extension: %w", err)
|
|
}
|
|
|
|
if c.Extensions == nil {
|
|
c.Extensions = make(map[string]typeurl.Any)
|
|
}
|
|
c.Extensions[name] = ext
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithNewSpec generates a new spec for a new container
|
|
func WithNewSpec(opts ...oci.SpecOpts) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
if _, ok := namespaces.Namespace(ctx); !ok {
|
|
ctx = namespaces.WithNamespace(ctx, client.DefaultNamespace())
|
|
}
|
|
s, err := oci.GenerateSpec(ctx, client, c, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Spec, err = typeurl.MarshalAny(s)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// WithSpec sets the provided spec on the container
|
|
func WithSpec(s *oci.Spec, opts ...oci.SpecOpts) NewContainerOpts {
|
|
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
|
if err := oci.ApplyOpts(ctx, client, c, s, opts...); err != nil {
|
|
return err
|
|
}
|
|
|
|
var err error
|
|
c.Spec, err = typeurl.MarshalAnyToProto(s)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// WithoutRefreshedMetadata will use the current metadata attached to the container object
|
|
func WithoutRefreshedMetadata(i *InfoConfig) {
|
|
i.Refresh = false
|
|
}
|