Loading CMakeLists.txt +2 −0 Changes for CMakeLists.txt: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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}) dist/certs.d/apps.fenriros.org:5000.pem 0 → 100644 +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----- dist/registries.conf +7 −0 Changes for dist/registries.conf: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 include/containerd/api.h +21 −1 Changes for include/containerd/api.h: 21 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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? Loading @@ -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"; Loading 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
CMakeLists.txt +2 −0 Changes for CMakeLists.txt: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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})
dist/certs.d/apps.fenriros.org:5000.pem 0 → 100644 +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-----
dist/registries.conf +7 −0 Changes for dist/registries.conf: 7 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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
include/containerd/api.h +21 −1 Changes for include/containerd/api.h: 21 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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? Loading @@ -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"; Loading
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