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
51 changes: 51 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Git
.git
.gitignore

# Python
__pycache__
*.py[cod]
*.pyo
*.pyd
.Python
*.egg-info
.eggs
dist
build
*.egg
.venv
venv
env

# IDE
.vscode
.idea
*.swp
*.swo

# Testing/Coverage
.pytest_cache
.coverage
htmlcov
coverage.xml
*.cover

# Node/Frontend
node_modules
client/node_modules
client/build

# Docs and misc
docs
*.md
!README.md
*.log
.DS_Store

# Local data
data/
logs/
__blobstorage__

# Deploy folder (avoid recursive copy)
deploy/
36 changes: 36 additions & 0 deletions deploy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft Corporation.

Check failure

Code scanning / Trivy

Image user should not be 'root' High

Artifact: deploy/Dockerfile
Type: dockerfile
Vulnerability DS002
Severity: HIGH
Message: Specify at least 1 USER command in Dockerfile with non-root user as argument
Link: DS002
# Licensed under the MIT License.

# Stage 1: Build stage
FROM mcr.microsoft.com/azurelinux/base/python:3.12 as builder
WORKDIR /app
RUN tdnf update -y && tdnf install -y \
gcc \
libffi-devel \
&& tdnf clean all
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# Stage 2: Startup stage
FROM mcr.microsoft.com/azurelinux/base/python:3.12
WORKDIR /app
RUN tdnf update -y && tdnf install -y \
openssh-clients \
git \
curl \
util-linux \
&& tdnf clean all \
&& mkdir -p /app/data /app/logs /root/.ssh \
&& chmod 700 /root/.ssh
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY src/ ./src/
COPY docs/ ./docs/
COPY WORKSPACES/ ./WORKSPACES/
RUN mkdir -p /app/data
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/healthz || exit 1
EXPOSE 8000
CMD ["uvicorn", "src.api.app:app", "--host", "0.0.0.0", "--port", "8000"]
44 changes: 44 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

services:
sap-qa-service:
build:
context: ..
dockerfile: deploy/Dockerfile
container_name: sap-qa-service
restart: unless-stopped
ports:
- "8000:8000"
environment:
- LOG_FORMAT=${LOG_FORMAT:-json}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- SCHEDULER_CHECK_INTERVAL=${SCHEDULER_CHECK_INTERVAL:-60}
- PYTHONPATH=/app
volumes:
- sap-qa-data:/app/data
- ssh-keys:/root/.ssh:ro
- ../WORKSPACES:/app/WORKSPACES
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
Comment on lines +22 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Point healthcheck at /healthz

The compose healthcheck hits /health, but the API only registers /healthz in src/api/routes/health.py. With curl -f, this will return 404 and mark the container unhealthy even when the service is up, causing false negatives and potential restarts in Docker Compose deployments.

Useful? React with 👍 / 👎.

interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
- sap-qa-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

volumes:
sap-qa-data:
driver: local
ssh-keys:
driver: local

networks:
sap-qa-network:
driver: bridge
153 changes: 153 additions & 0 deletions scripts/container_setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/bin/bash

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/utils.sh"
set_output_context

PROJECT_ROOT="$(dirname "$script_dir")"
DEPLOY_DIR="$PROJECT_ROOT/deploy"

# Check and install Docker if needed
ensure_docker() {
if ! command_exists docker; then
log "INFO" "Docker not found. Installing..."
install_docker
else
log "INFO" "Docker is already installed."
fi

if ! docker compose version &> /dev/null; then
log "ERROR" "Docker Compose plugin not available."
log "INFO" "Attempting to install Docker Compose plugin..."
install_docker
fi

if ! docker info &> /dev/null 2>&1; then
log "INFO" "Starting Docker daemon..."
if command_exists systemctl && systemctl is-system-running &>/dev/null; then
sudo systemctl start docker
else
sudo service docker start
fi
fi

if command_exists systemctl && systemctl is-system-running &>/dev/null; then
log "INFO" "Enabling Docker to start on system boot..."
sudo systemctl enable docker 2>/dev/null || true
fi
}

# Build and start the container
deploy_container() {
log "INFO" "=== SAP QA Service Deployment ==="

if [[ ! -d "$DEPLOY_DIR" ]]; then
log "ERROR" "Deploy directory not found: $DEPLOY_DIR"
exit 1
fi

check_file_exists "$DEPLOY_DIR/docker-compose.yml" "docker-compose.yml not found in $DEPLOY_DIR"
check_file_exists "$DEPLOY_DIR/Dockerfile" "Dockerfile not found in $DEPLOY_DIR"

log "INFO" "Creating data directories..."
mkdir -p "$PROJECT_ROOT/data/jobs/history"

log "INFO" "Stopping existing container (if any)..."
docker compose -f "$DEPLOY_DIR/docker-compose.yml" down 2>/dev/null || true

log "INFO" "Building Docker image..."
docker compose -f "$DEPLOY_DIR/docker-compose.yml" build

log "INFO" "Starting SAP QA service..."
docker compose -f "$DEPLOY_DIR/docker-compose.yml" up -d

log "INFO" "Waiting for service to be healthy..."
sleep 5

if docker compose -f "$DEPLOY_DIR/docker-compose.yml" ps | grep -q "Up"; then
log "INFO" "=== Deployment Successful ==="
echo ""
echo "SAP QA Service is running at: http://localhost:8000"
echo "Health check: http://localhost:8000/health"
echo ""
echo "The service will auto-restart on system reboot."
echo ""
echo "Commands:"
echo " View logs: docker compose -f $DEPLOY_DIR/docker-compose.yml logs -f"
echo " Stop: docker compose -f $DEPLOY_DIR/docker-compose.yml down"
echo " Restart: docker compose -f $DEPLOY_DIR/docker-compose.yml restart"
echo ""

if command_exists curl; then
curl -s http://localhost:8000/health && echo ""
fi
else
log "ERROR" "=== Deployment Failed ==="
log "ERROR" "Check logs: docker compose -f $DEPLOY_DIR/docker-compose.yml logs"
exit 1
fi
}

# Stop the container
stop_container() {
log "INFO" "Stopping SAP QA service..."
docker compose -f "$DEPLOY_DIR/docker-compose.yml" down
log "INFO" "Service stopped."
}

# Show container status
status_container() {
docker compose -f "$DEPLOY_DIR/docker-compose.yml" ps
}

# Show container logs
logs_container() {
docker compose -f "$DEPLOY_DIR/docker-compose.yml" logs -f
}

# Main entry point
main() {
local command="${1:-deploy}"

case "$command" in
deploy|start)
ensure_docker
deploy_container
;;
stop)
stop_container
;;
restart)
stop_container
deploy_container
;;
status)
status_container
;;
logs)
logs_container
;;
install-docker)
install_docker
;;
*)
echo "Usage: $0 {deploy|start|stop|restart|status|logs|install-docker}"
echo ""
echo "Commands:"
echo " deploy Install Docker (if needed) and start the service"
echo " start Same as deploy"
echo " stop Stop the service"
echo " restart Restart the service"
echo " status Show service status"
echo " logs Show service logs (follow mode)"
echo " install-docker Install Docker only"
exit 1
;;
esac
}

main "$@"
65 changes: 65 additions & 0 deletions scripts/utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,68 @@ install_packages() {
log "INFO" "All required packages are already installed."
fi
}


# Install Docker based on distribution
install_docker() {
log "INFO" "Installing Docker..."

detect_distro

case "$DISTRO_FAMILY" in
debian)
# Install prerequisites
sudo apt update -y
sudo apt install -y ca-certificates curl gnupg lsb-release

# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/$DISTRO/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Set up the repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$DISTRO \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine
sudo apt update -y
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
;;
rhel)
# Install prerequisites
sudo $PKG_INSTALL yum-utils

# Add Docker repository
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# Install Docker Engine
sudo $PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
;;
suse)
# Install Docker from SUSE repositories
sudo zypper install -y docker docker-compose
;;
*)
log "ERROR" "Unsupported distribution for Docker installation: $DISTRO_FAMILY"
exit 1
;;
esac

# Start and enable Docker (handle both systemd and SysV init)
if command_exists systemctl && systemctl is-system-running &>/dev/null; then
sudo systemctl start docker || true
sudo systemctl enable docker || true
else
# Fallback to SysV init (for WSL, containers, etc.)
sudo service docker start || true
fi

# Add current user to docker group
if ! groups | grep -q docker; then
sudo usermod -aG docker "$USER"
log "INFO" "Added $USER to docker group. You may need to log out and back in."
fi

log "INFO" "Docker installed successfully."
}
Loading
Loading