From f79ab5b159a2deeacb1426a29fc528795bff2b84 Mon Sep 17 00:00:00 2001 From: perkss Date: Mon, 15 Dec 2025 20:39:58 +0000 Subject: [PATCH] Load image from tar support --- include/docker_client.hh | 12 ++++-- include/load_image_cmd.hh | 48 ++++++++++++++++++++++++ include/webtarget.hh | 1 + src/CMakeLists.txt | 2 + src/curl_docker_http_client.cc | 12 +++++- src/docker_client.cc | 8 ++++ src/load_image_cmd.cc | 19 ++++++++++ src/load_image_cmd_exec.cc | 35 +++++++++++++++++ src/load_image_cmd_exec.hh | 23 ++++++++++++ src/webtarget.cc | 18 +++++++++ tests/CMakeLists.txt | 3 +- tests/load_image_cmd_test.cc | 68 ++++++++++++++++++++++++++++++++++ 12 files changed, 242 insertions(+), 7 deletions(-) create mode 100644 include/load_image_cmd.hh create mode 100644 src/load_image_cmd.cc create mode 100644 src/load_image_cmd_exec.cc create mode 100644 src/load_image_cmd_exec.hh create mode 100644 tests/load_image_cmd_test.cc diff --git a/include/docker_client.hh b/include/docker_client.hh index 287d5bb..cc2d55b 100644 --- a/include/docker_client.hh +++ b/include/docker_client.hh @@ -4,14 +4,15 @@ #include "create_container_cmd.hh" #include "events_cmd.hh" #include "info_cmd.hh" +#include "inspect_container_cmd.hh" +#include "inspect_image_cmd.hh" +#include "load_image_cmd.hh" #include "ping_cmd.hh" #include "pull_image_cmd.hh" -#include "remove_image_cmd.hh" #include "remove_container_cmd.hh" +#include "remove_image_cmd.hh" #include "start_container_cmd.hh" #include "stop_container_cmd.hh" -#include "inspect_container_cmd.hh" -#include "inspect_image_cmd.hh" #include "version_cmd.hh" namespace dockercpp { @@ -36,13 +37,16 @@ class DockerClient { std::shared_ptr pullImageCmd(std::string repository); + std::shared_ptr loadImageCmd(std::string tarContents); + std::shared_ptr infoCmd(); std::shared_ptr removeImageCmd(std::string image); std::shared_ptr inspectImageCmd(std::string image); - std::shared_ptr removeContainerCmd(std::string id); + std::shared_ptr removeContainerCmd( + std::string id); std::shared_ptr eventsCmd(); }; diff --git a/include/load_image_cmd.hh b/include/load_image_cmd.hh new file mode 100644 index 0000000..e76ab12 --- /dev/null +++ b/include/load_image_cmd.hh @@ -0,0 +1,48 @@ +#ifndef INCLUDE_LOAD_IMAGE_CMD_HPP +#define INCLUDE_LOAD_IMAGE_CMD_HPP + +#include +#include + +#include "abstr_sync_docker_cmd_exec.hh" +#include "synch_docker_cmd.hh" + +namespace dockercpp::command { + +class LoadImageCmd : public SynchDockerCmd, + public std::enable_shared_from_this { + public: + explicit LoadImageCmd(const std::string& tarContents); + + std::string getTarContents(); + + ~LoadImageCmd() {} + + private: + std::string m_tar; +}; + +namespace load { +class Exec : public exec::DockerCmdSyncExec { + public: + ~Exec() {} +}; +} // namespace load + +class LoadImageCmdImpl : public LoadImageCmd, + public AbstrDockerCmd { + public: + LoadImageCmdImpl(std::unique_ptr exec, const std::string& tar); + + std::string exec() override; + + void close() override; + ~LoadImageCmdImpl(); + + private: + std::string m_tar; +}; + +} // namespace dockercpp::command + +#endif /* INCLUDE_LOAD_IMAGE_CMD_HPP */ diff --git a/include/webtarget.hh b/include/webtarget.hh index a27315d..5e2094e 100644 --- a/include/webtarget.hh +++ b/include/webtarget.hh @@ -14,6 +14,7 @@ class InvocationBuilder { std::string get(); std::string post(std::string &json); + std::pair post_with_code(std::string &body); bool deletehttp(); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c1020cc..52c7b65 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,8 @@ add_library(docker_cpp_client stats_container_cmd.cc update_container_cmd.cc webtarget.cc + load_image_cmd.cc + load_image_cmd_exec.cc abstr_sync_docker_cmd_exec.cc remove_container_cmd.cc remove_container_cmd_exec.cc diff --git a/src/curl_docker_http_client.cc b/src/curl_docker_http_client.cc index 5093cf7..985d688 100644 --- a/src/curl_docker_http_client.cc +++ b/src/curl_docker_http_client.cc @@ -139,12 +139,20 @@ http::Response postcurl(Request &request) { } spdlog::info("About to send post: {}", request.body().c_str()); + // Ensure binary-safe upload for arbitrary body contents (e.g., tar archives) curl_easy_setopt(curl, CURLOPT_POSTFIELDS, fields.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, + static_cast(fields.size())); curl_slist *headers = NULL; headers = curl_slist_append(headers, "Accept: application/json"); - headers = curl_slist_append(headers, "Content-Type: application/json"); - headers = curl_slist_append(headers, "charset: utf-8"); + // If posting a tar archive for image load, use application/x-tar + if (request.path().find("/images/load") != std::string::npos) { + headers = curl_slist_append(headers, "Content-Type: application/x-tar"); + } else { + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, "charset: utf-8"); + } std::string response_string; std::string header_string; diff --git a/src/docker_client.cc b/src/docker_client.cc index 5040cc2..e63b1d9 100644 --- a/src/docker_client.cc +++ b/src/docker_client.cc @@ -21,6 +21,8 @@ #include "version_cmd.hh" #include "version_cmd_exec.hh" #include "events_cmd_exec.hh" +#include "load_image_cmd.hh" +#include "load_image_cmd_exec.hh" namespace dockercpp { @@ -69,6 +71,12 @@ std::shared_ptr DockerClient::pullImageCmd( repository); } +std::shared_ptr DockerClient::loadImageCmd( + std::string tarContents) { + return std::make_shared( + std::move(std::make_unique()), tarContents); +} + std::shared_ptr DockerClient::infoCmd() { return std::make_shared( std::move(std::make_unique())); diff --git a/src/load_image_cmd.cc b/src/load_image_cmd.cc new file mode 100644 index 0000000..f7e31c9 --- /dev/null +++ b/src/load_image_cmd.cc @@ -0,0 +1,19 @@ +#include "load_image_cmd.hh" + +namespace dockercpp::command { + +LoadImageCmd::LoadImageCmd(const std::string& tarContents) : m_tar(tarContents) {} + +std::string LoadImageCmd::getTarContents() { return m_tar; } + +LoadImageCmdImpl::LoadImageCmdImpl(std::unique_ptr exec, + const std::string& tar) + : AbstrDockerCmd(std::move(exec)), LoadImageCmd(tar), m_tar(tar) {} + +std::string LoadImageCmdImpl::exec() { return m_execution->exec(shared_from_this()); } + +void LoadImageCmdImpl::close() {} + +LoadImageCmdImpl::~LoadImageCmdImpl() {} + +} // namespace dockercpp::command diff --git a/src/load_image_cmd_exec.cc b/src/load_image_cmd_exec.cc new file mode 100644 index 0000000..de79d53 --- /dev/null +++ b/src/load_image_cmd_exec.cc @@ -0,0 +1,35 @@ +#include "load_image_cmd_exec.hh" + +#include + +#include "abstr_sync_docker_cmd_exec.hh" +#include "docker_exception.hh" +#include "webtarget.hh" + +namespace dockercpp::command::exec { + +LoadImageCmdExec::LoadImageCmdExec() + : AbstrSyncDockerCmdExec(), + load::Exec() {} + +std::string LoadImageCmdExec::exec( + std::shared_ptr command) { + return execute(command); +} + +std::string LoadImageCmdExec::execute( + std::shared_ptr command) { + core::WebTarget webResource = m_webTarget->path("/images/load"); + + auto body = command->getTarContents(); + + auto [response, statusCode] = webResource.request().post_with_code(body); + + if (statusCode != 200) { + throw dockercpp::DockerException("Error loading image", statusCode, response); + } + + return response; +} + +} // namespace dockercpp::command::exec diff --git a/src/load_image_cmd_exec.hh b/src/load_image_cmd_exec.hh new file mode 100644 index 0000000..230c6d2 --- /dev/null +++ b/src/load_image_cmd_exec.hh @@ -0,0 +1,23 @@ +#ifndef LOAD_IMAGE_CMD_EXEC_HH +#define LOAD_IMAGE_CMD_EXEC_HH + +#include "abstr_sync_docker_cmd_exec.hh" +#include "load_image_cmd.hh" + +namespace dockercpp::command::exec { + +class LoadImageCmdExec + : public AbstrSyncDockerCmdExec, + public load::Exec { + public: + LoadImageCmdExec(); + + std::string exec(std::shared_ptr command) override; + + std::string execute(std::shared_ptr command) override; + ~LoadImageCmdExec() {} +}; + +} // namespace dockercpp::command::exec + +#endif diff --git a/src/webtarget.cc b/src/webtarget.cc index e39d7d5..3c0bb39 100644 --- a/src/webtarget.cc +++ b/src/webtarget.cc @@ -45,6 +45,24 @@ std::string InvocationBuilder::post(std::string& json) { return client.execute(request).getBody(); } +std::pair InvocationBuilder::post_with_code(std::string& body) { + spdlog::info("post with code requested"); + transport::http::Request request = + transport::http::Request::make() + .withMethod(transport::http::Request::Method::POST) + .withBody(body) + .withPath(m_path); + + dockercpp::transport::http::CurlDockerHttpClient client = + dockercpp::transport::http::CurlDockerHttpClient::make() + .withDockerHost("") + .withConnectTimeout(10) + .withReadTimeout(10); + + auto response = client.execute(request); + return {response.getBody(), response.getStatusCode()}; +} + bool InvocationBuilder::deletehttp() { auto [response, code] = deletehttp_with_code(); return code == 200; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 59f8969..7749562 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,7 +1,7 @@ include(FetchContent) FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) @@ -22,6 +22,7 @@ add_executable( update_container_cmd_test.cc inspect_container_response_test.cc remove_image_cmd_exec_test.cc + load_image_cmd_test.cc version_test.cc ) diff --git a/tests/load_image_cmd_test.cc b/tests/load_image_cmd_test.cc new file mode 100644 index 0000000..0f755aa --- /dev/null +++ b/tests/load_image_cmd_test.cc @@ -0,0 +1,68 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "docker_client.hh" +#include "inspect_image_cmd.hh" + +namespace dockercpp::command::test { + +class LoadImageCmdIT : public ::testing::Test { + protected: + void SetUp() override { dockerClient = std::make_unique(); } + + void TearDown() override { dockerClient.reset(); } + + std::unique_ptr dockerClient; +}; + +TEST_F(LoadImageCmdIT, loadImageFromTar) { + // Build a tiny image, save it to tar, remove it, then load via API + const std::string tmpdir = "/tmp/docker_cpp_load_test"; + system((std::string("rm -rf ") + tmpdir).c_str()); + system((std::string("mkdir -p ") + tmpdir).c_str()); + + // Write a minimal Dockerfile + std::ofstream df(tmpdir + "/Dockerfile"); + df << "FROM busybox\nCMD [\"/bin/sh\"]\n"; + df.close(); + + // Build image + int rc = system((std::string("docker build -t docker-cpp/load:1.0 ") + tmpdir + " > /dev/null").c_str()); + ASSERT_EQ(rc, 0); + + // Inspect to get image id + auto info = dockerClient->inspectImageCmd("docker-cpp/load:1.0")->exec(); + ASSERT_FALSE(info.id.empty()); + std::string imageId = info.id; + + // Save to tar + std::string tarPath = "/tmp/docker_cpp_load_image.tar"; + rc = system((std::string("docker save docker-cpp/load:1.0 -o ") + tarPath).c_str()); + ASSERT_EQ(rc, 0); + + // Remove image + ASSERT_NO_THROW(dockerClient->removeImageCmd("docker-cpp/load:1.0")->withForce(true).exec()); + + // Read tar into string + std::ifstream ifs(tarPath, std::ios::binary); + ASSERT_TRUE(ifs.good()); + std::string tarContents((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + ifs.close(); + + // Load via our API + ASSERT_NO_THROW(dockerClient->loadImageCmd(tarContents)->exec()); + + // Allow Docker to register the image + std::this_thread::sleep_for(std::chrono::seconds(3)); + + // Verify image is present + auto info2 = dockerClient->inspectImageCmd("docker-cpp/load:1.0")->exec(); + EXPECT_FALSE(info2.id.empty()); +} + +} // namespace dockercpp::command::test