diff --git a/.github/linters/urunc-dict.txt b/.github/linters/urunc-dict.txt index b4c2495c..bf838cdf 100644 --- a/.github/linters/urunc-dict.txt +++ b/.github/linters/urunc-dict.txt @@ -88,6 +88,7 @@ TUNTAP Timestamping Tmpfs Tuntap +TTRPC UNBINDABLE URUNC Unikernel @@ -127,8 +128,11 @@ clis cmainas cmdline cntr +containerdshim containerd containerd's +containersapi +contentapi cpio creack crictl @@ -150,6 +154,7 @@ elevateprivileges elfloader endmacro epoll +errdefs etest etesting etfs @@ -177,6 +182,7 @@ httpreply iface ifaces ifname +imagesapi initpipe initrds inlinehilite @@ -198,6 +204,7 @@ kwds lazytime lenag lenpg +leasesapi levarage libc libcontainer @@ -296,6 +303,7 @@ setgroup settime sgid sharedfs +snapshotsapi sigaction sigreturn sigstr @@ -318,6 +326,7 @@ tmpl tomlq traefik triger +ttrpc twemoji uidmap ukernel diff --git a/Makefile b/Makefile index dbf4c498..dd694c7b 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,8 @@ URUNC_SRC += $(wildcard $(CURDIR)/pkg/unikontainers/types/*.go) URUNC_SRC += $(wildcard $(CURDIR)/pkg/unikontainers/initrd/*.go) URUNC_SRC += $(wildcard $(CURDIR)/pkg/network/*.go) SHIM_SRC := $(wildcard $(CURDIR)/cmd/containerd-shim-urunc-v2/*.go) +SHIM_SRC += $(wildcard $(CURDIR)/pkg/containerd-shim/*.go) +SHIM_SRC += $(wildcard $(CURDIR)/pkg/containerd-shim/containerd/*.go) #? CNTR_TOOL Tool to run the linter container (default: docker) CNTR_TOOL ?= docker diff --git a/cmd/containerd-shim-urunc-v2/main.go b/cmd/containerd-shim-urunc-v2/main.go index ec7c6bc5..dac1451c 100644 --- a/cmd/containerd-shim-urunc-v2/main.go +++ b/cmd/containerd-shim-urunc-v2/main.go @@ -18,8 +18,8 @@ import ( "context" "github.com/containerd/containerd/runtime/v2/runc/manager" - _ "github.com/containerd/containerd/runtime/v2/runc/task/plugin" "github.com/containerd/containerd/runtime/v2/shim" + _ "github.com/urunc-dev/urunc/pkg/containerd-shim" ) func main() { diff --git a/go.mod b/go.mod index 2da9e6ad..82252800 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/cavaliergopher/cpio v1.0.1 github.com/containerd/containerd v1.7.30 + github.com/containerd/containerd/api v1.10.0 + github.com/containerd/ttrpc v1.2.7 github.com/creack/pty v1.1.24 github.com/elastic/go-seccomp-bpf v1.6.0 github.com/hashicorp/go-version v1.9.0 @@ -26,6 +28,7 @@ require ( github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netns v0.0.5 golang.org/x/sys v0.43.0 + google.golang.org/grpc v1.79.3 k8s.io/cri-api v0.35.4 ) @@ -36,7 +39,6 @@ require ( github.com/cilium/ebpf v0.20.0 // indirect github.com/containerd/cgroups/v3 v3.1.0 // indirect github.com/containerd/console v1.0.5 // indirect - github.com/containerd/containerd/api v1.10.0 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -44,7 +46,6 @@ require ( github.com/containerd/go-runc v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -79,7 +80,6 @@ require ( golang.org/x/tools v0.41.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/containerd-shim/containerd/session.go b/pkg/containerd-shim/containerd/session.go new file mode 100644 index 00000000..996c0be8 --- /dev/null +++ b/pkg/containerd-shim/containerd/session.go @@ -0,0 +1,171 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// 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 containerd + +import ( + "context" + "fmt" + "time" + + containersapi "github.com/containerd/containerd/api/services/containers/v1" + contentapi "github.com/containerd/containerd/api/services/content/v1" + imagesapi "github.com/containerd/containerd/api/services/images/v1" + leasesapi "github.com/containerd/containerd/api/services/leases/v1" + snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" + "github.com/containerd/containerd/defaults" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/pkg/dialer" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials/insecure" +) + +const defaultConnectTimeout = 10 * time.Second + +type Session struct { + conn *grpc.ClientConn + + namespace string + containerID string + container *containersapi.Container +} + +// OpenSession opens a containerd session and loads the task container metadata. +func OpenSession(ctx context.Context, address, containerID string) (*Session, error) { + if address == "" { + return nil, fmt.Errorf("containerd address is empty") + } + if containerID == "" { + return nil, fmt.Errorf("container id is empty") + } + + namespace, err := namespaces.NamespaceRequired(ctx) + if err != nil { + return nil, err + } + + backoffConfig := backoff.DefaultConfig + backoffConfig.MaxDelay = 3 * time.Second + dialOptions := []grpc.DialOption{ + grpc.WithBlock(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.FailOnNonTempDialError(true), + grpc.WithConnectParams(grpc.ConnectParams{Backoff: backoffConfig}), + grpc.WithContextDialer(dialer.ContextDialer), + grpc.WithReturnConnectionError(), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize), + grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize), + ), + } + + dialCtx, cancel := context.WithTimeout(ctx, defaultConnectTimeout) + defer cancel() + + conn, err := grpc.DialContext(dialCtx, dialer.DialAddress(address), dialOptions...) + if err != nil { + return nil, fmt.Errorf("dial containerd at %q: %w", address, err) + } + + session := &Session{ + conn: conn, + namespace: namespace, + containerID: containerID, + } + + container, err := loadContainer(ctx, namespace, containerID, session.containersClient()) + if err != nil { + if closeErr := conn.Close(); closeErr != nil { + return nil, fmt.Errorf("loadContainer failed: %w; close containerd connection: %v", err, closeErr) + } + return nil, fmt.Errorf("loadContainer failed: %w", err) + } + session.container = container + + return session, nil +} + +func (s *Session) Close() error { + if s == nil || s.conn == nil { + return nil + } + return s.conn.Close() +} + +func (s *Session) GetNamespace() string { + return s.namespace +} + +func (s *Session) GetContainerID() string { + return s.containerID +} + +func (s *Session) GetContainer() *containersapi.Container { + return s.container +} + +func loadContainer(ctx context.Context, namespace, containerID string, client containersapi.ContainersClient) (*containersapi.Container, error) { + resp, err := client.Get(withNamespace(ctx, namespace), &containersapi.GetContainerRequest{ + ID: containerID, + }) + if err != nil { + return nil, fmt.Errorf("get container %q: %w", containerID, containerdErr(err)) + } + container := resp.GetContainer() + if container == nil { + return nil, fmt.Errorf("get container %q: response missing container", containerID) + } + + return container, nil +} + +func withNamespace(ctx context.Context, namespace string) context.Context { + if ctx == nil { + ctx = context.Background() + } + return namespaces.WithNamespace(ctx, namespace) +} + +func containerdErr(err error) error { + if err == nil { + return nil + } + return errdefs.FromGRPC(err) +} + +func (s *Session) containersClient() containersapi.ContainersClient { + return containersapi.NewContainersClient(s.conn) +} + +//nolint:unused // Used by follow-up feature-specific access constructors. +func (s *Session) imagesClient() imagesapi.ImagesClient { + return imagesapi.NewImagesClient(s.conn) +} + +//nolint:unused // Used by follow-up feature-specific access constructors. +func (s *Session) contentClient() contentapi.ContentClient { + return contentapi.NewContentClient(s.conn) +} + +//nolint:unused // Used by follow-up feature-specific access constructors. +func (s *Session) snapshotsClient() snapshotsapi.SnapshotsClient { + return snapshotsapi.NewSnapshotsClient(s.conn) +} + +//nolint:unused // Used by follow-up feature-specific access constructors. +func (s *Session) leasesClient() leasesapi.LeasesClient { + return leasesapi.NewLeasesClient(s.conn) +} diff --git a/pkg/containerd-shim/task_plugin.go b/pkg/containerd-shim/task_plugin.go new file mode 100644 index 00000000..85226f38 --- /dev/null +++ b/pkg/containerd-shim/task_plugin.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// 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 containerdshim + +import ( + "github.com/containerd/containerd/pkg/shutdown" + "github.com/containerd/containerd/plugin" + runcTask "github.com/containerd/containerd/runtime/v2/runc/task" + "github.com/containerd/containerd/runtime/v2/shim" +) + +func init() { + plugin.Register(&plugin.Registration{ + Type: plugin.TTRPCPlugin, + ID: "task", + Requires: []plugin.Type{ + plugin.EventPlugin, + plugin.InternalPlugin, + }, + InitFn: func(ic *plugin.InitContext) (interface{}, error) { + pp, err := ic.GetByID(plugin.EventPlugin, "publisher") + if err != nil { + return nil, err + } + + ss, err := ic.GetByID(plugin.InternalPlugin, "shutdown") + if err != nil { + return nil, err + } + + inner, err := runcTask.NewTaskService(ic.Context, pp.(shim.Publisher), ss.(shutdown.Service)) + if err != nil { + return nil, err + } + + return &taskService{ + TaskService: inner, + containerdAddress: ic.Address, + }, nil + }, + }) +} diff --git a/pkg/containerd-shim/task_service.go b/pkg/containerd-shim/task_service.go new file mode 100644 index 00000000..70fb44a7 --- /dev/null +++ b/pkg/containerd-shim/task_service.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// 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 containerdshim + +import ( + "context" + + taskAPI "github.com/containerd/containerd/api/runtime/task/v2" + "github.com/containerd/ttrpc" +) + +// taskService is urunc's shim-side wrapper around containerd's runc task +// service. It currently forwards calls to the wrapped service while keeping a +// urunc-owned place for task-level feature wiring. +type taskService struct { + taskAPI.TaskService + + containerdAddress string +} + +func (s *taskService) Create(ctx context.Context, r *taskAPI.CreateTaskRequest) (*taskAPI.CreateTaskResponse, error) { + return s.TaskService.Create(ctx, r) +} + +func (s *taskService) Delete(ctx context.Context, r *taskAPI.DeleteRequest) (*taskAPI.DeleteResponse, error) { + return s.TaskService.Delete(ctx, r) +} + +func (s *taskService) RegisterTTRPC(server *ttrpc.Server) error { + taskAPI.RegisterTaskService(server, s) + return nil +}