mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 13:27:16 +00:00
Error strings should not be capitalized (unless beginning with proper nouns or acronyms) or end with punctuation, per Go style guide and staticcheck ST1005. This change lowercases the first letter of error messages in fmt.Errorf calls throughout the codebase, while preserving uppercase for acronyms at the start of error strings (e.g., NRI, RDT, CDI, UID, GID, VHD, FUSE). Also fixes typo 'faild' -> 'failed' in resolver_test.go. Changes: - core/metadata: target.Digest, target.Size, target.MediaType - core/remotes/docker: fetcher.Seek, fetcherByDigest - integration: various test error messages - internal/cri/server: containerConfig - plugins/snapshots: erofs Fixes #12011 Signed-off-by: Aadhar Agarwal <aadagarwal@microsoft.com>
468 lines
12 KiB
Go
468 lines
12 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 metadata
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
eventstypes "github.com/containerd/containerd/api/events"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/core/metadata/boltutil"
|
|
"github.com/containerd/containerd/v2/pkg/epoch"
|
|
"github.com/containerd/containerd/v2/pkg/filters"
|
|
"github.com/containerd/containerd/v2/pkg/labels"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
"github.com/containerd/errdefs"
|
|
digest "github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
bolt "go.etcd.io/bbolt"
|
|
errbolt "go.etcd.io/bbolt/errors"
|
|
)
|
|
|
|
type imageStore struct {
|
|
db *DB
|
|
}
|
|
|
|
// NewImageStore returns a store backed by a bolt DB
|
|
func NewImageStore(db *DB) images.Store {
|
|
return &imageStore{db: db}
|
|
}
|
|
|
|
func (s *imageStore) Get(ctx context.Context, name string) (images.Image, error) {
|
|
var image images.Image
|
|
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
|
|
if err := view(ctx, s.db, func(tx *bolt.Tx) error {
|
|
bkt := getImagesBucket(tx, namespace)
|
|
if bkt == nil {
|
|
return fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound)
|
|
}
|
|
|
|
ibkt := bkt.Bucket([]byte(name))
|
|
if ibkt == nil {
|
|
return fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound)
|
|
}
|
|
|
|
image.Name = name
|
|
if err := readImage(&image, ibkt); err != nil {
|
|
return fmt.Errorf("image %q: %w", name, err)
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
|
|
return image, nil
|
|
}
|
|
|
|
func (s *imageStore) List(ctx context.Context, fs ...string) ([]images.Image, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filter, err := filters.ParseAll(fs...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", err.Error(), errdefs.ErrInvalidArgument)
|
|
}
|
|
|
|
var m []images.Image
|
|
if err := view(ctx, s.db, func(tx *bolt.Tx) error {
|
|
bkt := getImagesBucket(tx, namespace)
|
|
if bkt == nil {
|
|
return nil // empty store
|
|
}
|
|
|
|
return bkt.ForEach(func(k, v []byte) error {
|
|
var (
|
|
image = images.Image{
|
|
Name: string(k),
|
|
}
|
|
kbkt = bkt.Bucket(k)
|
|
)
|
|
|
|
if err := readImage(&image, kbkt); err != nil {
|
|
return err
|
|
}
|
|
|
|
if filter.Match(adaptImage(image)) {
|
|
m = append(m, image)
|
|
}
|
|
return nil
|
|
})
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (s *imageStore) Create(ctx context.Context, image images.Image) (images.Image, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
|
|
if err := update(ctx, s.db, func(tx *bolt.Tx) error {
|
|
if err := validateImage(&image); err != nil {
|
|
return err
|
|
}
|
|
|
|
bkt, err := createImagesBucket(tx, namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := addImageLease(ctx, tx, image.Name, image.Labels); err != nil {
|
|
return err
|
|
}
|
|
|
|
ibkt, err := bkt.CreateBucket([]byte(image.Name))
|
|
if err != nil {
|
|
if err != errbolt.ErrBucketExists {
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("image %q: %w", image.Name, errdefs.ErrAlreadyExists)
|
|
}
|
|
|
|
// The value of `image.CreatedAt` passed from the caller is discarded here.
|
|
// Ideally we should return an error when the value is already set.
|
|
// However, as `image.CreatedAt` is defined as a non-pointer `time.Time`, we can't compare it to nil.
|
|
// And we can't compare it to `time.Time{}` either, as `time.Time{}` is a proper timestamp (1970-01-01 00:00:00).
|
|
if tm := epoch.FromContext(ctx); tm != nil {
|
|
image.CreatedAt = tm.UTC()
|
|
} else {
|
|
image.CreatedAt = time.Now().UTC()
|
|
}
|
|
image.UpdatedAt = image.CreatedAt
|
|
return writeImage(ibkt, &image)
|
|
}); err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
|
|
if publisher := s.db.Publisher(ctx); publisher != nil {
|
|
if err := publisher.Publish(ctx, "/images/create", &eventstypes.ImageCreate{
|
|
Name: image.Name,
|
|
Labels: image.Labels,
|
|
}); err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
}
|
|
|
|
return image, nil
|
|
}
|
|
|
|
func (s *imageStore) Update(ctx context.Context, image images.Image, fieldpaths ...string) (images.Image, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
|
|
if image.Name == "" {
|
|
return images.Image{}, fmt.Errorf("image name is required for update: %w", errdefs.ErrInvalidArgument)
|
|
}
|
|
|
|
var updated images.Image
|
|
|
|
if err := update(ctx, s.db, func(tx *bolt.Tx) error {
|
|
bkt, err := createImagesBucket(tx, namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ibkt := bkt.Bucket([]byte(image.Name))
|
|
if ibkt == nil {
|
|
return fmt.Errorf("image %q: %w", image.Name, errdefs.ErrNotFound)
|
|
}
|
|
|
|
if err := readImage(&updated, ibkt); err != nil {
|
|
return fmt.Errorf("image %q: %w", image.Name, err)
|
|
}
|
|
createdat := updated.CreatedAt
|
|
updated.Name = image.Name
|
|
|
|
if len(fieldpaths) > 0 {
|
|
for _, path := range fieldpaths {
|
|
if strings.HasPrefix(path, "labels.") {
|
|
if updated.Labels == nil {
|
|
updated.Labels = map[string]string{}
|
|
}
|
|
|
|
key := strings.TrimPrefix(path, "labels.")
|
|
updated.Labels[key] = image.Labels[key]
|
|
continue
|
|
} else if strings.HasPrefix(path, "annotations.") {
|
|
if updated.Target.Annotations == nil {
|
|
updated.Target.Annotations = map[string]string{}
|
|
}
|
|
|
|
key := strings.TrimPrefix(path, "annotations.")
|
|
updated.Target.Annotations[key] = image.Target.Annotations[key]
|
|
continue
|
|
}
|
|
|
|
switch path {
|
|
case "labels":
|
|
updated.Labels = image.Labels
|
|
case "target":
|
|
// NOTE(stevvooe): While we allow setting individual labels, we
|
|
// only support replacing the target as a unit, since that is
|
|
// commonly pulled as a unit from other sources. It often doesn't
|
|
// make sense to modify the size or digest without touching the
|
|
// mediatype, as well, for example.
|
|
updated.Target = image.Target
|
|
case "annotations":
|
|
updated.Target.Annotations = image.Target.Annotations
|
|
default:
|
|
return fmt.Errorf("cannot update %q field on image %q: %w", path, image.Name, errdefs.ErrInvalidArgument)
|
|
}
|
|
}
|
|
} else {
|
|
updated = image
|
|
}
|
|
|
|
if err := validateImage(&updated); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Collectible label may be added, if so add to lease
|
|
if err := addImageLease(ctx, tx, updated.Name, updated.Labels); err != nil {
|
|
return err
|
|
}
|
|
|
|
updated.CreatedAt = createdat
|
|
if tm := epoch.FromContext(ctx); tm != nil {
|
|
updated.UpdatedAt = tm.UTC()
|
|
} else {
|
|
updated.UpdatedAt = time.Now().UTC()
|
|
}
|
|
return writeImage(ibkt, &updated)
|
|
}); err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
|
|
if publisher := s.db.Publisher(ctx); publisher != nil {
|
|
if err := publisher.Publish(ctx, "/images/update", &eventstypes.ImageUpdate{
|
|
Name: updated.Name,
|
|
Labels: updated.Labels,
|
|
}); err != nil {
|
|
return images.Image{}, err
|
|
}
|
|
}
|
|
|
|
return updated, nil
|
|
|
|
}
|
|
|
|
func (s *imageStore) Delete(ctx context.Context, name string, opts ...images.DeleteOpt) error {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var options images.DeleteOptions
|
|
for _, opt := range opts {
|
|
if err := opt(ctx, &options); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = update(ctx, s.db, func(tx *bolt.Tx) error {
|
|
bkt := getImagesBucket(tx, namespace)
|
|
if bkt == nil {
|
|
return fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound)
|
|
}
|
|
|
|
if err := removeImageLease(ctx, tx, name); err != nil {
|
|
return err
|
|
}
|
|
|
|
if options.Target != nil && options.Target.Digest != "" {
|
|
ibkt := bkt.Bucket([]byte(name))
|
|
if ibkt == nil {
|
|
return fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound)
|
|
}
|
|
|
|
var check images.Image
|
|
if err := readImage(&check, ibkt); err != nil {
|
|
return fmt.Errorf("image %q: %w", name, err)
|
|
}
|
|
|
|
if check.Target.Digest != options.Target.Digest {
|
|
return fmt.Errorf("image %q has target %v, not %v: %w", name, check.Target.Digest, options.Target.Digest, errdefs.ErrNotFound)
|
|
}
|
|
}
|
|
|
|
if err = bkt.DeleteBucket([]byte(name)); err != nil {
|
|
if err == errbolt.ErrBucketNotFound {
|
|
err = fmt.Errorf("image %q: %w", name, errdefs.ErrNotFound)
|
|
}
|
|
return err
|
|
}
|
|
|
|
s.db.dirty.Add(1)
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if publisher := s.db.Publisher(ctx); publisher != nil {
|
|
if err := publisher.Publish(ctx, "/images/delete", &eventstypes.ImageDelete{
|
|
Name: name,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateImage(image *images.Image) error {
|
|
if image.Name == "" {
|
|
return fmt.Errorf("image name must not be empty: %w", errdefs.ErrInvalidArgument)
|
|
}
|
|
|
|
for k, v := range image.Labels {
|
|
if err := labels.Validate(k, v); err != nil {
|
|
return fmt.Errorf("image.Labels: %w", err)
|
|
}
|
|
}
|
|
|
|
return validateTarget(&image.Target)
|
|
}
|
|
|
|
func validateTarget(target *ocispec.Descriptor) error {
|
|
// NOTE(stevvooe): Only validate fields we actually store.
|
|
|
|
if err := target.Digest.Validate(); err != nil {
|
|
return fmt.Errorf("target.Digest %q invalid: %v: %w", target.Digest, err, errdefs.ErrInvalidArgument)
|
|
}
|
|
|
|
if target.Size <= 0 {
|
|
return fmt.Errorf("target.Size must be greater than zero: %w", errdefs.ErrInvalidArgument)
|
|
}
|
|
|
|
if target.MediaType == "" {
|
|
return fmt.Errorf("target.MediaType must be set: %w", errdefs.ErrInvalidArgument)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readImage(image *images.Image, bkt *bolt.Bucket) error {
|
|
if err := boltutil.ReadTimestamps(bkt, &image.CreatedAt, &image.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
labels, err := boltutil.ReadLabels(bkt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
image.Labels = labels
|
|
|
|
image.Target.Annotations, err = boltutil.ReadAnnotations(bkt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tbkt := bkt.Bucket(bucketKeyTarget)
|
|
if tbkt == nil {
|
|
return errors.New("unable to read target bucket")
|
|
}
|
|
return tbkt.ForEach(func(k, v []byte) error {
|
|
if v == nil {
|
|
return nil // skip it? a bkt maybe?
|
|
}
|
|
|
|
// TODO(stevvooe): This is why we need to use byte values for
|
|
// keys, rather than full arrays.
|
|
switch string(k) {
|
|
case string(bucketKeyDigest):
|
|
image.Target.Digest = digest.Digest(v)
|
|
case string(bucketKeyMediaType):
|
|
image.Target.MediaType = string(v)
|
|
case string(bucketKeySize):
|
|
image.Target.Size, _ = binary.Varint(v)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func writeImage(bkt *bolt.Bucket, image *images.Image) error {
|
|
if err := boltutil.WriteTimestamps(bkt, image.CreatedAt, image.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := boltutil.WriteLabels(bkt, image.Labels); err != nil {
|
|
return fmt.Errorf("writing labels for image %v: %w", image.Name, err)
|
|
}
|
|
|
|
if err := boltutil.WriteAnnotations(bkt, image.Target.Annotations); err != nil {
|
|
return fmt.Errorf("writing Annotations for image %v: %w", image.Name, err)
|
|
}
|
|
|
|
// write the target bucket
|
|
tbkt, err := bkt.CreateBucketIfNotExists(bucketKeyTarget)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sizeEncoded, err := encodeInt(image.Target.Size)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v := range [][2][]byte{
|
|
{bucketKeyDigest, []byte(image.Target.Digest)},
|
|
{bucketKeyMediaType, []byte(image.Target.MediaType)},
|
|
{bucketKeySize, sizeEncoded},
|
|
} {
|
|
if err := tbkt.Put(v[0], v[1]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func encodeInt(i int64) ([]byte, error) {
|
|
var (
|
|
buf [binary.MaxVarintLen64]byte
|
|
iEncoded = buf[:]
|
|
)
|
|
iEncoded = iEncoded[:binary.PutVarint(iEncoded, i)]
|
|
|
|
if len(iEncoded) == 0 {
|
|
return nil, fmt.Errorf("failed encoding integer = %v", i)
|
|
}
|
|
return iEncoded, nil
|
|
}
|