// Package runtime provides low-level host and container-runtime primitives for snapshot execution. package runtime import ( "context" "fmt" "path/filepath" "strings" "github.com/containerd/containerd" "github.com/containerd/containerd/namespaces" specs "github.com/opencontainers/runtime-spec/specs-go" securejoin "github.com/cyphar/filepath-securejoin" ) const ( // k8sNamespace is the containerd namespace used by Kubernetes. k8sNamespace = "k8s.io" // ContainerdSocket is the default containerd socket path. ContainerdSocket = "/run/containerd/containerd.sock" ) // ResolveContainer resolves a container by ID and returns its PID and OCI spec. func ResolveContainer(ctx context.Context, client *containerd.Client, containerID string) (int, *specs.Spec, error) { ctx = namespaces.WithNamespace(ctx, k8sNamespace) container, err := client.LoadContainer(ctx, containerID) if err != nil { return 0, nil, fmt.Errorf("failed to load container %s: %w", containerID, err) } task, err := container.Task(ctx, nil) if err != nil { return 0, nil, fmt.Errorf("failed to get task for container %s: %w", containerID, err) } spec, err := container.Spec(ctx) if err != nil { return 0, nil, fmt.Errorf("failed to get spec for container %s: %w", containerID, err) } return int(task.Pid()), spec, nil } // ResolveContainerByPod finds a container by pod name, namespace, and container name // by listing containerd containers and matching CRI labels. func ResolveContainerByPod(ctx context.Context, client *containerd.Client, podName, podNamespace, containerName string) (int, *specs.Spec, error) { ctx = namespaces.WithNamespace(ctx, k8sNamespace) filter := fmt.Sprintf("labels.\"io.kubernetes.pod.name\"==%s,labels.\"io.kubernetes.pod.namespace\"==%s,labels.\"io.kubernetes.container.name\"==%s", podName, podNamespace, containerName) containers, err := client.Containers(ctx, filter) if err != nil { return 0, nil, fmt.Errorf("failed to list containers for pod %s/%s: %w", podNamespace, podName, err) } if len(containers) == 0 { return 0, nil, fmt.Errorf("no container found for pod %s/%s container %s", podNamespace, podName, containerName) } // During container restarts, containerd may transiently expose both the // old and new container with the same CRI labels. Pick the one with a // running task; fall back to the first container if none qualify. for _, c := range containers { task, err := c.Task(ctx, nil) if err != nil { continue } spec, err := c.Spec(ctx) if err != nil { continue } return int(task.Pid()), spec, nil } return 0, nil, fmt.Errorf("no running container found for pod %s/%s container %s (%d candidates)", podNamespace, podName, containerName, len(containers)) } func collectOCIManagedPaths(ociSpec *specs.Spec, rootFS string) map[string]struct{} { set := map[string]struct{}{} if ociSpec == nil { return set } paths := make([]string, 0, len(ociSpec.Mounts)) for _, mount := range ociSpec.Mounts { paths = append(paths, mount.Destination) } if ociSpec.Linux != nil { paths = append(paths, ociSpec.Linux.MaskedPaths...) paths = append(paths, ociSpec.Linux.ReadonlyPaths...) } for _, raw := range paths { if p := normalizeOCIPath(raw, rootFS); p != "" { set[p] = struct{}{} } } return set } // normalizeOCIPath resolves an OCI spec path relative to rootFS, following // symlinks within the rootfs boundary (matching runc's addCriuDumpMount pattern). func normalizeOCIPath(raw, rootFS string) string { p := filepath.Clean(strings.TrimSpace(raw)) if p == "" || p == "." { return "" } if rootFS == "" { return p } if resolved, err := securejoin.SecureJoin(rootFS, p); err == nil { p = strings.TrimPrefix(resolved, filepath.Clean(rootFS)) } if !strings.HasPrefix(p, "/") { p = "/" + p } return p }