From 5acb7573c57eb2ef9556eaea908378ee15b86eee Mon Sep 17 00:00:00 2001 From: ClausKlein Date: Sun, 6 Aug 2023 11:58:54 +0200 Subject: [PATCH] add docker multistage build Author: Michael Unknown - Updated m.css dependency (#151) - correct hash - eof newline - add boost, make boost work in docker, solve merge conflict - add docker ci/cd - add docker multistage build - solve conflict, add branchto ci - delete old ci file - fix cmake-format - fix errors in CI workflows --- .dockerignore | 6 ++ .github/workflows/docker-cd.yml | 94 ++++++++++++++++++++++++++++++++ .github/workflows/docker-ci.yml | 63 +++++++++++++++++++++ .github/workflows/standalone.yml | 3 + standalone/CMakeLists.txt | 43 ++++++++++++++- standalone/Dockerfile | 21 +++++++ standalone/source/main.cpp | 85 ++++++++++++++--------------- 7 files changed, 268 insertions(+), 47 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-cd.yml create mode 100644 .github/workflows/docker-ci.yml create mode 100644 standalone/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..2e889c47 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +/build* +/.vscode +/cpm_modules +.DS_Store +/.github +/standalone/Dockerfile diff --git a/.github/workflows/docker-cd.yml b/.github/workflows/docker-cd.yml new file mode 100644 index 00000000..0de80730 --- /dev/null +++ b/.github/workflows/docker-cd.yml @@ -0,0 +1,94 @@ +name: run Docker CD + +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + inputs: + tag: + description: "Test scenario tag" + required: true + default: "test" + set-latest-tag: + description: "Also set the 'latest' tag with this run? (y/n)" + required: true + default: "n" + +env: + CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm_modules + +jobs: + build: + name: CD build docker and push to DockerHub + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: handle manual workflow start and prepare docker image tag(s) + id: docker-tags + shell: bash + run: | + if [[ "x${{ github.event.inputs.tag }}" != "x" ]]; then + echo "Workflow started via workflow_dispatch! Parameters: tag=${{ github.event.inputs.tag }}, set-latest-tag=${{ github.event.inputs.set-latest-tag }}" + tag="${{ github.event.inputs.tag }}" + else + echo "Workflow started via push with tag! Complete tag: ${GITHUB_REF:10}" + tag="${GITHUB_REF:11}" + fi + + if [[ "x${{ github.event.inputs.set-latest-tag }}" == "xy" || "x${{ github.event.inputs.tag }}" == "x" ]]; then + tags="${{ secrets.DOCKERHUB_USERNAME }}/greeter-webapi:$tag, ${{ secrets.DOCKERHUB_USERNAME }}/greeter-webapi:latest" + echo "Docker image release tags: $tags" + else + tags="${{ secrets.DOCKERHUB_USERNAME }}/greeter-webapi:$tag" + echo "Docker image release tag: $tags" + fi + + echo ::set-output name=tags::$tags + + # + # configure and build in GitHub CI as smoke test + + # speed up configure step by installing boost as system lib, also use Ninja for faster builds + - name: speed up configure and build + shell: bash + run: sudo apt-get update && sudo apt-get install -y libboost-all-dev ninja-build + + # use GitHub cache to cache dependencies + - uses: actions/cache@v2 + with: + path: "**/cpm_modules" + key: ${{ github.workflow }}-cpm-modules-${{ hashFiles('**/CMakeLists.txt', '**/*.cmake') }} + + - name: configure + run: cmake -S standalone -B build -G Ninja -D CMAKE_BUILD_TYPE=Release + + - name: build + run: cmake --build build + + # + # end GitHub CI local build + + - name: set up Docker Buildx for multi-platform support + uses: docker/setup-buildx-action@v1 + + - name: set up QEMU for multi-platform support + uses: docker/setup-qemu-action@v1 + + - name: login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: build Docker image and push to DockerHub + uses: docker/build-push-action@v2 + with: + file: ./standalone/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.docker-tags.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 00000000..a09ec940 --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,63 @@ +name: run Docker CI + +on: + push: + branches: + - main + - master + - add-docker-build + pull_request: + branches: + - main + - master + +env: + CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm_modules + +jobs: + build: + name: CI build docker + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + # + # configure and build in GitHub CI as smoke test + + # speed up configure step by installing boost as system lib, also use Ninja for faster builds + - name: speed up configure and build + shell: bash + run: sudo apt-get update && sudo apt-get install -y libboost-all-dev ninja-build + + # use GitHub cache to cache dependencies + - uses: actions/cache@v2 + with: + path: "**/cpm_modules" + key: ${{ github.workflow }}-cpm-modules-${{ hashFiles('**/CMakeLists.txt', '**/*.cmake') }} + + - name: configure + run: cmake -S standalone -B build -G Ninja -D CMAKE_BUILD_TYPE=Release + + - name: build + run: cmake --build build + + # + # end GitHub CI local build + + - name: set up Docker Buildx for multi-platform support + uses: docker/setup-buildx-action@v1 + + - name: set up QEMU for multi-platform support + uses: docker/setup-qemu-action@v1 + + # build image but do NOT push to DockerHub + - name: build Docker image + uses: docker/build-push-action@v2 + with: + file: ./standalone/Dockerfile + platforms: linux/amd64,linux/arm64 + push: false + tags: ${{ secrets.DOCKERHUB_USERNAME }}/greeter-webapi:ci + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml index 214bca0d..cac430ca 100644 --- a/.github/workflows/standalone.yml +++ b/.github/workflows/standalone.yml @@ -36,3 +36,6 @@ jobs: run: | cd build ctest --build-config Debug + + - name: run + run: ./build/GreeterStandalone diff --git a/standalone/CMakeLists.txt b/standalone/CMakeLists.txt index 8bcd7de4..3bf9a0e2 100644 --- a/standalone/CMakeLists.txt +++ b/standalone/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.21...3.27) -project(GreeterStandalone LANGUAGES CXX) +project( + GreeterStandalone + DESCRIPTION "A standalone minimal webapi application using the Crow framework" + LANGUAGES CXX +) if(PROJECT_IS_TOP_LEVEL AND CMAKE_BUILD_TYPE STREQUAL "Release") set(CMAKE_SKIP_INSTALL_RULES @@ -19,6 +23,39 @@ include(../cmake/tools.cmake) include(../cmake/CPM.cmake) +#TODO(CK): add_compile_definitions(BOOST_ASIO_NO_DEPRECATED) + +# Crow needs Boost 1.72 and does not provide CPM.cmake integration itself, so we have to get Boost +# first +find_package(Boost 1.72 QUIET) +if(NOT Boost_FOUND) + # Use CPM.cmake to get Boost from the official repo if not provided as system lib + message(STATUS "GreeterStandalone: Boost system lib NOT found") + CPMAddPackage( + NAME Boost + GITHUB_REPOSITORY boostorg/boost + GIT_TAG boost-1.78.0 + VERSION 1.78.0 + ) + # Ugly workaround: Boost cmake support is still experimental, the Boost::boost target is not + # provided if downloaded via FetchContent_declare / CPM.cmake. Crow uses it for asio, so we fake + # the Boost:boost target as asio + if(NOT TARGET Boost::boost) + add_library(Boost::boost INTERFACE IMPORTED) + target_link_libraries(Boost::boost INTERFACE Boost::asio) + endif() +else() + message(STATUS "GreeterStandalone: Boost system lib found") +endif() +# add Crow +CPMAddPackage( + NAME Crow + GITHUB_REPOSITORY CrowCpp/Crow + GIT_TAG v1.0+5 + VERSION 1.0.0 + OPTIONS "CROW_INSTALL OFF" +) + CPMAddPackage( GITHUB_REPOSITORY jarro2783/cxxopts VERSION 3.1.1 @@ -26,6 +63,7 @@ CPMAddPackage( OPTIONS "CXXOPTS_BUILD_EXAMPLES NO" "CXXOPTS_BUILD_TESTS NO" "CXXOPTS_ENABLE_INSTALL YES" ) +# get the Greeter lib CPMAddPackage( NAME Greeter SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/.. @@ -40,7 +78,8 @@ add_executable(${PROJECT_NAME} ${sources}) if(CMAKE_DEBUG_POSTFIX) set_property(TARGET ${PROJECT_NAME} PROPERTY DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) endif() -target_link_libraries(${PROJECT_NAME} Greeter::Greeter cxxopts::cxxopts) +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) +target_link_libraries(${PROJECT_NAME} Greeter::Greeter cxxopts::cxxopts Crow::Crow) set_target_properties(${PROJECT_NAME} PROPERTIES UNITY_BUILD ${OPTION_ENABLE_UNITY}) # --- Test it --- diff --git a/standalone/Dockerfile b/standalone/Dockerfile new file mode 100644 index 00000000..61cd9604 --- /dev/null +++ b/standalone/Dockerfile @@ -0,0 +1,21 @@ +# build +FROM buildpack-deps:bullseye as webapp-build +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + cmake \ + ninja-build \ + # configure/build with Boost as system lib - this should be orders of magnitude faster to configure than + # downloading via CPM.cmake while Boost's CMake support is still experimental + libboost-all-dev \ + ; +COPY . . +RUN cmake -S standalone -B build -G Ninja -D CMAKE_BUILD_TYPE=Release +RUN cmake --build build + + +# deploy +FROM debian:bullseye-slim as webapp-run +WORKDIR /app +COPY --from=webapp-build /build/GreeterStandalone . +CMD ["./GreeterStandalone"] diff --git a/standalone/source/main.cpp b/standalone/source/main.cpp index 938a3864..6c3f7283 100644 --- a/standalone/source/main.cpp +++ b/standalone/source/main.cpp @@ -1,53 +1,48 @@ +#include #include #include -#include #include #include #include -auto main(int argc, char** argv) -> int { - const std::unordered_map languages{ - {"en", greeter::LanguageCode::EN}, - {"de", greeter::LanguageCode::DE}, - {"es", greeter::LanguageCode::ES}, - {"fr", greeter::LanguageCode::FR}, - }; - - cxxopts::Options options(*argv, "A program to welcome the world!"); - - std::string language; - std::string name; - - // clang-format off - options.add_options() - ("h,help", "Show help") - ("v,version", "Print the current version number") - ("n,name", "Name to greet", cxxopts::value(name)->default_value("World")) - ("l,lang", "Language code to use", cxxopts::value(language)->default_value("en")) - ; - // clang-format on - - auto result = options.parse(argc, argv); - - if (result["help"].as()) { - std::cout << options.help() << std::endl; - return 0; - } - - if (result["version"].as()) { - std::cout << "Greeter, version " << GREETER_VERSION << std::endl; - return 0; - } - - auto langIt = languages.find(language); - if (langIt == languages.end()) { - std::cerr << "unknown language code: " << language << std::endl; - return 1; - } - - greeter::Greeter greeter(name); - std::cout << greeter.greet(langIt->second) << std::endl; - - return 0; +int main() { + crow::SimpleApp app; + + CROW_ROUTE(app, "/hello") + ([](const crow::request& req) { + // check params + std::cout << "Params: " << req.url_params << "\n"; + std::cout << "The key 'language' was " + << (req.url_params.get("language") == nullptr ? "not " : "") << "found.\n"; + + if (req.url_params.get("language") == nullptr) { + // return bad request + return crow::response(400, "please provide a 'language' argument"); + } + const auto language = req.url_params.get("language"); + + // see if langauge was found + const std::unordered_map languages{ + {"en", greeter::LanguageCode::EN}, + {"de", greeter::LanguageCode::DE}, + {"es", greeter::LanguageCode::ES}, + {"fr", greeter::LanguageCode::FR}, + }; + const auto langIt = languages.find(language); + if (langIt == languages.end()) { + // return bad request + std::cout << "Greeting for language '" << language << "' is not available\n"; + return crow::response(400, "language not recognized"); + } + + const greeter::Greeter greeter("Crow & Greeter"); + std::cout << "Greeting for language '" << language << "' is available, returning message\n"; + const auto message = greeter.greet(langIt->second); + + crow::json::wvalue ret({{"answer", message}}); + return crow::response(200, ret); + }); + + app.port(3080).multithreaded().run(); }