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
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ─────────────────────────────────────────────────────────────────────────────
# CI — Run tests, vet, and build on every push and pull request to main
# ─────────────────────────────────────────────────────────────────────────────
name: CI

on:
push:
branches: ["main", "infra/docker-ci"]
paths:
- "server/**"
pull_request:
branches: ["main"]
paths:
- "server/**"

jobs:
test:
name: Test & Vet
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: server/go.mod
cache-dependency-path: server/go.sum

- name: Download modules
working-directory: server
run: go mod download

- name: Vet
working-directory: server
run: go vet ./...

- name: Test (with race detector)
working-directory: server
run: go test -race -count=1 -timeout 120s ./...

build:
name: Build Binary
runs-on: ubuntu-latest
needs: test

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: server/go.mod
cache-dependency-path: server/go.sum

- name: Build server
working-directory: server
run: CGO_ENABLED=0 go build -o kvstore ./cmd/server

- name: Build CLI
working-directory: server
run: CGO_ENABLED=0 go build -o kvcli ./cmd/kvcli
86 changes: 86 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# ─────────────────────────────────────────────────────────────────────────────
# Docker Publish — Build and push images to Docker Hub on merge to main
#
# Required GitHub Secrets:
# DOCKER_USERNAME — your Docker Hub username
# DOCKER_PASSWORD — your Docker Hub access token (NOT your password)
#
# Images produced:
# <DOCKER_USERNAME>/kvstore-server:latest
# <DOCKER_USERNAME>/kvstore-server:<git-sha>
# <DOCKER_USERNAME>/kvstore-web:latest
# <DOCKER_USERNAME>/kvstore-web:<git-sha>
# ─────────────────────────────────────────────────────────────────────────────
name: Docker Publish

on:
push:
branches: ["main"]

jobs:
publish:
name: Build & Push Docker Images
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

# ── Set up Docker Buildx (required for multi-arch + cache) ───────────
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# ── Log in to Docker Hub ─────────────────────────────────────────────
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

# ── Extract metadata (tags + labels) for server image ────────────────
- name: Docker metadata — server
id: meta-server
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USERNAME }}/kvstore-server
tags: |
type=raw,value=latest
type=sha,prefix=,format=short

# ── Build and push the Go server image ───────────────────────────────
- name: Build and push server image
uses: docker/build-push-action@v6
with:
context: ./server
file: ./server/Dockerfile
push: true
tags: ${{ steps.meta-server.outputs.tags }}
labels: ${{ steps.meta-server.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

# ── Extract metadata for web image ───────────────────────────────────
- name: Docker metadata — web
id: meta-web
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USERNAME }}/kvstore-web
tags: |
type=raw,value=latest
type=sha,prefix=,format=short

# ── Build and push the Next.js web image ─────────────────────────────
- name: Build and push web image
uses: docker/build-push-action@v6
with:
context: ./web
file: ./web/Dockerfile
push: true
tags: ${{ steps.meta-web.outputs.tags }}
labels: ${{ steps.meta-web.outputs.labels }}
# Pass build-time API URL — override via repo variable if needed
build-args: |
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_WS_URL=ws://localhost:8080
cache-from: type=gha
cache-to: type=gha,mode=max
80 changes: 80 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ======================================================================
# KVStore — Root Makefile
# Operates on the full stack (server + web + Docker cluster).
# Run individual server targets from inside server/ using its own Makefile.
# ======================================================================

.DEFAULT_GOAL := help

.PHONY: help
help:
@echo ""
@echo " KVStore — root targets"
@echo ""
@echo " Docker Cluster"
@echo " make docker-build build all images locally"
@echo " make docker-up build + start 3-node cluster and web dashboard"
@echo " make docker-down stop and remove containers (keeps volumes)"
@echo " make docker-clean stop, remove containers AND named volumes"
@echo " make docker-logs tail logs from all services"
@echo " make docker-ps show status of all services"
@echo ""
@echo " Individual services"
@echo " make docker-node1 tail logs for node1 only"
@echo " make docker-web tail logs for the web dashboard only"
@echo ""
@echo " Server (delegates to server/Makefile)"
@echo " make test run all Go tests with the race detector"
@echo " make build build server and CLI binaries"
@echo ""

# ── Docker Cluster ──────────────────────────────────────────────────────────

.PHONY: docker-build
docker-build:
docker compose build

.PHONY: docker-up
docker-up:
docker compose up --build -d
@echo ""
@echo " Cluster is running:"
@echo " node1 HTTP API → http://localhost:8080"
@echo " node1 TCP → localhost:6379"
@echo " Dashboard → http://localhost:3000"
@echo ""

.PHONY: docker-down
docker-down:
docker compose down

.PHONY: docker-clean
docker-clean:
docker compose down -v
@echo " All containers and volumes removed."

.PHONY: docker-logs
docker-logs:
docker compose logs -f

.PHONY: docker-ps
docker-ps:
docker compose ps

.PHONY: docker-node1
docker-node1:
docker compose logs -f node1

.PHONY: docker-web
docker-web:
docker compose logs -f web

# ── Server (delegate) ───────────────────────────────────────────────────────

.PHONY: test
test:
$(MAKE) -C server test

.PHONY: build
build:
$(MAKE) -C server build
126 changes: 126 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# ─────────────────────────────────────────────────────────────────────────────
# KVStore — 3-Node Raft Cluster + Next.js Dashboard
#
# Usage:
# docker compose up --build -d # start everything
# docker compose down -v # stop and remove volumes
# docker compose logs -f # tail all logs
# ─────────────────────────────────────────────────────────────────────────────

services:

# ── Raft Node 1 (bootstrap leader candidate) ────────────────────────────
node1:
build:
context: ./server
dockerfile: Dockerfile
image: kvstore-server:local
container_name: kvstore-node1
restart: unless-stopped
environment:
NODE_ID: node1
HTTP_ADDR: :8080
TCP_ADDR: :6379
DATA_DIR: /app/data
# Peer list: all three nodes know about each other
PEERS: "node2=http://node2:8080,node3=http://node3:8080"
ports:
- "6379:6379" # TCP protocol (exposed to host for kvcli)
- "8080:8080" # HTTP API (exposed to host for curl / dashboard)
volumes:
- node1-data:/app/data
networks:
- kvstore-net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

# ── Raft Node 2 ──────────────────────────────────────────────────────────
node2:
image: kvstore-server:local
container_name: kvstore-node2
restart: unless-stopped
depends_on:
node1:
condition: service_started
environment:
NODE_ID: node2
HTTP_ADDR: :8080
TCP_ADDR: :6379
DATA_DIR: /app/data
PEERS: "node1=http://node1:8080,node3=http://node3:8080"
# Internal ports only — node2/node3 are not exposed to the host
expose:
- "8080"
- "6379"
volumes:
- node2-data:/app/data
networks:
- kvstore-net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

# ── Raft Node 3 ──────────────────────────────────────────────────────────
node3:
image: kvstore-server:local
container_name: kvstore-node3
restart: unless-stopped
depends_on:
node1:
condition: service_started
environment:
NODE_ID: node3
HTTP_ADDR: :8080
TCP_ADDR: :6379
DATA_DIR: /app/data
PEERS: "node1=http://node1:8080,node2=http://node2:8080"
expose:
- "8080"
- "6379"
volumes:
- node3-data:/app/data
networks:
- kvstore-net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

# ── Next.js Dashboard ─────────────────────────────────────────────────────
web:
build:
context: ./web
dockerfile: Dockerfile
args:
# Dashboard points at node1's HTTP API
NEXT_PUBLIC_API_URL: http://localhost:8080
NEXT_PUBLIC_WS_URL: ws://localhost:8080
container_name: kvstore-web
restart: unless-stopped
depends_on:
node1:
condition: service_healthy
ports:
- "3000:3000"
networks:
- kvstore-net

# ── Named volumes (data persists across container restarts) ────────────────
volumes:
node1-data:
node2-data:
node3-data:

# ── Internal bridge network ────────────────────────────────────────────────
networks:
kvstore-net:
driver: bridge
22 changes: 22 additions & 0 deletions server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Build artifacts
kvstore
kvcli
*.exe
*.out
*.test
*.prof

# Runtime data — never bake into image
data/

# Coverage reports
coverage.out
coverage.html

# IDE / OS
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db
Loading
Loading