Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add docker multistage build #150

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
@@ -0,0 +1,6 @@
/build*
/.vscode
/cpm_modules
.DS_Store
/.github
/standalone/Dockerfile
94 changes: 94 additions & 0 deletions .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
63 changes: 63 additions & 0 deletions .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
2 changes: 1 addition & 1 deletion .github/workflows/standalone.yml
Expand Up @@ -32,4 +32,4 @@ jobs:
run: cmake --build build -j4

- name: run
run: ./build/Greeter
run: ./build/GreeterStandalone
2 changes: 1 addition & 1 deletion .gitignore
@@ -1,4 +1,4 @@
/build*
/.vscode
/cpm_modules
.DS_Store
.DS_Store
44 changes: 36 additions & 8 deletions standalone/CMakeLists.txt
@@ -1,6 +1,10 @@
cmake_minimum_required(VERSION 3.14...3.22)

project(GreeterStandalone LANGUAGES CXX)
project(
GreeterStandalone
DESCRIPTION "A standalone minimal webapi application using the Crow framework"
LANGUAGES CXX
)

# --- Import tools ----

Expand All @@ -10,20 +14,44 @@ include(../cmake/tools.cmake)

include(../cmake/CPM.cmake)

# Crow needs Boost 1.64 and does not provide CPM.cmake integration itself, so we have to get Boost
# first
find_package(Boost 1.64 QUIET)
Comment on lines 15 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dockerfile at this level does not work!
We need access to ../cmake and ../CMakeLists.txt while build standalone

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(
GITHUB_REPOSITORY jarro2783/cxxopts
VERSION 3.0.0
OPTIONS "CXXOPTS_BUILD_EXAMPLES NO" "CXXOPTS_BUILD_TESTS NO" "CXXOPTS_ENABLE_INSTALL YES"
NAME Crow
GITHUB_REPOSITORY CrowCpp/Crow
GIT_TAG v1.0+1
VERSION 1.0.1
OPTIONS "CROW_INSTALL OFF"
)

# get the Greeter lib
CPMAddPackage(NAME Greeter SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
Comment on lines +48 to 49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is something like add_subdirectory(.. Greeter)


# ---- Create standalone executable ----

file(GLOB sources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp)

add_executable(${PROJECT_NAME} ${sources})

set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17 OUTPUT_NAME "Greeter")

target_link_libraries(${PROJECT_NAME} Greeter::Greeter cxxopts)
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_11)
target_link_libraries(${PROJECT_NAME} PRIVATE Greeter::Greeter Crow::Crow)
21 changes: 21 additions & 0 deletions 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configure error here!

RUN cmake --build build


# deploy
FROM debian:bullseye-slim as webapp-run
WORKDIR /app
COPY --from=webapp-build /build/GreeterStandalone .
CMD ["./GreeterStandalone"]
85 changes: 40 additions & 45 deletions standalone/source/main.cpp
@@ -1,53 +1,48 @@
#include <crow.h>
#include <greeter/greeter.h>
#include <greeter/version.h>

#include <cxxopts.hpp>
#include <iostream>
#include <string>
#include <unordered_map>

auto main(int argc, char** argv) -> int {
const std::unordered_map<std::string, greeter::LanguageCode> 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<bool>()) {
std::cout << options.help() << std::endl;
return 0;
}

if (result["version"].as<bool>()) {
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<std::string, greeter::LanguageCode> 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();
}