Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
BUILD_JOBS?=4
BUILD_DIR?=build
ECR_NAME?=laps-relay
HEALTHCHECK_ECR_NAME?=healthcheck
CLANG_FORMAT=clang-format -i

PROJECTNAME := laps

.PHONY: all clean cclean format docs
.PHONY: all clean cclean format docs image-healthcheck image-healthcheck-amd64 image-healthcheck-arm64 publish-healthcheck publish-healthcheck-amd64 publish-healthcheck-arm64

# -----------------------------------------
# Help/other targets
Expand Down Expand Up @@ -73,7 +74,7 @@ docker-prep:
@echo "Prep normally requires submodule update, but skipping considering possible custom changes"
# @git submodule update --init --recursive

## image-amd64: Create AMD64 docker image
## image-amd64: Create AMD64 docker image
image-amd64: docker-prep
@docker buildx build --progress=plain \
--output type=docker --platform linux/amd64 \
Expand Down Expand Up @@ -110,6 +111,21 @@ image-arm64: docker-prep
--output type=docker --platform linux/arm64 \
-f Dockerfile -t quicr/${ECR_NAME}:${DOCKER_TAG}-arm64 .

## image-healthcheck: Create AMD64 and ARM64 relay health-check docker images
image-healthcheck: image-healthcheck-amd64 image-healthcheck-arm64

## image-healthcheck-amd64: Create AMD64 relay health-check docker image
image-healthcheck-amd64: docker-prep
@docker buildx build --progress=plain \
--output type=docker --platform linux/amd64 \
-f healthcheck.Dockerfile -t quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-amd64 .

## image-healthcheck-arm64: Create ARM64 relay health-check docker image
image-healthcheck-arm64: docker-prep
@docker buildx build --progress=plain \
--output type=docker --platform linux/arm64 \
-f healthcheck.Dockerfile -t quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-arm64 .

ecr-login:
@echo "==> Logging into ECR using environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY"
@docker run --rm \
Expand All @@ -134,4 +150,21 @@ publish-image-arm64: ecr-login
@echo "==> Pushing image 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${ECR_NAME}:${DOCKER_TAG}-arm64 to ECR"
@docker push 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${ECR_NAME}:${DOCKER_TAG}-arm64


## publish-healthcheck: Publish amd64 and arm64 relay health-check images to ECR
publish-healthcheck: publish-healthcheck-amd64 publish-healthcheck-arm64

## publish-healthcheck-amd64: Publish amd64 relay health-check image to ECR
publish-healthcheck-amd64: ecr-login
@echo "==> Tagging docker image to 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-amd64"
@docker tag quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-amd64 \
017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-amd64
@echo "==> Pushing image 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-amd64 to ECR"
@docker push 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-amd64

## publish-healthcheck-arm64: Publish arm64 relay health-check image to ECR
publish-healthcheck-arm64: ecr-login
@echo "==> Tagging docker image to 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-arm64"
@docker tag quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-arm64 \
017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-arm64
@echo "==> Pushing image 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-arm64 to ECR"
@docker push 017125485914.dkr.ecr.us-west-1.amazonaws.com/quicr/${HEALTHCHECK_ECR_NAME}:${DOCKER_TAG}-arm64
56 changes: 56 additions & 0 deletions healthcheck.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#---------------------------------------------------------------------
# LAPS Relay health-check HTTP image
#---------------------------------------------------------------------

FROM alpine:3.20.3 AS builder

RUN apk add --no-cache \
alpine-sdk \
bash \
ca-certificates \
clang \
cmake \
curl \
linux-headers \
lld \
openssl-dev \
python3 \
tcsh

WORKDIR /ws

COPY ./CMakeLists.txt ./
COPY ./version_config.h.in ./
COPY ./dependencies ./dependencies
COPY ./src ./src

ENV CFLAGS="-Wno-error=stringop-overflow"
ENV CXXFLAGS="-Wno-error=stringop-overflow -fpermissive -Wno-error=pedantic"

RUN cmake -S . -B build \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DBUILD_TESTING=OFF \
-DLAPS_BUILD_TESTS=OFF \
-DCMAKE_BUILD_TYPE=Release
RUN cmake --build build --target relay_health_check -j "$(nproc)"
RUN cp build/src/relay_health_check /usr/local/bin/.

FROM alpine:3.20.3

RUN apk add --no-cache libstdc++ python3

COPY --from=builder /usr/local/bin/relay_health_check /usr/local/bin/.
COPY ./scripts/relay_health_http.py /usr/local/bin/relay_health_http.py

RUN addgroup -S laps
RUN adduser -D -S -S -G laps laps

USER laps
WORKDIR /home/laps

EXPOSE 8080/tcp

ENV RELAY_HEALTH_HTTP_HOST=0.0.0.0
ENV RELAY_HEALTH_HTTP_PORT=8080

CMD ["python3", "/usr/local/bin/relay_health_http.py"]
105 changes: 105 additions & 0 deletions scripts/relay_health_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 Cisco Systems
# SPDX-License-Identifier: BSD-2-Clause

from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import os
import subprocess


CHECK_BIN = os.environ.get("RELAY_HEALTH_CHECK_BIN", "/usr/local/bin/relay_health_check")
HTTP_HOST = os.environ.get("RELAY_HEALTH_HTTP_HOST", "0.0.0.0")
HTTP_PORT = int(os.environ.get("RELAY_HEALTH_HTTP_PORT", "8080"))
CHECK_TIMEOUT_SECONDS = float(os.environ.get("RELAY_HEALTH_HTTP_TIMEOUT_SECONDS", "10"))


def _to_text(value):
if value is None:
return ""
if isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
return value


def _detail_text(stdout, stderr, returncode):
chunks = []

stdout = _to_text(stdout)
stderr = _to_text(stderr)

stdout_lines = stdout.strip().splitlines()
if stdout_lines and stdout_lines[0] in ("ok", "error"):
stdout_lines = stdout_lines[1:]

stdout_details = "\n".join(stdout_lines).strip()
stderr_details = stderr.strip()

if stdout_details:
chunks.append(stdout_details)
if stderr_details:
chunks.append(stderr_details)

if not chunks:
chunks.append(f"relay_health_check exited with status {returncode}")

return "\n".join(chunks)


def run_health_check():
try:
result = subprocess.run(
[CHECK_BIN],
capture_output=True,
check=False,
text=True,
timeout=CHECK_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout or ""
stderr = exc.stderr or ""
details = _detail_text(stdout, stderr, "timeout")
details = f"relay_health_check timed out after {CHECK_TIMEOUT_SECONDS:g} seconds\n{details}"
return HTTPStatus.SERVICE_UNAVAILABLE, f"error\n\n{details.strip()}\n"
except OSError as exc:
return HTTPStatus.SERVICE_UNAVAILABLE, f"error\n\nfailed to run {CHECK_BIN}: {exc}\n"

if result.returncode == 0:
return HTTPStatus.OK, "ok\n"

details = _detail_text(result.stdout, result.stderr, result.returncode)
return HTTPStatus.SERVICE_UNAVAILABLE, f"error\n\n{details.strip()}\n"


class RelayHealthHandler(BaseHTTPRequestHandler):
server_version = "relay-health-http/1.0"

def do_GET(self):
status, body = run_health_check()
body_bytes = body.encode("utf-8")

self.send_response(status)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body_bytes)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body_bytes)

def do_HEAD(self):
status, body = run_health_check()
body_bytes = body.encode("utf-8")

self.send_response(status)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body_bytes)))
self.send_header("Cache-Control", "no-store")
self.end_headers()

def log_message(self, format, *args):
print(f"{self.address_string()} - {format % args}", flush=True)


if __name__ == "__main__":
server = ThreadingHTTPServer((HTTP_HOST, HTTP_PORT), RelayHealthHandler)
print(f"relay health HTTP server listening on {HTTP_HOST}:{HTTP_PORT}", flush=True)
server.serve_forever()
14 changes: 14 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@ set_target_properties(lapsRelay
CXX_EXTENSIONS ON)

target_compile_definitions(lapsRelay PRIVATE SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG)

add_executable(relay_health_check relay_health_check.cpp)
target_link_libraries(relay_health_check PRIVATE quicr)

target_compile_options(relay_health_check
PRIVATE
$<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>: -Wpedantic -Wextra -Wall>
$<$<CXX_COMPILER_ID:MSVC>: >)

set_target_properties(relay_health_check
PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS OFF)
Loading
Loading