Move services to plugins/services

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2024-01-17 09:52:57 -08:00
parent ce41d1c90a
commit 92d2a5fc02
50 changed files with 70 additions and 70 deletions

View File

@@ -0,0 +1,85 @@
/*
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 containers
import (
api "github.com/containerd/containerd/v2/api/services/containers/v1"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/protobuf"
"github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/typeurl/v2"
)
func containersToProto(containers []containers.Container) []*api.Container {
var containerspb []*api.Container
for _, image := range containers {
image := image
containerspb = append(containerspb, containerToProto(&image))
}
return containerspb
}
func containerToProto(container *containers.Container) *api.Container {
extensions := make(map[string]*types.Any)
for k, v := range container.Extensions {
extensions[k] = protobuf.FromAny(v)
}
return &api.Container{
ID: container.ID,
Labels: container.Labels,
Image: container.Image,
Runtime: &api.Container_Runtime{
Name: container.Runtime.Name,
Options: protobuf.FromAny(container.Runtime.Options),
},
Spec: protobuf.FromAny(container.Spec),
Snapshotter: container.Snapshotter,
SnapshotKey: container.SnapshotKey,
CreatedAt: protobuf.ToTimestamp(container.CreatedAt),
UpdatedAt: protobuf.ToTimestamp(container.UpdatedAt),
Extensions: extensions,
Sandbox: container.SandboxID,
}
}
func containerFromProto(containerpb *api.Container) containers.Container {
var runtime containers.RuntimeInfo
if containerpb.Runtime != nil {
runtime = containers.RuntimeInfo{
Name: containerpb.Runtime.Name,
Options: containerpb.Runtime.Options,
}
}
extensions := make(map[string]typeurl.Any)
for k, v := range containerpb.Extensions {
v := v
extensions[k] = v
}
return containers.Container{
ID: containerpb.ID,
Labels: containerpb.Labels,
Image: containerpb.Image,
Runtime: runtime,
Spec: containerpb.Spec,
Snapshotter: containerpb.Snapshotter,
SnapshotKey: containerpb.SnapshotKey,
Extensions: extensions,
SandboxID: containerpb.Sandbox,
}
}

View File

@@ -0,0 +1,256 @@
/*
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 containers
import (
"context"
"io"
eventstypes "github.com/containerd/containerd/v2/api/events"
api "github.com/containerd/containerd/v2/api/services/containers/v1"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
bolt "go.etcd.io/bbolt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
grpcm "google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.ContainersService,
Requires: []plugin.Type{
plugins.EventPlugin,
plugins.MetadataPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
m, err := ic.GetSingle(plugins.MetadataPlugin)
if err != nil {
return nil, err
}
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
db := m.(*metadata.DB)
return &local{
Store: metadata.NewContainerStore(db),
db: db,
publisher: ep.(events.Publisher),
}, nil
},
})
}
type local struct {
containers.Store
db *metadata.DB
publisher events.Publisher
}
var _ api.ContainersClient = &local{}
func (l *local) Get(ctx context.Context, req *api.GetContainerRequest, _ ...grpc.CallOption) (*api.GetContainerResponse, error) {
var resp api.GetContainerResponse
return &resp, errdefs.ToGRPC(l.withStoreView(ctx, func(ctx context.Context) error {
container, err := l.Store.Get(ctx, req.ID)
if err != nil {
return err
}
containerpb := containerToProto(&container)
resp.Container = containerpb
return nil
}))
}
func (l *local) List(ctx context.Context, req *api.ListContainersRequest, _ ...grpc.CallOption) (*api.ListContainersResponse, error) {
var resp api.ListContainersResponse
return &resp, errdefs.ToGRPC(l.withStoreView(ctx, func(ctx context.Context) error {
containers, err := l.Store.List(ctx, req.Filters...)
if err != nil {
return err
}
resp.Containers = containersToProto(containers)
return nil
}))
}
func (l *local) ListStream(ctx context.Context, req *api.ListContainersRequest, _ ...grpc.CallOption) (api.Containers_ListStreamClient, error) {
stream := &localStream{
ctx: ctx,
}
return stream, errdefs.ToGRPC(l.withStoreView(ctx, func(ctx context.Context) error {
containers, err := l.Store.List(ctx, req.Filters...)
if err != nil {
return err
}
stream.containers = containersToProto(containers)
return nil
}))
}
func (l *local) Create(ctx context.Context, req *api.CreateContainerRequest, _ ...grpc.CallOption) (*api.CreateContainerResponse, error) {
var resp api.CreateContainerResponse
if err := l.withStoreUpdate(ctx, func(ctx context.Context) error {
container := containerFromProto(req.Container)
created, err := l.Store.Create(ctx, container)
if err != nil {
return err
}
resp.Container = containerToProto(&created)
return nil
}); err != nil {
return &resp, errdefs.ToGRPC(err)
}
if err := l.publisher.Publish(ctx, "/containers/create", &eventstypes.ContainerCreate{
ID: resp.Container.ID,
Image: resp.Container.Image,
Runtime: &eventstypes.ContainerCreate_Runtime{
Name: resp.Container.Runtime.Name,
Options: resp.Container.Runtime.Options,
},
}); err != nil {
return &resp, err
}
return &resp, nil
}
func (l *local) Update(ctx context.Context, req *api.UpdateContainerRequest, _ ...grpc.CallOption) (*api.UpdateContainerResponse, error) {
if req.Container.ID == "" {
return nil, status.Errorf(codes.InvalidArgument, "Container.ID required")
}
var (
resp api.UpdateContainerResponse
container = containerFromProto(req.Container)
)
if err := l.withStoreUpdate(ctx, func(ctx context.Context) error {
var fieldpaths []string
if req.UpdateMask != nil && len(req.UpdateMask.Paths) > 0 {
fieldpaths = append(fieldpaths, req.UpdateMask.Paths...)
}
updated, err := l.Store.Update(ctx, container, fieldpaths...)
if err != nil {
return err
}
resp.Container = containerToProto(&updated)
return nil
}); err != nil {
return &resp, errdefs.ToGRPC(err)
}
if err := l.publisher.Publish(ctx, "/containers/update", &eventstypes.ContainerUpdate{
ID: resp.Container.ID,
Image: resp.Container.Image,
Labels: resp.Container.Labels,
SnapshotKey: resp.Container.SnapshotKey,
}); err != nil {
return &resp, err
}
return &resp, nil
}
func (l *local) Delete(ctx context.Context, req *api.DeleteContainerRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
if err := l.withStoreUpdate(ctx, func(ctx context.Context) error {
return l.Store.Delete(ctx, req.ID)
}); err != nil {
return &ptypes.Empty{}, errdefs.ToGRPC(err)
}
if err := l.publisher.Publish(ctx, "/containers/delete", &eventstypes.ContainerDelete{
ID: req.ID,
}); err != nil {
return &ptypes.Empty{}, err
}
return &ptypes.Empty{}, nil
}
func (l *local) withStore(ctx context.Context, fn func(ctx context.Context) error) func(tx *bolt.Tx) error {
return func(tx *bolt.Tx) error {
return fn(metadata.WithTransactionContext(ctx, tx))
}
}
func (l *local) withStoreView(ctx context.Context, fn func(ctx context.Context) error) error {
return l.db.View(l.withStore(ctx, fn))
}
func (l *local) withStoreUpdate(ctx context.Context, fn func(ctx context.Context) error) error {
return l.db.Update(l.withStore(ctx, fn))
}
type localStream struct {
ctx context.Context
containers []*api.Container
i int
}
func (s *localStream) Recv() (*api.ListContainerMessage, error) {
if s.i >= len(s.containers) {
return nil, io.EOF
}
c := s.containers[s.i]
s.i++
return &api.ListContainerMessage{
Container: c,
}, nil
}
func (s *localStream) Context() context.Context {
return s.ctx
}
func (s *localStream) CloseSend() error {
return nil
}
func (s *localStream) Header() (grpcm.MD, error) {
return nil, nil
}
func (s *localStream) Trailer() grpcm.MD {
return nil
}
func (s *localStream) SendMsg(m interface{}) error {
return nil
}
func (s *localStream) RecvMsg(m interface{}) error {
return nil
}

View File

@@ -0,0 +1,103 @@
/*
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 containers
import (
"context"
"io"
api "github.com/containerd/containerd/v2/api/services/containers/v1"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "containers",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.ContainersService)
if err != nil {
return nil, err
}
return &service{local: i.(api.ContainersClient)}, nil
},
})
}
type service struct {
local api.ContainersClient
api.UnimplementedContainersServer
}
var _ api.ContainersServer = &service{}
func (s *service) Register(server *grpc.Server) error {
api.RegisterContainersServer(server, s)
return nil
}
func (s *service) Get(ctx context.Context, req *api.GetContainerRequest) (*api.GetContainerResponse, error) {
return s.local.Get(ctx, req)
}
func (s *service) List(ctx context.Context, req *api.ListContainersRequest) (*api.ListContainersResponse, error) {
return s.local.List(ctx, req)
}
func (s *service) ListStream(req *api.ListContainersRequest, stream api.Containers_ListStreamServer) error {
containers, err := s.local.ListStream(stream.Context(), req)
if err != nil {
return err
}
for {
select {
case <-stream.Context().Done():
return nil
default:
c, err := containers.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if err := stream.Send(c); err != nil {
return err
}
}
}
}
func (s *service) Create(ctx context.Context, req *api.CreateContainerRequest) (*api.CreateContainerResponse, error) {
return s.local.Create(ctx, req)
}
func (s *service) Update(ctx context.Context, req *api.UpdateContainerRequest) (*api.UpdateContainerResponse, error) {
return s.local.Update(ctx, req)
}
func (s *service) Delete(ctx context.Context, req *api.DeleteContainerRequest) (*ptypes.Empty, error) {
return s.local.Delete(ctx, req)
}

View File

@@ -0,0 +1,471 @@
/*
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 contentserver
import (
"context"
"fmt"
"io"
"sync"
api "github.com/containerd/containerd/v2/api/services/content/v1"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/protobuf"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/log"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type service struct {
store content.Store
api.UnimplementedContentServer
}
var (
empty = &ptypes.Empty{}
bufPool = sync.Pool{
New: func() interface{} {
buffer := make([]byte, 1<<20)
return &buffer
},
}
)
// New returns the content GRPC server
func New(cs content.Store) api.ContentServer {
return &service{store: cs}
}
func (s *service) Register(server *grpc.Server) error {
api.RegisterContentServer(server, s)
return nil
}
func (s *service) Info(ctx context.Context, req *api.InfoRequest) (*api.InfoResponse, error) {
dg, err := digest.Parse(req.Digest)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%q failed validation", req.Digest)
}
bi, err := s.store.Info(ctx, dg)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.InfoResponse{
Info: infoToGRPC(bi),
}, nil
}
func (s *service) Update(ctx context.Context, req *api.UpdateRequest) (*api.UpdateResponse, error) {
_, err := digest.Parse(req.Info.Digest)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%q failed validation", req.Info.Digest)
}
info, err := s.store.Update(ctx, infoFromGRPC(req.Info), req.UpdateMask.GetPaths()...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.UpdateResponse{
Info: infoToGRPC(info),
}, nil
}
func (s *service) List(req *api.ListContentRequest, session api.Content_ListServer) error {
var (
buffer []*api.Info
sendBlock = func(block []*api.Info) error {
// send last block
return session.Send(&api.ListContentResponse{
Info: block,
})
}
)
if err := s.store.Walk(session.Context(), func(info content.Info) error {
buffer = append(buffer, infoToGRPC(info))
if len(buffer) >= 100 {
if err := sendBlock(buffer); err != nil {
return err
}
buffer = buffer[:0]
}
return nil
}, req.Filters...); err != nil {
return errdefs.ToGRPC(err)
}
if len(buffer) > 0 {
// send last block
if err := sendBlock(buffer); err != nil {
return err
}
}
return nil
}
func (s *service) Delete(ctx context.Context, req *api.DeleteContentRequest) (*ptypes.Empty, error) {
log.G(ctx).WithField("digest", req.Digest).Debugf("delete content")
dg, err := digest.Parse(req.Digest)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
if err := s.store.Delete(ctx, dg); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (s *service) Read(req *api.ReadContentRequest, session api.Content_ReadServer) error {
dg, err := digest.Parse(req.Digest)
if err != nil {
return status.Errorf(codes.InvalidArgument, "%v: %v", req.Digest, err)
}
oi, err := s.store.Info(session.Context(), dg)
if err != nil {
return errdefs.ToGRPC(err)
}
ra, err := s.store.ReaderAt(session.Context(), ocispec.Descriptor{Digest: dg})
if err != nil {
return errdefs.ToGRPC(err)
}
defer ra.Close()
var (
offset = req.Offset
// size is read size, not the expected size of the blob (oi.Size), which the caller might not be aware of.
// offset+size can be larger than oi.Size.
size = req.Size
// TODO(stevvooe): Using the global buffer pool. At 32KB, it is probably
// little inefficient for work over a fast network. We can tune this later.
p = bufPool.Get().(*[]byte)
)
defer bufPool.Put(p)
if offset < 0 {
offset = 0
}
if offset > oi.Size {
return status.Errorf(codes.OutOfRange, "read past object length %v bytes", oi.Size)
}
if size <= 0 || offset+size > oi.Size {
size = oi.Size - offset
}
_, err = io.CopyBuffer(
&readResponseWriter{session: session},
io.NewSectionReader(ra, offset, size), *p)
return errdefs.ToGRPC(err)
}
// readResponseWriter is a writer that places the output into ReadContentRequest messages.
//
// This allows io.CopyBuffer to do the heavy lifting of chunking the responses
// into the buffer size.
type readResponseWriter struct {
offset int64
session api.Content_ReadServer
}
func (rw *readResponseWriter) Write(p []byte) (n int, err error) {
if err := rw.session.Send(&api.ReadContentResponse{
Offset: rw.offset,
Data: p,
}); err != nil {
return 0, err
}
rw.offset += int64(len(p))
return len(p), nil
}
func (s *service) Status(ctx context.Context, req *api.StatusRequest) (*api.StatusResponse, error) {
status, err := s.store.Status(ctx, req.Ref)
if err != nil {
return nil, errdefs.ToGRPCf(err, "could not get status for ref %q", req.Ref)
}
var resp api.StatusResponse
resp.Status = &api.Status{
StartedAt: protobuf.ToTimestamp(status.StartedAt),
UpdatedAt: protobuf.ToTimestamp(status.UpdatedAt),
Ref: status.Ref,
Offset: status.Offset,
Total: status.Total,
Expected: status.Expected.String(),
}
return &resp, nil
}
func (s *service) ListStatuses(ctx context.Context, req *api.ListStatusesRequest) (*api.ListStatusesResponse, error) {
statuses, err := s.store.ListStatuses(ctx, req.Filters...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
var resp api.ListStatusesResponse
for _, status := range statuses {
resp.Statuses = append(resp.Statuses, &api.Status{
StartedAt: protobuf.ToTimestamp(status.StartedAt),
UpdatedAt: protobuf.ToTimestamp(status.UpdatedAt),
Ref: status.Ref,
Offset: status.Offset,
Total: status.Total,
Expected: status.Expected.String(),
})
}
return &resp, nil
}
func (s *service) Write(session api.Content_WriteServer) (err error) {
var (
ctx = session.Context()
msg api.WriteContentResponse
req *api.WriteContentRequest
ref string
total int64
expected digest.Digest
)
defer func(msg *api.WriteContentResponse) {
// pump through the last message if no error was encountered
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() != codes.AlreadyExists {
// TODO(stevvooe): Really need a log line here to track which
// errors are actually causing failure on the server side. May want
// to configure the service with an interceptor to make this work
// identically across all GRPC methods.
//
// This is pretty noisy, so we can remove it but leave it for now.
log.G(ctx).WithError(err).Error("(*service).Write failed")
}
return
}
err = session.Send(msg)
}(&msg)
// handle the very first request!
req, err = session.Recv()
if err != nil {
return err
}
ref = req.Ref
if ref == "" {
return status.Errorf(codes.InvalidArgument, "first message must have a reference")
}
fields := log.Fields{
"ref": ref,
}
total = req.Total
expected = digest.Digest(req.Expected)
if total > 0 {
fields["total"] = total
}
if expected != "" {
fields["expected"] = expected
}
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(fields))
log.G(ctx).Debug("(*service).Write started")
// this action locks the writer for the session.
wr, err := s.store.Writer(ctx,
content.WithRef(ref),
content.WithDescriptor(ocispec.Descriptor{Size: total, Digest: expected}))
if err != nil {
return errdefs.ToGRPC(err)
}
defer wr.Close()
for {
msg.Action = req.Action
ws, err := wr.Status()
if err != nil {
return errdefs.ToGRPC(err)
}
msg.Offset = ws.Offset // always set the offset.
// NOTE(stevvooe): In general, there are two cases underwhich a remote
// writer is used.
//
// For pull, we almost always have this before fetching large content,
// through descriptors. We allow predeclaration of the expected size
// and digest.
//
// For push, it is more complex. If we want to cut through content into
// storage, we may have no expectation until we are done processing the
// content. The case here is the following:
//
// 1. Start writing content.
// 2. Compress inline.
// 3. Validate digest and size (maybe).
//
// Supporting these two paths is quite awkward but it lets both API
// users use the same writer style for each with a minimum of overhead.
if req.Expected != "" {
dg := digest.Digest(req.Expected)
if expected != "" && expected != dg {
log.G(ctx).Debugf("commit digest differs from writer digest: %v != %v", dg, expected)
}
expected = dg
if _, err := s.store.Info(session.Context(), dg); err == nil {
if err := wr.Close(); err != nil {
log.G(ctx).WithError(err).Error("failed to close writer")
}
if err := s.store.Abort(session.Context(), ref); err != nil {
log.G(ctx).WithError(err).Error("failed to abort write")
}
return status.Errorf(codes.AlreadyExists, "blob with expected digest %v exists", req.Expected)
}
}
if req.Total > 0 {
// Update the expected total. Typically, this could be seen at
// negotiation time or on a commit message.
if total > 0 && req.Total != total {
log.G(ctx).Debugf("commit size differs from writer size: %v != %v", req.Total, total)
}
total = req.Total
}
switch req.Action {
case api.WriteAction_STAT:
msg.Digest = wr.Digest().String()
msg.StartedAt = protobuf.ToTimestamp(ws.StartedAt)
msg.UpdatedAt = protobuf.ToTimestamp(ws.UpdatedAt)
msg.Total = total
case api.WriteAction_WRITE, api.WriteAction_COMMIT:
if req.Offset > 0 {
// validate the offset if provided
if req.Offset != ws.Offset {
return status.Errorf(codes.OutOfRange, "write @%v must occur at current offset %v", req.Offset, ws.Offset)
}
}
if req.Offset == 0 && ws.Offset > 0 {
if err := wr.Truncate(req.Offset); err != nil {
return fmt.Errorf("truncate failed: %w", err)
}
msg.Offset = req.Offset
}
// issue the write if we actually have data.
if len(req.Data) > 0 {
// While this looks like we could use io.WriterAt here, because we
// maintain the offset as append only, we just issue the write.
n, err := wr.Write(req.Data)
if err != nil {
return errdefs.ToGRPC(err)
}
if n != len(req.Data) {
// TODO(stevvooe): Perhaps, we can recover this by including it
// in the offset on the write return.
return status.Errorf(codes.DataLoss, "wrote %v of %v bytes", n, len(req.Data))
}
msg.Offset += int64(n)
}
if req.Action == api.WriteAction_COMMIT {
var opts []content.Opt
if req.Labels != nil {
opts = append(opts, content.WithLabels(req.Labels))
}
if err := wr.Commit(ctx, total, expected, opts...); err != nil {
return errdefs.ToGRPC(err)
}
}
msg.Digest = wr.Digest().String()
}
if err := session.Send(&msg); err != nil {
return err
}
if req.Action == api.WriteAction_COMMIT {
return nil
}
req, err = session.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}
func (s *service) Abort(ctx context.Context, req *api.AbortRequest) (*ptypes.Empty, error) {
if err := s.store.Abort(ctx, req.Ref); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func infoToGRPC(info content.Info) *api.Info {
return &api.Info{
Digest: info.Digest.String(),
Size: info.Size,
CreatedAt: protobuf.ToTimestamp(info.CreatedAt),
UpdatedAt: protobuf.ToTimestamp(info.UpdatedAt),
Labels: info.Labels,
}
}
func infoFromGRPC(info *api.Info) content.Info {
return content.Info{
Digest: digest.Digest(info.Digest),
Size: info.Size,
CreatedAt: protobuf.FromTimestamp(info.CreatedAt),
UpdatedAt: protobuf.FromTimestamp(info.UpdatedAt),
Labels: info.Labels,
}
}

View File

@@ -0,0 +1,43 @@
/*
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 content
import (
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/containerd/v2/plugins/services/content/contentserver"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "content",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
cs, err := ic.GetByID(plugins.ServicePlugin, services.ContentService)
if err != nil {
return nil, err
}
return contentserver.New(cs.(content.Store)), nil
},
})
}

View File

@@ -0,0 +1,78 @@
/*
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 content
import (
"context"
eventstypes "github.com/containerd/containerd/v2/api/events"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
digest "github.com/opencontainers/go-digest"
)
// store wraps content.Store with proper event published.
type store struct {
content.Store
publisher events.Publisher
}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.ContentService,
Requires: []plugin.Type{
plugins.EventPlugin,
plugins.MetadataPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
m, err := ic.GetSingle(plugins.MetadataPlugin)
if err != nil {
return nil, err
}
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
s, err := newContentStore(m.(*metadata.DB).ContentStore(), ep.(events.Publisher))
return s, err
},
})
}
func newContentStore(cs content.Store, publisher events.Publisher) (content.Store, error) {
return &store{
Store: cs,
publisher: publisher,
}, nil
}
func (s *store) Delete(ctx context.Context, dgst digest.Digest) error {
if err := s.Store.Delete(ctx, dgst); err != nil {
return err
}
// TODO: Consider whether we should return error here.
return s.publisher.Publish(ctx, "/content/delete", &eventstypes.ContentDelete{
Digest: dgst.String(),
})
}

View File

@@ -0,0 +1,164 @@
/*
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 diff
import (
"context"
"fmt"
diffapi "github.com/containerd/containerd/v2/api/services/diff/v1"
"github.com/containerd/containerd/v2/core/diff"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/typeurl/v2"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"google.golang.org/grpc"
)
type config struct {
// Order is the order of preference in which to try diff algorithms, the
// first differ which is supported is used.
// Note when multiple differs may be supported, this order will be
// respected for which is chosen. Each differ should return the same
// correct output, allowing any ordering to be used to prefer
// more optimimal implementations.
Order []string `toml:"default"`
}
type differ interface {
diff.Comparer
diff.Applier
}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.DiffService,
Requires: []plugin.Type{
plugins.DiffPlugin,
},
Config: defaultDifferConfig,
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
differs, err := ic.GetByType(plugins.DiffPlugin)
if err != nil {
return nil, err
}
orderedNames := ic.Config.(*config).Order
ordered := make([]differ, len(orderedNames))
for i, n := range orderedNames {
d, ok := differs[n]
if !ok {
return nil, fmt.Errorf("needed differ not loaded: %s", n)
}
ordered[i], ok = d.(differ)
if !ok {
return nil, fmt.Errorf("differ does not implement Comparer and Applier interface: %s", n)
}
}
return &local{
differs: ordered,
}, nil
},
})
}
type local struct {
differs []differ
}
var _ diffapi.DiffClient = &local{}
func (l *local) Apply(ctx context.Context, er *diffapi.ApplyRequest, _ ...grpc.CallOption) (*diffapi.ApplyResponse, error) {
var (
ocidesc ocispec.Descriptor
err error
desc = oci.DescriptorFromProto(er.Diff)
mounts = mount.FromProto(er.Mounts)
)
var opts []diff.ApplyOpt
if er.Payloads != nil {
payloads := make(map[string]typeurl.Any)
for k, v := range er.Payloads {
payloads[k] = v
}
opts = append(opts, diff.WithPayloads(payloads))
}
opts = append(opts, diff.WithSyncFs(er.SyncFs))
for _, differ := range l.differs {
ocidesc, err = differ.Apply(ctx, desc, mounts, opts...)
if !errdefs.IsNotImplemented(err) {
break
}
}
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &diffapi.ApplyResponse{
Applied: oci.DescriptorToProto(ocidesc),
}, nil
}
func (l *local) Diff(ctx context.Context, dr *diffapi.DiffRequest, _ ...grpc.CallOption) (*diffapi.DiffResponse, error) {
var (
ocidesc ocispec.Descriptor
err error
aMounts = mount.FromProto(dr.Left)
bMounts = mount.FromProto(dr.Right)
)
var opts []diff.Opt
if dr.MediaType != "" {
opts = append(opts, diff.WithMediaType(dr.MediaType))
}
if dr.Ref != "" {
opts = append(opts, diff.WithReference(dr.Ref))
}
if dr.Labels != nil {
opts = append(opts, diff.WithLabels(dr.Labels))
}
if dr.SourceDateEpoch != nil {
tm := dr.SourceDateEpoch.AsTime()
opts = append(opts, diff.WithSourceDateEpoch(&tm))
}
for _, d := range l.differs {
ocidesc, err = d.Compare(ctx, aMounts, bMounts, opts...)
if !errdefs.IsNotImplemented(err) {
break
}
}
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &diffapi.DiffResponse{
Diff: oci.DescriptorToProto(ocidesc),
}, nil
}

View File

@@ -0,0 +1,65 @@
/*
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 diff
import (
"context"
diffapi "github.com/containerd/containerd/v2/api/services/diff/v1"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "diff",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.DiffService)
if err != nil {
return nil, err
}
return &service{local: i.(diffapi.DiffClient)}, nil
},
})
}
type service struct {
local diffapi.DiffClient
diffapi.UnimplementedDiffServer
}
var _ diffapi.DiffServer = &service{}
func (s *service) Register(gs *grpc.Server) error {
diffapi.RegisterDiffServer(gs, s)
return nil
}
func (s *service) Apply(ctx context.Context, er *diffapi.ApplyRequest) (*diffapi.ApplyResponse, error) {
return s.local.Apply(ctx, er)
}
func (s *service) Diff(ctx context.Context, dr *diffapi.DiffRequest) (*diffapi.DiffResponse, error) {
return s.local.Diff(ctx, dr)
}

View File

@@ -0,0 +1,23 @@
//go:build !windows
/*
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 diff
var defaultDifferConfig = &config{
Order: []string{"walking"},
}

View File

@@ -0,0 +1,21 @@
/*
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 diff
var defaultDifferConfig = &config{
Order: []string{"windows", "windows-lcow"},
}

View File

@@ -0,0 +1,133 @@
/*
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 events
import (
"context"
"fmt"
api "github.com/containerd/containerd/v2/api/services/events/v1"
apittrpc "github.com/containerd/containerd/v2/api/services/ttrpc/events/v1"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/events/exchange"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/protobuf"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/ttrpc"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "events",
Requires: []plugin.Type{
plugins.EventPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ep, err := ic.GetByID(plugins.EventPlugin, "exchange")
if err != nil {
return nil, err
}
return NewService(ep.(*exchange.Exchange)), nil
},
})
}
type service struct {
ttService *ttrpcService
events *exchange.Exchange
api.UnimplementedEventsServer
}
// NewService returns the GRPC events server
func NewService(events *exchange.Exchange) api.EventsServer {
return &service{
ttService: &ttrpcService{
events: events,
},
events: events,
}
}
func (s *service) Register(server *grpc.Server) error {
api.RegisterEventsServer(server, s)
return nil
}
func (s *service) RegisterTTRPC(server *ttrpc.Server) error {
apittrpc.RegisterEventsService(server, s.ttService)
return nil
}
func (s *service) Publish(ctx context.Context, r *api.PublishRequest) (*ptypes.Empty, error) {
if err := s.events.Publish(ctx, r.Topic, r.Event); err != nil {
return nil, errdefs.ToGRPC(err)
}
return &ptypes.Empty{}, nil
}
func (s *service) Forward(ctx context.Context, r *api.ForwardRequest) (*ptypes.Empty, error) {
if err := s.events.Forward(ctx, fromProto(r.Envelope)); err != nil {
return nil, errdefs.ToGRPC(err)
}
return &ptypes.Empty{}, nil
}
func (s *service) Subscribe(req *api.SubscribeRequest, srv api.Events_SubscribeServer) error {
ctx, cancel := context.WithCancel(srv.Context())
defer cancel()
eventq, errq := s.events.Subscribe(ctx, req.Filters...)
for {
select {
case ev := <-eventq:
if err := srv.Send(toProto(ev)); err != nil {
return fmt.Errorf("failed sending event to subscriber: %w", err)
}
case err := <-errq:
if err != nil {
return fmt.Errorf("subscription error: %w", err)
}
return nil
}
}
}
func toProto(env *events.Envelope) *api.Envelope {
return &api.Envelope{
Timestamp: protobuf.ToTimestamp(env.Timestamp),
Namespace: env.Namespace,
Topic: env.Topic,
Event: protobuf.FromAny(env.Event),
}
}
func fromProto(env *api.Envelope) *events.Envelope {
return &events.Envelope{
Timestamp: protobuf.FromTimestamp(env.Timestamp),
Namespace: env.Namespace,
Topic: env.Topic,
Event: env.Event,
}
}

View File

@@ -0,0 +1,49 @@
/*
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 events
import (
"context"
api "github.com/containerd/containerd/v2/api/services/ttrpc/events/v1"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/events/exchange"
"github.com/containerd/containerd/v2/protobuf"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
)
type ttrpcService struct {
events *exchange.Exchange
}
func (s *ttrpcService) Forward(ctx context.Context, r *api.ForwardRequest) (*ptypes.Empty, error) {
if err := s.events.Forward(ctx, fromTProto(r.Envelope)); err != nil {
return nil, errdefs.ToGRPC(err)
}
return &ptypes.Empty{}, nil
}
func fromTProto(env *api.Envelope) *events.Envelope {
return &events.Envelope{
Timestamp: protobuf.FromTimestamp(env.Timestamp),
Namespace: env.Namespace,
Topic: env.Topic,
Event: env.Event,
}
}

View File

@@ -0,0 +1,52 @@
/*
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 healthcheck
import (
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
)
type service struct {
serve *health.Server
}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "healthcheck",
InitFn: func(*plugin.InitContext) (interface{}, error) {
return newService()
},
})
}
func newService() (*service, error) {
return &service{
health.NewServer(),
}, nil
}
func (s *service) Register(server *grpc.Server) error {
grpc_health_v1.RegisterHealthServer(server, s.serve)
return nil
}

View File

@@ -0,0 +1,75 @@
/*
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 images
import (
imagesapi "github.com/containerd/containerd/v2/api/services/images/v1"
"github.com/containerd/containerd/v2/api/types"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/protobuf"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func imagesToProto(images []images.Image) []*imagesapi.Image {
var imagespb []*imagesapi.Image
for _, image := range images {
image := image
imagespb = append(imagespb, imageToProto(&image))
}
return imagespb
}
func imageToProto(image *images.Image) *imagesapi.Image {
return &imagesapi.Image{
Name: image.Name,
Labels: image.Labels,
Target: descToProto(&image.Target),
CreatedAt: protobuf.ToTimestamp(image.CreatedAt),
UpdatedAt: protobuf.ToTimestamp(image.UpdatedAt),
}
}
func imageFromProto(imagepb *imagesapi.Image) images.Image {
return images.Image{
Name: imagepb.Name,
Labels: imagepb.Labels,
Target: descFromProto(imagepb.Target),
CreatedAt: protobuf.FromTimestamp(imagepb.CreatedAt),
UpdatedAt: protobuf.FromTimestamp(imagepb.UpdatedAt),
}
}
func descFromProto(desc *types.Descriptor) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: desc.MediaType,
Size: desc.Size,
Digest: digest.Digest(desc.Digest),
Annotations: desc.Annotations,
}
}
func descToProto(desc *ocispec.Descriptor) *types.Descriptor {
return &types.Descriptor{
MediaType: desc.MediaType,
Size: desc.Size,
Digest: desc.Digest.String(),
Annotations: desc.Annotations,
}
}

View File

@@ -0,0 +1,228 @@
/*
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 images
import (
"context"
"github.com/containerd/log"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
eventstypes "github.com/containerd/containerd/v2/api/events"
imagesapi "github.com/containerd/containerd/v2/api/services/images/v1"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/gc"
"github.com/containerd/containerd/v2/pkg/deprecation"
"github.com/containerd/containerd/v2/pkg/epoch"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/containerd/v2/plugins/services/warning"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.ImagesService,
Requires: []plugin.Type{
plugins.EventPlugin,
plugins.MetadataPlugin,
plugins.GCPlugin,
plugins.WarningPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
m, err := ic.GetSingle(plugins.MetadataPlugin)
if err != nil {
return nil, err
}
g, err := ic.GetSingle(plugins.GCPlugin)
if err != nil {
return nil, err
}
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
w, err := ic.GetSingle(plugins.WarningPlugin)
if err != nil {
return nil, err
}
return &local{
store: metadata.NewImageStore(m.(*metadata.DB)),
publisher: ep.(events.Publisher),
gc: g.(gcScheduler),
warnings: w.(warning.Service),
}, nil
},
})
}
type gcScheduler interface {
ScheduleAndWait(context.Context) (gc.Stats, error)
}
type local struct {
store images.Store
gc gcScheduler
publisher events.Publisher
warnings warning.Service
}
var _ imagesapi.ImagesClient = &local{}
func (l *local) Get(ctx context.Context, req *imagesapi.GetImageRequest, _ ...grpc.CallOption) (*imagesapi.GetImageResponse, error) {
image, err := l.store.Get(ctx, req.Name)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
imagepb := imageToProto(&image)
return &imagesapi.GetImageResponse{
Image: imagepb,
}, nil
}
func (l *local) List(ctx context.Context, req *imagesapi.ListImagesRequest, _ ...grpc.CallOption) (*imagesapi.ListImagesResponse, error) {
images, err := l.store.List(ctx, req.Filters...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &imagesapi.ListImagesResponse{
Images: imagesToProto(images),
}, nil
}
func (l *local) Create(ctx context.Context, req *imagesapi.CreateImageRequest, _ ...grpc.CallOption) (*imagesapi.CreateImageResponse, error) {
log.G(ctx).WithField("name", req.Image.Name).WithField("target", req.Image.Target.Digest).Debugf("create image")
if req.Image.Name == "" {
return nil, status.Errorf(codes.InvalidArgument, "Image.Name required")
}
var (
image = imageFromProto(req.Image)
resp imagesapi.CreateImageResponse
)
if req.SourceDateEpoch != nil {
tm := req.SourceDateEpoch.AsTime()
ctx = epoch.WithSourceDateEpoch(ctx, &tm)
}
created, err := l.store.Create(ctx, image)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
resp.Image = imageToProto(&created)
if err := l.publisher.Publish(ctx, "/images/create", &eventstypes.ImageCreate{
Name: resp.Image.Name,
Labels: resp.Image.Labels,
}); err != nil {
return nil, err
}
l.emitSchema1DeprecationWarning(ctx, &image)
return &resp, nil
}
func (l *local) Update(ctx context.Context, req *imagesapi.UpdateImageRequest, _ ...grpc.CallOption) (*imagesapi.UpdateImageResponse, error) {
if req.Image.Name == "" {
return nil, status.Errorf(codes.InvalidArgument, "Image.Name required")
}
var (
image = imageFromProto(req.Image)
resp imagesapi.UpdateImageResponse
fieldpaths []string
)
if req.UpdateMask != nil && len(req.UpdateMask.Paths) > 0 {
fieldpaths = append(fieldpaths, req.UpdateMask.Paths...)
}
if req.SourceDateEpoch != nil {
tm := req.SourceDateEpoch.AsTime()
ctx = epoch.WithSourceDateEpoch(ctx, &tm)
}
updated, err := l.store.Update(ctx, image, fieldpaths...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
resp.Image = imageToProto(&updated)
if err := l.publisher.Publish(ctx, "/images/update", &eventstypes.ImageUpdate{
Name: resp.Image.Name,
Labels: resp.Image.Labels,
}); err != nil {
return nil, err
}
l.emitSchema1DeprecationWarning(ctx, &image)
return &resp, nil
}
func (l *local) Delete(ctx context.Context, req *imagesapi.DeleteImageRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
log.G(ctx).WithField("name", req.Name).Debugf("delete image")
var opts []images.DeleteOpt
if req.Target != nil {
desc := descFromProto(req.Target)
opts = append(opts, images.DeleteTarget(&desc))
}
// Sync option handled here after event is published
if err := l.store.Delete(ctx, req.Name, opts...); err != nil {
return nil, errdefs.ToGRPC(err)
}
if err := l.publisher.Publish(ctx, "/images/delete", &eventstypes.ImageDelete{
Name: req.Name,
}); err != nil {
return nil, err
}
if req.Sync {
if _, err := l.gc.ScheduleAndWait(ctx); err != nil {
return nil, err
}
}
return &ptypes.Empty{}, nil
}
func (l *local) emitSchema1DeprecationWarning(ctx context.Context, image *images.Image) {
if image == nil {
return
}
dgst, ok := image.Labels[images.ConvertedDockerSchema1LabelKey]
if !ok {
return
}
log.G(ctx).WithField("name", image.Name).WithField("schema1digest", dgst).Warn("conversion from schema 1 images is deprecated")
l.warnings.Emit(ctx, deprecation.PullSchema1Image)
}

View File

@@ -0,0 +1,78 @@
/*
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 images
import (
"context"
imagesapi "github.com/containerd/containerd/v2/api/services/images/v1"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "images",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.ImagesService)
if err != nil {
return nil, err
}
return &service{local: i.(imagesapi.ImagesClient)}, nil
},
})
}
type service struct {
local imagesapi.ImagesClient
imagesapi.UnimplementedImagesServer
}
var _ imagesapi.ImagesServer = &service{}
func (s *service) Register(server *grpc.Server) error {
imagesapi.RegisterImagesServer(server, s)
return nil
}
func (s *service) Get(ctx context.Context, req *imagesapi.GetImageRequest) (*imagesapi.GetImageResponse, error) {
return s.local.Get(ctx, req)
}
func (s *service) List(ctx context.Context, req *imagesapi.ListImagesRequest) (*imagesapi.ListImagesResponse, error) {
return s.local.List(ctx, req)
}
func (s *service) Create(ctx context.Context, req *imagesapi.CreateImageRequest) (*imagesapi.CreateImageResponse, error) {
return s.local.Create(ctx, req)
}
func (s *service) Update(ctx context.Context, req *imagesapi.UpdateImageRequest) (*imagesapi.UpdateImageResponse, error) {
return s.local.Update(ctx, req)
}
func (s *service) Delete(ctx context.Context, req *imagesapi.DeleteImageRequest) (*ptypes.Empty, error) {
return s.local.Delete(ctx, req)
}

View File

@@ -0,0 +1,66 @@
/*
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 introspection
import (
context "context"
api "github.com/containerd/containerd/v2/api/services/introspection/v1"
"github.com/containerd/containerd/v2/errdefs"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/log"
)
// Service defines the introspection service interface
type Service interface {
Plugins(context.Context, []string) (*api.PluginsResponse, error)
Server(context.Context, *ptypes.Empty) (*api.ServerResponse, error)
}
type introspectionRemote struct {
client api.IntrospectionClient
}
var _ = (Service)(&introspectionRemote{})
// NewIntrospectionServiceFromClient creates a new introspection service from an API client
func NewIntrospectionServiceFromClient(c api.IntrospectionClient) Service {
return &introspectionRemote{client: c}
}
func (i *introspectionRemote) Plugins(ctx context.Context, filters []string) (*api.PluginsResponse, error) {
log.G(ctx).WithField("filters", filters).Debug("remote introspection plugin filters")
resp, err := i.client.Plugins(ctx, &api.PluginsRequest{
Filters: filters,
})
if err != nil {
return nil, errdefs.FromGRPC(err)
}
return resp, nil
}
func (i *introspectionRemote) Server(ctx context.Context, in *ptypes.Empty) (*api.ServerResponse, error) {
resp, err := i.client.Server(ctx, in)
if err != nil {
return nil, errdefs.FromGRPC(err)
}
return resp, nil
}

View File

@@ -0,0 +1,269 @@
/*
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 introspection
import (
context "context"
"errors"
"os"
"path/filepath"
"runtime"
"sync"
"github.com/google/uuid"
"google.golang.org/genproto/googleapis/rpc/code"
rpc "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
api "github.com/containerd/containerd/v2/api/services/introspection/v1"
"github.com/containerd/containerd/v2/api/types"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/filters"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/containerd/v2/plugins/services/warning"
"github.com/containerd/containerd/v2/protobuf"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.IntrospectionService,
Requires: []plugin.Type{plugins.WarningPlugin},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.WarningPlugin, plugins.DeprecationsPlugin)
if err != nil {
return nil, err
}
warningClient, ok := i.(warning.Service)
if !ok {
return nil, errors.New("could not create a local client for warning service")
}
// this service fetches all plugins through the plugin set of the plugin context
return &Local{
plugins: ic.Plugins(),
root: ic.Properties[plugins.PropertyRootDir],
warningClient: warningClient,
}, nil
},
})
}
// Local is a local implementation of the introspection service
type Local struct {
mu sync.Mutex
root string
plugins *plugin.Set
pluginCache []*api.Plugin
warningClient warning.Service
}
var _ = (api.IntrospectionClient)(&Local{})
// UpdateLocal updates the local introspection service
func (l *Local) UpdateLocal(root string) {
l.mu.Lock()
defer l.mu.Unlock()
l.root = root
}
// Plugins returns the locally defined plugins
func (l *Local) Plugins(ctx context.Context, req *api.PluginsRequest, _ ...grpc.CallOption) (*api.PluginsResponse, error) {
filter, err := filters.ParseAll(req.Filters...)
if err != nil {
return nil, errdefs.ToGRPCf(errdefs.ErrInvalidArgument, err.Error())
}
var plugins []*api.Plugin
allPlugins := l.getPlugins()
for _, p := range allPlugins {
p := p
if filter.Match(adaptPlugin(p)) {
plugins = append(plugins, p)
}
}
return &api.PluginsResponse{
Plugins: plugins,
}, nil
}
func (l *Local) getPlugins() []*api.Plugin {
l.mu.Lock()
defer l.mu.Unlock()
plugins := l.plugins.GetAll()
if l.pluginCache == nil || len(plugins) != len(l.pluginCache) {
l.pluginCache = pluginsToPB(plugins)
}
return l.pluginCache
}
// Server returns the local server information
func (l *Local) Server(ctx context.Context, _ *ptypes.Empty, _ ...grpc.CallOption) (*api.ServerResponse, error) {
u, err := l.getUUID()
if err != nil {
return nil, errdefs.ToGRPC(err)
}
pid := os.Getpid()
var pidns uint64
if runtime.GOOS == "linux" {
pidns, err = statPIDNS(pid)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
}
return &api.ServerResponse{
UUID: u,
Pid: uint64(pid),
Pidns: pidns,
Deprecations: l.getWarnings(ctx),
}, nil
}
func (l *Local) getUUID() (string, error) {
l.mu.Lock()
defer l.mu.Unlock()
data, err := os.ReadFile(l.uuidPath())
if err != nil {
if os.IsNotExist(err) {
return l.generateUUID()
}
return "", err
}
u := string(data)
if _, err := uuid.Parse(u); err != nil {
return "", err
}
return u, nil
}
func (l *Local) generateUUID() (string, error) {
u, err := uuid.NewRandom()
if err != nil {
return "", err
}
path := l.uuidPath()
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return "", err
}
uu := u.String()
if err := os.WriteFile(path, []byte(uu), 0666); err != nil {
return "", err
}
return uu, nil
}
func (l *Local) uuidPath() string {
return filepath.Join(l.root, "uuid")
}
func (l *Local) getWarnings(ctx context.Context) []*api.DeprecationWarning {
return warningsPB(ctx, l.warningClient.Warnings())
}
func adaptPlugin(o interface{}) filters.Adaptor {
obj := o.(*api.Plugin)
return filters.AdapterFunc(func(fieldpath []string) (string, bool) {
if len(fieldpath) == 0 {
return "", false
}
switch fieldpath[0] {
case "type":
return obj.Type, len(obj.Type) > 0
case "id":
return obj.ID, len(obj.ID) > 0
case "platforms":
// TODO(stevvooe): Another case here where have multiple values.
// May need to refactor the filter system to allow filtering by
// platform, if this is required.
case "capabilities":
// TODO(stevvooe): Need a better way to match against
// collections. We can only return "the value" but really it
// would be best if we could return a set of values for the
// path, any of which could match.
}
return "", false
})
}
func pluginsToPB(plugins []*plugin.Plugin) []*api.Plugin {
var pluginsPB []*api.Plugin
for _, p := range plugins {
var requires []string
for _, r := range p.Registration.Requires {
requires = append(requires, r.String())
}
var initErr *rpc.Status
if err := p.Err(); err != nil {
st, ok := status.FromError(errdefs.ToGRPC(err))
if ok {
var details []*ptypes.Any
for _, d := range st.Proto().Details {
details = append(details, &ptypes.Any{
TypeUrl: d.TypeUrl,
Value: d.Value,
})
}
initErr = &rpc.Status{
Code: int32(st.Code()),
Message: st.Message(),
Details: details,
}
} else {
initErr = &rpc.Status{
Code: int32(code.Code_UNKNOWN),
Message: err.Error(),
}
}
}
pluginsPB = append(pluginsPB, &api.Plugin{
Type: p.Registration.Type.String(),
ID: p.Registration.ID,
Requires: requires,
Platforms: types.OCIPlatformToProto(p.Meta.Platforms),
Capabilities: p.Meta.Capabilities,
Exports: p.Meta.Exports,
InitErr: initErr,
})
}
return pluginsPB
}
func warningsPB(ctx context.Context, warnings []warning.Warning) []*api.DeprecationWarning {
var pb []*api.DeprecationWarning
for _, w := range warnings {
pb = append(pb, &api.DeprecationWarning{
ID: string(w.ID),
Message: w.Message,
LastOccurrence: protobuf.ToTimestamp(w.LastOccurrence),
})
}
return pb
}

View File

@@ -0,0 +1,36 @@
/*
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 introspection
import (
"fmt"
"os"
"syscall"
)
func statPIDNS(pid int) (uint64, error) {
f := fmt.Sprintf("/proc/%d/ns/pid", pid)
st, err := os.Stat(f)
if err != nil {
return 0, err
}
stSys, ok := st.Sys().(*syscall.Stat_t)
if !ok {
return 0, fmt.Errorf("%T is not *syscall.Stat_t", st.Sys())
}
return stSys.Ino, nil
}

View File

@@ -0,0 +1,23 @@
//go:build !linux
/*
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 introspection
func statPIDNS(pid int) (uint64, error) {
return 0, nil
}

View File

@@ -0,0 +1,74 @@
/*
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 introspection
import (
context "context"
"errors"
api "github.com/containerd/containerd/v2/api/services/introspection/v1"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "introspection",
Requires: []plugin.Type{plugins.ServicePlugin},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.IntrospectionService)
if err != nil {
return nil, err
}
localClient, ok := i.(*Local)
if !ok {
return nil, errors.New("could not create a local client for introspection service")
}
localClient.UpdateLocal(ic.Properties[plugins.PropertyRootDir])
return &server{
local: localClient,
}, nil
},
})
}
type server struct {
local api.IntrospectionClient
api.UnimplementedIntrospectionServer
}
var _ = (api.IntrospectionServer)(&server{})
func (s *server) Register(server *grpc.Server) error {
api.RegisterIntrospectionServer(server, s)
return nil
}
func (s *server) Plugins(ctx context.Context, req *api.PluginsRequest) (*api.PluginsResponse, error) {
return s.local.Plugins(ctx, req)
}
func (s *server) Server(ctx context.Context, empty *ptypes.Empty) (*api.ServerResponse, error) {
return s.local.Server(ctx, empty)
}

View File

@@ -0,0 +1,165 @@
/*
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 leases
import (
"context"
api "github.com/containerd/containerd/v2/api/services/leases/v1"
"github.com/containerd/containerd/v2/core/leases"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/protobuf"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "leases",
Requires: []plugin.Type{
plugins.LeasePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.LeasePlugin, "manager")
if err != nil {
return nil, err
}
return &service{lm: i.(leases.Manager)}, nil
},
})
}
type service struct {
lm leases.Manager
api.UnimplementedLeasesServer
}
func (s *service) Register(server *grpc.Server) error {
api.RegisterLeasesServer(server, s)
return nil
}
func (s *service) Create(ctx context.Context, r *api.CreateRequest) (*api.CreateResponse, error) {
opts := []leases.Opt{
leases.WithLabels(r.Labels),
}
if r.ID == "" {
opts = append(opts, leases.WithRandomID())
} else {
opts = append(opts, leases.WithID(r.ID))
}
l, err := s.lm.Create(ctx, opts...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.CreateResponse{
Lease: leaseToGRPC(l),
}, nil
}
func (s *service) Delete(ctx context.Context, r *api.DeleteRequest) (*ptypes.Empty, error) {
var opts []leases.DeleteOpt
if r.Sync {
opts = append(opts, leases.SynchronousDelete)
}
if err := s.lm.Delete(ctx, leases.Lease{
ID: r.ID,
}, opts...); err != nil {
return nil, errdefs.ToGRPC(err)
}
return &ptypes.Empty{}, nil
}
func (s *service) List(ctx context.Context, r *api.ListRequest) (*api.ListResponse, error) {
l, err := s.lm.List(ctx, r.Filters...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
apileases := make([]*api.Lease, len(l))
for i := range l {
apileases[i] = leaseToGRPC(l[i])
}
return &api.ListResponse{
Leases: apileases,
}, nil
}
func (s *service) AddResource(ctx context.Context, r *api.AddResourceRequest) (*ptypes.Empty, error) {
lease := leases.Lease{
ID: r.ID,
}
if err := s.lm.AddResource(ctx, lease, leases.Resource{
ID: r.Resource.ID,
Type: r.Resource.Type,
}); err != nil {
return nil, errdefs.ToGRPC(err)
}
return &ptypes.Empty{}, nil
}
func (s *service) DeleteResource(ctx context.Context, r *api.DeleteResourceRequest) (*ptypes.Empty, error) {
lease := leases.Lease{
ID: r.ID,
}
if err := s.lm.DeleteResource(ctx, lease, leases.Resource{
ID: r.Resource.ID,
Type: r.Resource.Type,
}); err != nil {
return nil, errdefs.ToGRPC(err)
}
return &ptypes.Empty{}, nil
}
func (s *service) ListResources(ctx context.Context, r *api.ListResourcesRequest) (*api.ListResourcesResponse, error) {
lease := leases.Lease{
ID: r.ID,
}
rs, err := s.lm.ListResources(ctx, lease)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
apiResources := make([]*api.Resource, 0, len(rs))
for _, i := range rs {
apiResources = append(apiResources, &api.Resource{
ID: i.ID,
Type: i.Type,
})
}
return &api.ListResourcesResponse{
Resources: apiResources,
}, nil
}
func leaseToGRPC(l leases.Lease) *api.Lease {
return &api.Lease{
ID: l.ID,
Labels: l.Labels,
CreatedAt: protobuf.ToTimestamp(l.CreatedAt),
}
}

View File

@@ -0,0 +1,232 @@
/*
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 namespaces
import (
"context"
"strings"
eventstypes "github.com/containerd/containerd/v2/api/events"
api "github.com/containerd/containerd/v2/api/services/namespaces/v1"
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/namespaces"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
bolt "go.etcd.io/bbolt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.NamespacesService,
Requires: []plugin.Type{
plugins.EventPlugin,
plugins.MetadataPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
m, err := ic.GetSingle(plugins.MetadataPlugin)
if err != nil {
return nil, err
}
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
return &local{
db: m.(*metadata.DB),
publisher: ep.(events.Publisher),
}, nil
},
})
}
// Provide local namespaces service instead of local namespace store,
// because namespace store interface doesn't provide enough functionality
// for namespaces service.
type local struct {
db *metadata.DB
publisher events.Publisher
}
var _ api.NamespacesClient = &local{}
func (l *local) Get(ctx context.Context, req *api.GetNamespaceRequest, _ ...grpc.CallOption) (*api.GetNamespaceResponse, error) {
var resp api.GetNamespaceResponse
return &resp, l.withStoreView(ctx, func(ctx context.Context, store namespaces.Store) error {
labels, err := store.Labels(ctx, req.Name)
if err != nil {
return errdefs.ToGRPC(err)
}
resp.Namespace = &api.Namespace{
Name: req.Name,
Labels: labels,
}
return nil
})
}
func (l *local) List(ctx context.Context, req *api.ListNamespacesRequest, _ ...grpc.CallOption) (*api.ListNamespacesResponse, error) {
var resp api.ListNamespacesResponse
return &resp, l.withStoreView(ctx, func(ctx context.Context, store namespaces.Store) error {
namespaces, err := store.List(ctx)
if err != nil {
return err
}
for _, namespace := range namespaces {
labels, err := store.Labels(ctx, namespace)
if err != nil {
// In general, this should be unlikely, since we are holding a
// transaction to service this request.
return errdefs.ToGRPC(err)
}
resp.Namespaces = append(resp.Namespaces, &api.Namespace{
Name: namespace,
Labels: labels,
})
}
return nil
})
}
func (l *local) Create(ctx context.Context, req *api.CreateNamespaceRequest, _ ...grpc.CallOption) (*api.CreateNamespaceResponse, error) {
var resp api.CreateNamespaceResponse
if err := l.withStoreUpdate(ctx, func(ctx context.Context, store namespaces.Store) error {
if err := store.Create(ctx, req.Namespace.Name, req.Namespace.Labels); err != nil {
return errdefs.ToGRPC(err)
}
for k, v := range req.Namespace.Labels {
if err := store.SetLabel(ctx, req.Namespace.Name, k, v); err != nil {
return err
}
}
resp.Namespace = req.Namespace
return nil
}); err != nil {
return &resp, err
}
ctx = namespaces.WithNamespace(ctx, req.Namespace.Name)
if err := l.publisher.Publish(ctx, "/namespaces/create", &eventstypes.NamespaceCreate{
Name: req.Namespace.Name,
Labels: req.Namespace.Labels,
}); err != nil {
return &resp, err
}
return &resp, nil
}
func (l *local) Update(ctx context.Context, req *api.UpdateNamespaceRequest, _ ...grpc.CallOption) (*api.UpdateNamespaceResponse, error) {
var resp api.UpdateNamespaceResponse
if err := l.withStoreUpdate(ctx, func(ctx context.Context, store namespaces.Store) error {
if req.UpdateMask != nil && len(req.UpdateMask.Paths) > 0 {
for _, path := range req.UpdateMask.Paths {
switch {
case strings.HasPrefix(path, "labels."):
key := strings.TrimPrefix(path, "labels.")
if err := store.SetLabel(ctx, req.Namespace.Name, key, req.Namespace.Labels[key]); err != nil {
return err
}
default:
return status.Errorf(codes.InvalidArgument, "cannot update %q field", path)
}
}
} else {
// clear out the existing labels and then set them to the incoming request.
// get current set of labels
labels, err := store.Labels(ctx, req.Namespace.Name)
if err != nil {
return errdefs.ToGRPC(err)
}
for k := range labels {
if err := store.SetLabel(ctx, req.Namespace.Name, k, ""); err != nil {
return err
}
}
for k, v := range req.Namespace.Labels {
if err := store.SetLabel(ctx, req.Namespace.Name, k, v); err != nil {
return err
}
}
}
return nil
}); err != nil {
return &resp, err
}
ctx = namespaces.WithNamespace(ctx, req.Namespace.Name)
if err := l.publisher.Publish(ctx, "/namespaces/update", &eventstypes.NamespaceUpdate{
Name: req.Namespace.Name,
Labels: req.Namespace.Labels,
}); err != nil {
return &resp, err
}
return &resp, nil
}
func (l *local) Delete(ctx context.Context, req *api.DeleteNamespaceRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
if err := l.withStoreUpdate(ctx, func(ctx context.Context, store namespaces.Store) error {
return errdefs.ToGRPC(store.Delete(ctx, req.Name))
}); err != nil {
return &ptypes.Empty{}, err
}
// set the namespace in the context before publishing the event
ctx = namespaces.WithNamespace(ctx, req.Name)
if err := l.publisher.Publish(ctx, "/namespaces/delete", &eventstypes.NamespaceDelete{
Name: req.Name,
}); err != nil {
return &ptypes.Empty{}, err
}
return &ptypes.Empty{}, nil
}
func (l *local) withStore(ctx context.Context, fn func(ctx context.Context, store namespaces.Store) error) func(tx *bolt.Tx) error {
return func(tx *bolt.Tx) error { return fn(ctx, metadata.NewNamespaceStore(tx)) }
}
func (l *local) withStoreView(ctx context.Context, fn func(ctx context.Context, store namespaces.Store) error) error {
return l.db.View(l.withStore(ctx, fn))
}
func (l *local) withStoreUpdate(ctx context.Context, fn func(ctx context.Context, store namespaces.Store) error) error {
return l.db.Update(l.withStore(ctx, fn))
}

View File

@@ -0,0 +1,78 @@
/*
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 namespaces
import (
"context"
api "github.com/containerd/containerd/v2/api/services/namespaces/v1"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "namespaces",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.NamespacesService)
if err != nil {
return nil, err
}
return &service{local: i.(api.NamespacesClient)}, nil
},
})
}
type service struct {
local api.NamespacesClient
api.UnimplementedNamespacesServer
}
var _ api.NamespacesServer = &service{}
func (s *service) Register(server *grpc.Server) error {
api.RegisterNamespacesServer(server, s)
return nil
}
func (s *service) Get(ctx context.Context, req *api.GetNamespaceRequest) (*api.GetNamespaceResponse, error) {
return s.local.Get(ctx, req)
}
func (s *service) List(ctx context.Context, req *api.ListNamespacesRequest) (*api.ListNamespacesResponse, error) {
return s.local.List(ctx, req)
}
func (s *service) Create(ctx context.Context, req *api.CreateNamespaceRequest) (*api.CreateNamespaceResponse, error) {
return s.local.Create(ctx, req)
}
func (s *service) Update(ctx context.Context, req *api.UpdateNamespaceRequest) (*api.UpdateNamespaceResponse, error) {
return s.local.Update(ctx, req)
}
func (s *service) Delete(ctx context.Context, req *api.DeleteNamespaceRequest) (*ptypes.Empty, error) {
return s.local.Delete(ctx, req)
}

View File

@@ -0,0 +1,21 @@
//go:build !windows
/*
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 opt
const defaultPath = "/opt/containerd"

View File

@@ -0,0 +1,25 @@
/*
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 opt
import (
"path/filepath"
"github.com/containerd/containerd/v2/defaults"
)
var defaultPath = filepath.Join(defaults.DefaultRootDir, "opt")

View File

@@ -0,0 +1,66 @@
/*
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 opt
import (
"fmt"
"os"
"path/filepath"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
// Config for the opt manager
type Config struct {
// Path for the opt directory
Path string `toml:"path"`
}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.InternalPlugin,
ID: "opt",
Config: &Config{
Path: defaultPath,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
path := ic.Config.(*Config).Path
ic.Meta.Exports["path"] = path
bin := filepath.Join(path, "bin")
if err := os.MkdirAll(bin, 0711); err != nil {
return nil, err
}
if err := os.Setenv("PATH", fmt.Sprintf("%s%c%s", bin, os.PathListSeparator, os.Getenv("PATH"))); err != nil {
return nil, fmt.Errorf("set binary image directory in path %s: %w", bin, err)
}
lib := filepath.Join(path, "lib")
if err := os.MkdirAll(lib, 0711); err != nil {
return nil, err
}
if err := os.Setenv("LD_LIBRARY_PATH", fmt.Sprintf("%s%c%s", lib, os.PathListSeparator, os.Getenv("LD_LIBRARY_PATH"))); err != nil {
return nil, fmt.Errorf("set binary lib directory in path %s: %w", lib, err)
}
return &manager{}, nil
},
})
}
type manager struct {
}

View File

@@ -0,0 +1,226 @@
/*
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 sandbox
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/anypb"
eventtypes "github.com/containerd/containerd/v2/api/events"
api "github.com/containerd/containerd/v2/api/services/sandbox/v1"
"github.com/containerd/containerd/v2/core/sandbox"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/protobuf"
"github.com/containerd/log"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "sandbox-controllers",
Requires: []plugin.Type{
plugins.SandboxControllerPlugin,
plugins.EventPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
sandboxers, err := ic.GetByType(plugins.SandboxControllerPlugin)
if err != nil {
return nil, err
}
sc := make(map[string]sandbox.Controller)
for name, p := range sandboxers {
sc[name] = p.(sandbox.Controller)
}
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
return &controllerService{
sc: sc,
publisher: ep.(events.Publisher),
}, nil
},
})
}
type controllerService struct {
sc map[string]sandbox.Controller
publisher events.Publisher
api.UnimplementedControllerServer
}
var _ api.ControllerServer = (*controllerService)(nil)
func (s *controllerService) Register(server *grpc.Server) error {
api.RegisterControllerServer(server, s)
return nil
}
func (s *controllerService) getController(name string) (sandbox.Controller, error) {
if len(name) == 0 {
return nil, fmt.Errorf("%w: sandbox controller name can not be empty", errdefs.ErrInvalidArgument)
}
if ctrl, ok := s.sc[name]; ok {
return ctrl, nil
}
return nil, fmt.Errorf("%w: failed to get sandbox controller by %s", errdefs.ErrNotFound, name)
}
func (s *controllerService) Create(ctx context.Context, req *api.ControllerCreateRequest) (*api.ControllerCreateResponse, error) {
log.G(ctx).WithField("req", req).Debug("create sandbox")
// TODO: Rootfs
ctrl, err := s.getController(req.Sandboxer)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
err = ctrl.Create(ctx, sandbox.Sandbox{ID: req.GetSandboxID()}, sandbox.WithOptions(req.GetOptions()))
if err != nil {
return &api.ControllerCreateResponse{}, errdefs.ToGRPC(err)
}
if err := s.publisher.Publish(ctx, "sandboxes/create", &eventtypes.SandboxCreate{
SandboxID: req.GetSandboxID(),
}); err != nil {
return &api.ControllerCreateResponse{}, errdefs.ToGRPC(err)
}
return &api.ControllerCreateResponse{
SandboxID: req.GetSandboxID(),
}, nil
}
func (s *controllerService) Start(ctx context.Context, req *api.ControllerStartRequest) (*api.ControllerStartResponse, error) {
log.G(ctx).WithField("req", req).Debug("start sandbox")
ctrl, err := s.getController(req.Sandboxer)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
inst, err := ctrl.Start(ctx, req.GetSandboxID())
if err != nil {
return &api.ControllerStartResponse{}, errdefs.ToGRPC(err)
}
if err := s.publisher.Publish(ctx, "sandboxes/start", &eventtypes.SandboxStart{
SandboxID: req.GetSandboxID(),
}); err != nil {
return &api.ControllerStartResponse{}, errdefs.ToGRPC(err)
}
return &api.ControllerStartResponse{
SandboxID: inst.SandboxID,
Pid: inst.Pid,
CreatedAt: protobuf.ToTimestamp(inst.CreatedAt),
Labels: inst.Labels,
}, nil
}
func (s *controllerService) Stop(ctx context.Context, req *api.ControllerStopRequest) (*api.ControllerStopResponse, error) {
log.G(ctx).WithField("req", req).Debug("delete sandbox")
ctrl, err := s.getController(req.Sandboxer)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.ControllerStopResponse{}, errdefs.ToGRPC(ctrl.Stop(ctx, req.GetSandboxID(), sandbox.WithTimeout(time.Duration(req.TimeoutSecs)*time.Second)))
}
func (s *controllerService) Wait(ctx context.Context, req *api.ControllerWaitRequest) (*api.ControllerWaitResponse, error) {
log.G(ctx).WithField("req", req).Debug("wait sandbox")
ctrl, err := s.getController(req.Sandboxer)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
exitStatus, err := ctrl.Wait(ctx, req.GetSandboxID())
if err != nil {
return &api.ControllerWaitResponse{}, errdefs.ToGRPC(err)
}
if err := s.publisher.Publish(ctx, "sandboxes/exit", &eventtypes.SandboxExit{
SandboxID: req.GetSandboxID(),
ExitStatus: exitStatus.ExitStatus,
ExitedAt: protobuf.ToTimestamp(exitStatus.ExitedAt),
}); err != nil {
return &api.ControllerWaitResponse{}, errdefs.ToGRPC(err)
}
return &api.ControllerWaitResponse{
ExitStatus: exitStatus.ExitStatus,
ExitedAt: protobuf.ToTimestamp(exitStatus.ExitedAt),
}, nil
}
func (s *controllerService) Status(ctx context.Context, req *api.ControllerStatusRequest) (*api.ControllerStatusResponse, error) {
log.G(ctx).WithField("req", req).Debug("sandbox status")
ctrl, err := s.getController(req.Sandboxer)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
cstatus, err := ctrl.Status(ctx, req.GetSandboxID(), req.GetVerbose())
if err != nil {
return &api.ControllerStatusResponse{}, errdefs.ToGRPC(err)
}
extra := &anypb.Any{}
if cstatus.Extra != nil {
extra = &anypb.Any{
TypeUrl: cstatus.Extra.GetTypeUrl(),
Value: cstatus.Extra.GetValue(),
}
}
return &api.ControllerStatusResponse{
SandboxID: cstatus.SandboxID,
Pid: cstatus.Pid,
State: cstatus.State,
Info: cstatus.Info,
CreatedAt: protobuf.ToTimestamp(cstatus.CreatedAt),
ExitedAt: protobuf.ToTimestamp(cstatus.ExitedAt),
Extra: extra,
}, nil
}
func (s *controllerService) Shutdown(ctx context.Context, req *api.ControllerShutdownRequest) (*api.ControllerShutdownResponse, error) {
log.G(ctx).WithField("req", req).Debug("shutdown sandbox")
ctrl, err := s.getController(req.Sandboxer)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.ControllerShutdownResponse{}, errdefs.ToGRPC(ctrl.Shutdown(ctx, req.GetSandboxID()))
}
func (s *controllerService) Metrics(ctx context.Context, req *api.ControllerMetricsRequest) (*api.ControllerMetricsResponse, error) {
log.G(ctx).WithField("req", req).Debug("sandbox metrics")
ctrl, err := s.getController(req.Sandboxer)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
metrics, err := ctrl.Metrics(ctx, req.GetSandboxID())
if err != nil {
return &api.ControllerMetricsResponse{}, errdefs.ToGRPC(err)
}
return &api.ControllerMetricsResponse{
Metrics: metrics,
}, nil
}

View File

@@ -0,0 +1,119 @@
/*
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 sandbox
import (
"context"
"google.golang.org/grpc"
api "github.com/containerd/containerd/v2/api/services/sandbox/v1"
"github.com/containerd/containerd/v2/api/types"
"github.com/containerd/containerd/v2/core/sandbox"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/log"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "sandboxes",
Requires: []plugin.Type{
plugins.SandboxStorePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
sp, err := ic.GetByID(plugins.SandboxStorePlugin, "local")
if err != nil {
return nil, err
}
return &sandboxService{store: sp.(sandbox.Store)}, nil
},
})
}
type sandboxService struct {
store sandbox.Store
api.UnimplementedStoreServer
}
var _ api.StoreServer = (*sandboxService)(nil)
func (s *sandboxService) Register(server *grpc.Server) error {
api.RegisterStoreServer(server, s)
return nil
}
func (s *sandboxService) Create(ctx context.Context, req *api.StoreCreateRequest) (*api.StoreCreateResponse, error) {
log.G(ctx).WithField("req", req).Debug("create sandbox")
sb, err := s.store.Create(ctx, sandbox.FromProto(req.Sandbox))
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.StoreCreateResponse{Sandbox: sandbox.ToProto(&sb)}, nil
}
func (s *sandboxService) Update(ctx context.Context, req *api.StoreUpdateRequest) (*api.StoreUpdateResponse, error) {
log.G(ctx).WithField("req", req).Debug("update sandbox")
sb, err := s.store.Update(ctx, sandbox.FromProto(req.Sandbox), req.Fields...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.StoreUpdateResponse{Sandbox: sandbox.ToProto(&sb)}, nil
}
func (s *sandboxService) List(ctx context.Context, req *api.StoreListRequest) (*api.StoreListResponse, error) {
log.G(ctx).WithField("req", req).Debug("list sandboxes")
resp, err := s.store.List(ctx, req.Filters...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
list := make([]*types.Sandbox, len(resp))
for i := range resp {
list[i] = sandbox.ToProto(&resp[i])
}
return &api.StoreListResponse{List: list}, nil
}
func (s *sandboxService) Get(ctx context.Context, req *api.StoreGetRequest) (*api.StoreGetResponse, error) {
log.G(ctx).WithField("req", req).Debug("get sandbox")
resp, err := s.store.Get(ctx, req.SandboxID)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
desc := sandbox.ToProto(&resp)
return &api.StoreGetResponse{Sandbox: desc}, nil
}
func (s *sandboxService) Delete(ctx context.Context, req *api.StoreDeleteRequest) (*api.StoreDeleteResponse, error) {
log.G(ctx).WithField("req", req).Debug("delete sandbox")
if err := s.store.Delete(ctx, req.SandboxID); err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.StoreDeleteResponse{}, nil
}

View File

@@ -0,0 +1,40 @@
/*
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 services
const (
// ContentService is id of content service.
ContentService = "content-service"
// SnapshotsService is id of snapshots service.
SnapshotsService = "snapshots-service"
// SandboxControllersService is id of snapshots service.
SandboxControllersService = "sandboxes-service"
// ImagesService is id of images service.
ImagesService = "images-service"
// ContainersService is id of containers service.
ContainersService = "containers-service"
// TasksService is id of tasks service.
TasksService = "tasks-service"
// NamespacesService is id of namespaces service.
NamespacesService = "namespaces-service"
// DiffService is id of diff service.
DiffService = "diff-service"
// IntrospectionService is the id of introspection service
IntrospectionService = "introspection-service"
// StreamingService is the id of the streaming service
StreamingService = "streaming-service"
)

View File

@@ -0,0 +1,267 @@
/*
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 snapshots
import (
"context"
snapshotsapi "github.com/containerd/containerd/v2/api/services/snapshots/v1"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/containerd/v2/snapshots"
"github.com/containerd/log"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "snapshots",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
InitFn: newService,
})
}
var empty = &ptypes.Empty{}
type service struct {
ss map[string]snapshots.Snapshotter
snapshotsapi.UnimplementedSnapshotsServer
}
func newService(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.SnapshotsService)
if err != nil {
return nil, err
}
return &service{ss: i.(map[string]snapshots.Snapshotter)}, nil
}
func (s *service) getSnapshotter(name string) (snapshots.Snapshotter, error) {
if name == "" {
return nil, errdefs.ToGRPCf(errdefs.ErrInvalidArgument, "snapshotter argument missing")
}
sn := s.ss[name]
if sn == nil {
return nil, errdefs.ToGRPCf(errdefs.ErrInvalidArgument, "snapshotter not loaded: %s", name)
}
return sn, nil
}
func (s *service) Register(gs *grpc.Server) error {
snapshotsapi.RegisterSnapshotsServer(gs, s)
return nil
}
func (s *service) Prepare(ctx context.Context, pr *snapshotsapi.PrepareSnapshotRequest) (*snapshotsapi.PrepareSnapshotResponse, error) {
log.G(ctx).WithField("parent", pr.Parent).WithField("key", pr.Key).Debugf("prepare snapshot")
sn, err := s.getSnapshotter(pr.Snapshotter)
if err != nil {
return nil, err
}
var opts []snapshots.Opt
if pr.Labels != nil {
opts = append(opts, snapshots.WithLabels(pr.Labels))
}
mounts, err := sn.Prepare(ctx, pr.Key, pr.Parent, opts...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &snapshotsapi.PrepareSnapshotResponse{
Mounts: mount.ToProto(mounts),
}, nil
}
func (s *service) View(ctx context.Context, pr *snapshotsapi.ViewSnapshotRequest) (*snapshotsapi.ViewSnapshotResponse, error) {
log.G(ctx).WithField("parent", pr.Parent).WithField("key", pr.Key).Debugf("prepare view snapshot")
sn, err := s.getSnapshotter(pr.Snapshotter)
if err != nil {
return nil, err
}
var opts []snapshots.Opt
if pr.Labels != nil {
opts = append(opts, snapshots.WithLabels(pr.Labels))
}
mounts, err := sn.View(ctx, pr.Key, pr.Parent, opts...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &snapshotsapi.ViewSnapshotResponse{
Mounts: mount.ToProto(mounts),
}, nil
}
func (s *service) Mounts(ctx context.Context, mr *snapshotsapi.MountsRequest) (*snapshotsapi.MountsResponse, error) {
log.G(ctx).WithField("key", mr.Key).Debugf("get snapshot mounts")
sn, err := s.getSnapshotter(mr.Snapshotter)
if err != nil {
return nil, err
}
mounts, err := sn.Mounts(ctx, mr.Key)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &snapshotsapi.MountsResponse{
Mounts: mount.ToProto(mounts),
}, nil
}
func (s *service) Commit(ctx context.Context, cr *snapshotsapi.CommitSnapshotRequest) (*ptypes.Empty, error) {
log.G(ctx).WithField("key", cr.Key).WithField("name", cr.Name).Debugf("commit snapshot")
sn, err := s.getSnapshotter(cr.Snapshotter)
if err != nil {
return nil, err
}
var opts []snapshots.Opt
if cr.Labels != nil {
opts = append(opts, snapshots.WithLabels(cr.Labels))
}
if err := sn.Commit(ctx, cr.Name, cr.Key, opts...); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (s *service) Remove(ctx context.Context, rr *snapshotsapi.RemoveSnapshotRequest) (*ptypes.Empty, error) {
log.G(ctx).WithField("key", rr.Key).Debugf("remove snapshot")
sn, err := s.getSnapshotter(rr.Snapshotter)
if err != nil {
return nil, err
}
if err := sn.Remove(ctx, rr.Key); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (s *service) Stat(ctx context.Context, sr *snapshotsapi.StatSnapshotRequest) (*snapshotsapi.StatSnapshotResponse, error) {
log.G(ctx).WithField("key", sr.Key).Debugf("stat snapshot")
sn, err := s.getSnapshotter(sr.Snapshotter)
if err != nil {
return nil, err
}
info, err := sn.Stat(ctx, sr.Key)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &snapshotsapi.StatSnapshotResponse{Info: snapshots.InfoToProto(info)}, nil
}
func (s *service) Update(ctx context.Context, sr *snapshotsapi.UpdateSnapshotRequest) (*snapshotsapi.UpdateSnapshotResponse, error) {
log.G(ctx).WithField("key", sr.Info.Name).Debugf("update snapshot")
sn, err := s.getSnapshotter(sr.Snapshotter)
if err != nil {
return nil, err
}
info, err := sn.Update(ctx, snapshots.InfoFromProto(sr.Info), sr.UpdateMask.GetPaths()...)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &snapshotsapi.UpdateSnapshotResponse{Info: snapshots.InfoToProto(info)}, nil
}
func (s *service) List(sr *snapshotsapi.ListSnapshotsRequest, ss snapshotsapi.Snapshots_ListServer) error {
sn, err := s.getSnapshotter(sr.Snapshotter)
if err != nil {
return err
}
var (
buffer []*snapshotsapi.Info
sendBlock = func(block []*snapshotsapi.Info) error {
return ss.Send(&snapshotsapi.ListSnapshotsResponse{
Info: block,
})
}
)
err = sn.Walk(ss.Context(), func(ctx context.Context, info snapshots.Info) error {
buffer = append(buffer, snapshots.InfoToProto(info))
if len(buffer) >= 100 {
if err := sendBlock(buffer); err != nil {
return err
}
buffer = buffer[:0]
}
return nil
}, sr.Filters...)
if err != nil {
return err
}
if len(buffer) > 0 {
// Send remaining infos
if err := sendBlock(buffer); err != nil {
return err
}
}
return nil
}
func (s *service) Usage(ctx context.Context, ur *snapshotsapi.UsageRequest) (*snapshotsapi.UsageResponse, error) {
sn, err := s.getSnapshotter(ur.Snapshotter)
if err != nil {
return nil, err
}
usage, err := sn.Usage(ctx, ur.Key)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return snapshots.UsageToProto(usage), nil
}
func (s *service) Cleanup(ctx context.Context, cr *snapshotsapi.CleanupRequest) (*ptypes.Empty, error) {
sn, err := s.getSnapshotter(cr.Snapshotter)
if err != nil {
return nil, err
}
c, ok := sn.(snapshots.Cleaner)
if !ok {
return nil, errdefs.ToGRPCf(errdefs.ErrNotImplemented, "snapshotter does not implement Cleanup method")
}
err = c.Cleanup(ctx)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}

View File

@@ -0,0 +1,43 @@
/*
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 snapshots
import (
"github.com/containerd/containerd/v2/core/metadata"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.SnapshotsService,
Requires: []plugin.Type{
plugins.MetadataPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
m, err := ic.GetSingle(plugins.MetadataPlugin)
if err != nil {
return nil, err
}
return m.(*metadata.DB).Snapshotters(), nil
},
})
}

View File

@@ -0,0 +1,136 @@
/*
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 streaming
import (
"errors"
"io"
api "github.com/containerd/containerd/v2/api/services/streaming/v1"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/pkg/streaming"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/protobuf"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/log"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/typeurl/v2"
"google.golang.org/grpc"
)
var emptyResponse typeurl.Any
func init() {
// save marshalled empty response to avoid marshaling everytime
var err error
emptyResponse, err = typeurl.MarshalAny(&ptypes.Empty{})
if err != nil {
panic(err)
}
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "streaming",
Requires: []plugin.Type{
plugins.StreamingPlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.StreamingPlugin, "manager")
if err != nil {
return nil, err
}
return &service{manager: i.(streaming.StreamManager)}, nil
},
})
}
type service struct {
manager streaming.StreamManager
api.UnimplementedStreamingServer
}
func (s *service) Register(server *grpc.Server) error {
api.RegisterStreamingServer(server, s)
return nil
}
func (s *service) Stream(srv api.Streaming_StreamServer) error {
// TODO: Timeout waiting
a, err := srv.Recv()
if err != nil {
return err
}
var i api.StreamInit
if err := typeurl.UnmarshalTo(a, &i); err != nil {
return err
}
cc := make(chan struct{})
ss := &serviceStream{
s: srv,
cc: cc,
}
log.G(srv.Context()).WithField("stream", i.ID).Debug("registering stream")
if err := s.manager.Register(srv.Context(), i.ID, ss); err != nil {
return err
}
// Send response packet after registering stream
if err := srv.Send(protobuf.FromAny(emptyResponse)); err != nil {
return err
}
select {
case <-srv.Context().Done():
// TODO: Should return error if not cancelled?
case <-cc:
}
return nil
}
type serviceStream struct {
s api.Streaming_StreamServer
cc chan struct{}
}
func (ss *serviceStream) Send(a typeurl.Any) (err error) {
err = errdefs.FromGRPC(ss.s.Send(protobuf.FromAny(a)))
if !errors.Is(err, io.EOF) {
err = errdefs.FromGRPC(err)
}
return
}
func (ss *serviceStream) Recv() (a typeurl.Any, err error) {
a, err = ss.s.Recv()
if !errors.Is(err, io.EOF) {
err = errdefs.FromGRPC(err)
}
return
}
func (ss *serviceStream) Close() error {
select {
case <-ss.cc:
default:
close(ss.cc)
}
return nil
}

View File

@@ -0,0 +1,744 @@
/*
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 tasks
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"
api "github.com/containerd/containerd/v2/api/services/tasks/v1"
"github.com/containerd/containerd/v2/api/types"
"github.com/containerd/containerd/v2/api/types/task"
"github.com/containerd/containerd/v2/archive"
"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/metadata"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/events"
"github.com/containerd/containerd/v2/filters"
"github.com/containerd/containerd/v2/pkg/blockio"
"github.com/containerd/containerd/v2/pkg/rdt"
"github.com/containerd/containerd/v2/pkg/timeout"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
"github.com/containerd/containerd/v2/protobuf"
"github.com/containerd/containerd/v2/protobuf/proto"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/containerd/v2/runtime"
"github.com/containerd/containerd/v2/runtime/v2/runc/options"
"github.com/containerd/log"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/typeurl/v2"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
_ = (api.TasksClient)(&local{})
empty = &ptypes.Empty{}
)
const (
stateTimeout = "io.containerd.timeout.task.state"
)
// Config for the tasks service plugin
type Config struct {
// BlockIOConfigFile specifies the path to blockio configuration file
BlockIOConfigFile string `toml:"blockio_config_file" json:"blockioConfigFile"`
// RdtConfigFile specifies the path to RDT configuration file
RdtConfigFile string `toml:"rdt_config_file" json:"rdtConfigFile"`
}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.ServicePlugin,
ID: services.TasksService,
Requires: tasksServiceRequires,
Config: &Config{},
InitFn: initFunc,
})
timeout.Set(stateTimeout, 2*time.Second)
}
func initFunc(ic *plugin.InitContext) (interface{}, error) {
config := ic.Config.(*Config)
v2r, err := ic.GetByID(plugins.RuntimePluginV2, "task")
if err != nil {
return nil, err
}
m, err := ic.GetSingle(plugins.MetadataPlugin)
if err != nil {
return nil, err
}
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
monitor, err := ic.GetSingle(plugins.TaskMonitorPlugin)
if err != nil {
if !errors.Is(err, plugin.ErrPluginNotFound) {
return nil, err
}
monitor = runtime.NewNoopMonitor()
}
db := m.(*metadata.DB)
l := &local{
containers: metadata.NewContainerStore(db),
store: db.ContentStore(),
publisher: ep.(events.Publisher),
monitor: monitor.(runtime.TaskMonitor),
v2Runtime: v2r.(runtime.PlatformRuntime),
}
v2Tasks, err := l.v2Runtime.Tasks(ic.Context, true)
if err != nil {
return nil, err
}
for _, t := range v2Tasks {
l.monitor.Monitor(t, nil)
}
if err := blockio.SetConfig(config.BlockIOConfigFile); err != nil {
log.G(ic.Context).WithError(err).Errorf("blockio initialization failed")
}
if err := rdt.SetConfig(config.RdtConfigFile); err != nil {
log.G(ic.Context).WithError(err).Errorf("RDT initialization failed")
}
return l, nil
}
type local struct {
containers containers.Store
store content.Store
publisher events.Publisher
monitor runtime.TaskMonitor
v2Runtime runtime.PlatformRuntime
}
func (l *local) Create(ctx context.Context, r *api.CreateTaskRequest, _ ...grpc.CallOption) (*api.CreateTaskResponse, error) {
container, err := l.getContainer(ctx, r.ContainerID)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
checkpointPath, err := getRestorePath(container.Runtime.Name, r.Options)
if err != nil {
return nil, err
}
// jump get checkpointPath from checkpoint image
if checkpointPath == "" && r.Checkpoint != nil {
checkpointPath, err = os.MkdirTemp(os.Getenv("XDG_RUNTIME_DIR"), "ctrd-checkpoint")
if err != nil {
return nil, err
}
if r.Checkpoint.MediaType != images.MediaTypeContainerd1Checkpoint {
return nil, fmt.Errorf("unsupported checkpoint type %q", r.Checkpoint.MediaType)
}
reader, err := l.store.ReaderAt(ctx, ocispec.Descriptor{
MediaType: r.Checkpoint.MediaType,
Digest: digest.Digest(r.Checkpoint.Digest),
Size: r.Checkpoint.Size,
Annotations: r.Checkpoint.Annotations,
})
if err != nil {
return nil, err
}
_, err = archive.Apply(ctx, checkpointPath, content.NewReader(reader))
reader.Close()
if err != nil {
return nil, err
}
}
opts := runtime.CreateOpts{
Spec: container.Spec,
IO: runtime.IO{
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
Terminal: r.Terminal,
},
Checkpoint: checkpointPath,
Runtime: container.Runtime.Name,
RuntimeOptions: container.Runtime.Options,
TaskOptions: r.Options,
SandboxID: container.SandboxID,
}
if r.RuntimePath != "" {
opts.Runtime = r.RuntimePath
}
for _, m := range r.Rootfs {
opts.Rootfs = append(opts.Rootfs, mount.Mount{
Type: m.Type,
Source: m.Source,
Target: m.Target,
Options: m.Options,
})
}
rtime := l.v2Runtime
_, err = rtime.Get(ctx, r.ContainerID)
if err != nil && !errdefs.IsNotFound(err) {
return nil, errdefs.ToGRPC(err)
}
if err == nil {
return nil, errdefs.ToGRPC(fmt.Errorf("task %s: %w", r.ContainerID, errdefs.ErrAlreadyExists))
}
c, err := rtime.Create(ctx, r.ContainerID, opts)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
labels := map[string]string{"runtime": container.Runtime.Name}
if err := l.monitor.Monitor(c, labels); err != nil {
return nil, fmt.Errorf("monitor task: %w", err)
}
pid, err := c.PID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get task pid: %w", err)
}
return &api.CreateTaskResponse{
ContainerID: r.ContainerID,
Pid: pid,
}, nil
}
func (l *local) Start(ctx context.Context, r *api.StartRequest, _ ...grpc.CallOption) (*api.StartResponse, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
p := runtime.Process(t)
if r.ExecID != "" {
if p, err = t.Process(ctx, r.ExecID); err != nil {
return nil, errdefs.ToGRPC(err)
}
}
if err := p.Start(ctx); err != nil {
return nil, errdefs.ToGRPC(err)
}
state, err := p.State(ctx)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.StartResponse{
Pid: state.Pid,
}, nil
}
func (l *local) Delete(ctx context.Context, r *api.DeleteTaskRequest, _ ...grpc.CallOption) (*api.DeleteResponse, error) {
container, err := l.getContainer(ctx, r.ContainerID)
if err != nil {
return nil, err
}
// Get task object
t, err := l.v2Runtime.Get(ctx, container.ID)
if err != nil {
return nil, status.Errorf(codes.NotFound, "task %v not found", container.ID)
}
if err := l.monitor.Stop(t); err != nil {
return nil, err
}
exit, err := l.v2Runtime.Delete(ctx, r.ContainerID)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.DeleteResponse{
ExitStatus: exit.Status,
ExitedAt: protobuf.ToTimestamp(exit.Timestamp),
Pid: exit.Pid,
}, nil
}
func (l *local) DeleteProcess(ctx context.Context, r *api.DeleteProcessRequest, _ ...grpc.CallOption) (*api.DeleteResponse, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
process, err := t.Process(ctx, r.ExecID)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
exit, err := process.Delete(ctx)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.DeleteResponse{
ID: r.ExecID,
ExitStatus: exit.Status,
ExitedAt: protobuf.ToTimestamp(exit.Timestamp),
Pid: exit.Pid,
}, nil
}
func getProcessState(ctx context.Context, p runtime.Process) (*task.Process, error) {
ctx, cancel := timeout.WithContext(ctx, stateTimeout)
defer cancel()
state, err := p.State(ctx)
if err != nil {
if errdefs.IsNotFound(err) || errdefs.IsUnavailable(err) {
return nil, err
}
log.G(ctx).WithError(err).Errorf("get state for %s", p.ID())
}
status := task.Status_UNKNOWN
switch state.Status {
case runtime.CreatedStatus:
status = task.Status_CREATED
case runtime.RunningStatus:
status = task.Status_RUNNING
case runtime.StoppedStatus:
status = task.Status_STOPPED
case runtime.PausedStatus:
status = task.Status_PAUSED
case runtime.PausingStatus:
status = task.Status_PAUSING
default:
log.G(ctx).WithField("status", state.Status).Warn("unknown status")
}
return &task.Process{
ID: p.ID(),
Pid: state.Pid,
Status: status,
Stdin: state.Stdin,
Stdout: state.Stdout,
Stderr: state.Stderr,
Terminal: state.Terminal,
ExitStatus: state.ExitStatus,
ExitedAt: protobuf.ToTimestamp(state.ExitedAt),
}, nil
}
func (l *local) Get(ctx context.Context, r *api.GetRequest, _ ...grpc.CallOption) (*api.GetResponse, error) {
task, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
p := runtime.Process(task)
if r.ExecID != "" {
if p, err = task.Process(ctx, r.ExecID); err != nil {
return nil, errdefs.ToGRPC(err)
}
}
t, err := getProcessState(ctx, p)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.GetResponse{
Process: t,
}, nil
}
func (l *local) List(ctx context.Context, r *api.ListTasksRequest, _ ...grpc.CallOption) (*api.ListTasksResponse, error) {
resp := &api.ListTasksResponse{}
tasks, err := l.v2Runtime.Tasks(ctx, false)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
addTasks(ctx, resp, tasks)
return resp, nil
}
func addTasks(ctx context.Context, r *api.ListTasksResponse, tasks []runtime.Task) {
for _, t := range tasks {
tt, err := getProcessState(ctx, t)
if err != nil {
if !errdefs.IsNotFound(err) { // handle race with deletion
log.G(ctx).WithError(err).WithField("id", t.ID()).Error("converting task to protobuf")
}
continue
}
r.Tasks = append(r.Tasks, tt)
}
}
func (l *local) Pause(ctx context.Context, r *api.PauseTaskRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
err = t.Pause(ctx)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (l *local) Resume(ctx context.Context, r *api.ResumeTaskRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
err = t.Resume(ctx)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (l *local) Kill(ctx context.Context, r *api.KillRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
p := runtime.Process(t)
if r.ExecID != "" {
if p, err = t.Process(ctx, r.ExecID); err != nil {
return nil, errdefs.ToGRPC(err)
}
}
if err := p.Kill(ctx, r.Signal, r.All); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (l *local) ListPids(ctx context.Context, r *api.ListPidsRequest, _ ...grpc.CallOption) (*api.ListPidsResponse, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
processList, err := t.Pids(ctx)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
var processes []*task.ProcessInfo
for _, p := range processList {
pInfo := task.ProcessInfo{
Pid: p.Pid,
}
if p.Info != nil {
a, err := protobuf.MarshalAnyToProto(p.Info)
if err != nil {
return nil, fmt.Errorf("failed to marshal process %d info: %w", p.Pid, err)
}
pInfo.Info = a
}
processes = append(processes, &pInfo)
}
return &api.ListPidsResponse{
Processes: processes,
}, nil
}
func (l *local) Exec(ctx context.Context, r *api.ExecProcessRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
if r.ExecID == "" {
return nil, status.Errorf(codes.InvalidArgument, "exec id cannot be empty")
}
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
if _, err := t.Exec(ctx, r.ExecID, runtime.ExecOpts{
Spec: r.Spec,
IO: runtime.IO{
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
Terminal: r.Terminal,
},
}); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (l *local) ResizePty(ctx context.Context, r *api.ResizePtyRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
p := runtime.Process(t)
if r.ExecID != "" {
if p, err = t.Process(ctx, r.ExecID); err != nil {
return nil, errdefs.ToGRPC(err)
}
}
if err := p.ResizePty(ctx, runtime.ConsoleSize{
Width: r.Width,
Height: r.Height,
}); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (l *local) CloseIO(ctx context.Context, r *api.CloseIORequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
p := runtime.Process(t)
if r.ExecID != "" {
if p, err = t.Process(ctx, r.ExecID); err != nil {
return nil, errdefs.ToGRPC(err)
}
}
if r.Stdin {
if err := p.CloseIO(ctx); err != nil {
return nil, errdefs.ToGRPC(err)
}
}
return empty, nil
}
func (l *local) Checkpoint(ctx context.Context, r *api.CheckpointTaskRequest, _ ...grpc.CallOption) (*api.CheckpointTaskResponse, error) {
container, err := l.getContainer(ctx, r.ContainerID)
if err != nil {
return nil, err
}
t, err := l.getTaskFromContainer(ctx, container)
if err != nil {
return nil, err
}
image, err := getCheckpointPath(container.Runtime.Name, r.Options)
if err != nil {
return nil, err
}
checkpointImageExists := false
if image == "" {
checkpointImageExists = true
image, err = os.MkdirTemp(os.Getenv("XDG_RUNTIME_DIR"), "ctrd-checkpoint")
if err != nil {
return nil, errdefs.ToGRPC(err)
}
defer os.RemoveAll(image)
}
if err := t.Checkpoint(ctx, image, r.Options); err != nil {
return nil, errdefs.ToGRPC(err)
}
// do not commit checkpoint image if checkpoint ImagePath is passed,
// return if checkpointImageExists is false
if !checkpointImageExists {
return &api.CheckpointTaskResponse{}, nil
}
// write checkpoint to the content store
tar := archive.Diff(ctx, "", image)
cp, err := l.writeContent(ctx, images.MediaTypeContainerd1Checkpoint, image, tar)
// close tar first after write
if err := tar.Close(); err != nil {
return nil, err
}
if err != nil {
return nil, err
}
// write the config to the content store
pbany := protobuf.FromAny(container.Spec)
data, err := proto.Marshal(pbany)
if err != nil {
return nil, err
}
spec := bytes.NewReader(data)
specD, err := l.writeContent(ctx, images.MediaTypeContainerd1CheckpointConfig, filepath.Join(image, "spec"), spec)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.CheckpointTaskResponse{
Descriptors: []*types.Descriptor{
cp,
specD,
},
}, nil
}
func (l *local) Update(ctx context.Context, r *api.UpdateTaskRequest, _ ...grpc.CallOption) (*ptypes.Empty, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
if err := t.Update(ctx, r.Resources, r.Annotations); err != nil {
return nil, errdefs.ToGRPC(err)
}
return empty, nil
}
func (l *local) Metrics(ctx context.Context, r *api.MetricsRequest, _ ...grpc.CallOption) (*api.MetricsResponse, error) {
filter, err := filters.ParseAll(r.Filters...)
if err != nil {
return nil, err
}
var resp api.MetricsResponse
tasks, err := l.v2Runtime.Tasks(ctx, false)
if err != nil {
return nil, err
}
getTasksMetrics(ctx, filter, tasks, &resp)
return &resp, nil
}
func (l *local) Wait(ctx context.Context, r *api.WaitRequest, _ ...grpc.CallOption) (*api.WaitResponse, error) {
t, err := l.getTask(ctx, r.ContainerID)
if err != nil {
return nil, err
}
p := runtime.Process(t)
if r.ExecID != "" {
if p, err = t.Process(ctx, r.ExecID); err != nil {
return nil, errdefs.ToGRPC(err)
}
}
exit, err := p.Wait(ctx)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &api.WaitResponse{
ExitStatus: exit.Status,
ExitedAt: protobuf.ToTimestamp(exit.Timestamp),
}, nil
}
func getTasksMetrics(ctx context.Context, filter filters.Filter, tasks []runtime.Task, r *api.MetricsResponse) {
for _, tk := range tasks {
if !filter.Match(filters.AdapterFunc(func(fieldpath []string) (string, bool) {
t := tk
switch fieldpath[0] {
case "id":
return t.ID(), true
case "namespace":
return t.Namespace(), true
case "runtime":
// return t.Info().Runtime, true
}
return "", false
})) {
continue
}
collected := time.Now()
stats, err := tk.Stats(ctx)
if err != nil {
if !errdefs.IsNotFound(err) {
log.G(ctx).WithError(err).Errorf("collecting metrics for %s", tk.ID())
}
continue
}
r.Metrics = append(r.Metrics, &types.Metric{
Timestamp: protobuf.ToTimestamp(collected),
ID: tk.ID(),
Data: stats,
})
}
}
func (l *local) writeContent(ctx context.Context, mediaType, ref string, r io.Reader) (*types.Descriptor, error) {
writer, err := l.store.Writer(ctx, content.WithRef(ref), content.WithDescriptor(ocispec.Descriptor{MediaType: mediaType}))
if err != nil {
return nil, err
}
defer writer.Close()
size, err := io.Copy(writer, r)
if err != nil {
return nil, err
}
if err := writer.Commit(ctx, 0, ""); err != nil && !errdefs.IsAlreadyExists(err) {
return nil, err
}
return &types.Descriptor{
MediaType: mediaType,
Digest: writer.Digest().String(),
Size: size,
Annotations: make(map[string]string),
}, nil
}
func (l *local) getContainer(ctx context.Context, id string) (*containers.Container, error) {
var container containers.Container
container, err := l.containers.Get(ctx, id)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
return &container, nil
}
func (l *local) getTask(ctx context.Context, id string) (runtime.Task, error) {
container, err := l.getContainer(ctx, id)
if err != nil {
return nil, err
}
return l.getTaskFromContainer(ctx, container)
}
func (l *local) getTaskFromContainer(ctx context.Context, container *containers.Container) (runtime.Task, error) {
t, err := l.v2Runtime.Get(ctx, container.ID)
if err != nil {
return nil, status.Errorf(codes.NotFound, "task %v not found", container.ID)
}
return t, nil
}
// getCheckpointPath only suitable for runc runtime now
func getCheckpointPath(runtime string, option *ptypes.Any) (string, error) {
if option == nil {
return "", nil
}
var checkpointPath string
v, err := typeurl.UnmarshalAny(option)
if err != nil {
return "", err
}
opts, ok := v.(*options.CheckpointOptions)
if !ok {
return "", fmt.Errorf("invalid task checkpoint option for %s", runtime)
}
checkpointPath = opts.ImagePath
return checkpointPath, nil
}
// getRestorePath only suitable for runc runtime now
func getRestorePath(runtime string, option *ptypes.Any) (string, error) {
if option == nil {
return "", nil
}
var restorePath string
v, err := typeurl.UnmarshalAny(option)
if err != nil {
return "", err
}
opts, ok := v.(*options.Options)
if !ok {
return "", fmt.Errorf("invalid task create option for %s", runtime)
}
restorePath = opts.CriuImagePath
return restorePath, nil
}

View File

@@ -0,0 +1,30 @@
//go:build darwin
/*
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 tasks
import (
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
)
var tasksServiceRequires = []plugin.Type{
plugins.RuntimePluginV2,
plugins.MetadataPlugin,
plugins.TaskMonitorPlugin,
}

View File

@@ -0,0 +1,29 @@
/*
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 tasks
import (
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
)
var tasksServiceRequires = []plugin.Type{
plugins.EventPlugin,
plugins.RuntimePluginV2,
plugins.MetadataPlugin,
plugins.TaskMonitorPlugin,
}

View File

@@ -0,0 +1,31 @@
//go:build !windows && !freebsd && !darwin
/*
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 tasks
import (
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
)
var tasksServiceRequires = []plugin.Type{
plugins.EventPlugin,
plugins.RuntimePluginV2,
plugins.MetadataPlugin,
plugins.TaskMonitorPlugin,
}

View File

@@ -0,0 +1,29 @@
/*
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 tasks
import (
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
)
var tasksServiceRequires = []plugin.Type{
plugins.EventPlugin,
plugins.RuntimePluginV2,
plugins.MetadataPlugin,
plugins.TaskMonitorPlugin,
}

View File

@@ -0,0 +1,128 @@
/*
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 tasks
import (
"context"
api "github.com/containerd/containerd/v2/api/services/tasks/v1"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/plugins/services"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
var (
_ = (api.TasksServer)(&service{})
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "tasks",
Requires: []plugin.Type{
plugins.ServicePlugin,
},
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
i, err := ic.GetByID(plugins.ServicePlugin, services.TasksService)
if err != nil {
return nil, err
}
return &service{local: i.(api.TasksClient)}, nil
},
})
}
type service struct {
local api.TasksClient
api.UnimplementedTasksServer
}
func (s *service) Register(server *grpc.Server) error {
api.RegisterTasksServer(server, s)
return nil
}
func (s *service) Create(ctx context.Context, r *api.CreateTaskRequest) (*api.CreateTaskResponse, error) {
return s.local.Create(ctx, r)
}
func (s *service) Start(ctx context.Context, r *api.StartRequest) (*api.StartResponse, error) {
return s.local.Start(ctx, r)
}
func (s *service) Delete(ctx context.Context, r *api.DeleteTaskRequest) (*api.DeleteResponse, error) {
return s.local.Delete(ctx, r)
}
func (s *service) DeleteProcess(ctx context.Context, r *api.DeleteProcessRequest) (*api.DeleteResponse, error) {
return s.local.DeleteProcess(ctx, r)
}
func (s *service) Get(ctx context.Context, r *api.GetRequest) (*api.GetResponse, error) {
return s.local.Get(ctx, r)
}
func (s *service) List(ctx context.Context, r *api.ListTasksRequest) (*api.ListTasksResponse, error) {
return s.local.List(ctx, r)
}
func (s *service) Pause(ctx context.Context, r *api.PauseTaskRequest) (*ptypes.Empty, error) {
return s.local.Pause(ctx, r)
}
func (s *service) Resume(ctx context.Context, r *api.ResumeTaskRequest) (*ptypes.Empty, error) {
return s.local.Resume(ctx, r)
}
func (s *service) Kill(ctx context.Context, r *api.KillRequest) (*ptypes.Empty, error) {
return s.local.Kill(ctx, r)
}
func (s *service) ListPids(ctx context.Context, r *api.ListPidsRequest) (*api.ListPidsResponse, error) {
return s.local.ListPids(ctx, r)
}
func (s *service) Exec(ctx context.Context, r *api.ExecProcessRequest) (*ptypes.Empty, error) {
return s.local.Exec(ctx, r)
}
func (s *service) ResizePty(ctx context.Context, r *api.ResizePtyRequest) (*ptypes.Empty, error) {
return s.local.ResizePty(ctx, r)
}
func (s *service) CloseIO(ctx context.Context, r *api.CloseIORequest) (*ptypes.Empty, error) {
return s.local.CloseIO(ctx, r)
}
func (s *service) Checkpoint(ctx context.Context, r *api.CheckpointTaskRequest) (*api.CheckpointTaskResponse, error) {
return s.local.Checkpoint(ctx, r)
}
func (s *service) Update(ctx context.Context, r *api.UpdateTaskRequest) (*ptypes.Empty, error) {
return s.local.Update(ctx, r)
}
func (s *service) Metrics(ctx context.Context, r *api.MetricsRequest) (*api.MetricsResponse, error) {
return s.local.Metrics(ctx, r)
}
func (s *service) Wait(ctx context.Context, r *api.WaitRequest) (*api.WaitResponse, error) {
return s.local.Wait(ctx, r)
}

View File

@@ -0,0 +1,162 @@
/*
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 transfer
import (
"context"
transferapi "github.com/containerd/containerd/v2/api/services/transfer/v1"
"github.com/containerd/containerd/v2/api/types"
transferTypes "github.com/containerd/containerd/v2/api/types/transfer"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/oci"
"github.com/containerd/containerd/v2/pkg/streaming"
"github.com/containerd/containerd/v2/pkg/transfer"
tplugins "github.com/containerd/containerd/v2/pkg/transfer/plugins"
"github.com/containerd/containerd/v2/plugins"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
"github.com/containerd/log"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/typeurl/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "transfer",
Requires: []plugin.Type{
plugins.TransferPlugin,
plugins.StreamingPlugin,
},
InitFn: newService,
})
}
type service struct {
transferrers []transfer.Transferrer
streamManager streaming.StreamManager
transferapi.UnimplementedTransferServer
}
func newService(ic *plugin.InitContext) (interface{}, error) {
sps, err := ic.GetByType(plugins.TransferPlugin)
if err != nil {
return nil, err
}
// TODO: how to determine order?
t := make([]transfer.Transferrer, 0, len(sps))
for _, p := range sps {
t = append(t, p.(transfer.Transferrer))
}
sp, err := ic.GetByID(plugins.StreamingPlugin, "manager")
if err != nil {
return nil, err
}
return &service{
transferrers: t,
streamManager: sp.(streaming.StreamManager),
}, nil
}
func (s *service) Register(gs *grpc.Server) error {
transferapi.RegisterTransferServer(gs, s)
return nil
}
func (s *service) Transfer(ctx context.Context, req *transferapi.TransferRequest) (*emptypb.Empty, error) {
var transferOpts []transfer.Opt
if req.Options != nil {
if req.Options.ProgressStream != "" {
stream, err := s.streamManager.Get(ctx, req.Options.ProgressStream)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
defer stream.Close()
pf := func(p transfer.Progress) {
var descp *types.Descriptor
if p.Desc != nil {
descp = oci.DescriptorToProto(*p.Desc)
}
progress, err := typeurl.MarshalAny(&transferTypes.Progress{
Event: p.Event,
Name: p.Name,
Parents: p.Parents,
Progress: p.Progress,
Total: p.Total,
Desc: descp,
})
if err != nil {
log.G(ctx).WithError(err).Warnf("event could not be marshaled: %v/%v", p.Event, p.Name)
return
}
if err := stream.Send(progress); err != nil {
log.G(ctx).WithError(err).Warnf("event not sent: %v/%v", p.Event, p.Name)
return
}
}
transferOpts = append(transferOpts, transfer.WithProgress(pf))
}
}
src, err := s.convertAny(ctx, req.Source)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
dst, err := s.convertAny(ctx, req.Destination)
if err != nil {
return nil, errdefs.ToGRPC(err)
}
for _, t := range s.transferrers {
if err := t.Transfer(ctx, src, dst, transferOpts...); err == nil {
return &ptypes.Empty{}, nil
} else if !errdefs.IsNotImplemented(err) {
return nil, errdefs.ToGRPC(err)
}
}
return nil, status.Errorf(codes.Unimplemented, "method Transfer not implemented for %s to %s", req.Source.GetTypeUrl(), req.Destination.GetTypeUrl())
}
func (s *service) convertAny(ctx context.Context, a typeurl.Any) (interface{}, error) {
obj, err := tplugins.ResolveType(a)
if err != nil {
if errdefs.IsNotFound(err) {
return typeurl.UnmarshalAny(a)
}
return nil, err
}
switch v := obj.(type) {
case streamUnmarshaler:
err = v.UnmarshalAny(ctx, s.streamManager, a)
return obj, err
default:
log.G(ctx).Debug("unmarshling to..")
err = typeurl.UnmarshalTo(a, obj)
return obj, err
}
}
type streamUnmarshaler interface {
UnmarshalAny(context.Context, streaming.StreamGetter, typeurl.Any) error
}

View File

@@ -0,0 +1,59 @@
/*
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 version
import (
"context"
api "github.com/containerd/containerd/v2/api/services/version/v1"
"github.com/containerd/containerd/v2/plugins"
ptypes "github.com/containerd/containerd/v2/protobuf/types"
ctrdversion "github.com/containerd/containerd/v2/version"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"google.golang.org/grpc"
)
var _ api.VersionServer = &service{}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.GRPCPlugin,
ID: "version",
InitFn: initFunc,
})
}
func initFunc(ic *plugin.InitContext) (interface{}, error) {
return &service{}, nil
}
type service struct {
api.UnimplementedVersionServer
}
func (s *service) Register(server *grpc.Server) error {
api.RegisterVersionServer(server, s)
return nil
}
func (s *service) Version(ctx context.Context, _ *ptypes.Empty) (*api.VersionResponse, error) {
return &api.VersionResponse{
Version: ctrdversion.Version,
Revision: ctrdversion.Revision,
}, nil
}

View File

@@ -0,0 +1,85 @@
/*
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 warning
import (
"context"
"sync"
"time"
"github.com/containerd/log"
deprecation "github.com/containerd/containerd/v2/pkg/deprecation"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
)
type Service interface {
Emit(context.Context, deprecation.Warning)
Warnings() []Warning
}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.WarningPlugin,
ID: plugins.DeprecationsPlugin,
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
return &service{warnings: make(map[deprecation.Warning]time.Time)}, nil
},
})
}
type Warning struct {
ID deprecation.Warning
LastOccurrence time.Time
Message string
}
var _ Service = (*service)(nil)
type service struct {
warnings map[deprecation.Warning]time.Time
m sync.RWMutex
}
func (s *service) Emit(ctx context.Context, warning deprecation.Warning) {
if !deprecation.Valid(warning) {
log.G(ctx).WithField("warningID", string(warning)).Warn("invalid deprecation warning")
return
}
s.m.Lock()
defer s.m.Unlock()
s.warnings[warning] = time.Now()
}
func (s *service) Warnings() []Warning {
s.m.RLock()
defer s.m.RUnlock()
var warnings []Warning
for k, v := range s.warnings {
msg, ok := deprecation.Message(k)
if !ok {
continue
}
warnings = append(warnings, Warning{
ID: k,
LastOccurrence: v,
Message: msg,
})
}
return warnings
}