diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..c8c1b16 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,64 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + tags: + - 'v*' + paths: + - 'src/**' + - '.github/workflows/docker-build.yml' + pull_request: + branches: + - main + paths: + - 'src/**' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/gradle-cache + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./src + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bb1a84..ae4198b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,9 @@ on: push: branches: - main + paths: + - 'chart/**' + - '.github/workflows/release.yml' jobs: release: diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0932eb --- /dev/null +++ b/README.md @@ -0,0 +1,380 @@ +# Gradle Build Cache Server + +A high-performance, Kubernetes-native HTTP build cache server for Gradle builds. Built with Go and backed by MinIO for reliable, persistent storage. + + +## Overview + +This project provides a lightweight, self-hosted Gradle Build Cache server implementation that can be deployed in Kubernetes clusters to accelerate build times across teams and CI/CD pipelines. It implements the Gradle HTTP Build Cache API and uses MinIO as a persistent storage backend. + +### Key Features + +- **Gradle HTTP Build Cache API** - Fully compatible with Gradle's remote cache protocol +- **Persistent Storage** - Uses MinIO (S3-compatible) for reliable, scalable artifact storage +- **Authentication** - HTTP Basic Authentication for access control +- **Kubernetes-Native** - Designed for containerized deployments with production-ready Helm charts +- **Observability** - Built-in Prometheus metrics and structured logging (JSON/text) +- **Lightweight** - Minimal resource footprint (~256Mi RAM, ~100m CPU) +- **Health Checks** - Kubernetes-ready liveness and readiness probes +- **Easy Deployment** - Single Helm command to get started + +### Use Cases + +- **Team Development** - Share build cache across development teams +- **CI/CD Pipelines** - Accelerate build times in Jenkins, GitLab CI, GitHub Actions, etc. +- **Monorepos** - Reduce build times for large multi-module projects +- **Multi-Environment Builds** - Share cache between dev, staging, and production builds +- **Cost Optimization** - Reduce build server compute costs by avoiding redundant work + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Kubernetes Cluster │ +│ │ +│ ┌─────────────────┐ ┌─────────────────────┐ │ +│ │ Cache Server │──────▶│ MinIO │ │ +│ │ (Deployment) │ │ (StatefulSet) │ │ +│ │ Port: 8080 │ │ Port: 9000 │ │ +│ └────────┬────────┘ └─────────────────────┘ │ +│ │ │ +│ ┌────────▼────────┐ │ +│ │ Service │◀──── Developer Workstations │ +│ │ Port: 8080 │◀──── CI/CD Pipelines │ +│ └─────────────────┘◀──── Build Agents │ +└─────────────────────────────────────────────────────────┘ +``` + +### Components + +| Component | Description | Technology | +|-----------|-------------|------------| +| **Cache Server** | HTTP API server implementing Gradle Build Cache protocol | Go, Gin Framework | +| **Storage Backend** | S3-compatible object storage for cache artifacts | MinIO | +| **Kubernetes Service** | ClusterIP service for internal access | Kubernetes | +| **Helm Chart** | Declarative deployment configuration | Helm 3 | + +## Quick Start + +### Prerequisites + +- Kubernetes cluster (v1.19+) +- Helm 3.x +- kubectl configured to access your cluster +- (Optional) Domain name and TLS certificates for production deployments + +### Installation + +1. Clone the repository: +```bash +git clone https://github.com/kevingruber/theia-shared-cache.git +cd theia-shared-cache +``` + +2. Configure your deployment by editing `chart/values.yaml`: +```yaml +cacheServer: + auth: + username: "gradle" + password: "your-secure-password" # CHANGE THIS! + +minio: + auth: + accessKey: "your-access-key" # CHANGE THIS! + secretKey: "your-secret-key" # CHANGE THIS! +``` + +3. Deploy using Helm: +```bash +helm install theia-cache ./chart +``` + +4. Verify the deployment: +```bash +kubectl get pods +kubectl logs -f deployment/theia-cache +``` + +### Testing the Cache + +Port-forward the service to your local machine: +```bash +kubectl port-forward svc/theia-cache 8080:8080 +``` + +Test the health endpoint: +```bash +curl http://localhost:8080/ping +# Expected response: pong +``` + +Test cache operations: +```bash +# Store a cache entry +echo "test data" | curl -u gradle:your-password \ + -X PUT \ + -H "Content-Type: application/octet-stream" \ + --data-binary @- \ + http://localhost:8080/cache/test-key + +# Retrieve the cache entry +curl -u gradle:your-password http://localhost:8080/cache/test-key +``` + +## Configuration + +### Gradle Configuration + +Configure your Gradle builds to use the remote cache by adding to `settings.gradle`: + +```groovy +buildCache { + remote(HttpBuildCache) { + url = 'http://theia-cache:8080/cache/' + credentials { + username = 'gradle' + password = 'your-password' + } + push = true + } +} +``` + +Or via `gradle.properties`: +```properties +org.gradle.caching=true +org.gradle.caching.remote.url=http://theia-cache:8080/cache/ +org.gradle.caching.remote.username=gradle +org.gradle.caching.remote.password=your-password +org.gradle.caching.remote.push=true +``` + +### Helm Chart Configuration + +Key configuration options in `chart/values.yaml`: + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `cacheServer.replicaCount` | Number of cache server replicas | `1` | +| `cacheServer.image.repository` | Container image repository | `ghcr.io/kevingruber/theia-shared-cache/gradle-cache` | +| `cacheServer.image.tag` | Container image tag | `latest` | +| `cacheServer.tls.enabled` | Enable TLS/HTTPS | `false` | +| `cacheServer.tls.secretName` | TLS certificate secret name | `""` | +| `cacheServer.tls.certManager.enabled` | Use cert-manager for certificates | `false` | +| `cacheServer.auth.username` | Authentication username | `gradle` | +| `cacheServer.auth.password` | Authentication password | `changeme` | +| `cacheServer.config.maxEntrySizeMB` | Maximum cache entry size | `100` | +| `minio.enabled` | Deploy MinIO with the chart | `true` | +| `minio.persistence.size` | MinIO storage size | `50Gi` | +| `minio.auth.accessKey` | MinIO access key | `minioadmin` | +| `minio.auth.secretKey` | MinIO secret key | `minioadmin` | + +For a complete list of configuration options, see the [Helm chart documentation](chart/README.md). + +### Enabling TLS/HTTPS + +To secure communication with TLS, you can use cert-manager or provide your own certificates. Here's a quick example: + +**With cert-manager (recommended):** +```yaml +cacheServer: + tls: + enabled: true + secretName: gradle-cache-tls + certManager: + enabled: true + issuerName: "letsencrypt-prod" + issuerKind: "ClusterIssuer" +``` + +**With self-signed certificates:** +```bash +# Generate certificate +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout tls.key -out tls.crt \ + -subj "/CN=cache-server.default.svc.cluster.local" + +# Create Kubernetes secret +kubectl create secret tls gradle-cache-tls --cert=tls.crt --key=tls.key + +# Update values.yaml +cacheServer: + tls: + enabled: true + secretName: gradle-cache-tls +``` + +For detailed TLS setup instructions, see the [TLS Setup Guide](docs/tls-setup.md). + +### Environment Variables + +The cache server can be configured via environment variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `SERVER_PORT` | HTTP server port | `8080` | +| `SERVER_READ_TIMEOUT` | Request read timeout | `30s` | +| `SERVER_WRITE_TIMEOUT` | Request write timeout | `120s` | +| `MINIO_ENDPOINT` | MinIO server endpoint | `minio:9000` | +| `MINIO_ACCESS_KEY` | MinIO access key | - | +| `MINIO_SECRET_KEY` | MinIO secret key | - | +| `CACHE_PASSWORD` | Cache authentication password | - | +| `LOG_LEVEL` | Logging level (debug, info, warn, error) | `info` | +| `LOG_FORMAT` | Log format (json, text) | `json` | + +## API Reference + +### Endpoints + +| Endpoint | Method | Description | Authentication | +|----------|--------|-------------|----------------| +| `/ping` | GET | Health check (liveness probe) | No | +| `/health` | GET | Storage health check (readiness probe) | No | +| `/metrics` | GET | Prometheus metrics | No | +| `/cache/:key` | GET | Retrieve cache entry | Yes | +| `/cache/:key` | PUT | Store cache entry | Yes | +| `/cache/:key` | HEAD | Check if cache entry exists | Yes | + +### HTTP Status Codes + +| Code | Description | +|------|-------------| +| `200 OK` | Cache hit (GET), entry exists (HEAD) | +| `201 Created` | Cache entry stored successfully (PUT) | +| `204 No Content` | Entry does not exist (HEAD) | +| `401 Unauthorized` | Authentication failed | +| `404 Not Found` | Cache miss (GET) | +| `413 Payload Too Large` | Entry exceeds maximum size | +| `500 Internal Server Error` | Server or storage error | + +## Development + +### Building from Source + +#### Prerequisites +- Go 1.24+ +- Docker (optional, for containerization) + +#### Build the binary +```bash +cd src +go build -o bin/cache-server ./cmd/server +``` + +#### Run locally +```bash +# Set required environment variables +export MINIO_ENDPOINT=localhost:9000 +export MINIO_ACCESS_KEY=minioadmin +export MINIO_SECRET_KEY=minioadmin +export CACHE_PASSWORD=changeme + +# Run the server +./bin/cache-server +``` + +#### Build Docker image +```bash +cd src +docker build -t theia-cache:dev . +``` + +### Testing + +Run unit tests: +```bash +cd src +go test ./... +``` + +Run with coverage: +```bash +go test -cover ./... +``` + +### Project Structure + +``` +. +├── chart/ # Helm chart for Kubernetes deployment +│ ├── templates/ # Kubernetes manifests +│ ├── values.yaml # Default configuration +│ └── Chart.yaml # Chart metadata +├── src/ # Go source code +│ ├── cmd/ +│ │ └── server/ # Main application entry point +│ ├── internal/ +│ │ ├── config/ # Configuration management +│ │ ├── middleware/ # HTTP middleware (auth, logging) +│ │ ├── server/ # HTTP server and routes +│ │ └── storage/ # MinIO storage backend +│ ├── Dockerfile # Multi-stage Docker build +│ └── go.mod # Go module dependencies +└── .github/ + └── workflows/ # GitHub Actions CI/CD +``` + +## Monitoring + +### Prometheus Metrics + +The cache server exposes Prometheus metrics at `/metrics`: + +| Metric | Type | Description | +|--------|------|-------------| +| `cache_requests_total` | Counter | Total number of cache requests | +| `cache_hits_total` | Counter | Number of cache hits | +| `cache_misses_total` | Counter | Number of cache misses | +| `cache_errors_total` | Counter | Number of cache errors | +| `http_request_duration_seconds` | Histogram | HTTP request latency | + +### ServiceMonitor + +If using Prometheus Operator, enable the ServiceMonitor in `values.yaml`: +```yaml +metrics: + enabled: true + serviceMonitor: + enabled: true +``` + +## Security Considerations + +### Current Limitations + +- **TLS Optional** - TLS/HTTPS is disabled by default. Enable it for production deployments. +- **Basic Authentication** - Simple username/password authentication. +- **Single User** - Only one set of credentials supported. +- **Single Replica** - No built-in high availability (single point of failure). + +### Recommendations for Production + +1. **Enable TLS** - Encrypt all traffic (see [TLS Setup Guide](docs/tls-setup.md)) +2. **Change default credentials** - Use strong, randomly-generated passwords +3. **Network Policies** - Restrict access to authorized pods only +4. **Use Kubernetes Secrets** - Never commit credentials to version control +5. **Monitor certificate expiration** - Set up alerts for certificate renewal +6. **Regular updates** - Keep dependencies and base images up to date + +## Troubleshooting + +### Common Issues + +**Pods not starting** +```bash +kubectl describe pod +kubectl logs +``` + +**MinIO connection errors** +- Verify MinIO is running: `kubectl get pods -l app=minio` +- Check MinIO logs: `kubectl logs -l app=minio` +- Verify credentials match in both cache and MinIO configurations + +**Authentication failures** +- Ensure credentials in Gradle match those in `values.yaml` +- Check for special characters that need URL encoding + +**Out of storage** +- Increase MinIO PVC size in `values.yaml` +- Clean old cache entries manually via MinIO console \ No newline at end of file diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..b047376 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: theia-shared-cache +description: A custom Gradle Build Cache server with MinIO backend for Theia IDE deployments in Kubernetes + +type: application + +# Chart version - bump for breaking changes +version: 0.2.0 + +# Application version - matches the cache server version +appVersion: "0.1.0" + +keywords: + - gradle + - cache + - build-cache + - theia + - minio + +home: https://github.com/kevingruber/theia-shared-cache + +maintainers: + - name: Kevin Gruber diff --git a/chart/README.md b/chart/README.md new file mode 100644 index 0000000..9116a4d --- /dev/null +++ b/chart/README.md @@ -0,0 +1,173 @@ +# theia-shared-cache Helm Chart + +Helm chart to deploy a custom Gradle Build Cache server with MinIO backend for Theia IDE deployments in Kubernetes. + +## Architecture + +This chart deploys two components: + +1. **Cache Server** - A custom Go-based Gradle build cache server +2. **MinIO** - S3-compatible object storage for cache data + +``` +┌─────────────────────────────────────────────────────┐ +│ Kubernetes Cluster │ +│ │ +│ ┌─────────────────┐ ┌─────────────────────┐ │ +│ │ Cache Server │──────▶│ MinIO │ │ +│ │ (Deployment) │ │ (StatefulSet) │ │ +│ │ Port: 8080 │ │ Port: 9000 │ │ +│ └────────┬────────┘ └─────────────────────┘ │ +│ │ │ +│ ┌────────▼────────┐ │ +│ │ Service │◀──── Gradle Builds │ +│ │ Port: 8080 │ │ +│ └─────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +## Features + +- Custom Go-based cache server with MinIO storage backend +- Prometheus metrics endpoint (`/metrics`) +- Health checks (`/ping`, `/health`) +- Basic authentication for cache operations +- Persistent storage for MinIO data +- Configurable resource limits + +## Quick Start + +### Install with default values + +```bash +helm install gradle-cache ./chart +``` + +### Install with custom values + +```bash +helm install gradle-cache ./chart \ + --set cacheServer.auth.password=mysecretpassword \ + --set minio.auth.accessKey=myaccesskey \ + --set minio.auth.secretKey=mysecretkey \ + --set minio.persistence.size=100Gi +``` + +## Configuration + +### Cache Server + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `cacheServer.replicaCount` | Number of cache server replicas | `1` | +| `cacheServer.image.repository` | Cache server image repository | `ghcr.io/kevingruber/theia-shared-cache/gradle-cache` | +| `cacheServer.image.tag` | Cache server image tag | `latest` | +| `cacheServer.port` | Cache server port | `8080` | +| `cacheServer.auth.enabled` | Enable authentication | `true` | +| `cacheServer.auth.username` | Cache username | `gradle` | +| `cacheServer.auth.password` | Cache password | `changeme` | +| `cacheServer.config.maxEntrySizeMB` | Max cache entry size in MB | `100` | +| `cacheServer.resources` | Resource limits/requests | See values.yaml | + +### MinIO + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `minio.enabled` | Deploy MinIO alongside cache server | `true` | +| `minio.image.repository` | MinIO image repository | `minio/minio` | +| `minio.image.tag` | MinIO image tag | `latest` | +| `minio.auth.accessKey` | MinIO access key | `minioadmin` | +| `minio.auth.secretKey` | MinIO secret key | `minioadmin` | +| `minio.persistence.enabled` | Enable persistent storage | `true` | +| `minio.persistence.size` | Storage size | `50Gi` | +| `minio.persistence.storageClass` | Storage class | `""` (default) | +| `minio.resources` | Resource limits/requests | See values.yaml | + +### Metrics + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `metrics.enabled` | Enable Prometheus metrics | `true` | +| `metrics.serviceMonitor.enabled` | Create ServiceMonitor (Prometheus Operator) | `false` | + +### External Secret + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `existingSecret` | Use existing secret for credentials | `""` | + +If using `existingSecret`, the secret must contain these keys: +- `minio-access-key` +- `minio-secret-key` +- `cache-password` + +## Gradle Configuration + +Configure your Gradle build to use the cache: + +```kotlin +// settings.gradle.kts +buildCache { + remote { + url = uri("http://-cache:8080/cache/") + credentials { + username = "gradle" + password = "your-password" + } + isPush = true + } +} +``` + +Or via environment variables: + +```bash +export GRADLE_CACHE_URL=http://-cache:8080/cache/ +export GRADLE_CACHE_USERNAME=gradle +export GRADLE_CACHE_PASSWORD=your-password +``` + +## Port Forwarding (Development) + +For local testing, port-forward the cache service: + +```bash +kubectl port-forward svc/-cache 8080:8080 + +# Test the cache server +curl http://localhost:8080/ping +curl http://localhost:8080/health +``` + +## Monitoring + +If `metrics.enabled` is true, Prometheus metrics are available at `/metrics`: + +```bash +curl http://-cache:8080/metrics +``` + +Available metrics: +- `cache_requests_total` - Total cache requests by method and status +- `cache_hits_total` - Cache hit count +- `cache_misses_total` - Cache miss count +- `cache_request_duration_seconds` - Request duration histogram +- `cache_entry_size_bytes` - Cache entry size histogram + +## Upgrading + +### From 0.1.x to 0.2.0 + +Version 0.2.0 is a breaking change that replaces the official Gradle cache node with a custom implementation. + +Changes: +- New image: custom Go-based cache server instead of `gradle/build-cache-node` +- New storage: MinIO backend instead of local PersistentVolume +- New port: 8080 instead of 5071 +- New authentication: Basic auth built into cache server + +Migration steps: +1. Back up any important cache data (usually safe to lose) +2. Uninstall the old release: `helm uninstall ` +3. Install the new version with updated values +4. Update Gradle configuration to use new port and credentials diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt new file mode 100644 index 0000000..bbb2600 --- /dev/null +++ b/chart/templates/NOTES.txt @@ -0,0 +1,43 @@ +Gradle Build Cache has been deployed! + +1. Get the cache server URL: + + export CACHE_URL=http://{{ .Release.Name }}-cache:{{ .Values.cacheServer.port }} + +2. Port-forward to access the cache locally: + + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-cache {{ .Values.cacheServer.port }}:{{ .Values.cacheServer.port }} + +3. Test the cache server: + + curl http://localhost:{{ .Values.cacheServer.port }}/ping + curl http://localhost:{{ .Values.cacheServer.port }}/health + +4. Configure your Gradle build (settings.gradle.kts): + + buildCache { + remote { + url = uri("$CACHE_URL/cache/") + credentials { + username = "{{ .Values.cacheServer.auth.username }}" + password = "" + } + isPush = true + } + } + +{{- if .Values.metrics.enabled }} + +5. Prometheus metrics are available at: + + http://{{ .Release.Name }}-cache:{{ .Values.cacheServer.port }}/metrics +{{- end }} + +{{- if .Values.minio.enabled }} + +6. MinIO console (for debugging): + + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-minio 9001:9001 + + Then open http://localhost:9001 (credentials from your values) +{{- end }} diff --git a/chart/templates/certificate.yaml b/chart/templates/certificate.yaml new file mode 100644 index 0000000..0642a75 --- /dev/null +++ b/chart/templates/certificate.yaml @@ -0,0 +1,22 @@ +{{- if and .Values.cacheServer.tls.enabled .Values.cacheServer.tls.certManager.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .Release.Name }}-tls + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: cache-server + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + secretName: {{ .Values.cacheServer.tls.secretName }} + issuerRef: + name: {{ .Values.cacheServer.tls.certManager.issuerName }} + kind: {{ .Values.cacheServer.tls.certManager.issuerKind }} + dnsNames: + - {{ .Release.Name }}-cache-server + - {{ .Release.Name }}-cache-server.{{ .Release.Namespace }} + - {{ .Release.Name }}-cache-server.{{ .Release.Namespace }}.svc + - {{ .Release.Name }}-cache-server.{{ .Release.Namespace }}.svc.cluster.local +{{- end }} diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml new file mode 100644 index 0000000..e72072b --- /dev/null +++ b/chart/templates/configmap.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-config + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: cache-server + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +data: + config.yaml: | + server: + port: {{ .Values.cacheServer.port }} + read_timeout: {{ .Values.cacheServer.readTimeout | default "30s" }} + write_timeout: {{ .Values.cacheServer.writeTimeout | default "120s" }} + tls: + enabled: {{ .Values.cacheServer.tls.enabled }} + {{- if .Values.cacheServer.tls.enabled }} + cert_file: "/etc/certs/tls.crt" + key_file: "/etc/certs/tls.key" + {{- end }} + + storage: + {{- if .Values.minio.enabled }} + endpoint: "{{ .Release.Name }}-minio:9000" + {{- else }} + endpoint: {{ .Values.cacheServer.storage.endpoint | quote }} + {{- end }} + bucket: {{ .Values.cacheServer.storage.bucket | default "gradle-cache" | quote }} + use_ssl: {{ .Values.cacheServer.storage.useSSL | default false }} + + cache: + max_entry_size_mb: {{ .Values.cacheServer.config.maxEntrySizeMB | default 100 }} + + auth: + enabled: {{ .Values.cacheServer.auth.enabled }} + users: + - username: {{ .Values.cacheServer.auth.username | quote }} + + metrics: + enabled: {{ .Values.metrics.enabled }} + + logging: + level: {{ .Values.cacheServer.logging.level | default "info" | quote }} + format: {{ .Values.cacheServer.logging.format | default "json" | quote }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..89ab817 --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,140 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-cache-server + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: cache-server + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: {{ .Values.cacheServer.replicaCount | default 1 }} + selector: + matchLabels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: cache-server + template: + metadata: + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: cache-server + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }} + spec: + {{- if .Values.minio.enabled }} + initContainers: + - name: wait-for-minio + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Waiting for MinIO to be ready..." + until nc -z {{ .Release.Name }}-minio 9000; do + echo "MinIO not ready, waiting..." + sleep 2 + done + echo "MinIO is ready!" + - name: create-bucket + image: minio/mc:latest + command: + - sh + - -c + - | + mc alias set minio http://{{ .Release.Name }}-minio:9000 $MINIO_ACCESS_KEY $MINIO_SECRET_KEY + mc mb --ignore-existing minio/{{ .Values.cacheServer.storage.bucket | default "gradle-cache" }} + echo "Bucket created or already exists" + env: + - name: MINIO_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret | default (printf "%s-secrets" .Release.Name) }} + key: minio-access-key + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret | default (printf "%s-secrets" .Release.Name) }} + key: minio-secret-key + {{- end }} + containers: + - name: cache-server + image: "{{ .Values.cacheServer.image.repository }}:{{ .Values.cacheServer.image.tag }}" + imagePullPolicy: {{ .Values.cacheServer.image.pullPolicy | default "IfNotPresent" }} + ports: + - name: http + containerPort: {{ .Values.cacheServer.port }} + protocol: TCP + env: + - name: MINIO_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret | default (printf "%s-secrets" .Release.Name) }} + key: minio-access-key + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret | default (printf "%s-secrets" .Release.Name) }} + key: minio-secret-key + - name: CACHE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret | default (printf "%s-secrets" .Release.Name) }} + key: cache-password + args: + - --config + - /etc/gradle-cache/config.yaml + resources: + {{- toYaml .Values.cacheServer.resources | nindent 12 }} + livenessProbe: + httpGet: + path: /ping + port: http + {{- if .Values.cacheServer.tls.enabled }} + scheme: HTTPS + {{- end }} + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + {{- if .Values.cacheServer.tls.enabled }} + scheme: HTTPS + {{- end }} + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumeMounts: + - name: config + mountPath: /etc/gradle-cache + readOnly: true + {{- if .Values.cacheServer.tls.enabled }} + - name: tls-certs + mountPath: /etc/certs + readOnly: true + {{- end }} + volumes: + - name: config + configMap: + name: {{ .Release.Name }}-config + {{- if .Values.cacheServer.tls.enabled }} + - name: tls-certs + secret: + secretName: {{ .Values.cacheServer.tls.secretName }} + defaultMode: 0400 + {{- end }} + {{- if .Values.cacheServer.nodeSelector }} + nodeSelector: + {{- toYaml .Values.cacheServer.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.cacheServer.tolerations }} + tolerations: + {{- toYaml .Values.cacheServer.tolerations | nindent 8 }} + {{- end }} diff --git a/chart/templates/minio-service.yaml b/chart/templates/minio-service.yaml new file mode 100644 index 0000000..3ec497e --- /dev/null +++ b/chart/templates/minio-service.yaml @@ -0,0 +1,26 @@ +{{- if .Values.minio.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-minio + labels: + app.kubernetes.io/name: {{ .Chart.Name }}-minio + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: storage + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + type: ClusterIP + ports: + - name: api + port: 9000 + targetPort: api + protocol: TCP + - name: console + port: 9001 + targetPort: console + protocol: TCP + selector: + app.kubernetes.io/name: {{ .Chart.Name }}-minio + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/chart/templates/minio-statefulset.yaml b/chart/templates/minio-statefulset.yaml new file mode 100644 index 0000000..6d41219 --- /dev/null +++ b/chart/templates/minio-statefulset.yaml @@ -0,0 +1,98 @@ +{{- if .Values.minio.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ .Release.Name }}-minio + labels: + app.kubernetes.io/name: {{ .Chart.Name }}-minio + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: storage + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + selector: + matchLabels: + app.kubernetes.io/name: {{ .Chart.Name }}-minio + app.kubernetes.io/instance: {{ .Release.Name }} + serviceName: {{ .Release.Name }}-minio + replicas: 1 + template: + metadata: + labels: + app.kubernetes.io/name: {{ .Chart.Name }}-minio + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: storage + spec: + containers: + - name: minio + image: "{{ .Values.minio.image.repository }}:{{ .Values.minio.image.tag }}" + imagePullPolicy: {{ .Values.minio.image.pullPolicy | default "IfNotPresent" }} + args: + - server + - /data + - --console-address + - ":9001" + ports: + - name: api + containerPort: 9000 + - name: console + containerPort: 9001 + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret | default (printf "%s-secrets" .Release.Name) }} + key: minio-access-key + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret | default (printf "%s-secrets" .Release.Name) }} + key: minio-secret-key + resources: + {{- toYaml .Values.minio.resources | nindent 12 }} + livenessProbe: + httpGet: + path: /minio/health/live + port: api + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /minio/health/ready + port: api + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumeMounts: + - name: data + mountPath: /data + {{- if .Values.minio.nodeSelector }} + nodeSelector: + {{- toYaml .Values.minio.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.minio.tolerations }} + tolerations: + {{- toYaml .Values.minio.tolerations | nindent 8 }} + {{- end }} + {{- if .Values.minio.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: + - ReadWriteOnce + {{- if .Values.minio.persistence.storageClass }} + storageClassName: {{ .Values.minio.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.minio.persistence.size | quote }} + {{- else }} + volumes: + - name: data + emptyDir: {} + {{- end }} +{{- end }} diff --git a/chart/templates/secrets.yaml b/chart/templates/secrets.yaml new file mode 100644 index 0000000..1e34101 --- /dev/null +++ b/chart/templates/secrets.yaml @@ -0,0 +1,16 @@ +{{- if not .Values.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-secrets + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +type: Opaque +data: + minio-access-key: {{ .Values.minio.auth.accessKey | b64enc | quote }} + minio-secret-key: {{ .Values.minio.auth.secretKey | b64enc | quote }} + cache-password: {{ .Values.cacheServer.auth.password | b64enc | quote }} +{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..9a753f7 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-cache + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: cache-server + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.metrics.enabled }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.cacheServer.port }}" + prometheus.io/path: "/metrics" + {{- end }} +spec: + type: {{ .Values.cacheServer.service.type | default "ClusterIP" }} + ports: + - name: http + port: {{ .Values.cacheServer.port }} + targetPort: http + protocol: TCP + selector: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: cache-server diff --git a/chart/templates/tests/test-connection.yaml b/chart/templates/tests/test-connection.yaml new file mode 100644 index 0000000..bde3eb1 --- /dev/null +++ b/chart/templates/tests/test-connection.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ .Release.Name }}-test-connection" + labels: + app.kubernetes.io/name: {{ .Chart.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": hook-succeeded +spec: + containers: + - name: test-ping + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Testing cache server ping endpoint..." + wget -q -O- http://{{ .Release.Name }}-cache:{{ .Values.cacheServer.port }}/ping + if [ $? -eq 0 ]; then + echo "Ping test passed!" + else + echo "Ping test failed!" + exit 1 + fi + + echo "Testing cache server health endpoint..." + wget -q -O- http://{{ .Release.Name }}-cache:{{ .Values.cacheServer.port }}/health + if [ $? -eq 0 ]; then + echo "Health test passed!" + else + echo "Health test failed!" + exit 1 + fi + + {{- if .Values.minio.enabled }} + echo "Testing MinIO connectivity..." + wget -q -O- http://{{ .Release.Name }}-minio:9000/minio/health/live + if [ $? -eq 0 ]; then + echo "MinIO test passed!" + else + echo "MinIO test failed!" + exit 1 + fi + {{- end }} + + echo "All tests passed!" + restartPolicy: Never diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..d966214 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,117 @@ +# Cache Server Configuration +cacheServer: + replicaCount: 1 + + image: + # Update this to your actual registry path after first push + repository: ghcr.io/kevingruber/theia-shared-cache/gradle-cache + tag: "latest" + pullPolicy: IfNotPresent + + port: 8080 + + # TLS Configuration + tls: + enabled: false + # Secret name containing tls.crt and tls.key + secretName: "" + # Optional: Use cert-manager to generate certificates + certManager: + enabled: false + # Name of the ClusterIssuer or Issuer + issuerName: "letsencrypt-prod" + # Type of issuer: ClusterIssuer or Issuer + issuerKind: "ClusterIssuer" + + # Timeouts + readTimeout: 30s + writeTimeout: 120s + + # Service configuration + service: + type: ClusterIP + + # Authentication + auth: + enabled: true + username: "gradle" + # IMPORTANT: Change this password in production! + password: "changeme" + + # Cache configuration + config: + maxEntrySizeMB: 100 + + # Storage configuration (used when minio.enabled=false) + storage: + endpoint: "" + bucket: "gradle-cache" + useSSL: false + + # Logging + logging: + level: "info" + format: "json" + + # Resource limits + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "500m" + + # Optional node selector + nodeSelector: {} + + # Optional tolerations + tolerations: [] + +# MinIO Configuration +minio: + enabled: true + + image: + repository: minio/minio + tag: "latest" + pullPolicy: IfNotPresent + + # Authentication + auth: + # IMPORTANT: Change these credentials in production! + accessKey: "minioadmin" + secretKey: "minioadmin" + + # Persistence + persistence: + enabled: true + size: 50Gi + # Leave empty to use default storage class + storageClass: "" + + # Resource limits + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "1000m" + + # Optional node selector + nodeSelector: {} + + # Optional tolerations + tolerations: [] + +# Metrics/Monitoring +metrics: + enabled: true + serviceMonitor: + # Enable if using Prometheus Operator + enabled: false + +# Use an existing secret instead of creating one +# The secret must contain: minio-access-key, minio-secret-key, cache-password +existingSecret: "" diff --git a/docs/tls-setup.md b/docs/tls-setup.md new file mode 100644 index 0000000..12b07a8 --- /dev/null +++ b/docs/tls-setup.md @@ -0,0 +1,402 @@ +# TLS Setup Guide + +This guide explains how to enable TLS/HTTPS for the Gradle Build Cache Server to secure communication between clients and the cache server. + +## Table of Contents + +- [Why Enable TLS?](#why-enable-tls) +- [Prerequisites](#prerequisites) +- [Option 1: Using cert-manager (Recommended)](#option-1-using-cert-manager-recommended) +- [Option 2: Using Self-Signed Certificates](#option-2-using-self-signed-certificates) +- [Option 3: Using External Certificates](#option-3-using-external-certificates) +- [Verifying TLS Configuration](#verifying-tls-configuration) +- [Client Configuration](#client-configuration) +- [Troubleshooting](#troubleshooting) + +## Why Enable TLS? + +Without TLS, all communication with the cache server is unencrypted, including: +- HTTP Basic Authentication credentials +- Cached build artifacts +- Metadata about your builds + +Enabling TLS provides: +- **Encryption** - All traffic is encrypted in transit +- **Authentication** - Verify you're connecting to the legitimate cache server +- **Integrity** - Prevent tampering with cached artifacts + +## Prerequisites + +Before enabling TLS, ensure you have: +- Kubernetes cluster (v1.19+) +- Helm 3.x installed +- Cache server already deployed (or ready to deploy) +- One of the following: + - cert-manager installed (for automatic certificate management) + - Your own TLS certificates + - OpenSSL installed (for self-signed certificates) + +## Option 1: Using cert-manager (Recommended) + +cert-manager automates certificate creation, renewal, and management. + +### Step 1: Install cert-manager + +If not already installed: + +```bash +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml +``` + +Verify installation: +```bash +kubectl get pods -n cert-manager +``` + +### Step 2: Create a ClusterIssuer + +For Let's Encrypt (production): + +```yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: your-email@example.com + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: nginx +``` + +For self-signed certificates (testing): + +```yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: {} +``` + +Apply the issuer: +```bash +kubectl apply -f clusterissuer.yaml +``` + +### Step 3: Configure Helm Values + +Update `chart/values.yaml`: + +```yaml +cacheServer: + tls: + enabled: true + secretName: gradle-cache-tls + certManager: + enabled: true + issuerName: "selfsigned-issuer" # or "letsencrypt-prod" + issuerKind: "ClusterIssuer" +``` + +### Step 4: Deploy with Helm + +```bash +helm upgrade --install theia-cache ./chart +``` + +cert-manager will automatically: +1. Create a Certificate resource +2. Request a certificate from the issuer +3. Store the certificate in the specified secret +4. Renew the certificate before expiration + +### Step 5: Verify Certificate + +```bash +kubectl get certificate +kubectl describe certificate theia-cache-tls +kubectl get secret gradle-cache-tls +``` + +## Option 2: Using Self-Signed Certificates + +For development or internal use without cert-manager. + +### Step 1: Generate Self-Signed Certificate + +```bash +# Generate private key +openssl genrsa -out tls.key 2048 + +# Generate certificate (valid for 365 days) +openssl req -new -x509 -key tls.key -out tls.crt -days 365 \ + -subj "/CN=theia-cache-cache-server.default.svc.cluster.local" \ + -addext "subjectAltName=DNS:theia-cache-cache-server,DNS:theia-cache-cache-server.default,DNS:theia-cache-cache-server.default.svc,DNS:theia-cache-cache-server.default.svc.cluster.local" +``` + +**Note**: Replace `default` with your namespace and `theia-cache` with your release name. + +### Step 2: Create Kubernetes Secret + +```bash +kubectl create secret tls gradle-cache-tls \ + --cert=tls.crt \ + --key=tls.key \ + --namespace=default +``` + +### Step 3: Configure Helm Values + +Update `chart/values.yaml`: + +```yaml +cacheServer: + tls: + enabled: true + secretName: gradle-cache-tls + certManager: + enabled: false # Not using cert-manager +``` + +### Step 4: Deploy with Helm + +```bash +helm upgrade --install theia-cache ./chart +``` + +### Step 5: Trust the Certificate (Clients) + +Since the certificate is self-signed, clients will need to either: + +**Option A**: Trust the certificate +```bash +# Copy tls.crt to your client machine +sudo cp tls.crt /usr/local/share/ca-certificates/gradle-cache.crt +sudo update-ca-certificates +``` + +**Option B**: Configure Gradle to skip verification (NOT RECOMMENDED for production) +```groovy +// settings.gradle +buildCache { + remote(HttpBuildCache) { + url = 'https://theia-cache-cache-server:8080/cache/' + allowUntrustedServer = true // Only for self-signed certs + credentials { + username = 'gradle' + password = 'your-password' + } + } +} +``` + +## Option 3: Using External Certificates + +If you have certificates from a trusted CA (e.g., purchased SSL certificate). + +### Step 1: Prepare Certificate Files + +Ensure you have: +- `tls.crt` - Your certificate (including intermediate certificates) +- `tls.key` - Your private key + +### Step 2: Create Kubernetes Secret + +```bash +kubectl create secret tls gradle-cache-tls \ + --cert=path/to/tls.crt \ + --key=path/to/tls.key \ + --namespace=default +``` + +### Step 3: Configure Helm Values + +```yaml +cacheServer: + tls: + enabled: true + secretName: gradle-cache-tls + certManager: + enabled: false +``` + +### Step 4: Deploy with Helm + +```bash +helm upgrade --install theia-cache ./chart +``` + +## Verifying TLS Configuration + +### Check Server Logs + +```bash +kubectl logs -f deployment/theia-cache-cache-server +``` + +Look for: +``` +{"level":"info","addr":":8080","mode":"https","message":"starting server with TLS"} +``` + +### Test with curl + +```bash +# Port-forward the service +kubectl port-forward svc/theia-cache-cache-server 8080:8080 + +# Test HTTPS endpoint +curl -k https://localhost:8080/ping +# Expected: pong +``` + +### Test with OpenSSL + +```bash +openssl s_client -connect localhost:8080 -showcerts +``` + +### Check Health Probes + +```bash +kubectl describe pod -l app.kubernetes.io/component=cache-server +``` + +Ensure liveness and readiness probes are succeeding. + +## Client Configuration + +### Gradle Configuration with TLS + +Update your `settings.gradle`: + +```groovy +buildCache { + remote(HttpBuildCache) { + url = 'https://theia-cache-cache-server:8080/cache/' + credentials { + username = 'gradle' + password = System.getenv('GRADLE_CACHE_PASSWORD') + } + push = true + } +} +``` + +Or `gradle.properties`: +```properties +org.gradle.caching=true +org.gradle.caching.remote.url=https://theia-cache-cache-server:8080/cache/ +org.gradle.caching.remote.username=gradle +org.gradle.caching.remote.password=${GRADLE_CACHE_PASSWORD} +org.gradle.caching.remote.push=true +``` + +### CI/CD Configuration + +**GitHub Actions:** +```yaml +- name: Build with Gradle + env: + GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} + run: ./gradlew build --build-cache +``` + +**GitLab CI:** +```yaml +build: + script: + - ./gradlew build --build-cache + variables: + GRADLE_CACHE_PASSWORD: $GRADLE_CACHE_PASSWORD +``` + +**Jenkins:** +```groovy +withCredentials([string(credentialsId: 'gradle-cache-password', variable: 'GRADLE_CACHE_PASSWORD')]) { + sh './gradlew build --build-cache' +} +``` + +## Troubleshooting + +### Certificate Not Found + +**Error**: `failed to read config file: server.tls.cert_file is required when TLS is enabled` + +**Solution**: Ensure the TLS secret exists and is mounted correctly: +```bash +kubectl get secret gradle-cache-tls +kubectl describe pod theia-cache-cache-server-xxx +``` + +### Health Probes Failing + +**Error**: Readiness probe failed: HTTP probe failed + +**Solution**: Check if health probes are using the correct scheme (HTTPS): +```bash +kubectl get pod theia-cache-cache-server-xxx -o yaml | grep -A 5 livenessProbe +``` + +Should show `scheme: HTTPS`. + +### Certificate Expired + +**Error**: `x509: certificate has expired` + +**Solution**: +- If using cert-manager, it should auto-renew. Check cert-manager logs. +- If using manual certificates, regenerate and update the secret. + +### Certificate Name Mismatch + +**Error**: `x509: certificate is valid for X, not Y` + +**Solution**: Regenerate certificate with correct DNS names in SAN field: +```bash +-addext "subjectAltName=DNS:service-name,DNS:service-name.namespace.svc.cluster.local" +``` + +### Self-Signed Certificate Rejected + +**Error**: `unable to get local issuer certificate` + +**Solution**: Either: +1. Add certificate to client's trust store +2. Use `allowUntrustedServer = true` in Gradle (development only) + +### TLS Handshake Errors + +**Error**: `TLS handshake timeout` + +**Solution**: Ensure the certificate and key match: +```bash +openssl x509 -noout -modulus -in tls.crt | openssl md5 +openssl rsa -noout -modulus -in tls.key | openssl md5 +# Should produce identical MD5 hashes +``` + +## Best Practices + +1. **Use cert-manager for production** - Automates certificate renewal +2. **Use Let's Encrypt for public services** - Free, trusted certificates +3. **Use self-signed only for development** - Requires manual trust management +4. **Monitor certificate expiration** - Set up alerts 30 days before expiry +5. **Rotate certificates regularly** - Even if not expired +6. **Secure private keys** - Use Kubernetes RBAC to restrict secret access +7. **Use strong key sizes** - Minimum 2048-bit RSA or 256-bit ECDSA + +## Additional Resources + +- [cert-manager Documentation](https://cert-manager.io/docs/) +- [Kubernetes TLS Secrets](https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets) +- [Let's Encrypt](https://letsencrypt.org/) +- [Gradle Build Cache Documentation](https://docs.gradle.org/current/userguide/build_cache.html) diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..70275ed --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,28 @@ +# Binaries +gradle-cache +*.exe +*.dll +*.so +*.dylib + +# Test binary +*.test + +# Output of go coverage +coverage.out +coverage.html + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store + +# Local config overrides +configs/config.local.yaml + +# Docker volumes (if not using named volumes) +data/ diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..6124f02 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,42 @@ +# Build stage +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache git + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /gradle-cache ./cmd/server + +# Runtime stage +FROM alpine:3.19 + +# Add ca-certificates for HTTPS and create non-root user +RUN apk --no-cache add ca-certificates && \ + addgroup -g 1000 -S appgroup && \ + adduser -u 1000 -S appuser -G appgroup + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /gradle-cache . + +# Use non-root user +USER appuser + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ping || exit 1 + +ENTRYPOINT ["./gradle-cache"] diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..ce24787 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,71 @@ +.PHONY: build run test clean docker-build docker-up docker-down lint fmt + +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOTEST=$(GOCMD) test +GOMOD=$(GOCMD) mod +GOFMT=gofmt +BINARY_NAME=gradle-cache + +# Build the application +build: + $(GOBUILD) -o $(BINARY_NAME) ./cmd/server + +# Run the application locally +run: build + ./$(BINARY_NAME) -config configs/config.yaml + +# Run tests +test: + $(GOTEST) -v ./... + +# Run tests with coverage +test-coverage: + $(GOTEST) -v -coverprofile=coverage.out ./... + $(GOCMD) tool cover -html=coverage.out -o coverage.html + +# Clean build artifacts +clean: + rm -f $(BINARY_NAME) + rm -f coverage.out coverage.html + +# Download dependencies +deps: + $(GOMOD) download + $(GOMOD) tidy + +# Format code +fmt: + $(GOFMT) -s -w . + +# Lint code (requires golangci-lint) +lint: + golangci-lint run + +# Build Docker image +docker-build: + docker build -t gradle-cache:latest . + +# Start all services with Docker Compose +docker-up: + cd deployments && docker-compose up -d + +# Stop all services +docker-down: + cd deployments && docker-compose down + +# View logs +docker-logs: + cd deployments && docker-compose logs -f + +# Rebuild and restart +docker-restart: docker-down docker-build docker-up + +# Quick health check +health: + curl -s http://localhost:8080/health | jq . + +# Check metrics +metrics: + curl -s http://localhost:8080/metrics | grep gradle_cache diff --git a/src/cmd/server/main.go b/src/cmd/server/main.go new file mode 100644 index 0000000..4e0268d --- /dev/null +++ b/src/cmd/server/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "flag" + "os" + "os/signal" + "syscall" + + "github.com/kevingruber/gradle-cache/internal/config" + "github.com/kevingruber/gradle-cache/internal/server" + "github.com/kevingruber/gradle-cache/internal/storage" + "github.com/rs/zerolog" +) + +func main() { + // Parse command line flags + configPath := flag.String("config", "", "Path to configuration file") + flag.Parse() + + // Load configuration + cfg, err := config.Load(*configPath) + if err != nil { + panic("failed to load configuration: " + err.Error()) + } + + // Setup logger + logger := setupLogger(cfg.Logging) + + // Validate configuration + if err := cfg.Validate(); err != nil { + logger.Fatal().Err(err).Msg("invalid configuration") + } + + // Create storage + store, err := storage.NewMinIOStorage(storage.MinIOConfig{ + Endpoint: cfg.Storage.Endpoint, + AccessKey: cfg.Storage.AccessKey, + SecretKey: cfg.Storage.SecretKey, + Bucket: cfg.Storage.Bucket, + UseSSL: cfg.Storage.UseSSL, + }) + if err != nil { + logger.Fatal().Err(err).Msg("failed to create storage") + } + + // Ensure bucket exists + ctx := context.Background() + if err := store.EnsureBucket(ctx); err != nil { + logger.Fatal().Err(err).Msg("failed to ensure bucket exists") + } + logger.Info().Str("bucket", cfg.Storage.Bucket).Msg("storage bucket ready") + + // Create and run server + srv := server.New(cfg, store, logger) + + // Setup graceful shutdown + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + go func() { + sig := <-sigCh + logger.Info().Str("signal", sig.String()).Msg("received shutdown signal") + cancel() + }() + + // Run server + if err := srv.Run(ctx); err != nil { + logger.Fatal().Err(err).Msg("server error") + } + + logger.Info().Msg("server stopped") +} + +func setupLogger(cfg config.LoggingConfig) zerolog.Logger { + // Set log level + level, err := zerolog.ParseLevel(cfg.Level) + if err != nil { + level = zerolog.InfoLevel + } + zerolog.SetGlobalLevel(level) + + // Configure output format + var logger zerolog.Logger + if cfg.Format == "json" { + logger = zerolog.New(os.Stdout).With().Timestamp().Logger() + } else { + logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}). + With().Timestamp().Logger() + } + + return logger +} diff --git a/src/configs/config.yaml b/src/configs/config.yaml new file mode 100644 index 0000000..46d5b45 --- /dev/null +++ b/src/configs/config.yaml @@ -0,0 +1,29 @@ +server: + port: 8080 + read_timeout: 30s + write_timeout: 120s + +storage: + endpoint: "minio:9000" + # These are overridden by environment variables + access_key: "${MINIO_ACCESS_KEY}" + secret_key: "${MINIO_SECRET_KEY}" + bucket: "gradle-cache" + use_ssl: false + +cache: + max_entry_size_mb: 100 + +auth: + enabled: true + users: + - username: "gradle" + # Overridden by CACHE_PASSWORD environment variable + password: "${CACHE_PASSWORD}" + +metrics: + enabled: true + +logging: + level: "info" + format: "json" diff --git a/src/deployments/docker-compose.yaml b/src/deployments/docker-compose.yaml new file mode 100644 index 0000000..e5fe2d8 --- /dev/null +++ b/src/deployments/docker-compose.yaml @@ -0,0 +1,52 @@ +services: + cache-server: + build: + context: .. + dockerfile: Dockerfile + ports: + - "8080:8080" + environment: + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin + - CACHE_PASSWORD=changeme + volumes: + - ../configs/config.yaml:/app/config.yaml:ro + command: ["-config", "/app/config.yaml"] + depends_on: + minio: + condition: service_healthy + restart: unless-stopped + + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + ports: + - "9000:9000" + - "9001:9001" + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin + volumes: + - minio-data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + restart: unless-stopped + +volumes: + minio-data: + prometheus-data: diff --git a/src/deployments/prometheus.yaml b/src/deployments/prometheus.yaml new file mode 100644 index 0000000..2dbf733 --- /dev/null +++ b/src/deployments/prometheus.yaml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'gradle-cache' + static_configs: + - targets: ['cache-server:8080'] + metrics_path: /metrics diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..99e11cc --- /dev/null +++ b/src/go.mod @@ -0,0 +1,73 @@ +module github.com/kevingruber/gradle-cache + +go 1.24.3 + +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/minio/minio-go/v7 v7.0.97 + github.com/prometheus/client_golang v1.23.2 + github.com/rs/zerolog v1.34.0 + github.com/spf13/viper v1.21.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/crc64nvme v1.1.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..3a9a93f --- /dev/null +++ b/src/go.sum @@ -0,0 +1,173 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= +github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= +github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/internal/config/config.go b/src/internal/config/config.go new file mode 100644 index 0000000..7d97a2d --- /dev/null +++ b/src/internal/config/config.go @@ -0,0 +1,157 @@ +package config + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/viper" +) + +type Config struct { + Server ServerConfig `mapstructure:"server"` + Storage StorageConfig `mapstructure:"storage"` + Cache CacheConfig `mapstructure:"cache"` + Auth AuthConfig `mapstructure:"auth"` + Metrics MetricsConfig `mapstructure:"metrics"` + Logging LoggingConfig `mapstructure:"logging"` +} + +type ServerConfig struct { + Port int `mapstructure:"port"` + ReadTimeout time.Duration `mapstructure:"read_timeout"` + WriteTimeout time.Duration `mapstructure:"write_timeout"` + TLS TLSConfig `mapstructure:"tls"` +} + +type TLSConfig struct { + Enabled bool `mapstructure:"enabled"` + CertFile string `mapstructure:"cert_file"` + KeyFile string `mapstructure:"key_file"` +} + +type StorageConfig struct { + Endpoint string `mapstructure:"endpoint"` + AccessKey string `mapstructure:"access_key"` + SecretKey string `mapstructure:"secret_key"` + Bucket string `mapstructure:"bucket"` + UseSSL bool `mapstructure:"use_ssl"` +} + +type CacheConfig struct { + MaxEntrySizeMB int64 `mapstructure:"max_entry_size_mb"` +} + +type AuthConfig struct { + Enabled bool `mapstructure:"enabled"` + Users []UserAuth `mapstructure:"users"` +} + +type UserAuth struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` +} + +type MetricsConfig struct { + Enabled bool `mapstructure:"enabled"` +} + +type LoggingConfig struct { + Level string `mapstructure:"level"` + Format string `mapstructure:"format"` +} + +func Load(configPath string) (*Config, error) { + v := viper.New() + + // Set defaults + v.SetDefault("server.port", 8080) + v.SetDefault("server.read_timeout", "30s") + v.SetDefault("server.write_timeout", "120s") + v.SetDefault("server.tls.enabled", false) + v.SetDefault("server.tls.cert_file", "/etc/certs/tls.crt") + v.SetDefault("server.tls.key_file", "/etc/certs/tls.key") + + v.SetDefault("storage.endpoint", "localhost:9000") + v.SetDefault("storage.bucket", "gradle-cache") + v.SetDefault("storage.use_ssl", false) + + v.SetDefault("cache.max_entry_size_mb", 100) + + v.SetDefault("auth.enabled", true) + + v.SetDefault("metrics.enabled", true) + + v.SetDefault("logging.level", "info") + v.SetDefault("logging.format", "json") + + // Read from config file if provided + if configPath != "" { + v.SetConfigFile(configPath) + if err := v.ReadInConfig(); err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + } + + // Enable environment variable overrides + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.AutomaticEnv() + + // Bind specific environment variables + v.BindEnv("storage.access_key", "MINIO_ACCESS_KEY") + v.BindEnv("storage.secret_key", "MINIO_SECRET_KEY") + v.BindEnv("auth.users.0.password", "CACHE_PASSWORD") + + var cfg Config + if err := v.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + // Handle CACHE_PASSWORD environment variable for default user + if cachePassword := v.GetString("CACHE_PASSWORD"); cachePassword != "" { + if len(cfg.Auth.Users) > 0 { + cfg.Auth.Users[0].Password = cachePassword + } + } + + return &cfg, nil +} + +func (c *Config) Validate() error { + if c.Storage.Endpoint == "" { + return fmt.Errorf("storage.endpoint is required") + } + if c.Storage.AccessKey == "" { + return fmt.Errorf("storage.access_key is required") + } + if c.Storage.SecretKey == "" { + return fmt.Errorf("storage.secret_key is required") + } + if c.Storage.Bucket == "" { + return fmt.Errorf("storage.bucket is required") + } + if c.Auth.Enabled && len(c.Auth.Users) == 0 { + return fmt.Errorf("auth.users is required when auth is enabled") + } + for i, user := range c.Auth.Users { + if user.Username == "" { + return fmt.Errorf("auth.users[%d].username is required", i) + } + if user.Password == "" { + return fmt.Errorf("auth.users[%d].password is required", i) + } + } + if c.Server.TLS.Enabled { + if c.Server.TLS.CertFile == "" { + return fmt.Errorf("server.tls.cert_file is required when TLS is enabled") + } + if c.Server.TLS.KeyFile == "" { + return fmt.Errorf("server.tls.key_file is required when TLS is enabled") + } + } + return nil +} + +func (c *Config) MaxEntrySizeBytes() int64 { + return c.Cache.MaxEntrySizeMB * 1024 * 1024 +} diff --git a/src/internal/handler/cache.go b/src/internal/handler/cache.go new file mode 100644 index 0000000..cda0b3c --- /dev/null +++ b/src/internal/handler/cache.go @@ -0,0 +1,149 @@ +package handler + +import ( + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/kevingruber/gradle-cache/internal/storage" + "github.com/rs/zerolog" +) + +// CacheHandler handles Gradle build cache HTTP requests. +type CacheHandler struct { + storage storage.Storage + maxEntrySize int64 + logger zerolog.Logger +} + +// NewCacheHandler creates a new cache handler. +func NewCacheHandler(store storage.Storage, maxEntrySize int64, logger zerolog.Logger) *CacheHandler { + return &CacheHandler{ + storage: store, + maxEntrySize: maxEntrySize, + logger: logger, + } +} + +// Get handles GET requests to retrieve cache entries. +// Gradle expects: 200 with body on hit, 404 on miss. +func (h *CacheHandler) Get(c *gin.Context) { + key := c.Param("key") + if key == "" { + c.Status(http.StatusBadRequest) + return + } + + reader, size, err := h.storage.Get(c.Request.Context(), key) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + c.Status(http.StatusNotFound) + return + } + h.logger.Error().Err(err).Str("key", key).Msg("failed to get cache entry") + c.Status(http.StatusInternalServerError) + return + } + defer reader.Close() + + c.Header("Content-Type", "application/octet-stream") + c.Header("Content-Length", string(rune(size))) + + c.DataFromReader(http.StatusOK, size, "application/octet-stream", reader, nil) +} + +// Put handles PUT requests to store cache entries. +// Gradle expects: 2xx on success, 413 if too large. +func (h *CacheHandler) Put(c *gin.Context) { + key := c.Param("key") + if key == "" { + c.Status(http.StatusBadRequest) + return + } + + // Check Content-Length header for size validation + contentLength := c.Request.ContentLength + if contentLength > h.maxEntrySize { + h.logger.Warn(). + Str("key", key). + Int64("size", contentLength). + Int64("max_size", h.maxEntrySize). + Msg("cache entry too large") + c.Status(http.StatusRequestEntityTooLarge) + return + } + + // Handle Expect: 100-continue + // Gin/Go handles this automatically, but we validate size first + + // For chunked transfers or unknown size, we need to handle differently + if contentLength < 0 { + // Read with size limit + limitedReader := io.LimitReader(c.Request.Body, h.maxEntrySize+1) + data, err := io.ReadAll(limitedReader) + if err != nil { + h.logger.Error().Err(err).Str("key", key).Msg("failed to read request body") + c.Status(http.StatusInternalServerError) + return + } + + if int64(len(data)) > h.maxEntrySize { + c.Status(http.StatusRequestEntityTooLarge) + return + } + + contentLength = int64(len(data)) + c.Request.Body = io.NopCloser(io.NewSectionReader( + &bytesReaderAt{data: data}, 0, contentLength, + )) + } + + err := h.storage.Put(c.Request.Context(), key, c.Request.Body, contentLength) + if err != nil { + h.logger.Error().Err(err).Str("key", key).Msg("failed to store cache entry") + c.Status(http.StatusInternalServerError) + return + } + + c.Status(http.StatusCreated) +} + +// Head handles HEAD requests to check cache entry existence. +func (h *CacheHandler) Head(c *gin.Context) { + key := c.Param("key") + if key == "" { + c.Status(http.StatusBadRequest) + return + } + + exists, err := h.storage.Exists(c.Request.Context(), key) + if err != nil { + h.logger.Error().Err(err).Str("key", key).Msg("failed to check cache entry existence") + c.Status(http.StatusInternalServerError) + return + } + + if !exists { + c.Status(http.StatusNotFound) + return + } + + c.Status(http.StatusOK) +} + +// bytesReaderAt implements io.ReaderAt for a byte slice. +type bytesReaderAt struct { + data []byte +} + +func (b *bytesReaderAt) ReadAt(p []byte, off int64) (n int, err error) { + if off >= int64(len(b.data)) { + return 0, io.EOF + } + n = copy(p, b.data[off:]) + if n < len(p) { + err = io.EOF + } + return +} diff --git a/src/internal/middleware/auth.go b/src/internal/middleware/auth.go new file mode 100644 index 0000000..2c63288 --- /dev/null +++ b/src/internal/middleware/auth.go @@ -0,0 +1,45 @@ +package middleware + +import ( + "crypto/subtle" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/kevingruber/gradle-cache/internal/config" +) + +// BasicAuth creates a middleware that validates HTTP Basic Authentication. +func BasicAuth(users []config.UserAuth) gin.HandlerFunc { + // Build a map for O(1) lookup + credentials := make(map[string]string, len(users)) + for _, user := range users { + credentials[user.Username] = user.Password + } + + return func(c *gin.Context) { + username, password, ok := c.Request.BasicAuth() + if !ok { + c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + expectedPassword, userExists := credentials[username] + if !userExists { + c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Use constant-time comparison to prevent timing attacks + if subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) != 1 { + c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Store username in context for logging/metrics + c.Set("username", username) + c.Next() + } +} diff --git a/src/internal/middleware/logging.go b/src/internal/middleware/logging.go new file mode 100644 index 0000000..4df3c16 --- /dev/null +++ b/src/internal/middleware/logging.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" +) + +// RequestLogger creates a middleware that logs HTTP requests. +func RequestLogger(logger zerolog.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + method := c.Request.Method + + // Process request + c.Next() + + // Calculate latency + latency := time.Since(start) + status := c.Writer.Status() + size := c.Writer.Size() + + // Build log event + event := logger.Info() + if status >= 500 { + event = logger.Error() + } else if status >= 400 { + event = logger.Warn() + } + + event. + Str("method", method). + Str("path", path). + Int("status", status). + Int("size", size). + Dur("latency", latency). + Str("client_ip", c.ClientIP()) + + // Add username if authenticated + if username, exists := c.Get("username"); exists { + event.Str("user", username.(string)) + } + + // Add cache key if present + if key := c.Param("key"); key != "" { + event.Str("cache_key", key) + } + + // Add error if present + if len(c.Errors) > 0 { + event.Str("error", c.Errors.String()) + } + + event.Msg("request") + } +} diff --git a/src/internal/middleware/metrics.go b/src/internal/middleware/metrics.go new file mode 100644 index 0000000..58da128 --- /dev/null +++ b/src/internal/middleware/metrics.go @@ -0,0 +1,108 @@ +package middleware + +import ( + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// Metrics holds all Prometheus metrics for the cache server. +type Metrics struct { + RequestsTotal *prometheus.CounterVec + RequestDuration *prometheus.HistogramVec + CacheHitsTotal prometheus.Counter + CacheMissesTotal prometheus.Counter + StoredBytesTotal prometheus.Counter + EntrySize prometheus.Histogram +} + +// NewMetrics creates and registers all Prometheus metrics. +func NewMetrics(namespace string) *Metrics { + return &Metrics{ + RequestsTotal: promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "status"}, + ), + RequestDuration: promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: namespace, + Name: "request_duration_seconds", + Help: "HTTP request duration in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"method"}, + ), + CacheHitsTotal: promauto.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "cache_hits_total", + Help: "Total number of cache hits", + }, + ), + CacheMissesTotal: promauto.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "cache_misses_total", + Help: "Total number of cache misses", + }, + ), + StoredBytesTotal: promauto.NewCounter( + prometheus.CounterOpts{ + Namespace: namespace, + Name: "stored_bytes_total", + Help: "Total bytes stored in cache", + }, + ), + EntrySize: promauto.NewHistogram( + prometheus.HistogramOpts{ + Namespace: namespace, + Name: "entry_size_bytes", + Help: "Size of cache entries in bytes", + Buckets: prometheus.ExponentialBuckets(1024, 2, 20), // 1KB to 1GB + }, + ), + } +} + +// Middleware creates a Gin middleware that records metrics. +func (m *Metrics) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + // Process request + c.Next() + + // Record metrics + duration := time.Since(start).Seconds() + status := strconv.Itoa(c.Writer.Status()) + method := c.Request.Method + + m.RequestsTotal.WithLabelValues(method, status).Inc() + m.RequestDuration.WithLabelValues(method).Observe(duration) + + // Record cache hit/miss for GET requests + if method == "GET" && c.Param("key") != "" { + if c.Writer.Status() == 200 { + m.CacheHitsTotal.Inc() + } else if c.Writer.Status() == 404 { + m.CacheMissesTotal.Inc() + } + } + + // Record stored bytes for PUT requests + if method == "PUT" && c.Writer.Status() >= 200 && c.Writer.Status() < 300 { + size := c.Request.ContentLength + if size > 0 { + m.StoredBytesTotal.Add(float64(size)) + m.EntrySize.Observe(float64(size)) + } + } + } +} diff --git a/src/internal/server/server.go b/src/internal/server/server.go new file mode 100644 index 0000000..6edb3a9 --- /dev/null +++ b/src/internal/server/server.go @@ -0,0 +1,163 @@ +package server + +import ( + "context" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/kevingruber/gradle-cache/internal/config" + "github.com/kevingruber/gradle-cache/internal/handler" + "github.com/kevingruber/gradle-cache/internal/middleware" + "github.com/kevingruber/gradle-cache/internal/storage" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" +) + +// Server represents the HTTP server. +type Server struct { + cfg *config.Config + router *gin.Engine + storage storage.Storage + logger zerolog.Logger + metrics *middleware.Metrics +} + +// New creates a new server instance. +func New(cfg *config.Config, store storage.Storage, logger zerolog.Logger) *Server { + // Set Gin mode based on log level + if cfg.Logging.Level == "debug" { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + + s := &Server{ + cfg: cfg, + router: gin.New(), + storage: store, + logger: logger, + } + + // Initialize metrics if enabled + if cfg.Metrics.Enabled { + s.metrics = middleware.NewMetrics("gradle_cache") + } + + s.setupRoutes() + return s +} + +// setupRoutes configures all HTTP routes. +func (s *Server) setupRoutes() { + // Recovery middleware + s.router.Use(gin.Recovery()) + + // Logging middleware + s.router.Use(middleware.RequestLogger(s.logger)) + + // Metrics middleware + if s.metrics != nil { + s.router.Use(s.metrics.Middleware()) + } + + // Health endpoints (no auth required) + s.router.GET("/ping", s.handlePing) + s.router.GET("/health", s.handleHealth) + + // Metrics endpoint (no auth required) + if s.cfg.Metrics.Enabled { + s.router.GET("/metrics", gin.WrapH(promhttp.Handler())) + } + + // Cache endpoints + cacheHandler := handler.NewCacheHandler( + s.storage, + s.cfg.MaxEntrySizeBytes(), + s.logger, + ) + + // Create cache group with optional auth + cacheGroup := s.router.Group("/cache") + if s.cfg.Auth.Enabled { + cacheGroup.Use(middleware.BasicAuth(s.cfg.Auth.Users)) + } + + cacheGroup.GET("/:key", cacheHandler.Get) + cacheGroup.PUT("/:key", cacheHandler.Put) + cacheGroup.HEAD("/:key", cacheHandler.Head) +} + +// handlePing is a simple health check endpoint. +func (s *Server) handlePing(c *gin.Context) { + c.String(http.StatusOK, "pong") +} + +// handleHealth performs a detailed health check including storage connectivity. +func (s *Server) handleHealth(c *gin.Context) { + ctx := c.Request.Context() + + if err := s.storage.Ping(ctx); err != nil { + s.logger.Error().Err(err).Msg("health check failed: storage unreachable") + c.JSON(http.StatusServiceUnavailable, gin.H{ + "status": "unhealthy", + "storage": "unreachable", + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "storage": "connected", + }) +} + +// Run starts the HTTP server. +func (s *Server) Run(ctx context.Context) error { + addr := fmt.Sprintf(":%d", s.cfg.Server.Port) + + srv := &http.Server{ + Addr: addr, + Handler: s.router, + ReadTimeout: s.cfg.Server.ReadTimeout, + WriteTimeout: s.cfg.Server.WriteTimeout, + } + + // Channel to capture server errors + errCh := make(chan error, 1) + + go func() { + if s.cfg.Server.TLS.Enabled { + s.logger.Info(). + Str("addr", addr). + Str("mode", "https"). + Msg("starting server with TLS") + if err := srv.ListenAndServeTLS(s.cfg.Server.TLS.CertFile, s.cfg.Server.TLS.KeyFile); err != nil && err != http.ErrServerClosed { + errCh <- err + } + } else { + s.logger.Info(). + Str("addr", addr). + Str("mode", "http"). + Msg("starting server") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errCh <- err + } + } + }() + + // Wait for context cancellation or server error + select { + case <-ctx.Done(): + s.logger.Info().Msg("shutting down server") + return srv.Shutdown(context.Background()) + case err := <-errCh: + return err + } +} + +// Router returns the Gin router for testing purposes. +func (s *Server) Router() *gin.Engine { + return s.router +} diff --git a/src/internal/storage/minio.go b/src/internal/storage/minio.go new file mode 100644 index 0000000..590e325 --- /dev/null +++ b/src/internal/storage/minio.go @@ -0,0 +1,150 @@ +package storage + +import ( + "context" + "fmt" + "io" + "path" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// MinIOStorage implements Storage using MinIO as the backend. +type MinIOStorage struct { + client *minio.Client + bucket string + namespace string +} + +// MinIOConfig holds the configuration for MinIO connection. +type MinIOConfig struct { + Endpoint string + AccessKey string + SecretKey string + Bucket string + UseSSL bool +} + +// NewMinIOStorage creates a new MinIO storage instance. +func NewMinIOStorage(cfg MinIOConfig) (*MinIOStorage, error) { + client, err := minio.New(cfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), + Secure: cfg.UseSSL, + }) + if err != nil { + return nil, fmt.Errorf("failed to create MinIO client: %w", err) + } + + return &MinIOStorage{ + client: client, + bucket: cfg.Bucket, + }, nil +} + +// EnsureBucket creates the bucket if it doesn't exist. +func (s *MinIOStorage) EnsureBucket(ctx context.Context) error { + exists, err := s.client.BucketExists(ctx, s.bucket) + if err != nil { + return fmt.Errorf("failed to check bucket existence: %w", err) + } + + if !exists { + err = s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}) + if err != nil { + return fmt.Errorf("failed to create bucket: %w", err) + } + } + + return nil +} + +// objectKey returns the full object key including namespace prefix. +func (s *MinIOStorage) objectKey(key string) string { + if s.namespace == "" { + return key + } + return path.Join(s.namespace, key) +} + +// Get retrieves a cache entry from MinIO. +func (s *MinIOStorage) Get(ctx context.Context, key string) (io.ReadCloser, int64, error) { + objectKey := s.objectKey(key) + + obj, err := s.client.GetObject(ctx, s.bucket, objectKey, minio.GetObjectOptions{}) + if err != nil { + return nil, 0, fmt.Errorf("failed to get object: %w", err) + } + + stat, err := obj.Stat() + if err != nil { + obj.Close() + errResp := minio.ToErrorResponse(err) + if errResp.Code == "NoSuchKey" { + return nil, 0, ErrNotFound + } + return nil, 0, fmt.Errorf("failed to stat object: %w", err) + } + + return obj, stat.Size, nil +} + +// Put stores a cache entry in MinIO. +func (s *MinIOStorage) Put(ctx context.Context, key string, reader io.Reader, size int64) error { + objectKey := s.objectKey(key) + + _, err := s.client.PutObject(ctx, s.bucket, objectKey, reader, size, minio.PutObjectOptions{ + ContentType: "application/octet-stream", + }) + if err != nil { + return fmt.Errorf("failed to put object: %w", err) + } + + return nil +} + +// Exists checks if a cache entry exists in MinIO. +func (s *MinIOStorage) Exists(ctx context.Context, key string) (bool, error) { + objectKey := s.objectKey(key) + + _, err := s.client.StatObject(ctx, s.bucket, objectKey, minio.StatObjectOptions{}) + if err != nil { + errResp := minio.ToErrorResponse(err) + if errResp.Code == "NoSuchKey" { + return false, nil + } + return false, fmt.Errorf("failed to stat object: %w", err) + } + + return true, nil +} + +// Delete removes a cache entry from MinIO. +func (s *MinIOStorage) Delete(ctx context.Context, key string) error { + objectKey := s.objectKey(key) + + err := s.client.RemoveObject(ctx, s.bucket, objectKey, minio.RemoveObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to remove object: %w", err) + } + + return nil +} + +// Ping checks if MinIO is reachable. +func (s *MinIOStorage) Ping(ctx context.Context) error { + _, err := s.client.BucketExists(ctx, s.bucket) + if err != nil { + return fmt.Errorf("failed to ping MinIO: %w", err) + } + return nil +} + +// WithNamespace returns a new MinIOStorage instance scoped to the given namespace. +func (s *MinIOStorage) WithNamespace(namespace string) Storage { + return &MinIOStorage{ + client: s.client, + bucket: s.bucket, + namespace: namespace, + } +} diff --git a/src/internal/storage/storage.go b/src/internal/storage/storage.go new file mode 100644 index 0000000..98d0d8b --- /dev/null +++ b/src/internal/storage/storage.go @@ -0,0 +1,42 @@ +package storage + +import ( + "context" + "errors" + "io" +) + +var ( + ErrNotFound = errors.New("cache entry not found") +) + +// Storage defines the interface for cache storage backends. +// This abstraction allows for different implementations (MinIO, S3, filesystem, etc.) +type Storage interface { + // Get retrieves a cache entry by key. + // Returns the content reader, content size, and any error. + // Returns ErrNotFound if the entry does not exist. + Get(ctx context.Context, key string) (io.ReadCloser, int64, error) + + // Put stores a cache entry. + // The size parameter is the content length for the upload. + Put(ctx context.Context, key string, reader io.Reader, size int64) error + + // Exists checks if a cache entry exists. + Exists(ctx context.Context, key string) (bool, error) + + // Delete removes a cache entry. + // Returns nil if the entry does not exist. + Delete(ctx context.Context, key string) error + + // Ping checks if the storage backend is reachable. + Ping(ctx context.Context) error +} + +// NamespacedStorage extends Storage with namespace support. +// This will be used for per-exercise cache isolation in the future. +type NamespacedStorage interface { + Storage + // WithNamespace returns a new Storage instance scoped to the given namespace. + WithNamespace(namespace string) Storage +} diff --git a/theia-shared-cache/Chart.yaml b/theia-shared-cache/Chart.yaml deleted file mode 100644 index 8cc5abe..0000000 --- a/theia-shared-cache/Chart.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: v2 -name: theia-shared-cache -description: A Gradle Shared Cache for Theia IDE deployments in Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.5 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "1.16.0" diff --git a/theia-shared-cache/README.md b/theia-shared-cache/README.md deleted file mode 100644 index ee203d0..0000000 --- a/theia-shared-cache/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# theia-shared-cache Helm chart - -Helm chart to deploy the official Gradle Build Cache Node using the image `gradle/build-cache-node`. - -Features - -- Uses `gradle/build-cache-node` container image (configurable via `values.yaml`). -- Persistent storage for cache data via a PVC (`persistence.enabled`, `persistence.size`, `persistence.storageClass`). -- Service exposing port (default 5071). -- Configurable extra environment variables and resource limits. - -Quickstart - -1. Install the chart with default persistence enabled: - -```bash -helm install my-cache ./ -``` - -2. To override values (example: use 20Gi and a specific storage class): - -```bash -helm install my-cache ./ --set persistence.size=20Gi --set persistence.storageClass=fast -``` - -Port forwarding / usage - -If you deployed as ClusterIP (default), port-forward the service to test locally: - -```bash -# example: port-forward the deployed service (release name `my-cache`) -kubectl port-forward svc/my-cache-theia-shared-cache 5071:5071 -# then configure your Gradle build cache to point at http://127.0.0.1:5071 -``` - -Notes & tips - -- If you need a multi-writer volume (shared between nodes) set `persistence.accessMode=ReadWriteMany` and provide a StorageClass which supports it (e.g. NFS, CephFS). -- The chart provides `extraEnv` for setting environment variables (e.g. authentication, JVM options). Sensitive values should be provided via Kubernetes Secrets and referenced via `volumeMounts` / `volumes` or by customizing templates. -- Adjust `persistence.mountPath` if the upstream image expects a different data directory. - -Further reading - -- Gradle Build Cache Node — Kubernetes deployment recommendations and examples: -- https://docs.gradle.com/develocity/build-cache-node/#kubernetes - -Configuration - -- See `values.yaml` for all configurable options. diff --git a/theia-shared-cache/templates/NOTES.txt b/theia-shared-cache/templates/NOTES.txt deleted file mode 100644 index 65adde1..0000000 --- a/theia-shared-cache/templates/NOTES.txt +++ /dev/null @@ -1,5 +0,0 @@ -1. Port-forward to access the build cache node locally: - - kubectl --namespace {{ .Release.Namespace }} port-forward svc/theia-shared-cache 5071:5071 - -Open http://127.0.0.1:5071 in your local Gradle build cache config to test the service. diff --git a/theia-shared-cache/templates/ingress.yaml b/theia-shared-cache/templates/ingress.yaml deleted file mode 100644 index cdd9b94..0000000 --- a/theia-shared-cache/templates/ingress.yaml +++ /dev/null @@ -1,27 +0,0 @@ -{{- if .Values.ui.enabled }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: theia-shared-cache-ui - labels: - app.kubernetes.io/name: theia-shared-cache - app.kubernetes.io/instance: theia-shared-cache - app.kubernetes.io/component: ui -spec: - ingressClassName: nginx - tls: - - hosts: - - {{ .Values.hosts.configuration.cache }}.{{ .Values.hosts.configuration.baseHost }} - secretName: cache-cert-secret # References the cert created by theia-certificates - rules: - - host: {{ .Values.hosts.configuration.cache }}.{{ .Values.hosts.configuration.baseHost }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: theia-shared-cache - port: - number: 5071 -{{- end }} diff --git a/theia-shared-cache/templates/service.yaml b/theia-shared-cache/templates/service.yaml deleted file mode 100644 index eb1c7cb..0000000 --- a/theia-shared-cache/templates/service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: theia-shared-cache - labels: - app.kubernetes.io/name: theia-shared-cache - app.kubernetes.io/instance: theia-shared-cache -spec: - selector: - app.kubernetes.io/name: theia-shared-cache - app.kubernetes.io/instance: theia-shared-cache - ports: - - name: http - port: 5071 - targetPort: 5071 - - name: grpc-bazel - port: 6011 - targetPort: 6011 diff --git a/theia-shared-cache/templates/statefulset.yaml b/theia-shared-cache/templates/statefulset.yaml deleted file mode 100644 index fdc228c..0000000 --- a/theia-shared-cache/templates/statefulset.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: theia-shared-cache - labels: - app.kubernetes.io/name: theia-shared-cache - app.kubernetes.io/instance: theia-shared-cache -spec: - selector: - matchLabels: - app.kubernetes.io/name: theia-shared-cache - app.kubernetes.io/instance: theia-shared-cache - serviceName: theia-shared-cache - replicas: {{ .Values.replicaCount | default 1 }} - template: - metadata: - labels: - app.kubernetes.io/name: theia-shared-cache - app.kubernetes.io/instance: theia-shared-cache - spec: - containers: - - name: theia-shared-cache - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - args: - - "start" - {{- if .Values.tls.enabled }} - - "--generate-self-signed-cert" - {{- end }} - ports: - - name: {{- if .Values.tls.enabled }} https{{- else }} http{{- end }} - containerPort: 5071 - - name: grpc-bazel - containerPort: 6011 - resources: -{{ toYaml .Values.resources | nindent 12 }} - livenessProbe: - tcpSocket: - port: 5071 - initialDelaySeconds: 60 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - - readinessProbe: - tcpSocket: - port: 5071 - initialDelaySeconds: 30 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 3 - env: - - name: EXCLUSIVE_VOLUME_SIZE - value: "{{ .Values.persistence.size }}" - volumeMounts: - - mountPath: /data - name: theia-shared-cache-data-volume - volumeClaimTemplates: - - metadata: - name: theia-shared-cache-data-volume - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "{{ .Values.persistence.size }}" - storageClassName: "{{ .Values.persistence.storageClass }}" diff --git a/theia-shared-cache/templates/tests/test-connection.yaml b/theia-shared-cache/templates/tests/test-connection.yaml deleted file mode 100644 index ce8eef2..0000000 --- a/theia-shared-cache/templates/tests/test-connection.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "theia-shared-cache-test-connection" - labels: - app.kubernetes.io/name: theia-shared-cache - app.kubernetes.io/instance: theia-shared-cache - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ["wget"] - args: ["http://theia-shared-cache:5071"] - restartPolicy: Never diff --git a/theia-shared-cache/values.yaml b/theia-shared-cache/values.yaml deleted file mode 100644 index 5da7dfe..0000000 --- a/theia-shared-cache/values.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Minimal values for theia-shared-cache chart -replicaCount: 1 - -image: - repository: gradle/build-cache-node - tag: "21.2" - pullPolicy: IfNotPresent - -tls: - enabled: false - -# Persistence: used for StatefulSet volumeClaimTemplates -persistence: - size: 10Gi - storageClass: "standard" - -# Resources applied to the container. Keep empty to use cluster defaults. -resources: - requests: - memory: "512Mi" - cpu: "250m" - limits: - memory: "10Gi" - cpu: "1000m" - -# UI Ingress Configuration -ui: - enabled: false # Set to true to enable the web UI - -hosts: - configuration: - baseHost: "" - cache: ""