Merge pull request #52898 from vvoland/work-containerd-process

daemon: Add embedded containerd mode
This commit is contained in:
Paweł Gronowski
2026-07-24 20:32:59 +02:00
committed by GitHub
351 changed files with 69657 additions and 36 deletions

View File

@@ -230,6 +230,9 @@ jobs:
if [[ "$MODE" == *"nftables"* ]]; then
echo "DOCKER_FIREWALL_BACKEND=nftables" >> $GITHUB_ENV
fi
if [[ "$MODE" == *"embedded"* ]]; then
echo "TEST_INTEGRATION_CONTAINERD_EMBEDDED=1" >> $GITHUB_ENV
fi
echo "CACHE_DEV_SCOPE=${CACHE_DEV_SCOPE}" >> $GITHUB_ENV
-
name: Set up AppArmor for RootlessKit

View File

@@ -20,6 +20,10 @@ on:
required: false
type: boolean
default: false
containerd:
required: false
type: string
default: "socket"
secrets:
CODECOV_TOKEN:
required: false
@@ -373,6 +377,7 @@ jobs:
id: start-daemon
env:
INPUT_STORAGE: ${{ inputs.storage }}
INPUT_CONTAINERD: ${{ inputs.containerd }}
MATRIX_RUNTIME: ${{ matrix.runtime }}
run: |
Write-Host "Creating service"
@@ -384,7 +389,9 @@ jobs:
"--pidfile=$env:TEMP\docker.pid", `
"--register-service"
)
If ($env:MATRIX_RUNTIME -eq "containerd") {
If ($env:INPUT_CONTAINERD -eq "embedded") {
$args += "--feature=embedded-containerd"
} ElseIf ($env:MATRIX_RUNTIME -eq "containerd") {
$args += "--default-runtime=io.containerd.runhcs.v1"
echo "DOCKER_WINDOWS_CONTAINERD_RUNTIME=1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append
}
@@ -401,6 +408,10 @@ jobs:
$dockerEnviron += @("TEST_INTEGRATION_USE_GRAPHDRIVER=1")
echo "TEST_INTEGRATION_USE_GRAPHDRIVER=1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append
}
If ($env:INPUT_CONTAINERD -eq "embedded") {
$dockerEnviron += @("TEST_INTEGRATION_CONTAINERD_EMBEDDED=1")
echo "TEST_INTEGRATION_CONTAINERD_EMBEDDED=1" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append
}
New-ItemProperty -Name "Environment" -Path "HKLM:\SYSTEM\CurrentControlSet\Services\docker" -PropertyType MultiString -Value $dockerEnviron
Write-Host "Starting service"
Start-Service -Name docker
@@ -408,6 +419,7 @@ jobs:
-
name: Waiting for test daemon to start
env:
INPUT_CONTAINERD: ${{ inputs.containerd }}
MATRIX_RUNTIME: ${{ matrix.runtime }}
DOCKER_HOST: npipe:////./pipe/docker_engine
run: |
@@ -428,7 +440,7 @@ jobs:
Start-Sleep -Seconds 1
}
Write-Host "Test daemon started and replied!"
If ($env:MATRIX_RUNTIME -eq "containerd") {
If ($env:INPUT_CONTAINERD -ne "embedded" -and $env:MATRIX_RUNTIME -eq "containerd") {
$containerdProcesses = Get-Process -Name containerd -ErrorAction:SilentlyContinue
If (-not $containerdProcesses) {
Throw "containerd process is not running"

View File

@@ -92,7 +92,7 @@ jobs:
- arch: amd64
storage: snapshotter
runnerSuffix: ""
extraModes: '["rootless", "systemd", "rootless-systemd", "iptables+firewalld", "nftables", "nftables+firewalld"]'
extraModes: '["rootless", "systemd", "rootless-systemd", "iptables+firewalld", "nftables", "nftables+firewalld", "embedded"]'
- arch: amd64
storage: graphdriver
runnerSuffix: ""

View File

@@ -24,12 +24,17 @@ jobs:
strategy:
fail-fast: false
matrix:
storage:
- graphdriver
- snapshotter
include:
- storage: graphdriver
containerd: socket
- storage: snapshotter
containerd: socket
- storage: snapshotter
containerd: embedded
with:
os: windows-2022
storage: ${{ matrix.storage }}
containerd: ${{ matrix.containerd }}
send_coverage: true
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -29,12 +29,17 @@ jobs:
strategy:
fail-fast: false
matrix:
storage:
- graphdriver
- snapshotter
include:
- storage: graphdriver
containerd: socket
- storage: snapshotter
containerd: socket
- storage: snapshotter
containerd: embedded
with:
os: windows-2025
storage: ${{ matrix.storage }}
containerd: ${{ matrix.containerd }}
send_coverage: false
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -30,7 +30,7 @@ import (
"github.com/moby/moby/v2/daemon/config"
buildkit "github.com/moby/moby/v2/daemon/internal/builder-next"
"github.com/moby/moby/v2/daemon/internal/builder-next/exporter"
"github.com/moby/moby/v2/daemon/internal/libcontainerd/supervisor"
"github.com/moby/moby/v2/daemon/internal/containerd/server/supervisor"
"github.com/moby/moby/v2/daemon/internal/otelutil"
"github.com/moby/moby/v2/daemon/internal/rootless"
"github.com/moby/moby/v2/daemon/listeners"
@@ -76,6 +76,10 @@ type daemonCLI struct {
d *daemon.Daemon
authzMiddleware *authorization.Middleware // authzMiddleware enables to dynamically reload the authorization plugins
// containerdDialer is the in-memory dialer for the embedded containerd. It
// is set in embedded mode and nil otherwise. See [daemon.ContainerdDialer].
containerdDialer daemon.ContainerdDialer
stopOnce sync.Once
apiShutdown chan struct{}
apiTLSConfig *tls.Config
@@ -294,7 +298,7 @@ func (cli *daemonCLI) start(ctx context.Context) (retErr error) {
}
cli.authzMiddleware = authz
d, err := daemon.NewDaemon(ctx, cli.Config, pluginStore, cli.authzMiddleware)
d, err := daemon.NewDaemon(ctx, cli.Config, pluginStore, cli.authzMiddleware, cli.containerdDialer)
if err != nil {
return errors.Wrap(err, "failed to start daemon")
}
@@ -322,7 +326,7 @@ func (cli *daemonCLI) start(ctx context.Context) (retErr error) {
// initialized the cluster.
d.RestartSwarmContainers()
b, shutdownBuildKit, err := initBuildkit(ctx, d, cdiCache)
b, shutdownBuildKit, err := initBuildkit(ctx, d, cdiCache, cli.containerdDialer)
if err != nil {
return fmt.Errorf("error initializing buildkit: %w", err)
}
@@ -444,7 +448,7 @@ func setOTLPProtoDefault() {
}
}
func initBuildkit(ctx context.Context, d *daemon.Daemon, cdiCache *cdi.Cache) (_ builderOptions, closeFn func(), _ error) {
func initBuildkit(ctx context.Context, d *daemon.Daemon, cdiCache *cdi.Cache, containerdDialer daemon.ContainerdDialer) (_ builderOptions, closeFn func(), _ error) {
log.G(ctx).Info("Initializing buildkit")
closeFn = func() {}
@@ -477,6 +481,7 @@ func initBuildkit(ctx context.Context, d *daemon.Daemon, cdiCache *cdi.Cache) (_
UseSnapshotter: d.UsesSnapshotter(),
Snapshotter: d.ImageService().StorageDriver(),
ContainerdAddress: cfg.ContainerdAddr,
ContainerdDialer: containerdDialer,
ContainerdNamespace: cfg.ContainerdNamespace,
HyperVIsolation: d.DefaultIsolation().IsHyperV(),
Callbacks: exporter.BuildkitCallbacks{
@@ -1159,7 +1164,11 @@ func (cli *daemonCLI) initializeContainerd(ctx context.Context) (func(time.Durat
return nil, errors.Wrap(err, "failed to generate containerd options")
}
r, err := supervisor.Start(ctx, filepath.Join(cli.Config.Root, "containerd"), filepath.Join(cli.Config.ExecRoot, "containerd"), opts...)
rootDir, err := containerdRootDir(ctx, cli.Config)
if err != nil {
return nil, err
}
r, err := supervisor.Start(ctx, rootDir, filepath.Join(cli.Config.ExecRoot, "containerd"), opts...)
if err != nil {
return nil, errors.Wrap(err, "failed to start containerd")
}
@@ -1169,6 +1178,24 @@ func (cli *daemonCLI) initializeContainerd(ctx context.Context) (func(time.Durat
return r.WaitTimeout, nil
}
// containerdRootEnv is a temporary testing override for reusing a standalone
// containerd data root with embedded or supervised containerd.
const containerdRootEnv = "DOCKER_CONTAINERD_ROOT"
func containerdRootDir(ctx context.Context, cfg *config.Config) (string, error) {
if root := os.Getenv(containerdRootEnv); root != "" {
if !filepath.IsAbs(root) {
return "", fmt.Errorf("%s must be an absolute path: %q", containerdRootEnv, root)
}
log.G(ctx).WithFields(log.Fields{
"environment-variable": containerdRootEnv,
"root": root,
}).Warn("overriding containerd root from environment")
return root, nil
}
return filepath.Join(cfg.Root, "containerd", "daemon"), nil
}
// cdiEnabled returns true if CDI feature wasn't explicitly disabled via
// features.
func cdiEnabled(conf *config.Config) bool {

View File

@@ -0,0 +1,36 @@
//go:build !no_embedded_containerd
package command
import (
"context"
"path/filepath"
"time"
"github.com/containerd/log"
"github.com/moby/moby/v2/daemon/internal/containerd/server/embedded"
"github.com/pkg/errors"
)
// initEmbeddedContainerd starts containerd inside the daemon process and points
// the containerd clients at it. It is selected by the "embedded-containerd"
// feature.
func (cli *daemonCLI) initEmbeddedContainerd(ctx context.Context) (func(time.Duration) error, error) {
rootDir, err := containerdRootDir(ctx, cli.Config)
if err != nil {
return nil, err
}
log.G(ctx).Warn("Running with experimental embedded-containerd mode")
d, err := embedded.Start(
ctx,
rootDir,
filepath.Join(cli.Config.ExecRoot, "containerd"),
)
if err != nil {
return nil, errors.Wrap(err, "failed to start embedded containerd")
}
cli.Config.ContainerdAddr = d.Address()
cli.containerdDialer = d.Dial
return d.WaitTimeout, nil
}

View File

@@ -0,0 +1,13 @@
//go:build no_embedded_containerd
package command
import (
"context"
"errors"
"time"
)
func (cli *daemonCLI) initEmbeddedContainerd(context.Context) (func(time.Duration) error, error) {
return nil, errors.New("embedded containerd is not supported in this build")
}

View File

@@ -1,6 +1,7 @@
package command
import (
"path/filepath"
"runtime"
"testing"
@@ -181,6 +182,24 @@ func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) {
assert.Check(t, is.Equal("syslog", loadedConfig.LogConfig.Type))
}
func TestContainerdRootDir(t *testing.T) {
cfg := config.Config{CommonConfig: config.CommonConfig{Root: t.TempDir()}}
t.Setenv(containerdRootEnv, "")
root, err := containerdRootDir(t.Context(), &cfg)
assert.NilError(t, err)
assert.Check(t, is.Equal(root, filepath.Join(cfg.Root, "containerd", "daemon")))
override := t.TempDir()
t.Setenv(containerdRootEnv, override)
root, err = containerdRootDir(t.Context(), &cfg)
assert.NilError(t, err)
assert.Check(t, is.Equal(root, override))
t.Setenv(containerdRootEnv, filepath.Join("relative", "containerd"))
_, err = containerdRootDir(t.Context(), &cfg)
assert.Check(t, is.ErrorContains(err, "must be an absolute path"))
}
func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) {
content := `{
"registry-mirrors": ["https://mirrors.example.com"],

View File

@@ -117,6 +117,11 @@ func newCgroupParent(cfg *config.Config) string {
}
func (cli *daemonCLI) initContainerd(ctx context.Context) (func(time.Duration) error, error) {
// Check embedded-containerd first so daemon.json can opt in even when
// packaged service units pass a default --containerd socket.
if cli.Config.Features["embedded-containerd"] {
return cli.initEmbeddedContainerd(ctx)
}
if cli.Config.ContainerdAddr != "" {
// use system containerd at the given address.
return nil, nil

View File

@@ -110,6 +110,11 @@ func newCgroupParent(*config.Config) string {
}
func (cli *daemonCLI) initContainerd(ctx context.Context) (func(time.Duration) error, error) {
// Check embedded-containerd first so daemon.json can opt in even when
// packaged service units pass a default --containerd socket.
if cli.Config.Features["embedded-containerd"] {
return cli.initEmbeddedContainerd(ctx)
}
if cli.Config.ContainerdAddr != "" {
return nil, nil
}

View File

@@ -119,6 +119,10 @@ var skipDuplicates = map[string]bool{
"runtimes": true,
}
// errEmbeddedContainerdWithCRI is returned when both the "embedded-containerd"
// feature and CRI support are enabled.
var errEmbeddedContainerdWithCRI = errors.New(`conflicting options: cannot use the "embedded-containerd" feature and CRI support (--cri-containerd) at the same time`)
// migratedNamedConfig describes legacy configuration file keys that have been migrated
// from simple entries equivalent to command line flags, to a named option.
//
@@ -710,6 +714,10 @@ func Validate(config *Config) error {
return err
}
if config.Features["embedded-containerd"] && config.CriContainerd {
return errEmbeddedContainerdWithCRI
}
// validate DNSSearch
for _, dnsSearch := range config.DNSSearch {
if _, err := opts.ValidateDNSSearch(dnsSearch); err != nil {

View File

@@ -234,6 +234,16 @@ func TestValidateConfigurationErrors(t *testing.T) {
},
expectedErr: "bad attribute format: one",
},
{
name: "embedded-containerd with cri-containerd",
config: &Config{
CommonConfig: CommonConfig{
Features: map[string]bool{"embedded-containerd": true},
CriContainerd: true,
},
},
expectedErr: errEmbeddedContainerdWithCRI.Error(),
},
{
name: "multiple label without value",
config: &Config{
@@ -459,6 +469,17 @@ func TestValidateConfigurationErrors(t *testing.T) {
}
}
func TestValidateEmbeddedContainerdAllowsExplicitAddr(t *testing.T) {
cfg := &Config{
CommonConfig: CommonConfig{
Features: map[string]bool{"embedded-containerd": true},
ContainerdAddr: "/run/containerd/containerd.sock",
},
}
assert.NilError(t, Validate(cfg))
}
func withForceOverwrite(fieldName string) func(config *mergo.Config) {
return mergo.WithTransformers(overwriteTransformer{fieldName: fieldName})
}

View File

@@ -96,6 +96,13 @@ type configStore struct {
Runtimes runtimes
}
// ContainerdDialer dials the in-process containerd over an in-memory pipe.
//
// It is set in embedded mode, so the daemon's own client can skip socket
// syscalls, and is nil otherwise. The signature matches grpc.WithContextDialer,
// and the address argument is ignored.
type ContainerdDialer = func(ctx context.Context, addr string) (net.Conn, error)
// Daemon holds information about the Docker daemon.
type Daemon struct {
id string
@@ -846,7 +853,7 @@ func CheckSystem() error {
// NewDaemon sets up everything for the daemon to be able to service
// requests from the webserver.
func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.Store, authzMiddleware *authorization.Middleware) (_ *Daemon, retErr error) {
func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.Store, authzMiddleware *authorization.Middleware, containerdDialer ContainerdDialer) (_ *Daemon, retErr error) {
registryService, err := registry.NewService(config.ServiceOptions)
if err != nil {
return nil, err
@@ -1009,6 +1016,12 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
grpc.WithUnaryInterceptor(grpcerrors.UnaryClientInterceptor),
grpc.WithStreamInterceptor(grpcerrors.StreamClientInterceptor),
}
if containerdDialer != nil {
// Keep ContainerdAddr as the gRPC target (so containerd.New still
// installs its namespace interceptors) but override the connection to
// use the in-memory pipe.
gopts = append(gopts, grpc.WithContextDialer(containerdDialer))
}
if cfgStore.ContainerdAddr != "" {
log.G(ctx).WithFields(log.Fields{

View File

@@ -251,6 +251,9 @@ func (daemon *Daemon) fillDebugInfo(ctx context.Context, v *system.Info) {
// fillContainerdInfo provides information about the containerd configuration
// for debugging purposes.
func (daemon *Daemon) fillContainerdInfo(v *system.Info, cfg *config.Config) {
if cfg.Features["embedded-containerd"] {
v.Warnings = append(v.Warnings, "WARNING: Running with experimental embedded-containerd mode. This feature may change or be removed in a future release.")
}
if cfg.ContainerdAddr == "" {
return
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"maps"
"net"
"net/netip"
"strconv"
"strings"
@@ -98,6 +99,10 @@ type Opt struct {
UseSnapshotter bool
Snapshotter string
ContainerdAddress string
// ContainerdDialer, when set (embedded mode), connects to the in-process
// containerd over an in-memory pipe so BuildKit avoids socket syscalls.
// ContainerdAddress is still the gRPC target.
ContainerdDialer func(ctx context.Context, address string) (net.Conn, error)
ContainerdNamespace string
HyperVIsolation bool
Callbacks exporter.BuildkitCallbacks

View File

@@ -58,6 +58,7 @@ import (
"github.com/pkg/errors"
bolt "go.etcd.io/bbolt"
"go.opentelemetry.io/otel/sdk/trace"
"google.golang.org/grpc"
)
func newController(ctx context.Context, rt http.RoundTripper, opt Opt) (*control.Controller, error) {
@@ -134,7 +135,16 @@ func newSnapshotterController(ctx context.Context, rt http.RoundTripper, opt Opt
CDIManager: cdiManager,
}
wo, err := containerd.NewWorkerOpt(workerOpts, ctd.WithTimeout(60*time.Second))
ctdOpts := []ctd.Opt{ctd.WithTimeout(60 * time.Second)}
if opt.ContainerdDialer != nil {
// Embedded mode: dial the in-process containerd over the in-memory
// pipe. The address stays the gRPC target, and the dialer overrides the
// connection.
ctdOpts = append(ctdOpts, ctd.WithExtraDialOpts([]grpc.DialOption{
grpc.WithContextDialer(opt.ContainerdDialer),
}))
}
wo, err := containerd.NewWorkerOpt(workerOpts, ctdOpts...)
if err != nil {
return nil, err
}
@@ -163,6 +173,7 @@ func newSnapshotterController(ctx context.Context, rt http.RoundTripper, opt Opt
rootless: opt.Rootless,
identityMapping: opt.IdentityMapping,
containerdAddr: opt.ContainerdAddress,
containerdDialer: opt.ContainerdDialer,
containerdNamespace: opt.ContainerdNamespace,
hypervIsolation: opt.HyperVIsolation,
})
@@ -384,6 +395,7 @@ func newGraphDriverController(ctx context.Context, rt http.RoundTripper, opt Opt
// Windows-only fields (currently not used, as newExecutorGD is not implemented on Windows)
containerdAddr: opt.ContainerdAddress,
containerdDialer: opt.ContainerdDialer,
containerdNamespace: opt.ContainerdNamespace,
hypervIsolation: opt.HyperVIsolation,
})

View File

@@ -1,6 +1,9 @@
package buildkit
import (
"context"
"net"
"github.com/moby/buildkit/executor/oci"
"github.com/moby/buildkit/solver/llbsolver/cdidevices"
"github.com/moby/buildkit/util/network"
@@ -26,6 +29,7 @@ type executorOpts struct {
// windows-only fields
containerdAddr string
containerdDialer func(ctx context.Context, address string) (net.Conn, error)
containerdNamespace string
hypervIsolation bool
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/network"
"github.com/opencontainers/runtime-spec/specs-go"
"google.golang.org/grpc"
)
const networkName = "nat"
@@ -23,8 +24,14 @@ func newExecutor(opts executorOpts) (executor.Executor, network.ProxyProvider, e
pb.NetMode_NONE: network.NewNoneProvider(),
}
opt := ctd.WithDefaultNamespace(opts.containerdNamespace)
client, err := ctd.New(opts.containerdAddr, opt)
ctdOpts := []ctd.Opt{ctd.WithDefaultNamespace(opts.containerdNamespace)}
if opts.containerdDialer != nil {
// Embedded containerd: connect over the in-memory pipe.
ctdOpts = append(ctdOpts, ctd.WithExtraDialOpts([]grpc.DialOption{
grpc.WithContextDialer(opts.containerdDialer),
}))
}
client, err := ctd.New(opts.containerdAddr, ctdOpts...)
if err != nil {
return nil, nil, err
}

View File

@@ -0,0 +1,284 @@
//go:build (linux || windows) && !no_embedded_containerd
package embedded
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"sync"
"sync/atomic"
"github.com/containerd/containerd/v2/defaults"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/sys"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/log"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
"github.com/containerd/ttrpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
type grpcService interface {
Register(*grpc.Server) error
}
type ttrpcService interface {
RegisterTTRPC(*ttrpc.Server) error
}
type serverConfig struct {
root string
state string
grpcAddress string
ttrpcAddress string
maxRecvMessageSize int
maxSendMessageSize int
}
type containerdServer struct {
grpcServer *grpc.Server
ttrpcServer *ttrpc.Server
serveCtx context.Context
cancelServe context.CancelFunc
plugins []*plugin.Plugin
ready sync.WaitGroup
stopOnce sync.Once
}
// newServer initializes the containerd plugin graph and the RPC servers needed
// by dockerd.
// Command-owned behavior such as process mutation, proxy plugins, TCP/TLS, and
// Prometheus registration is intentionally not part of the embedded server.
func newServer(ctx context.Context, cfg *serverConfig) (*containerdServer, error) {
registrations := registry.Graph(nil)
return newServerWithRegistrations(ctx, cfg, registrations)
}
// newServerWithRegistrations mirrors containerd's plugin lifecycle for the
// subset used by dockerd.
// It is based on containerd v2.3.3's cmd/containerd/server.New:
// https://github.com/containerd/containerd/blob/aad11006b869517fcd3009450b6f82da282e1a9b/cmd/containerd/server/server.go
func newServerWithRegistrations(ctx context.Context, cfg *serverConfig, registrations []plugin.Registration) (_ *containerdServer, retErr error) {
// BuildKit registers collectors with the same names in dockerd, and the
// embedded containerd metrics are not exposed, so omit gRPC metrics instead
// of mutating the process-global Prometheus registerer.
grpcOptions := []grpc.ServerOption{
grpc.StatsHandler(otelgrpc.NewServerHandler()),
grpc.ChainStreamInterceptor(streamNamespaceInterceptor),
grpc.ChainUnaryInterceptor(unaryNamespaceInterceptor),
}
if cfg.maxRecvMessageSize > 0 {
grpcOptions = append(grpcOptions, grpc.MaxRecvMsgSize(cfg.maxRecvMessageSize))
}
if cfg.maxSendMessageSize > 0 {
grpcOptions = append(grpcOptions, grpc.MaxSendMsgSize(cfg.maxSendMessageSize))
}
ttrpcServer, err := newTTRPCServer()
if err != nil {
return nil, fmt.Errorf("creating containerd ttrpc server: %w", err)
}
serveCtx, cancelServe := context.WithCancel(ctx)
srv := &containerdServer{
grpcServer: grpc.NewServer(grpcOptions...),
ttrpcServer: ttrpcServer,
serveCtx: serveCtx,
cancelServe: cancelServe,
}
defer func() {
if retErr != nil {
srv.Stop()
}
}()
initialized := plugin.NewPluginSet()
var grpcServices []grpcService
var ttrpcServices []ttrpcService
for _, registration := range registrations {
id := registration.URI()
log.G(ctx).WithFields(log.Fields{"id": id, "type": registration.Type}).Info("loading plugin")
var mustSucceed atomic.Bool
initContext := plugin.NewContext(
ctx,
initialized,
map[string]string{
plugins.PropertyRootDir: filepath.Join(cfg.root, id),
plugins.PropertyStateDir: filepath.Join(cfg.state, id),
plugins.PropertyGRPCAddress: cfg.grpcAddress,
plugins.PropertyTTRPCAddress: cfg.ttrpcAddress,
},
)
initContext.RegisterReadiness = func() func() {
mustSucceed.Store(true)
return srv.registerReadiness()
}
initContext.Config = registration.Config
result := registration.Init(initContext)
if err := initialized.Add(result); err != nil {
return nil, fmt.Errorf("adding plugin %q to initialized set: %w", id, err)
}
instance, err := result.Instance()
if err != nil {
fields := log.Fields{"error": err, "id": id, "type": registration.Type}
if plugin.IsSkipPlugin(err) {
log.G(ctx).WithFields(fields).Info("skip loading plugin")
} else {
log.G(ctx).WithFields(fields).Warn("failed to load plugin")
}
if mustSucceed.Load() {
return nil, fmt.Errorf("plugin %q failed after registering readiness: %w", id, err)
}
continue
}
if service, ok := instance.(grpcService); ok {
grpcServices = append(grpcServices, service)
}
if service, ok := instance.(ttrpcService); ok {
ttrpcServices = append(ttrpcServices, service)
}
srv.plugins = append(srv.plugins, result)
}
for _, service := range grpcServices {
if err := service.Register(srv.grpcServer); err != nil {
return nil, fmt.Errorf("registering containerd grpc service: %w", err)
}
}
for _, service := range ttrpcServices {
if err := service.RegisterTTRPC(srv.ttrpcServer); err != nil {
return nil, fmt.Errorf("registering containerd ttrpc service: %w", err)
}
}
return srv, nil
}
func createTopLevelDirectories(cfg *serverConfig) error {
switch {
case cfg.root == "":
return errors.New("containerd root must be specified")
case cfg.state == "":
return errors.New("containerd state must be specified")
case cfg.root == cfg.state:
return errors.New("containerd root and state must be different paths")
}
if err := sys.MkdirAllWithACL(cfg.root, 0o700); err != nil {
return fmt.Errorf("creating containerd root directory: %w", err)
}
if err := os.Chmod(cfg.root, 0o700); err != nil {
return fmt.Errorf("setting containerd root directory permissions: %w", err)
}
// State must be searchable by remapped users so they can reach plugin-owned
// directories with more restrictive permissions below it.
if err := sys.MkdirAllWithACL(cfg.state, 0o711); err != nil {
return fmt.Errorf("creating containerd state directory: %w", err)
}
if cfg.state != defaults.DefaultStateDir {
// Shim sockets and FIFOs still use the default state directory even when
// containerd is configured with a different state directory.
if err := sys.MkdirAllWithACL(defaults.DefaultStateDir, 0o711); err != nil {
return fmt.Errorf("creating default containerd state directory: %w", err)
}
}
return nil
}
func (s *containerdServer) ServeGRPC(listener net.Listener) error {
return trapClosedConnErr(s.grpcServer.Serve(listener))
}
func (s *containerdServer) ServeTTRPC(listener net.Listener) error {
return trapClosedConnErr(s.ttrpcServer.Serve(s.serveCtx, listener))
}
func (s *containerdServer) Stop() {
s.stopOnce.Do(func() {
s.cancelServe()
s.grpcServer.Stop()
_ = s.ttrpcServer.Close()
s.closePlugins()
})
}
func (s *containerdServer) registerReadiness() func() {
s.ready.Add(1)
return s.ready.Done
}
func (s *containerdServer) Wait() {
s.ready.Wait()
}
func (s *containerdServer) closePlugins() {
for i := len(s.plugins) - 1; i >= 0; i-- {
initialized := s.plugins[i]
instance, err := initialized.Instance()
if err != nil {
log.L.WithFields(log.Fields{
"error": err,
"id": initialized.Registration.URI(),
}).Error("could not get plugin instance")
continue
}
closer, ok := instance.(io.Closer)
if !ok {
continue
}
if err := closer.Close(); err != nil {
log.L.WithFields(log.Fields{
"error": err,
"id": initialized.Registration.URI(),
}).Error("failed to close plugin")
}
}
}
func unaryNamespaceInterceptor(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
if namespace, ok := namespaces.Namespace(ctx); ok {
// Namespace reads incoming metadata; add it to outgoing metadata for
// service handlers that call other containerd components.
ctx = namespaces.WithNamespace(ctx, namespace)
}
return handler(ctx, req)
}
func streamNamespaceInterceptor(server any, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := stream.Context()
if namespace, ok := namespaces.Namespace(ctx); ok {
// Namespace reads incoming metadata; add it to outgoing metadata for
// service handlers that call other containerd components.
ctx = namespaces.WithNamespace(ctx, namespace)
stream = &serverStreamWithContext{ServerStream: stream, ctx: ctx}
}
return handler(server, stream)
}
type serverStreamWithContext struct {
grpc.ServerStream
ctx context.Context
}
func (s *serverStreamWithContext) Context() context.Context {
return s.ctx
}
func trapClosedConnErr(err error) error {
if err == nil || errors.Is(err, net.ErrClosed) || errors.Is(err, ttrpc.ErrServerClosed) {
return nil
}
return err
}

View File

@@ -0,0 +1,203 @@
//go:build (linux || windows) && !no_embedded_containerd
package embedded
import (
"context"
"errors"
"net"
"path/filepath"
"testing"
"time"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/plugin"
"github.com/containerd/ttrpc"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
type recordingService struct {
id string
grpcRegistrations *[]string
ttrpcRegistrations *[]string
closed *[]string
registerErr error
}
func (s *recordingService) Register(*grpc.Server) error {
if s.grpcRegistrations != nil {
*s.grpcRegistrations = append(*s.grpcRegistrations, s.id)
}
return s.registerErr
}
func (s *recordingService) RegisterTTRPC(*ttrpc.Server) error {
if s.ttrpcRegistrations != nil {
*s.ttrpcRegistrations = append(*s.ttrpcRegistrations, s.id)
}
return nil
}
func (s *recordingService) Close() error {
if s.closed != nil {
*s.closed = append(*s.closed, s.id)
}
return nil
}
func TestContainerdServerPluginLifecycle(t *testing.T) {
var initialized, grpcRegistrations, ttrpcRegistrations, closed []string
cfg := &serverConfig{
root: filepath.Join(t.TempDir(), "root"),
state: filepath.Join(t.TempDir(), "state"),
grpcAddress: "grpc-address",
ttrpcAddress: "ttrpc-address",
}
const pluginType = plugin.Type("io.moby.test.v1")
newRegistration := func(id string) plugin.Registration {
pluginConfig := &struct{ ID string }{ID: id}
return plugin.Registration{
Type: pluginType,
ID: id,
Config: pluginConfig,
InitFn: func(initContext *plugin.InitContext) (any, error) {
uri := string(pluginType) + "." + id
assert.Check(t, is.Equal(initContext.Properties[plugins.PropertyRootDir], filepath.Join(cfg.root, uri)))
assert.Check(t, is.Equal(initContext.Properties[plugins.PropertyStateDir], filepath.Join(cfg.state, uri)))
assert.Check(t, is.Equal(initContext.Properties[plugins.PropertyGRPCAddress], cfg.grpcAddress))
assert.Check(t, is.Equal(initContext.Properties[plugins.PropertyTTRPCAddress], cfg.ttrpcAddress))
assert.Check(t, is.Equal(initContext.Config, pluginConfig))
ready := initContext.RegisterReadiness()
ready()
initialized = append(initialized, id)
return &recordingService{
id: id,
grpcRegistrations: &grpcRegistrations,
ttrpcRegistrations: &ttrpcRegistrations,
closed: &closed,
}, nil
},
}
}
registrations := []plugin.Registration{
newRegistration("first"),
newRegistration("second"),
}
server, err := newServerWithRegistrations(t.Context(), cfg, registrations)
assert.NilError(t, err)
server.Wait()
assert.Check(t, is.DeepEqual(initialized, []string{"first", "second"}))
assert.Check(t, is.DeepEqual(grpcRegistrations, []string{"first", "second"}))
assert.Check(t, is.DeepEqual(ttrpcRegistrations, []string{"first", "second"}))
server.Stop()
server.Stop()
assert.Check(t, is.DeepEqual(closed, []string{"second", "first"}))
}
func TestContainerdServerClosesPluginsAfterRegistrationFailure(t *testing.T) {
var closed []string
cfg := &serverConfig{
root: t.TempDir(),
state: t.TempDir(),
}
registrations := []plugin.Registration{
{
Type: "io.moby.test.v1",
ID: "loaded",
InitFn: func(*plugin.InitContext) (any, error) {
return &recordingService{id: "loaded", closed: &closed}, nil
},
},
{
Type: "io.moby.test.v1",
ID: "failing",
InitFn: func(*plugin.InitContext) (any, error) {
return &recordingService{
id: "failing",
closed: &closed,
registerErr: errors.New("registration failed"),
}, nil
},
},
}
server, err := newServerWithRegistrations(t.Context(), cfg, registrations)
assert.Check(t, is.Nil(server))
assert.Check(t, is.ErrorContains(err, "registration failed"))
assert.Check(t, is.DeepEqual(closed, []string{"failing", "loaded"}))
}
func TestContainerdServerStopCancelsTTRPCRequests(t *testing.T) {
ttrpcServer, err := ttrpc.NewServer()
assert.NilError(t, err)
requestStarted := make(chan struct{})
requestCanceled := make(chan struct{})
ttrpcServer.RegisterService("test", &ttrpc.ServiceDesc{
Methods: map[string]ttrpc.Method{
"Wait": func(ctx context.Context, _ func(any) error) (any, error) {
close(requestStarted)
<-ctx.Done()
close(requestCanceled)
return nil, ctx.Err()
},
},
})
serveCtx, cancelServe := context.WithCancel(t.Context())
server := &containerdServer{
grpcServer: grpc.NewServer(),
ttrpcServer: ttrpcServer,
serveCtx: serveCtx,
cancelServe: cancelServe,
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
assert.NilError(t, err)
t.Cleanup(func() { _ = listener.Close() })
serveErr := make(chan error, 1)
go func() {
serveErr <- server.ServeTTRPC(listener)
}()
conn, err := net.Dial("tcp", listener.Addr().String())
assert.NilError(t, err)
client := ttrpc.NewClient(conn)
t.Cleanup(func() { _ = client.Close() })
callErr := make(chan error, 1)
go func() {
callErr <- client.Call(t.Context(), "test", "Wait", &emptypb.Empty{}, &emptypb.Empty{})
}()
deadline, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
select {
case <-requestStarted:
case <-deadline.Done():
t.Fatal("timed out waiting for ttrpc request to start")
}
server.Stop()
select {
case <-requestCanceled:
case <-deadline.Done():
t.Fatal("timed out waiting for ttrpc request cancellation")
}
select {
case err := <-serveErr:
assert.NilError(t, err)
case <-deadline.Done():
t.Fatal("timed out waiting for ttrpc server to stop")
}
select {
case <-callErr:
case <-deadline.Done():
t.Fatal("timed out waiting for ttrpc client call to return")
}
}

View File

@@ -0,0 +1,34 @@
//go:build !no_embedded_containerd
// Package embedded runs containerd's full gRPC server inside the dockerd
// process.
//
// The same API is served on two endpoints. One is a unix socket (a named pipe
// on Windows) in the daemon's exec-root, used by the plugin executor and by
// tooling such as ctr. The other is an in-memory pipe, used by dockerd's own
// client to avoid socket syscalls.
//
// containerd still runs each container's shim as a separate process, so
// containers keep running across a daemon restart, as they do today.
package embedded
import (
"context"
"net"
"time"
)
// Daemon is an in-process containerd server.
type Daemon interface {
// Address returns the containerd gRPC address (unix socket path, or named
// pipe on Windows) external clients and tooling should dial.
Address() string
// Dial returns an in-memory connection to the server, which dockerd's own
// client uses to avoid socket syscalls. The signature matches
// grpc.WithContextDialer, and the addr argument is ignored.
Dial(ctx context.Context, addr string) (net.Conn, error)
// WaitTimeout waits up to d for the server to stop after Shutdown.
WaitTimeout(d time.Duration) error
// Shutdown gracefully stops the in-process server and waits for it to exit.
Shutdown(ctx context.Context) error
}

View File

@@ -0,0 +1,35 @@
//go:build !no_embedded_containerd
package embedded
import (
"net"
"path/filepath"
"github.com/containerd/ttrpc"
"github.com/docker/go-connections/sockets"
// Linux-specific containerd plugin registrations: the overlayfs
// snapshotter, the walking differ, the cgroups task monitor (for container
// metrics), and the runc runtime options type. Cross-platform plugins are
// registered in server.go.
_ "github.com/containerd/containerd/api/types/runc/options"
_ "github.com/containerd/containerd/v2/core/metrics/cgroups"
_ "github.com/containerd/containerd/v2/plugins/diff/walking/plugin"
_ "github.com/containerd/containerd/v2/plugins/snapshots/native/plugin"
_ "github.com/containerd/containerd/v2/plugins/snapshots/overlay/plugin"
)
func defaultAddress(stateDir string) string {
return filepath.Join(stateDir, "containerd.sock")
}
func listen(address string) (net.Listener, error) {
return sockets.NewUnixSocketWithOpts(address, sockets.WithChmod(0o660))
}
func newTTRPCServer() (*ttrpc.Server, error) {
return ttrpc.NewServer(
ttrpc.WithServerHandshaker(ttrpc.UnixSocketRequireSameUser()),
)
}

View File

@@ -0,0 +1,42 @@
//go:build (linux || windows) && !no_embedded_containerd
package embedded
import (
"path/filepath"
"strings"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
// TestPluginGraphResolves guards the blank-import plugin set: it starts the
// embedded containerd server and fails if any required plugin is missing from
// the registry or its dependency graph cannot be satisfied. Runtime init
// failures unrelated to registration (e.g. requiring root) are tolerated, so
// the test is safe to run unprivileged.
func TestPluginGraphResolves(t *testing.T) {
ctx := t.Context()
d, err := Start(ctx, t.TempDir(), t.TempDir())
if err != nil {
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "no plugins registered") {
t.Fatalf("embedded containerd plugin graph is incomplete: %v", err)
}
t.Skipf("embedded containerd did not start in this environment: %v", err)
}
t.Cleanup(func() { _ = d.Shutdown(ctx) })
}
func TestBuildServerConfigUsesSupervisedLayout(t *testing.T) {
rootDir := filepath.Join(t.TempDir(), "root")
stateDir := filepath.Join(t.TempDir(), "state")
address := defaultAddress(stateDir)
cfg := buildServerConfig(rootDir, stateDir, address)
assert.Check(t, is.Equal(cfg.root, rootDir))
assert.Check(t, is.Equal(cfg.state, filepath.Join(stateDir, "daemon")))
assert.Check(t, is.Equal(cfg.grpcAddress, address))
assert.Check(t, is.Equal(cfg.ttrpcAddress, address+".ttrpc"))
}

View File

@@ -0,0 +1,43 @@
//go:build !no_embedded_containerd
package embedded
import (
"crypto/sha256"
"encoding/hex"
"net"
"path/filepath"
"github.com/Microsoft/go-winio"
"github.com/containerd/ttrpc"
// Windows-specific containerd plugin registrations. Cross-platform plugins
// are registered in server.go.
_ "github.com/containerd/containerd/v2/plugins/diff/lcow"
_ "github.com/containerd/containerd/v2/plugins/diff/windows"
_ "github.com/containerd/containerd/v2/plugins/snapshots/windows"
)
func defaultAddress(stateDir string) string {
return `\\.\pipe\docker-containerd-embedded-` + stateDirID(stateDir)
}
// stateDirID returns a stable identifier for the daemon state directory.
func stateDirID(stateDir string) string {
sum := sha256.Sum256([]byte(filepath.Clean(stateDir)))
return hex.EncodeToString(sum[:])
}
// namedPipePermissions grants full access only to built-in Administrators and
// LocalSystem.
const namedPipePermissions = "D:P(A;;GA;;;BA)(A;;GA;;;SY)"
func listen(address string) (net.Listener, error) {
return winio.ListenPipe(address, &winio.PipeConfig{
SecurityDescriptor: namedPipePermissions,
})
}
func newTTRPCServer() (*ttrpc.Server, error) {
return ttrpc.NewServer()
}

View File

@@ -0,0 +1,20 @@
//go:build windows && !no_embedded_containerd
package embedded
import (
"strings"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestDefaultAddressUsesStateDir(t *testing.T) {
stateDir := `C:\ProgramData\docker\execroot\daemon-1\containerd`
address := defaultAddress(stateDir)
assert.Check(t, is.Equal(address, defaultAddress(stateDir)))
assert.Check(t, strings.HasPrefix(address, `\\.\pipe\docker-containerd-embedded-`))
assert.Check(t, address != defaultAddress(`C:\ProgramData\docker\execroot\daemon-2\containerd`))
}

View File

@@ -0,0 +1,243 @@
//go:build (linux || windows) && !no_embedded_containerd
package embedded
import (
"context"
"errors"
"net"
"path/filepath"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"github.com/containerd/containerd/v2/defaults"
"github.com/containerd/containerd/v2/version"
"github.com/containerd/log"
// Cross-platform plugin registrations. Each blank import registers one or
// more containerd plugins that the local bootstrap then loads. Platform-specific
// snapshotters and differs are registered in embedded_linux.go /
// embedded_windows.go.
//
// This is a trimmed subset of containerd's cmd/containerd/builtins: dockerd
// does not need CRI, sandbox, streaming, transfer, NRI, or the restart
// monitor.
_ "github.com/containerd/containerd/v2/core/runtime/v2"
_ "github.com/containerd/containerd/v2/plugins/content/local/plugin"
_ "github.com/containerd/containerd/v2/plugins/events"
_ "github.com/containerd/containerd/v2/plugins/gc"
_ "github.com/containerd/containerd/v2/plugins/leases"
_ "github.com/containerd/containerd/v2/plugins/metadata"
_ "github.com/containerd/containerd/v2/plugins/mount"
_ "github.com/containerd/containerd/v2/plugins/services/containers"
_ "github.com/containerd/containerd/v2/plugins/services/content"
_ "github.com/containerd/containerd/v2/plugins/services/diff"
_ "github.com/containerd/containerd/v2/plugins/services/events"
_ "github.com/containerd/containerd/v2/plugins/services/healthcheck"
_ "github.com/containerd/containerd/v2/plugins/services/images"
_ "github.com/containerd/containerd/v2/plugins/services/introspection"
_ "github.com/containerd/containerd/v2/plugins/services/leases"
_ "github.com/containerd/containerd/v2/plugins/services/namespaces"
_ "github.com/containerd/containerd/v2/plugins/services/snapshots"
_ "github.com/containerd/containerd/v2/plugins/services/tasks"
_ "github.com/containerd/containerd/v2/plugins/services/version"
_ "github.com/containerd/containerd/v2/plugins/services/warning"
)
// Start initializes and runs the embedded containerd server, using rootDir for
// persistent state (bolt DB, content store) and the daemon subdirectory under
// stateDir for runtime state.
// See the package doc for the transport layout.
// Plugins self-register via the blank imports above and in the platform-specific
// files.
func Start(ctx context.Context, rootDir, stateDir string) (Daemon, error) {
setContainerdVersion()
address := defaultAddress(stateDir)
cfg := buildServerConfig(rootDir, stateDir, address)
// Create the root and state directories with the permissions containerd
// expects (e.g. the state dir at 0o711 for userns-remapped containers),
// matching what containerd's own command does before initializing plugins.
if err := createTopLevelDirectories(cfg); err != nil {
return nil, err
}
log.G(ctx).WithField("address", address).Info("starting embedded containerd server")
srv, err := newServer(ctx, cfg)
if err != nil {
return nil, err
}
socket, err := listen(cfg.grpcAddress)
if err != nil {
srv.Stop()
return nil, err
}
// Shims (separate processes) publish task events back to containerd over
// ttrpc, so it must be a real socket. The runtime plugin hands its address
// (PropertyTTRPCAddress) to each shim.
ttrpcL, err := listen(cfg.ttrpcAddress)
if err != nil {
srv.Stop()
_ = socket.Close()
return nil, err
}
inMemory := newInMemoryListener()
e := &embeddedDaemon{srv: srv, address: address, inMemory: inMemory, ttrpcL: ttrpcL, stopCh: make(chan struct{})}
var wg sync.WaitGroup
serve := func(name string, l net.Listener, fn func(net.Listener) error) {
wg.Add(1)
go func() {
defer wg.Done()
// Serve returns an error once the server is stopped, which is
// expected during Shutdown, so only log unexpected failures.
if err := fn(l); err != nil && !e.stopping.Load() {
log.G(ctx).WithError(err).Errorf("embedded containerd %s server exited", name)
}
}()
}
serve("gRPC socket", socket, srv.ServeGRPC)
serve("gRPC in-memory", inMemory, srv.ServeGRPC)
serve("ttrpc", ttrpcL, srv.ServeTTRPC)
go func() {
wg.Wait()
close(e.stopCh)
}()
// Tie the server to the daemon context: shut down when it is cancelled,
// leaving running shims untouched. WithoutCancel so Shutdown can still wait
// for the server to stop after ctx is already done.
go func() {
<-ctx.Done()
if err := e.Shutdown(context.WithoutCancel(ctx)); err != nil {
log.G(ctx).WithError(err).Error("failed to shut down embedded containerd")
}
}()
// Do not report successful startup until asynchronous plugin initialization
// has completed.
srv.Wait()
return e, nil
}
// setContainerdVersion makes the embedded server report the vendored containerd
// version in "docker info" instead of the source default ("2.2.4+unknown").
//
// The version comes from the build's module info. The git commit is not
// recorded there, so only the version is set.
func setContainerdVersion() {
bi, ok := debug.ReadBuildInfo()
if !ok {
return
}
for _, dep := range bi.Deps {
if dep.Path == "github.com/containerd/containerd/v2" && dep.Version != "" {
version.Version = dep.Version
break
}
}
}
func buildServerConfig(rootDir, stateDir, address string) *serverConfig {
return &serverConfig{
root: rootDir,
state: filepath.Join(stateDir, "daemon"),
grpcAddress: address,
ttrpcAddress: address + ".ttrpc",
maxRecvMessageSize: defaults.DefaultMaxRecvMsgSize,
maxSendMessageSize: defaults.DefaultMaxSendMsgSize,
}
}
type embeddedDaemon struct {
srv *containerdServer
address string
inMemory *inMemoryListener
ttrpcL net.Listener
stopCh chan struct{}
stopping atomic.Bool
}
func (e *embeddedDaemon) Address() string {
return e.address
}
// Dial returns one end of an in-memory pipe connected to the gRPC server. The
// addr argument is ignored, and only present to satisfy grpc.WithContextDialer.
func (e *embeddedDaemon) Dial(ctx context.Context, _ string) (net.Conn, error) {
serverConn, clientConn := net.Pipe()
select {
case e.inMemory.ch <- serverConn:
return clientConn, nil
case <-ctx.Done():
_ = serverConn.Close()
_ = clientConn.Close()
return nil, ctx.Err()
case <-e.inMemory.done:
_ = serverConn.Close()
_ = clientConn.Close()
return nil, errors.New("embedded containerd server is stopped")
}
}
func (e *embeddedDaemon) WaitTimeout(d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-timer.C:
return errors.New("timeout waiting for embedded containerd to stop")
case <-e.stopCh:
return nil
}
}
func (e *embeddedDaemon) Shutdown(ctx context.Context) error {
e.stopping.Store(true)
// Stop closes both RPC servers and their listeners.
e.srv.Stop()
e.inMemory.Close()
select {
case <-e.stopCh:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
// inMemoryListener is a net.Listener whose connections are supplied by
// Dial rather than accepted from the kernel.
type inMemoryListener struct {
ch chan net.Conn
done chan struct{}
once sync.Once
}
func newInMemoryListener() *inMemoryListener {
return &inMemoryListener{
ch: make(chan net.Conn, 16),
done: make(chan struct{}),
}
}
func (l *inMemoryListener) Accept() (net.Conn, error) {
select {
case conn := <-l.ch:
return conn, nil
case <-l.done:
return nil, net.ErrClosed
}
}
func (l *inMemoryListener) Close() error {
l.once.Do(func() { close(l.done) })
return nil
}
func (l *inMemoryListener) Addr() net.Addr {
return &net.UnixAddr{Name: "embedded-containerd", Net: "inmem"}
}

View File

@@ -61,12 +61,14 @@ type Daemon interface {
// DaemonOpt allows to configure parameters of container daemons
type DaemonOpt func(c *remote) error
// Start starts a containerd daemon and monitors it
// Start starts a containerd daemon and monitors it.
// It uses rootDir for persistent state and the daemon subdirectory under
// stateDir for runtime state.
func Start(ctx context.Context, rootDir, stateDir string, opts ...DaemonOpt) (Daemon, error) {
r := &remote{
Config: config.Config{
Version: 2, // FIXME(thaJeztah): update to v3 when we drop support for containerd v1.
Root: filepath.Join(rootDir, "daemon"),
Root: rootDir,
State: filepath.Join(stateDir, "daemon"),
GRPC: config.GRPCConfig{ //nolint:staticcheck // Deprecated in config v4, but required for config v3.
Address: defaultGRPCAddress(stateDir), //nolint:staticcheck // Deprecated in config v4, but required for config v3.

16
go.mod
View File

@@ -32,8 +32,10 @@ require (
github.com/containerd/errdefs v1.0.0
github.com/containerd/fifo v1.1.0
github.com/containerd/log v0.1.0
github.com/containerd/nri v0.12.1
github.com/containerd/nri v0.12.0
github.com/containerd/platforms v1.0.0-rc.4
github.com/containerd/plugin v1.1.0
github.com/containerd/ttrpc v1.2.9
github.com/containerd/typeurl/v2 v2.3.0
github.com/coreos/go-systemd/v22 v22.7.0
github.com/cpuguy83/tar2go v0.3.1
@@ -154,6 +156,7 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/checkpoint-restore/checkpointctl v1.5.0 // indirect
github.com/cilium/ebpf v0.17.3 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/container-storage-interface/spec v1.5.0 // indirect
@@ -163,9 +166,8 @@ require (
github.com/containerd/go-cni v1.1.13 // indirect
github.com/containerd/go-runc v1.1.0 // indirect
github.com/containerd/nydus-snapshotter v0.15.15 // indirect
github.com/containerd/plugin v1.1.0 // indirect
github.com/containerd/otelttrpc v0.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
github.com/containerd/ttrpc v1.2.9 // indirect
github.com/containernetworking/cni v1.3.0 // indirect
github.com/containernetworking/plugins v1.9.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
@@ -180,6 +182,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.25.2 // indirect
@@ -230,9 +233,12 @@ require (
github.com/hiddeco/sshsig v0.2.0 // indirect
github.com/in-toto/attestation v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/intel/goresctrl v0.12.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/knqyf263/go-plugin v0.9.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mdlayher/vsock v1.2.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
github.com/morikuni/aec v1.1.0 // indirect
@@ -271,6 +277,7 @@ require (
github.com/transparency-dev/formats v0.1.1 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/weppos/publicsuffix-go v0.30.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect
github.com/zmap/zlint/v3 v3.5.0 // indirect
@@ -303,8 +310,11 @@ require (
google.golang.org/api v0.283.0 // indirect
google.golang.org/genproto v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apimachinery v0.36.0 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
tags.cncf.io/container-device-interface/specs-go v1.1.0 // indirect
)

24
go.sum
View File

@@ -135,6 +135,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/checkpoint-restore/checkpointctl v1.5.0 h1:Uu+D2cOf/GUyCMk23Y8L69P6YoATTe6pH+Au64O3y28=
github.com/checkpoint-restore/checkpointctl v1.5.0/go.mod h1:y5HRs1ZWQUZGyEuthlTHmTJN9PUMOjlaH6JvVaNq9kE=
github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg=
github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
@@ -177,10 +179,12 @@ github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gG
github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nri v0.12.1 h1:Nkp14W/mdhP0ze83ja/O437du6NQA33W5Y0O+ar3aKM=
github.com/containerd/nri v0.12.1/go.mod h1:TGAfPLH4a+qwbv0PxsefPiR+PobYecDj2aXMtz7GQcg=
github.com/containerd/nri v0.12.0 h1:RvZtyCM64XOB1UmMAFOlfReTTwCd+hE2IEQ5XEpkKA0=
github.com/containerd/nri v0.12.0/go.mod h1:TGAfPLH4a+qwbv0PxsefPiR+PobYecDj2aXMtz7GQcg=
github.com/containerd/nydus-snapshotter v0.15.15 h1:kVYbFpYA4K43qxGVoc/VBwRXLAVWn4X9mdwGrR+HsLk=
github.com/containerd/nydus-snapshotter v0.15.15/go.mod h1:L96yO+4iE6qqDiqXKhxMXBoPeaE7JgzXir9yanUVuOY=
github.com/containerd/otelttrpc v0.1.0 h1:UOX68eVTE8H/T45JveIg+I22Ev2aFj4qPITCmXsskjw=
github.com/containerd/otelttrpc v0.1.0/go.mod h1:XhoA2VvaGPW1clB2ULwrBZfXVuEWuyOd2NUD1IM0yTg=
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U=
@@ -263,6 +267,8 @@ github.com/fluent/fluent-logger-golang v1.10.1/go.mod h1:qOuXG4ZMrXaSTk12ua+uAb2
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
@@ -473,6 +479,8 @@ github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMe
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/intel/goresctrl v0.12.0 h1:F44m7jiVgOdqWfTTWaREF+5HTeX3i06qhvpuzpnrBko=
github.com/intel/goresctrl v0.12.0/go.mod h1:5GWtmPY4BWl/a9rU8apGED9Xul5b5WoLtg/qOWaghWU=
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2 h1:36qep4gxKs+JgeHGWeQ040RyZdt9kQlLglL1rFVn/oQ=
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8=
@@ -520,6 +528,8 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4=
@@ -788,6 +798,8 @@ github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8L
github.com/weppos/publicsuffix-go v0.30.0 h1:QHPZ2GRu/YE7cvejH9iyavPOkVCB4dNxp2ZvtT+vQLY=
github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY=
github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -1039,6 +1051,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1053,6 +1067,8 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.78 h1:jgqg4gyu2BaYW9L6uzEtGLf8GNREwk/z4UFdwt5F3pE=
@@ -1063,6 +1079,10 @@ pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc=
pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk=
resenje.org/singleflight v0.4.3 h1:l7foFYg8X/VEHPxWs1K/Pw77807RMVzvXgWGb0J1sdM=
resenje.org/singleflight v0.4.3/go.mod h1:lAgQK7VfjG6/pgredbQfmV0RvG/uVhKo6vSuZ0vCWfk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=

View File

@@ -143,8 +143,14 @@ func RegistryHosting() bool {
return err == nil
}
// RuntimeIsWindowsContainerd returns whether the containerd runtime is used on
// Windows.
// It is true when either the legacy DOCKER_WINDOWS_CONTAINERD_RUNTIME=1 env
// var is set, or when the embedded-containerd feature is enabled via
// TEST_INTEGRATION_CONTAINERD_EMBEDDED (which also uses containerd).
func RuntimeIsWindowsContainerd() bool {
return os.Getenv("DOCKER_WINDOWS_CONTAINERD_RUNTIME") == "1"
return os.Getenv("DOCKER_WINDOWS_CONTAINERD_RUNTIME") == "1" ||
(runtime.GOOS == "windows" && os.Getenv("TEST_INTEGRATION_CONTAINERD_EMBEDDED") != "")
}
func SwarmInactive() bool {

View File

@@ -56,6 +56,13 @@ const (
var errDaemonNotStarted = errors.New("daemon not started")
// containerdEmbeddedFromEnv reports whether test daemons should run containerd
// in-process, via the embedded-containerd feature. It is controlled by
// TEST_INTEGRATION_CONTAINERD_EMBEDDED.
func containerdEmbeddedFromEnv() bool {
return os.Getenv("TEST_INTEGRATION_CONTAINERD_EMBEDDED") != ""
}
// SockRoot holds the path of the default docker integration daemon socket
var SockRoot = filepath.Join(os.TempDir(), "docker-integration")
@@ -88,6 +95,7 @@ type Daemon struct {
args []string
extraEnv []string
containerdSocket string
containerdEmbedded bool
usernsRemap string
rootlessUser *user.User
rootlessXDGRuntimeDir string
@@ -142,12 +150,13 @@ func NewDaemon(workingDir string, ops ...Option) (*Daemon, error) {
storageDriver: storageDriver,
userlandProxy: userlandProxy,
// dxr stands for docker-execroot (shortened for avoiding unix(7) path length limitation)
execRoot: filepath.Join(os.TempDir(), "dxr", id),
dockerdBinary: defaultDockerdBinary,
swarmListenAddr: defaultSwarmListenAddr,
SwarmPort: DefaultSwarmPort,
log: nopLog{},
containerdSocket: defaultContainerdSocket,
execRoot: filepath.Join(os.TempDir(), "dxr", id),
dockerdBinary: defaultDockerdBinary,
swarmListenAddr: defaultSwarmListenAddr,
SwarmPort: DefaultSwarmPort,
log: nopLog{},
containerdSocket: defaultContainerdSocket,
containerdEmbedded: containerdEmbeddedFromEnv(),
}
for _, op := range ops {
@@ -537,7 +546,9 @@ func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error {
"--containerd-namespace", d.id,
"--containerd-plugins-namespace", d.id+"p",
)
if d.containerdSocket != "" {
if d.containerdEmbedded {
d.args = append(d.args, "--feature", "embedded-containerd")
} else if d.containerdSocket != "" {
d.args = append(d.args, "--containerd", d.containerdSocket)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -156,9 +157,13 @@ func (e *Execution) IsUserNamespace() bool {
return root != ""
}
// RuntimeIsWindowsContainerd returns whether containerd runtime is used on Windows
// RuntimeIsWindowsContainerd returns whether the containerd runtime is used on Windows.
// It is true when either the legacy DOCKER_WINDOWS_CONTAINERD_RUNTIME=1 env
// var is set, or when the embedded-containerd feature is enabled via
// TEST_INTEGRATION_CONTAINERD_EMBEDDED (which also uses containerd).
func (e *Execution) RuntimeIsWindowsContainerd() bool {
return os.Getenv("DOCKER_WINDOWS_CONTAINERD_RUNTIME") == "1"
return os.Getenv("DOCKER_WINDOWS_CONTAINERD_RUNTIME") == "1" ||
(runtime.GOOS == "windows" && os.Getenv("TEST_INTEGRATION_CONTAINERD_EMBEDDED") != "")
}
// IsRootless returns whether the rootless mode is enabled

View File

@@ -0,0 +1,167 @@
//go:build windows
package security
import (
"fmt"
"os"
"unsafe"
"golang.org/x/sys/windows"
)
type (
accessMask uint32
accessMode uint32
desiredAccess uint32
inheritMode uint32
objectType uint32
shareMode uint32
securityInformation uint32
trusteeForm uint32
trusteeType uint32
explicitAccess struct {
accessPermissions accessMask
accessMode accessMode
inheritance inheritMode
trustee trustee
}
trustee struct {
multipleTrustee *trustee
multipleTrusteeOperation int32
trusteeForm trusteeForm
trusteeType trusteeType
name uintptr
}
)
const (
accessMaskDesiredPermission accessMask = 1 << 31 // GENERIC_READ
accessModeGrant accessMode = 1
desiredAccessReadControl desiredAccess = 0x20000
desiredAccessWriteDac desiredAccess = 0x40000
//cspell:disable-next-line
gvmga = "GrantVmGroupAccess:"
inheritModeNoInheritance inheritMode = 0x0
inheritModeSubContainersAndObjectsInherit inheritMode = 0x3
objectTypeFileObject objectType = 0x1
securityInformationDACL securityInformation = 0x4
shareModeRead shareMode = 0x1
shareModeWrite shareMode = 0x2
sidVMGroup = "S-1-5-83-0"
trusteeFormIsSID trusteeForm = 0
trusteeTypeWellKnownGroup trusteeType = 5
)
// GrantVMGroupAccess sets the DACL for a specified file or directory to
// include Grant ACE entries for the VM Group SID. This is a golang re-
// implementation of the same function in vmcompute, just not exported in
// RS5. Which kind of sucks. Sucks a lot :/
//
//revive:disable-next-line:var-naming VM, not Vm
func GrantVmGroupAccess(name string) error {
// Stat (to determine if `name` is a directory).
s, err := os.Stat(name)
if err != nil {
return fmt.Errorf("%s os.Stat %s: %w", gvmga, name, err)
}
// Get a handle to the file/directory. Must defer Close on success.
fd, err := createFile(name, s.IsDir())
if err != nil {
return err // Already wrapped
}
defer windows.CloseHandle(fd) //nolint:errcheck
// Get the current DACL and Security Descriptor. Must defer LocalFree on success.
ot := objectTypeFileObject
si := securityInformationDACL
sd := uintptr(0)
origDACL := uintptr(0)
if err := getSecurityInfo(fd, uint32(ot), uint32(si), nil, nil, &origDACL, nil, &sd); err != nil {
return fmt.Errorf("%s GetSecurityInfo %s: %w", gvmga, name, err)
}
defer windows.LocalFree(windows.Handle(sd)) //nolint:errcheck
// Generate a new DACL which is the current DACL with the required ACEs added.
// Must defer LocalFree on success.
newDACL, err := generateDACLWithAcesAdded(name, s.IsDir(), origDACL)
if err != nil {
return err // Already wrapped
}
defer windows.LocalFree(windows.Handle(newDACL)) //nolint:errcheck
// And finally use SetSecurityInfo to apply the updated DACL.
if err := setSecurityInfo(fd, uint32(ot), uint32(si), uintptr(0), uintptr(0), newDACL, uintptr(0)); err != nil {
return fmt.Errorf("%s SetSecurityInfo %s: %w", gvmga, name, err)
}
return nil
}
// createFile is a helper function to call [Nt]CreateFile to get a handle to
// the file or directory.
func createFile(name string, isDir bool) (windows.Handle, error) {
namep, err := windows.UTF16FromString(name)
if err != nil {
return windows.InvalidHandle, fmt.Errorf("could not convernt name to UTF-16: %w", err)
}
da := uint32(desiredAccessReadControl | desiredAccessWriteDac)
sm := uint32(shareModeRead | shareModeWrite)
fa := uint32(windows.FILE_ATTRIBUTE_NORMAL)
if isDir {
fa |= windows.FILE_FLAG_BACKUP_SEMANTICS
}
fd, err := windows.CreateFile(&namep[0], da, sm, nil, windows.OPEN_EXISTING, fa, 0)
if err != nil {
return windows.InvalidHandle, fmt.Errorf("%s windows.CreateFile %s: %w", gvmga, name, err)
}
return fd, nil
}
// generateDACLWithAcesAdded generates a new DACL with the two needed ACEs added.
// The caller is responsible for LocalFree of the returned DACL on success.
func generateDACLWithAcesAdded(name string, isDir bool, origDACL uintptr) (uintptr, error) {
// Generate pointers to the SIDs based on the string SIDs
sid, err := windows.StringToSid(sidVMGroup)
if err != nil {
return 0, fmt.Errorf("%s windows.StringToSid %s %s: %w", gvmga, name, sidVMGroup, err)
}
inheritance := inheritModeNoInheritance
if isDir {
inheritance = inheritModeSubContainersAndObjectsInherit
}
eaArray := []explicitAccess{
{
accessPermissions: accessMaskDesiredPermission,
accessMode: accessModeGrant,
inheritance: inheritance,
trustee: trustee{
trusteeForm: trusteeFormIsSID,
trusteeType: trusteeTypeWellKnownGroup,
name: uintptr(unsafe.Pointer(sid)),
},
},
}
modifiedDACL := uintptr(0)
if err := setEntriesInAcl(uintptr(uint32(1)), uintptr(unsafe.Pointer(&eaArray[0])), origDACL, &modifiedDACL); err != nil {
return 0, fmt.Errorf("%s SetEntriesInAcl %s: %w", gvmga, name, err)
}
return modifiedDACL, nil
}

View File

@@ -0,0 +1,7 @@
package security
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go syscall_windows.go
//sys getSecurityInfo(handle windows.Handle, objectType uint32, si uint32, ppsidOwner **uintptr, ppsidGroup **uintptr, ppDacl *uintptr, ppSacl *uintptr, ppSecurityDescriptor *uintptr) (win32err error) = advapi32.GetSecurityInfo
//sys setSecurityInfo(handle windows.Handle, objectType uint32, si uint32, psidOwner uintptr, psidGroup uintptr, pDacl uintptr, pSacl uintptr) (win32err error) = advapi32.SetSecurityInfo
//sys setEntriesInAcl(count uintptr, pListOfEEs uintptr, oldAcl uintptr, newAcl *uintptr) (win32err error) = advapi32.SetEntriesInAclW

View File

@@ -0,0 +1,69 @@
//go:build windows
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
package security
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
return e
}
var (
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
procGetSecurityInfo = modadvapi32.NewProc("GetSecurityInfo")
procSetEntriesInAclW = modadvapi32.NewProc("SetEntriesInAclW")
procSetSecurityInfo = modadvapi32.NewProc("SetSecurityInfo")
)
func getSecurityInfo(handle windows.Handle, objectType uint32, si uint32, ppsidOwner **uintptr, ppsidGroup **uintptr, ppDacl *uintptr, ppSacl *uintptr, ppSecurityDescriptor *uintptr) (win32err error) {
r0, _, _ := syscall.SyscallN(procGetSecurityInfo.Addr(), uintptr(handle), uintptr(objectType), uintptr(si), uintptr(unsafe.Pointer(ppsidOwner)), uintptr(unsafe.Pointer(ppsidGroup)), uintptr(unsafe.Pointer(ppDacl)), uintptr(unsafe.Pointer(ppSacl)), uintptr(unsafe.Pointer(ppSecurityDescriptor)))
if r0 != 0 {
win32err = syscall.Errno(r0)
}
return
}
func setEntriesInAcl(count uintptr, pListOfEEs uintptr, oldAcl uintptr, newAcl *uintptr) (win32err error) {
r0, _, _ := syscall.SyscallN(procSetEntriesInAclW.Addr(), uintptr(count), uintptr(pListOfEEs), uintptr(oldAcl), uintptr(unsafe.Pointer(newAcl)))
if r0 != 0 {
win32err = syscall.Errno(r0)
}
return
}
func setSecurityInfo(handle windows.Handle, objectType uint32, si uint32, psidOwner uintptr, psidGroup uintptr, pDacl uintptr, pSacl uintptr) (win32err error) {
r0, _, _ := syscall.SyscallN(procSetSecurityInfo.Addr(), uintptr(handle), uintptr(objectType), uintptr(si), uintptr(psidOwner), uintptr(psidGroup), uintptr(pDacl), uintptr(pSacl))
if r0 != 0 {
win32err = syscall.Errno(r0)
}
return
}

View File

@@ -0,0 +1,268 @@
package dmverity
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"fmt"
"io"
"os"
"github.com/pkg/errors"
"github.com/Microsoft/hcsshim/ext4/internal/compactext4"
"github.com/Microsoft/hcsshim/internal/memory"
)
const (
blockSize = compactext4.BlockSize
// MerkleTreeBufioSize is a default buffer size to use with bufio.Reader
MerkleTreeBufioSize = memory.MiB // 1MB
// RecommendedVHDSizeGB is the recommended size in GB for VHDs, which is not a hard limit.
RecommendedVHDSizeGB = 128 * memory.GiB
// VeritySignature is a value written to dm-verity super-block.
VeritySignature = "verity"
)
var (
salt = bytes.Repeat([]byte{0}, 32)
sbSize = binary.Size(dmveritySuperblock{})
)
var (
ErrSuperBlockReadFailure = errors.New("failed to read dm-verity super block")
ErrSuperBlockParseFailure = errors.New("failed to parse dm-verity super block")
ErrRootHashReadFailure = errors.New("failed to read dm-verity root hash")
ErrNotVeritySuperBlock = errors.New("invalid dm-verity super-block signature")
)
type dmveritySuperblock struct {
/* (0) "verity\0\0" */
Signature [8]byte
/* (8) superblock version, 1 */
Version uint32
/* (12) 0 - Chrome OS, 1 - normal */
HashType uint32
/* (16) UUID of hash device */
UUID [16]byte
/* (32) Name of the hash algorithm (e.g., sha256) */
Algorithm [32]byte
/* (64) The data block size in bytes */
DataBlockSize uint32
/* (68) The hash block size in bytes */
HashBlockSize uint32
/* (72) The number of data blocks */
DataBlocks uint64
/* (80) Size of the salt */
SaltSize uint16
/* (82) Padding */
_ [6]byte
/* (88) The salt */
Salt [256]byte
/* (344) Padding */
_ [168]byte
}
// VerityInfo is minimal exported version of dmveritySuperblock
type VerityInfo struct {
// Offset in blocks on hash device
HashOffsetInBlocks int64
// Set to true, when dm-verity super block is also written on the hash device
SuperBlock bool
RootDigest string
Salt string
Algorithm string
DataBlockSize uint32
HashBlockSize uint32
DataBlocks uint64
Version uint32
}
// MerkleTree constructs dm-verity hash-tree for a given io.Reader with a fixed salt (0-byte) and algorithm (sha256).
func MerkleTree(r io.Reader) ([]byte, error) {
layers := make([][]byte, 0)
currentLevel := r
for {
nextLevel := bytes.NewBuffer(make([]byte, 0))
for {
block := make([]byte, blockSize)
if _, err := io.ReadFull(currentLevel, block); err != nil {
if err == io.EOF {
break
}
return nil, errors.Wrap(err, "failed to read data block")
}
h := hash2(salt, block)
nextLevel.Write(h)
}
if nextLevel.Len()%blockSize != 0 {
padding := bytes.Repeat([]byte{0}, blockSize-(nextLevel.Len()%blockSize))
nextLevel.Write(padding)
}
layers = append(layers, nextLevel.Bytes())
currentLevel = bufio.NewReaderSize(nextLevel, MerkleTreeBufioSize)
// This means that only root hash remains and our job is done
if nextLevel.Len() == blockSize {
break
}
}
tree := bytes.NewBuffer(make([]byte, 0))
for i := len(layers) - 1; i >= 0; i-- {
if _, err := tree.Write(layers[i]); err != nil {
return nil, errors.Wrap(err, "failed to write merkle tree")
}
}
return tree.Bytes(), nil
}
// RootHash computes root hash of dm-verity hash-tree
func RootHash(tree []byte) []byte {
return hash2(salt, tree[:blockSize])
}
// NewDMVeritySuperblock returns a dm-verity superblock for a device with a given size, salt, algorithm and versions are
// fixed.
func NewDMVeritySuperblock(size uint64) *dmveritySuperblock {
superblock := &dmveritySuperblock{
Version: 1,
HashType: 1,
UUID: generateUUID(),
DataBlockSize: blockSize,
HashBlockSize: blockSize,
DataBlocks: size / blockSize,
SaltSize: uint16(len(salt)),
}
copy(superblock.Signature[:], VeritySignature)
copy(superblock.Algorithm[:], "sha256")
copy(superblock.Salt[:], salt)
return superblock
}
func hash2(a, b []byte) []byte {
h := sha256.New()
h.Write(append(a, b...))
return h.Sum(nil)
}
func generateUUID() [16]byte {
res := [16]byte{}
if _, err := rand.Read(res[:]); err != nil {
panic(err)
}
return res
}
// ReadDMVerityInfo extracts dm-verity super block information and merkle tree root hash
func ReadDMVerityInfo(vhdPath string, offsetInBytes int64) (*VerityInfo, error) {
vhd, err := os.OpenFile(vhdPath, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer vhd.Close()
// Skip the ext4 data to get to dm-verity super block
if s, err := vhd.Seek(offsetInBytes, io.SeekStart); err != nil || s != offsetInBytes {
if err != nil {
return nil, errors.Wrap(err, "failed to seek dm-verity super block")
}
return nil, errors.Errorf("failed to seek dm-verity super block: expected bytes=%d, actual=%d", offsetInBytes, s)
}
return ReadDMVerityInfoReader(vhd)
}
func ReadDMVerityInfoReader(r io.Reader) (*VerityInfo, error) {
block := make([]byte, blockSize)
if s, err := r.Read(block); err != nil || s != blockSize {
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrSuperBlockReadFailure, err)
}
return nil, fmt.Errorf("unexpected bytes read expected=%d actual=%d: %w", blockSize, s, ErrSuperBlockReadFailure)
}
dmvSB := &dmveritySuperblock{}
b := bytes.NewBuffer(block)
if err := binary.Read(b, binary.LittleEndian, dmvSB); err != nil {
return nil, fmt.Errorf("%w: %w", ErrSuperBlockParseFailure, err)
}
if string(bytes.Trim(dmvSB.Signature[:], "\x00")[:]) != VeritySignature {
return nil, ErrNotVeritySuperBlock
}
if s, err := r.Read(block); err != nil || s != blockSize {
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrRootHashReadFailure, err)
}
return nil, fmt.Errorf("unexpected bytes read expected=%d, actual=%d: %w", blockSize, s, ErrRootHashReadFailure)
}
rootHash := hash2(dmvSB.Salt[:dmvSB.SaltSize], block)
return &VerityInfo{
RootDigest: fmt.Sprintf("%x", rootHash),
Algorithm: string(bytes.Trim(dmvSB.Algorithm[:], "\x00")),
Salt: fmt.Sprintf("%x", dmvSB.Salt[:dmvSB.SaltSize]),
HashOffsetInBlocks: int64(dmvSB.DataBlocks),
SuperBlock: true,
DataBlocks: dmvSB.DataBlocks,
DataBlockSize: dmvSB.DataBlockSize,
HashBlockSize: blockSize,
Version: dmvSB.Version,
}, nil
}
// ComputeAndWriteHashDevice builds merkle tree from a given io.ReadSeeker and
// writes the result hash device (dm-verity super-block combined with merkle
// tree) to io.Writer.
func ComputeAndWriteHashDevice(r io.ReadSeeker, w io.Writer) error {
// save current reader position
currBytePos, err := r.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
// reset to the beginning to find the device size
if _, err := r.Seek(0, io.SeekStart); err != nil {
return err
}
tree, err := MerkleTree(r)
if err != nil {
return errors.Wrap(err, "failed to build merkle tree")
}
devSize, err := r.Seek(0, io.SeekEnd)
if err != nil {
return err
}
// reset reader to initial position
if _, err := r.Seek(currBytePos, io.SeekStart); err != nil {
return err
}
dmVeritySB := NewDMVeritySuperblock(uint64(devSize))
if err := binary.Write(w, binary.LittleEndian, dmVeritySB); err != nil {
return errors.Wrap(err, "failed to write dm-verity super-block")
}
// write super-block padding
padding := bytes.Repeat([]byte{0}, blockSize-(sbSize%blockSize))
if _, err = w.Write(padding); err != nil {
return err
}
// write tree
if _, err := w.Write(tree); err != nil {
return errors.Wrap(err, "failed to write merkle tree")
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,411 @@
package format
type SuperBlock struct {
InodesCount uint32
BlocksCountLow uint32
RootBlocksCountLow uint32
FreeBlocksCountLow uint32
FreeInodesCount uint32
FirstDataBlock uint32
LogBlockSize uint32
LogClusterSize uint32
BlocksPerGroup uint32
ClustersPerGroup uint32
InodesPerGroup uint32
Mtime uint32
Wtime uint32
MountCount uint16
MaxMountCount uint16
Magic uint16
State uint16
Errors uint16
MinorRevisionLevel uint16
LastCheck uint32
CheckInterval uint32
CreatorOS uint32
RevisionLevel uint32
DefaultReservedUid uint16
DefaultReservedGid uint16
FirstInode uint32
InodeSize uint16
BlockGroupNr uint16
FeatureCompat CompatFeature
FeatureIncompat IncompatFeature
FeatureRoCompat RoCompatFeature
UUID [16]uint8
VolumeName [16]byte
LastMounted [64]byte
AlgorithmUsageBitmap uint32
PreallocBlocks uint8
PreallocDirBlocks uint8
ReservedGdtBlocks uint16
JournalUUID [16]uint8
JournalInum uint32
JournalDev uint32
LastOrphan uint32
HashSeed [4]uint32
DefHashVersion uint8
JournalBackupType uint8
DescSize uint16
DefaultMountOpts uint32
FirstMetaBg uint32
MkfsTime uint32
JournalBlocks [17]uint32
BlocksCountHigh uint32
RBlocksCountHigh uint32
FreeBlocksCountHigh uint32
MinExtraIsize uint16
WantExtraIsize uint16
Flags uint32
RaidStride uint16
MmpInterval uint16
MmpBlock uint64
RaidStripeWidth uint32
LogGroupsPerFlex uint8
ChecksumType uint8
ReservedPad uint16
KbytesWritten uint64
SnapshotInum uint32
SnapshotID uint32
SnapshotRBlocksCount uint64
SnapshotList uint32
ErrorCount uint32
FirstErrorTime uint32
FirstErrorInode uint32
FirstErrorBlock uint64
FirstErrorFunc [32]uint8
FirstErrorLine uint32
LastErrorTime uint32
LastErrorInode uint32
LastErrorLine uint32
LastErrorBlock uint64
LastErrorFunc [32]uint8
MountOpts [64]uint8
UserQuotaInum uint32
GroupQuotaInum uint32
OverheadBlocks uint32
BackupBgs [2]uint32
EncryptAlgos [4]uint8
EncryptPwSalt [16]uint8
LpfInode uint32
ProjectQuotaInum uint32
ChecksumSeed uint32
WtimeHigh uint8
MtimeHigh uint8
MkfsTimeHigh uint8
LastcheckHigh uint8
FirstErrorTimeHigh uint8
LastErrorTimeHigh uint8
Pad [2]uint8
Reserved [96]uint32
Checksum uint32
}
const SuperBlockMagic uint16 = 0xef53
type CompatFeature uint32
type IncompatFeature uint32
type RoCompatFeature uint32
const (
CompatDirPrealloc CompatFeature = 0x1
CompatImagicInodes CompatFeature = 0x2
CompatHasJournal CompatFeature = 0x4
CompatExtAttr CompatFeature = 0x8
CompatResizeInode CompatFeature = 0x10
CompatDirIndex CompatFeature = 0x20
CompatLazyBg CompatFeature = 0x40
CompatExcludeInode CompatFeature = 0x80
CompatExcludeBitmap CompatFeature = 0x100
CompatSparseSuper2 CompatFeature = 0x200
IncompatCompression IncompatFeature = 0x1
IncompatFiletype IncompatFeature = 0x2
IncompatRecover IncompatFeature = 0x4
IncompatJournalDev IncompatFeature = 0x8
IncompatMetaBg IncompatFeature = 0x10
IncompatExtents IncompatFeature = 0x40
Incompat_64Bit IncompatFeature = 0x80
IncompatMmp IncompatFeature = 0x100
IncompatFlexBg IncompatFeature = 0x200
IncompatEaInode IncompatFeature = 0x400
IncompatDirdata IncompatFeature = 0x1000
IncompatCsumSeed IncompatFeature = 0x2000
IncompatLargedir IncompatFeature = 0x4000
IncompatInlineData IncompatFeature = 0x8000
IncompatEncrypt IncompatFeature = 0x10000
RoCompatSparseSuper RoCompatFeature = 0x1
RoCompatLargeFile RoCompatFeature = 0x2
RoCompatBtreeDir RoCompatFeature = 0x4
RoCompatHugeFile RoCompatFeature = 0x8
RoCompatGdtCsum RoCompatFeature = 0x10
RoCompatDirNlink RoCompatFeature = 0x20
RoCompatExtraIsize RoCompatFeature = 0x40
RoCompatHasSnapshot RoCompatFeature = 0x80
RoCompatQuota RoCompatFeature = 0x100
RoCompatBigalloc RoCompatFeature = 0x200
RoCompatMetadataCsum RoCompatFeature = 0x400
RoCompatReplica RoCompatFeature = 0x800
RoCompatReadonly RoCompatFeature = 0x1000
RoCompatProject RoCompatFeature = 0x2000
)
type BlockGroupFlag uint16
const (
BlockGroupInodeUninit BlockGroupFlag = 0x1
BlockGroupBlockUninit BlockGroupFlag = 0x2
BlockGroupInodeZeroed BlockGroupFlag = 0x4
)
type GroupDescriptor struct {
BlockBitmapLow uint32
InodeBitmapLow uint32
InodeTableLow uint32
FreeBlocksCountLow uint16
FreeInodesCountLow uint16
UsedDirsCountLow uint16
Flags BlockGroupFlag
ExcludeBitmapLow uint32
BlockBitmapCsumLow uint16
InodeBitmapCsumLow uint16
ItableUnusedLow uint16
Checksum uint16
}
type GroupDescriptor64 struct {
GroupDescriptor
BlockBitmapHigh uint32
InodeBitmapHigh uint32
InodeTableHigh uint32
FreeBlocksCountHigh uint16
FreeInodesCountHigh uint16
UsedDirsCountHigh uint16
ItableUnusedHigh uint16
ExcludeBitmapHigh uint32
BlockBitmapCsumHigh uint16
InodeBitmapCsumHigh uint16
Reserved uint32
}
const (
S_IXOTH = 0x1
S_IWOTH = 0x2
S_IROTH = 0x4
S_IXGRP = 0x8
S_IWGRP = 0x10
S_IRGRP = 0x20
S_IXUSR = 0x40
S_IWUSR = 0x80
S_IRUSR = 0x100
S_ISVTX = 0x200
S_ISGID = 0x400
S_ISUID = 0x800
S_IFIFO = 0x1000
S_IFCHR = 0x2000
S_IFDIR = 0x4000
S_IFBLK = 0x6000
S_IFREG = 0x8000
S_IFLNK = 0xA000
S_IFSOCK = 0xC000
TypeMask uint16 = 0xF000
)
type InodeNumber uint32
const (
InodeRoot = 2
)
type Inode struct {
Mode uint16
Uid uint16
SizeLow uint32
Atime uint32
Ctime uint32
Mtime uint32
Dtime uint32
Gid uint16
LinksCount uint16
BlocksLow uint32
Flags InodeFlag
Version uint32
Block [60]byte
Generation uint32
XattrBlockLow uint32
SizeHigh uint32
ObsoleteFragmentAddr uint32
BlocksHigh uint16
XattrBlockHigh uint16
UidHigh uint16
GidHigh uint16
ChecksumLow uint16
Reserved uint16
ExtraIsize uint16
ChecksumHigh uint16
CtimeExtra uint32
MtimeExtra uint32
AtimeExtra uint32
Crtime uint32
CrtimeExtra uint32
VersionHigh uint32
Projid uint32
}
type InodeFlag uint32
const (
InodeFlagSecRm InodeFlag = 0x1
InodeFlagUnRm InodeFlag = 0x2
InodeFlagCompressed InodeFlag = 0x4
InodeFlagSync InodeFlag = 0x8
InodeFlagImmutable InodeFlag = 0x10
InodeFlagAppend InodeFlag = 0x20
InodeFlagNoDump InodeFlag = 0x40
InodeFlagNoAtime InodeFlag = 0x80
InodeFlagDirtyCompressed InodeFlag = 0x100
InodeFlagCompressedClusters InodeFlag = 0x200
InodeFlagNoCompress InodeFlag = 0x400
InodeFlagEncrypted InodeFlag = 0x800
InodeFlagHashedIndex InodeFlag = 0x1000
InodeFlagMagic InodeFlag = 0x2000
InodeFlagJournalData InodeFlag = 0x4000
InodeFlagNoTail InodeFlag = 0x8000
InodeFlagDirSync InodeFlag = 0x10000
InodeFlagTopDir InodeFlag = 0x20000
InodeFlagHugeFile InodeFlag = 0x40000
InodeFlagExtents InodeFlag = 0x80000
InodeFlagEaInode InodeFlag = 0x200000
InodeFlagEOFBlocks InodeFlag = 0x400000
InodeFlagSnapfile InodeFlag = 0x01000000
InodeFlagSnapfileDeleted InodeFlag = 0x04000000
InodeFlagSnapfileShrunk InodeFlag = 0x08000000
InodeFlagInlineData InodeFlag = 0x10000000
InodeFlagProjectIDInherit InodeFlag = 0x20000000
InodeFlagReserved InodeFlag = 0x80000000
)
const (
MaxLinks = 65000
)
type ExtentHeader struct {
Magic uint16
Entries uint16
Max uint16
Depth uint16
Generation uint32
}
const ExtentHeaderMagic uint16 = 0xf30a
type ExtentIndexNode struct {
Block uint32
LeafLow uint32
LeafHigh uint16
Unused uint16
}
type ExtentLeafNode struct {
Block uint32
Length uint16
StartHigh uint16
StartLow uint32
}
type ExtentTail struct {
Checksum uint32
}
type DirectoryEntry struct {
Inode InodeNumber
RecordLength uint16
NameLength uint8
FileType FileType
//Name []byte
}
type FileType uint8
const (
FileTypeUnknown FileType = 0x0
FileTypeRegular FileType = 0x1
FileTypeDirectory FileType = 0x2
FileTypeCharacter FileType = 0x3
FileTypeBlock FileType = 0x4
FileTypeFIFO FileType = 0x5
FileTypeSocket FileType = 0x6
FileTypeSymbolicLink FileType = 0x7
)
type DirectoryEntryTail struct {
ReservedZero1 uint32
RecordLength uint16
ReservedZero2 uint8
FileType uint8
Checksum uint32
}
type DirectoryTreeRoot struct {
Dot DirectoryEntry
DotName [4]byte
DotDot DirectoryEntry
DotDotName [4]byte
ReservedZero uint32
HashVersion uint8
InfoLength uint8
IndirectLevels uint8
UnusedFlags uint8
Limit uint16
Count uint16
Block uint32
//Entries []DirectoryTreeEntry
}
type DirectoryTreeNode struct {
FakeInode uint32
FakeRecordLength uint16
NameLength uint8
FileType uint8
Limit uint16
Count uint16
Block uint32
//Entries []DirectoryTreeEntry
}
type DirectoryTreeEntry struct {
Hash uint32
Block uint32
}
type DirectoryTreeTail struct {
Reserved uint32
Checksum uint32
}
type XAttrInodeBodyHeader struct {
Magic uint32
}
type XAttrHeader struct {
Magic uint32
ReferenceCount uint32
Blocks uint32
Hash uint32
Checksum uint32
Reserved [3]uint32
}
const XAttrHeaderMagic uint32 = 0xea020000
type XAttrEntry struct {
NameLength uint8
NameIndex uint8
ValueOffset uint16
ValueInum uint32
ValueSize uint32
Hash uint32
//Name []byte
}

View File

@@ -0,0 +1,369 @@
package tar2ext4
import (
"archive/tar"
"bufio"
"encoding/binary"
"fmt"
"io"
"os"
"path"
"strings"
"github.com/Microsoft/hcsshim/ext4/dmverity"
"github.com/Microsoft/hcsshim/ext4/internal/compactext4"
"github.com/Microsoft/hcsshim/ext4/internal/format"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/pkg/errors"
)
type params struct {
convertWhiteout bool
convertBackslash bool
appendVhdFooter bool
onlyAppendVhdFooter bool
appendDMVerity bool
ext4opts []compactext4.Option
}
// Option is the type for optional parameters to Convert.
type Option func(*params)
// ConvertWhiteout instructs the converter to convert OCI-style whiteouts
// (beginning with .wh.) to overlay-style whiteouts.
func ConvertWhiteout(p *params) {
p.convertWhiteout = true
}
// ConvertBackslash instructs the converter to replace `\` in path names with `/`.
// This is useful if the tar file was created on Windows, where `\` is the filepath separator.
func ConvertBackslash(p *params) {
p.convertBackslash = true
}
// AppendVhdFooter instructs the converter to add a fixed VHD footer to the
// file.
func AppendVhdFooter(p *params) {
p.appendVhdFooter = true
}
// OnlyAppendVhdFooter instructs the converter not to convert but still to add a fixed VHD footer to the
// file.
func OnlyAppendVhdFooter(p *params) {
p.onlyAppendVhdFooter = true
}
// AppendDMVerity instructs the converter to add a dmverity Merkle tree for
// the ext4 filesystem after the filesystem and before the optional VHD footer
func AppendDMVerity(p *params) {
p.appendDMVerity = true
}
// InlineData instructs the converter to write small files into the inode
// structures directly. This creates smaller images but currently is not
// compatible with DAX.
func InlineData(p *params) {
p.ext4opts = append(p.ext4opts, compactext4.InlineData)
}
// MaximumDiskSize instructs the writer to limit the disk size to the specified
// value. This also reserves enough metadata space for the specified disk size.
// If not provided, then 16GB is the default.
func MaximumDiskSize(size int64) Option {
return func(p *params) {
p.ext4opts = append(p.ext4opts, compactext4.MaximumDiskSize(size))
}
}
const (
whiteoutPrefix = ".wh."
opaqueWhiteout = ".wh..wh..opq"
)
// ConvertTarToExt4 writes a compact ext4 file system image that contains the files in the
// input tar stream.
func ConvertTarToExt4(r io.Reader, w io.ReadWriteSeeker, options ...Option) error {
var p params
for _, opt := range options {
opt(&p)
}
t := tar.NewReader(bufio.NewReader(r))
fs := compactext4.NewWriter(w, p.ext4opts...)
for {
hdr, err := t.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
name := hdr.Name
linkName := hdr.Linkname
if p.convertBackslash {
// compactext assumes all paths are `/` separated
// unconditionally replace all instances of `/`, regardless of GOOS
name = strings.ReplaceAll(name, `\`, "/")
linkName = strings.ReplaceAll(linkName, `\`, "/")
}
if err = fs.MakeParents(name); err != nil {
return errors.Wrapf(err, "failed to ensure parent directories for %s", name)
}
if p.convertWhiteout {
dir, file := path.Split(name)
if strings.HasPrefix(file, whiteoutPrefix) {
if file == opaqueWhiteout {
// Update the directory with the appropriate xattr.
f, err := fs.Stat(dir)
if err != nil {
return errors.Wrapf(err, "failed to stat parent directory of whiteout %s", file)
}
f.Xattrs["trusted.overlay.opaque"] = []byte("y")
err = fs.Create(dir, f)
if err != nil {
return errors.Wrapf(err, "failed to create opaque dir %s", file)
}
} else {
// Create an overlay-style whiteout.
f := &compactext4.File{
Mode: compactext4.S_IFCHR,
Devmajor: 0,
Devminor: 0,
}
err = fs.Create(path.Join(dir, file[len(whiteoutPrefix):]), f)
if err != nil {
return errors.Wrapf(err, "failed to create whiteout file for %s", file)
}
}
continue
}
}
if hdr.Typeflag == tar.TypeLink {
err = fs.Link(linkName, name)
if err != nil {
return err
}
} else {
f := &compactext4.File{
Mode: uint16(hdr.Mode),
Atime: hdr.AccessTime,
Mtime: hdr.ModTime,
Ctime: hdr.ChangeTime,
Crtime: hdr.ModTime,
Size: hdr.Size,
Uid: uint32(hdr.Uid),
Gid: uint32(hdr.Gid),
Linkname: linkName,
Devmajor: uint32(hdr.Devmajor),
Devminor: uint32(hdr.Devminor),
Xattrs: make(map[string][]byte),
}
for key, value := range hdr.PAXRecords {
const xattrPrefix = "SCHILY.xattr."
if strings.HasPrefix(key, xattrPrefix) {
f.Xattrs[key[len(xattrPrefix):]] = []byte(value)
}
}
var typ uint16
switch hdr.Typeflag {
case tar.TypeReg:
typ = compactext4.S_IFREG
case tar.TypeSymlink:
typ = compactext4.S_IFLNK
case tar.TypeChar:
typ = compactext4.S_IFCHR
case tar.TypeBlock:
typ = compactext4.S_IFBLK
case tar.TypeDir:
typ = compactext4.S_IFDIR
case tar.TypeFifo:
typ = compactext4.S_IFIFO
}
f.Mode &= ^compactext4.TypeMask
f.Mode |= typ
err = fs.Create(name, f)
if err != nil {
return err
}
_, err = io.Copy(fs, t)
if err != nil {
return err
}
}
}
return fs.Close()
}
// Convert wraps ConvertTarToExt4 and conditionally computes (and appends) the file image's cryptographic
// hashes (merkle tree) or/and appends a VHD footer.
func Convert(r io.Reader, w io.ReadWriteSeeker, options ...Option) error {
var p params
for _, opt := range options {
opt(&p)
}
if p.onlyAppendVhdFooter {
_, err := io.Copy(w, r)
if err != nil {
return err
}
return ConvertToVhd(w)
}
if err := ConvertTarToExt4(r, w, options...); err != nil {
return err
}
if p.appendDMVerity {
if err := dmverity.ComputeAndWriteHashDevice(w, w); err != nil {
return err
}
}
if p.appendVhdFooter {
return ConvertToVhd(w)
}
return nil
}
// ReadExt4SuperBlock reads and returns ext4 super block from given device.
func ReadExt4SuperBlock(devicePath string) (*format.SuperBlock, error) {
dev, err := os.OpenFile(devicePath, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
defer dev.Close()
return ReadExt4SuperBlockReadSeeker(dev)
}
// ReadExt4SuperBlockReadSeeker reads and returns ext4 super block given
// an io.ReadSeeker.
//
// The layout on disk is as follows:
// | Group 0 padding | - 1024 bytes
// | ext4 SuperBlock | - 1 block
// | Group Descriptors | - many blocks
// | Reserved GDT Blocks | - many blocks
// | Data Block Bitmap | - 1 block
// | inode Bitmap | - 1 block
// | inode Table | - many blocks
// | Data Blocks | - many blocks
//
// More details can be found here https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout
//
// Our goal is to skip the Group 0 padding, read and return the ext4 SuperBlock
func ReadExt4SuperBlockReadSeeker(rsc io.ReadSeeker) (*format.SuperBlock, error) {
// save current reader position
currBytePos, err := rsc.Seek(0, io.SeekCurrent)
if err != nil {
return nil, err
}
if _, err := rsc.Seek(1024, io.SeekCurrent); err != nil {
return nil, err
}
var sb format.SuperBlock
if err := binary.Read(rsc, binary.LittleEndian, &sb); err != nil {
return nil, err
}
// reset the reader to initial position
if _, err := rsc.Seek(currBytePos, io.SeekStart); err != nil {
return nil, err
}
if sb.Magic != format.SuperBlockMagic {
return nil, errors.New("not an ext4 file system")
}
return &sb, nil
}
// IsDeviceExt4 is will read the device's superblock and determine if it is
// and ext4 superblock.
func IsDeviceExt4(devicePath string) bool {
// ReadExt4SuperBlock will check the superblock magic number for us,
// so we know if no error is returned, this is an ext4 device.
_, err := ReadExt4SuperBlock(devicePath)
if err != nil {
log.L.Warnf("failed to read Ext4 superblock: %s", err)
}
return err == nil
}
// Ext4FileSystemSize reads ext4 superblock and returns the size of the underlying
// ext4 file system and its block size.
func Ext4FileSystemSize(r io.ReadSeeker) (int64, int, error) {
sb, err := ReadExt4SuperBlockReadSeeker(r)
if err != nil {
return 0, 0, fmt.Errorf("failed to read ext4 superblock: %w", err)
}
blockSize := 1024 * (1 << sb.LogBlockSize)
fsSize := int64(blockSize) * int64(sb.BlocksCountLow)
return fsSize, blockSize, nil
}
// ConvertAndComputeRootDigest writes a compact ext4 file system image that contains the files in the
// input tar stream, computes the resulting file image's cryptographic hashes (merkle tree) and returns
// merkle tree root digest. Convert is called with minimal options: ConvertWhiteout and MaximumDiskSize
// set to dmverity.RecommendedVHDSizeGB.
func ConvertAndComputeRootDigest(r io.Reader) (string, error) {
out, err := os.CreateTemp("", "")
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
defer func() {
_ = os.Remove(out.Name())
}()
defer out.Close()
options := []Option{
ConvertWhiteout,
MaximumDiskSize(dmverity.RecommendedVHDSizeGB),
}
if err := ConvertTarToExt4(r, out, options...); err != nil {
return "", fmt.Errorf("failed to convert tar to ext4: %w", err)
}
if _, err := out.Seek(0, io.SeekStart); err != nil {
return "", fmt.Errorf("failed to seek start on temp file when creating merkle tree: %w", err)
}
tree, err := dmverity.MerkleTree(bufio.NewReaderSize(out, dmverity.MerkleTreeBufioSize))
if err != nil {
return "", fmt.Errorf("failed to create merkle tree: %w", err)
}
hash := dmverity.RootHash(tree)
return fmt.Sprintf("%x", hash), nil
}
// ConvertToVhd converts given io.WriteSeeker to VHD, by appending the VHD footer with a fixed size.
func ConvertToVhd(w io.WriteSeeker) error {
size, err := w.Seek(0, io.SeekEnd)
if err != nil {
return err
}
return binary.Write(w, binary.BigEndian, makeFixedVHDFooter(size))
}
// A convenience wrapper for ConverToVhd, instead of asking the caller to open the file and pass an io.WriteSeeker, this
// takes in a file path and appends the VHD footer to that file.
func ConvertFileToVhd(filePath string) error {
f, err := os.OpenFile(filePath, os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open file `%s` : %w", filePath, err)
}
defer f.Close()
if err := ConvertToVhd(f); err != nil {
return fmt.Errorf("failed to append VHD footer: %w", err)
}
return nil
}

View File

@@ -0,0 +1,76 @@
package tar2ext4
import (
"bytes"
"crypto/rand"
"encoding/binary"
)
// Constants for the VHD footer
const (
cookieMagic = "conectix"
featureMask = 0x2
fileFormatVersionMagic = 0x00010000
fixedDataOffset = -1
creatorVersionMagic = 0x000a0000
diskTypeFixed = 2
)
type vhdFooter struct {
Cookie [8]byte
Features uint32
FileFormatVersion uint32
DataOffset int64
TimeStamp uint32
CreatorApplication [4]byte
CreatorVersion uint32
CreatorHostOS [4]byte
OriginalSize int64
CurrentSize int64
DiskGeometry uint32
DiskType uint32
Checksum uint32
UniqueID [16]uint8
SavedState uint8
Reserved [427]uint8
}
func makeFixedVHDFooter(size int64) *vhdFooter {
footer := &vhdFooter{
Features: featureMask,
FileFormatVersion: fileFormatVersionMagic,
DataOffset: fixedDataOffset,
CreatorVersion: creatorVersionMagic,
OriginalSize: size,
CurrentSize: size,
DiskType: diskTypeFixed,
UniqueID: generateUUID(),
}
copy(footer.Cookie[:], cookieMagic)
footer.Checksum = calculateCheckSum(footer)
return footer
}
func calculateCheckSum(footer *vhdFooter) uint32 {
oldchk := footer.Checksum
footer.Checksum = 0
buf := &bytes.Buffer{}
_ = binary.Write(buf, binary.BigEndian, footer)
var chk uint32
bufBytes := buf.Bytes()
for i := 0; i < len(bufBytes); i++ {
chk += uint32(bufBytes[i])
}
footer.Checksum = oldchk
return uint32(^chk)
}
func generateUUID() [16]byte {
res := [16]byte{}
if _, err := rand.Read(res[:]); err != nil {
panic(err)
}
return res
}

View File

@@ -0,0 +1,157 @@
//go:build windows
package cim
import (
"context"
"fmt"
"path/filepath"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/wclayer"
"github.com/Microsoft/hcsshim/pkg/cimfs"
)
// A BlockCIMLayerWriter implements the CIMLayerWriter interface to allow writing
// container image layers in the blocked cim format.
type BlockCIMLayerWriter struct {
*cimLayerWriter
// the layer that we are writing
layer *cimfs.BlockCIM
// parent layers
parentLayers []*cimfs.BlockCIM
// added files maintains a map of all files that have been added to this layer
addedFiles map[string]struct{}
}
var _ CIMLayerWriter = &BlockCIMLayerWriter{}
// NewBlockCIMLayerWriterWithOpts returns a writer for writing image layers in the block CIM format. The writer's behavior can be
// controlled with the supports opts.
func NewBlockCIMLayerWriterWithOpts(ctx context.Context, layer *cimfs.BlockCIM, parentLayers []*cimfs.BlockCIM, opts ...cimfs.BlockCIMOpt) (_ *BlockCIMLayerWriter, err error) {
if layer.Type != cimfs.BlockCIMTypeSingleFile {
// we only support writing single file CIMs for now because in layer
// writing process we still need to write some files (registry hives)
// outside the CIM. We currently use the parent directory of the CIM (i.e
// the parent directory of block path in this case) for this. This can't
// be reliably done with the block device CIM since the block path
// provided will be a volume path. However, once we get rid of hive rollup
// step during layer import we should be able to support block device
// CIMs.
return nil, ErrBlockCIMWriterNotSupported
}
parentLayerPaths := make([]string, 0, len(parentLayers))
for _, pl := range parentLayers {
if pl.Type != layer.Type {
return nil, ErrBlockCIMParentTypeMismatch
}
parentLayerPaths = append(parentLayerPaths, filepath.Dir(pl.BlockPath))
}
// We always want to write layers with consistent flag
bcimOpts := append([]cimfs.BlockCIMOpt{cimfs.WithConsistentCIM()}, opts...)
cim, err := cimfs.CreateBlockCIMWithOptions(ctx, layer, bcimOpts...)
if err != nil {
return nil, fmt.Errorf("error in creating a new cim: %w", err)
}
defer func() {
if err != nil {
cErr := cim.Close()
if cErr != nil {
log.G(ctx).WithError(err).Warnf("failed to close cim after error: %s", cErr)
}
}
}()
// std file writer writes registry hives outside the CIM for 2 reasons. 1. We can
// merge the hives of this layer with the parent layer hives and then write the
// merged hives into the CIM. 2. When importing child layer of this layer, we
// have access to the merges hives of this layer.
sfw, err := newStdFileWriter(filepath.Dir(layer.BlockPath), parentLayerPaths)
if err != nil {
return nil, fmt.Errorf("error in creating new standard file writer: %w", err)
}
return &BlockCIMLayerWriter{
layer: layer,
parentLayers: parentLayers,
addedFiles: make(map[string]struct{}),
cimLayerWriter: &cimLayerWriter{
ctx: ctx,
cimWriter: cim,
stdFileWriter: sfw,
layerPath: filepath.Dir(layer.BlockPath),
parentLayerPaths: parentLayerPaths,
},
}, nil
}
// NewBlockCIMLayerWriter writes the layer files in the block CIM format.
func NewBlockCIMLayerWriter(ctx context.Context, layer *cimfs.BlockCIM, parentLayers []*cimfs.BlockCIM) (_ *BlockCIMLayerWriter, err error) {
return NewBlockCIMLayerWriterWithOpts(ctx, layer, parentLayers)
}
// Add adds a file to the layer with given metadata.
func (cw *BlockCIMLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
cw.addedFiles[name] = struct{}{}
if name == wclayer.UtilityVMPath && len(cw.parentLayers) > 0 {
// If there are UtilityVM files in non base layers, we will have to merge
// those files with the parent layer UtilityVM files - either during image
// pull or at runtime (i.e when starting the UVM). In order to merge at
// image pull time, we will have to read parent layer block CIMs and copy
// all the UtilityVM files from those CIMs into this block CIM one by one
// i.e effectively merge all parent layer UtilityVM files in this
// layer. Or we will need to be able to boot the UtilityVM with merged
// block CIMs. None of these options are implemented yet so log a
// warning. However, this shouldn't cause issues with most of the standard
// use cases because usually the pod is a nanoserver image and that is
// always a single layer.
log.G(cw.ctx).Warn("UtilityVM files in non base layers is not supported for block CIMs")
}
return cw.cimLayerWriter.Add(name, fileInfo, fileSize, securityDescriptor, extendedAttributes, reparseData)
}
// Remove removes a file that was present in a parent layer from the layer.
func (cw *BlockCIMLayerWriter) Remove(name string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil
err := cw.cimWriter.AddTombstone(name)
if err != nil {
return fmt.Errorf("failed to remove file : %w", err)
}
return nil
}
// AddLink adds a hard link to the layer. Note that the link added here is evaluated only
// at the CIM merge time. So an invalid link will not throw an error here.
func (cw *BlockCIMLayerWriter) AddLink(name string, target string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil
// when adding links to a block CIM, we need to know if the target file is present
// in this same block CIM or if it is coming from one of the parent layers. If the
// file is in the same CIM we add a standard hard link. If the file is not in the
// same CIM we add a special type of link called merged link. This merged link is
// resolved when all the individual block CIM layers are merged. In order to
// reliably know if the target is a part of the CIM or not, we wait until all
// files are added and then lookup the added entries in a map to make the
// decision.
pendingLinkOp := func(c *cimfs.CimFsWriter) error {
if _, ok := cw.addedFiles[target]; ok {
// target was added in this layer - add a normal link. Once a
// hardlink is added that hardlink also becomes a valid target for
// other links so include it in the map.
cw.addedFiles[name] = struct{}{}
return c.AddLink(target, name)
} else {
// target is from a parent layer - add a merged link
return c.AddMergedLink(target, name)
}
}
cw.pendingOps = append(cw.pendingOps, pendingCimOpFunc(pendingLinkOp))
return nil
}

View File

@@ -0,0 +1,204 @@
//go:build windows
package cim
import (
"context"
"fmt"
"io"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/wclayer"
"github.com/Microsoft/hcsshim/pkg/cimfs"
)
var (
ErrBlockCIMWriterNotSupported = fmt.Errorf("writing block device CIM isn't supported")
ErrBlockCIMParentTypeMismatch = fmt.Errorf("parent layer block CIM type doesn't match with extraction layer")
ErrBlockCIMIntegrityMismatch = fmt.Errorf("verified CIMs can not be mixed with non verified CIMs")
)
type hive struct {
name string
base string
delta string
}
var (
hives = []hive{
{"SYSTEM", "SYSTEM_BASE", "SYSTEM_DELTA"},
{"SOFTWARE", "SOFTWARE_BASE", "SOFTWARE_DELTA"},
{"SAM", "SAM_BASE", "SAM_DELTA"},
{"SECURITY", "SECURITY_BASE", "SECURITY_DELTA"},
{"DEFAULT", "DEFAULTUSER_BASE", "DEFAULTUSER_DELTA"},
}
)
// CIMLayerWriter is an interface that supports writing a new container image layer to the
// CIM format
type CIMLayerWriter interface {
// Add adds a file to the layer with given metadata.
Add(string, *winio.FileBasicInfo, int64, []byte, []byte, []byte) error
// AddLink adds a hard link to the layer. The target must already have been added.
AddLink(string, string) error
// AddAlternateStream adds an alternate stream to a file
AddAlternateStream(string, uint64) error
// Remove removes a file that was present in a parent layer from the layer.
Remove(string) error
// Write writes data to the current file. The data must be in the format of a Win32
// backup stream.
Write([]byte) (int, error)
// Close finishes the layer writing process and releases any resources.
Close(context.Context) error
}
func isDeltaOrBaseHive(path string) bool {
for _, hv := range hives {
if strings.EqualFold(path, filepath.Join(wclayer.HivesPath, hv.delta)) ||
strings.EqualFold(path, filepath.Join(wclayer.RegFilesPath, hv.name)) {
return true
}
}
return false
}
// checks if this particular file should be written with a stdFileWriter instead of
// using the cimWriter.
func isStdFile(path string) bool {
return (isDeltaOrBaseHive(path) ||
path == filepath.Join(wclayer.UtilityVMPath, wclayer.RegFilesPath, "SYSTEM") ||
path == filepath.Join(wclayer.UtilityVMPath, wclayer.RegFilesPath, "SOFTWARE") ||
path == wclayer.BcdFilePath || path == wclayer.BootMgrFilePath)
}
// cimLayerWriter is a base struct that is further extended by forked cim writer & blocked
// cim writer to provide full functionality of writing layers.
type cimLayerWriter struct {
ctx context.Context
// Handle to the layer cim - writes to the cim file
cimWriter *cimfs.CimFsWriter
// Handle to the writer for writing files in the local filesystem
stdFileWriter *stdFileWriter
// reference to currently active writer either cimWriter or stdFileWriter
activeWriter io.Writer
// denotes if this layer has the UtilityVM directory
hasUtilityVM bool
// path to the layer (i.e layer's directory) as provided by the caller.
// Even if a layer is stored as a cim in the cim directory, some files associated
// with a layer are still stored in this path.
layerPath string
// parent layer paths
parentLayerPaths []string
// some files are written outside the cim during initial import (via stdFileWriter) because we need to
// make some modifications to these files before writing them to the cim. The pendingOps slice
// maintains a list of such delayed modifications to the layer cim. These modifications are applied at
// the very end of layer import process.
pendingOps []pendingCimOp
}
// Add adds a file to the layer with given metadata.
func (cw *cimLayerWriter) Add(name string, fileInfo *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
if name == wclayer.UtilityVMPath {
cw.hasUtilityVM = true
}
if isStdFile(name) {
// create a pending op for this file
cw.pendingOps = append(cw.pendingOps, &addOp{
pathInCim: name,
hostPath: filepath.Join(cw.layerPath, name),
fileInfo: fileInfo,
securityDescriptor: securityDescriptor,
extendedAttributes: extendedAttributes,
reparseData: reparseData,
})
if err := cw.stdFileWriter.Add(name); err != nil {
return err
}
cw.activeWriter = cw.stdFileWriter
} else {
if err := cw.cimWriter.AddFile(name, fileInfo, fileSize, securityDescriptor, extendedAttributes, reparseData); err != nil {
return err
}
cw.activeWriter = cw.cimWriter
}
return nil
}
// AddLink adds a hard link to the layer. The target must already have been added.
func (cw *cimLayerWriter) AddLink(name string, target string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil
if isStdFile(target) {
// If this is a link to a std file it will have to be added later once the
// std file is written to the CIM. Create a pending op for this
cw.pendingOps = append(cw.pendingOps, &linkOp{
oldPath: target,
newPath: name,
})
return nil
} else if isStdFile(name) {
// None of the predefined std files are links. If they show up as links this is unexpected
// behavior. Error out.
return fmt.Errorf("unexpected link %s in layer", name)
} else {
return cw.cimWriter.AddLink(target, name)
}
}
// AddAlternateStream creates another alternate stream at the given
// path. Any writes made after this call will go to that stream.
func (cw *cimLayerWriter) AddAlternateStream(name string, size uint64) error {
if isStdFile(name) {
// As of now there is no known case of std file having multiple data streams.
// If such a file is encountered our assumptions are wrong. Error out.
return fmt.Errorf("unexpected alternate stream %s in layer", name)
}
if err := cw.cimWriter.CreateAlternateStream(name, size); err != nil {
return err
}
cw.activeWriter = cw.cimWriter
return nil
}
// Write writes data to the current file. The data must be in the format of a Win32
// backup stream.
func (cw *cimLayerWriter) Write(b []byte) (int, error) {
return cw.activeWriter.Write(b)
}
// Close finishes the layer writing process and releases any resources.
func (cw *cimLayerWriter) Close(ctx context.Context) (retErr error) {
if err := cw.stdFileWriter.Close(ctx); err != nil {
return err
}
// cimWriter must be closed even if there are errors.
defer func() {
if err := cw.cimWriter.Close(); retErr == nil {
retErr = err
}
}()
// We don't support running UtilityVM with CIM layers yet.
processUtilityVM := false
if len(cw.parentLayerPaths) == 0 {
if err := cw.processBaseLayer(ctx, processUtilityVM); err != nil {
return fmt.Errorf("process base layer: %w", err)
}
} else {
if err := cw.processNonBaseLayer(ctx, processUtilityVM); err != nil {
return fmt.Errorf("process non base layer: %w", err)
}
}
for _, op := range cw.pendingOps {
if err := op.apply(cw.cimWriter); err != nil {
return fmt.Errorf("apply pending operations: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,3 @@
// This package provides utilities for working with container image layers in the cim format
// via the wclayer APIs.
package cim

View File

@@ -0,0 +1,93 @@
//go:build windows
package cim
import (
"context"
"fmt"
"os"
"path/filepath"
"syscall"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/safefile"
"github.com/Microsoft/hcsshim/internal/winapi"
)
// stdFileWriter writes the files of a layer to the layer folder instead of writing them inside the cim.
// For some files (like the Hive files or some UtilityVM files) it is necessary to write them as a normal file
// first, do some modifications on them (for example merging of hives or processing of UtilityVM files)
// and then write the modified versions into the cim. This writer is used for such files.
type stdFileWriter struct {
activeFile *os.File
// parent layer paths
parentLayerPaths []string
// path to the current layer
path string
// the open handle to the path directory
root *os.File
}
func newStdFileWriter(root string, parentRoots []string) (sfw *stdFileWriter, err error) {
sfw = &stdFileWriter{
path: root,
parentLayerPaths: parentRoots,
}
sfw.root, err = safefile.OpenRoot(root)
if err != nil {
return
}
return
}
func (sfw *stdFileWriter) closeActiveFile() (err error) {
if sfw.activeFile != nil {
err = sfw.activeFile.Close()
sfw.activeFile = nil
}
return
}
// Adds a new file or an alternate data stream to an existing file inside the layer directory.
func (sfw *stdFileWriter) Add(name string) error {
if err := sfw.closeActiveFile(); err != nil {
return err
}
// The directory of this file might be created inside the cim.
// make sure we have the same parent directory chain here
if err := safefile.MkdirAllRelative(filepath.Dir(name), sfw.root); err != nil {
return fmt.Errorf("failed to create file %s: %w", name, err)
}
f, err := safefile.OpenRelative(
name,
sfw.root,
syscall.GENERIC_READ|syscall.GENERIC_WRITE|winio.WRITE_DAC|winio.WRITE_OWNER,
syscall.FILE_SHARE_READ,
winapi.FILE_CREATE,
0,
)
if err != nil {
return fmt.Errorf("error creating file %s: %w", name, err)
}
sfw.activeFile = f
return nil
}
// Write writes data to the current file. The data must be in the format of a Win32
// backup stream.
func (sfw *stdFileWriter) Write(b []byte) (int, error) {
return sfw.activeFile.Write(b)
}
// Close finishes the layer writing process and releases any resources.
func (sfw *stdFileWriter) Close(ctx context.Context) error {
if err := sfw.closeActiveFile(); err != nil {
return fmt.Errorf("failed to close active file %s : %w", sfw.activeFile.Name(), err)
}
if err := sfw.root.Close(); err != nil {
return fmt.Errorf("failed to close root dir: %w", err)
}
return nil
}

View File

@@ -0,0 +1,78 @@
//go:build windows
package cim
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/pkg/cimfs"
)
// A ForkedCimLayerWriter implements the wclayer.LayerWriter interface to allow writing container
// image layers in the cim format.
// A cim layer consist of cim files (which are usually stored in the `cim-layers` directory and
// some other files which are stored in the directory of that layer (i.e the `path` directory).
type ForkedCimLayerWriter struct {
*cimLayerWriter
}
var _ CIMLayerWriter = &ForkedCimLayerWriter{}
func NewForkedCimLayerWriter(ctx context.Context, layerPath, cimPath string, parentLayerPaths, parentLayerCimPaths []string) (_ *ForkedCimLayerWriter, err error) {
if !cimfs.IsCimFSSupported() {
return nil, fmt.Errorf("CimFs not supported on this build")
}
parentCim := ""
if len(parentLayerPaths) > 0 {
// We only need to provide parent CIM name, it is assumed that both parent CIM
// and newly created CIM are present in the same directory.
parentCim = filepath.Base(parentLayerCimPaths[0])
}
cim, err := cimfs.Create(filepath.Dir(cimPath), parentCim, filepath.Base(cimPath))
if err != nil {
return nil, fmt.Errorf("error in creating a new cim: %w", err)
}
defer func() {
if err != nil {
cErr := cim.Close()
if cErr != nil {
log.G(ctx).WithError(err).Warnf("failed to close cim after error: %s", cErr)
}
cErr = cimfs.DestroyCim(ctx, cimPath)
if cErr != nil {
log.G(ctx).WithError(err).Warnf("failed to cleanup cim after error: %s", cErr)
}
}
}()
sfw, err := newStdFileWriter(layerPath, parentLayerPaths)
if err != nil {
return nil, fmt.Errorf("error in creating new standard file writer: %w", err)
}
return &ForkedCimLayerWriter{
cimLayerWriter: &cimLayerWriter{
parentLayerPaths: parentLayerPaths,
ctx: ctx,
cimWriter: cim,
stdFileWriter: sfw,
layerPath: layerPath,
},
}, nil
}
// Remove removes a file that was present in a parent layer from the layer.
func (cw *ForkedCimLayerWriter) Remove(name string) error {
// set active write to nil so that we panic if layer tar is incorrectly formatted.
cw.activeWriter = nil
err := cw.cimWriter.Unlink(name)
if err == nil || os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to remove file: %w", err)
}

View File

@@ -0,0 +1,150 @@
//go:build windows
package cim
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio/pkg/guid"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/oc"
cimfs "github.com/Microsoft/hcsshim/pkg/cimfs"
"github.com/sirupsen/logrus"
"go.opencensus.io/trace"
)
var cimMountNamespace guid.GUID = guid.GUID{Data1: 0x6827367b, Data2: 0xc388, Data3: 0x4e9b, Data4: [8]byte{0x96, 0x1c, 0x6d, 0x2c, 0x93, 0x6c}}
// MountForkedCimLayer mounts the cim at path `cimPath` and returns the mount location of
// that cim. The containerID is used to generate the volumeID for the volume at which
// this CIM is mounted. containerID is used so that if the shim process crashes for any
// reason, the mounted cim can be correctly cleaned up during `shim delete` call.
func MountForkedCimLayer(ctx context.Context, cimPath, containerID string) (string, error) {
volumeGUID, err := guid.NewV5(cimMountNamespace, []byte(containerID))
if err != nil {
return "", fmt.Errorf("generated cim mount GUID: %w", err)
}
vol, err := cimfs.Mount(cimPath, volumeGUID, 0)
if err != nil {
return "", err
}
return vol, nil
}
// MountBlockCIMLayer mounts the given block cim and returns the mount
// location of that cim. The containerID is used to generate the volumeID for the volume
// at which this CIM is mounted. containerID is used so that if the shim process crashes
// for any reason, the mounted cim can be correctly cleaned up during `shim delete` call.
func MountBlockCIMLayer(ctx context.Context, layer *cimfs.BlockCIM, containerID string) (_ string, err error) {
ctx, span := oc.StartSpan(ctx, "MountBlockCIMLayer")
defer func() {
oc.SetSpanStatus(span, err)
span.End()
}()
span.AddAttributes(
trace.StringAttribute("layer", layer.String()))
var mountFlags uint32
switch layer.Type {
case cimfs.BlockCIMTypeDevice:
mountFlags |= cimfs.CimMountBlockDeviceCim
case cimfs.BlockCIMTypeSingleFile:
mountFlags |= cimfs.CimMountSingleFileCim
default:
return "", fmt.Errorf("invalid BlockCIMType for merged layer: %w", os.ErrInvalid)
}
volumeGUID, err := guid.NewV5(cimMountNamespace, []byte(containerID))
if err != nil {
return "", fmt.Errorf("generated cim mount GUID: %w", err)
}
cimPath := filepath.Join(layer.BlockPath, layer.CimName)
log.G(ctx).WithFields(logrus.Fields{
"flags": mountFlags,
"volume": volumeGUID.String(),
}).Debug("mounting block layer CIM")
vol, err := cimfs.Mount(cimPath, volumeGUID, mountFlags)
if err != nil {
return "", err
}
return vol, nil
}
// MergeMountBlockCIMLayer mounts the given merged block cim and returns the mount
// location of that cim. The containerID is used to generate the volumeID for the volume
// at which this CIM is mounted. containerID is used so that if the shim process crashes
// for any reason, the mounted cim can be correctly cleaned up during `shim delete` call.
// parentLayers MUST be in the base to topmost order. I.e base layer should be at index 0
// and immediate parent MUST be at the last index.
func MergeMountBlockCIMLayer(ctx context.Context, mergedLayer *cimfs.BlockCIM, parentLayers []*cimfs.BlockCIM, containerID string) (_ string, err error) {
_, span := oc.StartSpan(ctx, "MergeMountBlockCIMLayer")
defer func() {
oc.SetSpanStatus(span, err)
span.End()
}()
span.AddAttributes(
trace.StringAttribute("merged layer", mergedLayer.String()),
trace.StringAttribute("parent layers", fmt.Sprintf("%v", parentLayers)))
var mountFlags uint32
switch mergedLayer.Type {
case cimfs.BlockCIMTypeDevice:
mountFlags |= cimfs.CimMountBlockDeviceCim
case cimfs.BlockCIMTypeSingleFile:
mountFlags |= cimfs.CimMountSingleFileCim
default:
return "", fmt.Errorf("invalid BlockCIMType for merged layer: %w", os.ErrInvalid)
}
volumeGUID, err := guid.NewV5(cimMountNamespace, []byte(containerID))
if err != nil {
return "", fmt.Errorf("generated cim mount GUID: %w", err)
}
return cimfs.MountMergedBlockCIMs(mergedLayer, parentLayers, mountFlags, volumeGUID)
}
// Unmounts the cim mounted at the given volume
func UnmountCimLayer(ctx context.Context, volume string) error {
return cimfs.Unmount(volume)
}
func CleanupContainerMounts(containerID string) error {
volumeGUID, err := guid.NewV5(cimMountNamespace, []byte(containerID))
if err != nil {
return fmt.Errorf("generated cim mount GUID: %w", err)
}
volPath := fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String())
log.L.WithFields(logrus.Fields{
"volume": volPath,
"containerID": containerID,
}).Debug("cleanup container CIM mounts")
if _, err := os.Stat(volPath); err == nil {
err = cimfs.Unmount(volPath)
if err != nil {
return err
}
}
return nil
}
// LayerID provides a unique GUID for each mounted CIM volume.
func LayerID(vol string) (string, error) {
// since each mounted volume has a unique GUID, just return the same GUID as ID
if !strings.HasPrefix(vol, "\\\\?\\Volume{") || !strings.HasSuffix(vol, "}\\") {
return "", fmt.Errorf("volume path %s is not in the expected format", vol)
} else {
return strings.TrimSuffix(strings.TrimPrefix(vol, "\\\\?\\Volume{"), "}\\"), nil
}
}

View File

@@ -0,0 +1,75 @@
//go:build windows
package cim
import (
"fmt"
"io"
"os"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/pkg/cimfs"
"golang.org/x/sys/windows"
)
type pendingCimOp interface {
apply(cw *cimfs.CimFsWriter) error
}
type pendingCimOpFunc func(cw *cimfs.CimFsWriter) error
func (f pendingCimOpFunc) apply(cw *cimfs.CimFsWriter) error {
return f(cw)
}
// add op represents a pending operation of adding a new file inside the cim
type addOp struct {
// path inside the cim at which the file should be added
pathInCim string
// host path where this file was temporarily written.
hostPath string
// other file metadata fields that were provided during the add call.
fileInfo *winio.FileBasicInfo
securityDescriptor []byte
extendedAttributes []byte
reparseData []byte
}
func (o *addOp) apply(cw *cimfs.CimFsWriter) error {
f, err := os.Open(o.hostPath)
if err != nil {
return fmt.Errorf("open file %s: %w", o.hostPath, err)
}
defer f.Close()
fs, err := f.Stat()
if err != nil {
return fmt.Errorf("stat file %s: %w", o.hostPath, err)
}
if err := cw.AddFile(o.pathInCim, o.fileInfo, fs.Size(), o.securityDescriptor, o.extendedAttributes, o.reparseData); err != nil {
return fmt.Errorf("cim add file %s: %w", o.hostPath, err)
}
if o.fileInfo.FileAttributes != windows.FILE_ATTRIBUTE_DIRECTORY {
written, err := io.Copy(cw, f)
if err != nil {
return fmt.Errorf("write file %s inside cim: %w", o.hostPath, err)
} else if written != fs.Size() {
return fmt.Errorf("short write to cim for file %s, expected %d bytes wrote %d", o.hostPath, fs.Size(), written)
}
}
return nil
}
// linkOp represents a pending link file operation inside the cim
type linkOp struct {
// old & new paths inside the cim where the link should be created
oldPath string
newPath string
}
func (o *linkOp) apply(cw *cimfs.CimFsWriter) error {
return cw.AddLink(o.oldPath, o.newPath)
}

View File

@@ -0,0 +1,138 @@
//go:build windows
package cim
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/wclayer"
"golang.org/x/sys/windows"
)
// processUtilityVMLayer will handle processing of UVM specific files when we start
// supporting UVM based containers with CimFS in the future.
func processUtilityVMLayer(ctx context.Context, layerPath string) error {
return nil
}
// processBaseLayerHives make the base layer specific modifications on the hives and emits equivalent the
// pendingCimOps that should be applied on the CIM. In base layer we need to create hard links from registry
// hives under Files/Windows/Sysetm32/config into Hives/*_BASE. This function creates these links outside so
// that the registry hives under Hives/ are available during children layers import. Then we write these hive
// files inside the cim and create links inside the cim.
func processBaseLayerHives(layerPath string) ([]pendingCimOp, error) {
pendingOps := []pendingCimOp{}
// make hives directory both outside and in the cim
if err := os.Mkdir(filepath.Join(layerPath, wclayer.HivesPath), 0755); err != nil {
return pendingOps, fmt.Errorf("hives directory creation: %w", err)
}
hivesDirInfo := &winio.FileBasicInfo{
FileAttributes: windows.FILE_ATTRIBUTE_DIRECTORY,
}
pendingOps = append(pendingOps, &addOp{
pathInCim: wclayer.HivesPath,
hostPath: filepath.Join(layerPath, wclayer.HivesPath),
fileInfo: hivesDirInfo,
})
// add hard links from base hive files.
for _, hv := range hives {
oldHivePathRelative := filepath.Join(wclayer.RegFilesPath, hv.name)
newHivePathRelative := filepath.Join(wclayer.HivesPath, hv.base)
if err := os.Link(filepath.Join(layerPath, oldHivePathRelative), filepath.Join(layerPath, newHivePathRelative)); err != nil {
return pendingOps, fmt.Errorf("hive link creation: %w", err)
}
pendingOps = append(pendingOps, &linkOp{
oldPath: oldHivePathRelative,
newPath: newHivePathRelative,
})
}
return pendingOps, nil
}
// processLayoutFile creates a file named "layout" in the root of the base layer. This allows certain
// container startup related functions to understand that the hives are a part of the container rootfs.
func processLayoutFile(layerPath string) ([]pendingCimOp, error) {
fileContents := "vhd-with-hives\n"
if err := os.WriteFile(filepath.Join(layerPath, "layout"), []byte(fileContents), 0755); err != nil {
return []pendingCimOp{}, fmt.Errorf("write layout file: %w", err)
}
layoutFileInfo := &winio.FileBasicInfo{
FileAttributes: windows.FILE_ATTRIBUTE_NORMAL,
}
op := &addOp{
pathInCim: "layout",
hostPath: filepath.Join(layerPath, "layout"),
fileInfo: layoutFileInfo,
}
return []pendingCimOp{op}, nil
}
// Some of the layer files that are generated during the processBaseLayer call must be added back
// inside the cim, some registry file links must be updated. This function takes care of all those
// steps. This function opens the cim file for writing and updates it.
func (cw *cimLayerWriter) processBaseLayer(ctx context.Context, processUtilityVM bool) (err error) {
if processUtilityVM {
if err = processUtilityVMLayer(ctx, cw.layerPath); err != nil {
return fmt.Errorf("process utilityVM layer: %w", err)
}
}
ops, err := processBaseLayerHives(cw.layerPath)
if err != nil {
return err
}
cw.pendingOps = append(cw.pendingOps, ops...)
ops, err = processLayoutFile(cw.layerPath)
if err != nil {
return err
}
cw.pendingOps = append(cw.pendingOps, ops...)
return nil
}
// processNonBaseLayer takes care of the processing required for a non base layer. As of now
// the only processing required for non base layer is to merge the delta registry hives of the
// non-base layer with it's parent layer.
func (cw *cimLayerWriter) processNonBaseLayer(ctx context.Context, processUtilityVM bool) (err error) {
for _, hv := range hives {
baseHive := filepath.Join(wclayer.HivesPath, hv.base)
deltaHive := filepath.Join(wclayer.HivesPath, hv.delta)
_, err := os.Stat(filepath.Join(cw.layerPath, deltaHive))
// merge with parent layer if delta exists.
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("stat delta hive %s: %w", filepath.Join(cw.layerPath, deltaHive), err)
} else if err == nil {
// merge base hive of parent layer with the delta hive of this layer and write it as
// the base hive of this layer.
err = mergeHive(filepath.Join(cw.parentLayerPaths[0], baseHive), filepath.Join(cw.layerPath, deltaHive), filepath.Join(cw.layerPath, baseHive))
if err != nil {
return err
}
// the newly created merged file must be added to the cim
cw.pendingOps = append(cw.pendingOps, &addOp{
pathInCim: baseHive,
hostPath: filepath.Join(cw.layerPath, baseHive),
fileInfo: &winio.FileBasicInfo{
FileAttributes: windows.FILE_ATTRIBUTE_NORMAL,
},
})
}
}
if processUtilityVM {
return processUtilityVMLayer(ctx, cw.layerPath)
}
return nil
}

View File

@@ -0,0 +1,49 @@
//go:build windows
package cim
import (
"fmt"
"github.com/Microsoft/hcsshim/internal/winapi"
"github.com/Microsoft/hcsshim/osversion"
"github.com/pkg/errors"
)
// mergeHive merges the hive located at parentHivePath with the hive located at deltaHivePath and stores
// the result into the file at mergedHivePath. If a file already exists at path `mergedHivePath` then it
// throws an error.
func mergeHive(parentHivePath, deltaHivePath, mergedHivePath string) (err error) {
var baseHive, deltaHive, mergedHive winapi.ORHKey
if err := winapi.OROpenHive(parentHivePath, &baseHive); err != nil {
return fmt.Errorf("failed to open base hive %s: %w", parentHivePath, err)
}
defer func() {
err2 := winapi.ORCloseHive(baseHive)
if err == nil {
err = errors.Wrap(err2, "failed to close base hive")
}
}()
if err := winapi.OROpenHive(deltaHivePath, &deltaHive); err != nil {
return fmt.Errorf("failed to open delta hive %s: %w", deltaHivePath, err)
}
defer func() {
err2 := winapi.ORCloseHive(deltaHive)
if err == nil {
err = errors.Wrap(err2, "failed to close delta hive")
}
}()
if err := winapi.ORMergeHives([]winapi.ORHKey{baseHive, deltaHive}, &mergedHive); err != nil {
return fmt.Errorf("failed to merge hives: %w", err)
}
defer func() {
err2 := winapi.ORCloseHive(mergedHive)
if err == nil {
err = errors.Wrap(err2, "failed to close merged hive")
}
}()
if err := winapi.ORSaveHive(mergedHive, mergedHivePath, uint32(osversion.Get().MajorVersion), uint32(osversion.Get().MinorVersion)); err != nil {
return fmt.Errorf("failed to save hive: %w", err)
}
return
}

View File

@@ -0,0 +1,542 @@
//go:build windows
// +build windows
package cimfs
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"unsafe"
"github.com/Microsoft/go-winio"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/winapi"
winapitypes "github.com/Microsoft/hcsshim/internal/winapi/types"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
// CimFsWriter represents a writer to a single CimFS filesystem instance. On disk, the
// image is composed of a filesystem file and several object ID and region files.
// Note: The CimFsWriter isn't thread safe!
type CimFsWriter struct {
// name of this cim. Usually a <name>.cim file will be created to represent this cim.
name string
// handle is the CIMFS_IMAGE_HANDLE that must be passed when calling CIMFS APIs.
handle winapitypes.FsHandle
// name of the active file i.e the file to which we are currently writing.
activeName string
// stream to currently active file.
activeStream winapitypes.StreamHandle
// amount of bytes that can be written to the activeStream.
activeLeft uint64
// if true the CIM will be sealed after the writer is closed.
sealOnClose bool
}
// Create creates a new cim image. The CimFsWriter returned can then be used to do
// operations on this cim. If `oldFSName` is provided the new image is "forked" from the
// CIM with name `oldFSName` located under `imagePath`.
func Create(imagePath string, oldFSName string, newFSName string) (_ *CimFsWriter, err error) {
var oldNameBytes *uint16
// CimCreateImage API call has different behavior if the value of oldNameBytes / newNameBytes
// is empty than if it is nil. So we have to convert those strings into *uint16 here.
fsName := oldFSName
if oldFSName != "" {
oldNameBytes, err = windows.UTF16PtrFromString(oldFSName)
if err != nil {
return nil, err
}
}
var newNameBytes *uint16
if newFSName != "" {
fsName = newFSName
newNameBytes, err = windows.UTF16PtrFromString(newFSName)
if err != nil {
return nil, err
}
}
var handle winapitypes.FsHandle
if err := winapi.CimCreateImage(imagePath, oldNameBytes, newNameBytes, &handle); err != nil {
return nil, fmt.Errorf("failed to create cim image at path %s, oldName: %s, newName: %s: %w", imagePath, oldFSName, newFSName, err)
}
return &CimFsWriter{handle: handle, name: filepath.Join(imagePath, fsName)}, nil
}
// blockCIMConfig represents options for creating or merging block CIMs
type blockCIMConfig struct {
// ensures that the generted CIM is identical every time when created from the same source data.
// This is mostly required for image layers. Dissabled by default.
consistentCIM bool
// enables data integrity checking, which means the CIM will be verified and sealed on close.
// This is useful for ensuring that the CIM is tamper-proof. Disabled by default.
dataIntegrity bool
}
// BlockCIMOpt is a function type for configuring block CIM creation options
type BlockCIMOpt func(*blockCIMConfig) error
// enabled consistent CIM creation, this ensures that CIMs created from identical source data will always be identical (i.e. SHA256 digest of the CIM will remain same)
func WithConsistentCIM() BlockCIMOpt {
return func(opts *blockCIMConfig) error {
opts.consistentCIM = true
return nil
}
}
// WithDataIntegrity enables data integrity checking (verified CIM with sealing on close)
func WithDataIntegrity() BlockCIMOpt {
return func(opts *blockCIMConfig) error {
opts.dataIntegrity = true
return nil
}
}
// CreateBlockCIMWithOptions creates a new block CIM with the specified options and opens it for writing.
// The CimFsWriter returned can then be used to add/remove files to/from this CIM.
func CreateBlockCIMWithOptions(ctx context.Context, bCIM *BlockCIM, options ...BlockCIMOpt) (_ *CimFsWriter, err error) {
// Apply default options
config := &blockCIMConfig{}
// Apply provided options
for _, option := range options {
if err = option(config); err != nil {
return nil, err
}
}
// Validate options
if bCIM.BlockPath == "" || bCIM.CimName == "" {
return nil, fmt.Errorf("both blockPath & name must be non empty: %w", os.ErrInvalid)
}
if bCIM.Type == BlockCIMTypeNone {
return nil, fmt.Errorf("invalid block CIM type `%d`: %w", bCIM.Type, os.ErrInvalid)
}
// Check OS support
if !IsBlockCimWriteSupported() {
return nil, fmt.Errorf("block CIM not supported on this OS version")
}
if config.dataIntegrity && !IsVerifiedCimWriteSupported() {
return nil, fmt.Errorf("verified CIMs are not supported on this OS version")
}
// Build create flags based on options
var createFlags uint32
if config.consistentCIM {
createFlags |= CimCreateFlagConsistentCim
}
if config.dataIntegrity {
createFlags |= CimCreateFlagVerifiedCim
}
switch bCIM.Type {
case BlockCIMTypeDevice:
createFlags |= CimCreateFlagBlockDeviceCim
case BlockCIMTypeSingleFile:
createFlags |= CimCreateFlagSingleFileCim
default:
return nil, fmt.Errorf("invalid block CIM type `%d`: %w", bCIM.Type, os.ErrInvalid)
}
winapi.LogCimDLLSupport()
var newNameUTF16 *uint16
newNameUTF16, err = windows.UTF16PtrFromString(bCIM.CimName)
if err != nil {
return nil, err
}
var handle winapitypes.FsHandle
if err := winapi.CimCreateImage2(bCIM.BlockPath, createFlags, nil, newNameUTF16, &handle); err != nil {
return nil, fmt.Errorf("failed to create block CIM at path %s,%s: %w", bCIM.BlockPath, bCIM.CimName, err)
}
return &CimFsWriter{
handle: handle,
name: filepath.Join(bCIM.BlockPath, bCIM.CimName),
sealOnClose: config.dataIntegrity, // Seal on close if data integrity is enabled
}, nil
}
// Create creates a new block CIM and opens it for writing. The CimFsWriter
// returned can then be used to add/remove files to/from this CIM.
func CreateBlockCIM(blockPath, name string, blockType BlockCIMType) (_ *CimFsWriter, err error) {
return CreateBlockCIMWithOptions(context.Background(), &BlockCIM{
Type: blockType,
BlockPath: blockPath,
CimName: name,
}, WithConsistentCIM())
}
// CreateAlternateStream creates alternate stream of given size at the given path inside the cim. This will
// replace the current active stream. Always, finish writing current active stream and then create an
// alternate stream.
func (c *CimFsWriter) CreateAlternateStream(path string, size uint64) (err error) {
err = c.closeStream()
if err != nil {
return err
}
err = winapi.CimCreateAlternateStream(c.handle, path, size, &c.activeStream)
if err != nil {
return fmt.Errorf("failed to create alternate stream for path %s: %w", path, err)
}
c.activeName = path
return nil
}
// closes the currently active stream.
func (c *CimFsWriter) closeStream() error {
if c.activeStream == 0 {
return nil
}
err := winapi.CimCloseStream(c.activeStream)
if err == nil && c.activeLeft > 0 {
// Validate here because CimCloseStream does not and this improves error
// reporting. Otherwise the error will occur in the context of
// cimWriteStream.
err = fmt.Errorf("incomplete write, %d bytes left in the stream %s", c.activeLeft, c.activeName)
}
if err != nil {
err = &PathError{Cim: c.name, Op: "closeStream", Path: c.activeName, Err: err}
}
c.activeLeft = 0
c.activeStream = 0
c.activeName = ""
return err
}
// AddFile adds a new file to the image. The file is added at the specified path. After
// calling this function, the file is set as the active stream for the image, so data can
// be written by calling `Write`.
func (c *CimFsWriter) AddFile(path string, info *winio.FileBasicInfo, fileSize int64, securityDescriptor []byte, extendedAttributes []byte, reparseData []byte) error {
err := c.closeStream()
if err != nil {
return err
}
fileMetadata := &winapitypes.CimFsFileMetadata{
Attributes: info.FileAttributes,
FileSize: fileSize,
CreationTime: info.CreationTime,
LastWriteTime: info.LastWriteTime,
ChangeTime: info.ChangeTime,
LastAccessTime: info.LastAccessTime,
}
if len(securityDescriptor) == 0 {
// Passing an empty security descriptor creates a CIM in a weird state.
// Pass the NULL DACL.
securityDescriptor = nullSd
}
fileMetadata.SecurityDescriptorBuffer = unsafe.Pointer(&securityDescriptor[0])
fileMetadata.SecurityDescriptorSize = uint32(len(securityDescriptor))
if len(reparseData) > 0 {
fileMetadata.ReparseDataBuffer = unsafe.Pointer(&reparseData[0])
fileMetadata.ReparseDataSize = uint32(len(reparseData))
}
if len(extendedAttributes) > 0 {
fileMetadata.ExtendedAttributes = unsafe.Pointer(&extendedAttributes[0])
fileMetadata.EACount = uint32(len(extendedAttributes))
}
// remove the trailing `\` if present, otherwise it trips off the cim writer
path = strings.TrimSuffix(path, "\\")
err = winapi.CimCreateFile(c.handle, path, fileMetadata, &c.activeStream)
if err != nil {
return &PathError{Cim: c.name, Op: "addFile", Path: path, Err: err}
}
c.activeName = path
if info.FileAttributes&(windows.FILE_ATTRIBUTE_DIRECTORY) == 0 {
c.activeLeft = uint64(fileSize)
}
return nil
}
// Write writes bytes to the active stream.
func (c *CimFsWriter) Write(p []byte) (int, error) {
if c.activeStream == 0 {
return 0, fmt.Errorf("no active stream")
}
if uint64(len(p)) > c.activeLeft {
return 0, &PathError{Cim: c.name, Op: "write", Path: c.activeName, Err: fmt.Errorf("wrote too much")}
}
err := winapi.CimWriteStream(c.activeStream, uintptr(unsafe.Pointer(&p[0])), uint32(len(p)))
if err != nil {
err = &PathError{Cim: c.name, Op: "write", Path: c.activeName, Err: err}
return 0, err
}
c.activeLeft -= uint64(len(p))
return len(p), nil
}
// AddLink adds a hard link at `newPath` that points to `oldPath`.
func (c *CimFsWriter) AddLink(oldPath string, newPath string) error {
err := c.closeStream()
if err != nil {
return err
}
err = winapi.CimCreateHardLink(c.handle, newPath, oldPath)
if err != nil {
err = &LinkError{Cim: c.name, Op: "addLink", Old: oldPath, New: newPath, Err: err}
}
return err
}
// AddMergedLink adds a hard link at `newPath` that points to `oldPath` in the
// image. However unlike AddLink this link is resolved at merge time. This allows us to
// create links to files that are in other CIMs.
func (c *CimFsWriter) AddMergedLink(oldPath string, newPath string) error {
err := c.closeStream()
if err != nil {
return err
}
err = winapi.CimCreateMergeLink(c.handle, newPath, oldPath)
if err != nil {
err = &LinkError{Cim: c.name, Op: "addMergedLink", Old: oldPath, New: newPath, Err: err}
}
return err
}
// Unlink deletes the file at `path` from the image. Note that the file MUST have been
// already added to the image.
func (c *CimFsWriter) Unlink(path string) error {
err := c.closeStream()
if err != nil {
return err
}
return winapi.CimDeletePath(c.handle, path)
}
// Adds a tombstone at given path. This ensures that when the the CIMs are merged, the
// file at this path from lower layers won't show up in a mounted CIM. In case of Unlink,
// the file from the lower layers still shows up after merge.
func (c *CimFsWriter) AddTombstone(path string) error {
err := c.closeStream()
if err != nil {
return err
}
return winapi.CimTombstoneFile(c.handle, path)
}
func (c *CimFsWriter) commit() error {
err := c.closeStream()
if err != nil {
return err
}
err = winapi.CimCommitImage(c.handle)
if err != nil {
err = &OpError{Cim: c.name, Op: "commit", Err: err}
}
return err
}
// Close closes the CimFS filesystem.
func (c *CimFsWriter) Close() (err error) {
if c.handle == 0 {
return fmt.Errorf("invalid writer")
}
if err = c.commit(); err != nil {
return &OpError{Cim: c.name, Op: "commit", Err: err}
}
err = winapi.CimCloseImage(c.handle)
c.handle = 0
if err != nil {
return &OpError{Cim: c.name, Op: "close", Err: err}
}
if c.sealOnClose {
if err = sealBlockCIM(filepath.Dir(c.name)); err != nil {
return &OpError{Cim: filepath.Dir(c.name), Op: "seal", Err: err}
}
}
return nil
}
// DestroyCim finds out the region files, object files of this cim and then delete the
// region files, object files and the <layer-id>.cim file itself. Note that any other
// CIMs that were forked off of this CIM would become unusable after this operation. This
// should not be used for block CIMs, os.Remove is sufficient for block CIMs.
func DestroyCim(ctx context.Context, cimPath string) (retErr error) {
regionFilePaths, err := getRegionFilePaths(ctx, cimPath)
if err != nil {
log.G(ctx).WithError(err).Warnf("get region files for cim %s", cimPath)
if retErr == nil { //nolint:govet // nilness: consistency with below
retErr = err
}
}
objectFilePaths, err := getObjectIDFilePaths(ctx, cimPath)
if err != nil {
log.G(ctx).WithError(err).Warnf("get objectid file for cim %s", cimPath)
if retErr == nil {
retErr = err
}
}
log.G(ctx).WithFields(logrus.Fields{
"cimPath": cimPath,
"regionFiles": regionFilePaths,
"objectFiles": objectFilePaths,
}).Debug("destroy cim")
for _, regFilePath := range regionFilePaths {
if err := os.Remove(regFilePath); err != nil {
log.G(ctx).WithError(err).Warnf("remove file %s", regFilePath)
if retErr == nil {
retErr = err
}
}
}
for _, objFilePath := range objectFilePaths {
if err := os.Remove(objFilePath); err != nil {
log.G(ctx).WithError(err).Warnf("remove file %s", objFilePath)
if retErr == nil {
retErr = err
}
}
}
if err := os.Remove(cimPath); err != nil {
log.G(ctx).WithError(err).Warnf("remove file %s", cimPath)
if retErr == nil {
retErr = err
}
}
return retErr
}
// GetCimUsage returns the total disk usage in bytes by the cim at path `cimPath`.
func GetCimUsage(ctx context.Context, cimPath string) (uint64, error) {
regionFilePaths, err := getRegionFilePaths(ctx, cimPath)
if err != nil {
return 0, fmt.Errorf("get region file paths for cim %s: %w", cimPath, err)
}
objectFilePaths, err := getObjectIDFilePaths(ctx, cimPath)
if err != nil {
return 0, fmt.Errorf("get objectid file for cim %s: %w", cimPath, err)
}
var totalUsage uint64
for _, f := range append(regionFilePaths, objectFilePaths...) {
fi, err := os.Stat(f)
if err != nil {
return 0, fmt.Errorf("stat file %s: %w", f, err)
}
totalUsage += uint64(fi.Size())
}
return totalUsage, nil
}
// MergeBlockCIMs creates a new merged BlockCIM from the provided source BlockCIMs. CIM
// at index 0 is considered to be topmost CIM and the CIM at index `length-1` is
// considered the base CIM. (i.e file with the same path in CIM at index 0 will shadow
// files with the same path at all other CIMs) When mounting this merged CIM the source
// CIMs MUST be provided in the exact same order.
func MergeBlockCIMsWithOpts(ctx context.Context, mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, opts ...BlockCIMOpt) (err error) {
if !IsMergedCimWriteSupported() {
return fmt.Errorf("merged CIMs aren't supported on this OS version")
} else if len(sourceCIMs) < 2 {
return fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid)
}
// Apply default options
config := &blockCIMConfig{}
// Apply provided options
for _, opt := range opts {
if err = opt(config); err != nil {
return err
}
}
for _, sCIM := range sourceCIMs {
if sCIM.Type != mergedCIM.Type {
return fmt.Errorf("source CIM (%s) type MUST match with merged CIM type: %w", sCIM.String(), os.ErrInvalid)
}
}
var mergeFlags uint32
switch mergedCIM.Type {
case BlockCIMTypeDevice:
mergeFlags = CimMergeFlagBlockDevice
case BlockCIMTypeSingleFile:
mergeFlags = CimMergeFlagSingleFile
default:
return fmt.Errorf("invalid block CIM type `%d`: %w", mergedCIM.Type, os.ErrInvalid)
}
if config.dataIntegrity {
mergeFlags |= CimMergeFlagVerifiedCim
}
cim, err := CreateBlockCIMWithOptions(ctx, mergedCIM, opts...)
if err != nil {
return fmt.Errorf("create merged CIM: %w", err)
}
defer func() {
cErr := cim.Close()
if err == nil {
err = cErr
}
}()
// CimAddFsToMergedImage expects that topmost CIM is added first and the bottom
// most CIM is added last.
for _, sCIM := range sourceCIMs {
fullPath := filepath.Join(sCIM.BlockPath, sCIM.CimName)
if err := winapi.CimAddFsToMergedImage2(cim.handle, fullPath, mergeFlags); err != nil {
return fmt.Errorf("add cim to merged image: %w", err)
}
}
return nil
}
// MergeBlockCIMs creates a new merged BlockCIM from the provided source BlockCIMs. CIM
// at index 0 is considered to be topmost CIM and the CIM at index `length-1` is
// considered the base CIM. (i.e file with the same path in CIM at index 0 will shadow
// files with the same path at all other CIMs) When mounting this merged CIM the source
// CIMs MUST be provided in the exact same order.
func MergeBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM) (err error) {
return MergeBlockCIMsWithOpts(context.Background(), mergedCIM, sourceCIMs, WithConsistentCIM())
}
// sealBlockCIM seals a blockCIM at the given path so that no further modifications are allowed on it. This also writes a
// root hash in the block header so that in future any reads happening on the CIM can be easily verified against this root hash
// to detect tampering.
func sealBlockCIM(blockPath string) error {
var hashSize, fixedHeaderSize uint64
hashBuf := make([]byte, cimHashSize)
// the blockPath could be a path to a block device or a file. In either case there should be no trailing backslash.
blockPath = strings.TrimSuffix(blockPath, "\\")
if err := winapi.CimSealImage(blockPath, &hashSize, &fixedHeaderSize, &hashBuf[0]); err != nil {
return fmt.Errorf("failed to seal block CIM: %w", err)
} else if hashSize != cimHashSize {
return fmt.Errorf("unexpected cim hash size %d", hashSize)
}
return nil
}
// GetVerificationInfo returns the root digest of the given block CIM. This is only
// applicable for CIMs that are sealed after writing.
func GetVerificationInfo(blockPath string) ([]byte, error) {
var (
isSealed uint32
hashSize uint64
signatureSize uint64
fixedHeaderSize uint64
hash = make([]byte, cimHashSize)
)
if err := winapi.CimGetVerificationInformation(blockPath, &isSealed, &hashSize, &signatureSize, &fixedHeaderSize, &hash[0], nil); err != nil {
return nil, fmt.Errorf("failed to get verification info from the CIM: %w", err)
} else if hashSize != cimHashSize {
return nil, fmt.Errorf("unexpected cim hash size %d", hashSize)
} else if isSealed == 0 {
return nil, fmt.Errorf("cim is not sealed")
}
return hash, nil
}

161
vendor/github.com/Microsoft/hcsshim/pkg/cimfs/cimfs.go generated vendored Normal file
View File

@@ -0,0 +1,161 @@
//go:build windows
// +build windows
package cimfs
import (
"github.com/Microsoft/hcsshim/internal/winapi/cimfs"
"github.com/Microsoft/hcsshim/internal/winapi/cimwriter"
"path/filepath"
"github.com/Microsoft/hcsshim/osversion"
"github.com/sirupsen/logrus"
)
func IsCimFSSupported() bool {
rv, err := osversion.BuildRevision()
if err != nil {
logrus.WithError(err).Warn("get build revision")
}
build := osversion.Build()
// CimFS support is backported to LTSC2022 starting with revision 2031 and should
// otherwise be available on all builds >= V25H1Server
return (build >= osversion.V25H1Server || (build == osversion.V21H2Server && rv >= 2031)) && cimfs.Supported()
}
// IsBlockCimSupported returns true if block formatted CIMs (i.e block device CIM &
// single file CIM) are supported on the current OS build.
func IsBlockCimSupported() bool {
build := osversion.Build()
// TODO(ambarve): Currently we are checking against a higher build number since there is no
// official build with block CIM support yet. Once we have that build, we should
// update the build number here.
return build >= 27766 && cimfs.Supported()
}
// IsBlockCimWriteSupported returns true if block formatted CIMs (i.e block device CIM &
// single file CIM) are supported on the current OS build or if CimWriter is present.
func IsBlockCimWriteSupported() bool {
// TODO(ambarve): Currently we are checking against a higher build number since there is no
// official build with block CIM support yet. Once we have that build, we should
// update the build number here.
return IsBlockCimSupported() || cimwriter.Supported()
}
// IsBlockCimMountSupported returns true if block formatted CIMs (i.e block device CIM &
// single file CIM) are supported on the current OS build.
func IsBlockCimMountSupported() bool {
// TODO(ambarve): Currently we are checking against a higher build number since there is no
// official build with block CIM support yet. Once we have that build, we should
// update the build number here.
return IsBlockCimSupported()
}
// IsVerifiedCimSupported returns true if block CIM format supports also writing verification information in the CIM.
func IsVerifiedCimSupported() bool {
build := osversion.Build()
// TODO(ambarve): Currently we are checking against a higher build number since there is no
// official build with block CIM support yet. Once we have that build, we should
// update the build number here.
return build >= 27800 && cimfs.Supported()
}
// IsVerifiedCimWriteSupported returns true if block CIM format supports also writing verification information in the CIM.
func IsVerifiedCimWriteSupported() bool {
// TODO(ambarve): Currently we are checking against a higher build number since there is no
// official build with block CIM support yet. Once we have that build, we should
// update the build number here.
return IsVerifiedCimSupported() || cimwriter.Supported()
}
// IsVerifiedCimMountSupported returns true if block CIM format supports mounting.
func IsVerifiedCimMountSupported() bool {
// TODO(ambarve): Currently we are checking against a higher build number since there is no
// official build with block CIM support yet. Once we have that build, we should
// update the build number here.
return IsVerifiedCimSupported()
}
func IsMergedCimSupported() bool {
// The merged CIM support was originally added before block CIM support. However,
// some of the merged CIM features that we use (e.g. merged hard links) were added
// later along with block CIM support. So use the same check as block CIM here.
return IsBlockCimSupported()
}
func IsMergedCimWriteSupported() bool {
// The merged CIM support was originally added before block CIM support. However,
// some of the merged CIM features that we use (e.g. merged hard links) were added
// later along with block CIM support. So use the same check as block CIM here.
return IsBlockCimWriteSupported()
}
func IsMergedCimMountSupported() bool {
// The merged CIM support was originally added before block CIM support. However,
// some of the merged CIM features that we use (e.g. merged hard links) were added
// later along with block CIM support. So use the same check as block CIM here.
return IsBlockCimMountSupported()
}
type BlockCIMType uint32
const (
BlockCIMTypeNone BlockCIMType = iota
BlockCIMTypeSingleFile
BlockCIMTypeDevice
CimMountFlagNone uint32 = 0x0
CimMountFlagEnableDax uint32 = 0x2
CimMountBlockDeviceCim uint32 = 0x10
CimMountSingleFileCim uint32 = 0x20
CimMountVerifiedCim uint32 = 0x80
CimCreateFlagNone uint32 = 0x0
CimCreateFlagDoNotExpandPEImages uint32 = 0x1
CimCreateFlagFixedSizeChunks uint32 = 0x2
CimCreateFlagBlockDeviceCim uint32 = 0x4
CimCreateFlagSingleFileCim uint32 = 0x8
CimCreateFlagConsistentCim uint32 = 0x10
CimCreateFlagVerifiedCim uint32 = 0x40
CimMergeFlagNone uint32 = 0x0
CimMergeFlagSingleFile uint32 = 0x1
CimMergeFlagBlockDevice uint32 = 0x2
CimMergeFlagVerifiedCim uint32 = 0x4
)
// BlockCIM represents a CIM stored in a block formatted way.
//
// A CIM usually is made up of a .cim file and multiple region & objectID
// files. Currently, all of these files are stored together in the same directory. To
// refer to such a CIM, we provide the path to the `.cim` file and the corresponding
// region & objectID files are assumed to be present right next to it. In this case the
// directory on the host's filesystem which holds one or more such CIMs is the container
// for those CIMs.
//
// Using multiple files for a single CIM can be very limiting. (For example, if you want
// to do a remote mount for a CIM layer, you now need to mount multiple files for a single
// layer). In such cases having a single container which contains all of the CIM related
// data is a great option. For this reason, CimFS has added support for a new type of a
// CIM named BlockCIM. A BlockCIM is a CIM for which the container used to store all of
// the CIM files is a block device or a binary file formatted like a block device. Such a
// block device (or a binary file) doesn't have a separate filesystem (like NTFS or FAT32)
// on it. Instead it is formatted in such a way that CimFS driver can read the blocks and
// find out which CIMs are present on that block device. The CIMs stored on a raw block
// device are sometimes referred to as block device CIMs and CIMs stored on the block
// formatted single file are referred as single file CIMs.
type BlockCIM struct {
Type BlockCIMType
// BlockPath is a path to the block device or the single file which contains the
// CIM.
BlockPath string
// Since a block device CIM or a single file CIM can container multiple CIMs, we
// refer to an individual CIM using its name.
CimName string
}
// added for logging convenience
func (b *BlockCIM) String() string {
return filepath.Join(b.BlockPath, b.CimName)
}

138
vendor/github.com/Microsoft/hcsshim/pkg/cimfs/common.go generated vendored Normal file
View File

@@ -0,0 +1,138 @@
//go:build windows
// +build windows
package cimfs
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/pkg/cimfs/format"
)
const (
cimHashSize = 32 // size of a hash of a verified CIM in bytes
)
var (
// Equivalent to SDDL of "D:NO_ACCESS_CONTROL".
nullSd = []byte{1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
)
type OpError struct {
Cim string
Op string
Err error
}
func (e *OpError) Error() string {
s := "cim " + e.Op + " " + e.Cim
s += ": " + e.Err.Error()
return s
}
// PathError is the error type returned by most functions in this package.
type PathError struct {
Cim string
Op string
Path string
Err error
}
func (e *PathError) Error() string {
s := "cim " + e.Op + " " + e.Cim
s += ":" + e.Path
s += ": " + e.Err.Error()
return s
}
type LinkError struct {
Cim string
Op string
Old string
New string
Err error
}
func (e *LinkError) Error() string {
return "cim " + e.Op + " " + e.Old + " " + e.New + ": " + e.Err.Error()
}
func validateHeader(h *format.CommonHeader) error {
if !bytes.Equal(h.Magic[:], format.MagicValue[:]) {
return fmt.Errorf("not a cim file")
}
if h.Version.Major > format.CurrentVersion.Major || h.Version.Major < format.MinSupportedVersion.Major {
return fmt.Errorf("unsupported cim version. cim version %v must be between %v & %v", h.Version, format.MinSupportedVersion, format.CurrentVersion)
}
return nil
}
func readFilesystemHeader(f *os.File) (format.FilesystemHeader, error) {
var fsh format.FilesystemHeader
if err := binary.Read(f, binary.LittleEndian, &fsh); err != nil {
return fsh, fmt.Errorf("reading filesystem header: %w", err)
}
if err := validateHeader(&fsh.Common); err != nil {
return fsh, fmt.Errorf("validating filesystem header: %w", err)
}
return fsh, nil
}
// Returns the paths of all the objectID files associated with the cim at `cimPath`.
func getObjectIDFilePaths(ctx context.Context, cimPath string) ([]string, error) {
f, err := os.Open(cimPath)
if err != nil {
return []string{}, fmt.Errorf("open cim file %s: %w", cimPath, err)
}
defer f.Close()
fsh, err := readFilesystemHeader(f)
if err != nil {
return []string{}, fmt.Errorf("readingp cim header: %w", err)
}
paths := []string{}
for i := 0; i < int(fsh.Regions.Count); i++ {
path := filepath.Join(filepath.Dir(cimPath), fmt.Sprintf("%s_%v_%d", format.ObjectIDFileName, fsh.Regions.ID, i))
if _, err := os.Stat(path); err == nil {
paths = append(paths, path)
} else {
log.G(ctx).WithError(err).Warnf("stat for object file %s", path)
}
}
return paths, nil
}
// Returns the paths of all the region files associated with the cim at `cimPath`.
func getRegionFilePaths(ctx context.Context, cimPath string) ([]string, error) {
f, err := os.Open(cimPath)
if err != nil {
return []string{}, fmt.Errorf("open cim file %s: %w", cimPath, err)
}
defer f.Close()
fsh, err := readFilesystemHeader(f)
if err != nil {
return []string{}, fmt.Errorf("reading cim header: %w", err)
}
paths := []string{}
for i := 0; i < int(fsh.Regions.Count); i++ {
path := filepath.Join(filepath.Dir(cimPath), fmt.Sprintf("%s_%v_%d", format.RegionFileName, fsh.Regions.ID, i))
if _, err := os.Stat(path); err == nil {
paths = append(paths, path)
} else {
log.G(ctx).WithError(err).Warnf("stat for region file %s", path)
}
}
return paths, nil
}

99
vendor/github.com/Microsoft/hcsshim/pkg/cimfs/doc.go generated vendored Normal file
View File

@@ -0,0 +1,99 @@
/*
This package provides simple go wrappers on top of the win32 CIMFS APIs.
Details about CimFS & related win32 APIs can be found here:
https://learn.microsoft.com/en-us/windows/win32/api/_cimfs/
Details about how CimFS is being used in containerd can be found here:
https://github.com/containerd/containerd/issues/8346
CIM types:
Currently we support 2 types of CIMs:
- Standard/classic (for the lack of a better term) CIMs.
- Block CIMs.
Standard CIMs store all the contents of a CIM in one or more region & objectID files. This
means a single CIM is made up of a `.cim` file, one or more region files and one or more
objectID files. All of these files MUST be present in the same directory in order for that
CIM to work. Block CIMs store all the data of a CIM in a single block device. A VHD can be
such a block device. For convenience CimFS also allows using a block formatted file as a
block device.
Standard CIMs can be created with the `func Create(imagePath string, oldFSName string,
newFSName string) (_ *CimFsWriter, err error)` function defined in this package, whereas
block CIMs can be created with the `func CreateBlockCIM(blockPath, oldName, newName
string, blockType BlockCIMType) (_ *CimFsWriter, err error)` function.
Verified CIMs:
A block CIM can also provide integrity checking (via a hash/Merkel tree,
similar to dm-verity on Linux). If a CIM is written and sealed, it generates a
root hash of all of its contents and shares it back with the client. Any
verified CIM can be mounted by passing a hash that we expect to be its root
hash. All read operations on such a mounted CIM will then validate that the
generated root hash matches with the one that was provided at mount time. If it
doesn't match the read fails. This allows us to guarantee that the CIM based
layered aren't being modified underneath us.
Forking & Merging CIMs:
In container world, CIMs are used for storing container image layers. Usually, one layer
is stored in one CIM. This means we need a way to combine multiple CIMs to create the
rootfs of a container. This can be achieved either by forking the CIMs or merging the
CIMs.
Forking CIMs:
Forking means every time a CIM is created for a non-base layer, we fork it off of a parent
layer CIM. This ensures that contents that are written to this CIM are merged with that of
parent layer CIMs at the time of CIM creation itself. When such a CIM is mounted we get a
combined view of the contents of this CIM as well as the parent CIM from which this CIM
was forked. However, this means that all the CIMs MUST be stored in the same directory in
order for forked CIMs to work. And every non-base layer CIM is dependent on all of its
parent layer CIMs.
Merging CIMs:
If we create one or more CIMs without forking them at the time of creation, we can still
merge those CIMs later to create a new special type of CIM called merged CIM. When
mounted, this merged CIM provides a view of the combined contents of all the layers that
were merged. The advantage of this approach is that each layer CIM (also referred to as
source CIMs in the context of merging CIMs) can be created & stored independent of its
parent CIMs. (Currently we only support merging block CIMs).
In order to create a merged CIM we need at least 2 non-forked block CIMs (we can not merge
forked & non-forked CIMs), these CIMs are also referred to as source CIMs. We first create
a new CIM (for storing the merge) via the `CreateBlockCIM` API, then call
`CimAddFsToMergedImage2` repeatedly to add the source CIMs one by one to the merged
CIM. Closing the handle on this new CIM commits it automatically. The order in which
source CIMs are added matters. A source CIM that was added before another source CIM takes
precedence when merging the CIM contents. Crating this merged CIM only combines the
metadata of all the source CIMs, however the actual data isn't copied to the merged
CIM. This is why when mounting the merged CIM, we still need to provide paths to the
source CIMs.
`CimMergeMountImage` is used to mount a merged CIM. This API expects an array of paths of
the merged CIM and all the source CIMs. Note that the array MUST include the merged CIM
path at the 0th index and all the source CIMs in the same order in which they were added
at the time of creation of the merged CIM. For example, if we merged CIMs 1.cim & 2.cim by
first adding 1.cim (via CimAddFsToMergedImage) and then adding 2.cim, then the array
should be [merged.cim, 1.cim, 2.cim]
Merged CIM specific APIs.
`CimTombstoneFile`: is used for creating a tombstone file in a CIM. Tombstone file is
similar to a whiteout file used in case of overlayFS. A tombstone's primary use case is
for merged CIMs. When multiple source CIMs are merged, a tombstone file/directory ensures
that any files with the same path in the lower layers (i.e source CIMs that are added
after the CIM that has a tombstone) do not show up in the mounted filesystem view. For
example, imagine 1.cim has a file at path `foo/bar.txt` and 2.cim has a tombstone at path
`foo/bar.txt`. If a merged CIM is created by first adding 2.cim (via
CimAddFsToMergedImage) and then adding 1.cim and then when that merged CIM is mounted,
`foo/bar.txt` will not show up in the mounted filesystem. A tombstone isn't required when
using forked CIMs, because we can just call `CimDeletePath` to remove a file from the
lower layers in that case. However, that doesn't work for merged CIMs since at the time of
writing one of the source CIMs, we can't delete files from other source CIMs.
`CimCreateMergeLink`: is used to create a file link that is resolved at the time of
merging CIMs. This is required if we want to create a hardlink in one source CIM that
points to a file in another source CIM. Such a hardlink can not be resolved at the time of
writing the source CIM. It can only be resolved at the time of merge. This API allows us
to create such cross layer hard links.
*/
package cimfs

View File

@@ -0,0 +1,4 @@
// format package maintains some basic structures to allows us to read header of a cim file. This is mostly
// required to understand the region & objectid files associated with a particular cim. Otherwise, we don't
// need to parse the cim format.
package format

View File

@@ -0,0 +1,61 @@
//go:build windows
// +build windows
package format
import "github.com/Microsoft/go-winio/pkg/guid"
const (
RegionFileName = "region"
ObjectIDFileName = "objectid"
)
// Magic specifies the magic number at the beginning of a file.
type Magic [8]uint8
var MagicValue = Magic([8]uint8{'c', 'i', 'm', 'f', 'i', 'l', 'e', '0'})
type Version struct {
Major, Minor uint32
}
var CurrentVersion = Version{3, 0}
var MinSupportedVersion = Version{2, 0}
type FileType uint8
// RegionOffset encodes an offset to objects as index of the region file
// containing the object and the byte offset within that file.
type RegionOffset uint64
// CommonHeader is the common header for all CIM-related files.
type CommonHeader struct {
Magic Magic
HeaderLength uint32
Type FileType
Reserved uint8
Reserved2 uint16
Version Version
Reserved3 uint64
}
type RegionSet struct {
ID guid.GUID
Count uint16
Reserved uint16
Reserved1 uint32
}
// FilesystemHeader is the header for a filesystem file.
//
// The filesystem file points to the filesystem object inside a region
// file and specifies regions sets.
type FilesystemHeader struct {
Common CommonHeader
Regions RegionSet
FilesystemOffset RegionOffset
Reserved uint32
Reserved1 uint16
ParentCount uint16
}

View File

@@ -0,0 +1,214 @@
//go:build windows
// +build windows
package cimfs
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio/pkg/guid"
"github.com/Microsoft/hcsshim/internal/winapi"
winapitypes "github.com/Microsoft/hcsshim/internal/winapi/types"
"github.com/pkg/errors"
"golang.org/x/sys/windows"
)
type MountError struct {
Cim string
Op string
VolumeGUID guid.GUID
Err error
}
func (e *MountError) Error() string {
s := "cim " + e.Op
if e.Cim != "" {
s += " " + e.Cim
}
s += " " + e.VolumeGUID.String() + ": " + e.Err.Error()
return s
}
const (
VolumePathFormat = "\\\\?\\Volume{%s}\\"
)
// Mount mounts the given cim at a volume with given GUID. Returns the full volume
// path if mount is successful.
func Mount(cimPath string, volumeGUID guid.GUID, mountFlags uint32) (string, error) {
if err := winapi.CimMountImage(filepath.Dir(cimPath), filepath.Base(cimPath), mountFlags, &volumeGUID); err != nil {
return "", &MountError{Cim: cimPath, Op: "Mount", VolumeGUID: volumeGUID, Err: err}
}
return fmt.Sprintf(VolumePathFormat, volumeGUID.String()), nil
}
// Unmount unmounts the cim at mounted at path `volumePath`.
func Unmount(volumePath string) error {
// The path is expected to be in the \\?\Volume{GUID}\ format
if volumePath[len(volumePath)-1] != '\\' {
volumePath += "\\"
}
if !strings.HasPrefix(volumePath, "\\\\?\\Volume{") || !strings.HasSuffix(volumePath, "}\\") {
return errors.Errorf("volume path %s is not in the expected format", volumePath)
}
trimmedStr := strings.TrimPrefix(volumePath, "\\\\?\\Volume{")
trimmedStr = strings.TrimSuffix(trimmedStr, "}\\")
volGUID, err := guid.FromString(trimmedStr)
if err != nil {
return errors.Wrapf(err, "guid parsing failed for %s", trimmedStr)
}
if err := winapi.CimDismountImage(&volGUID); err != nil {
return &MountError{VolumeGUID: volGUID, Op: "Unmount", Err: err}
}
return nil
}
// MountMergedBlockCIMs mounts the given merged BlockCIM (usually created with
// `MergeBlockCIMs`) at a volume with given GUID. The `sourceCIMs` MUST be identical
// to the `sourceCIMs` passed to `MergeBlockCIMs` when creating this merged CIM.
func MountMergedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlags uint32, volumeGUID guid.GUID) (string, error) {
if !IsMergedCimMountSupported() {
return "", fmt.Errorf("merged CIMs aren't supported on this OS version")
} else if len(sourceCIMs) < 2 {
return "", fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid)
}
switch mergedCIM.Type {
case BlockCIMTypeDevice:
mountFlags |= CimMountBlockDeviceCim
case BlockCIMTypeSingleFile:
mountFlags |= CimMountSingleFileCim
default:
return "", fmt.Errorf("invalid block CIM type `%d`", mergedCIM.Type)
}
for _, sCIM := range sourceCIMs {
if sCIM.Type != mergedCIM.Type {
return "", fmt.Errorf("source CIM (%s) type doesn't match with merged CIM type: %w", sCIM.String(), os.ErrInvalid)
}
}
// win32 mount merged CIM API expects an array of all CIMs. 0th entry in the array
// should be the merged CIM. All remaining entries should be the source CIM paths
// in the same order that was used while creating the merged CIM.
allcims := append([]*BlockCIM{mergedCIM}, sourceCIMs...)
cimsToMerge := []winapitypes.CimFsImagePath{}
for _, bcim := range allcims {
// Trailing backslashes cause problems-remove those
imageDir, err := windows.UTF16PtrFromString(strings.TrimRight(bcim.BlockPath, `\`))
if err != nil {
return "", fmt.Errorf("convert string to utf16: %w", err)
}
cimName, err := windows.UTF16PtrFromString(bcim.CimName)
if err != nil {
return "", fmt.Errorf("convert string to utf16: %w", err)
}
cimsToMerge = append(cimsToMerge, winapitypes.CimFsImagePath{
ImageDir: imageDir,
ImageName: cimName,
})
}
if err := winapi.CimMergeMountImage(uint32(len(cimsToMerge)), &cimsToMerge[0], mountFlags, &volumeGUID); err != nil {
return "", &MountError{Cim: filepath.Join(mergedCIM.BlockPath, mergedCIM.CimName), Op: "MountMerged", Err: err}
}
return fmt.Sprintf(VolumePathFormat, volumeGUID.String()), nil
}
// Mounts a verified block CIM with the provided root hash. The root hash is usually
// returned when the CIM is sealed or the root hash can be queried from a block CIM.
// Every read on the mounted volume will be verified to match against the provided root
// hash if it doesn't, the read will fail. The CIM MUST have been created with the
// verified creation flag.
func MountVerifiedBlockCIM(bCIM *BlockCIM, mountFlags uint32, volumeGUID guid.GUID, rootHash []byte) (string, error) {
if len(rootHash) != cimHashSize {
return "", fmt.Errorf("unexpected root hash size %d, expected size is %d", len(rootHash), cimHashSize)
}
// The CimMountVerifiedCim flag should only be used when using the regular mount
// CIM API. That flag is required to tell that API that this is a verified
// CIM. This API doesn't need that flag as it is already assumed that the CIM is
// verified.
switch bCIM.Type {
case BlockCIMTypeDevice:
mountFlags |= CimMountBlockDeviceCim
case BlockCIMTypeSingleFile:
mountFlags |= CimMountSingleFileCim
default:
return "", fmt.Errorf("invalid block CIM type `%d`: %w", bCIM.Type, os.ErrInvalid)
}
if err := winapi.CimMountVerifiedImage(bCIM.BlockPath, bCIM.CimName, mountFlags, &volumeGUID, cimHashSize, &rootHash[0]); err != nil {
return "", &MountError{Cim: bCIM.String(), Op: "MountVerifiedCIM", Err: err}
}
return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil
}
// MountMergedVerifiedBlockCIMs mounts the given merged verified BlockCIM (usually created
// with `MergeBlockCIMs`) at a volume with given GUID, with the given root hash. The
// `sourceCIMs` MUST be identical to the `sourceCIMs` passed to `MergeBlockCIMs` when
// creating this merged CIM. The root hash is usually returned when the CIM is sealed or
// the root hash can be queried from a block CIM. In case of merged CIMs, the root hash of
// the merged CIM should be passed here. Every read on the mounted volume will be verified
// to match against the provided root hash if it doesn't, the read will fail. The source
// CIMs and the merged CIM MUST have been created with the verified creation flag.
func MountMergedVerifiedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlags uint32, volumeGUID guid.GUID, rootHash []byte) (string, error) {
if !IsVerifiedCimMountSupported() {
return "", fmt.Errorf("verified CIMs aren't supported on this OS version")
} else if len(sourceCIMs) < 2 {
return "", fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid)
} else if len(rootHash) != cimHashSize {
return "", fmt.Errorf("unexpected root hash size %d, expected size is %d", len(rootHash), cimHashSize)
}
switch mergedCIM.Type {
case BlockCIMTypeDevice:
mountFlags |= CimMountBlockDeviceCim
case BlockCIMTypeSingleFile:
mountFlags |= CimMountSingleFileCim
default:
return "", fmt.Errorf("invalid block CIM type `%d`", mergedCIM.Type)
}
for _, sCIM := range sourceCIMs {
if sCIM.Type != mergedCIM.Type {
return "", fmt.Errorf("source CIM (%s) type doesn't match with merged CIM type: %w", sCIM.String(), os.ErrInvalid)
}
}
// win32 mount merged CIM API expects an array of all CIMs. 0th entry in the array
// should be the merged CIM. All remaining entries should be the source CIM paths
// in the same order that was used while creating the merged CIM.
allcims := append([]*BlockCIM{mergedCIM}, sourceCIMs...)
cimsToMerge := []winapitypes.CimFsImagePath{}
for _, bcim := range allcims {
// Trailing backslashes cause problems-remove those
imageDir, err := windows.UTF16PtrFromString(strings.TrimRight(bcim.BlockPath, `\`))
if err != nil {
return "", fmt.Errorf("convert string to utf16: %w", err)
}
cimName, err := windows.UTF16PtrFromString(bcim.CimName)
if err != nil {
return "", fmt.Errorf("convert string to utf16: %w", err)
}
cimsToMerge = append(cimsToMerge, winapitypes.CimFsImagePath{
ImageDir: imageDir,
ImageName: cimName,
})
}
if err := winapi.CimMergeMountVerifiedImage(uint32(len(cimsToMerge)), &cimsToMerge[0], mountFlags, &volumeGUID, cimHashSize, &rootHash[0]); err != nil {
return "", &MountError{Cim: filepath.Join(mergedCIM.BlockPath, mergedCIM.CimName), Op: "MountMergedVerified", Err: err}
}
return fmt.Sprintf(VolumePathFormat, volumeGUID.String()), nil
}

View File

@@ -0,0 +1,372 @@
//go:build windows
// +build windows
package cim
import (
"archive/tar"
"bufio"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/Microsoft/go-winio/backuptar"
"github.com/Microsoft/hcsshim/ext4/tar2ext4"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/wclayer/cim"
"github.com/Microsoft/hcsshim/pkg/cimfs"
"github.com/Microsoft/hcsshim/pkg/ociwclayer"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)
// ImportCimLayerFromTar reads a layer from an OCI layer tar stream and extracts it into
// the CIM format at the specified path.
// `layerPath` is the directory which can be used to store intermediate files generated during layer extraction (and these file are also used when extracting children layers of this layer)
// `cimPath` is the path to the CIM in which layer files must be stored. Note that region & object files are created when writing to a CIM, these files will be created next to the `cimPath`.
// `parentLayerCimPaths` are paths to the parent layer CIMs, ordered from highest to lowest, i.e the CIM at `parentLayerCimPaths[0]` will be the immediate parent of the layer that is being extracted here.
// `parentLayerPaths` are paths to the parent layer directories. Ordered from highest to lowest.
//
// This function returns the total size of the layer's files, in bytes.
func ImportCimLayerFromTar(ctx context.Context, r io.Reader, layerPath, cimPath string, parentLayerPaths, parentLayerCimPaths []string) (_ int64, err error) {
log.G(ctx).WithFields(logrus.Fields{
"layer path": layerPath,
"layer cim path": cimPath,
"parent layer paths": strings.Join(parentLayerPaths, ", "),
"parent layer CIM paths": strings.Join(parentLayerCimPaths, ", "),
}).Debug("Importing cim layer from tar")
err = os.MkdirAll(layerPath, 0755)
if err != nil {
return 0, err
}
w, err := cim.NewForkedCimLayerWriter(ctx, layerPath, cimPath, parentLayerPaths, parentLayerCimPaths)
if err != nil {
return 0, err
}
n, err := writeCimLayerFromTar(ctx, r, w)
cerr := w.Close(ctx)
if err != nil {
return 0, err
}
if cerr != nil {
return 0, cerr
}
return n, nil
}
type blockCIMLayerImportConfig struct {
// import layers with integrity enabled CIMs
dataIntegrity bool
// parent layers
parentLayers []*cimfs.BlockCIM
// append VHD footer to the import CIMs
appendVHDFooter bool
}
// BlockCIMOpt is a function type for configuring block CIM creation options
type BlockCIMLayerImportOpt func(*blockCIMLayerImportConfig) error
func WithLayerIntegrity() BlockCIMLayerImportOpt {
return func(opts *blockCIMLayerImportConfig) error {
opts.dataIntegrity = true
return nil
}
}
func WithVHDFooter() BlockCIMLayerImportOpt {
return func(opts *blockCIMLayerImportConfig) error {
opts.appendVHDFooter = true
return nil
}
}
func WithParentLayers(parentLayers []*cimfs.BlockCIM) BlockCIMLayerImportOpt {
return func(opts *blockCIMLayerImportConfig) error {
opts.parentLayers = parentLayers
return nil
}
}
func GetIntegrityChecksum(ctx context.Context, blockPath string, pathName string) (string, error) {
log.G(ctx).Debugf("writing integrity checksum file for block CIM `%s`", blockPath)
// for convenience write a file that has the hex encoded root digest of the generated verified CIM.
// this same hex string can be used in the confidential policy.
// also return the integrity checksum as a string for integrity-vhd tooling.
digest, err := cimfs.GetVerificationInfo(blockPath)
if err != nil {
return "", fmt.Errorf("failed to query verified info of the CIM layer: %w", err)
}
digestStr := hex.EncodeToString(digest)
// only create a file if a path name is provided
if pathName != "" {
digestFile, err := os.Create(filepath.Join(filepath.Dir(blockPath), pathName))
if err != nil {
return "", fmt.Errorf("failed to create verification info file: %w", err)
}
defer digestFile.Close()
if wn, err := digestFile.WriteString(digestStr); err != nil {
return "", fmt.Errorf("failed to write verification info: %w", err)
} else if wn != len(digestStr) {
return "", fmt.Errorf("incomplete write of verification info: %w", err)
}
}
return digestStr, nil
}
func ImportBlockCIMLayerWithOpts(ctx context.Context, r io.Reader, layer *cimfs.BlockCIM, opts ...BlockCIMLayerImportOpt) (_ int64, err error) {
log.G(ctx).WithField("layer", layer).Debug("Importing block CIM layer from tar")
err = os.MkdirAll(filepath.Dir(layer.BlockPath), 0755)
if err != nil {
return 0, err
}
config := &blockCIMLayerImportConfig{}
for _, opt := range opts {
if err := opt(config); err != nil {
return 0, fmt.Errorf("block CIM import config option failure: %w", err)
}
}
log.G(ctx).WithField("config", *config).Debug("layer import config")
bcimWriterOpts := []cimfs.BlockCIMOpt{}
if config.dataIntegrity {
bcimWriterOpts = append(bcimWriterOpts, cimfs.WithDataIntegrity())
}
w, err := cim.NewBlockCIMLayerWriterWithOpts(ctx, layer, config.parentLayers, bcimWriterOpts...)
if err != nil {
return 0, err
}
n, err := writeCimLayerFromTar(ctx, r, w)
cerr := w.Close(ctx)
if err != nil {
return 0, err
}
if cerr != nil {
return 0, cerr
}
if config.appendVHDFooter {
log.G(ctx).Debugf("appending VHD footer to block CIM at `%s`", layer.BlockPath)
if err = tar2ext4.ConvertFileToVhd(layer.BlockPath); err != nil {
return 0, fmt.Errorf("append VHD footer to block CIM: %w", err)
}
}
if config.dataIntegrity {
if _, err = GetIntegrityChecksum(ctx, layer.BlockPath, "integrity_checksum"); err != nil {
return 0, err
}
}
return n, nil
}
// ImportSingleFileCimLayerFromTar reads a layer from an OCI layer tar stream and extracts
// it into the SingleFileCIM format.
func ImportSingleFileCimLayerFromTar(ctx context.Context, r io.Reader, layer *cimfs.BlockCIM, parentLayers []*cimfs.BlockCIM) (_ int64, err error) {
return ImportBlockCIMLayerWithOpts(ctx, r, layer, WithParentLayers(parentLayers))
}
func writeCimLayerFromTar(ctx context.Context, r io.Reader, w cim.CIMLayerWriter) (int64, error) {
tr := tar.NewReader(r)
buf := bufio.NewWriter(w)
size := int64(0)
// Iterate through the files in the archive.
hdr, loopErr := tr.Next()
for loopErr == nil {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
}
// Note: path is used instead of filepath to prevent OS specific handling
// of the tar path
base := path.Base(hdr.Name)
if strings.HasPrefix(base, ociwclayer.WhiteoutPrefix) {
name := path.Join(path.Dir(hdr.Name), base[len(ociwclayer.WhiteoutPrefix):])
if rErr := w.Remove(filepath.FromSlash(name)); rErr != nil {
return 0, rErr
}
hdr, loopErr = tr.Next()
} else if hdr.Typeflag == tar.TypeLink {
if linkErr := w.AddLink(filepath.FromSlash(hdr.Name), filepath.FromSlash(hdr.Linkname)); linkErr != nil {
return 0, linkErr
}
hdr, loopErr = tr.Next()
} else {
name, fileSize, fileInfo, err := backuptar.FileInfoFromHeader(hdr)
if err != nil {
return 0, err
}
sddl, err := backuptar.SecurityDescriptorFromTarHeader(hdr)
if err != nil {
return 0, err
}
eadata, err := backuptar.ExtendedAttributesFromTarHeader(hdr)
if err != nil {
return 0, err
}
var reparse []byte
// As of now the only valid reparse data in a layer will be for a symlink. If file is
// a symlink set reparse attribute and ensure reparse data buffer isn't
// empty. Otherwise remove the reparse attributed.
fileInfo.FileAttributes &^= uint32(windows.FILE_ATTRIBUTE_REPARSE_POINT)
if hdr.Typeflag == tar.TypeSymlink {
reparse = backuptar.EncodeReparsePointFromTarHeader(hdr)
if len(reparse) > 0 {
fileInfo.FileAttributes |= uint32(windows.FILE_ATTRIBUTE_REPARSE_POINT)
}
}
if addErr := w.Add(filepath.FromSlash(name), fileInfo, fileSize, sddl, eadata, reparse); addErr != nil {
return 0, addErr
}
if hdr.Typeflag == tar.TypeReg {
if _, cpErr := io.Copy(buf, tr); cpErr != nil {
return 0, cpErr
}
}
size += fileSize
// Copy all the alternate data streams and return the next non-ADS header.
var ahdr *tar.Header
for {
ahdr, loopErr = tr.Next()
if loopErr != nil {
break
}
if ahdr.Typeflag != tar.TypeReg || !strings.HasPrefix(ahdr.Name, hdr.Name+":") {
hdr = ahdr
break
}
// stream names have following format: '<filename>:<stream name>:$DATA'
// $DATA is one of the valid types of streams. We currently only support
// data streams so fail if this is some other type of stream.
if !strings.HasSuffix(ahdr.Name, ":$DATA") {
return 0, fmt.Errorf("stream types other than $DATA are not supported, found: %s", ahdr.Name)
}
if addErr := w.AddAlternateStream(filepath.FromSlash(ahdr.Name), uint64(ahdr.Size)); addErr != nil {
return 0, addErr
}
if _, cpErr := io.Copy(buf, tr); cpErr != nil {
return 0, cpErr
}
}
}
if flushErr := buf.Flush(); flushErr != nil {
if loopErr == nil {
loopErr = flushErr
} else {
log.G(ctx).WithError(flushErr).Warn("flush buffer during layer write failed")
}
}
}
if !errors.Is(loopErr, io.EOF) {
return 0, loopErr
}
return size, nil
}
// MergeBlockCIMLayersWithOpts create a new block CIM at mergedCIM.BlockPath and then
// creates a new CIM inside that block CIM that merges all the contents of the provided
// sourceCIMs. Note that this is only a metadata merge, so when this merged CIM is being
// mounted, all the sourceCIMs must be provided too and they MUST be provided in the same
// order. Expected order of sourceCIMs is that the base layer should be at the last index
// and the topmost layer should be at 0'th index. Merge operation can take a long time in
// certain situations, this function respects context deadlines in such cases. This
// function is NOT thread safe, it is caller's responsibility to handle thread safety.
func MergeBlockCIMLayersWithOpts(ctx context.Context, sourceCIMs []*cimfs.BlockCIM, mergedCIM *cimfs.BlockCIM, opts ...BlockCIMLayerImportOpt) (retErr error) {
log.G(ctx).WithFields(logrus.Fields{
"source CIMs": sourceCIMs,
"merged CIM": mergedCIM,
}).Debug("Merging block CIM layers")
// check if a merged CIM already exists
_, err := os.Stat(mergedCIM.BlockPath)
if err == nil {
return os.ErrExist
}
// Apply configuration options
config := &blockCIMLayerImportConfig{}
for _, opt := range opts {
if err := opt(config); err != nil {
return fmt.Errorf("apply merge option: %w", err)
}
}
// Prepare options for the underlying cimfs.MergeBlockCIMsWithOpts call
cimfsOpts := []cimfs.BlockCIMOpt{cimfs.WithConsistentCIM()}
if config.dataIntegrity {
cimfsOpts = append(cimfsOpts, cimfs.WithDataIntegrity())
}
// Ensure the directory for the merged CIM exists
if err := os.MkdirAll(filepath.Dir(mergedCIM.BlockPath), 0755); err != nil {
return fmt.Errorf("create directory for merged CIM: %w", err)
}
defer func() {
if retErr != nil {
if rmErr := os.Remove(mergedCIM.BlockPath); rmErr != nil {
log.G(ctx).WithError(retErr).Warnf("error in cleanup on failure: %s", rmErr)
}
}
}()
// Run the merge operation in a goroutine to handle context cancellation
// The merge operation can take a long time, so we need to respect context deadlines
errCh := make(chan error, 1)
go func() {
defer close(errCh)
err := cimfs.MergeBlockCIMsWithOpts(ctx, mergedCIM, sourceCIMs, cimfsOpts...)
errCh <- err
}()
// Wait for either the merge to complete or context to be cancelled
select {
case <-ctx.Done():
err = ctx.Err()
case err = <-errCh:
}
if err != nil {
return fmt.Errorf("merge block CIMs failed: %w", err)
}
// Handle VHD footer if requested
if config.appendVHDFooter {
log.G(ctx).Debugf("appending VHD footer to block CIM at `%s`", mergedCIM.BlockPath)
if err = tar2ext4.ConvertFileToVhd(mergedCIM.BlockPath); err != nil {
return fmt.Errorf("append VHD footer to block CIM: %w", err)
}
}
if config.dataIntegrity {
if _, err = GetIntegrityChecksum(ctx, mergedCIM.BlockPath, "merged_integrity_checksum"); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

View File

@@ -0,0 +1,65 @@
package metadata
const (
// CheckpointAnnotationEngine specifies the name of the container engine (e.g., podman, cri-o).
CheckpointAnnotationEngine = "org.criu.checkpoint.engine.name"
// CheckpointAnnotationEngineVersion specifies the version of the container engine.
CheckpointAnnotationEngineVersion = "org.criu.checkpoint.engine.version"
// CheckpointAnnotationName specifies the name of the container associated with the checkpoint.
CheckpointAnnotationName = "org.criu.checkpoint.container.name"
// CheckpointAnnotationPod specifies the name of the pod associated with the checkpoint.
CheckpointAnnotationPod = "org.criu.checkpoint.pod.name"
// CheckpointAnnotationPodID specifies the ID of the pod associated with the checkpoint.
CheckpointAnnotationPodID = "org.criu.checkpoint.pod.id"
// CheckpointAnnotationNamespace specifies the namespace of the pod associated with the checkpoint.
CheckpointAnnotationNamespace = "org.criu.checkpoint.pod.namespace"
// CheckpointAnnotationRootfsImageName specifies the name of the root filesystem image associated with the checkpoint.
CheckpointAnnotationRootfsImageName = "org.criu.checkpoint.rootfsImageName"
// CheckpointAnnotationRootfsImageUserRequested specifies the name of the root filesystem image requested by the user.
CheckpointAnnotationRootfsImageUserRequested = "org.criu.checkpoint.rootfsImageUserRequested"
// CheckpointAnnotationRootfsImageSha specifies the SHA hash of the root filesystem image associated with the checkpoint.
CheckpointAnnotationRootfsImageSha = "org.criu.checkpoint.rootfsImageSha"
// CheckpointAnnotationRootfsImageID specifies the ID of the root filesystem image associated with the checkpoint.
CheckpointAnnotationRootfsImageID = "org.criu.checkpoint.rootfsImageID"
// CheckpointAnnotationRawImageName specifies the original unprocessed name of the image used to create the container.
CheckpointAnnotationRawImageName = "org.criu.checkpoint.rawImageName"
// CheckpointAnnotationRuntimeName specifies the runtime used on the host where the checkpoint was created.
CheckpointAnnotationRuntimeName = "org.criu.checkpoint.runtime.name"
// CheckpointAnnotationRuntimeVersion specifies the version of the runtime used on the host where the checkpoint was created.
CheckpointAnnotationRuntimeVersion = "org.criu.checkpoint.runtime.version"
// CheckpointAnnotationCriuVersion specifies the version of CRIU used on the host where the checkpoint was created.
CheckpointAnnotationCriuVersion = "org.criu.checkpoint.criu.version"
// CheckpointAnnotationConmonVersion specifies the version of conmon used on the host where the checkpoint was created.
CheckpointAnnotationConmonVersion = "org.criu.checkpoint.conmon.version"
// CheckpointAnnotationHostArch specifies the CPU architecture of the host where the checkpoint was created.
CheckpointAnnotationHostArch = "org.criu.checkpoint.host.arch"
// CheckpointAnnotationHostKernel specifies the kernel version used by the host where the checkpoint was created.
CheckpointAnnotationHostKernel = "org.criu.checkpoint.host.kernel"
// CheckpointAnnotationCgroupVersion specifies the cgroup version used by the host where the checkpoint was created.
CheckpointAnnotationCgroupVersion = "org.criu.checkpoint.cgroup.version"
// CheckpointAnnotationDistributionVersion specifies the name of the host distribution on which the checkpoint was created.
// This annotation is particularly useful because some distributions may include non-upstream patches
// that can cause CRIU (Checkpoint/Restore in Userspace) to fail.
CheckpointAnnotationDistributionName = "org.criu.checkpoint.distribution.name"
// CheckpointAnnotationDistributionVersion specifies the version of the host distribution on which the checkpoint was created.
CheckpointAnnotationDistributionVersion = "org.criu.checkpoint.distribution.version"
)

View File

@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
package metadata
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
spec "github.com/opencontainers/runtime-spec/specs-go"
)
const (
// container archive
ConfigDumpFile = "config.dump"
SpecDumpFile = "spec.dump"
StatusDumpFile = "status.dump"
NetworkStatusFile = "network.status"
CheckpointDirectory = "checkpoint"
CheckpointVolumesDirectory = "volumes"
DevShmCheckpointTar = "devshm-checkpoint.tar"
RootFsDiffTar = "rootfs-diff.tar"
DeletedFilesFile = "deleted.files"
DumpLogFile = "dump.log"
RestoreLogFile = "restore.log"
// pod archive
PodOptionsFile = "pod.options"
PodDumpFile = "pod.dump"
// containerd only
StatusFile = "status"
// CRIU Images
PagesPrefix = "pages-"
AmdgpuPagesPrefix = "amdgpu-pages-"
)
// This is a reduced copy of what Podman uses to store checkpoint metadata
type ContainerConfig struct {
ID string `json:"id"`
Name string `json:"name"`
RootfsImage string `json:"rootfsImage,omitempty"`
RootfsImageRef string `json:"rootfsImageRef,omitempty"`
RootfsImageName string `json:"rootfsImageName,omitempty"`
OCIRuntime string `json:"runtime,omitempty"`
CreatedTime time.Time `json:"createdTime"`
CheckpointedAt time.Time `json:"checkpointedTime"`
RestoredAt time.Time `json:"restoredTime"`
Restored bool `json:"restored"`
}
type Spec struct {
Annotations map[string]string `json:"annotations,omitempty"`
}
type ContainerdStatus struct {
CreatedAt int64
StartedAt int64
FinishedAt int64
ExitCode int32
Pid uint32
Reason string
Message string
}
// This structure is used by the KubernetesContainerCheckpointMetadata structure
type KubernetesCheckpoint struct {
Archive string `json:"archive,omitempty"`
Size int64 `json:"size,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
// This structure is the basis for Kubernetes to track how many checkpoints
// for a certain container have been created.
type KubernetesContainerCheckpointMetadata struct {
PodFullName string `json:"podFullName,omitempty"`
ContainerName string `json:"containerName,omitempty"`
TotalSize int64 `json:"totalSize,omitempty"`
Checkpoints []KubernetesCheckpoint `json:"checkpoints"`
}
func ReadContainerCheckpointSpecDump(checkpointDirectory string) (*spec.Spec, string, error) {
var specDump spec.Spec
specDumpFile, err := ReadJSONFile(&specDump, checkpointDirectory, SpecDumpFile)
return &specDump, specDumpFile, err
}
func ReadContainerCheckpointConfigDump(checkpointDirectory string) (*ContainerConfig, string, error) {
var containerConfig ContainerConfig
configDumpFile, err := ReadJSONFile(&containerConfig, checkpointDirectory, ConfigDumpFile)
return &containerConfig, configDumpFile, err
}
func ReadContainerCheckpointDeletedFiles(checkpointDirectory string) ([]string, string, error) {
var deletedFiles []string
deletedFilesFile, err := ReadJSONFile(&deletedFiles, checkpointDirectory, DeletedFilesFile)
return deletedFiles, deletedFilesFile, err
}
func ReadContainerCheckpointStatusFile(checkpointDirectory string) (*ContainerdStatus, string, error) {
var containerdStatus ContainerdStatus
statusFile, err := ReadJSONFile(&containerdStatus, checkpointDirectory, StatusFile)
return &containerdStatus, statusFile, err
}
// WriteJSONFile marshalls and writes the given data to a JSON file
func WriteJSONFile(v interface{}, dir, file string) (string, error) {
fileJSON, err := json.MarshalIndent(v, "", " ")
if err != nil {
return "", fmt.Errorf("error marshalling JSON: %w", err)
}
file = filepath.Join(dir, file)
if err := os.WriteFile(file, fileJSON, 0o600); err != nil {
return "", err
}
return file, nil
}
func ReadJSONFile(v interface{}, dir, file string) (string, error) {
file = filepath.Join(dir, file)
content, err := os.ReadFile(file)
if err != nil {
return "", err
}
if err = json.Unmarshal(content, v); err != nil {
return "", fmt.Errorf("failed to unmarshal %s: %w", file, err)
}
return file, nil
}
func ByteToString(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp])
}

View File

@@ -0,0 +1,621 @@
//
//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.
// Bootstrap Protocol
//
// This protocol defines the interface between containerd and shims at startup.
// It replaces the previous scattered configuration mechanisms (CLI args, env vars,
// stdin JSON, spec.json annotations) with a single, versioned, extensible protocol.
//
// Flow:
// 1. containerd spawns the shim process
// 2. containerd writes BootstrapParams as JSON to shim's stdin
// 3. shim initializes and writes BootstrapResult as JSON to stdout
// 4. containerd connects to the address provided in BootstrapResult
//
// This design enables:
// - Forward/backward compatibility via version field
// - Typed extensibility via google.protobuf.Any and Extension
// - Clear capability negotiation between containerd and shims
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc (unknown)
// source: runtime/bootstrap/v1/bootstrap.proto
package bootstrap
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
anypb "google.golang.org/protobuf/types/known/anypb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// LogLevel defines log verbosity. INFO = 0, more critical levels are
// positive, and more verbose levels are negative. This is an open enum;
// unknown numeric values are preserved on the wire.
type LogLevel int32
const (
LogLevel_LOG_LEVEL_INFO LogLevel = 0
LogLevel_LOG_LEVEL_TRACE LogLevel = -8
LogLevel_LOG_LEVEL_DEBUG LogLevel = -4
LogLevel_LOG_LEVEL_WARN LogLevel = 4
LogLevel_LOG_LEVEL_ERROR LogLevel = 8
LogLevel_LOG_LEVEL_FATAL LogLevel = 10
LogLevel_LOG_LEVEL_PANIC LogLevel = 12
)
// Enum value maps for LogLevel.
var (
LogLevel_name = map[int32]string{
0: "LOG_LEVEL_INFO",
-8: "LOG_LEVEL_TRACE",
-4: "LOG_LEVEL_DEBUG",
4: "LOG_LEVEL_WARN",
8: "LOG_LEVEL_ERROR",
10: "LOG_LEVEL_FATAL",
12: "LOG_LEVEL_PANIC",
}
LogLevel_value = map[string]int32{
"LOG_LEVEL_INFO": 0,
"LOG_LEVEL_TRACE": -8,
"LOG_LEVEL_DEBUG": -4,
"LOG_LEVEL_WARN": 4,
"LOG_LEVEL_ERROR": 8,
"LOG_LEVEL_FATAL": 10,
"LOG_LEVEL_PANIC": 12,
}
)
func (x LogLevel) Enum() *LogLevel {
p := new(LogLevel)
*p = x
return p
}
func (x LogLevel) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (LogLevel) Descriptor() protoreflect.EnumDescriptor {
return file_runtime_bootstrap_v1_bootstrap_proto_enumTypes[0].Descriptor()
}
func (LogLevel) Type() protoreflect.EnumType {
return &file_runtime_bootstrap_v1_bootstrap_proto_enumTypes[0]
}
func (x LogLevel) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use LogLevel.Descriptor instead.
func (LogLevel) EnumDescriptor() ([]byte, []int) {
return file_runtime_bootstrap_v1_bootstrap_proto_rawDescGZIP(), []int{0}
}
// Capability defines optional features that can be negotiated between
// containerd and shims.
type Capability int32
const (
Capability_CAPABILITY_UNSPECIFIED Capability = 0
)
// Enum value maps for Capability.
var (
Capability_name = map[int32]string{
0: "CAPABILITY_UNSPECIFIED",
}
Capability_value = map[string]int32{
"CAPABILITY_UNSPECIFIED": 0,
}
)
func (x Capability) Enum() *Capability {
p := new(Capability)
*p = x
return p
}
func (x Capability) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Capability) Descriptor() protoreflect.EnumDescriptor {
return file_runtime_bootstrap_v1_bootstrap_proto_enumTypes[1].Descriptor()
}
func (Capability) Type() protoreflect.EnumType {
return &file_runtime_bootstrap_v1_bootstrap_proto_enumTypes[1]
}
func (x Capability) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Capability.Descriptor instead.
func (Capability) EnumDescriptor() ([]byte, []int) {
return file_runtime_bootstrap_v1_bootstrap_proto_rawDescGZIP(), []int{1}
}
// BootstrapParams contains all configuration passed from containerd to shim at startup.
type BootstrapParams struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Container/sandbox ID
InstanceID string `protobuf:"bytes,1,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"`
// Namespace for the container
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
// Requested shim log level.
// INFO = 0, more critical levels are positive, and more verbose
// levels are negative. This is an open enum; shims should treat
// unrecognized values by mapping to the nearest known level.
LogLevel LogLevel `protobuf:"varint,3,opt,name=log_level,json=logLevel,proto3,enum=containerd.runtime.bootstrap.v1.LogLevel" json:"log_level,omitempty"`
// containerd daemon version that is launching this shim.
ContainerdVersion string `protobuf:"bytes,4,opt,name=containerd_version,json=containerdVersion,proto3" json:"containerd_version,omitempty"`
// Containerd's TTRPC API address (e.g., "unix:///run/containerd/containerd.sock.ttrpc")
ContainerdTtrpcAddress string `protobuf:"bytes,5,opt,name=containerd_ttrpc_address,json=containerdTtrpcAddress,proto3" json:"containerd_ttrpc_address,omitempty"`
// Containerd's gRPC API address (e.g., "unix:///run/containerd/containerd.sock")
ContainerdGrpcAddress string `protobuf:"bytes,6,opt,name=containerd_grpc_address,json=containerdGrpcAddress,proto3" json:"containerd_grpc_address,omitempty"`
// Path to containerd binary for event publishing
ContainerdBinary string `protobuf:"bytes,7,opt,name=containerd_binary,json=containerdBinary,proto3" json:"containerd_binary,omitempty"`
// Extensible configuration sections for new features
// Each section can contain arbitrary structured data identified by type URL
// Examples: CRI config, NRI config, sandbox config, etc.
Extensions []*Extension `protobuf:"bytes,8,rep,name=extensions,proto3" json:"extensions,omitempty"`
// Optional directory for the shim to place its unix socket.
// If empty, the shim defaults to a short, well-known path
// (e.g., /run/containerd/s). The path must be kept short because
// the socket filename is a 64-character SHA256 hash and unix
// socket paths are limited to 104-108 bytes depending on platform.
SocketDir *string `protobuf:"bytes,9,opt,name=socket_dir,json=socketDir,proto3,oneof" json:"socket_dir,omitempty"`
}
func (x *BootstrapParams) Reset() {
*x = BootstrapParams{}
if protoimpl.UnsafeEnabled {
mi := &file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BootstrapParams) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BootstrapParams) ProtoMessage() {}
func (x *BootstrapParams) ProtoReflect() protoreflect.Message {
mi := &file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BootstrapParams.ProtoReflect.Descriptor instead.
func (*BootstrapParams) Descriptor() ([]byte, []int) {
return file_runtime_bootstrap_v1_bootstrap_proto_rawDescGZIP(), []int{0}
}
func (x *BootstrapParams) GetInstanceID() string {
if x != nil {
return x.InstanceID
}
return ""
}
func (x *BootstrapParams) GetNamespace() string {
if x != nil {
return x.Namespace
}
return ""
}
func (x *BootstrapParams) GetLogLevel() LogLevel {
if x != nil {
return x.LogLevel
}
return LogLevel_LOG_LEVEL_INFO
}
func (x *BootstrapParams) GetContainerdVersion() string {
if x != nil {
return x.ContainerdVersion
}
return ""
}
func (x *BootstrapParams) GetContainerdTtrpcAddress() string {
if x != nil {
return x.ContainerdTtrpcAddress
}
return ""
}
func (x *BootstrapParams) GetContainerdGrpcAddress() string {
if x != nil {
return x.ContainerdGrpcAddress
}
return ""
}
func (x *BootstrapParams) GetContainerdBinary() string {
if x != nil {
return x.ContainerdBinary
}
return ""
}
func (x *BootstrapParams) GetExtensions() []*Extension {
if x != nil {
return x.Extensions
}
return nil
}
func (x *BootstrapParams) GetSocketDir() string {
if x != nil && x.SocketDir != nil {
return *x.SocketDir
}
return ""
}
// Extension provides extensibility for new configuration types
// without changing the core BootstrapParams protocol
type Extension struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Configuration data with embedded type URL
// Examples of type URLs:
// - "containerd.io/cri.v1.PodSandboxConfig"
// - "containerd.io/nri.v1.PluginConfig"
// - "containerd.io/sandbox.v1.SandboxConfig"
Value *anypb.Any `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *Extension) Reset() {
*x = Extension{}
if protoimpl.UnsafeEnabled {
mi := &file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Extension) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Extension) ProtoMessage() {}
func (x *Extension) ProtoReflect() protoreflect.Message {
mi := &file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Extension.ProtoReflect.Descriptor instead.
func (*Extension) Descriptor() ([]byte, []int) {
return file_runtime_bootstrap_v1_bootstrap_proto_rawDescGZIP(), []int{1}
}
func (x *Extension) GetValue() *anypb.Any {
if x != nil {
return x.Value
}
return nil
}
// BootstrapResult is returned by shim via stdout after successful startup
type BootstrapResult struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Version of shim parameters (expected 2 for shim v2)
Version int32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
// Address where shim is listening (e.g., "unix:///run/containerd/shim.sock")
// Containerd will connect to this address for task operations
Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
// Protocol used by shim: "ttrpc" or "grpc"
Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"`
// Optional: Capabilities supported by this shim instance.
// Reserved for future use to allow optional capability negotiation
// between the daemon and shim.
Capabilities []Capability `protobuf:"varint,4,rep,packed,name=capabilities,proto3,enum=containerd.runtime.bootstrap.v1.Capability" json:"capabilities,omitempty"`
// Optional: Additional metadata from shim
Metadata map[string]string `protobuf:"bytes,5,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *BootstrapResult) Reset() {
*x = BootstrapResult{}
if protoimpl.UnsafeEnabled {
mi := &file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BootstrapResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BootstrapResult) ProtoMessage() {}
func (x *BootstrapResult) ProtoReflect() protoreflect.Message {
mi := &file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BootstrapResult.ProtoReflect.Descriptor instead.
func (*BootstrapResult) Descriptor() ([]byte, []int) {
return file_runtime_bootstrap_v1_bootstrap_proto_rawDescGZIP(), []int{2}
}
func (x *BootstrapResult) GetVersion() int32 {
if x != nil {
return x.Version
}
return 0
}
func (x *BootstrapResult) GetAddress() string {
if x != nil {
return x.Address
}
return ""
}
func (x *BootstrapResult) GetProtocol() string {
if x != nil {
return x.Protocol
}
return ""
}
func (x *BootstrapResult) GetCapabilities() []Capability {
if x != nil {
return x.Capabilities
}
return nil
}
func (x *BootstrapResult) GetMetadata() map[string]string {
if x != nil {
return x.Metadata
}
return nil
}
var File_runtime_bootstrap_v1_bootstrap_proto protoreflect.FileDescriptor
var file_runtime_bootstrap_v1_bootstrap_proto_rawDesc = []byte{
0x0a, 0x24, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74,
0x72, 0x61, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x64, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x73,
0x74, 0x72, 0x61, 0x70, 0x2e, 0x76, 0x31, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x22, 0xe5, 0x03, 0x0a, 0x0f, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70,
0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e,
0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73,
0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65,
0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, 0x65, 0x76,
0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x62, 0x6f,
0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65,
0x76, 0x65, 0x6c, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x2d, 0x0a,
0x12, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x5f, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x18,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x5f, 0x74, 0x74, 0x72, 0x70, 0x63,
0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x54, 0x74, 0x72, 0x70, 0x63, 0x41,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x64, 0x5f, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73,
0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x64, 0x47, 0x72, 0x70, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2b,
0x0a, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x5f, 0x62, 0x69, 0x6e,
0x61, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x64, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x12, 0x4a, 0x0a, 0x0a, 0x65,
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x72, 0x75, 0x6e,
0x74, 0x69, 0x6d, 0x65, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x2e, 0x76,
0x31, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x65, 0x78, 0x74,
0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x0a, 0x73, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x73,
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x69, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f,
0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x64, 0x69, 0x72, 0x22, 0x37, 0x0a, 0x09, 0x45, 0x78,
0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x22, 0xcb, 0x02, 0x0a, 0x0f, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61,
0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x4f, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62,
0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x2b, 0x2e,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69,
0x6d, 0x65, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x2e, 0x76, 0x31, 0x2e,
0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61,
0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x5a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x63, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e,
0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x6f, 0x6f,
0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
0x01, 0x2a, 0xad, 0x01, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x12,
0x0a, 0x0e, 0x4c, 0x4f, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x49, 0x4e, 0x46, 0x4f,
0x10, 0x00, 0x12, 0x1c, 0x0a, 0x0f, 0x4c, 0x4f, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f,
0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
0x12, 0x1c, 0x0a, 0x0f, 0x4c, 0x4f, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x44, 0x45,
0x42, 0x55, 0x47, 0x10, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x12, 0x12,
0x0a, 0x0e, 0x4c, 0x4f, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x57, 0x41, 0x52, 0x4e,
0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x4c, 0x4f, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f,
0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x08, 0x12, 0x13, 0x0a, 0x0f, 0x4c, 0x4f, 0x47, 0x5f, 0x4c,
0x45, 0x56, 0x45, 0x4c, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x0a, 0x12, 0x13, 0x0a, 0x0f,
0x4c, 0x4f, 0x47, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10,
0x0c, 0x2a, 0x28, 0x0a, 0x0a, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12,
0x1a, 0x0a, 0x16, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e,
0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x42, 0x45, 0x5a, 0x43, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f, 0x62, 0x6f, 0x6f, 0x74,
0x73, 0x74, 0x72, 0x61, 0x70, 0x2f, 0x76, 0x31, 0x3b, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72,
0x61, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_runtime_bootstrap_v1_bootstrap_proto_rawDescOnce sync.Once
file_runtime_bootstrap_v1_bootstrap_proto_rawDescData = file_runtime_bootstrap_v1_bootstrap_proto_rawDesc
)
func file_runtime_bootstrap_v1_bootstrap_proto_rawDescGZIP() []byte {
file_runtime_bootstrap_v1_bootstrap_proto_rawDescOnce.Do(func() {
file_runtime_bootstrap_v1_bootstrap_proto_rawDescData = protoimpl.X.CompressGZIP(file_runtime_bootstrap_v1_bootstrap_proto_rawDescData)
})
return file_runtime_bootstrap_v1_bootstrap_proto_rawDescData
}
var file_runtime_bootstrap_v1_bootstrap_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_runtime_bootstrap_v1_bootstrap_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_runtime_bootstrap_v1_bootstrap_proto_goTypes = []interface{}{
(LogLevel)(0), // 0: containerd.runtime.bootstrap.v1.LogLevel
(Capability)(0), // 1: containerd.runtime.bootstrap.v1.Capability
(*BootstrapParams)(nil), // 2: containerd.runtime.bootstrap.v1.BootstrapParams
(*Extension)(nil), // 3: containerd.runtime.bootstrap.v1.Extension
(*BootstrapResult)(nil), // 4: containerd.runtime.bootstrap.v1.BootstrapResult
nil, // 5: containerd.runtime.bootstrap.v1.BootstrapResult.MetadataEntry
(*anypb.Any)(nil), // 6: google.protobuf.Any
}
var file_runtime_bootstrap_v1_bootstrap_proto_depIdxs = []int32{
0, // 0: containerd.runtime.bootstrap.v1.BootstrapParams.log_level:type_name -> containerd.runtime.bootstrap.v1.LogLevel
3, // 1: containerd.runtime.bootstrap.v1.BootstrapParams.extensions:type_name -> containerd.runtime.bootstrap.v1.Extension
6, // 2: containerd.runtime.bootstrap.v1.Extension.value:type_name -> google.protobuf.Any
1, // 3: containerd.runtime.bootstrap.v1.BootstrapResult.capabilities:type_name -> containerd.runtime.bootstrap.v1.Capability
5, // 4: containerd.runtime.bootstrap.v1.BootstrapResult.metadata:type_name -> containerd.runtime.bootstrap.v1.BootstrapResult.MetadataEntry
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_runtime_bootstrap_v1_bootstrap_proto_init() }
func file_runtime_bootstrap_v1_bootstrap_proto_init() {
if File_runtime_bootstrap_v1_bootstrap_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BootstrapParams); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Extension); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BootstrapResult); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_runtime_bootstrap_v1_bootstrap_proto_msgTypes[0].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_runtime_bootstrap_v1_bootstrap_proto_rawDesc,
NumEnums: 2,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_runtime_bootstrap_v1_bootstrap_proto_goTypes,
DependencyIndexes: file_runtime_bootstrap_v1_bootstrap_proto_depIdxs,
EnumInfos: file_runtime_bootstrap_v1_bootstrap_proto_enumTypes,
MessageInfos: file_runtime_bootstrap_v1_bootstrap_proto_msgTypes,
}.Build()
File_runtime_bootstrap_v1_bootstrap_proto = out.File
file_runtime_bootstrap_v1_bootstrap_proto_rawDesc = nil
file_runtime_bootstrap_v1_bootstrap_proto_goTypes = nil
file_runtime_bootstrap_v1_bootstrap_proto_depIdxs = nil
}

View File

@@ -0,0 +1,130 @@
/*
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.
*/
// Bootstrap Protocol
//
// This protocol defines the interface between containerd and shims at startup.
// It replaces the previous scattered configuration mechanisms (CLI args, env vars,
// stdin JSON, spec.json annotations) with a single, versioned, extensible protocol.
//
// Flow:
// 1. containerd spawns the shim process
// 2. containerd writes BootstrapParams as JSON to shim's stdin
// 3. shim initializes and writes BootstrapResult as JSON to stdout
// 4. containerd connects to the address provided in BootstrapResult
//
// This design enables:
// - Forward/backward compatibility via version field
// - Typed extensibility via google.protobuf.Any and Extension
// - Clear capability negotiation between containerd and shims
syntax = "proto3";
package containerd.runtime.bootstrap.v1;
import "google/protobuf/any.proto";
option go_package = "github.com/containerd/containerd/api/runtime/bootstrap/v1;bootstrap";
// BootstrapParams contains all configuration passed from containerd to shim at startup.
message BootstrapParams {
// Container/sandbox ID
string instance_id = 1;
// Namespace for the container
string namespace = 2;
// Requested shim log level.
// INFO = 0, more critical levels are positive, and more verbose
// levels are negative. This is an open enum; shims should treat
// unrecognized values by mapping to the nearest known level.
LogLevel log_level = 3;
// containerd daemon version that is launching this shim.
string containerd_version = 4;
// Containerd's TTRPC API address (e.g., "unix:///run/containerd/containerd.sock.ttrpc")
string containerd_ttrpc_address = 5;
// Containerd's gRPC API address (e.g., "unix:///run/containerd/containerd.sock")
string containerd_grpc_address = 6;
// Path to containerd binary for event publishing
string containerd_binary = 7;
// Extensible configuration sections for new features
// Each section can contain arbitrary structured data identified by type URL
// Examples: CRI config, NRI config, sandbox config, etc.
repeated Extension extensions = 8;
// Optional directory for the shim to place its unix socket.
// If empty, the shim defaults to a short, well-known path
// (e.g., /run/containerd/s). The path must be kept short because
// the socket filename is a 64-character SHA256 hash and unix
// socket paths are limited to 104-108 bytes depending on platform.
optional string socket_dir = 9;
}
// Extension provides extensibility for new configuration types
// without changing the core BootstrapParams protocol
message Extension {
// Configuration data with embedded type URL
// Examples of type URLs:
// - "containerd.io/cri.v1.PodSandboxConfig"
// - "containerd.io/nri.v1.PluginConfig"
// - "containerd.io/sandbox.v1.SandboxConfig"
google.protobuf.Any value = 1;
}
// BootstrapResult is returned by shim via stdout after successful startup
message BootstrapResult {
// Version of shim parameters (expected 2 for shim v2)
int32 version = 1;
// Address where shim is listening (e.g., "unix:///run/containerd/shim.sock")
// Containerd will connect to this address for task operations
string address = 2;
// Protocol used by shim: "ttrpc" or "grpc"
string protocol = 3;
// Optional: Capabilities supported by this shim instance.
// Reserved for future use to allow optional capability negotiation
// between the daemon and shim.
repeated Capability capabilities = 4;
// Optional: Additional metadata from shim
map<string, string> metadata = 5;
}
// LogLevel defines log verbosity. INFO = 0, more critical levels are
// positive, and more verbose levels are negative. This is an open enum;
// unknown numeric values are preserved on the wire.
enum LogLevel {
LOG_LEVEL_INFO = 0;
LOG_LEVEL_TRACE = -8;
LOG_LEVEL_DEBUG = -4;
LOG_LEVEL_WARN = 4;
LOG_LEVEL_ERROR = 8;
LOG_LEVEL_FATAL = 10;
LOG_LEVEL_PANIC = 12;
}
// Capability defines optional features that can be negotiated between
// containerd and shims.
enum Capability {
CAPABILITY_UNSPECIFIED = 0;
}

View File

@@ -0,0 +1,17 @@
/*
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 bootstrap

View File

@@ -0,0 +1,91 @@
/*
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 bootstrap
import (
"fmt"
"strconv"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
// LogLevelFromString converts a log level string (e.g. "debug", "info") to a
// LogLevel enum value. The accepted strings are compatible with logrus level names.
func LogLevelFromString(s string) LogLevel {
switch s {
case "trace":
return LogLevel_LOG_LEVEL_TRACE
case "debug":
return LogLevel_LOG_LEVEL_DEBUG
case "info":
return LogLevel_LOG_LEVEL_INFO
case "warn", "warning":
return LogLevel_LOG_LEVEL_WARN
case "error":
return LogLevel_LOG_LEVEL_ERROR
case "fatal":
return LogLevel_LOG_LEVEL_FATAL
case "panic":
return LogLevel_LOG_LEVEL_PANIC
default:
if v, err := strconv.ParseInt(s, 10, 32); err == nil {
return LogLevel(v)
}
return LogLevel_LOG_LEVEL_INFO
}
}
// AddExtension adds a new extension to the BootstrapParams.
// The message is wrapped in a google.protobuf.Any with its type URL automatically set.
// If the message is already an *anypb.Any, it is used directly without double-wrapping.
func (p *BootstrapParams) AddExtension(msg proto.Message) error {
var anyVal *anypb.Any
if a, ok := msg.(*anypb.Any); ok {
// Already an Any, use it directly
anyVal = a
} else {
var err error
anyVal, err = anypb.New(msg)
if err != nil {
return err
}
}
p.Extensions = append(p.Extensions, &Extension{Value: anyVal})
return nil
}
// FindExtension finds an extension matching the type of dst and unmarshals it.
func (p *BootstrapParams) FindExtension(dst proto.Message) (bool, error) {
if p == nil {
return false, nil
}
name := dst.ProtoReflect().Descriptor().FullName()
for _, ext := range p.Extensions {
if ext.GetValue().MessageIs(dst) {
if err := ext.GetValue().UnmarshalTo(dst); err != nil {
return false, fmt.Errorf("failed to unmarshal extension %q: %w", name, err)
}
return true, nil
}
}
return false, nil
}

View File

@@ -0,0 +1,17 @@
/*
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 task

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
/*
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.
*/
syntax = "proto3";
package containerd.task.v2;
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "types/mount.proto";
import "types/task/task.proto";
option go_package = "github.com/containerd/containerd/api/runtime/task/v2;task";
// Shim service is launched for each container and is responsible for owning the IO
// for the container and its additional processes. The shim is also the parent of
// each container and allows reattaching to the IO and receiving the exit status
// for the container processes.
service Task {
rpc State(StateRequest) returns (StateResponse);
rpc Create(CreateTaskRequest) returns (CreateTaskResponse);
rpc Start(StartRequest) returns (StartResponse);
rpc Delete(DeleteRequest) returns (DeleteResponse);
rpc Pids(PidsRequest) returns (PidsResponse);
rpc Pause(PauseRequest) returns (google.protobuf.Empty);
rpc Resume(ResumeRequest) returns (google.protobuf.Empty);
rpc Checkpoint(CheckpointTaskRequest) returns (google.protobuf.Empty);
rpc Kill(KillRequest) returns (google.protobuf.Empty);
rpc Exec(ExecProcessRequest) returns (google.protobuf.Empty);
rpc ResizePty(ResizePtyRequest) returns (google.protobuf.Empty);
rpc CloseIO(CloseIORequest) returns (google.protobuf.Empty);
rpc Update(UpdateTaskRequest) returns (google.protobuf.Empty);
rpc Wait(WaitRequest) returns (WaitResponse);
rpc Stats(StatsRequest) returns (StatsResponse);
rpc Connect(ConnectRequest) returns (ConnectResponse);
rpc Shutdown(ShutdownRequest) returns (google.protobuf.Empty);
}
message CreateTaskRequest {
string id = 1;
string bundle = 2;
repeated containerd.types.Mount rootfs = 3;
bool terminal = 4;
string stdin = 5;
string stdout = 6;
string stderr = 7;
string checkpoint = 8;
string parent_checkpoint = 9;
google.protobuf.Any options = 10;
}
message CreateTaskResponse {
uint32 pid = 1;
}
message DeleteRequest {
string id = 1;
string exec_id = 2;
}
message DeleteResponse {
uint32 pid = 1;
uint32 exit_status = 2;
google.protobuf.Timestamp exited_at = 3;
}
message ExecProcessRequest {
string id = 1;
string exec_id = 2;
bool terminal = 3;
string stdin = 4;
string stdout = 5;
string stderr = 6;
google.protobuf.Any spec = 7;
}
message ExecProcessResponse {}
message ResizePtyRequest {
string id = 1;
string exec_id = 2;
uint32 width = 3;
uint32 height = 4;
}
message StateRequest {
string id = 1;
string exec_id = 2;
}
message StateResponse {
string id = 1;
string bundle = 2;
uint32 pid = 3;
containerd.v1.types.Status status = 4;
string stdin = 5;
string stdout = 6;
string stderr = 7;
bool terminal = 8;
uint32 exit_status = 9;
google.protobuf.Timestamp exited_at = 10;
string exec_id = 11;
}
message KillRequest {
string id = 1;
string exec_id = 2;
uint32 signal = 3;
bool all = 4;
}
message CloseIORequest {
string id = 1;
string exec_id = 2;
bool stdin = 3;
}
message PidsRequest {
string id = 1;
}
message PidsResponse {
repeated containerd.v1.types.ProcessInfo processes = 1;
}
message CheckpointTaskRequest {
string id = 1;
string path = 2;
google.protobuf.Any options = 3;
}
message UpdateTaskRequest {
string id = 1;
google.protobuf.Any resources = 2;
map<string, string> annotations = 3;
}
message StartRequest {
string id = 1;
string exec_id = 2;
}
message StartResponse {
uint32 pid = 1;
}
message WaitRequest {
string id = 1;
string exec_id = 2;
}
message WaitResponse {
uint32 exit_status = 1;
google.protobuf.Timestamp exited_at = 2;
}
message StatsRequest {
string id = 1;
}
message StatsResponse {
google.protobuf.Any stats = 1;
}
message ConnectRequest {
string id = 1;
}
message ConnectResponse {
uint32 shim_pid = 1;
uint32 task_pid = 2;
string version = 3;
}
message ShutdownRequest {
string id = 1;
bool now = 2;
}
message PauseRequest {
string id = 1;
}
message ResumeRequest {
string id = 1;
}

View File

@@ -0,0 +1,301 @@
// Code generated by protoc-gen-go-ttrpc. DO NOT EDIT.
// source: runtime/task/v2/shim.proto
package task
import (
context "context"
ttrpc "github.com/containerd/ttrpc"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
type TTRPCTaskService interface {
State(context.Context, *StateRequest) (*StateResponse, error)
Create(context.Context, *CreateTaskRequest) (*CreateTaskResponse, error)
Start(context.Context, *StartRequest) (*StartResponse, error)
Delete(context.Context, *DeleteRequest) (*DeleteResponse, error)
Pids(context.Context, *PidsRequest) (*PidsResponse, error)
Pause(context.Context, *PauseRequest) (*emptypb.Empty, error)
Resume(context.Context, *ResumeRequest) (*emptypb.Empty, error)
Checkpoint(context.Context, *CheckpointTaskRequest) (*emptypb.Empty, error)
Kill(context.Context, *KillRequest) (*emptypb.Empty, error)
Exec(context.Context, *ExecProcessRequest) (*emptypb.Empty, error)
ResizePty(context.Context, *ResizePtyRequest) (*emptypb.Empty, error)
CloseIO(context.Context, *CloseIORequest) (*emptypb.Empty, error)
Update(context.Context, *UpdateTaskRequest) (*emptypb.Empty, error)
Wait(context.Context, *WaitRequest) (*WaitResponse, error)
Stats(context.Context, *StatsRequest) (*StatsResponse, error)
Connect(context.Context, *ConnectRequest) (*ConnectResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*emptypb.Empty, error)
}
func RegisterTTRPCTaskService(srv *ttrpc.Server, svc TTRPCTaskService) {
srv.RegisterService("containerd.task.v2.Task", &ttrpc.ServiceDesc{
Methods: map[string]ttrpc.Method{
"State": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req StateRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.State(ctx, &req)
},
"Create": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req CreateTaskRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Create(ctx, &req)
},
"Start": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req StartRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Start(ctx, &req)
},
"Delete": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req DeleteRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Delete(ctx, &req)
},
"Pids": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req PidsRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Pids(ctx, &req)
},
"Pause": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req PauseRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Pause(ctx, &req)
},
"Resume": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ResumeRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Resume(ctx, &req)
},
"Checkpoint": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req CheckpointTaskRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Checkpoint(ctx, &req)
},
"Kill": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req KillRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Kill(ctx, &req)
},
"Exec": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ExecProcessRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Exec(ctx, &req)
},
"ResizePty": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ResizePtyRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.ResizePty(ctx, &req)
},
"CloseIO": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req CloseIORequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.CloseIO(ctx, &req)
},
"Update": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req UpdateTaskRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Update(ctx, &req)
},
"Wait": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req WaitRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Wait(ctx, &req)
},
"Stats": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req StatsRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Stats(ctx, &req)
},
"Connect": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ConnectRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Connect(ctx, &req)
},
"Shutdown": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ShutdownRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Shutdown(ctx, &req)
},
},
})
}
type ttrpctaskClient struct {
client *ttrpc.Client
}
func NewTTRPCTaskClient(client *ttrpc.Client) TTRPCTaskService {
return &ttrpctaskClient{
client: client,
}
}
func (c *ttrpctaskClient) State(ctx context.Context, req *StateRequest) (*StateResponse, error) {
var resp StateResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "State", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Create(ctx context.Context, req *CreateTaskRequest) (*CreateTaskResponse, error) {
var resp CreateTaskResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Create", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Start(ctx context.Context, req *StartRequest) (*StartResponse, error) {
var resp StartResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Start", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Delete(ctx context.Context, req *DeleteRequest) (*DeleteResponse, error) {
var resp DeleteResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Delete", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Pids(ctx context.Context, req *PidsRequest) (*PidsResponse, error) {
var resp PidsResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Pids", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Pause(ctx context.Context, req *PauseRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Pause", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Resume(ctx context.Context, req *ResumeRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Resume", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Checkpoint(ctx context.Context, req *CheckpointTaskRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Checkpoint", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Kill(ctx context.Context, req *KillRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Kill", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Exec(ctx context.Context, req *ExecProcessRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Exec", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) ResizePty(ctx context.Context, req *ResizePtyRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "ResizePty", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) CloseIO(ctx context.Context, req *CloseIORequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "CloseIO", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Update(ctx context.Context, req *UpdateTaskRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Update", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Wait(ctx context.Context, req *WaitRequest) (*WaitResponse, error) {
var resp WaitResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Wait", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Stats(ctx context.Context, req *StatsRequest) (*StatsResponse, error) {
var resp StatsResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Stats", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Connect(ctx context.Context, req *ConnectRequest) (*ConnectResponse, error) {
var resp ConnectResponse
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Connect", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Shutdown(ctx context.Context, req *ShutdownRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v2.Task", "Shutdown", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}

View File

@@ -0,0 +1,17 @@
/*
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 task

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
/*
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.
*/
syntax = "proto3";
package containerd.task.v3;
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "types/mount.proto";
import "types/task/task.proto";
option go_package = "github.com/containerd/containerd/api/runtime/task/v3;task";
// Shim service is launched for each container and is responsible for owning the IO
// for the container and its additional processes. The shim is also the parent of
// each container and allows reattaching to the IO and receiving the exit status
// for the container processes.
service Task {
rpc State(StateRequest) returns (StateResponse);
rpc Create(CreateTaskRequest) returns (CreateTaskResponse);
rpc Start(StartRequest) returns (StartResponse);
rpc Delete(DeleteRequest) returns (DeleteResponse);
rpc Pids(PidsRequest) returns (PidsResponse);
rpc Pause(PauseRequest) returns (google.protobuf.Empty);
rpc Resume(ResumeRequest) returns (google.protobuf.Empty);
rpc Checkpoint(CheckpointTaskRequest) returns (google.protobuf.Empty);
rpc Kill(KillRequest) returns (google.protobuf.Empty);
rpc Exec(ExecProcessRequest) returns (google.protobuf.Empty);
rpc ResizePty(ResizePtyRequest) returns (google.protobuf.Empty);
rpc CloseIO(CloseIORequest) returns (google.protobuf.Empty);
rpc Update(UpdateTaskRequest) returns (google.protobuf.Empty);
rpc Wait(WaitRequest) returns (WaitResponse);
rpc Stats(StatsRequest) returns (StatsResponse);
rpc Connect(ConnectRequest) returns (ConnectResponse);
rpc Shutdown(ShutdownRequest) returns (google.protobuf.Empty);
}
message CreateTaskRequest {
string id = 1;
string bundle = 2;
repeated containerd.types.Mount rootfs = 3;
bool terminal = 4;
string stdin = 5;
string stdout = 6;
string stderr = 7;
string checkpoint = 8;
string parent_checkpoint = 9;
google.protobuf.Any options = 10;
}
message CreateTaskResponse {
uint32 pid = 1;
}
message DeleteRequest {
string id = 1;
string exec_id = 2;
}
message DeleteResponse {
uint32 pid = 1;
uint32 exit_status = 2;
google.protobuf.Timestamp exited_at = 3;
}
message ExecProcessRequest {
string id = 1;
string exec_id = 2;
bool terminal = 3;
string stdin = 4;
string stdout = 5;
string stderr = 6;
google.protobuf.Any spec = 7;
}
message ExecProcessResponse {}
message ResizePtyRequest {
string id = 1;
string exec_id = 2;
uint32 width = 3;
uint32 height = 4;
}
message StateRequest {
string id = 1;
string exec_id = 2;
}
message StateResponse {
string id = 1;
string bundle = 2;
uint32 pid = 3;
containerd.v1.types.Status status = 4;
string stdin = 5;
string stdout = 6;
string stderr = 7;
bool terminal = 8;
uint32 exit_status = 9;
google.protobuf.Timestamp exited_at = 10;
string exec_id = 11;
}
message KillRequest {
string id = 1;
string exec_id = 2;
uint32 signal = 3;
bool all = 4;
}
message CloseIORequest {
string id = 1;
string exec_id = 2;
bool stdin = 3;
}
message PidsRequest {
string id = 1;
}
message PidsResponse {
repeated containerd.v1.types.ProcessInfo processes = 1;
}
message CheckpointTaskRequest {
string id = 1;
string path = 2;
google.protobuf.Any options = 3;
}
message UpdateTaskRequest {
string id = 1;
google.protobuf.Any resources = 2;
map<string, string> annotations = 3;
}
message StartRequest {
string id = 1;
string exec_id = 2;
}
message StartResponse {
uint32 pid = 1;
}
message WaitRequest {
string id = 1;
string exec_id = 2;
}
message WaitResponse {
uint32 exit_status = 1;
google.protobuf.Timestamp exited_at = 2;
}
message StatsRequest {
string id = 1;
}
message StatsResponse {
google.protobuf.Any stats = 1;
}
message ConnectRequest {
string id = 1;
}
message ConnectResponse {
uint32 shim_pid = 1;
uint32 task_pid = 2;
string version = 3;
}
message ShutdownRequest {
string id = 1;
bool now = 2;
}
message PauseRequest {
string id = 1;
}
message ResumeRequest {
string id = 1;
}

View File

@@ -0,0 +1,684 @@
//go:build !no_grpc
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc (unknown)
// source: runtime/task/v3/shim.proto
package task
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// TaskClient is the client API for Task service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TaskClient interface {
State(ctx context.Context, in *StateRequest, opts ...grpc.CallOption) (*StateResponse, error)
Create(ctx context.Context, in *CreateTaskRequest, opts ...grpc.CallOption) (*CreateTaskResponse, error)
Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption) (*StartResponse, error)
Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error)
Pids(ctx context.Context, in *PidsRequest, opts ...grpc.CallOption) (*PidsResponse, error)
Pause(ctx context.Context, in *PauseRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Resume(ctx context.Context, in *ResumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Checkpoint(ctx context.Context, in *CheckpointTaskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Kill(ctx context.Context, in *KillRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Exec(ctx context.Context, in *ExecProcessRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ResizePty(ctx context.Context, in *ResizePtyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CloseIO(ctx context.Context, in *CloseIORequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Update(ctx context.Context, in *UpdateTaskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Wait(ctx context.Context, in *WaitRequest, opts ...grpc.CallOption) (*WaitResponse, error)
Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsResponse, error)
Connect(ctx context.Context, in *ConnectRequest, opts ...grpc.CallOption) (*ConnectResponse, error)
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type taskClient struct {
cc grpc.ClientConnInterface
}
func NewTaskClient(cc grpc.ClientConnInterface) TaskClient {
return &taskClient{cc}
}
func (c *taskClient) State(ctx context.Context, in *StateRequest, opts ...grpc.CallOption) (*StateResponse, error) {
out := new(StateResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/State", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Create(ctx context.Context, in *CreateTaskRequest, opts ...grpc.CallOption) (*CreateTaskResponse, error) {
out := new(CreateTaskResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Create", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption) (*StartResponse, error) {
out := new(StartResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Start", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error) {
out := new(DeleteResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Delete", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Pids(ctx context.Context, in *PidsRequest, opts ...grpc.CallOption) (*PidsResponse, error) {
out := new(PidsResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Pids", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Pause(ctx context.Context, in *PauseRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Pause", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Resume(ctx context.Context, in *ResumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Resume", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Checkpoint(ctx context.Context, in *CheckpointTaskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Checkpoint", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Kill(ctx context.Context, in *KillRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Kill", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Exec(ctx context.Context, in *ExecProcessRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Exec", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) ResizePty(ctx context.Context, in *ResizePtyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/ResizePty", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) CloseIO(ctx context.Context, in *CloseIORequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/CloseIO", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Update(ctx context.Context, in *UpdateTaskRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Update", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Wait(ctx context.Context, in *WaitRequest, opts ...grpc.CallOption) (*WaitResponse, error) {
out := new(WaitResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Wait", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsResponse, error) {
out := new(StatsResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Stats", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Connect(ctx context.Context, in *ConnectRequest, opts ...grpc.CallOption) (*ConnectResponse, error) {
out := new(ConnectResponse)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Connect", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *taskClient) Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/containerd.task.v3.Task/Shutdown", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// TaskServer is the server API for Task service.
// All implementations must embed UnimplementedTaskServer
// for forward compatibility
type TaskServer interface {
State(context.Context, *StateRequest) (*StateResponse, error)
Create(context.Context, *CreateTaskRequest) (*CreateTaskResponse, error)
Start(context.Context, *StartRequest) (*StartResponse, error)
Delete(context.Context, *DeleteRequest) (*DeleteResponse, error)
Pids(context.Context, *PidsRequest) (*PidsResponse, error)
Pause(context.Context, *PauseRequest) (*emptypb.Empty, error)
Resume(context.Context, *ResumeRequest) (*emptypb.Empty, error)
Checkpoint(context.Context, *CheckpointTaskRequest) (*emptypb.Empty, error)
Kill(context.Context, *KillRequest) (*emptypb.Empty, error)
Exec(context.Context, *ExecProcessRequest) (*emptypb.Empty, error)
ResizePty(context.Context, *ResizePtyRequest) (*emptypb.Empty, error)
CloseIO(context.Context, *CloseIORequest) (*emptypb.Empty, error)
Update(context.Context, *UpdateTaskRequest) (*emptypb.Empty, error)
Wait(context.Context, *WaitRequest) (*WaitResponse, error)
Stats(context.Context, *StatsRequest) (*StatsResponse, error)
Connect(context.Context, *ConnectRequest) (*ConnectResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedTaskServer()
}
// UnimplementedTaskServer must be embedded to have forward compatible implementations.
type UnimplementedTaskServer struct {
}
func (UnimplementedTaskServer) State(context.Context, *StateRequest) (*StateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method State not implemented")
}
func (UnimplementedTaskServer) Create(context.Context, *CreateTaskRequest) (*CreateTaskResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Create not implemented")
}
func (UnimplementedTaskServer) Start(context.Context, *StartRequest) (*StartResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Start not implemented")
}
func (UnimplementedTaskServer) Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
}
func (UnimplementedTaskServer) Pids(context.Context, *PidsRequest) (*PidsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Pids not implemented")
}
func (UnimplementedTaskServer) Pause(context.Context, *PauseRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Pause not implemented")
}
func (UnimplementedTaskServer) Resume(context.Context, *ResumeRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Resume not implemented")
}
func (UnimplementedTaskServer) Checkpoint(context.Context, *CheckpointTaskRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Checkpoint not implemented")
}
func (UnimplementedTaskServer) Kill(context.Context, *KillRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Kill not implemented")
}
func (UnimplementedTaskServer) Exec(context.Context, *ExecProcessRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Exec not implemented")
}
func (UnimplementedTaskServer) ResizePty(context.Context, *ResizePtyRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ResizePty not implemented")
}
func (UnimplementedTaskServer) CloseIO(context.Context, *CloseIORequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method CloseIO not implemented")
}
func (UnimplementedTaskServer) Update(context.Context, *UpdateTaskRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Update not implemented")
}
func (UnimplementedTaskServer) Wait(context.Context, *WaitRequest) (*WaitResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Wait not implemented")
}
func (UnimplementedTaskServer) Stats(context.Context, *StatsRequest) (*StatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Stats not implemented")
}
func (UnimplementedTaskServer) Connect(context.Context, *ConnectRequest) (*ConnectResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Connect not implemented")
}
func (UnimplementedTaskServer) Shutdown(context.Context, *ShutdownRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Shutdown not implemented")
}
func (UnimplementedTaskServer) mustEmbedUnimplementedTaskServer() {}
// UnsafeTaskServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to TaskServer will
// result in compilation errors.
type UnsafeTaskServer interface {
mustEmbedUnimplementedTaskServer()
}
func RegisterTaskServer(s grpc.ServiceRegistrar, srv TaskServer) {
s.RegisterService(&Task_ServiceDesc, srv)
}
func _Task_State_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).State(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/State",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).State(ctx, req.(*StateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateTaskRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Create(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Create",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Create(ctx, req.(*CreateTaskRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Start(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Start",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Start(ctx, req.(*StartRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Delete(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Delete",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Delete(ctx, req.(*DeleteRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Pids_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PidsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Pids(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Pids",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Pids(ctx, req.(*PidsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Pause_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PauseRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Pause(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Pause",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Pause(ctx, req.(*PauseRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Resume_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResumeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Resume(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Resume",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Resume(ctx, req.(*ResumeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Checkpoint_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CheckpointTaskRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Checkpoint(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Checkpoint",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Checkpoint(ctx, req.(*CheckpointTaskRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Kill_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(KillRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Kill(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Kill",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Kill(ctx, req.(*KillRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Exec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExecProcessRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Exec(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Exec",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Exec(ctx, req.(*ExecProcessRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_ResizePty_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResizePtyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).ResizePty(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/ResizePty",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).ResizePty(ctx, req.(*ResizePtyRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_CloseIO_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CloseIORequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).CloseIO(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/CloseIO",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).CloseIO(ctx, req.(*CloseIORequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateTaskRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Update(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Update",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Update(ctx, req.(*UpdateTaskRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Wait_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(WaitRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Wait(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Wait",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Wait(ctx, req.(*WaitRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Stats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Stats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Stats",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Stats(ctx, req.(*StatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Connect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ConnectRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Connect(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Connect",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Connect(ctx, req.(*ConnectRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Task_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ShutdownRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TaskServer).Shutdown(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.task.v3.Task/Shutdown",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TaskServer).Shutdown(ctx, req.(*ShutdownRequest))
}
return interceptor(ctx, in, info, handler)
}
// Task_ServiceDesc is the grpc.ServiceDesc for Task service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Task_ServiceDesc = grpc.ServiceDesc{
ServiceName: "containerd.task.v3.Task",
HandlerType: (*TaskServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "State",
Handler: _Task_State_Handler,
},
{
MethodName: "Create",
Handler: _Task_Create_Handler,
},
{
MethodName: "Start",
Handler: _Task_Start_Handler,
},
{
MethodName: "Delete",
Handler: _Task_Delete_Handler,
},
{
MethodName: "Pids",
Handler: _Task_Pids_Handler,
},
{
MethodName: "Pause",
Handler: _Task_Pause_Handler,
},
{
MethodName: "Resume",
Handler: _Task_Resume_Handler,
},
{
MethodName: "Checkpoint",
Handler: _Task_Checkpoint_Handler,
},
{
MethodName: "Kill",
Handler: _Task_Kill_Handler,
},
{
MethodName: "Exec",
Handler: _Task_Exec_Handler,
},
{
MethodName: "ResizePty",
Handler: _Task_ResizePty_Handler,
},
{
MethodName: "CloseIO",
Handler: _Task_CloseIO_Handler,
},
{
MethodName: "Update",
Handler: _Task_Update_Handler,
},
{
MethodName: "Wait",
Handler: _Task_Wait_Handler,
},
{
MethodName: "Stats",
Handler: _Task_Stats_Handler,
},
{
MethodName: "Connect",
Handler: _Task_Connect_Handler,
},
{
MethodName: "Shutdown",
Handler: _Task_Shutdown_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "runtime/task/v3/shim.proto",
}

View File

@@ -0,0 +1,301 @@
// Code generated by protoc-gen-go-ttrpc. DO NOT EDIT.
// source: runtime/task/v3/shim.proto
package task
import (
context "context"
ttrpc "github.com/containerd/ttrpc"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
type TTRPCTaskService interface {
State(context.Context, *StateRequest) (*StateResponse, error)
Create(context.Context, *CreateTaskRequest) (*CreateTaskResponse, error)
Start(context.Context, *StartRequest) (*StartResponse, error)
Delete(context.Context, *DeleteRequest) (*DeleteResponse, error)
Pids(context.Context, *PidsRequest) (*PidsResponse, error)
Pause(context.Context, *PauseRequest) (*emptypb.Empty, error)
Resume(context.Context, *ResumeRequest) (*emptypb.Empty, error)
Checkpoint(context.Context, *CheckpointTaskRequest) (*emptypb.Empty, error)
Kill(context.Context, *KillRequest) (*emptypb.Empty, error)
Exec(context.Context, *ExecProcessRequest) (*emptypb.Empty, error)
ResizePty(context.Context, *ResizePtyRequest) (*emptypb.Empty, error)
CloseIO(context.Context, *CloseIORequest) (*emptypb.Empty, error)
Update(context.Context, *UpdateTaskRequest) (*emptypb.Empty, error)
Wait(context.Context, *WaitRequest) (*WaitResponse, error)
Stats(context.Context, *StatsRequest) (*StatsResponse, error)
Connect(context.Context, *ConnectRequest) (*ConnectResponse, error)
Shutdown(context.Context, *ShutdownRequest) (*emptypb.Empty, error)
}
func RegisterTTRPCTaskService(srv *ttrpc.Server, svc TTRPCTaskService) {
srv.RegisterService("containerd.task.v3.Task", &ttrpc.ServiceDesc{
Methods: map[string]ttrpc.Method{
"State": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req StateRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.State(ctx, &req)
},
"Create": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req CreateTaskRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Create(ctx, &req)
},
"Start": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req StartRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Start(ctx, &req)
},
"Delete": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req DeleteRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Delete(ctx, &req)
},
"Pids": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req PidsRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Pids(ctx, &req)
},
"Pause": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req PauseRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Pause(ctx, &req)
},
"Resume": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ResumeRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Resume(ctx, &req)
},
"Checkpoint": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req CheckpointTaskRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Checkpoint(ctx, &req)
},
"Kill": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req KillRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Kill(ctx, &req)
},
"Exec": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ExecProcessRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Exec(ctx, &req)
},
"ResizePty": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ResizePtyRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.ResizePty(ctx, &req)
},
"CloseIO": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req CloseIORequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.CloseIO(ctx, &req)
},
"Update": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req UpdateTaskRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Update(ctx, &req)
},
"Wait": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req WaitRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Wait(ctx, &req)
},
"Stats": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req StatsRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Stats(ctx, &req)
},
"Connect": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ConnectRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Connect(ctx, &req)
},
"Shutdown": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ShutdownRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Shutdown(ctx, &req)
},
},
})
}
type ttrpctaskClient struct {
client *ttrpc.Client
}
func NewTTRPCTaskClient(client *ttrpc.Client) TTRPCTaskService {
return &ttrpctaskClient{
client: client,
}
}
func (c *ttrpctaskClient) State(ctx context.Context, req *StateRequest) (*StateResponse, error) {
var resp StateResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "State", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Create(ctx context.Context, req *CreateTaskRequest) (*CreateTaskResponse, error) {
var resp CreateTaskResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Create", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Start(ctx context.Context, req *StartRequest) (*StartResponse, error) {
var resp StartResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Start", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Delete(ctx context.Context, req *DeleteRequest) (*DeleteResponse, error) {
var resp DeleteResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Delete", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Pids(ctx context.Context, req *PidsRequest) (*PidsResponse, error) {
var resp PidsResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Pids", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Pause(ctx context.Context, req *PauseRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Pause", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Resume(ctx context.Context, req *ResumeRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Resume", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Checkpoint(ctx context.Context, req *CheckpointTaskRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Checkpoint", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Kill(ctx context.Context, req *KillRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Kill", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Exec(ctx context.Context, req *ExecProcessRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Exec", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) ResizePty(ctx context.Context, req *ResizePtyRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "ResizePty", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) CloseIO(ctx context.Context, req *CloseIORequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "CloseIO", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Update(ctx context.Context, req *UpdateTaskRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Update", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Wait(ctx context.Context, req *WaitRequest) (*WaitResponse, error) {
var resp WaitResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Wait", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Stats(ctx context.Context, req *StatsRequest) (*StatsResponse, error) {
var resp StatsResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Stats", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Connect(ctx context.Context, req *ConnectRequest) (*ConnectResponse, error) {
var resp ConnectResponse
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Connect", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *ttrpctaskClient) Shutdown(ctx context.Context, req *ShutdownRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.task.v3.Task", "Shutdown", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}

View File

@@ -0,0 +1,23 @@
/*
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 defines the ttrpc event service.
package events
import types "github.com/containerd/containerd/api/types"
// Deprecated: Use [types.Envelope].
type Envelope = types.Envelope

View File

@@ -0,0 +1,182 @@
//
//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.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc (unknown)
// source: services/ttrpc/events/v1/events.proto
package events
import (
types "github.com/containerd/containerd/api/types"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ForwardRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Envelope *types.Envelope `protobuf:"bytes,1,opt,name=envelope,proto3" json:"envelope,omitempty"`
}
func (x *ForwardRequest) Reset() {
*x = ForwardRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_services_ttrpc_events_v1_events_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ForwardRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ForwardRequest) ProtoMessage() {}
func (x *ForwardRequest) ProtoReflect() protoreflect.Message {
mi := &file_services_ttrpc_events_v1_events_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ForwardRequest.ProtoReflect.Descriptor instead.
func (*ForwardRequest) Descriptor() ([]byte, []int) {
return file_services_ttrpc_events_v1_events_proto_rawDescGZIP(), []int{0}
}
func (x *ForwardRequest) GetEnvelope() *types.Envelope {
if x != nil {
return x.Envelope
}
return nil
}
var File_services_ttrpc_events_v1_events_proto protoreflect.FileDescriptor
var file_services_ttrpc_events_v1_events_proto_rawDesc = []byte{
0x0a, 0x25, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x74, 0x72, 0x70, 0x63,
0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74,
0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x23, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x73, 0x2e, 0x74, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d,
0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x11, 0x74, 0x79, 0x70, 0x65, 0x73,
0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x48, 0x0a, 0x0e,
0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x36,
0x0a, 0x08, 0x65, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x74, 0x79,
0x70, 0x65, 0x73, 0x2e, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x52, 0x08, 0x65, 0x6e,
0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x32, 0x60, 0x0a, 0x06, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
0x12, 0x56, 0x0a, 0x07, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x73, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x74, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x76,
0x31, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x74, 0x72, 0x70, 0x63, 0x2f,
0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_services_ttrpc_events_v1_events_proto_rawDescOnce sync.Once
file_services_ttrpc_events_v1_events_proto_rawDescData = file_services_ttrpc_events_v1_events_proto_rawDesc
)
func file_services_ttrpc_events_v1_events_proto_rawDescGZIP() []byte {
file_services_ttrpc_events_v1_events_proto_rawDescOnce.Do(func() {
file_services_ttrpc_events_v1_events_proto_rawDescData = protoimpl.X.CompressGZIP(file_services_ttrpc_events_v1_events_proto_rawDescData)
})
return file_services_ttrpc_events_v1_events_proto_rawDescData
}
var file_services_ttrpc_events_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_services_ttrpc_events_v1_events_proto_goTypes = []interface{}{
(*ForwardRequest)(nil), // 0: containerd.services.events.ttrpc.v1.ForwardRequest
(*types.Envelope)(nil), // 1: containerd.types.Envelope
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
}
var file_services_ttrpc_events_v1_events_proto_depIdxs = []int32{
1, // 0: containerd.services.events.ttrpc.v1.ForwardRequest.envelope:type_name -> containerd.types.Envelope
0, // 1: containerd.services.events.ttrpc.v1.Events.Forward:input_type -> containerd.services.events.ttrpc.v1.ForwardRequest
2, // 2: containerd.services.events.ttrpc.v1.Events.Forward:output_type -> google.protobuf.Empty
2, // [2:3] is the sub-list for method output_type
1, // [1:2] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_services_ttrpc_events_v1_events_proto_init() }
func file_services_ttrpc_events_v1_events_proto_init() {
if File_services_ttrpc_events_v1_events_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_services_ttrpc_events_v1_events_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ForwardRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_services_ttrpc_events_v1_events_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_services_ttrpc_events_v1_events_proto_goTypes,
DependencyIndexes: file_services_ttrpc_events_v1_events_proto_depIdxs,
MessageInfos: file_services_ttrpc_events_v1_events_proto_msgTypes,
}.Build()
File_services_ttrpc_events_v1_events_proto = out.File
file_services_ttrpc_events_v1_events_proto_rawDesc = nil
file_services_ttrpc_events_v1_events_proto_goTypes = nil
file_services_ttrpc_events_v1_events_proto_depIdxs = nil
}

View File

@@ -0,0 +1,37 @@
/*
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.
*/
syntax = "proto3";
package containerd.services.events.ttrpc.v1;
import "google/protobuf/empty.proto";
import "types/event.proto";
option go_package = "github.com/containerd/containerd/api/services/ttrpc/events/v1;events";
service Events {
// Forward sends an event that has already been packaged into an envelope
// with a timestamp and namespace.
//
// This is useful if earlier timestamping is required or when forwarding on
// behalf of another component, namespace or publisher.
rpc Forward(ForwardRequest) returns (google.protobuf.Empty);
}
message ForwardRequest {
containerd.types.Envelope envelope = 1;
}

View File

@@ -0,0 +1,45 @@
// Code generated by protoc-gen-go-ttrpc. DO NOT EDIT.
// source: services/ttrpc/events/v1/events.proto
package events
import (
context "context"
ttrpc "github.com/containerd/ttrpc"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
type TTRPCEventsService interface {
Forward(context.Context, *ForwardRequest) (*emptypb.Empty, error)
}
func RegisterTTRPCEventsService(srv *ttrpc.Server, svc TTRPCEventsService) {
srv.RegisterService("containerd.services.events.ttrpc.v1.Events", &ttrpc.ServiceDesc{
Methods: map[string]ttrpc.Method{
"Forward": func(ctx context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req ForwardRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
return svc.Forward(ctx, &req)
},
},
})
}
type ttrpceventsClient struct {
client *ttrpc.Client
}
func NewTTRPCEventsClient(client *ttrpc.Client) TTRPCEventsService {
return &ttrpceventsClient{
client: client,
}
}
func (c *ttrpceventsClient) Forward(ctx context.Context, req *ForwardRequest) (*emptypb.Empty, error) {
var resp emptypb.Empty
if err := c.client.Call(ctx, "containerd.services.events.ttrpc.v1.Events", "Forward", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}

View File

@@ -0,0 +1,210 @@
/*
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 apply
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"time"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/diff"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/errdefs"
"github.com/containerd/log"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// NewFileSystemApplier returns an applier which simply mounts
// and applies diff onto the mounted filesystem.
func NewFileSystemApplier(cs content.Provider) diff.Applier {
return NewFileSystemApplierWithMountManager(cs, nil)
}
// NewFileSystemApplierWithMountManager returns an applier which simply mounts and
// applies diff onto the mounted filesystem.
// An optional mount manager can be specified and it will take effect when applying.
func NewFileSystemApplierWithMountManager(cs content.Provider, mm mount.Manager) diff.Applier {
return &fsApplier{
store: cs,
mount: mm,
}
}
type fsApplier struct {
store content.Provider
mount mount.Manager
}
var emptyDesc = ocispec.Descriptor{}
// Apply applies the content associated with the provided digests onto the
// provided mounts. Archive content will be extracted and decompressed if
// necessary.
func (s *fsApplier) Apply(ctx context.Context, desc ocispec.Descriptor, mounts []mount.Mount, opts ...diff.ApplyOpt) (d ocispec.Descriptor, err error) {
t1 := time.Now()
defer func() {
if err == nil {
log.G(ctx).WithFields(log.Fields{
"d": time.Since(t1),
"digest": desc.Digest,
"size": desc.Size,
"media": desc.MediaType,
}).Debugf("diff applied")
}
}()
var config diff.ApplyConfig
for _, o := range opts {
if err := o(ctx, desc, &config); err != nil {
return emptyDesc, fmt.Errorf("failed to apply config opt: %w", err)
}
}
ra, err := s.store.ReaderAt(ctx, desc)
if err != nil {
return emptyDesc, fmt.Errorf("failed to get reader from content store: %w", err)
}
var r io.ReadCloser
if config.Progress != nil {
r = newProgressReader(ra, config.Progress)
} else {
r = newReadCloser(ra)
}
defer r.Close()
var processors []diff.StreamProcessor
processor := diff.NewProcessorChain(desc.MediaType, r)
processors = append(processors, processor)
for {
if processor, err = diff.GetProcessor(ctx, processor, config.ProcessorPayloads); err != nil {
return emptyDesc, fmt.Errorf("failed to get stream processor for %s: %w", desc.MediaType, err)
}
processors = append(processors, processor)
if processor.MediaType() == ocispec.MediaTypeImageLayer {
break
}
}
defer processor.Close()
digester := digest.Canonical.Digester()
rc := &readCounter{
r: io.TeeReader(processor, digester.Hash()),
}
// The number of `mounts` that need to be parsed by the mount manager
// will be more than 1 in reality; this is needed to work around some
// overlayfs/bind shortcuts in core/diff/apply/apply_linux.go
if s.mount != nil && len(mounts) > 1 {
var b [3]byte
// Ignore read failures, just decreases uniqueness
rand.Read(b[:])
id := fmt.Sprintf("fs-diffapply-%d-%s", t1.Nanosecond(), base64.URLEncoding.EncodeToString(b[:]))
info, err := s.mount.Activate(ctx, id, mounts)
if err == nil {
defer s.mount.Deactivate(ctx, id)
mounts = info.System
} else if !errdefs.IsNotImplemented(err) {
return emptyDesc, fmt.Errorf("failed to activate mounts: %w", err)
}
}
if err := apply(ctx, mounts, rc, config.SyncFs); err != nil {
return emptyDesc, err
}
// Read any trailing data
if _, err := io.Copy(io.Discard, rc); err != nil {
return emptyDesc, err
}
for _, p := range processors {
if ep, ok := p.(interface {
Err() error
}); ok {
if err := ep.Err(); err != nil {
return emptyDesc, err
}
}
}
return ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Size: rc.c,
Digest: digester.Digest(),
}, nil
}
type readCounter struct {
r io.Reader
c int64
}
func (rc *readCounter) Read(p []byte) (n int, err error) {
n, err = rc.r.Read(p)
if n > 0 {
rc.c += int64(n)
}
return
}
type progressReader struct {
rc *readCounter
c io.Closer
p func(int64)
}
func newProgressReader(ra content.ReaderAt, p func(int64)) io.ReadCloser {
return &progressReader{
rc: &readCounter{
r: content.NewReader(ra),
c: 0,
},
c: ra,
p: p,
}
}
func (pr *progressReader) Read(p []byte) (n int, err error) {
// Call the progress function with the current count, indicating
// the previously read content has been processed. Initial
// progress of 0 indicates start of processing.
pr.p(pr.rc.c)
n, err = pr.rc.Read(p)
return
}
func (pr *progressReader) Close() error {
pr.p(pr.rc.c)
return pr.c.Close()
}
type readCloser struct {
io.Reader
io.Closer
}
func newReadCloser(ra content.ReaderAt) io.ReadCloser {
return &readCloser{
Reader: content.NewReader(ra),
Closer: ra,
}
}

View File

@@ -0,0 +1,105 @@
/*
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 apply
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/moby/sys/userns"
"golang.org/x/sys/unix"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/pkg/archive"
"github.com/containerd/errdefs"
)
func apply(ctx context.Context, mounts []mount.Mount, r io.Reader, sync bool) (retErr error) {
switch {
case len(mounts) == 1 && mounts[0].Type == "overlay":
// OverlayConvertWhiteout (mknod c 0 0) doesn't work in userns.
// https://github.com/containerd/containerd/issues/3762
if userns.RunningInUserNS() {
break
}
path, parents, err := getOverlayPath(mounts[0].Options)
if err != nil {
if errdefs.IsInvalidArgument(err) {
break
}
return err
}
opts := []archive.ApplyOpt{
archive.WithConvertWhiteout(archive.OverlayConvertWhiteout),
}
if len(parents) > 0 {
opts = append(opts, archive.WithParents(parents))
}
_, err = archive.Apply(ctx, path, r, opts...)
if err == nil && sync {
err = doSyncFs(path)
}
return err
case sync && len(mounts) == 1 && mounts[0].Type == "bind":
defer func() {
if retErr != nil {
return
}
retErr = doSyncFs(mounts[0].Source)
}()
}
return mount.WithTempMount(ctx, mounts, func(root string) error {
_, err := archive.Apply(ctx, root, r)
return err
})
}
func getOverlayPath(options []string) (upper string, lower []string, err error) {
const upperdirPrefix = "upperdir="
const lowerdirPrefix = "lowerdir="
for _, o := range options {
if after, ok := strings.CutPrefix(o, upperdirPrefix); ok {
upper = after
} else if after, ok := strings.CutPrefix(o, lowerdirPrefix); ok {
lower = strings.Split(after, ":")
}
}
if upper == "" {
return "", nil, fmt.Errorf("upperdir not found: %w", errdefs.ErrInvalidArgument)
}
return
}
func doSyncFs(file string) error {
fd, err := os.Open(file)
if err != nil {
return fmt.Errorf("failed to open %s: %w", file, err)
}
defer fd.Close()
err = unix.Syncfs(int(fd.Fd()))
if err != nil {
return fmt.Errorf("failed to syncfs for %s: %w", file, err)
}
return nil
}

View File

@@ -0,0 +1,51 @@
//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 apply
import (
"context"
"io"
"os"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/pkg/archive"
)
func apply(ctx context.Context, mounts []mount.Mount, r io.Reader, _sync bool) error {
// TODO: for windows, how to sync?
if !mount.HasBindMounts && len(mounts) == 1 && mounts[0].Type == "bind" {
opts := []archive.ApplyOpt{}
if os.Getuid() != 0 {
opts = append(opts, archive.WithNoSameOwner())
}
path := mounts[0].Source
_, err := archive.Apply(ctx, path, r, opts...)
return err
// TODO: Do we need to sync all the filesystems?
}
return mount.WithTempMount(ctx, mounts, func(root string) error {
_, err := archive.Apply(ctx, root, r)
return err
})
}

View File

@@ -0,0 +1,248 @@
/*
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 exchange
import (
"context"
"fmt"
"strings"
"time"
"github.com/containerd/containerd/v2/core/events"
"github.com/containerd/containerd/v2/pkg/filters"
"github.com/containerd/containerd/v2/pkg/identifiers"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/containerd/typeurl/v2"
goevents "github.com/docker/go-events"
)
// Exchange broadcasts events
type Exchange struct {
broadcaster *goevents.Broadcaster
}
// NewExchange returns a new event Exchange
func NewExchange() *Exchange {
return &Exchange{
broadcaster: goevents.NewBroadcaster(),
}
}
var _ events.Publisher = &Exchange{}
var _ events.Forwarder = &Exchange{}
var _ events.Subscriber = &Exchange{}
// Forward accepts an envelope to be directly distributed on the exchange.
//
// This is useful when an event is forwarded on behalf of another namespace or
// when the event is propagated on behalf of another publisher.
func (e *Exchange) Forward(ctx context.Context, envelope *events.Envelope) (err error) {
if err := validateEnvelope(envelope); err != nil {
return err
}
defer func() {
logger := log.G(ctx).WithFields(log.Fields{
"topic": envelope.Topic,
"ns": envelope.Namespace,
"type": envelope.Event.GetTypeUrl(),
})
if err != nil {
logger.WithError(err).Error("error forwarding event")
} else {
logger.Trace("event forwarded")
}
}()
return e.broadcaster.Write(envelope)
}
// Publish packages and sends an event. The caller will be considered the
// initial publisher of the event. This means the timestamp will be calculated
// at this point and this method may read from the calling context.
func (e *Exchange) Publish(ctx context.Context, topic string, event events.Event) (err error) {
var (
namespace string
envelope events.Envelope
)
namespace, err = namespaces.NamespaceRequired(ctx)
if err != nil {
return fmt.Errorf("failed publishing event: %w", err)
}
if err := validateTopic(topic); err != nil {
return fmt.Errorf("envelope topic %q: %w", topic, err)
}
encoded, err := typeurl.MarshalAny(event)
if err != nil {
return err
}
envelope.Timestamp = time.Now().UTC()
envelope.Namespace = namespace
envelope.Topic = topic
envelope.Event = encoded
defer func() {
logger := log.G(ctx).WithFields(log.Fields{
"topic": envelope.Topic,
"ns": envelope.Namespace,
"type": envelope.Event.GetTypeUrl(),
})
if err != nil {
logger.WithError(err).Error("error publishing event")
} else {
logger.Trace("event published")
}
}()
return e.broadcaster.Write(&envelope)
}
// Subscribe to events on the exchange. Events are sent through the returned
// channel ch. If an error is encountered, it will be sent on channel errs and
// errs will be closed. To end the subscription, cancel the provided context.
//
// Zero or more filters may be provided as strings. Only events that match
// *any* of the provided filters will be sent on the channel. The filters use
// the standard containerd filters package syntax.
func (e *Exchange) Subscribe(ctx context.Context, fs ...string) (ch <-chan *events.Envelope, errs <-chan error) {
var (
evch = make(chan *events.Envelope)
errq = make(chan error, 1)
channel = goevents.NewChannel(0)
queue = goevents.NewQueue(channel)
dst goevents.Sink = queue
)
closeAll := func() {
channel.Close()
queue.Close()
e.broadcaster.Remove(dst)
close(errq)
}
ch = evch
errs = errq
if len(fs) > 0 {
filter, err := filters.ParseAll(fs...)
if err != nil {
errq <- fmt.Errorf("failed parsing subscription filters: %w", err)
closeAll()
return
}
dst = goevents.NewFilter(queue, goevents.MatcherFunc(func(gev goevents.Event) bool {
return filter.Match(adapt(gev))
}))
}
e.broadcaster.Add(dst)
go func() {
defer closeAll()
var err error
loop:
for {
select {
case ev := <-channel.C:
env, ok := ev.(*events.Envelope)
if !ok {
// TODO(stevvooe): For the most part, we are well protected
// from this condition. Both Forward and Publish protect
// from this.
err = fmt.Errorf("invalid envelope encountered %#v; please file a bug", ev)
break
}
select {
case evch <- env:
case <-ctx.Done():
break loop
}
case <-ctx.Done():
break loop
}
}
if err == nil {
if cerr := ctx.Err(); cerr != context.Canceled {
err = cerr
}
}
errq <- err
}()
return
}
func validateTopic(topic string) error {
if topic == "" {
return fmt.Errorf("must not be empty: %w", errdefs.ErrInvalidArgument)
}
if topic[0] != '/' {
return fmt.Errorf("must start with '/': %w", errdefs.ErrInvalidArgument)
}
if len(topic) == 1 {
return fmt.Errorf("must have at least one component: %w", errdefs.ErrInvalidArgument)
}
components := strings.SplitSeq(topic[1:], "/")
for component := range components {
if err := identifiers.Validate(component); err != nil {
return fmt.Errorf("failed validation on component %q: %w", component, err)
}
}
return nil
}
func validateEnvelope(envelope *events.Envelope) error {
if err := identifiers.Validate(envelope.Namespace); err != nil {
return fmt.Errorf("event envelope has invalid namespace: %w", err)
}
if err := validateTopic(envelope.Topic); err != nil {
return fmt.Errorf("envelope topic %q: %w", envelope.Topic, err)
}
if envelope.Timestamp.IsZero() {
return fmt.Errorf("timestamp must be set on forwarded event: %w", errdefs.ErrInvalidArgument)
}
return nil
}
func adapt(ev any) filters.Adaptor {
if adaptor, ok := ev.(filters.Adaptor); ok {
return adaptor
}
return filters.AdapterFunc(func(fieldpath []string) (string, bool) {
return "", false
})
}

View File

@@ -0,0 +1,98 @@
//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 cgroups
import (
"context"
"github.com/containerd/cgroups/v3"
"github.com/containerd/containerd/v2/core/events"
v1 "github.com/containerd/containerd/v2/core/metrics/cgroups/v1"
v2 "github.com/containerd/containerd/v2/core/metrics/cgroups/v2"
"github.com/containerd/containerd/v2/core/runtime"
"github.com/containerd/containerd/v2/plugins"
"github.com/containerd/containerd/v2/version"
"github.com/containerd/platforms"
"github.com/containerd/plugin"
"github.com/containerd/plugin/registry"
metrics "github.com/docker/go-metrics"
)
// Config for the cgroups monitor
type Config struct {
NoPrometheus bool `toml:"no_prometheus"`
}
func init() {
registry.Register(&plugin.Registration{
Type: plugins.TaskMonitorPlugin,
ID: "cgroups",
InitFn: New,
Requires: []plugin.Type{
plugins.EventPlugin,
},
Config: &Config{},
ConfigMigration: func(ctx context.Context, configVersion int, pluginConfigs map[string]any) error {
if configVersion >= version.ConfigVersion {
return nil
}
// Previous plugin name
const pluginName = "io.containerd.monitor.v1.cgroups"
c, ok := pluginConfigs[pluginName]
if ok {
pluginConfigs[string(plugins.TaskMonitorPlugin)+".cgroups"] = c
delete(pluginConfigs, pluginName)
}
return nil
},
})
}
// New returns a new cgroups monitor
func New(ic *plugin.InitContext) (any, error) {
var ns *metrics.Namespace
config := ic.Config.(*Config)
if !config.NoPrometheus {
ns = metrics.NewNamespace("container", "", nil)
}
var (
tm runtime.TaskMonitor
err error
)
ep, err := ic.GetSingle(plugins.EventPlugin)
if err != nil {
return nil, err
}
if cgroups.Mode() == cgroups.Unified {
tm, err = v2.NewTaskMonitor(ic.Context, ep.(events.Publisher), ns)
} else {
tm, err = v1.NewTaskMonitor(ic.Context, ep.(events.Publisher), ns)
}
if err != nil {
return nil, err
}
if ns != nil {
metrics.Register(ns)
}
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
return tm, nil
}

View File

@@ -0,0 +1,32 @@
//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 common
import (
"context"
"github.com/containerd/containerd/v2/pkg/protobuf/types"
)
// Statable type that returns cgroup metrics
type Statable interface {
ID() string
Namespace() string
Stats(context.Context) (*types.Any, error)
}

View File

@@ -0,0 +1,132 @@
//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 v1
import (
"strconv"
v1 "github.com/containerd/containerd/v2/core/metrics/types/v1"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
var blkioMetrics = []*metric{
{
name: "blkio_io_merged_recursive",
help: "The blkio io merged recursive",
unit: metrics.Total,
vt: prometheus.GaugeValue,
labels: []string{"op", "device", "major", "minor"},
getValues: func(stats *v1.Metrics) []value {
if stats.Blkio == nil {
return nil
}
return blkioValues(stats.Blkio.IoMergedRecursive)
},
},
{
name: "blkio_io_queued_recursive",
help: "The blkio io queued recursive",
unit: metrics.Total,
vt: prometheus.GaugeValue,
labels: []string{"op", "device", "major", "minor"},
getValues: func(stats *v1.Metrics) []value {
if stats.Blkio == nil {
return nil
}
return blkioValues(stats.Blkio.IoQueuedRecursive)
},
},
{
name: "blkio_io_service_bytes_recursive",
help: "The blkio io service bytes recursive",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
labels: []string{"op", "device", "major", "minor"},
getValues: func(stats *v1.Metrics) []value {
if stats.Blkio == nil {
return nil
}
return blkioValues(stats.Blkio.IoServiceBytesRecursive)
},
},
{
name: "blkio_io_service_time_recursive",
help: "The blkio io service time recursive",
unit: metrics.Total,
vt: prometheus.GaugeValue,
labels: []string{"op", "device", "major", "minor"},
getValues: func(stats *v1.Metrics) []value {
if stats.Blkio == nil {
return nil
}
return blkioValues(stats.Blkio.IoServiceTimeRecursive)
},
},
{
name: "blkio_io_serviced_recursive",
help: "The blkio io serviced recursive",
unit: metrics.Total,
vt: prometheus.GaugeValue,
labels: []string{"op", "device", "major", "minor"},
getValues: func(stats *v1.Metrics) []value {
if stats.Blkio == nil {
return nil
}
return blkioValues(stats.Blkio.IoServicedRecursive)
},
},
{
name: "blkio_io_time_recursive",
help: "The blkio io time recursive",
unit: metrics.Total,
vt: prometheus.GaugeValue,
labels: []string{"op", "device", "major", "minor"},
getValues: func(stats *v1.Metrics) []value {
if stats.Blkio == nil {
return nil
}
return blkioValues(stats.Blkio.IoTimeRecursive)
},
},
{
name: "blkio_sectors_recursive",
help: "The blkio sectors recursive",
unit: metrics.Total,
vt: prometheus.GaugeValue,
labels: []string{"op", "device", "major", "minor"},
getValues: func(stats *v1.Metrics) []value {
if stats.Blkio == nil {
return nil
}
return blkioValues(stats.Blkio.SectorsRecursive)
},
},
}
func blkioValues(l []*v1.BlkIOEntry) []value {
var out []value
for _, e := range l {
out = append(out, value{
v: float64(e.Value),
l: []string{e.Op, e.Device, strconv.FormatUint(e.Major, 10), strconv.FormatUint(e.Minor, 10)},
})
}
return out
}

View File

@@ -0,0 +1,95 @@
//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 v1
import (
"context"
cgroups "github.com/containerd/cgroups/v3/cgroup1"
eventstypes "github.com/containerd/containerd/api/events"
"github.com/containerd/containerd/v2/core/events"
"github.com/containerd/containerd/v2/core/runtime"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/docker/go-metrics"
)
// NewTaskMonitor returns a new cgroups monitor
func NewTaskMonitor(ctx context.Context, publisher events.Publisher, ns *metrics.Namespace) (runtime.TaskMonitor, error) {
collector := NewCollector(ns)
oom, err := newOOMCollector(ns)
if err != nil {
return nil, err
}
return &cgroupsMonitor{
collector: collector,
oom: oom,
context: ctx,
publisher: publisher,
}, nil
}
type cgroupsMonitor struct {
collector *Collector
oom *oomCollector
context context.Context
publisher events.Publisher
}
type cgroupTask interface {
Cgroup() (cgroups.Cgroup, error)
}
func (m *cgroupsMonitor) Monitor(c runtime.Task, labels map[string]string) error {
if err := m.collector.Add(c, labels); err != nil {
return err
}
t, ok := c.(cgroupTask)
if !ok {
return nil
}
cg, err := t.Cgroup()
if err != nil {
if errdefs.IsNotFound(err) {
return nil
}
return err
}
err = m.oom.Add(c.ID(), c.Namespace(), cg, m.trigger)
if err == cgroups.ErrMemoryNotSupported {
log.L.WithError(err).Warn("OOM monitoring failed")
return nil
}
return err
}
func (m *cgroupsMonitor) Stop(c runtime.Task) error {
m.collector.Remove(c)
return nil
}
func (m *cgroupsMonitor) trigger(id, namespace string, cg cgroups.Cgroup) {
ctx := namespaces.WithNamespace(m.context, namespace)
if err := m.publisher.Publish(ctx, runtime.TaskOOMEventTopic, &eventstypes.TaskOOM{
ContainerID: id,
}); err != nil {
log.G(m.context).WithError(err).Error("post OOM event")
}
}

View File

@@ -0,0 +1,146 @@
//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 v1
import (
"strconv"
v1 "github.com/containerd/containerd/v2/core/metrics/types/v1"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
var cpuMetrics = []*metric{
{
name: "cpu_total",
help: "The total cpu time",
unit: metrics.Nanoseconds,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.Usage.Total),
},
}
},
},
{
name: "cpu_kernel",
help: "The total kernel cpu time",
unit: metrics.Nanoseconds,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.Usage.Kernel),
},
}
},
},
{
name: "cpu_user",
help: "The total user cpu time",
unit: metrics.Nanoseconds,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.Usage.User),
},
}
},
},
{
name: "per_cpu",
help: "The total cpu time per cpu",
unit: metrics.Nanoseconds,
vt: prometheus.GaugeValue,
labels: []string{"cpu"},
getValues: func(stats *v1.Metrics) []value {
if stats.CPU == nil {
return nil
}
var out []value
for i, v := range stats.CPU.Usage.PerCPU {
out = append(out, value{
v: float64(v),
l: []string{strconv.Itoa(i)},
})
}
return out
},
},
{
name: "cpu_throttle_periods",
help: "The total cpu throttle periods",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.Throttling.Periods),
},
}
},
},
{
name: "cpu_throttled_periods",
help: "The total cpu throttled periods",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.Throttling.ThrottledPeriods),
},
}
},
},
{
name: "cpu_throttled_time",
help: "The total cpu throttled time",
unit: metrics.Nanoseconds,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.Throttling.ThrottledTime),
},
}
},
},
}

View File

@@ -0,0 +1,88 @@
//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 v1
import (
v1 "github.com/containerd/containerd/v2/core/metrics/types/v1"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
var hugetlbMetrics = []*metric{
{
name: "hugetlb_usage",
help: "The hugetlb usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
labels: []string{"page"},
getValues: func(stats *v1.Metrics) []value {
if stats.Hugetlb == nil {
return nil
}
var out []value
for _, v := range stats.Hugetlb {
out = append(out, value{
v: float64(v.Usage),
l: []string{v.Pagesize},
})
}
return out
},
},
{
name: "hugetlb_failcnt",
help: "The hugetlb failcnt",
unit: metrics.Total,
vt: prometheus.GaugeValue,
labels: []string{"page"},
getValues: func(stats *v1.Metrics) []value {
if stats.Hugetlb == nil {
return nil
}
var out []value
for _, v := range stats.Hugetlb {
out = append(out, value{
v: float64(v.Failcnt),
l: []string{v.Pagesize},
})
}
return out
},
},
{
name: "hugetlb_max",
help: "The hugetlb maximum usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
labels: []string{"page"},
getValues: func(stats *v1.Metrics) []value {
if stats.Hugetlb == nil {
return nil
}
var out []value
for _, v := range stats.Hugetlb {
out = append(out, value{
v: float64(v.Max),
l: []string{v.Pagesize},
})
}
return out
},
},
}

View File

@@ -0,0 +1,796 @@
//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 v1
import (
v1 "github.com/containerd/containerd/v2/core/metrics/types/v1"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
var memoryMetrics = []*metric{
{
name: "memory_cache",
help: "The cache amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Cache),
},
}
},
},
{
name: "memory_rss",
help: "The rss amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.RSS),
},
}
},
},
{
name: "memory_rss_huge",
help: "The rss_huge amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.RSSHuge),
},
}
},
},
{
name: "memory_mapped_file",
help: "The mapped_file amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.MappedFile),
},
}
},
},
{
name: "memory_dirty",
help: "The dirty amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Dirty),
},
}
},
},
{
name: "memory_writeback",
help: "The writeback amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Writeback),
},
}
},
},
{
name: "memory_pgpgin",
help: "The pgpgin amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.PgPgIn),
},
}
},
},
{
name: "memory_pgpgout",
help: "The pgpgout amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.PgPgOut),
},
}
},
},
{
name: "memory_pgfault",
help: "The pgfault amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.PgFault),
},
}
},
},
{
name: "memory_pgmajfault",
help: "The pgmajfault amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.PgMajFault),
},
}
},
},
{
name: "memory_inactive_anon",
help: "The inactive_anon amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.InactiveAnon),
},
}
},
},
{
name: "memory_active_anon",
help: "The active_anon amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.ActiveAnon),
},
}
},
},
{
name: "memory_inactive_file",
help: "The inactive_file amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.InactiveFile),
},
}
},
},
{
name: "memory_active_file",
help: "The active_file amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.ActiveFile),
},
}
},
},
{
name: "memory_unevictable",
help: "The unevictable amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Unevictable),
},
}
},
},
{
name: "memory_hierarchical_memory_limit",
help: "The hierarchical_memory_limit amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.HierarchicalMemoryLimit),
},
}
},
},
{
name: "memory_hierarchical_memsw_limit",
help: "The hierarchical_memsw_limit amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.HierarchicalSwapLimit),
},
}
},
},
{
name: "memory_total_cache",
help: "The total_cache amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalCache),
},
}
},
},
{
name: "memory_total_rss",
help: "The total_rss amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalRSS),
},
}
},
},
{
name: "memory_total_rss_huge",
help: "The total_rss_huge amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalRSSHuge),
},
}
},
},
{
name: "memory_total_mapped_file",
help: "The total_mapped_file amount used",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalMappedFile),
},
}
},
},
{
name: "memory_total_dirty",
help: "The total_dirty amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalDirty),
},
}
},
},
{
name: "memory_total_writeback",
help: "The total_writeback amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalWriteback),
},
}
},
},
{
name: "memory_total_pgpgin",
help: "The total_pgpgin amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalPgPgIn),
},
}
},
},
{
name: "memory_total_pgpgout",
help: "The total_pgpgout amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalPgPgOut),
},
}
},
},
{
name: "memory_total_pgfault",
help: "The total_pgfault amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalPgFault),
},
}
},
},
{
name: "memory_total_pgmajfault",
help: "The total_pgmajfault amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalPgMajFault),
},
}
},
},
{
name: "memory_total_inactive_anon",
help: "The total_inactive_anon amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalInactiveAnon),
},
}
},
},
{
name: "memory_total_active_anon",
help: "The total_active_anon amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalActiveAnon),
},
}
},
},
{
name: "memory_total_inactive_file",
help: "The total_inactive_file amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalInactiveFile),
},
}
},
},
{
name: "memory_total_active_file",
help: "The total_active_file amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalActiveFile),
},
}
},
},
{
name: "memory_total_unevictable",
help: "The total_unevictable amount",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Memory == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.TotalUnevictable),
},
}
},
},
{
name: "memory_usage_failcnt",
help: "The usage failcnt",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetUsage() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Usage.Failcnt),
},
}
},
},
{
name: "memory_usage_limit",
help: "The memory limit",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetUsage() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Usage.Limit),
},
}
},
},
{
name: "memory_usage_max",
help: "The memory maximum usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetUsage() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Usage.Max),
},
}
},
},
{
name: "memory_usage_usage",
help: "The memory usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetUsage() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Usage.Usage),
},
}
},
},
{
name: "memory_swap_failcnt",
help: "The swap failcnt",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetSwap() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Swap.Failcnt),
},
}
},
},
{
name: "memory_swap_limit",
help: "The swap limit",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetSwap() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Swap.Limit),
},
}
},
},
{
name: "memory_swap_max",
help: "The swap maximum usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetSwap() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Swap.Max),
},
}
},
},
{
name: "memory_swap_usage",
help: "The swap usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetSwap() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Swap.Usage),
},
}
},
},
{
name: "memory_kernel_failcnt",
help: "The kernel failcnt",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernel() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Kernel.Failcnt),
},
}
},
},
{
name: "memory_kernel_limit",
help: "The kernel limit",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernel() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Kernel.Limit),
},
}
},
},
{
name: "memory_kernel_max",
help: "The kernel maximum usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernel() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Kernel.Max),
},
}
},
},
{
name: "memory_kernel_usage",
help: "The kernel usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernel() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.Kernel.Usage),
},
}
},
},
{
name: "memory_kerneltcp_failcnt",
help: "The kerneltcp failcnt",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernelTCP() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.KernelTCP.Failcnt),
},
}
},
},
{
name: "memory_kerneltcp_limit",
help: "The kerneltcp limit",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernelTCP() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.KernelTCP.Limit),
},
}
},
},
{
name: "memory_kerneltcp_max",
help: "The kerneltcp maximum usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernelTCP() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.KernelTCP.Max),
},
}
},
},
{
name: "memory_kerneltcp_usage",
help: "The kerneltcp usage",
unit: metrics.Bytes,
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.GetMemory().GetKernelTCP() == nil {
return nil
}
return []value{
{
v: float64(stats.Memory.KernelTCP.Usage),
},
}
},
},
}

View File

@@ -0,0 +1,64 @@
//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 v1
import (
v1 "github.com/containerd/containerd/v2/core/metrics/types/v1"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
// IDName is the name that is used to identify the id being collected in the metric
var IDName = "container_id"
type value struct {
v float64
l []string
}
type metric struct {
name string
help string
unit metrics.Unit
vt prometheus.ValueType
labels []string
// getValues returns the value and labels for the data
getValues func(stats *v1.Metrics) []value
}
func (m *metric) desc(ns *metrics.Namespace) *prometheus.Desc {
// the namespace label is for containerd namespaces
return ns.NewDesc(m.name, m.help, m.unit, append([]string{IDName, "namespace"}, m.labels...)...)
}
func (m *metric) collect(id, namespace string, stats *v1.Metrics, ns *metrics.Namespace, ch chan<- prometheus.Metric, block bool) {
values := m.getValues(stats)
for _, v := range values {
// block signals to block on the sending the metrics so none are missed
if block {
ch <- prometheus.MustNewConstMetric(m.desc(ns), m.vt, v.v, append([]string{id, namespace}, v.l...)...)
continue
}
// non-blocking metrics can be dropped if the chan is full
select {
case ch <- prometheus.MustNewConstMetric(m.desc(ns), m.vt, v.v, append([]string{id, namespace}, v.l...)...):
default:
}
}
}

View File

@@ -0,0 +1,208 @@
//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 v1
import (
"context"
"fmt"
"sync"
cgroups "github.com/containerd/cgroups/v3/cgroup1"
cmetrics "github.com/containerd/containerd/v2/core/metrics"
"github.com/containerd/containerd/v2/core/metrics/cgroups/common"
v1 "github.com/containerd/containerd/v2/core/metrics/types/v1"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/timeout"
"github.com/containerd/log"
"github.com/containerd/typeurl/v2"
"github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
// Trigger will be called when an event happens and provides the cgroup
// where the event originated from
type Trigger func(string, string, cgroups.Cgroup)
// NewCollector registers the collector with the provided namespace and returns it so
// that cgroups can be added for collection
func NewCollector(ns *metrics.Namespace) *Collector {
if ns == nil {
return &Collector{}
}
// add machine cpus and memory info
c := &Collector{
ns: ns,
tasks: make(map[string]entry),
}
c.metrics = append(c.metrics, pidMetrics...)
c.metrics = append(c.metrics, cpuMetrics...)
c.metrics = append(c.metrics, memoryMetrics...)
c.metrics = append(c.metrics, hugetlbMetrics...)
c.metrics = append(c.metrics, blkioMetrics...)
c.storedMetrics = make(chan prometheus.Metric, 100*len(c.metrics))
ns.Add(c)
return c
}
func taskID(id, namespace string) string {
return fmt.Sprintf("%s-%s", id, namespace)
}
type entry struct {
task common.Statable
// ns is an optional child namespace that contains additional to parent labels.
// This can be used to append task specific labels to be able to differentiate the different containerd metrics.
ns *metrics.Namespace
}
// Collector provides the ability to collect container stats and export
// them in the prometheus format
type Collector struct {
ns *metrics.Namespace
storedMetrics chan prometheus.Metric
// TODO(fuweid):
//
// The Collector.Collect will be the field ns'Collect's callback,
// which be invoked periodically with internal lock. And Collector.Add
// might also invoke ns.Lock if the labels is not nil, which is easy to
// cause dead-lock.
//
// Goroutine X:
//
// ns.Collect
// ns.Lock
// Collector.Collect
// Collector.RLock
//
//
// Goroutine Y:
//
// Collector.Add
// ...(RLock/Lock)
// ns.Lock
//
// I think we should seek the way to decouple ns from Collector.
mu sync.RWMutex
tasks map[string]entry
metrics []*metric
}
// Describe prometheus metrics
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
for _, m := range c.metrics {
ch <- m.desc(c.ns)
}
}
// Collect prometheus metrics
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
c.mu.RLock()
wg := &sync.WaitGroup{}
for _, t := range c.tasks {
wg.Add(1)
go c.collect(t, ch, true, wg)
}
storedLoop:
for {
// read stored metrics until the channel is flushed
select {
case m := <-c.storedMetrics:
ch <- m
default:
break storedLoop
}
}
c.mu.RUnlock()
wg.Wait()
}
func (c *Collector) collect(entry entry, ch chan<- prometheus.Metric, block bool, wg *sync.WaitGroup) {
if wg != nil {
defer wg.Done()
}
t := entry.task
ctx, cancel := timeout.WithContext(context.Background(), cmetrics.ShimStatsRequestTimeout)
stats, err := t.Stats(namespaces.WithNamespace(ctx, t.Namespace()))
cancel()
if err != nil {
log.L.WithError(err).Errorf("stat task %s", t.ID())
return
}
s := &v1.Metrics{}
if err := typeurl.UnmarshalTo(stats, s); err != nil {
log.L.WithError(err).Errorf("unmarshal stats for %s", t.ID())
return
}
ns := entry.ns
if ns == nil {
ns = c.ns
}
for _, m := range c.metrics {
m.collect(t.ID(), t.Namespace(), s, ns, ch, block)
}
}
// Add adds the provided cgroup and id so that metrics are collected and exported
func (c *Collector) Add(t common.Statable, labels map[string]string) error {
if c.ns == nil {
return nil
}
c.mu.RLock()
id := taskID(t.ID(), t.Namespace())
_, ok := c.tasks[id]
c.mu.RUnlock()
if ok {
return nil // requests to collect metrics should be idempotent
}
entry := entry{task: t}
if labels != nil {
entry.ns = c.ns.WithConstLabels(labels)
}
c.mu.Lock()
c.tasks[id] = entry
c.mu.Unlock()
return nil
}
// Remove removes the provided cgroup by id from the collector
func (c *Collector) Remove(t common.Statable) {
if c.ns == nil {
return
}
c.mu.Lock()
delete(c.tasks, taskID(t.ID(), t.Namespace()))
c.mu.Unlock()
}
// RemoveAll statable items from the collector
func (c *Collector) RemoveAll() {
if c.ns == nil {
return
}
c.mu.Lock()
c.tasks = make(map[string]entry)
c.mu.Unlock()
}

View File

@@ -0,0 +1,165 @@
//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 v1
import (
"sync"
"sync/atomic"
"golang.org/x/sys/unix"
cgroups "github.com/containerd/cgroups/v3/cgroup1"
"github.com/containerd/containerd/v2/pkg/sys"
"github.com/containerd/log"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
func newOOMCollector(ns *metrics.Namespace) (*oomCollector, error) {
fd, err := unix.EpollCreate1(unix.EPOLL_CLOEXEC)
if err != nil {
return nil, err
}
var desc *prometheus.Desc
if ns != nil {
desc = ns.NewDesc("memory_oom", "The number of times a container has received an oom event", metrics.Total, "container_id", "namespace")
}
c := &oomCollector{
fd: fd,
desc: desc,
set: make(map[uintptr]*oom),
}
if ns != nil {
ns.Add(c)
}
go c.start()
return c, nil
}
type oomCollector struct {
mu sync.Mutex
desc *prometheus.Desc
fd int
set map[uintptr]*oom
}
type oom struct {
// count needs to stay the first member of this struct to ensure 64bits
// alignment on a 32bits machine (e.g. arm32). This is necessary as we use
// the sync/atomic operations on this field.
count atomic.Int64
id string
namespace string
c cgroups.Cgroup
triggers []Trigger
}
func (o *oomCollector) Add(id, namespace string, cg cgroups.Cgroup, triggers ...Trigger) error {
o.mu.Lock()
defer o.mu.Unlock()
fd, err := cg.OOMEventFD()
if err != nil {
return err
}
o.set[fd] = &oom{
id: id,
c: cg,
triggers: triggers,
namespace: namespace,
}
event := unix.EpollEvent{
Fd: int32(fd),
Events: unix.EPOLLHUP | unix.EPOLLIN | unix.EPOLLERR,
}
return unix.EpollCtl(o.fd, unix.EPOLL_CTL_ADD, int(fd), &event)
}
func (o *oomCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- o.desc
}
func (o *oomCollector) Collect(ch chan<- prometheus.Metric) {
o.mu.Lock()
defer o.mu.Unlock()
for _, t := range o.set {
c := t.count.Load()
ch <- prometheus.MustNewConstMetric(o.desc, prometheus.CounterValue, float64(c), t.id, t.namespace)
}
}
// Close closes the epoll fd
func (o *oomCollector) Close() error {
return unix.Close(o.fd)
}
func (o *oomCollector) start() {
var (
n int
err error
events [128]unix.EpollEvent
)
for {
if err := sys.IgnoringEINTR(func() error {
n, err = unix.EpollWait(o.fd, events[:], -1)
return err
}); err != nil {
log.L.WithError(err).Error("cgroups: epoll wait failed, OOM notifications disabled")
return
}
for i := 0; i < n; i++ {
o.process(uintptr(events[i].Fd))
}
}
}
func (o *oomCollector) process(fd uintptr) {
// make sure to always flush the eventfd
flushEventfd(fd)
o.mu.Lock()
info, ok := o.set[fd]
if !ok {
o.mu.Unlock()
return
}
o.mu.Unlock()
// if we received an event but it was caused by the cgroup being deleted and the fd
// being closed make sure we close our copy and remove the container from the set
if info.c.State() == cgroups.Deleted {
o.mu.Lock()
delete(o.set, fd)
o.mu.Unlock()
unix.Close(int(fd))
return
}
info.count.Add(1)
for _, t := range info.triggers {
t(info.id, info.namespace, info.c)
}
}
func flushEventfd(efd uintptr) error {
// Buffer must be >= 8 bytes for eventfd reads
// https://man7.org/linux/man-pages/man2/eventfd.2.html
var buf [8]byte
_, err := unix.Read(int(efd), buf[:])
return err
}

View File

@@ -0,0 +1,60 @@
//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 v1
import (
v1 "github.com/containerd/containerd/v2/core/metrics/types/v1"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
var pidMetrics = []*metric{
{
name: "pids",
help: "The limit to the number of pids allowed",
unit: metrics.Unit("limit"),
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Pids == nil {
return nil
}
return []value{
{
v: float64(stats.Pids.Limit),
},
}
},
},
{
name: "pids",
help: "The current number of pids",
unit: metrics.Unit("current"),
vt: prometheus.GaugeValue,
getValues: func(stats *v1.Metrics) []value {
if stats.Pids == nil {
return nil
}
return []value{
{
v: float64(stats.Pids.Current),
},
}
},
},
}

View File

@@ -0,0 +1,55 @@
//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 v2
import (
"context"
"github.com/containerd/containerd/v2/core/events"
"github.com/containerd/containerd/v2/core/runtime"
"github.com/docker/go-metrics"
)
// NewTaskMonitor returns a new cgroups monitor
func NewTaskMonitor(ctx context.Context, publisher events.Publisher, ns *metrics.Namespace) (runtime.TaskMonitor, error) {
collector := NewCollector(ns)
return &cgroupsMonitor{
collector: collector,
context: ctx,
publisher: publisher,
}, nil
}
type cgroupsMonitor struct {
collector *Collector
context context.Context
publisher events.Publisher
}
func (m *cgroupsMonitor) Monitor(c runtime.Task, labels map[string]string) error {
if err := m.collector.Add(c, labels); err != nil {
return err
}
return nil
}
func (m *cgroupsMonitor) Stop(c runtime.Task) error {
m.collector.Remove(c)
return nil
}

View File

@@ -0,0 +1,124 @@
//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 v2
import (
v2 "github.com/containerd/containerd/v2/core/metrics/types/v2"
metrics "github.com/docker/go-metrics"
"github.com/prometheus/client_golang/prometheus"
)
var cpuMetrics = []*metric{
{
name: "cpu_usage_usec",
help: "Total cpu usage (cgroup v2)",
unit: metrics.Unit("microseconds"),
vt: prometheus.GaugeValue,
getValues: func(stats *v2.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.UsageUsec),
},
}
},
},
{
name: "cpu_user_usec",
help: "Current cpu usage in user space (cgroup v2)",
unit: metrics.Unit("microseconds"),
vt: prometheus.GaugeValue,
getValues: func(stats *v2.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.UserUsec),
},
}
},
},
{
name: "cpu_system_usec",
help: "Current cpu usage in kernel space (cgroup v2)",
unit: metrics.Unit("microseconds"),
vt: prometheus.GaugeValue,
getValues: func(stats *v2.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.SystemUsec),
},
}
},
},
{
name: "cpu_nr_periods",
help: "Current cpu number of periods (only if controller is enabled)",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v2.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.NrPeriods),
},
}
},
},
{
name: "cpu_nr_throttled",
help: "Total number of times tasks have been throttled (only if controller is enabled)",
unit: metrics.Total,
vt: prometheus.GaugeValue,
getValues: func(stats *v2.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.NrThrottled),
},
}
},
},
{
name: "cpu_throttled_usec",
help: "Total time duration for which tasks have been throttled. (only if controller is enabled)",
unit: metrics.Unit("microseconds"),
vt: prometheus.GaugeValue,
getValues: func(stats *v2.Metrics) []value {
if stats.CPU == nil {
return nil
}
return []value{
{
v: float64(stats.CPU.ThrottledUsec),
},
}
},
},
}

Some files were not shown because too many files have changed in this diff Show More