Commit 9cf0f983 authored by jan.koester's avatar jan.koester
Browse files

test

parent 89569c1c
Loading
Loading
Loading
Loading
+2 −0
Changes for CMakeLists.txt: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ add_executable(containerd
    src/registry.cpp
    src/layer.cpp
    src/images.cpp
    src/apps.cpp
    src/netlink.cpp
    src/devices.cpp
    src/dhcp.cpp
@@ -59,5 +60,6 @@ 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)
install(DIRECTORY dist/certs.d DESTINATION /etc/containerd)
set(CONTAINERD_UNITDIR "/etc/syslite/services" CACHE PATH "syslite unit directory")
install(FILES dist/containerd.service DESTINATION ${CONTAINERD_UNITDIR})
+18 −0
Changes for dist/certs.d/apps.fenriros.org:5000.pem: 18 added lines, 0 removed lines.
Original line number Diff line number Diff line
-----BEGIN CERTIFICATE-----
MIIC6DCCAdCgAwIBAgIIMJOwSkm1SpYwDQYJKoZIhvcNAQELBQAwHDEaMBgGA1UE
AwwRYXBwcy5mZW5yaXJvcy5vcmcwHhcNMjYxMTAxMDEyMTI4WhcNMjcxMjAzMDEy
MTI4WjAcMRowGAYDVQQDDBFhcHBzLmZlbnJpcm9zLm9yZzCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAJa5kbBBLrq/0nmPBO7cl+ADkj50orthG6NbdhSY
iRAOqwxFSDmeSi/k3p2aMLnU+y1RIjy0Irj3hlEKA4n9Aql3NseJa8KHWHx8QK30
4FUkn/aAC9wFKmBkgOASpzP59JWqxq5mjXYT4nyNtISRyoMoNheOYjfrxR6FiqwM
0dD6VoFZ2n7oL6oakohCPN0jZORlMno+BPa69du7VgYl50lBzMTiKvHdmXmG6i0+
M3bk7UqEsCxeFmp3l4Jaceyusbo3H443QVkwa7wPHNgUEFNEJFeaB4U5zrn6WD2m
PbTAlsxia8xLyyPYugPur0c7nhdYb7IBsJ8q+x6tscQG1U8CAwEAAaMuMCwwHAYD
VR0RBBUwE4IRYXBwcy5mZW5yaXJvcy5vcmcwDAYDVR0TAQH/BAIwADANBgkqhkiG
9w0BAQsFAAOCAQEAIu7a94ksOV9UZtuz6ZZ9+12FLKyku/mYXAvV8f96R42MeE9o
VWV0Ti3Wr19OtQ8CexoXyT+h8NV4HeiIiRPpJKZdAIqUU/H0YQkDvN3vQq4r2nP6
DQqqWdMOd6S0qI2oOQCPhbzRUfabfno4CJl+8qiLt9QxofJN3B937uV2pnvLpfp7
la7I7ui+GsIjWpwQjWl7Gs8Vc9W5La7r3reDe0+3udjB3+B0WRiD/HZ5tzlpf44E
5TABSP9Hrc2AjueOuZcITFlgKpXSeWbhndqsuswmMkApEczm+anHGxNHWjpF5yWy
Y2tp2DPw2B7if4cdXhIo7W0u1Gks07bktk0Xmw==
-----END CERTIFICATE-----
+7 −0
Changes for dist/registries.conf: 7 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -6,3 +6,10 @@
#
# [registry.lan:5000]
# Insecure=yes
#
# A certificate of its own (self-signed, private CA):
#   /etc/containerd/certs.d/registry.lan:5000.pem
#
# Where the web interface's apps come from (a FenStore):
# [apps]
# Registry=apps.fenriros.org:5000
+21 −1
Changes for include/containerd/api.h: 21 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -55,6 +55,22 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *   RemoveImage {image}         not while a container uses it; layers no
 *                               other image has go too
 *
 * ---- apps ----
 *
 * Apps are the repositories of one FenStore registry, by default
 * apps.fenriros.org:5000 ([apps] Registry= in registries.conf): the public
 * ones, and those its credentials there may read. Asking a registry can
 * take long, so a job reads them into a cache:
 *
 *   ListApps -> registry, updated (0: never read), then per app: app
 *       (repository), image (without tag), description, owner, tag
 *       (repeated, newest first, latest before the others), icon? (base64
 *       webp)
 *   RefreshApps -> a job
 *   InstallApp {name, image, settings as CreateContainer} -> a job: pulls
 *       the image, creates the container and starts it; the settings are
 *       checked before it begins
 *
 * ---- containers ----
 *
 * Modeled on systemd-nspawn: a container is a settings file
@@ -153,7 +169,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ---- jobs ----
 *
 * Pulls and deletions run in a child process each; the methods reply at
 * Pulls, installs, reading apps 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?
@@ -170,6 +186,10 @@ constexpr const char *ListImages = "ListImages";
constexpr const char *PullImage = "PullImage";
constexpr const char *RemoveImage = "RemoveImage";

constexpr const char *ListApps = "ListApps";
constexpr const char *RefreshApps = "RefreshApps";
constexpr const char *InstallApp = "InstallApp";

constexpr const char *ListContainers = "ListContainers";
constexpr const char *GetContainer = "GetContainer";
constexpr const char *CreateContainer = "CreateContainer";

src/apps.cpp

0 → 100644
+201 −0
Changes for src/apps.cpp: 201 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.
*******************************************************************************/

#include "apps.h"

#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <iostream>
#include <ctime>
#include <vector>

#include <json-c/json.h>
#include <unistd.h>

#include <containerd/api.h>
#include <sysbus/client.h>

#include "images.h"
#include "jobs.h"
#include "registry.h"
#include "util.h"

namespace containerd {

namespace {

const size_t MaxIcon = 128 * 1024;
const size_t MaxReply = 900 * 1024;  // of sysbus's 1 MiB; icons go first
const int MaxApps = 500;

std::string cachePath() { return std::string(StateDir) + "/apps.json"; }

std::string text(json_object *o, const char *key) {
    json_object *v = nullptr;
    const char *s = json_object_object_get_ex(o, key, &v) ? json_object_get_string(v) : nullptr;
    return s ? s : "";
}

json_object *member(json_object *o, const char *key) {
    json_object *v = nullptr;
    return json_object_object_get_ex(o, key, &v) ? v : nullptr;
}

// the tags of a repository's page, newest first, latest before the others
json_object *sortedTags(json_object *repo) {
    json_object *out = json_object_new_array();
    json_object *tags = member(repo, "tags");
    if (!tags || !json_object_is_type(tags, json_type_array)) return out;
    std::vector<std::pair<std::string, std::string>> list;  // updated, name
    for (size_t i = 0; i < json_object_array_length(tags); ++i) {
        json_object *t = json_object_array_get_idx(tags, i);
        if (!text(t, "name").empty()) list.emplace_back(text(t, "updated"), text(t, "name"));
    }
    std::sort(list.begin(), list.end(), [](const auto &a, const auto &b) {
        if ((a.second == "latest") != (b.second == "latest")) return a.second == "latest";
        return a.first != b.first ? a.first > b.first : a.second < b.second;
    });
    for (auto &t : list) json_object_array_add(out, json_object_new_string(t.second.c_str()));
    return out;
}

} // namespace

Result listApps() {
    Result r;
    std::string registry = appRegistry(), body;
    json_object *j = readFile(cachePath(), body) ? json_tokener_parse(body.c_str()) : nullptr;
    // a cache of another registry says nothing about this one
    if (!j || text(j, "registry") != registry) {
        if (j) json_object_put(j);
        r.add("registry", registry);
        r.add("updated", 0LL);
        return r;
    }
    r.add("registry", registry);
    r.add("updated", (long long)json_object_get_int64(member(j, "updated")));
    json_object *apps = member(j, "apps");
    size_t total = 0, icons = 0;
    for (size_t i = 0; apps && i < json_object_array_length(apps); ++i) {
        json_object *a = json_object_array_get_idx(apps, i);
        total += text(a, "name").size() * 2 + text(a, "description").size() + text(a, "owner").size() + 256;
        if (json_object *tags = member(a, "tags")) total += 64 * json_object_array_length(tags);
        if (total > MaxReply) break;
        r.add("app", text(a, "name"));
        r.add("image", registry + "/" + text(a, "name"));
        r.add("description", text(a, "description"));
        r.add("owner", text(a, "owner"));
        json_object *tags = member(a, "tags");
        for (size_t t = 0; tags && t < json_object_array_length(tags); ++t)
            r.add("tag", json_object_get_string(json_object_array_get_idx(tags, t)));
        // the text of all apps comes before the icons of the first
        if (!text(a, "icon").empty() && total + icons + text(a, "icon").size() <= MaxReply / 2) {
            icons += text(a, "icon").size();
            r.add("icon", text(a, "icon"));
        }
    }
    json_object_put(j);
    return r;
}

Result refreshApps() {
    Reference ref;
    ref.domain = appRegistry();
    Registry reg(ref);
    std::string body, err;
    if (!reg.api("/api/repositories", body, err)) return fail(err, 500);
    json_object *list = json_tokener_parse(body.c_str());
    json_object *repos = list ? member(list, "repositories") : nullptr;
    if (!repos || !json_object_is_type(repos, json_type_array)) {
        if (list) json_object_put(list);
        return fail(tr("%1 sent no list of apps; is it a FenStore?", "%1 schickte keine Liste von Apps; ist das ein FenStore?", {ref.domain}), 500);
    }
    json_object *apps = json_object_new_array();
    size_t count = std::min(json_object_array_length(repos), (size_t)MaxApps);
    for (size_t i = 0; i < count; ++i) {
        json_object *r = json_object_array_get_idx(repos, i);
        std::string name = text(r, "name");
        jobs::progress(tr("%1 of %2: %3", "%1 von %2: %3", {std::to_string(i + 1), std::to_string(count), name}));
        std::string query = "?name=" + urlEscape(name), page;
        // one repository that doesn't answer doesn't hide the others
        if (!reg.api("/api/repository" + query, page, err)) {
            std::cerr << "containerd: apps: " << name << ": " << err << std::endl;
            continue;
        }
        json_object *repo = json_tokener_parse(page.c_str());
        json_object *tags = repo ? sortedTags(repo) : nullptr;
        if (repo) json_object_put(repo);
        if (!tags || !json_object_array_length(tags)) {  // nothing to install yet
            if (tags) json_object_put(tags);
            continue;
        }
        json_object *a = json_object_new_object();
        json_object_object_add(a, "name", json_object_new_string(name.c_str()));
        json_object_object_add(a, "description", json_object_new_string(text(r, "description").c_str()));
        json_object_object_add(a, "owner", json_object_new_string(text(r, "owner").c_str()));
        json_object_object_add(a, "tags", tags);
        std::string icon;
        if (json_object_get_boolean(member(r, "image")) && reg.api("/api/image" + query + "&w=96", icon, err) &&
            !icon.empty() && icon.size() <= MaxIcon)
            json_object_object_add(a, "icon", json_object_new_string(base64(icon).c_str()));
        json_object_array_add(apps, a);
    }
    json_object_put(list);

    json_object *cache = json_object_new_object();
    json_object_object_add(cache, "registry", json_object_new_string(ref.domain.c_str()));
    json_object_object_add(cache, "updated", json_object_new_int64((int64_t)time(nullptr)));
    json_object_object_add(cache, "apps", apps);
    std::string out = json_object_to_json_string_ext(cache, JSON_C_TO_STRING_PLAIN);
    json_object_put(cache);
    mkdirs(StateDir, 0700);
    std::string tmp = cachePath() + ".tmp" + std::to_string(getpid());
    if (!writeFile(tmp, out, 0644)) return fail(sysErr(tmp, errno), 500);
    if (rename(tmp.c_str(), cachePath().c_str()) != 0) {
        int e = errno;
        unlink(tmp.c_str());
        return fail(sysErr(cachePath(), e), 500);
    }
    return ok();
}

Result installApp(const std::string &ref, const std::string &name, const Fields &fields) {
    Result pulled = pullImage(ref);
    if (pulled.failed()) return pulled;
    // containerd itself is answering meanwhile: back to it over the bus
    sysbus::Client bus;
    if (!bus.connect()) return fail(tr("sysbus isn't reachable: %1", "sysbus ist nicht erreichbar: %1", {bus.lastError()}), 503);
    jobs::progress(tr("starting %1", "%1 wird gestartet", {name}));
    sysbus::Message r = bus.callService(Service, method::CreateContainer, fields, 60000);
    if (r.type != sysbus::MessageType::Error)
        r = bus.callService(Service, method::StartContainer, {{"name", name}, {"lang", language()}}, 60000);
    if (r.type == sysbus::MessageType::Error) return fail(r.get("message", r.member), 500);
    return ok();
}

} // namespace containerd
Loading