| /* |
| 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" |
| "errors" |
| "fmt" |
| "strings" |
| "time" |
| |
| "github.com/containerd/containerd/v2/core/metadata/boltutil" |
| api "github.com/containerd/containerd/v2/core/sandbox" |
| "github.com/containerd/containerd/v2/pkg/filters" |
| "github.com/containerd/containerd/v2/pkg/identifiers" |
| "github.com/containerd/containerd/v2/pkg/namespaces" |
| "github.com/containerd/containerd/v2/pkg/tracing" |
| "github.com/containerd/errdefs" |
| "github.com/containerd/typeurl/v2" |
| "go.etcd.io/bbolt" |
| errbolt "go.etcd.io/bbolt/errors" |
| ) |
| |
| const ( |
| spanSandboxPrefix = "metadata.sandbox" |
| ) |
| |
| type sandboxStore struct { |
| db *DB |
| } |
| |
| var _ api.Store = (*sandboxStore)(nil) |
| |
| // NewSandboxStore creates a datababase client for sandboxes |
| func NewSandboxStore(db *DB) api.Store { |
| return &sandboxStore{db: db} |
| } |
| |
| // Create a sandbox record in the store |
| func (s *sandboxStore) Create(ctx context.Context, sandbox api.Sandbox) (api.Sandbox, error) { |
| ctx, span := tracing.StartSpan(ctx, |
| tracing.Name(spanSandboxPrefix, "Create"), |
| tracing.WithAttribute("sandbox.id", sandbox.ID), |
| ) |
| defer span.End() |
| ns, err := namespaces.NamespaceRequired(ctx) |
| if err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| sandbox.CreatedAt = time.Now().UTC() |
| sandbox.UpdatedAt = sandbox.CreatedAt |
| |
| if err := s.validate(&sandbox); err != nil { |
| return api.Sandbox{}, fmt.Errorf("failed to validate sandbox: %w", err) |
| } |
| |
| if err := update(ctx, s.db, func(tx *bbolt.Tx) error { |
| parent, err := createSandboxBucket(tx, ns) |
| if err != nil { |
| return fmt.Errorf("create error: %w", err) |
| } |
| |
| if err := s.write(parent, &sandbox, false); err != nil { |
| return fmt.Errorf("write error: %w", err) |
| } |
| |
| span.SetAttributes( |
| tracing.Attribute("sandbox.CreatedAt", sandbox.CreatedAt.Format(time.RFC3339)), |
| ) |
| return nil |
| }); err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| return sandbox, nil |
| } |
| |
| // Update the sandbox with the provided sandbox object and fields |
| func (s *sandboxStore) Update(ctx context.Context, sandbox api.Sandbox, fieldpaths ...string) (api.Sandbox, error) { |
| ctx, span := tracing.StartSpan(ctx, |
| tracing.Name(spanSandboxPrefix, "Update"), |
| tracing.WithAttribute("sandbox.id", sandbox.ID), |
| ) |
| defer span.End() |
| ns, err := namespaces.NamespaceRequired(ctx) |
| if err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| ret := api.Sandbox{} |
| if err := update(ctx, s.db, func(tx *bbolt.Tx) error { |
| parent := getSandboxBucket(tx, ns) |
| if parent == nil { |
| return fmt.Errorf("no sandbox buckets: %w", errdefs.ErrNotFound) |
| } |
| |
| updated, err := s.read(parent, []byte(sandbox.ID)) |
| if err != nil { |
| return err |
| } |
| |
| if len(fieldpaths) == 0 { |
| fieldpaths = []string{"labels", "extensions", "spec", "runtime"} |
| |
| if updated.Runtime.Name != sandbox.Runtime.Name { |
| return fmt.Errorf("sandbox.Runtime.Name field is immutable: %w", errdefs.ErrInvalidArgument) |
| } |
| } |
| |
| 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] = sandbox.Labels[key] |
| continue |
| } else if strings.HasPrefix(path, "extensions.") { |
| if updated.Extensions == nil { |
| updated.Extensions = map[string]typeurl.Any{} |
| } |
| |
| key := strings.TrimPrefix(path, "extensions.") |
| updated.Extensions[key] = sandbox.Extensions[key] |
| continue |
| } |
| |
| switch path { |
| case "labels": |
| updated.Labels = sandbox.Labels |
| case "extensions": |
| updated.Extensions = sandbox.Extensions |
| case "runtime": |
| updated.Runtime = sandbox.Runtime |
| case "spec": |
| updated.Spec = sandbox.Spec |
| default: |
| return fmt.Errorf("cannot update %q field on sandbox %q: %w", path, sandbox.ID, errdefs.ErrInvalidArgument) |
| } |
| } |
| |
| updated.UpdatedAt = time.Now().UTC() |
| |
| if err := s.write(parent, &updated, true); err != nil { |
| return err |
| } |
| |
| span.SetAttributes( |
| tracing.Attribute("sandbox.CreatedAt", updated.CreatedAt.Format(time.RFC3339)), |
| tracing.Attribute("sandbox.UpdatedAt", updated.UpdatedAt.Format(time.RFC3339)), |
| ) |
| ret = updated |
| return nil |
| }); err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| return ret, nil |
| } |
| |
| // Get sandbox metadata using the id |
| func (s *sandboxStore) Get(ctx context.Context, id string) (api.Sandbox, error) { |
| ns, err := namespaces.NamespaceRequired(ctx) |
| if err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| ret := api.Sandbox{} |
| if err := view(ctx, s.db, func(tx *bbolt.Tx) error { |
| bucket := getSandboxBucket(tx, ns) |
| if bucket == nil { |
| return fmt.Errorf("no sandbox buckets: %w", errdefs.ErrNotFound) |
| } |
| |
| out, err := s.read(bucket, []byte(id)) |
| if err != nil { |
| return err |
| } |
| |
| ret = out |
| return nil |
| }); err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| return ret, nil |
| } |
| |
| // List returns sandboxes that match one or more of the provided filters |
| func (s *sandboxStore) List(ctx context.Context, fields ...string) ([]api.Sandbox, error) { |
| ns, err := namespaces.NamespaceRequired(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| filter, err := filters.ParseAll(fields...) |
| if err != nil { |
| return nil, fmt.Errorf("%s: %w", err.Error(), errdefs.ErrInvalidArgument) |
| } |
| |
| var ( |
| list []api.Sandbox |
| ) |
| |
| if err := view(ctx, s.db, func(tx *bbolt.Tx) error { |
| bucket := getSandboxBucket(tx, ns) |
| if bucket == nil { |
| // We haven't created any sandboxes yet, just return empty list |
| return nil |
| } |
| |
| if err := bucket.ForEach(func(k, v []byte) error { |
| info, err := s.read(bucket, k) |
| if err != nil { |
| return fmt.Errorf("failed to read bucket %q: %w", string(k), err) |
| } |
| |
| if filter.Match(adaptSandbox(&info)) { |
| list = append(list, info) |
| } |
| |
| return nil |
| }); err != nil { |
| return err |
| } |
| |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| |
| return list, nil |
| } |
| |
| // Delete a sandbox from metadata store using the id |
| func (s *sandboxStore) Delete(ctx context.Context, id string) error { |
| ctx, span := tracing.StartSpan(ctx, |
| tracing.Name(spanSandboxPrefix, "Delete"), |
| tracing.WithAttribute("sandbox.id", id), |
| ) |
| defer span.End() |
| ns, err := namespaces.NamespaceRequired(ctx) |
| if err != nil { |
| return err |
| } |
| |
| if err := update(ctx, s.db, func(tx *bbolt.Tx) error { |
| buckets := getSandboxBucket(tx, ns) |
| if buckets == nil { |
| return fmt.Errorf("no sandbox buckets: %w", errdefs.ErrNotFound) |
| } |
| |
| if err := buckets.DeleteBucket([]byte(id)); err != nil { |
| if err == errbolt.ErrBucketNotFound { |
| err = errdefs.ErrNotFound |
| } |
| return fmt.Errorf("failed to delete sandbox %q: %w", id, err) |
| } |
| |
| return nil |
| }); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (s *sandboxStore) write(parent *bbolt.Bucket, instance *api.Sandbox, overwrite bool) error { |
| if err := s.validate(instance); err != nil { |
| return err |
| } |
| |
| var ( |
| bucket *bbolt.Bucket |
| err error |
| id = []byte(instance.ID) |
| ) |
| |
| if overwrite { |
| bucket, err = parent.CreateBucketIfNotExists(id) |
| if err != nil { |
| return err |
| } |
| } else { |
| bucket, err = parent.CreateBucket(id) |
| if err != nil { |
| if err == errbolt.ErrBucketExists { |
| return fmt.Errorf("sandbox bucket %q already exists: %w", instance.ID, errdefs.ErrAlreadyExists) |
| } |
| return err |
| } |
| } |
| |
| if err := boltutil.WriteTimestamps(bucket, instance.CreatedAt, instance.UpdatedAt); err != nil { |
| return err |
| } |
| |
| if err := boltutil.WriteLabels(bucket, instance.Labels); err != nil { |
| return err |
| } |
| |
| if err := boltutil.WriteExtensions(bucket, instance.Extensions); err != nil { |
| return err |
| } |
| |
| if err := boltutil.WriteAny(bucket, bucketKeySpec, instance.Spec); err != nil { |
| return err |
| } |
| |
| if err := bucket.Put(bucketKeySandboxer, []byte(instance.Sandboxer)); err != nil { |
| return err |
| } |
| |
| runtimeBucket, err := bucket.CreateBucketIfNotExists(bucketKeyRuntime) |
| if err != nil { |
| return err |
| } |
| |
| if err := runtimeBucket.Put(bucketKeyName, []byte(instance.Runtime.Name)); err != nil { |
| return err |
| } |
| |
| if err := boltutil.WriteAny(runtimeBucket, bucketKeyOptions, instance.Runtime.Options); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (s *sandboxStore) read(parent *bbolt.Bucket, id []byte) (api.Sandbox, error) { |
| var ( |
| inst api.Sandbox |
| err error |
| ) |
| |
| bucket := parent.Bucket(id) |
| if bucket == nil { |
| return api.Sandbox{}, fmt.Errorf("bucket %q not found: %w", id, errdefs.ErrNotFound) |
| } |
| |
| inst.ID = string(id) |
| |
| inst.Labels, err = boltutil.ReadLabels(bucket) |
| if err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| if err := boltutil.ReadTimestamps(bucket, &inst.CreatedAt, &inst.UpdatedAt); err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| inst.Spec, err = boltutil.ReadAny(bucket, bucketKeySpec) |
| if err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| runtimeBucket := bucket.Bucket(bucketKeyRuntime) |
| if runtimeBucket == nil { |
| return api.Sandbox{}, errors.New("no runtime bucket") |
| } |
| |
| inst.Runtime.Name = string(runtimeBucket.Get(bucketKeyName)) |
| inst.Runtime.Options, err = boltutil.ReadAny(runtimeBucket, bucketKeyOptions) |
| if err != nil { |
| return api.Sandbox{}, err |
| } |
| |
| inst.Extensions, err = boltutil.ReadExtensions(bucket) |
| if err != nil { |
| return api.Sandbox{}, err |
| } |
| sandboxer := bucket.Get(bucketKeySandboxer) |
| if sandboxer == nil { |
| inst.Sandboxer = "" |
| } else { |
| inst.Sandboxer = string(sandboxer) |
| } |
| |
| return inst, nil |
| } |
| |
| func (s *sandboxStore) validate(new *api.Sandbox) error { |
| if err := identifiers.Validate(new.ID); err != nil { |
| return fmt.Errorf("invalid sandbox ID: %w", err) |
| } |
| |
| if new.CreatedAt.IsZero() { |
| return fmt.Errorf("creation date must not be zero: %w", errdefs.ErrInvalidArgument) |
| } |
| |
| if new.UpdatedAt.IsZero() { |
| return fmt.Errorf("updated date must not be zero: %w", errdefs.ErrInvalidArgument) |
| } |
| |
| return nil |
| } |