Commit 057e0391 authored by jan.koester's avatar jan.koester
Browse files

Initial import of containerd

Containers from Docker and OCI images, modeled on systemd-nspawn, as a
sysbus service: settings in /etc/containerd/<name>.nspawn (nspawn's
sections and keys), images pulled from Docker Hub and other registries
(OCI distribution API over libhttppp, token login, manifest lists,
digest-checked layers, overlayfs whiteouts), containers in their own
mount/PID/UTS/IPC/cgroup namespaces with Docker's capability set, a
supervisor per container that survives containerd restarts, a small init
as PID 1 (or the image's own with boot=yes), restart policy, autostart,
memory/task limits, bind mounts and tmpfs.

Networking: the host's, loopback only, or a veth pair in a bridge with a
static address or DHCP (lease renewed by the supervisor, stable MAC from
the name). Devices such as GPUs (/dev/dri) can be passed in; a cgroup BPF
device filter keeps all others out (without BPF in the kernel CAP_MKNOD
is dropped instead).

containerctl is the command line; webctl's containers page the web UI.
API: include/containerd/api.h.

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parents
Loading
Loading
Loading
Loading

CMakeLists.txt

0 → 100644
+62 −0
Changes for CMakeLists.txt: 62 added lines, 0 removed lines.
Original line number Diff line number Diff line
cmake_minimum_required(VERSION 3.16)
project(containerd VERSION 0.1.0 LANGUAGES CXX)

# containerd: containers from Docker and OCI images, modeled on
# systemd-nspawn. Images are pulled from a registry (Docker Hub & co.,
# the OCI distribution API over libhttppp) and unpacked once per layer
# for overlayfs; a container is a settings file
# /etc/containerd/<name>.nspawn (nspawn's sections and keys) and runs in
# its own namespaces and cgroup, its network shared with the host, only
# loopback, or a veth pair in a bridge. It is controlled over sysbus as
# the service "containerd" (include/containerd/api.h) -- webctl's
# containers page and containerctl are its clients.
#
#   containerd     the daemon (root)
#   containerctl   command line over sysbus

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

include(GNUInstallDirs)

# libhttppp's package config doesn't chain its dependencies
find_package(libnetplus REQUIRED)
find_package(libcmdplus REQUIRED)
find_package(libhttppp REQUIRED)
find_package(json-c REQUIRED)
find_package(sysbus REQUIRED)
find_package(LibreSSL REQUIRED)
find_package(ZLIB REQUIRED)
find_package(zstd REQUIRED)

add_executable(containerd
    src/main.cpp
    src/result.cpp
    src/jobs.cpp
    src/util.cpp
    src/settings.cpp
    src/registry.cpp
    src/layer.cpp
    src/images.cpp
    src/netlink.cpp
    src/devices.cpp
    src/dhcp.cpp
    src/runtime.cpp
    src/containers.cpp
)
target_include_directories(containerd PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(containerd PRIVATE sysbus::sysbus httppp::httppp netplus::netplus json-c::json-c
                      LibreSSL::Crypto ZLIB::ZLIB zstd::libzstd)

add_executable(containerctl src/containerctl.cpp)
target_include_directories(containerctl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(containerctl PRIVATE sysbus::sysbus)

install(TARGETS containerd containerctl RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
install(DIRECTORY include/containerd DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES dist/registries.conf DESTINATION /etc/containerd)
set(CONTAINERD_UNITDIR "/etc/syslite/services" CACHE PATH "syslite unit directory")
install(FILES dist/containerd.service DESTINATION ${CONTAINERD_UNITDIR})
+11 −0
Changes for dist/containerd.service: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
[Unit]
Description=containerd: containers from Docker/OCI images (nspawn style)
After=network.target

[Service]
Type=simple
ExecStart=/usr/sbin/containerd
Restart=always

[Install]
WantedBy=multi-user.target

dist/registries.conf

0 → 100644
+8 −0
Changes for dist/registries.conf: 8 added lines, 0 removed lines.
Original line number Diff line number Diff line
# containerd: credentials for registries (see containerd/api.h)
#
# [docker.io]
# User=me
# Password=secret
#
# [registry.lan:5000]
# Insecure=yes
+177 −0
Changes for include/containerd/api.h: 177 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* containerd on sysbus: the service "containerd", reached with
 * sysbus::Client::callService(containerd::Service, method, fields).
 * Every method needs root (the bus passes the caller's uid along). Error
 * messages are in English, or in German if the call has a field "lang"
 * starting with "de". Flags are yes|no, sizes in bytes, times unix time.
 *
 * Lists are flat: a record starts with its key (image, container, job,
 * line) and the fields after it belong to it until the next one starts.
 * Keys may repeat within a record (env, arg, bind, ...).
 *
 * ---- images ----
 *
 * Docker and OCI images, pulled from a registry with the distribution API
 * (Docker Hub and others; anonymous, or with the credentials in
 * /etc/containerd/registries.conf). A reference is
 * [registry/]repository[:tag|@sha256:digest]; "nginx" means
 * docker.io/library/nginx:latest. Images for this machine's architecture
 * are taken from multi-platform images. Layers are unpacked once
 * (overlayfs whiteouts) and shared by every image that has them.
 *
 *   ListImages -> per image: image (reference), id (config digest),
 *       digest (manifest), size (compressed), layers, created, pulled,
 *       architecture, entrypoint?, cmd?, workdir?, user?, env (repeated),
 *       port (repeated, e.g. 80/tcp), volume (repeated), used_by (repeated:
 *       containers on it)
 *   PullImage {image} -> a job (always in the background, see below);
 *       pulling an image that is there again updates it
 *   RemoveImage {image}         not while a container uses it; layers no
 *                               other image has go too
 *
 * ---- containers ----
 *
 * Modeled on systemd-nspawn: a container is a settings file
 * /etc/containerd/<name>.nspawn (sections [Container], [Exec], [Files],
 * [Network], nspawn's keys where nspawn has one) and a root file system:
 * the image's layers read-only below, the container's own changes in
 * /var/lib/containerd/containers/<name>/upper. It runs in its own mount,
 * PID, UTS, IPC and cgroup namespaces, and its own network namespace
 * unless it shares the host's network, in the cgroup
 * /sys/fs/cgroup/containerd/<name>, with a reduced capability bounding
 * set. Its command runs as PID 2 under a small init (PID 1) that passes
 * signals on and reaps, or -- boot = yes -- the image's /sbin/init runs as
 * PID 1, like nspawn --boot. stdout and stderr go to
 * /var/log/containerd/<name>.log.
 *
 * Settings, in replies and as arguments (Create: name and image needed;
 * Edit: only what is given changes, a repeated key given once with an
 * empty value clears its list; changes apply at the next start):
 *   name        letters, digits, - _ . (max. 64); the host name by default
 *   image       reference of a pulled image
 *   entrypoint  repeated: replaces the image's Entrypoint
 *   arg         repeated: replaces the image's Cmd (nspawn Parameters=)
 *   env         repeated KEY=value, added to the image's Env
 *   user        name|uid[:group|gid] inside the container (image: User)
 *   workdir     (image: WorkingDir)
 *   hostname
 *   boot        yes: run the image's init as PID 1 (an OS image)
 *   autostart   yes: started with containerd (at boot)
 *   restart     no|on-failure|always
 *   network     host (the host's network, nspawn's default) | none (only
 *               loopback) | bridge (a veth pair, the host end in bridge)
 *   bridge      a bridge of the host, e.g. br0 (network = bridge)
 *   address     a.b.c.d/len of eth0 in the container (network = bridge),
 *               or "dhcp": containerd asks the bridge's DHCP server, keeps
 *               the lease renewed, takes router and name servers from it
 *   gateway     its default route (not with dhcp)
 *   mac         eth0's MAC address; default: 02:.. made from the name,
 *               the same at every start (for DHCP reservations)
 *   dns         repeated: name servers (default: the host's)
 *   bind        repeated host[:container][:ro] -- a host directory or file
 *   tmpfs       repeated: a directory that gets an empty tmpfs
 *   device      repeated /dev/...[:/dev/... in the container]: a device
 *               node or a directory of them (a GPU: /dev/dri), read-write;
 *               the command gets the nodes' groups (render, video). Other
 *               devices can't be used in the container (a cgroup device
 *               filter; without BPF in the kernel CAP_MKNOD goes instead)
 *   read_only   yes: the root file system is mounted read-only
 *   memory_max  bytes (0: no limit)        tasks_max  (0: no limit)
 *   capability  repeated: added to the bounding set (e.g. NET_ADMIN)
 *   drop_capability  repeated: taken from it
 *
 *   ListContainers -> per container: container (name), image, state
 *       (stopped|starting|running|stopping|exited|failed), pid (host PID of
 *       its PID 1), started, exit_code, restarts, error?, network,
 *       address?, lease? (the address DHCP gave it), autostart, restart,
 *       memory (current bytes), tasks
 *   GetContainer {name} -> container (name), state & co. as in the list,
 *       then all settings
 *   CreateContainer {settings}       the image must have been pulled
 *   EditContainer {name, settings}   rename: new_name (stopped only)
 *   DeleteContainer {name}           stopped only; its changes go too
 *   StartContainer {name} -> state (running, or failed and error)
 *   StopContainer {name, timeout? (s, default 10)}  the image's
 *       StopSignal (default SIGTERM) to the container, after timeout
 *       everything in its cgroup is killed; replies at once (stopping)
 *   RestartContainer {name}          stop (if running), then start
 *   KillContainer {name, signal? (number or name, default KILL)}
 *   ContainerLog {name, lines? (default 200, max 5000)} -> line (repeated)
 *
 * ---- jobs ----
 *
 * Pulls and deletions run in a child process each; the methods reply at
 * once with background = yes and the job record:
 *   job (id), title, state (running|done|failed), progress (text),
 *   seconds, finished (0 while running), error?
 *   ListJobs -> the jobs of the last ten minutes */

#pragma once

namespace containerd {

constexpr const char *Service = "containerd";

namespace method {
constexpr const char *ListImages = "ListImages";
constexpr const char *PullImage = "PullImage";
constexpr const char *RemoveImage = "RemoveImage";

constexpr const char *ListContainers = "ListContainers";
constexpr const char *GetContainer = "GetContainer";
constexpr const char *CreateContainer = "CreateContainer";
constexpr const char *EditContainer = "EditContainer";
constexpr const char *DeleteContainer = "DeleteContainer";
constexpr const char *StartContainer = "StartContainer";
constexpr const char *StopContainer = "StopContainer";
constexpr const char *RestartContainer = "RestartContainer";
constexpr const char *KillContainer = "KillContainer";
constexpr const char *ContainerLog = "ContainerLog";

constexpr const char *ListJobs = "ListJobs";
} // namespace method

namespace signal {
// fields: container, state, exit_code
constexpr const char *ContainerStateChanged = "ContainerStateChanged";
// fields: job, state -- a job ended (done or failed)
constexpr const char *JobFinished = "JobFinished";
// an image was pulled or removed
constexpr const char *ImagesChanged = "ImagesChanged";
} // namespace signal

// Error names besides sysbus's own (InvalidArgs, Failed, AccessDenied,
// UnknownMethod).
namespace error {
constexpr const char *NotFound = "containerd.Error.NotFound";        // no such image, container
constexpr const char *Busy = "containerd.Error.Busy";                // running, in use, exists
constexpr const char *Unavailable = "containerd.Error.Unavailable";  // the kernel lacks something
} // namespace error

} // namespace containerd

src/child.h

0 → 100644
+51 −0
Changes for src/child.h: 51 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

/* containerd forks for every job and every container it starts
 * (supervisor). Such a child may run for longer than containerd itself
 * does, so it lets go of the sysbus connection at once -- else
 * the bus would keep containerd's name registered to it after a restart --
 * and takes the default signal actions back (containerd's handlers only
 * write to its signal pipe). */

#pragma once

#include <string>

namespace containerd {

// the sysbus connection's fd (-1: none), for afterFork() to close
void setBusFd(int fd);
// first thing in every child
void afterFork();
// what ps shows for this process (the forks would all be "/usr/sbin/containerd",
// and a pattern meant for containerd would hit the containers as well)
void setTitle(const std::string &title);
// in main(), before anything else: makes room for setTitle()
void initTitle(int argc, char **argv);

} // namespace containerd
Loading