daemon: Add embedded containerd mode
Run containerd inside dockerd when the experimental `embedded-containerd` feature is enabled through `--feature` or the daemon configuration. Serve containerd's gRPC API on a Unix socket, or a named pipe on Windows, for the plugin executor and external tools. Use an in-memory listener for dockerd's own containerd client. Serve TTRPC on a platform endpoint so task shims can publish events. Register only the containerd plugins dockerd needs. Leave CRI, sandbox, streaming, transfer, NRI, and the restart monitor out of the embedded server. Reject `--cri-containerd` when embedded mode is enabled instead of silently ignoring the requested CRI support. Check the feature before `ContainerdAddr` so it can override the default containerd socket supplied by packaged service units. Continue to use the configured external containerd when the feature is disabled. Derive the Windows named-pipe address from the daemon state directory so multiple daemons can run on the same host. Restrict the pipes to the built-in Administrators group and LocalSystem with the same protected DACL as dockerd's API pipe, as the default security descriptor depends on the process token and can expose the containerd endpoints more broadly than intended. Treat embedded mode as a containerd runtime in the Windows test environment. Add the `no_embedded_containerd` build tag so distributors can omit the embedded plugin graph and its dependencies. Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
334 files changed
+69624
-5
No files matched your search
@@ -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")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//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) {
|
||||
log.G(ctx).Warn("Running with experimental embedded-containerd mode")
|
||||
opts := []embedded.DaemonOpt{embedded.WithCRIDisabled()}
|
||||
if cli.Config.Debug {
|
||||
opts = append(opts, embedded.WithLogLevel("debug"))
|
||||
} else if lvl := cli.Config.DaemonLogConfig.LogLevel; lvl != "" {
|
||||
opts = append(opts, embedded.WithLogLevel(lvl))
|
||||
}
|
||||
|
||||
d, err := embedded.Start(
|
||||
ctx,
|
||||
filepath.Join(cli.Config.Root, "containerd"),
|
||||
filepath.Join(cli.Config.ExecRoot, "containerd"),
|
||||
opts...,
|
||||
)
|
||||
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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
+14
-1
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//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"
|
||||
)
|
||||
|
||||
// Config holds parameters for the embedded containerd daemon.
|
||||
type Config struct {
|
||||
// DisabledPlugins lists containerd plugin URIs to disable (e.g. CRI).
|
||||
DisabledPlugins []string
|
||||
// LogLevel overrides the containerd log level (default: inherit from dockerd).
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
// DaemonOpt configures the embedded daemon.
|
||||
type DaemonOpt func(*Config)
|
||||
|
||||
// WithCRIDisabled disables the CRI plugin, which would otherwise start a
|
||||
// Kubernetes-facing gRPC server.
|
||||
func WithCRIDisabled() DaemonOpt {
|
||||
return func(c *Config) {
|
||||
c.DisabledPlugins = append(c.DisabledPlugins, "io.containerd.grpc.v1.cri")
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogLevel sets the containerd log level for the embedded daemon.
|
||||
func WithLogLevel(level string) DaemonOpt {
|
||||
return func(c *Config) { c.LogLevel = level }
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//go:build !no_embedded_containerd
|
||||
|
||||
package embedded
|
||||
|
||||
import (
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"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))
|
||||
}
|
||||
@@ -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(), WithCRIDisabled())
|
||||
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 TestBuildSrvConfigUsesSupervisedLayout(t *testing.T) {
|
||||
rootDir := filepath.Join(t.TempDir(), "root")
|
||||
stateDir := filepath.Join(t.TempDir(), "state")
|
||||
address := defaultAddress(stateDir)
|
||||
|
||||
cfg := buildSrvConfig(&Config{}, rootDir, stateDir, address)
|
||||
|
||||
assert.Check(t, is.Equal(cfg.Root, filepath.Join(rootDir, "daemon")))
|
||||
assert.Check(t, is.Equal(cfg.State, filepath.Join(stateDir, "daemon")))
|
||||
assert.Check(t, is.Equal(cfg.GRPC.Address, address))
|
||||
assert.Check(t, is.Equal(cfg.TTRPC.Address, address+".ttrpc"))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//go:build !no_embedded_containerd
|
||||
|
||||
package embedded
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
@@ -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`))
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//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/cmd/containerd/server"
|
||||
srvconfig "github.com/containerd/containerd/v2/cmd/containerd/server/config"
|
||||
"github.com/containerd/containerd/v2/defaults"
|
||||
"github.com/containerd/containerd/v2/version"
|
||||
"github.com/containerd/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
// Cross-platform plugin registrations. Each blank import registers one or
|
||||
// more containerd plugins that server.New 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 the daemon
|
||||
// subdirectory under 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, opts ...DaemonOpt) (Daemon, error) {
|
||||
cfg := &Config{}
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
setContainerdVersion()
|
||||
|
||||
address := defaultAddress(stateDir)
|
||||
srvCfg := buildSrvConfig(cfg, 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 server.New.
|
||||
if err := server.CreateTopLevelDirectories(srvCfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.G(ctx).WithField("address", address).Info("starting embedded containerd server")
|
||||
srv, err := newServer(ctx, srvCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
socket, err := listen(srvCfg.GRPC.Address)
|
||||
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
|
||||
// (config.TTRPC.Address) to each shim.
|
||||
ttrpcL, err := listen(srvCfg.TTRPC.Address)
|
||||
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")
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newServer initializes the embedded containerd server.
|
||||
//
|
||||
// containerd's server.New adds gRPC metrics to the global Prometheus registry
|
||||
// with MustRegister, which panics if they are already there. BuildKit adds the
|
||||
// same metrics in dockerd, so point containerd's registration at a temporary
|
||||
// registry just for this call. The embedded server's gRPC metrics do not need
|
||||
// to be exposed anyway.
|
||||
//
|
||||
// This is safe because Start runs early in startup, before anything else
|
||||
// registers metrics.
|
||||
func newServer(ctx context.Context, cfg *srvconfig.Config) (*server.Server, error) {
|
||||
saved := prometheus.DefaultRegisterer
|
||||
prometheus.DefaultRegisterer = prometheus.NewRegistry()
|
||||
defer func() { prometheus.DefaultRegisterer = saved }()
|
||||
return server.New(ctx, cfg)
|
||||
}
|
||||
|
||||
func buildSrvConfig(cfg *Config, rootDir, stateDir, address string) *srvconfig.Config {
|
||||
srvCfg := &srvconfig.Config{
|
||||
Version: 2,
|
||||
Root: filepath.Join(rootDir, "daemon"),
|
||||
State: filepath.Join(stateDir, "daemon"),
|
||||
GRPC: srvconfig.GRPCConfig{
|
||||
Address: address,
|
||||
MaxRecvMsgSize: defaults.DefaultMaxRecvMsgSize,
|
||||
MaxSendMsgSize: defaults.DefaultMaxSendMsgSize,
|
||||
},
|
||||
TTRPC: srvconfig.TTRPCConfig{
|
||||
Address: address + ".ttrpc",
|
||||
},
|
||||
DisabledPlugins: cfg.DisabledPlugins,
|
||||
}
|
||||
if cfg.LogLevel != "" {
|
||||
srvCfg.Debug.Level = cfg.LogLevel
|
||||
}
|
||||
return srvCfg
|
||||
}
|
||||
|
||||
type embeddedDaemon struct {
|
||||
srv *server.Server
|
||||
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 the gRPC server and its listeners (socket + pipe). The ttrpc
|
||||
// server is independent, so close its listener to make ServeTTRPC return.
|
||||
e.srv.Stop()
|
||||
e.inMemory.Close()
|
||||
_ = e.ttrpcL.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"}
|
||||
}
|
||||
@@ -154,6 +154,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,6 +164,7 @@ 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/otelttrpc v0.1.0 // indirect
|
||||
github.com/containerd/plugin v1.1.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
|
||||
github.com/containerd/ttrpc v1.2.9 // 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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
@@ -181,6 +183,8 @@ 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/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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+167
@@ -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
|
||||
}
|
||||
+7
@@ -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
|
||||
+69
@@ -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
|
||||
}
|
||||
+268
@@ -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
|
||||
}
|
||||
+1351
File diff suppressed because it is too large.
Load diff
+411
@@ -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
|
||||
}
|
||||
+369
@@ -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
|
||||
}
|
||||
+76
@@ -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
|
||||
}
|
||||
+157
@@ -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
|
||||
|
||||
}
|
||||
+204
@@ -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
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// This package provides utilities for working with container image layers in the cim format
|
||||
// via the wclayer APIs.
|
||||
package cim
|
||||
+93
@@ -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
|
||||
}
|
||||
+78
@@ -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)
|
||||
}
|
||||
+150
@@ -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
|
||||
}
|
||||
}
|
||||
+75
@@ -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)
|
||||
}
|
||||
+138
@@ -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
|
||||
}
|
||||
+49
@@ -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
|
||||
}
|
||||
+542
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+4
@@ -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
|
||||
+61
@@ -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
|
||||
}
|
||||
+214
@@ -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
|
||||
}
|
||||
+372
@@ -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
|
||||
}
|
||||
+201
@@ -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.
|
||||
+65
@@ -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"
|
||||
)
|
||||
+150
@@ -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])
|
||||
}
|
||||
+621
@@ -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
|
||||
}
|
||||
+130
@@ -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;
|
||||
}
|
||||
+17
@@ -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
|
||||
+91
@@ -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
|
||||
}
|
||||
+17
@@ -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
|
||||
+2331
File diff suppressed because it is too large.
Load diff
+200
@@ -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;
|
||||
}
|
||||
+301
@@ -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
|
||||
}
|
||||
+17
@@ -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
|
||||
+2331
File diff suppressed because it is too large.
Load diff
+200
@@ -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;
|
||||
}
|
||||
+684
@@ -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",
|
||||
}
|
||||
+301
@@ -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
|
||||
}
|
||||
+23
@@ -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
|
||||
Generated
Vendored
+182
@@ -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
|
||||
}
|
||||
Generated
Vendored
+37
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+45
@@ -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
|
||||
}
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
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 server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/log"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
diffapi "github.com/containerd/containerd/api/services/diff/v1"
|
||||
sbapi "github.com/containerd/containerd/api/services/sandbox/v1"
|
||||
ssapi "github.com/containerd/containerd/api/services/snapshots/v1"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/containerd/plugin"
|
||||
"github.com/containerd/plugin/registry"
|
||||
|
||||
srvconfig "github.com/containerd/containerd/v2/cmd/containerd/server/config"
|
||||
csproxy "github.com/containerd/containerd/v2/core/content/proxy"
|
||||
"github.com/containerd/containerd/v2/core/diff"
|
||||
diffproxy "github.com/containerd/containerd/v2/core/diff/proxy"
|
||||
sbproxy "github.com/containerd/containerd/v2/core/sandbox/proxy"
|
||||
ssproxy "github.com/containerd/containerd/v2/core/snapshots/proxy"
|
||||
"github.com/containerd/containerd/v2/defaults"
|
||||
"github.com/containerd/containerd/v2/pkg/dialer"
|
||||
"github.com/containerd/containerd/v2/pkg/sys"
|
||||
"github.com/containerd/containerd/v2/pkg/timeout"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
"github.com/containerd/containerd/v2/plugins/services/warning"
|
||||
)
|
||||
|
||||
// CreateTopLevelDirectories creates the top-level root and state directories.
|
||||
func CreateTopLevelDirectories(config *srvconfig.Config) error {
|
||||
switch {
|
||||
case config.Root == "":
|
||||
return errors.New("root must be specified")
|
||||
case config.State == "":
|
||||
return errors.New("state must be specified")
|
||||
case config.Root == config.State:
|
||||
return errors.New("root and state must be different paths")
|
||||
}
|
||||
|
||||
if err := sys.MkdirAllWithACL(config.Root, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
// chmod is needed for upgrading from an older release that created the dir with 0o711
|
||||
// We ignore file permission issues due to non-standard rootless deployments that do not put the daemon in UserNS: https://github.com/containerd/containerd/issues/12520
|
||||
// These deployments fundamentally cannot perform this migration without sudo intervention.
|
||||
if err := os.Chmod(config.Root, 0o700); err != nil && !errors.Is(err, os.ErrPermission) {
|
||||
return err
|
||||
}
|
||||
// For supporting userns-remapped containers, the state dir cannot be just mkdired with 0o700.
|
||||
// Each of plugins creates a dedicated directory beneath the state dir with appropriate permission bits.
|
||||
if err := sys.MkdirAllWithACL(config.State, 0o711); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.TempDir != "" {
|
||||
if err := sys.MkdirAllWithACL(config.TempDir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
// chmod is needed for upgrading from an older release that created the dir with 0o711
|
||||
// We ignore file permission issues due to non-standard rootless deployments that do not put the daemon in UserNS: https://github.com/containerd/containerd/issues/12520
|
||||
// These deployments fundamentally cannot perform this migration without sudo intervention.
|
||||
if err := os.Chmod(config.TempDir, 0o700); err != nil && !errors.Is(err, os.ErrPermission) {
|
||||
return err
|
||||
}
|
||||
setTempDirEnv(config.TempDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setTempDirEnv points the process' temp-directory environment variables at
|
||||
// tempDir so that components (and the OS) honor the configured temp location.
|
||||
func setTempDirEnv(tempDir string) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// On Windows, the Host Compute Service (vmcompute) will read the
|
||||
// TEMP/TMP setting from the calling process when creating the
|
||||
// tempdir to extract an image layer to. This allows the
|
||||
// administrator to align the tempdir location with the same volume
|
||||
// as the snapshot dir to avoid a copy operation when moving the
|
||||
// extracted layer to the snapshot dir location.
|
||||
os.Setenv("TEMP", tempDir)
|
||||
os.Setenv("TMP", tempDir)
|
||||
// Since Go 1.21, os.MkdirTemp/os.TempDir resolve the temp dir via
|
||||
// Windows' GetTempPath2W. For processes running as SYSTEM (as
|
||||
// containerd does under the SCM), that API reads SystemTemp rather
|
||||
// than TEMP/TMP, so set it too to keep the override effective.
|
||||
// https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/os/file_windows.go
|
||||
os.Setenv("SystemTemp", tempDir)
|
||||
} else {
|
||||
os.Setenv("TMPDIR", tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates and initializes a new containerd server
|
||||
func New(ctx context.Context, config *srvconfig.Config) (*Server, error) {
|
||||
if err := apply(ctx, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, sec := range config.Timeouts {
|
||||
d, err := time.ParseDuration(sec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse %s into a time duration", sec)
|
||||
}
|
||||
timeout.Set(key, d)
|
||||
}
|
||||
loaded, err := LoadPlugins(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for id, p := range config.StreamProcessors {
|
||||
diff.RegisterProcessor(diff.BinaryHandler(id, p.Returns, p.Accepts, p.Path, p.Args, p.Env))
|
||||
}
|
||||
var (
|
||||
s = &Server{
|
||||
config: config,
|
||||
}
|
||||
initialized = plugin.NewPluginSet()
|
||||
required = make(map[string]struct{})
|
||||
grpcAddress = readString(config.Plugins, "io.containerd.server.v1.grpc", "address")
|
||||
ttrpcAddress = readString(config.Plugins, "io.containerd.server.v1.ttrpc", "address")
|
||||
)
|
||||
if grpcAddress == "" {
|
||||
grpcAddress = defaults.DefaultAddress
|
||||
}
|
||||
if ttrpcAddress == "" {
|
||||
ttrpcAddress = defaults.DefaultAddress + ".ttrpc"
|
||||
}
|
||||
for _, r := range config.RequiredPlugins {
|
||||
required[r] = struct{}{}
|
||||
}
|
||||
|
||||
for _, p := range loaded {
|
||||
id := p.URI()
|
||||
log.G(ctx).WithFields(log.Fields{"id": id, "type": p.Type}).Info("loading plugin")
|
||||
var mustSucceed atomic.Int32
|
||||
|
||||
initContext := plugin.NewContext(
|
||||
ctx,
|
||||
initialized,
|
||||
map[string]string{
|
||||
plugins.PropertyRootDir: filepath.Join(config.Root, id),
|
||||
plugins.PropertyStateDir: filepath.Join(config.State, id),
|
||||
plugins.PropertyGRPCAddress: grpcAddress,
|
||||
plugins.PropertyTTRPCAddress: ttrpcAddress,
|
||||
},
|
||||
)
|
||||
initContext.RegisterReadiness = func() func() {
|
||||
mustSucceed.Store(1)
|
||||
return s.RegisterReadiness()
|
||||
}
|
||||
|
||||
// load the plugin specific configuration if it is provided
|
||||
if p.Config != nil {
|
||||
pc, err := config.Decode(ctx, id, p.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
initContext.Config = pc
|
||||
}
|
||||
result := p.Init(initContext)
|
||||
if err := initialized.Add(result); err != nil {
|
||||
return nil, fmt.Errorf("could not add plugin result to plugin set: %w", err)
|
||||
}
|
||||
|
||||
instance, err := result.Instance()
|
||||
if err != nil {
|
||||
if plugin.IsSkipPlugin(err) {
|
||||
log.G(ctx).WithFields(log.Fields{"error": err, "id": id, "type": p.Type}).Info("skip loading plugin")
|
||||
} else {
|
||||
log.G(ctx).WithFields(log.Fields{"error": err, "id": id, "type": p.Type}).Warn("failed to load plugin")
|
||||
}
|
||||
if _, ok := required[id]; ok {
|
||||
return nil, fmt.Errorf("load required plugin %s: %w", id, err)
|
||||
}
|
||||
// If readiness was registered during initialization, the plugin cannot fail
|
||||
if mustSucceed.Load() != 0 {
|
||||
return nil, fmt.Errorf("plugin failed after registering readiness %s: %w", id, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
delete(required, id)
|
||||
if p.Type == plugins.ServerPlugin {
|
||||
srv, ok := instance.(server)
|
||||
if !ok {
|
||||
log.G(ctx).WithField("id", id).Warn("plugin does not implement server interface, will not be started")
|
||||
} else {
|
||||
s.servers = append(s.servers, srv)
|
||||
}
|
||||
}
|
||||
|
||||
s.plugins = append(s.plugins, result)
|
||||
}
|
||||
if len(required) != 0 {
|
||||
var missing []string
|
||||
for id := range required {
|
||||
missing = append(missing, id)
|
||||
}
|
||||
return nil, fmt.Errorf("required plugin %s not included", missing)
|
||||
}
|
||||
|
||||
recordConfigDeprecations(ctx, config, initialized)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// recordConfigDeprecations attempts to record use of any deprecated config field. Failures are logged and ignored.
|
||||
func recordConfigDeprecations(ctx context.Context, config *srvconfig.Config, set *plugin.Set) {
|
||||
// record any detected deprecations without blocking server startup
|
||||
p := set.Get(plugins.WarningPlugin, plugins.DeprecationsPlugin)
|
||||
if p == nil {
|
||||
log.G(ctx).Warn("failed to find warning service to record deprecations")
|
||||
return
|
||||
}
|
||||
instance, err := p.Instance()
|
||||
if err != nil {
|
||||
log.G(ctx).WithError(err).Warn("failed to load warning service to record deprecations")
|
||||
return
|
||||
}
|
||||
warn, ok := instance.(warning.Service)
|
||||
if !ok {
|
||||
log.G(ctx).WithError(err).Warn("failed to load warning service to record deprecations, unexpected plugin type")
|
||||
return
|
||||
}
|
||||
|
||||
// warn.Emit(ctx, deprecation...) will be used for future deprecations
|
||||
_ = warn
|
||||
}
|
||||
|
||||
type server interface {
|
||||
Start(context.Context) error
|
||||
}
|
||||
|
||||
// Server is the containerd main daemon
|
||||
type Server struct {
|
||||
servers []server
|
||||
config *srvconfig.Config
|
||||
plugins []*plugin.Plugin
|
||||
ready sync.WaitGroup
|
||||
}
|
||||
|
||||
// Start the services, this will normally start listening on sockets
|
||||
// and serving requests.
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
for _, srv := range s.servers {
|
||||
if err := srv.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop the containerd server canceling any open connections
|
||||
func (s *Server) Stop() {
|
||||
for i := len(s.plugins) - 1; i >= 0; i-- {
|
||||
p := s.plugins[i]
|
||||
instance, err := p.Instance()
|
||||
if err != nil {
|
||||
log.L.WithFields(log.Fields{"error": err, "id": p.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": p.Registration.URI()}).Error("failed to close plugin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterReadiness() func() {
|
||||
s.ready.Add(1)
|
||||
return func() {
|
||||
s.ready.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Wait() {
|
||||
s.ready.Wait()
|
||||
}
|
||||
|
||||
// LoadPlugins loads all plugins into containerd and generates an ordered graph
|
||||
// of all plugins.
|
||||
func LoadPlugins(ctx context.Context, config *srvconfig.Config) ([]plugin.Registration, error) {
|
||||
// load all plugins into containerd
|
||||
clients := &proxyClients{}
|
||||
for name, pp := range config.ProxyPlugins {
|
||||
var (
|
||||
t plugin.Type
|
||||
f func(*grpc.ClientConn) any
|
||||
|
||||
address = pp.Address
|
||||
p v1.Platform
|
||||
err error
|
||||
)
|
||||
|
||||
switch pp.Type {
|
||||
case string(plugins.SnapshotPlugin), "snapshot":
|
||||
t = plugins.SnapshotPlugin
|
||||
ssname := name
|
||||
f = func(conn *grpc.ClientConn) any {
|
||||
return ssproxy.NewSnapshotter(ssapi.NewSnapshotsClient(conn), ssname)
|
||||
}
|
||||
|
||||
case string(plugins.ContentPlugin), "content":
|
||||
t = plugins.ContentPlugin
|
||||
f = func(conn *grpc.ClientConn) any {
|
||||
return csproxy.NewContentStore(conn)
|
||||
}
|
||||
case string(plugins.SandboxControllerPlugin), "sandbox":
|
||||
t = plugins.SandboxControllerPlugin
|
||||
f = func(conn *grpc.ClientConn) any {
|
||||
return sbproxy.NewSandboxController(sbapi.NewControllerClient(conn), name)
|
||||
}
|
||||
case string(plugins.DiffPlugin), "diff":
|
||||
t = plugins.DiffPlugin
|
||||
f = func(conn *grpc.ClientConn) any {
|
||||
return diffproxy.NewDiffApplier(diffapi.NewDiffClient(conn))
|
||||
}
|
||||
default:
|
||||
log.G(ctx).WithField("type", pp.Type).Warn("unknown proxy plugin type")
|
||||
}
|
||||
if pp.Platform != "" {
|
||||
p, err = platforms.Parse(pp.Platform)
|
||||
if err != nil {
|
||||
log.G(ctx).WithFields(log.Fields{"error": err, "plugin": name}).Warn("skipping proxy platform with bad platform")
|
||||
}
|
||||
} else {
|
||||
p = platforms.DefaultSpec()
|
||||
}
|
||||
|
||||
exports := pp.Exports
|
||||
if exports == nil {
|
||||
exports = map[string]string{}
|
||||
}
|
||||
exports["address"] = address
|
||||
|
||||
registry.Register(&plugin.Registration{
|
||||
Type: t,
|
||||
ID: name,
|
||||
InitFn: func(ic *plugin.InitContext) (any, error) {
|
||||
ic.Meta.Exports = exports
|
||||
ic.Meta.Platforms = append(ic.Meta.Platforms, p)
|
||||
ic.Meta.Capabilities = pp.Capabilities
|
||||
conn, err := clients.getClient(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f(conn), nil
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
filter := srvconfig.V2DisabledFilter
|
||||
// return the ordered graph for plugins
|
||||
return registry.Graph(filter(config.DisabledPlugins)), nil
|
||||
}
|
||||
|
||||
type proxyClients struct {
|
||||
m sync.Mutex
|
||||
clients map[string]*grpc.ClientConn
|
||||
}
|
||||
|
||||
func (pc *proxyClients) getClient(address string) (*grpc.ClientConn, error) {
|
||||
pc.m.Lock()
|
||||
defer pc.m.Unlock()
|
||||
if pc.clients == nil {
|
||||
pc.clients = map[string]*grpc.ClientConn{}
|
||||
} else if c, ok := pc.clients[address]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
backoffConfig := backoff.DefaultConfig
|
||||
backoffConfig.MaxDelay = 3 * time.Second
|
||||
connParams := grpc.ConnectParams{
|
||||
Backoff: backoffConfig,
|
||||
}
|
||||
gopts := []grpc.DialOption{
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithConnectParams(connParams),
|
||||
grpc.WithContextDialer(dialer.ContextDialer),
|
||||
|
||||
// TODO(stevvooe): We may need to allow configuration of this on the client.
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)),
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(dialer.DialAddress(address), gopts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %q: %w", address, err)
|
||||
}
|
||||
|
||||
pc.clients[address] = conn
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func readString(properties map[string]any, key ...string) string {
|
||||
for i, k := range key {
|
||||
v, ok := properties[k]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if i < len(key)-1 {
|
||||
properties, ok = v.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return v
|
||||
case fmt.Stringer:
|
||||
return v.String()
|
||||
case []byte:
|
||||
return string(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/containerd/cgroups/v3"
|
||||
cgroup1 "github.com/containerd/cgroups/v3/cgroup1"
|
||||
cgroupsv2 "github.com/containerd/cgroups/v3/cgroup2"
|
||||
srvconfig "github.com/containerd/containerd/v2/cmd/containerd/server/config"
|
||||
"github.com/containerd/containerd/v2/pkg/sys"
|
||||
"github.com/containerd/log"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
// apply sets config settings on the server process
|
||||
func apply(ctx context.Context, config *srvconfig.Config) error {
|
||||
if config.OOMScore != 0 {
|
||||
log.G(ctx).Debugf("changing OOM score to %d", config.OOMScore)
|
||||
if err := sys.SetOOMScore(os.Getpid(), config.OOMScore); err != nil {
|
||||
log.G(ctx).WithError(err).Errorf("failed to change OOM score to %d", config.OOMScore)
|
||||
}
|
||||
}
|
||||
if config.Cgroup.Path != "" {
|
||||
if cgroups.Mode() == cgroups.Unified {
|
||||
cg, err := cgroupsv2.Load(config.Cgroup.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cg.AddProc(uint64(os.Getpid())); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cg, err := cgroup1.Load(cgroup1.StaticPath(config.Cgroup.Path))
|
||||
if err != nil {
|
||||
if err != cgroup1.ErrCgroupDeleted {
|
||||
return err
|
||||
}
|
||||
if cg, err = cgroup1.New(cgroup1.StaticPath(config.Cgroup.Path), &specs.LinuxResources{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := cg.AddProc(uint64(os.Getpid())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
//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 server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
srvconfig "github.com/containerd/containerd/v2/cmd/containerd/server/config"
|
||||
)
|
||||
|
||||
func apply(_ context.Context, _ *srvconfig.Config) error {
|
||||
return nil
|
||||
}
|
||||
+210
@@ -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,
|
||||
}
|
||||
}
|
||||
+105
@@ -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
|
||||
}
|
||||
+51
@@ -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
|
||||
})
|
||||
}
|
||||
+248
@@ -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
|
||||
})
|
||||
}
|
||||
+98
@@ -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
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
+132
@@ -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
|
||||
}
|
||||
+95
@@ -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")
|
||||
}
|
||||
}
|
||||
+146
@@ -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),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
+88
@@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
+796
@@ -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),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
+64
@@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
+208
@@ -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()
|
||||
}
|
||||
+165
@@ -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
|
||||
}
|
||||
+60
@@ -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),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
+55
@@ -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
|
||||
}
|
||||
+124
@@ -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),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
//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 (
|
||||
"strconv"
|
||||
|
||||
v2 "github.com/containerd/containerd/v2/core/metrics/types/v2"
|
||||
metrics "github.com/docker/go-metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var ioMetrics = []*metric{
|
||||
{
|
||||
name: "io_rbytes",
|
||||
help: "IO bytes read",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
labels: []string{"major", "minor"},
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Io == nil {
|
||||
return nil
|
||||
}
|
||||
var out []value
|
||||
for _, e := range stats.Io.Usage {
|
||||
out = append(out, value{
|
||||
v: float64(e.Rbytes),
|
||||
l: []string{strconv.FormatUint(e.Major, 10), strconv.FormatUint(e.Minor, 10)},
|
||||
})
|
||||
}
|
||||
return out
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "io_wbytes",
|
||||
help: "IO bytes written",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
labels: []string{"major", "minor"},
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Io == nil {
|
||||
return nil
|
||||
}
|
||||
var out []value
|
||||
for _, e := range stats.Io.Usage {
|
||||
out = append(out, value{
|
||||
v: float64(e.Wbytes),
|
||||
l: []string{strconv.FormatUint(e.Major, 10), strconv.FormatUint(e.Minor, 10)},
|
||||
})
|
||||
}
|
||||
return out
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "io_rios",
|
||||
help: "Number of read IOs",
|
||||
unit: metrics.Total,
|
||||
vt: prometheus.GaugeValue,
|
||||
labels: []string{"major", "minor"},
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Io == nil {
|
||||
return nil
|
||||
}
|
||||
var out []value
|
||||
for _, e := range stats.Io.Usage {
|
||||
out = append(out, value{
|
||||
v: float64(e.Rios),
|
||||
l: []string{strconv.FormatUint(e.Major, 10), strconv.FormatUint(e.Minor, 10)},
|
||||
})
|
||||
}
|
||||
return out
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "io_wios",
|
||||
help: "Number of write IOs",
|
||||
unit: metrics.Total,
|
||||
vt: prometheus.GaugeValue,
|
||||
labels: []string{"major", "minor"},
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Io == nil {
|
||||
return nil
|
||||
}
|
||||
var out []value
|
||||
for _, e := range stats.Io.Usage {
|
||||
out = append(out, value{
|
||||
v: float64(e.Wios),
|
||||
l: []string{strconv.FormatUint(e.Major, 10), strconv.FormatUint(e.Minor, 10)},
|
||||
})
|
||||
}
|
||||
return out
|
||||
},
|
||||
},
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
//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 memoryMetrics = []*metric{
|
||||
{
|
||||
name: "memory_usage",
|
||||
help: "Current memory usage (cgroup v2)",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Usage),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_usage_limit",
|
||||
help: "Current memory usage limit (cgroup v2)",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.UsageLimit),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_swap_usage",
|
||||
help: "Current swap usage (cgroup v2)",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.SwapUsage),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_swap_limit",
|
||||
help: "Current swap usage limit (cgroup v2)",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.SwapLimit),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "memory_file_mapped",
|
||||
help: "The file_mapped amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.FileMapped),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_file_dirty",
|
||||
help: "The file_dirty amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.FileDirty),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_file_writeback",
|
||||
help: "The file_writeback amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.FileWriteback),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pgactivate",
|
||||
help: "The pgactivate amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pgactivate),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pgdeactivate",
|
||||
help: "The pgdeactivate amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pgdeactivate),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pgfault",
|
||||
help: "The pgfault amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.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 *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pgmajfault),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pglazyfree",
|
||||
help: "The pglazyfree amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pglazyfree),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pgrefill",
|
||||
help: "The pgrefill amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pgrefill),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pglazyfreed",
|
||||
help: "The pglazyfreed amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pglazyfreed),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pgscan",
|
||||
help: "The pgscan amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pgscan),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_pgsteal",
|
||||
help: "The pgsteal amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Pgsteal),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_inactive_anon",
|
||||
help: "The inactive_anon amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.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 *v2.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 *v2.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 *v2.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 *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Unevictable),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_anon",
|
||||
help: "The anon amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Anon),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_file",
|
||||
help: "The file amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.File),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_kernel_stack",
|
||||
help: "The kernel_stack amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.KernelStack),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_slab",
|
||||
help: "The slab amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Slab),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_sock",
|
||||
help: "The sock amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Sock),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_shmem",
|
||||
help: "The shmem amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.Shmem),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_anon_thp",
|
||||
help: "The anon_thp amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.AnonThp),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_slab_reclaimable",
|
||||
help: "The slab_reclaimable amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.SlabReclaimable),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_slab_unreclaimable",
|
||||
help: "The slab_unreclaimable amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.SlabUnreclaimable),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_workingset_refault",
|
||||
help: "The workingset_refault amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.WorkingsetRefault),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_workingset_activate",
|
||||
help: "The workingset_activate amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.WorkingsetActivate),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_workingset_nodereclaim",
|
||||
help: "The workingset_nodereclaim amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.WorkingsetNodereclaim),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_thp_fault_alloc",
|
||||
help: "The thp_fault_alloc amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.ThpFaultAlloc),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_thp_collapse_alloc",
|
||||
help: "The thp_collapse_alloc amount",
|
||||
unit: metrics.Bytes,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Memory == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Memory.ThpCollapseAlloc),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "memory_oom",
|
||||
help: "The number of times a container has received an oom event",
|
||||
unit: metrics.Total,
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.MemoryEvents == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.MemoryEvents.Oom),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
+64
@@ -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 v2
|
||||
|
||||
import (
|
||||
v2 "github.com/containerd/containerd/v2/core/metrics/types/v2"
|
||||
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 *v2.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 *v2.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:
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
//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"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
cmetrics "github.com/containerd/containerd/v2/core/metrics"
|
||||
"github.com/containerd/containerd/v2/core/metrics/cgroups/common"
|
||||
v2 "github.com/containerd/containerd/v2/core/metrics/types/v2"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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{}
|
||||
}
|
||||
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, ioMetrics...)
|
||||
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 := &v2.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()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.tasks, taskID(t.ID(), t.Namespace()))
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
+60
@@ -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 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 pidMetrics = []*metric{
|
||||
{
|
||||
name: "pids",
|
||||
help: "The limit to the number of pids allowed (cgroup v2)",
|
||||
unit: metrics.Unit("limit"),
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Pids == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Pids.Limit),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pids",
|
||||
help: "The current number of pids (cgroup v2)",
|
||||
unit: metrics.Unit("current"),
|
||||
vt: prometheus.GaugeValue,
|
||||
getValues: func(stats *v2.Metrics) []value {
|
||||
if stats.Pids == nil {
|
||||
return nil
|
||||
}
|
||||
return []value{
|
||||
{
|
||||
v: float64(stats.Pids.Current),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
+37
@@ -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.
|
||||
*/
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/pkg/timeout"
|
||||
"github.com/containerd/containerd/v2/version"
|
||||
goMetrics "github.com/docker/go-metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
ShimStatsRequestTimeout = "io.containerd.timeout.metrics.shimstats"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ns := goMetrics.NewNamespace("containerd", "", nil)
|
||||
c := ns.NewLabeledCounter("build_info", "containerd build information", "version", "revision")
|
||||
c.WithValues(version.Version, version.Revision).Inc()
|
||||
goMetrics.Register(ns)
|
||||
timeout.Set(ShimStatsRequestTimeout, 2*time.Second)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
//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/cgroups/v3/cgroup1/stats"
|
||||
)
|
||||
|
||||
type (
|
||||
// Metrics alias
|
||||
Metrics = v1.Metrics
|
||||
// BlkIOEntry alias
|
||||
BlkIOEntry = v1.BlkIOEntry
|
||||
// MemoryStat alias
|
||||
MemoryStat = v1.MemoryStat
|
||||
// CPUStat alias
|
||||
CPUStat = v1.CPUStat
|
||||
// CPUUsage alias
|
||||
CPUUsage = v1.CPUUsage
|
||||
// BlkIOStat alias
|
||||
BlkIOStat = v1.BlkIOStat
|
||||
// PidsStat alias
|
||||
PidsStat = v1.PidsStat
|
||||
// RdmaStat alias
|
||||
RdmaStat = v1.RdmaStat
|
||||
// RdmaEntry alias
|
||||
RdmaEntry = v1.RdmaEntry
|
||||
// HugetlbStat alias
|
||||
HugetlbStat = v1.HugetlbStat
|
||||
)
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
//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/cgroups/v3/cgroup2/stats"
|
||||
)
|
||||
|
||||
type (
|
||||
// Metrics alias
|
||||
Metrics = v2.Metrics
|
||||
// MemoryStat alias
|
||||
MemoryStat = v2.MemoryStat
|
||||
// CPUStat alias
|
||||
CPUStat = v2.CPUStat
|
||||
// PidsStat alias
|
||||
PidsStat = v2.PidsStat
|
||||
// IOStat alias
|
||||
IOStat = v2.IOStat
|
||||
)
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package manager is used to manage mounts in a bolt database, normally
|
||||
// backed by a tempfs.
|
||||
/*
|
||||
Database schema
|
||||
|
||||
v1
|
||||
╘══*namespace*
|
||||
├──mounts
|
||||
│ ╘══*mount name*
|
||||
│ ├──id : <varuint64> - Unique ID for mount (auto incrementing)
|
||||
│ ├──createdat : <binary time> - Created at
|
||||
│ ├──updatedat : <binary time> - Updated at
|
||||
│ ├──lease : <string> - Lease
|
||||
│ ├──active
|
||||
│ │ ╘══*order*
|
||||
│ │ ├──mountedat : <binary time> - Mounted at
|
||||
│ │ ├──state : <enum> - (0 - unmounted, 1 - filesystem, 2 - device, 3 - process)
|
||||
│ │ ├──type : <string> - Mount type
|
||||
│ │ ├──source : <string> - Mount source
|
||||
│ │ ├──target : <string> - Mount target (relative to previous mount point)
|
||||
│ │ ├──mp : <string> - Mount point for filesystem mount
|
||||
│ │ ├──mpg : <bool> - Whether the mount point is auto generated (NOT USED)
|
||||
│ │ ├──dev : <string> - Device active for this mount (NOT USED)
|
||||
│ │ ├──pid : <int> - Process created for this mount (NOT USED)
|
||||
│ │ └──options : <string> - Comma separate options
|
||||
│ ├──system
|
||||
│ │ ╘══*order*
|
||||
│ │ ├──type : <string> - Mount type
|
||||
│ │ ├──source : <string> - Mount source
|
||||
│ │ ├──target : <string> - Mount target (relative to previous mount point)
|
||||
│ │ └──options : <string> - Comma separate options
|
||||
│ └──labels
|
||||
│ ╘══*key* : <string> - Label value
|
||||
├──leases
|
||||
│ ╘══*lease id*
|
||||
│ ╘══*mount name*: nil
|
||||
└──unmountq (CURRENTLY NOT USED, may remove)
|
||||
└──*mount name + auto-increment*
|
||||
├──type : <string> - Mount type
|
||||
├──target : <string> - Path to check && unmount
|
||||
├──rm : <bool> - Whether to remove target after unmount
|
||||
├──dev : <string> - Device to check before unmount
|
||||
├──pid : <int> - Process to check and kill
|
||||
├──target : <string> - Path to unmount
|
||||
├──state : <enum> - (0 - unmounted, 1 - filesystem, 2 - device, 3 - process)
|
||||
└─ order : <int> - Order in which was mounted, unmount high to low
|
||||
*/
|
||||
package manager
|
||||
|
||||
var (
|
||||
bucketKeyID = []byte("id")
|
||||
bucketKeyMounts = []byte("mounts")
|
||||
bucketKeyLeases = []byte("leases")
|
||||
bucketKeyLease = []byte("lease")
|
||||
bucketKeyActive = []byte("active")
|
||||
bucketKeySystem = []byte("system")
|
||||
bucketKeyType = []byte("type")
|
||||
bucketKeySource = []byte("source")
|
||||
bucketKeyTarget = []byte("target")
|
||||
bucketKeyOptions = []byte("options")
|
||||
bucketKeyMountedAt = []byte("mat")
|
||||
bucketKeyMountPoint = []byte("mp")
|
||||
bucketKeyLabels = []byte("labels")
|
||||
|
||||
labelGCContainerBackRef = []byte("containerd.io/gc.bref.container")
|
||||
labelGCContentBackRef = []byte("containerd.io/gc.bref.content")
|
||||
labelGCImageBackRef = []byte("containerd.io/gc.bref.image")
|
||||
labelGCSnapBackRef = []byte("containerd.io/gc.bref.snapshot.")
|
||||
)
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
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 manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
)
|
||||
|
||||
const formatCheck = "{{"
|
||||
|
||||
type mountFormatter struct{}
|
||||
|
||||
func (mountFormatter) Transform(_ context.Context, m mount.Mount, a []mount.ActiveMount) (mount.Mount, error) {
|
||||
if sc := formatString(m.Source); sc != nil {
|
||||
f, err := sc(a)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
m.Source = f
|
||||
}
|
||||
|
||||
if tc := formatString(m.Target); tc != nil {
|
||||
f, err := tc(a)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
m.Target = f
|
||||
}
|
||||
|
||||
var o []string
|
||||
for i := range m.Options {
|
||||
if oc := formatString(m.Options[i]); oc != nil {
|
||||
f, err := oc(a)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
if o == nil {
|
||||
o = make([]string, len(m.Options))
|
||||
copy(o, m.Options)
|
||||
}
|
||||
o[i] = f
|
||||
}
|
||||
}
|
||||
if o != nil {
|
||||
m.Options = o
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func formatString(s string) func([]mount.ActiveMount) (string, error) {
|
||||
if !strings.Contains(s, formatCheck) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(a []mount.ActiveMount) (string, error) {
|
||||
// TODO: The formatting is very easy, don't use template
|
||||
fm := template.FuncMap{
|
||||
"source": func(i int) (string, error) {
|
||||
if i < 0 || i >= len(a) {
|
||||
return "", fmt.Errorf("index out of bounds: %d, has %d active mounts", i, len(a))
|
||||
}
|
||||
return a[i].Source, nil
|
||||
},
|
||||
"target": func(i int) (string, error) {
|
||||
if i < 0 || i >= len(a) {
|
||||
return "", fmt.Errorf("index out of bounds: %d, has %d active mounts", i, len(a))
|
||||
}
|
||||
return a[i].Target, nil
|
||||
},
|
||||
"mount": func(i int) (string, error) {
|
||||
if i < 0 || i >= len(a) {
|
||||
return "", fmt.Errorf("index out of bounds: %d, has %d active mounts", i, len(a))
|
||||
}
|
||||
return a[i].MountPoint, nil
|
||||
},
|
||||
"overlay": func(start, end int) (string, error) {
|
||||
var dirs []string
|
||||
if start > end {
|
||||
if start >= len(a) || end < 0 {
|
||||
return "", fmt.Errorf("invalid range: %d-%d, has %d active mounts", start, end, len(a))
|
||||
}
|
||||
for i := start; i >= end; i-- {
|
||||
dirs = append(dirs, a[i].MountPoint)
|
||||
}
|
||||
} else {
|
||||
if start < 0 || end >= len(a) {
|
||||
return "", fmt.Errorf("invalid range: %d-%d, has %d active mounts", start, end, len(a))
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
dirs = append(dirs, a[i].MountPoint)
|
||||
}
|
||||
}
|
||||
return strings.Join(dirs, ":"), nil
|
||||
},
|
||||
}
|
||||
t, err := template.New("").Funcs(fm).Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := t.Execute(buf, nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
}
|
||||
+1215
File diff suppressed because it is too large.
Load diff
+132
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
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 manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
)
|
||||
|
||||
// mkdir is a mount transformer that creates directories
|
||||
// it can be used to ensure directories are created before
|
||||
// overlay options are applied or any case that might need
|
||||
// a directory
|
||||
type mkdir struct {
|
||||
rootMap map[string]*os.Root
|
||||
}
|
||||
|
||||
func (h *mkdir) Transform(ctx context.Context, m mount.Mount, _ []mount.ActiveMount) (mount.Mount, error) {
|
||||
|
||||
var options []string
|
||||
for _, o := range m.Options {
|
||||
if mkdirOption, isMkdir := strings.CutPrefix(o, prefixMkdir); isMkdir {
|
||||
// Format is X-containerd.mkdir.path=value[:mode[:uid:gid]]
|
||||
|
||||
value, isPath := strings.CutPrefix(mkdirOption, "path=")
|
||||
if !isPath {
|
||||
return mount.Mount{}, fmt.Errorf("invalid mkdir option %q: %w", o, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
parts := strings.SplitN(value, ":", 4)
|
||||
var (
|
||||
dir string
|
||||
mode os.FileMode = 0700
|
||||
luid = os.Getuid()
|
||||
lgid = os.Getgid()
|
||||
uid, gid = luid, lgid
|
||||
err error
|
||||
)
|
||||
switch len(parts) {
|
||||
case 4:
|
||||
gid, err = strconv.Atoi(parts[3])
|
||||
if err != nil {
|
||||
return mount.Mount{}, fmt.Errorf("invalid gid %q: %w", parts[3], errdefs.ErrInvalidArgument)
|
||||
}
|
||||
uid, err = strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return mount.Mount{}, fmt.Errorf("invalid uid %q: %w", parts[2], errdefs.ErrInvalidArgument)
|
||||
}
|
||||
fallthrough
|
||||
case 2:
|
||||
var p uint64
|
||||
p, err = strconv.ParseUint(parts[1], 8, 32)
|
||||
if err == nil {
|
||||
mode = os.FileMode(p)
|
||||
if mode != mode&os.ModePerm {
|
||||
return mount.Mount{}, fmt.Errorf("invalid mode %o", p)
|
||||
}
|
||||
} else {
|
||||
return mount.Mount{}, fmt.Errorf("invalid mode %s: %w", parts[1], err)
|
||||
}
|
||||
fallthrough
|
||||
case 1:
|
||||
dir = parts[0]
|
||||
default:
|
||||
return mount.Mount{}, fmt.Errorf("invalid mkdir option %q: %w", o, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
var r *os.Root
|
||||
var subpath string
|
||||
|
||||
for path, root := range h.rootMap {
|
||||
if strings.HasPrefix(dir, path) {
|
||||
r = root
|
||||
subpath = strings.TrimPrefix(dir, path)
|
||||
subpath, _ = filepath.Rel("/", subpath)
|
||||
break
|
||||
}
|
||||
}
|
||||
if r == nil {
|
||||
return mount.Mount{}, fmt.Errorf("no root %q configured for mkdir: %w", dir, errdefs.ErrNotImplemented)
|
||||
}
|
||||
|
||||
if st, err := r.Stat(subpath); err == nil {
|
||||
if st.Mode()&os.ModePerm != mode {
|
||||
// TODO: Chmod support added in go1.25
|
||||
return mount.Mount{}, fmt.Errorf("chmod not supported yet for mkdir handler: %w", errdefs.ErrNotImplemented)
|
||||
}
|
||||
// TODO: check ownership, chown support added in go1.25
|
||||
} else if os.IsNotExist(err) {
|
||||
// TODO: MkdirAll added in go1.25
|
||||
if err := r.Mkdir(subpath, mode); err != nil {
|
||||
return mount.Mount{}, fmt.Errorf("failed to create directory %q: %w", dir, err)
|
||||
}
|
||||
if luid != -1 && (luid != uid || lgid != gid) {
|
||||
// TODO: Chown support added in go1.25
|
||||
//if err := r.Chown(subpath, uid, gid); err != nil {
|
||||
// return fmt.Errorf("failed to chown directory %q: %w", m.Source, err)
|
||||
//}
|
||||
return mount.Mount{}, fmt.Errorf("chown not supported yet for mkdir handler: %w", errdefs.ErrNotImplemented)
|
||||
}
|
||||
} else {
|
||||
return mount.Mount{}, fmt.Errorf("failed to stat %q: %w", dir, err)
|
||||
}
|
||||
} else {
|
||||
options = append(options, o)
|
||||
}
|
||||
}
|
||||
m.Options = options
|
||||
|
||||
return m, nil
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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 manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/log"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
type mkfs struct {
|
||||
rootMap map[string]*os.Root
|
||||
}
|
||||
|
||||
func (t *mkfs) Transform(ctx context.Context, m mount.Mount, a []mount.ActiveMount) (mount.Mount, error) {
|
||||
var r *os.Root
|
||||
var subpath string
|
||||
|
||||
for path, root := range t.rootMap {
|
||||
if strings.HasPrefix(m.Source, path) {
|
||||
r = root
|
||||
subpath = strings.TrimPrefix(m.Source, path)
|
||||
subpath, _ = filepath.Rel("/", subpath)
|
||||
break
|
||||
}
|
||||
}
|
||||
if r == nil {
|
||||
err := fmt.Errorf("no root %q configured for mkfs: %w", m.Source, errdefs.ErrNotImplemented)
|
||||
log.G(ctx).WithError(err).Debugf("skipping mkfs")
|
||||
return m, err
|
||||
}
|
||||
|
||||
log.G(ctx).Debugf("transforming mkfs mount: %+v", m)
|
||||
|
||||
var (
|
||||
size int64
|
||||
id string
|
||||
fs = "ext4"
|
||||
)
|
||||
var options []string
|
||||
for _, o := range m.Options {
|
||||
if mkfsOption, isMkfs := strings.CutPrefix(o, prefixMkfs); isMkfs {
|
||||
key, value, ok := strings.Cut(mkfsOption, "=")
|
||||
if !ok {
|
||||
key = o
|
||||
value = "true"
|
||||
}
|
||||
switch key {
|
||||
case "size":
|
||||
var err error
|
||||
size, err = units.RAMInBytes(value)
|
||||
if err != nil {
|
||||
return mount.Mount{}, fmt.Errorf("bad option %s: %w", key, err)
|
||||
}
|
||||
case "fs":
|
||||
fs = value
|
||||
case "uuid":
|
||||
id = value
|
||||
default:
|
||||
return mount.Mount{}, fmt.Errorf("unknown mount option %s: %w", key, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
} else {
|
||||
options = append(options, o)
|
||||
}
|
||||
}
|
||||
m.Options = options
|
||||
if size == 0 {
|
||||
return mount.Mount{}, fmt.Errorf("mkfs requires mkfs.size option: %w", errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
if _, err := r.Stat(subpath); err == nil {
|
||||
// Check magic number
|
||||
} else if os.IsNotExist(err) {
|
||||
createArgs := []string{"-q"}
|
||||
|
||||
// TODO: Pre-resolve the binaries to absolute path on startup for supported fs types
|
||||
var binary string
|
||||
|
||||
// Check fs
|
||||
switch fs {
|
||||
case "ext2", "ext3", "ext4":
|
||||
binary = fmt.Sprintf("mkfs.%s", fs)
|
||||
if id != "" {
|
||||
createArgs = append(createArgs, []string{"-U", id}...)
|
||||
}
|
||||
case "xfs":
|
||||
binary = "mkfs.xfs"
|
||||
if id != "" {
|
||||
createArgs = append(createArgs, []string{"-m", fmt.Sprintf("uuid=%s", id)}...)
|
||||
}
|
||||
default:
|
||||
return mount.Mount{}, fmt.Errorf("unsupported filesystem %q: %w", fs, errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
f, err := r.OpenFile(subpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0640)
|
||||
if err != nil {
|
||||
return mount.Mount{}, fmt.Errorf("failed to create file %q: %w", m.Source, err)
|
||||
}
|
||||
|
||||
createArgs = append(createArgs, f.Name())
|
||||
|
||||
err = f.Truncate(size)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return mount.Mount{}, fmt.Errorf("failed to truncate file %q: %w", m.Source, err)
|
||||
}
|
||||
|
||||
if err := createWritableImage(ctx, binary, createArgs...); err != nil {
|
||||
return mount.Mount{}, fmt.Errorf("failed format %q: %w", m.Source, err)
|
||||
}
|
||||
} else {
|
||||
return mount.Mount{}, fmt.Errorf("failed to stat %q: %w", m.Source, err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func createWritableImage(ctx context.Context, binary string, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, binary, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s failed: %s: %w", filepath.Base(binary), out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/mount"
|
||||
)
|
||||
|
||||
const (
|
||||
// define mount options using X-containerd prefix as defined by
|
||||
// https://man7.org/linux/man-pages/man8/mount.8.html
|
||||
|
||||
prefixMkdir = "X-containerd.mkdir."
|
||||
prefixMkfs = "X-containerd.mkfs."
|
||||
)
|
||||
|
||||
type typeTransformer struct {
|
||||
mount.Transformer
|
||||
|
||||
mountType string
|
||||
}
|
||||
|
||||
func (t typeTransformer) Transform(ctx context.Context, m mount.Mount, a []mount.ActiveMount) (mount.Mount, error) {
|
||||
m.Type = t.mountType
|
||||
return t.Transformer.Transform(ctx, m, a)
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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 runtime
|
||||
|
||||
import (
|
||||
"github.com/containerd/containerd/api/events"
|
||||
"github.com/containerd/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// TaskCreateEventTopic for task create
|
||||
TaskCreateEventTopic = "/tasks/create"
|
||||
// TaskStartEventTopic for task start
|
||||
TaskStartEventTopic = "/tasks/start"
|
||||
// TaskOOMEventTopic for task oom
|
||||
TaskOOMEventTopic = "/tasks/oom"
|
||||
// TaskExitEventTopic for task exit
|
||||
TaskExitEventTopic = "/tasks/exit"
|
||||
// TaskDeleteEventTopic for task delete
|
||||
TaskDeleteEventTopic = "/tasks/delete"
|
||||
// TaskExecAddedEventTopic for task exec create
|
||||
TaskExecAddedEventTopic = "/tasks/exec-added"
|
||||
// TaskExecStartedEventTopic for task exec start
|
||||
TaskExecStartedEventTopic = "/tasks/exec-started"
|
||||
// TaskPausedEventTopic for task pause
|
||||
TaskPausedEventTopic = "/tasks/paused"
|
||||
// TaskResumedEventTopic for task resume
|
||||
TaskResumedEventTopic = "/tasks/resumed"
|
||||
// TaskCheckpointedEventTopic for task checkpoint
|
||||
TaskCheckpointedEventTopic = "/tasks/checkpointed"
|
||||
// TaskUnknownTopic for unknown task events
|
||||
TaskUnknownTopic = "/tasks/?"
|
||||
)
|
||||
|
||||
// GetTopic converts an event from an interface type to the specific
|
||||
// event topic id
|
||||
func GetTopic(e any) string {
|
||||
switch e.(type) {
|
||||
case *events.TaskCreate:
|
||||
return TaskCreateEventTopic
|
||||
case *events.TaskStart:
|
||||
return TaskStartEventTopic
|
||||
case *events.TaskOOM:
|
||||
return TaskOOMEventTopic
|
||||
case *events.TaskExit:
|
||||
return TaskExitEventTopic
|
||||
case *events.TaskDelete:
|
||||
return TaskDeleteEventTopic
|
||||
case *events.TaskExecAdded:
|
||||
return TaskExecAddedEventTopic
|
||||
case *events.TaskExecStarted:
|
||||
return TaskExecStartedEventTopic
|
||||
case *events.TaskPaused:
|
||||
return TaskPausedEventTopic
|
||||
case *events.TaskResumed:
|
||||
return TaskResumedEventTopic
|
||||
case *events.TaskCheckpointed:
|
||||
return TaskCheckpointedEventTopic
|
||||
default:
|
||||
log.L.Warnf("no topic for type %#v", e)
|
||||
}
|
||||
return TaskUnknownTopic
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
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 runtime
|
||||
|
||||
// TaskMonitor provides an interface for monitoring of containers within containerd
|
||||
type TaskMonitor interface {
|
||||
// Monitor adds the provided container to the monitor.
|
||||
// Labels are optional (can be nil) key value pairs to be added to the metrics namespace.
|
||||
Monitor(task Task, labels map[string]string) error
|
||||
// Stop stops and removes the provided container from the monitor
|
||||
Stop(task Task) error
|
||||
}
|
||||
|
||||
// NewMultiTaskMonitor returns a new TaskMonitor broadcasting to the provided monitors
|
||||
func NewMultiTaskMonitor(monitors ...TaskMonitor) TaskMonitor {
|
||||
return &multiTaskMonitor{
|
||||
monitors: monitors,
|
||||
}
|
||||
}
|
||||
|
||||
// NewNoopMonitor is a task monitor that does nothing
|
||||
func NewNoopMonitor() TaskMonitor {
|
||||
return &noopTaskMonitor{}
|
||||
}
|
||||
|
||||
type noopTaskMonitor struct {
|
||||
}
|
||||
|
||||
func (mm *noopTaskMonitor) Monitor(c Task, labels map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mm *noopTaskMonitor) Stop(c Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type multiTaskMonitor struct {
|
||||
monitors []TaskMonitor
|
||||
}
|
||||
|
||||
func (mm *multiTaskMonitor) Monitor(task Task, labels map[string]string) error {
|
||||
for _, m := range mm.monitors {
|
||||
if err := m.Monitor(task, labels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mm *multiTaskMonitor) Stop(c Task) error {
|
||||
for _, m := range mm.monitors {
|
||||
if err := m.Stop(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loaded 100 of 334 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user