Skip to content

Commit c91c8aa

Browse files
author
omersiar
committed
Initial version
0 parents  commit c91c8aa

36 files changed

Lines changed: 6969 additions & 0 deletions

.env.example

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Kafka Configuration
2+
RIPT_KAFKA_BROKERS=localhost:9092
3+
4+
# Kafka Authentication - only SSL and PLAINTEXT is tested, but other options should work as well
5+
# OAUTHBEARER, AWS MSK AUTH testers are welcome
6+
7+
# RIPT_KAFKA_SECURITY_PROTOCOL=PLAINTEXT # PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL
8+
# RIPT_KAFKA_SASL_MECHANISM= # PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, OAUTHBEARER
9+
# RIPT_KAFKA_SASL_USERNAME=
10+
# RIPT_KAFKA_SASL_PASSWORD=
11+
# RIPT_KAFKA_SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL=
12+
# RIPT_KAFKA_SASL_OAUTHBEARER_CLIENT_ID=
13+
# RIPT_KAFKA_SASL_OAUTHBEARER_CLIENT_SECRET=
14+
# RIPT_KAFKA_SASL_OAUTHBEARER_SCOPE=
15+
16+
# TLS / mTLS
17+
# RIPT_KAFKA_TLS_CA_CERT_FILE= # Path to CA certificate (PEM)
18+
# RIPT_KAFKA_TLS_CLIENT_CERT_FILE= # Path to client certificate (PEM) for mTLS
19+
# RIPT_KAFKA_TLS_CLIENT_KEY_FILE= # Path to client private key (PEM) for mTLS
20+
# RIPT_KAFKA_TLS_INSECURE_SKIP_VERIFY=false
21+
22+
# Scan Configuration
23+
RIPT_SCAN_INTERVAL_MINUTES=5
24+
25+
# State / Tracker Configuration
26+
RIPT_STATE_TOPIC=ript-state
27+
RIPT_KAFKA_CONSUMER_GROUP_ID=ript-scan
28+
RIPT_STATE_TOPIC_PARTITIONS=6
29+
RIPT_STATE_TOPIC_REPLICATION_FACTOR=2
30+
RIPT_STATE_LOAD_TIMEOUT_SECONDS=30
31+
RIPT_INSTANCE_ID=
32+
33+
# Staleness Thresholds - used for cosmetics and can be set to different values on frontend
34+
RIPT_STALE_PARTITION_DAYS=7
35+
RIPT_UNUSED_TOPIC_DAYS=30
36+
37+
# HTTP Server Configuration
38+
RIPT_HTTP_PORT=8080
39+
RIPT_HTTP_HOST=0.0.0.0
40+
41+
# Logging Configuration
42+
RIPT_LOG_LEVEL=info

.github/copilot-instructions.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copilot Instructions
2+
3+
## Project Overview
4+
5+
RIPT (Reclaimer of Inactive Partitions and Topics) is a stateless Go daemon that monitors Apache Kafka topics to identify unused ones. It persists state in its own internal Kafka topic (`ript-state`) and exposes a REST API and web dashboard.
6+
7+
## Build, Test, and Run
8+
9+
```bash
10+
make build # Build binary to bin/ript
11+
make run # Build and run locally (requires .env or env vars)
12+
make test # go test -v ./...
13+
make clean # Remove build artifacts
14+
make deps # go mod download && go mod tidy
15+
16+
# Docker
17+
make docker-up # Start Zookeeper + Kafka + RIPT
18+
make docker-down # Tear down stack
19+
make docker-logs # Tail RIPT logs
20+
```
21+
22+
**Single test:**
23+
```bash
24+
go test -v -run TestFunctionName ./internal/package_name
25+
```
26+
27+
**Integration tests** (requires Docker):
28+
```bash
29+
./test-integration.sh
30+
```
31+
32+
## Architecture
33+
34+
```
35+
cmd/ript/ Entry point — wires up all components, handles OS signals, graceful shutdown
36+
internal/config/ Loads and validates env vars with defaults (via godotenv)
37+
internal/models/ Domain types: TopicStatus, PartitionInfo, ClusterSnapshot, Duration
38+
internal/kafka/ Kafka client (topic metadata + offset fetching) and StateManager (snapshot persistence)
39+
internal/tracker/ TopicTracker — core scan loop, staleness logic, in-memory snapshot
40+
internal/api/ Gin HTTP server — routes and handlers read from TopicTracker snapshot
41+
internal/logging/ Custom leveled logger (DEBUG/INFO/WARN/ERROR/FATAL)
42+
web/templates/ dashboard.html — Bootstrap 5 frontend served by the API server
43+
```
44+
45+
**Data flow:** `main` → initialize config/logging → connect Kafka (with retry) → `StateManager.Load()` → start `TopicTracker` scan loop → start `api.Server` → serve traffic from in-memory snapshot.
46+
47+
Each scan: fetch Kafka metadata + offsets → compute ages → update snapshot → persist to `ript-state` (log-compacted, one message per topic key).
48+
49+
**Staleness thresholds (configurable):**
50+
- Partition "stale": no offset update in `RIPT_STALE_PARTITION_DAYS` days (default 7)
51+
- Topic "unused": all partitions stale for `RIPT_UNUSED_TOPIC_DAYS` days (default 30)
52+
53+
## Key Conventions
54+
55+
**Concurrency:** Snapshot access uses `sync.RWMutex` (read-heavy). Goroutines are coordinated with context cancellation and `sync.WaitGroup` for shutdown.
56+
57+
**HTTP handlers (Gin):** All handlers are pointer receiver methods on `api.Server`. Return `503 ServiceUnavailable` if the tracker snapshot is not yet initialized. Use `gin.H` maps for all JSON responses.
58+
59+
**Error handling:** Wrap errors with `fmt.Errorf("context: %w", err)`. Log warnings and degrade gracefully for non-fatal errors; use `logger.Fatal()` only for initialization failures.
60+
61+
**State persistence:** `StateManager` writes one compacted Kafka message per topic (topic name as key). On restart, it reads back to EOF to restore the last snapshot. The app is stateless from infrastructure's perspective — Kafka is the durable store.
62+
63+
**Configuration:** All config via environment variables with `RIPT_` prefix. `.env` file auto-loaded if present. See `.env.example` for all supported variables. Key vars: `RIPT_KAFKA_BROKERS`, `RIPT_SCAN_INTERVAL_MINUTES`, `RIPT_HTTP_PORT`, `RIPT_LOG_LEVEL`.
64+
65+
**Models:** `Duration` is a custom type with `Days/Hours/Minutes/Seconds` fields and a `String()` method for human-readable output. Use `models.CalculateDuration(t time.Time)` to convert timestamps.
66+
67+
## Local Development Setup
68+
69+
```bash
70+
cp .env.example .env # Configure Kafka brokers and other settings
71+
make docker-up # Start Kafka stack (listens on localhost:9092)
72+
make run # Run RIPT against local Kafka
73+
```
74+
75+
The RIPT dashboard is available at `http://localhost:8080` once running.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: Docker
2+
3+
# This workflow uses actions that are not certified by GitHub.
4+
# They are provided by a third-party and are governed by
5+
# separate terms of service, privacy policy, and support
6+
# documentation.
7+
8+
on:
9+
push:
10+
branches: [ "main" ]
11+
# Publish semver tags as releases.
12+
tags: [ 'v*.*.*' ]
13+
pull_request:
14+
branches: [ "main" ]
15+
16+
env:
17+
# Use docker.io for Docker Hub if empty
18+
REGISTRY: ghcr.io
19+
# github.repository as <account>/<repo>
20+
IMAGE_NAME: ${{ github.repository }}
21+
22+
jobs:
23+
build:
24+
25+
runs-on: ubuntu-latest
26+
permissions:
27+
contents: read
28+
packages: write
29+
# This is used to complete the identity challenge
30+
# with sigstore/fulcio when running outside of PRs.
31+
id-token: write
32+
33+
steps:
34+
- name: Checkout repository
35+
uses: actions/checkout@v4
36+
37+
- name: Install cosign
38+
if: github.event_name != 'pull_request'
39+
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20
40+
with:
41+
cosign-release: "v2.2.4"
42+
43+
- name: Set up Docker Buildx
44+
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226
45+
46+
- name: Log into registry ${{ env.REGISTRY }}
47+
if: github.event_name != 'pull_request'
48+
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d
49+
with:
50+
registry: ${{ env.REGISTRY }}
51+
username: ${{ github.actor }}
52+
password: ${{ secrets.GITHUB_TOKEN }}
53+
54+
- name: Compute build metadata
55+
id: vars
56+
run: |
57+
echo "build_time=$(date -u +%Y%m%dT%H%M%SZ)" >> "$GITHUB_OUTPUT"
58+
echo "git_commit=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
59+
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
60+
echo "version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
61+
else
62+
echo "version=dev" >> "$GITHUB_OUTPUT"
63+
fi
64+
65+
- name: Extract Docker metadata
66+
id: meta
67+
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934
68+
with:
69+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
70+
tags: |
71+
type=ref,event=branch
72+
type=ref,event=tag
73+
type=sha
74+
75+
- name: Build and push Docker image
76+
id: build-and-push
77+
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09
78+
with:
79+
context: .
80+
push: ${{ github.event_name != 'pull_request' }}
81+
tags: ${{ steps.meta.outputs.tags }}
82+
labels: ${{ steps.meta.outputs.labels }}
83+
build-args: |
84+
VERSION=${{ steps.vars.outputs.version }}
85+
GIT_COMMIT=${{ steps.vars.outputs.git_commit }}
86+
BUILD_TIME=${{ steps.vars.outputs.build_time }}
87+
cache-from: type=gha
88+
cache-to: type=gha,mode=max
89+
90+
- name: Sign the published Docker image
91+
if: ${{ github.event_name != 'pull_request' }}
92+
env:
93+
TAGS: ${{ steps.meta.outputs.tags }}
94+
DIGEST: ${{ steps.build-and-push.outputs.digest }}
95+
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Release Artifacts
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
build-and-upload:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Go
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version: "1.23"
22+
23+
- name: Build binaries
24+
run: |
25+
mkdir -p dist
26+
VERSION="${{ github.event.release.tag_name }}"
27+
GIT_COMMIT="${GITHUB_SHA::7}"
28+
BUILD_TIME="$(date -u +%Y%m%dT%H%M%SZ)"
29+
LDFLAGS="-s -w \
30+
-X github.com/omersiar/ript/internal/version.Version=${VERSION} \
31+
-X github.com/omersiar/ript/internal/version.GitCommit=${GIT_COMMIT} \
32+
-X github.com/omersiar/ript/internal/version.BuildTime=${BUILD_TIME}"
33+
34+
GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o dist/ript-linux-amd64 ./cmd/ript
35+
GOOS=linux GOARCH=arm64 go build -ldflags "$LDFLAGS" -o dist/ript-linux-arm64 ./cmd/ript
36+
GOOS=darwin GOARCH=amd64 go build -ldflags "$LDFLAGS" -o dist/ript-darwin-amd64 ./cmd/ript
37+
GOOS=darwin GOARCH=arm64 go build -ldflags "$LDFLAGS" -o dist/ript-darwin-arm64 ./cmd/ript
38+
GOOS=windows GOARCH=amd64 go build -ldflags "$LDFLAGS" -o dist/ript-windows-amd64.exe ./cmd/ript
39+
40+
- name: Generate checksums
41+
run: |
42+
cd dist
43+
sha256sum * > checksums.txt
44+
45+
- name: Upload assets to release
46+
uses: softprops/action-gh-release@v2
47+
with:
48+
files: |
49+
dist/ript-linux-amd64
50+
dist/ript-linux-arm64
51+
dist/ript-darwin-amd64
52+
dist/ript-darwin-arm64
53+
dist/ript-windows-amd64.exe
54+
dist/checksums.txt
55+
env:
56+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Binaries
2+
bin/
3+
*.exe
4+
*.exe~
5+
*.dll
6+
*.so
7+
*.so.*
8+
*.dylib
9+
10+
# Test binaries
11+
*.test
12+
*.out
13+
14+
# Build cache
15+
*.o
16+
*.a
17+
18+
# Go modules
19+
vendor/
20+
21+
# IDE
22+
.vscode/
23+
.idea/
24+
*.swp
25+
*.swo
26+
*~
27+
.DS_Store
28+
29+
# Environment
30+
.env
31+
.env.local
32+
.env.*.local
33+
34+
# Docker
35+
.dockerignore
36+
37+
# Logs
38+
*.log
39+
logs/
40+
41+
# Temporary files
42+
tmp/
43+
temp/
44+
*.tmp
45+
46+
kafkactl

ACLs.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
### Kafka ACLs (secured clusters)
2+
3+
If your Kafka cluster enforce ACLs, grant the RIPT principal permissions for:
4+
5+
- Cluster metadata discovery
6+
- Reading monitored topics (offset and metadata inspection)
7+
- Read/write access to the internal state topic
8+
- Consumer group access for scan workload balancing (`RIPT_KAFKA_CONSUMER_GROUP_ID`)
9+
10+
Example (`User:ript`, Kafka ACL authorizer):
11+
12+
Adjust principal format as needed for your environment (for example, mTLS DNs or SASL usernames).
13+
14+
```bash
15+
# 1) Cluster metadata
16+
kafka-acls.sh --bootstrap-server localhost:9092 \
17+
--add --allow-principal User:ript \
18+
--operation Describe --cluster
19+
20+
# 2) Read monitored topics (use --topic '*' for all topics, or scope to prefixes)
21+
kafka-acls.sh --bootstrap-server localhost:9092 \
22+
--add --allow-principal User:ript \
23+
--operation Describe --operation Read \
24+
--topic '*'
25+
26+
# 3) Internal state topic access
27+
kafka-acls.sh --bootstrap-server localhost:9092 \
28+
--add --allow-principal User:ript \
29+
--operation Describe --operation Read --operation Write \
30+
--topic ript-state
31+
32+
# 4) Consumer group access used for balancing scans
33+
kafka-acls.sh --bootstrap-server localhost:9092 \
34+
--add --allow-principal User:ript \
35+
--operation Describe --operation Read \
36+
--group ript-scan
37+
```
38+
39+
Optional (only if RIPT should create `ript-state` itself):
40+
41+
```bash
42+
kafka-acls.sh --bootstrap-server localhost:9092 \
43+
--add --allow-principal User:ript \
44+
--operation Create \
45+
--topic ript-state
46+
```

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
9+
## v0.0.1 - 2026-04-02
10+
### Added
11+
- Initial version

0 commit comments

Comments
 (0)