diff mbox series

[13/13] Add support for docker

Message ID 20231120155459.45188-14-stefano.babic@swupdate.org
State Accepted
Headers show
Series Native Docker Support | expand

Commit Message

Stefano Babic Nov. 20, 2023, 3:54 p.m. UTC
Start to add native support for containers. Implement the docker REST API
to load an image via the docker's socket. Add a docker handler that take
an image and push to the docker daemon. Following are implemented:

- Image load
- Image remove
. Container create - it takes a JSON configurazion file as input.
- Container delete

Signed-off-by: Stefano Babic <stefano.babic@swupdate.org>
---
 Kconfig                   |   3 +
 Makefile                  |   2 +-
 containers/docker.c       | 237 +++++++++++++++++++++++++++++
 containers/docker.h       |  32 ++++
 doc/source/handlers.rst   | 132 ++++++++++++++++
 handlers/Makefile         |   1 +
 handlers/docker_handler.c | 307 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 713 insertions(+), 1 deletion(-)
 create mode 100644 containers/docker.c
 create mode 100644 containers/docker.h
 create mode 100644 handlers/docker_handler.c
diff mbox series

Patch

diff --git a/Kconfig b/Kconfig
index 2ae2e4bc..5a3dc9a4 100644
--- a/Kconfig
+++ b/Kconfig
@@ -546,3 +546,6 @@  source parser/Config.in
 
 comment Handlers
 source handlers/Config.in
+
+comment Containers
+source containers/Config.in
diff --git a/Makefile b/Makefile
index c693635c..3940a18b 100644
--- a/Makefile
+++ b/Makefile
@@ -363,7 +363,7 @@  include $(srctree)/Makefile.flags
 # Defaults to vmlinux, but the arch makefile usually adds further targets
 
 objs-y		:= core handlers bootloader suricatta
-libs-y		:= corelib mongoose parser fs
+libs-y		:= corelib mongoose parser fs containers
 bindings-y	:= bindings
 tools-y		:= tools
 
diff --git a/containers/docker.c b/containers/docker.c
new file mode 100644
index 00000000..353d4ada
--- /dev/null
+++ b/containers/docker.c
@@ -0,0 +1,237 @@ 
+/*
+ * (C) Copyright 2023
+ * Stefano Babic <stefano.babic@swupdate.org>
+ *
+ * SPDX-License-Identifier:     GPL-2.0-only
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <handler.h>
+#include <util.h>
+#include <json-c/json.h>
+#include "parselib.h"
+#include "channel.h"
+#include "channel_curl.h"
+#include "docker.h"
+#include "docker_interface.h"
+#include "swupdate_dict.h"
+
+typedef struct {
+	const char *url;
+	channel_method_t method;
+} docker_api_t;
+
+docker_api_t docker_api[] = {
+	[DOCKER_IMAGE_LOAD] = {"/images/load", CHANNEL_POST},
+	[DOCKER_IMAGE_DELETE] = {"/images/%s", CHANNEL_DELETE},
+	[DOCKER_CONTAINER_CREATE] = {"/containers/create", CHANNEL_POST},
+	[DOCKER_CONTAINER_DELETE] = {"/containers/%s", CHANNEL_DELETE},
+	[DOCKER_CONTAINER_START] = {"/containers/%s/start", CHANNEL_POST},
+	[DOCKER_CONTAINER_STOP] = {"/containers/%s/stop", CHANNEL_POST},
+};
+
+static channel_data_t channel_data_defaults = {.debug = true,
+					       .unix_socket =(char *) DOCKER_DEFAULT_SOCKET,
+					       .retries = 1,
+					       .retry_sleep =
+						   CHANNEL_DEFAULT_RESUME_DELAY,
+					       .format = CHANNEL_PARSE_JSON,
+					       .nocheckanswer = false,
+					       .nofollow = false,
+					       .noipc = true,
+					       .range = NULL,
+					       .connection_timeout = 0,
+					       .headers = NULL,
+					       .headers_to_send = NULL,
+					       .received_headers = NULL
+						};
+
+
+static const char *docker_base_url(void)
+{
+	return DOCKER_SOCKET_URL;
+}
+
+static void docker_prepare_url(docker_services_t service, char *buf, size_t size)
+{
+	snprintf(buf, size, "%s%s", docker_base_url(), docker_api[service].url);
+}
+
+static channel_t *docker_prepare_channel(channel_data_t *channel_data)
+{
+	channel_t *channel = channel_new();
+	if (!channel) {
+		ERROR("New channel cannot be requested");
+		return NULL;
+	}
+
+	if (channel->open(channel, channel_data) != CHANNEL_OK) {
+		channel->close(channel);
+		free(channel);
+		return NULL;
+	}
+
+	return channel;
+}
+
+static server_op_res_t evaluate_docker_answer(json_object *json_reply)
+{
+	if (!json_reply) {
+		ERROR("No JSON answer from Docker Daemon");
+		return SERVER_EBADMSG;
+	}
+
+	/*
+	 * Check for errors
+	 */
+	json_object *json_error = json_get_path_key(json_reply,
+			      (const char *[]){"error", NULL});
+	if (json_object_get_type(json_error) == json_type_string) {
+		ERROR("Image not loaded, daemon reports: %s",
+		      json_object_get_string(json_error));
+		return SERVER_EBADMSG;
+	}
+
+	json_object *json_stream = json_get_path_key(json_reply,
+			      (const char *[]){"stream", NULL});
+	if (json_object_get_type(json_stream) == json_type_string) {
+		INFO("%s", json_object_get_string(json_stream));
+		return SERVER_OK;
+	}
+	return SERVER_EBADMSG;
+}
+
+server_op_res_t docker_load_image(int fd, size_t len)
+{
+	channel_t *channel;
+	channel_op_res_t ch_response;
+	server_op_res_t result = SERVER_OK;
+	char dockerurl[1024];
+
+	channel_data_t channel_data = channel_data_defaults;
+	struct dict httpheaders_to_send;
+
+	LIST_INIT(&httpheaders_to_send);
+	if (dict_insert_value(&httpheaders_to_send, "Expect", "")) {
+		ERROR("Error initializing HTTP Headers");
+		return SERVER_EINIT;
+	}
+
+	docker_prepare_url(DOCKER_IMAGE_LOAD, dockerurl, sizeof(dockerurl));
+	channel_data.url = dockerurl;
+
+	channel_data.read_fifo = fd;
+	channel_data.method = docker_api[DOCKER_IMAGE_LOAD].method;
+	channel_data.upload_filesize = len;
+	channel_data.headers_to_send = &httpheaders_to_send;
+	channel_data.content_type = "application/x-tar";
+	channel_data.accept_content_type = "application/json";
+
+	channel = docker_prepare_channel(&channel_data);
+	if (!channel) {
+		return SERVER_EERR;
+	}
+	ch_response = channel->put_file(channel, &channel_data);
+
+	if ((result = map_channel_retcode(ch_response)) !=
+	    SERVER_OK) {
+		channel->close(channel);
+		free(channel);
+		return SERVER_EERR;
+	}
+
+	dict_drop_db(&httpheaders_to_send);
+
+	channel->close(channel);
+	free(channel);
+
+	return evaluate_docker_answer(channel_data.json_reply);
+}
+
+static server_op_res_t docker_send_request(docker_services_t service, char *url, char *setup)
+{
+	channel_t *channel;
+	channel_op_res_t ch_response;
+	server_op_res_t result = SERVER_OK;
+	channel_data_t channel_data = channel_data_defaults;
+
+	channel_data.url = url;
+	channel_data.method = docker_api[service].method;
+
+	channel = docker_prepare_channel(&channel_data);
+	if (!channel) {
+		return SERVER_EERR;
+	}
+
+	if (setup)
+		channel_data.request_body = setup;
+
+	ch_response = channel->put(channel, &channel_data);
+
+	if ((result = map_channel_retcode(ch_response)) !=
+	    SERVER_OK) {
+		channel->close(channel);
+		free(channel);
+		return SERVER_EERR;
+	}
+
+	channel->close(channel);
+	free(channel);
+
+	return result;
+}
+
+static server_op_res_t docker_simple_post(docker_services_t service, const char *name)
+{
+	char url[256];
+
+	docker_prepare_url(service, url, sizeof(url));
+	if (name) {
+		char *tmp=strdup(url);
+		snprintf(url, sizeof(url), tmp, name);
+		free(tmp);
+	}
+
+	return docker_send_request(service, url, NULL);
+}
+
+server_op_res_t docker_container_create(const char *name, char *setup)
+{
+	char url[256];
+	
+	if (name) {
+		snprintf(url, sizeof(url), "%s%s?name=%s",
+			 docker_base_url(), docker_api[DOCKER_CONTAINER_CREATE].url, name);
+	} else
+		docker_prepare_url(DOCKER_CONTAINER_CREATE, url, sizeof(url));
+
+	return docker_send_request(DOCKER_CONTAINER_CREATE, url, setup);
+}
+
+server_op_res_t docker_container_remove(const char *name)
+{
+	return docker_simple_post(DOCKER_CONTAINER_DELETE, name);
+}
+
+server_op_res_t docker_container_start(const char *name)
+{
+	return docker_simple_post(DOCKER_CONTAINER_START, name);
+}
+
+server_op_res_t docker_container_stop(const char *name)
+{
+	return docker_simple_post(DOCKER_CONTAINER_STOP, name);
+}
+
+server_op_res_t docker_remove_image(const char *name)
+{
+	return docker_simple_post(DOCKER_IMAGE_DELETE, name);
+}
diff --git a/containers/docker.h b/containers/docker.h
new file mode 100644
index 00000000..671b1e2c
--- /dev/null
+++ b/containers/docker.h
@@ -0,0 +1,32 @@ 
+/*
+ * (C) Copyright 2023
+ * Stefano Babic <stefano.babic@swupdate.org>
+ *
+ * SPDX-License-Identifier:     GPL-2.0-only
+ */
+
+#pragma once
+
+#include "server_utils.h"
+
+/* Supported API */
+#define DOCKER_API_VERSION		"1.43"
+
+/* Default socker for docker */
+#define DOCKER_DEFAULT_SOCKET		"/run/docker.sock"
+
+/* For UDS connection, this is the URL */
+#define DOCKER_SOCKET_URL			"http://localhost"
+
+/* Docker base URL */
+
+#define DOCKER_BASE_URL	DOCKER_SOCKET_URL DOCKER_API_VERSION "/"
+
+typedef enum {
+	DOCKER_IMAGE_LOAD,
+	DOCKER_IMAGE_DELETE,
+	DOCKER_CONTAINER_CREATE,
+	DOCKER_CONTAINER_DELETE,
+	DOCKER_CONTAINER_START,
+	DOCKER_CONTAINER_STOP
+} docker_services_t;
diff --git a/doc/source/handlers.rst b/doc/source/handlers.rst
index e8e2e43f..5bafb558 100644
--- a/doc/source/handlers.rst
+++ b/doc/source/handlers.rst
@@ -1274,3 +1274,135 @@  Zchunk creates a temporary file with all chunks in /tmp, that is at the end conc
 header and written to the destination file. This means that an amount of memory equal to the
 partition (SWUpdate does not compress the chunks) is required. This was solved with later version
 of Zchunk - check inside zchunk code if ZCK_NO_WRITE is supported.
+
+Docker handlers
+----------------
+
+To improve containers update, a docker set of handlers implements part of the API to communicate
+with the docker daemon. Podman (another container solution) has a compatibility layer for
+docker REST API and could be used as well, but SWUpdate is currently not checking if a podman
+daemon must be started.
+
+Goal of these handlers is just to provice the few API to update images and start containers - it
+does not implement the full API.
+
+Docker Load Image
+-----------------
+
+This handler allow to load an image without copying temporarily and push it to the docker daemon.
+It implements the /images/load entry point., and it is implemented as "image" handler. The file
+should be in a format accepted by docker.
+
+::
+
+        images: (
+                filename = "docker-image.tar"
+                type = "docker_imageload";
+                installed-directly = true;
+        )
+
+The handler checks return value (JSON message) from the daemon, and returns with success if the image
+is added.
+
+In case the file must be decompressed, SWUpdate requires the size of the decompressed image to be
+passed to the daemon:
+
+
+::
+
+        images: (
+        {
+                filename = "alpine.tar.gz";
+                type = "docker_imageload";
+                installed-directly = true;
+                compressed = "zlib";
+                properties: {
+                     decompressed-size = "5069312";
+                };
+        });
+
+
+Docker Remove Image
+-------------------
+
+It is implemented as script (post install). 
+Example:
+
+::
+
+	scripts: ( {
+		type = "docker_imagedelete";
+		properties: {
+			name = "alpine:3.4";
+		};
+	});
+
+Docker: container create
+------------------------
+
+It is implemented as post-install script. The script itself is the json file passed
+to the daemon to configure and set the container. The container is just created, not started.
+
+For example, having this hello-world.json file:
+
+::
+
+        {
+                "Image": "hello-world",
+                "HostConfig": {
+                        "RestartPolicy": {
+                                "Name": "always"
+                        },
+                "AutoRemove": false
+                }
+        }
+
+Creating the container can be done in sw-description with:
+
+::
+
+	scripts: ( {
+	        filename = "hello-world.json";
+		type = "docker_containercreate";
+		properties: {
+			name = "helloworld"; /* Name of container */
+		}
+	});
+
+Docker Remove Container
+-----------------------
+
+It is implemented as script (post install). 
+Example:
+
+::
+
+	scripts: ( {
+		type = "docker_containerdelete";
+		properties: {
+			name = "helloworld";
+		};
+	});
+
+Docker : Start / Stop containers
+--------------------------------
+
+Examples:
+
+::
+
+	scripts: ( {
+		type = "docker_containerstart";
+		properties: {
+			name = "helloworld";
+		};
+	});
+       
+::
+
+	scripts: ( {
+		type = "docker_containerstop";
+		properties: {
+			name = "helloworld";
+		};
+	});
diff --git a/handlers/Makefile b/handlers/Makefile
index b5203f96..eb24a49c 100644
--- a/handlers/Makefile
+++ b/handlers/Makefile
@@ -28,3 +28,4 @@  obj-$(CONFIG_SSBLSWITCH) += ssbl_handler.o
 obj-$(CONFIG_SWUFORWARDER_HANDLER) += swuforward_handler.o swuforward-ws.o
 obj-$(CONFIG_UBIVOL)	+= ubivol_handler.o
 obj-$(CONFIG_UCFWHANDLER)	+= ucfw_handler.o
+obj-$(CONFIG_DOCKER)	+= docker_handler.o
diff --git a/handlers/docker_handler.c b/handlers/docker_handler.c
new file mode 100644
index 00000000..9d0aba41
--- /dev/null
+++ b/handlers/docker_handler.c
@@ -0,0 +1,307 @@ 
+/*
+ * (C) Copyright 2017
+ * Stefano Babic, DENX Software Engineering, sbabic@denx.de.
+ *
+ * SPDX-License-Identifier:     GPL-2.0-only
+ */
+
+/*
+ * This handler communicates with the docker socket via REST API
+ * to install new images and manage containers.
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <handler.h>
+#include <pthread.h>
+#include <util.h>
+#include <signal.h>
+#include <json-c/json.h>
+#include "parselib.h"
+#include "swupdate_image.h"
+#include "docker_interface.h"
+
+void docker_loadimage_handler(void);
+void docker_deleteimage_handler(void);
+void docker_createcontainer_handler(void);
+void docker_deletecontainer_handler(void);
+void docker_container_start_handler(void);
+void docker_container_stop_handler(void);
+
+typedef server_op_res_t (*docker_fn)(const char *name);
+
+#define FIFO_THREAD_READ	0
+#define FIFO_HND_WRITE		1
+
+struct hnd_load_priv {
+	int fifo[2];	/* PIPE where to write */
+	size_t	totalbytes;
+	int exit_status;
+};
+
+/*
+ * This is the copyimage's callback. When called,
+ * there is a buffer to be passed to curl connections
+ * This is part of the image load: using copyimage() the image is
+ * transferred without copy to the daemon.
+ */
+static int transfer_data(void *data, const void *buf, size_t len)
+{
+	struct hnd_load_priv *priv = (struct hnd_load_priv *)data;
+	ssize_t written;
+	unsigned int nbytes = len;
+	const void *tmp = buf;
+
+	while (nbytes) {
+		written = write(priv->fifo[FIFO_HND_WRITE], buf, len);
+		if (written < 0) {
+			ERROR ("Cannot write to fifo");
+			return -EFAULT;
+		}
+		nbytes -= written;
+		tmp += written;
+	}
+
+	return 0;
+}
+
+/*
+ * Background threa dto transfer the image to the daemon.
+ * main thread ==> run copyimage
+ * backgound thread ==> push the incoming data to ddaemon
+ */
+static void *curl_transfer_thread(void *p)
+{
+	struct hnd_load_priv *hnd = (struct hnd_load_priv *)p;
+
+	hnd->exit_status = docker_load_image(hnd->fifo[FIFO_THREAD_READ], hnd->totalbytes);
+
+	close(hnd->fifo[FIFO_THREAD_READ]);
+
+	pthread_exit(NULL);
+}
+
+/*
+ * Implementation /images/load
+ */
+static int docker_install_image(struct img_type *img,
+	void __attribute__ ((__unused__)) *data)
+{
+	struct hnd_load_priv priv;
+	pthread_attr_t attr;
+	pthread_t transfer_thread;
+	int thread_ret;
+	int ret = 0;
+	ssize_t bytes;
+
+	signal(SIGPIPE, SIG_IGN);
+
+	/*
+	 * Requires the size of file to be transferrred
+	 */
+	bytes = get_output_size(img, true);
+	if (bytes < 0) {
+		ERROR("Size to be uploaded undefined");
+		return -EINVAL;
+	}
+
+	priv.totalbytes = bytes;
+
+	/*
+	 * Create one FIFO for each connection to be thread safe
+	 */
+	if (pipe(priv.fifo) < 0) {
+		ERROR("Cannot create internal pipes, exit..");
+		ret = FAILURE;
+		goto handler_exit;
+	}
+	pthread_attr_init(&attr);
+	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
+
+	thread_ret = pthread_create(&transfer_thread, &attr, curl_transfer_thread, &priv);
+	if (thread_ret) {
+		ERROR("Code from pthread_create() is %d",
+			thread_ret);
+			transfer_thread = 0;
+			ret = FAILURE;
+			goto handler_exit;
+	}
+
+	ret = copyimage(&priv, img, transfer_data);
+	if (ret) {
+		ERROR("Transferring SWU image was not successful");
+		ret = FAILURE;
+		goto handler_exit;
+	}
+
+	void *status;
+	ret = pthread_join(transfer_thread, &status);
+
+	close(priv.fifo[FIFO_HND_WRITE]);
+
+	return priv.exit_status;
+
+handler_exit:
+	return ret;
+}
+
+/*
+ * Implementation POST /container/create
+ */
+static int docker_create_container(struct img_type *img,
+	void __attribute__ ((__unused__)) *data)
+{
+	struct script_handler_data *script_data = data;
+	char *script = NULL;
+	char *buf = NULL;
+	struct stat sb;
+	int result = 0;
+	int fd = -1;
+
+	/*
+	 * Call only in case of postinstall
+	 */
+	if (!script_data || script_data->scriptfn != POSTINSTALL)
+		return 0;
+
+
+	if (asprintf(&script, "%s%s", get_tmpdirscripts(), img->fname) == ENOMEM_ASPRINTF) {
+		ERROR("OOM when creating script path");
+		return -ENOMEM;
+	}
+
+	if (stat(script, &sb) == -1) {
+		ERROR("stat fails on %s", script);
+		result = -EFAULT;
+		goto create_container_exit;
+	}
+
+	fd = open(script, O_RDONLY);
+	if (fd < 0) {
+		ERROR("%s cannot be opened, exiting..", script);
+		result = -EFAULT;
+		goto create_container_exit;
+	}
+
+	buf = (char *)malloc(sb.st_size);
+	if (!buf) {
+		ERROR("OOM creating buffer for reading %s of %ld bytes",
+		      script, sb.st_size);
+		result =  -ENOMEM;
+		goto create_container_exit;
+	}
+
+	ssize_t n = read(fd, buf, sb.st_size);
+	if (n != sb.st_size) {
+		ERROR("Script %s cannot be read, return value %ld != %ld",
+		      script, n, sb.st_size);
+		result = -EFAULT;
+		goto create_container_exit;
+	}
+
+	char *name = dict_get_value(&img->properties, "name");
+
+	TRACE("DOCKER CREATE CONTAINER");
+
+	result = docker_container_create(name, buf);
+
+create_container_exit:
+	free(script);
+	free(buf);
+	if (fd > 0) close(fd);
+
+	return result;
+
+}
+
+/*
+ * Implementation DELETE /container/{id}
+ */
+static int docker_query(struct img_type *img, void *data, docker_fn fn)
+{
+	struct script_handler_data *script_data = data;
+	/*
+	 * Call only in case of postinstall
+	 */
+	if (!script_data || script_data->scriptfn != POSTINSTALL)
+		return 0;
+
+	char *name = dict_get_value(&img->properties, "name");
+
+	if (!name) {
+		ERROR("DELETE container: name is missing, it is mandatory");
+		return -EINVAL;
+	}
+
+	return fn(name);
+}
+
+static int docker_delete_container(struct img_type *img, void *data)
+{
+	return docker_query(img, data, docker_container_remove);
+}
+
+static int docker_delete_image(struct img_type *img, void *data)
+{
+	return docker_query(img, data, docker_remove_image);
+}
+
+static int container_start(struct img_type *img, void *data)
+{
+	return docker_query(img, data, docker_container_start);
+}
+
+
+static int container_stop(struct img_type *img, void *data)
+{
+	return docker_query(img, data, docker_container_stop);
+}
+
+__attribute__((constructor))
+void docker_loadimage_handler(void)
+{
+	register_handler("docker_imageload", docker_install_image,
+				IMAGE_HANDLER, NULL);
+}
+
+__attribute__((constructor))
+void docker_deleteimage_handler(void)
+{
+	register_handler("docker_imagedelete", docker_delete_image,
+				SCRIPT_HANDLER | NO_DATA_HANDLER, NULL);
+}
+
+__attribute__((constructor))
+void docker_createcontainer_handler(void)
+{
+	register_handler("docker_containercreate", docker_create_container,
+				SCRIPT_HANDLER, NULL);
+}
+
+__attribute__((constructor))
+void docker_deletecontainer_handler(void)
+{
+	register_handler("docker_containerdelete", docker_delete_container,
+				SCRIPT_HANDLER | NO_DATA_HANDLER, NULL);
+}
+
+__attribute__((constructor))
+void docker_container_start_handler(void)
+{
+	register_handler("docker_containerstart", container_start,
+				SCRIPT_HANDLER | NO_DATA_HANDLER, NULL);
+}
+
+__attribute__((constructor))
+void docker_container_stop_handler(void)
+{
+	register_handler("docker_containerstart", container_stop,
+				SCRIPT_HANDLER | NO_DATA_HANDLER, NULL);
+}