Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Python CI and Docker Release

on:
push:
branches:
- main
- lab3
tags:
- "v*.*.*"
pull_request:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: python-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Lint and Test
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: true
matrix:
python-version: ["3.11", "3.12"]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: app_python/requirements.txt

- name: Install dependencies
id: deps
run: |
START=$(date +%s)
python -m pip install --upgrade pip
pip install -r app_python/requirements.txt
pip install ruff
END=$(date +%s)
echo "install_seconds=$((END-START))" >> "$GITHUB_OUTPUT"

- name: Lint (ruff)
run: ruff check app_python

- name: Run unit tests
run: python -m unittest discover -s app_python/tests -v

- name: Dependency cache report
if: always()
run: |
echo "### Dependency install metrics (Python ${{ matrix.python-version }})" >> "$GITHUB_STEP_SUMMARY"
echo "- cache-hit: \`${{ steps.setup-python.outputs.cache-hit }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- install-seconds: \`${{ steps.deps.outputs.install_seconds }}\`" >> "$GITHUB_STEP_SUMMARY"

security:
name: Snyk Dependency Scan
runs-on: ubuntu-latest
needs: test
timeout-minutes: 15
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
cache-dependency-path: app_python/requirements.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r app_python/requirements.txt

- name: Set up Snyk CLI
if: ${{ env.SNYK_TOKEN != '' }}
uses: snyk/actions/setup@master

- name: Run Snyk scan (high and critical)
if: ${{ env.SNYK_TOKEN != '' }}
continue-on-error: true
env:
SNYK_TOKEN: ${{ env.SNYK_TOKEN }}
run: snyk test \
--org=sofiakulagina \
--file=app_python/requirements.txt \
--severity-threshold=high


- name: Snyk token reminder
if: ${{ env.SNYK_TOKEN == '' }}
run: echo "SNYK_TOKEN secret is not configured; Snyk scan skipped."

docker:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: [test, security]
if: startsWith(github.ref, 'refs/tags/v')
timeout-minutes: 20
env:
IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v6
with:
context: app_python
file: app_python/Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
5 changes: 5 additions & 0 deletions app_go/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
*.log
__pycache__/
.git
vendor/
10 changes: 10 additions & 0 deletions app_go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Go build output
bin/
*.out

# IDE / Editor
.vscode/
.idea/

# OS
.DS_Store
30 changes: 30 additions & 0 deletions app_go/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Multi-stage Dockerfile for Go app

# Builder stage: use official Go image to compile a static Linux binary
FROM golang:1.22-alpine AS builder

WORKDIR /src

# Download dependencies separately to leverage caching
COPY go.mod ./
RUN apk add --no-cache git \
&& go mod tidy

# Copy source
COPY . .

# Build a statically linked binary for Linux
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags "-s -w" -o /devops-info ./

# Final stage: tiny runtime image
FROM scratch

# Copy binary from builder
COPY --from=builder /devops-info /devops-info

# Expose port and use non-root numeric UID
EXPOSE 5002
USER 1000

ENTRYPOINT ["/devops-info"]
54 changes: 54 additions & 0 deletions app_go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# DevOps Info Service (Go, Bonus)

## Overview

This directory contains a Go implementation of the **DevOps info service** with the same endpoints and JSON structure as the Python version:

- `GET /` — service, system, runtime, request and endpoints information
- `GET /health` — simple health check for monitoring and Kubernetes probes

## Prerequisites

- Go 1.22+ installed

## Build and Run

```bash
cd app_go

# Run directly
go run ./...

# Or build a binary
mkdir -p bin
go build -o bin/devops-info-service-go ./...

# Run the binary (default PORT=5002)
./bin/devops-info-service-go

# Custom port
PORT=8080 ./bin/devops-info-service-go
```

## API Endpoints

- `GET /`
- Returns JSON with:
- Service metadata (name, version, description, framework)
- System info (hostname, platform, architecture, CPU count, Go version)
- Runtime info (uptime in seconds and human readable, current time, timezone)
- Request info (client IP, user agent, method, path)
- List of available endpoints

- `GET /health`
- Returns JSON with status, timestamp and uptime in seconds.

## Configuration

Configuration is done through environment variables:

| Variable | Default | Description |
|----------|---------|------------------------------------|
| `PORT` | `5002` | TCP port for HTTP server |

The server listens on `0.0.0.0:PORT` by default.
103 changes: 103 additions & 0 deletions app_go/docs/LAB02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# LAB02 — Multi-stage Docker build for Go (English)

Date: 2026-02-05

Goal: demonstrate a multi-stage Docker build for the Go application and explain decisions, trade-offs and measurements.

## Multi-stage build strategy

- **Builder stage (golang:1.22-alpine)** — compiles a statically linked binary with `CGO_ENABLED=0` and `-ldflags "-s -w"` to strip debugging symbols.
- **Runtime stage (scratch)** — copies only the resulting binary from the builder (`COPY --from=builder`) into a minimal image with no package manager or shell.

Why this matters:
- The builder image contains compilers, source and toolchain (large). The final image contains only the binary, dramatically reducing size and attack surface.
- `COPY --from=builder` pulls artifacts from the named build stage into the final image.

Dockerfile excerpts:

```dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod ./
RUN apk add --no-cache git && go mod tidy
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o /devops-info ./

FROM scratch
COPY --from=builder /devops-info /devops-info
EXPOSE 5002
USER 1000
ENTRYPOINT ["/devops-info"]
```

Notes:
- `CGO_ENABLED=0` ensures a static binary that can run in `scratch` (no libc present). If the app uses C bindings, static build may fail and a different runtime base (e.g., distroless) may be required.
- We set `GOARCH=amd64` to produce an amd64 binary; adjust for your target architecture if needed (e.g., `arm64`).

## Build & size measurements

Commands executed locally (captured output below):

```bash
docker build --platform=linux/amd64 -t devops-go:lab2 app_go
docker build --platform=linux/amd64 --target builder -t devops-go:builder app_go
docker images devops-go:lab2 --format "{{.Repository}}:{{.Tag}} {{.Size}}"
docker images devops-go:builder --format "{{.Repository}}:{{.Tag}} {{.Size}}"
```

Key terminal output (build & sizes):

```
... (build output omitted) ...
devops-go:lab2 2.16MB
devops-go:builder 103MB
```

Analysis:
- **Builder image (103MB)**: includes the Go toolchain and apk packages required to fetch dependencies and build the binary.
- **Final image (2.16MB)**: contains only the stripped static binary — well under the 20MB challenge target.

Why you can't use the builder image as the final image:
- The builder contains compilers and package managers which increase image size and add additional attack surface. Keeping build tools out of production images reduces risk and distribution size.

Security implications:
- Smaller images have fewer packages and fewer potential vulnerabilities.
- `scratch` has no shell; if an attacker gains code execution they have very limited tooling.
- Running as a non-root numeric user (`USER 1000`) reduces privilege even further.

Trade-offs and notes:
- Using `scratch` gives the smallest possible image but also removes diagnostic tools; debugging requires reproducing the builder environment or adding temporary debug builds.
- If the binary depends on cgo or system libs, `scratch` may be unsuitable; use a minimal distro or distroless image.

## Technical explanation of each stage

- Builder stage:
- Installs `git` to allow `go mod tidy` to fetch modules.
- Copies `go.mod`, runs `go mod tidy` to populate `go.sum` and download dependencies (cached when `go.mod` hasn't changed).
- Copies source and builds a static binary with optimizations and stripped symbols.

- Final stage:
- Uses `scratch` for minimal footprint.
- Copies only the binary.
- Exposes the application port and runs the binary directly.

## Terminal outputs (selected)

Build final image (abridged):

```
#10 [builder 6/6] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o /devops-info ./
#12 exporting to image
#12 naming to docker.io/library/devops-go:lab2 done
```

Image sizes:

```
devops-go:lab2 2.16MB
devops-go:builder 103MB
```

## Conclusion

The multi-stage build achieved a very small runtime image (2.16MB) by compiling a static Go binary and copying only the artifact into a `scratch` image. This reduces distribution size and attack surface, and demonstrates the typical pattern to containerize compiled-language applications efficiently.
3 changes: 3 additions & 0 deletions app_go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module devops-info-service-go

go 1.22
20 changes: 20 additions & 0 deletions app_go/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package config

import "os"

// Config holds application configuration.
type Config struct {
Port string
}

// FromEnv reads configuration from environment variables with sensible defaults.
func FromEnv() Config {
port := os.Getenv("PORT")
if port == "" {
port = "5002"
}

return Config{
Port: port,
}
}
Loading